You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RebaseCommand.java 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  1. /*
  2. * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.api;
  44. import java.io.BufferedReader;
  45. import java.io.BufferedWriter;
  46. import java.io.ByteArrayOutputStream;
  47. import java.io.File;
  48. import java.io.FileInputStream;
  49. import java.io.FileNotFoundException;
  50. import java.io.FileOutputStream;
  51. import java.io.IOException;
  52. import java.io.InputStreamReader;
  53. import java.io.OutputStreamWriter;
  54. import java.text.MessageFormat;
  55. import java.util.ArrayList;
  56. import java.util.Collection;
  57. import java.util.Collections;
  58. import java.util.HashMap;
  59. import java.util.List;
  60. import java.util.Map;
  61. import org.eclipse.jgit.JGitText;
  62. import org.eclipse.jgit.api.RebaseResult.Status;
  63. import org.eclipse.jgit.api.errors.GitAPIException;
  64. import org.eclipse.jgit.api.errors.JGitInternalException;
  65. import org.eclipse.jgit.api.errors.NoHeadException;
  66. import org.eclipse.jgit.api.errors.RefNotFoundException;
  67. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  68. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  69. import org.eclipse.jgit.diff.DiffFormatter;
  70. import org.eclipse.jgit.dircache.DirCache;
  71. import org.eclipse.jgit.dircache.DirCacheCheckout;
  72. import org.eclipse.jgit.dircache.DirCacheIterator;
  73. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  74. import org.eclipse.jgit.lib.Constants;
  75. import org.eclipse.jgit.lib.NullProgressMonitor;
  76. import org.eclipse.jgit.lib.ObjectId;
  77. import org.eclipse.jgit.lib.ObjectReader;
  78. import org.eclipse.jgit.lib.PersonIdent;
  79. import org.eclipse.jgit.lib.ProgressMonitor;
  80. import org.eclipse.jgit.lib.Ref;
  81. import org.eclipse.jgit.lib.RefUpdate;
  82. import org.eclipse.jgit.lib.Repository;
  83. import org.eclipse.jgit.lib.RefUpdate.Result;
  84. import org.eclipse.jgit.revwalk.RevCommit;
  85. import org.eclipse.jgit.revwalk.RevWalk;
  86. import org.eclipse.jgit.treewalk.TreeWalk;
  87. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  88. import org.eclipse.jgit.util.FileUtils;
  89. import org.eclipse.jgit.util.IO;
  90. import org.eclipse.jgit.util.RawParseUtils;
  91. /**
  92. * A class used to execute a {@code Rebase} command. It has setters for all
  93. * supported options and arguments of this command and a {@link #call()} method
  94. * to finally execute the command. Each instance of this class should only be
  95. * used for one invocation of the command (means: one call to {@link #call()})
  96. * <p>
  97. *
  98. * @see <a
  99. * href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html"
  100. * >Git documentation about Rebase</a>
  101. */
  102. public class RebaseCommand extends GitCommand<RebaseResult> {
  103. /**
  104. * The name of the "rebase-merge" folder
  105. */
  106. public static final String REBASE_MERGE = "rebase-merge";
  107. /**
  108. * The name of the "stopped-sha" file
  109. */
  110. public static final String STOPPED_SHA = "stopped-sha";
  111. private static final String AUTHOR_SCRIPT = "author-script";
  112. private static final String DONE = "done";
  113. private static final String GIT_AUTHOR_DATE = "GIT_AUTHOR_DATE";
  114. private static final String GIT_AUTHOR_EMAIL = "GIT_AUTHOR_EMAIL";
  115. private static final String GIT_AUTHOR_NAME = "GIT_AUTHOR_NAME";
  116. private static final String GIT_REBASE_TODO = "git-rebase-todo";
  117. private static final String HEAD_NAME = "head-name";
  118. private static final String INTERACTIVE = "interactive";
  119. private static final String MESSAGE = "message";
  120. private static final String ONTO = "onto";
  121. private static final String PATCH = "patch";
  122. private static final String REBASE_HEAD = "head";
  123. /**
  124. * The available operations
  125. */
  126. public enum Operation {
  127. /**
  128. * Initiates rebase
  129. */
  130. BEGIN,
  131. /**
  132. * Continues after a conflict resolution
  133. */
  134. CONTINUE,
  135. /**
  136. * Skips the "current" commit
  137. */
  138. SKIP,
  139. /**
  140. * Aborts and resets the current rebase
  141. */
  142. ABORT;
  143. }
  144. private Operation operation = Operation.BEGIN;
  145. private RevCommit upstreamCommit;
  146. private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
  147. private final RevWalk walk;
  148. private final File rebaseDir;
  149. /**
  150. * @param repo
  151. */
  152. protected RebaseCommand(Repository repo) {
  153. super(repo);
  154. walk = new RevWalk(repo);
  155. rebaseDir = new File(repo.getDirectory(), REBASE_MERGE);
  156. }
  157. /**
  158. * Executes the {@code Rebase} command with all the options and parameters
  159. * collected by the setter methods of this class. Each instance of this
  160. * class should only be used for one invocation of the command. Don't call
  161. * this method twice on an instance.
  162. *
  163. * @return an object describing the result of this command
  164. */
  165. public RebaseResult call() throws NoHeadException, RefNotFoundException,
  166. JGitInternalException, GitAPIException {
  167. RevCommit newHead = null;
  168. checkCallable();
  169. checkParameters();
  170. try {
  171. switch (operation) {
  172. case ABORT:
  173. try {
  174. return abort();
  175. } catch (IOException ioe) {
  176. throw new JGitInternalException(ioe.getMessage(), ioe);
  177. }
  178. case SKIP:
  179. // fall through
  180. case CONTINUE:
  181. String upstreamCommitName = readFile(rebaseDir, ONTO);
  182. this.upstreamCommit = walk.parseCommit(repo
  183. .resolve(upstreamCommitName));
  184. break;
  185. case BEGIN:
  186. RebaseResult res = initFilesAndRewind();
  187. if (res != null)
  188. return res;
  189. }
  190. if (monitor.isCancelled())
  191. return abort();
  192. if (this.operation == Operation.CONTINUE)
  193. newHead = continueRebase();
  194. List<Step> steps = loadSteps();
  195. if (this.operation == Operation.SKIP && !steps.isEmpty())
  196. checkoutCurrentHead();
  197. ObjectReader or = repo.newObjectReader();
  198. int stepsToPop = 0;
  199. for (Step step : steps) {
  200. if (step.action != Action.PICK)
  201. continue;
  202. popSteps(1);
  203. Collection<ObjectId> ids = or.resolve(step.commit);
  204. if (ids.size() != 1)
  205. throw new JGitInternalException(
  206. "Could not resolve uniquely the abbreviated object ID");
  207. RevCommit commitToPick = walk
  208. .parseCommit(ids.iterator().next());
  209. if (monitor.isCancelled())
  210. return new RebaseResult(commitToPick);
  211. monitor.beginTask(MessageFormat.format(
  212. JGitText.get().applyingCommit, commitToPick
  213. .getShortMessage()), ProgressMonitor.UNKNOWN);
  214. // TODO if the first parent of commitToPick is the current HEAD,
  215. // we should fast-forward instead of cherry-pick to avoid
  216. // unnecessary object rewriting
  217. newHead = new Git(repo).cherryPick().include(commitToPick)
  218. .call();
  219. monitor.endTask();
  220. if (newHead == null) {
  221. return stop(commitToPick);
  222. }
  223. stepsToPop++;
  224. }
  225. if (newHead != null || steps.isEmpty()) {
  226. // point the previous head (if any) to the new commit
  227. String headName = readFile(rebaseDir, HEAD_NAME);
  228. if (headName.startsWith(Constants.R_REFS)) {
  229. RefUpdate rup = repo.updateRef(headName);
  230. if (newHead != null) {
  231. rup.setNewObjectId(newHead);
  232. rup.forceUpdate();
  233. }
  234. rup = repo.updateRef(Constants.HEAD);
  235. rup.link(headName);
  236. }
  237. if (this.operation == Operation.SKIP && steps.isEmpty()) {
  238. checkoutCurrentHead();
  239. }
  240. FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
  241. return new RebaseResult(Status.OK);
  242. }
  243. return new RebaseResult(Status.UP_TO_DATE);
  244. } catch (IOException ioe) {
  245. throw new JGitInternalException(ioe.getMessage(), ioe);
  246. }
  247. }
  248. private void checkoutCurrentHead() throws IOException, NoHeadException,
  249. JGitInternalException {
  250. ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}");
  251. if (headTree == null)
  252. throw new NoHeadException(
  253. JGitText.get().cannotRebaseWithoutCurrentHead);
  254. DirCache dc = repo.lockDirCache();
  255. try {
  256. DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree);
  257. dco.setFailOnConflict(false);
  258. boolean needsDeleteFiles = dco.checkout();
  259. if (needsDeleteFiles) {
  260. List<String> fileList = dco.getToBeDeleted();
  261. for (String filePath : fileList) {
  262. File fileToDelete = new File(repo.getWorkTree(), filePath);
  263. if (fileToDelete.exists())
  264. FileUtils.delete(fileToDelete, FileUtils.RECURSIVE
  265. | FileUtils.RETRY);
  266. }
  267. }
  268. } finally {
  269. dc.unlock();
  270. }
  271. }
  272. /**
  273. * @return the commit if we had to do a commit, otherwise null
  274. * @throws GitAPIException
  275. * @throws IOException
  276. */
  277. private RevCommit continueRebase() throws GitAPIException, IOException {
  278. // if there are still conflicts, we throw a specific Exception
  279. DirCache dc = repo.readDirCache();
  280. boolean hasUnmergedPaths = dc.hasUnmergedPaths();
  281. if (hasUnmergedPaths)
  282. throw new UnmergedPathsException();
  283. // determine whether we need to commit
  284. TreeWalk treeWalk = new TreeWalk(repo);
  285. treeWalk.reset();
  286. treeWalk.setRecursive(true);
  287. treeWalk.addTree(new DirCacheIterator(dc));
  288. ObjectId id = repo.resolve(Constants.HEAD + "^{tree}");
  289. if (id == null)
  290. throw new NoHeadException(
  291. JGitText.get().cannotRebaseWithoutCurrentHead);
  292. treeWalk.addTree(id);
  293. treeWalk.setFilter(TreeFilter.ANY_DIFF);
  294. boolean needsCommit = treeWalk.next();
  295. treeWalk.release();
  296. if (needsCommit) {
  297. CommitCommand commit = new Git(repo).commit();
  298. commit.setMessage(readFile(rebaseDir, MESSAGE));
  299. commit.setAuthor(parseAuthor());
  300. return commit.call();
  301. }
  302. return null;
  303. }
  304. private PersonIdent parseAuthor() throws IOException {
  305. File authorScriptFile = new File(rebaseDir, AUTHOR_SCRIPT);
  306. byte[] raw;
  307. try {
  308. raw = IO.readFully(authorScriptFile);
  309. } catch (FileNotFoundException notFound) {
  310. return null;
  311. }
  312. return parseAuthor(raw);
  313. }
  314. private RebaseResult stop(RevCommit commitToPick) throws IOException {
  315. PersonIdent author = commitToPick.getAuthorIdent();
  316. String authorScript = toAuthorScript(author);
  317. createFile(rebaseDir, AUTHOR_SCRIPT, authorScript);
  318. createFile(rebaseDir, MESSAGE, commitToPick.getFullMessage());
  319. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  320. DiffFormatter df = new DiffFormatter(bos);
  321. df.setRepository(repo);
  322. df.format(commitToPick.getParent(0), commitToPick);
  323. createFile(rebaseDir, PATCH, new String(bos.toByteArray(),
  324. Constants.CHARACTER_ENCODING));
  325. createFile(rebaseDir, STOPPED_SHA, repo.newObjectReader().abbreviate(
  326. commitToPick).name());
  327. return new RebaseResult(commitToPick);
  328. }
  329. String toAuthorScript(PersonIdent author) {
  330. StringBuilder sb = new StringBuilder(100);
  331. sb.append(GIT_AUTHOR_NAME);
  332. sb.append("='");
  333. sb.append(author.getName());
  334. sb.append("'\n");
  335. sb.append(GIT_AUTHOR_EMAIL);
  336. sb.append("='");
  337. sb.append(author.getEmailAddress());
  338. sb.append("'\n");
  339. // the command line uses the "external String"
  340. // representation for date and timezone
  341. sb.append(GIT_AUTHOR_DATE);
  342. sb.append("='");
  343. String externalString = author.toExternalString();
  344. sb
  345. .append(externalString.substring(externalString
  346. .lastIndexOf('>') + 2));
  347. sb.append("'\n");
  348. return sb.toString();
  349. }
  350. /**
  351. * Removes the number of lines given in the parameter from the
  352. * <code>git-rebase-todo</code> file but preserves comments and other lines
  353. * that can not be parsed as steps
  354. *
  355. * @param numSteps
  356. * @throws IOException
  357. */
  358. private void popSteps(int numSteps) throws IOException {
  359. if (numSteps == 0)
  360. return;
  361. List<String> todoLines = new ArrayList<String>();
  362. List<String> poppedLines = new ArrayList<String>();
  363. File todoFile = new File(rebaseDir, GIT_REBASE_TODO);
  364. File doneFile = new File(rebaseDir, DONE);
  365. BufferedReader br = new BufferedReader(new InputStreamReader(
  366. new FileInputStream(todoFile), Constants.CHARACTER_ENCODING));
  367. try {
  368. // check if the line starts with a action tag (pick, skip...)
  369. while (poppedLines.size() < numSteps) {
  370. String popCandidate = br.readLine();
  371. if (popCandidate == null)
  372. break;
  373. int spaceIndex = popCandidate.indexOf(' ');
  374. boolean pop = false;
  375. if (spaceIndex >= 0) {
  376. String actionToken = popCandidate.substring(0, spaceIndex);
  377. pop = Action.parse(actionToken) != null;
  378. }
  379. if (pop)
  380. poppedLines.add(popCandidate);
  381. else
  382. todoLines.add(popCandidate);
  383. }
  384. String readLine = br.readLine();
  385. while (readLine != null) {
  386. todoLines.add(readLine);
  387. readLine = br.readLine();
  388. }
  389. } finally {
  390. br.close();
  391. }
  392. BufferedWriter todoWriter = new BufferedWriter(new OutputStreamWriter(
  393. new FileOutputStream(todoFile), Constants.CHARACTER_ENCODING));
  394. try {
  395. for (String writeLine : todoLines) {
  396. todoWriter.write(writeLine);
  397. todoWriter.newLine();
  398. }
  399. } finally {
  400. todoWriter.close();
  401. }
  402. if (poppedLines.size() > 0) {
  403. // append here
  404. BufferedWriter doneWriter = new BufferedWriter(
  405. new OutputStreamWriter(
  406. new FileOutputStream(doneFile, true),
  407. Constants.CHARACTER_ENCODING));
  408. try {
  409. for (String writeLine : poppedLines) {
  410. doneWriter.write(writeLine);
  411. doneWriter.newLine();
  412. }
  413. } finally {
  414. doneWriter.close();
  415. }
  416. }
  417. }
  418. private RebaseResult initFilesAndRewind() throws RefNotFoundException,
  419. IOException, NoHeadException, JGitInternalException {
  420. // we need to store everything into files so that we can implement
  421. // --skip, --continue, and --abort
  422. // first of all, we determine the commits to be applied
  423. List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
  424. Ref head = repo.getRef(Constants.HEAD);
  425. if (head == null || head.getObjectId() == null)
  426. throw new RefNotFoundException(MessageFormat.format(
  427. JGitText.get().refNotResolved, Constants.HEAD));
  428. String headName;
  429. if (head.isSymbolic())
  430. headName = head.getTarget().getName();
  431. else
  432. headName = "detached HEAD";
  433. ObjectId headId = head.getObjectId();
  434. if (headId == null)
  435. throw new RefNotFoundException(MessageFormat.format(
  436. JGitText.get().refNotResolved, Constants.HEAD));
  437. RevCommit headCommit = walk.lookupCommit(headId);
  438. monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
  439. ProgressMonitor.UNKNOWN);
  440. LogCommand cmd = new Git(repo).log().addRange(upstreamCommit,
  441. headCommit);
  442. Iterable<RevCommit> commitsToUse = cmd.call();
  443. for (RevCommit commit : commitsToUse) {
  444. cherryPickList.add(commit);
  445. }
  446. // nothing to do: return with UP_TO_DATE_RESULT
  447. if (cherryPickList.isEmpty())
  448. return RebaseResult.UP_TO_DATE_RESULT;
  449. Collections.reverse(cherryPickList);
  450. // create the folder for the meta information
  451. rebaseDir.mkdir();
  452. createFile(repo.getDirectory(), Constants.ORIG_HEAD, headId.name());
  453. createFile(rebaseDir, REBASE_HEAD, headId.name());
  454. createFile(rebaseDir, HEAD_NAME, headName);
  455. createFile(rebaseDir, ONTO, upstreamCommit.name());
  456. createFile(rebaseDir, INTERACTIVE, "");
  457. BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
  458. new FileOutputStream(new File(rebaseDir, GIT_REBASE_TODO)),
  459. Constants.CHARACTER_ENCODING));
  460. fw.write("# Created by EGit: rebasing " + upstreamCommit.name()
  461. + " onto " + headId.name());
  462. fw.newLine();
  463. try {
  464. StringBuilder sb = new StringBuilder();
  465. ObjectReader reader = walk.getObjectReader();
  466. for (RevCommit commit : cherryPickList) {
  467. sb.setLength(0);
  468. sb.append(Action.PICK.toToken());
  469. sb.append(" ");
  470. sb.append(reader.abbreviate(commit).name());
  471. sb.append(" ");
  472. sb.append(commit.getShortMessage());
  473. fw.write(sb.toString());
  474. fw.newLine();
  475. }
  476. } finally {
  477. fw.close();
  478. }
  479. monitor.endTask();
  480. // we rewind to the upstream commit
  481. monitor.beginTask(MessageFormat.format(JGitText.get().rewinding,
  482. upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
  483. checkoutCommit(upstreamCommit);
  484. monitor.endTask();
  485. return null;
  486. }
  487. private void checkParameters() throws WrongRepositoryStateException {
  488. if (this.operation != Operation.BEGIN) {
  489. // these operations are only possible while in a rebasing state
  490. switch (repo.getRepositoryState()) {
  491. case REBASING_INTERACTIVE:
  492. break;
  493. default:
  494. throw new WrongRepositoryStateException(MessageFormat.format(
  495. JGitText.get().wrongRepositoryState, repo
  496. .getRepositoryState().name()));
  497. }
  498. } else
  499. switch (repo.getRepositoryState()) {
  500. case SAFE:
  501. if (this.upstreamCommit == null)
  502. throw new JGitInternalException(MessageFormat
  503. .format(JGitText.get().missingRequiredParameter,
  504. "upstream"));
  505. return;
  506. default:
  507. throw new WrongRepositoryStateException(MessageFormat.format(
  508. JGitText.get().wrongRepositoryState, repo
  509. .getRepositoryState().name()));
  510. }
  511. }
  512. private void createFile(File parentDir, String name, String content)
  513. throws IOException {
  514. File file = new File(parentDir, name);
  515. FileOutputStream fos = new FileOutputStream(file);
  516. try {
  517. fos.write(content.getBytes(Constants.CHARACTER_ENCODING));
  518. fos.write('\n');
  519. } finally {
  520. fos.close();
  521. }
  522. }
  523. private RebaseResult abort() throws IOException {
  524. try {
  525. String commitId = readFile(repo.getDirectory(), Constants.ORIG_HEAD);
  526. monitor.beginTask(MessageFormat.format(
  527. JGitText.get().abortingRebase, commitId),
  528. ProgressMonitor.UNKNOWN);
  529. RevCommit commit = walk.parseCommit(repo.resolve(commitId));
  530. // no head in order to reset --hard
  531. DirCacheCheckout dco = new DirCacheCheckout(repo, repo
  532. .lockDirCache(), commit.getTree());
  533. dco.setFailOnConflict(false);
  534. dco.checkout();
  535. walk.release();
  536. } finally {
  537. monitor.endTask();
  538. }
  539. try {
  540. String headName = readFile(rebaseDir, HEAD_NAME);
  541. if (headName.startsWith(Constants.R_REFS)) {
  542. monitor.beginTask(MessageFormat.format(
  543. JGitText.get().resettingHead, headName),
  544. ProgressMonitor.UNKNOWN);
  545. // update the HEAD
  546. RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false);
  547. Result res = refUpdate.link(headName);
  548. switch (res) {
  549. case FAST_FORWARD:
  550. case FORCED:
  551. case NO_CHANGE:
  552. break;
  553. default:
  554. throw new JGitInternalException(
  555. JGitText.get().abortingRebaseFailed);
  556. }
  557. }
  558. // cleanup the files
  559. FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
  560. return new RebaseResult(Status.ABORTED);
  561. } finally {
  562. monitor.endTask();
  563. }
  564. }
  565. private String readFile(File directory, String fileName) throws IOException {
  566. byte[] content = IO.readFully(new File(directory, fileName));
  567. // strip off the last LF
  568. int end = content.length;
  569. while (0 < end && content[end - 1] == '\n')
  570. end--;
  571. return RawParseUtils.decode(content, 0, end);
  572. }
  573. private void checkoutCommit(RevCommit commit) throws IOException {
  574. try {
  575. RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
  576. DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
  577. repo.lockDirCache(), commit.getTree());
  578. dco.setFailOnConflict(true);
  579. dco.checkout();
  580. // update the HEAD
  581. RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
  582. refUpdate.setExpectedOldObjectId(head);
  583. refUpdate.setNewObjectId(commit);
  584. Result res = refUpdate.forceUpdate();
  585. switch (res) {
  586. case FAST_FORWARD:
  587. case NO_CHANGE:
  588. case FORCED:
  589. break;
  590. default:
  591. throw new IOException("Could not rewind to upstream commit");
  592. }
  593. } finally {
  594. walk.release();
  595. monitor.endTask();
  596. }
  597. }
  598. private List<Step> loadSteps() throws IOException {
  599. byte[] buf = IO.readFully(new File(rebaseDir, GIT_REBASE_TODO));
  600. int ptr = 0;
  601. int tokenBegin = 0;
  602. ArrayList<Step> r = new ArrayList<Step>();
  603. while (ptr < buf.length) {
  604. tokenBegin = ptr;
  605. ptr = RawParseUtils.nextLF(buf, ptr);
  606. int nextSpace = 0;
  607. int tokenCount = 0;
  608. Step current = null;
  609. while (tokenCount < 3 && nextSpace < ptr) {
  610. switch (tokenCount) {
  611. case 0:
  612. nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  613. String actionToken = new String(buf, tokenBegin, nextSpace
  614. - tokenBegin - 1);
  615. tokenBegin = nextSpace;
  616. Action action = Action.parse(actionToken);
  617. if (action != null)
  618. current = new Step(Action.parse(actionToken));
  619. break;
  620. case 1:
  621. if (current == null)
  622. break;
  623. nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  624. String commitToken = new String(buf, tokenBegin, nextSpace
  625. - tokenBegin - 1);
  626. tokenBegin = nextSpace;
  627. current.commit = AbbreviatedObjectId
  628. .fromString(commitToken);
  629. break;
  630. case 2:
  631. if (current == null)
  632. break;
  633. nextSpace = ptr;
  634. int length = ptr - tokenBegin;
  635. current.shortMessage = new byte[length];
  636. System.arraycopy(buf, tokenBegin, current.shortMessage, 0,
  637. length);
  638. r.add(current);
  639. break;
  640. }
  641. tokenCount++;
  642. }
  643. }
  644. return r;
  645. }
  646. /**
  647. * @param upstream
  648. * the upstream commit
  649. * @return {@code this}
  650. */
  651. public RebaseCommand setUpstream(RevCommit upstream) {
  652. this.upstreamCommit = upstream;
  653. return this;
  654. }
  655. /**
  656. * @param upstream
  657. * the upstream branch
  658. * @return {@code this}
  659. * @throws RefNotFoundException
  660. */
  661. public RebaseCommand setUpstream(String upstream)
  662. throws RefNotFoundException {
  663. try {
  664. ObjectId upstreamId = repo.resolve(upstream);
  665. if (upstreamId == null)
  666. throw new RefNotFoundException(MessageFormat.format(JGitText
  667. .get().refNotResolved, upstream));
  668. upstreamCommit = walk.parseCommit(repo.resolve(upstream));
  669. return this;
  670. } catch (IOException ioe) {
  671. throw new JGitInternalException(ioe.getMessage(), ioe);
  672. }
  673. }
  674. /**
  675. * @param operation
  676. * the operation to perform
  677. * @return {@code this}
  678. */
  679. public RebaseCommand setOperation(Operation operation) {
  680. this.operation = operation;
  681. return this;
  682. }
  683. /**
  684. * @param monitor
  685. * a progress monitor
  686. * @return this instance
  687. */
  688. public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
  689. this.monitor = monitor;
  690. return this;
  691. }
  692. static enum Action {
  693. PICK("pick"); // later add SQUASH, EDIT, etc.
  694. private final String token;
  695. private Action(String token) {
  696. this.token = token;
  697. }
  698. public String toToken() {
  699. return this.token;
  700. }
  701. static Action parse(String token) {
  702. if (token.equals("pick") || token.equals("p"))
  703. return PICK;
  704. return null;
  705. }
  706. }
  707. static class Step {
  708. Action action;
  709. AbbreviatedObjectId commit;
  710. byte[] shortMessage;
  711. Step(Action action) {
  712. this.action = action;
  713. }
  714. }
  715. PersonIdent parseAuthor(byte[] raw) {
  716. if (raw.length == 0)
  717. return null;
  718. Map<String, String> keyValueMap = new HashMap<String, String>();
  719. for (int p = 0; p < raw.length;) {
  720. int end = RawParseUtils.nextLF(raw, p);
  721. if (end == p)
  722. break;
  723. int equalsIndex = RawParseUtils.next(raw, p, '=');
  724. if (equalsIndex == end)
  725. break;
  726. String key = RawParseUtils.decode(raw, p, equalsIndex - 1);
  727. String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2);
  728. p = end;
  729. keyValueMap.put(key, value);
  730. }
  731. String name = keyValueMap.get(GIT_AUTHOR_NAME);
  732. String email = keyValueMap.get(GIT_AUTHOR_EMAIL);
  733. String time = keyValueMap.get(GIT_AUTHOR_DATE);
  734. // the time is saved as <seconds since 1970> <timezone offset>
  735. long when = Long.parseLong(time.substring(0, time.indexOf(' '))) * 1000;
  736. String tzOffsetString = time.substring(time.indexOf(' ') + 1);
  737. int multiplier = -1;
  738. if (tzOffsetString.charAt(0) == '+')
  739. multiplier = 1;
  740. int hours = Integer.parseInt(tzOffsetString.substring(1, 3));
  741. int minutes = Integer.parseInt(tzOffsetString.substring(3, 5));
  742. // this is in format (+/-)HHMM (hours and minutes)
  743. // we need to convert into minutes
  744. int tz = (hours * 60 + minutes) * multiplier;
  745. if (name != null && email != null)
  746. return new PersonIdent(name, email, when, tz);
  747. return null;
  748. }
  749. }