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 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  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.InvalidRefNameException;
  65. import org.eclipse.jgit.api.errors.JGitInternalException;
  66. import org.eclipse.jgit.api.errors.NoHeadException;
  67. import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
  68. import org.eclipse.jgit.api.errors.RefNotFoundException;
  69. import org.eclipse.jgit.api.errors.UnmergedPathsException;
  70. import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
  71. import org.eclipse.jgit.diff.DiffFormatter;
  72. import org.eclipse.jgit.dircache.DirCache;
  73. import org.eclipse.jgit.dircache.DirCacheCheckout;
  74. import org.eclipse.jgit.dircache.DirCacheIterator;
  75. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  76. import org.eclipse.jgit.lib.AnyObjectId;
  77. import org.eclipse.jgit.lib.Constants;
  78. import org.eclipse.jgit.lib.NullProgressMonitor;
  79. import org.eclipse.jgit.lib.ObjectId;
  80. import org.eclipse.jgit.lib.ObjectReader;
  81. import org.eclipse.jgit.lib.PersonIdent;
  82. import org.eclipse.jgit.lib.ProgressMonitor;
  83. import org.eclipse.jgit.lib.Ref;
  84. import org.eclipse.jgit.lib.RefUpdate;
  85. import org.eclipse.jgit.lib.Repository;
  86. import org.eclipse.jgit.lib.RefUpdate.Result;
  87. import org.eclipse.jgit.revwalk.RevCommit;
  88. import org.eclipse.jgit.revwalk.RevWalk;
  89. import org.eclipse.jgit.treewalk.TreeWalk;
  90. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  91. import org.eclipse.jgit.util.FileUtils;
  92. import org.eclipse.jgit.util.IO;
  93. import org.eclipse.jgit.util.RawParseUtils;
  94. /**
  95. * A class used to execute a {@code Rebase} command. It has setters for all
  96. * supported options and arguments of this command and a {@link #call()} method
  97. * to finally execute the command. Each instance of this class should only be
  98. * used for one invocation of the command (means: one call to {@link #call()})
  99. * <p>
  100. *
  101. * @see <a
  102. * href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html"
  103. * >Git documentation about Rebase</a>
  104. */
  105. public class RebaseCommand extends GitCommand<RebaseResult> {
  106. /**
  107. * The name of the "rebase-merge" folder
  108. */
  109. public static final String REBASE_MERGE = "rebase-merge";
  110. /**
  111. * The name of the "stopped-sha" file
  112. */
  113. public static final String STOPPED_SHA = "stopped-sha";
  114. private static final String AUTHOR_SCRIPT = "author-script";
  115. private static final String DONE = "done";
  116. private static final String GIT_AUTHOR_DATE = "GIT_AUTHOR_DATE";
  117. private static final String GIT_AUTHOR_EMAIL = "GIT_AUTHOR_EMAIL";
  118. private static final String GIT_AUTHOR_NAME = "GIT_AUTHOR_NAME";
  119. private static final String GIT_REBASE_TODO = "git-rebase-todo";
  120. private static final String HEAD_NAME = "head-name";
  121. private static final String INTERACTIVE = "interactive";
  122. private static final String MESSAGE = "message";
  123. private static final String ONTO = "onto";
  124. private static final String PATCH = "patch";
  125. private static final String REBASE_HEAD = "head";
  126. /**
  127. * The available operations
  128. */
  129. public enum Operation {
  130. /**
  131. * Initiates rebase
  132. */
  133. BEGIN,
  134. /**
  135. * Continues after a conflict resolution
  136. */
  137. CONTINUE,
  138. /**
  139. * Skips the "current" commit
  140. */
  141. SKIP,
  142. /**
  143. * Aborts and resets the current rebase
  144. */
  145. ABORT;
  146. }
  147. private Operation operation = Operation.BEGIN;
  148. private RevCommit upstreamCommit;
  149. private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
  150. private final RevWalk walk;
  151. private final File rebaseDir;
  152. /**
  153. * @param repo
  154. */
  155. protected RebaseCommand(Repository repo) {
  156. super(repo);
  157. walk = new RevWalk(repo);
  158. rebaseDir = new File(repo.getDirectory(), REBASE_MERGE);
  159. }
  160. /**
  161. * Executes the {@code Rebase} command with all the options and parameters
  162. * collected by the setter methods of this class. Each instance of this
  163. * class should only be used for one invocation of the command. Don't call
  164. * this method twice on an instance.
  165. *
  166. * @return an object describing the result of this command
  167. */
  168. public RebaseResult call() throws NoHeadException, RefNotFoundException,
  169. JGitInternalException, GitAPIException {
  170. RevCommit newHead = null;
  171. boolean lastStepWasForward = false;
  172. checkCallable();
  173. checkParameters();
  174. try {
  175. switch (operation) {
  176. case ABORT:
  177. try {
  178. return abort();
  179. } catch (IOException ioe) {
  180. throw new JGitInternalException(ioe.getMessage(), ioe);
  181. }
  182. case SKIP:
  183. // fall through
  184. case CONTINUE:
  185. String upstreamCommitName = readFile(rebaseDir, ONTO);
  186. this.upstreamCommit = walk.parseCommit(repo
  187. .resolve(upstreamCommitName));
  188. break;
  189. case BEGIN:
  190. RebaseResult res = initFilesAndRewind();
  191. if (res != null)
  192. return res;
  193. }
  194. if (monitor.isCancelled())
  195. return abort();
  196. if (this.operation == Operation.CONTINUE)
  197. newHead = continueRebase();
  198. if (this.operation == Operation.SKIP)
  199. newHead = checkoutCurrentHead();
  200. ObjectReader or = repo.newObjectReader();
  201. List<Step> steps = loadSteps();
  202. for (Step step : steps) {
  203. popSteps(1);
  204. Collection<ObjectId> ids = or.resolve(step.commit);
  205. if (ids.size() != 1)
  206. throw new JGitInternalException(
  207. "Could not resolve uniquely the abbreviated object ID");
  208. RevCommit commitToPick = walk
  209. .parseCommit(ids.iterator().next());
  210. if (monitor.isCancelled())
  211. return new RebaseResult(commitToPick);
  212. monitor.beginTask(MessageFormat.format(
  213. JGitText.get().applyingCommit, commitToPick
  214. .getShortMessage()), ProgressMonitor.UNKNOWN);
  215. // if the first parent of commitToPick is the current HEAD,
  216. // we do a fast-forward instead of cherry-pick to avoid
  217. // unnecessary object rewriting
  218. newHead = tryFastForward(commitToPick);
  219. lastStepWasForward = newHead != null;
  220. if (!lastStepWasForward)
  221. // TODO if the content of this commit is already merged here
  222. // we should skip this step in order to avoid confusing
  223. // pseudo-changed
  224. newHead = new Git(repo).cherryPick().include(commitToPick)
  225. .call();
  226. monitor.endTask();
  227. if (newHead == null) {
  228. return stop(commitToPick);
  229. }
  230. }
  231. if (newHead != null) {
  232. // point the previous head (if any) to the new commit
  233. String headName = readFile(rebaseDir, HEAD_NAME);
  234. if (headName.startsWith(Constants.R_REFS)) {
  235. RefUpdate rup = repo.updateRef(headName);
  236. rup.setNewObjectId(newHead);
  237. Result res = rup.forceUpdate();
  238. switch (res) {
  239. case FAST_FORWARD:
  240. case FORCED:
  241. case NO_CHANGE:
  242. break;
  243. default:
  244. throw new JGitInternalException("Updating HEAD failed");
  245. }
  246. rup = repo.updateRef(Constants.HEAD);
  247. res = rup.link(headName);
  248. switch (res) {
  249. case FAST_FORWARD:
  250. case FORCED:
  251. case NO_CHANGE:
  252. break;
  253. default:
  254. throw new JGitInternalException("Updating HEAD failed");
  255. }
  256. }
  257. FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
  258. if (lastStepWasForward)
  259. return new RebaseResult(Status.FAST_FORWARD);
  260. return new RebaseResult(Status.OK);
  261. }
  262. return new RebaseResult(Status.UP_TO_DATE);
  263. } catch (IOException ioe) {
  264. throw new JGitInternalException(ioe.getMessage(), ioe);
  265. }
  266. }
  267. private RevCommit checkoutCurrentHead() throws IOException,
  268. NoHeadException, JGitInternalException {
  269. ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}");
  270. if (headTree == null)
  271. throw new NoHeadException(
  272. JGitText.get().cannotRebaseWithoutCurrentHead);
  273. DirCache dc = repo.lockDirCache();
  274. try {
  275. DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree);
  276. dco.setFailOnConflict(false);
  277. boolean needsDeleteFiles = dco.checkout();
  278. if (needsDeleteFiles) {
  279. List<String> fileList = dco.getToBeDeleted();
  280. for (String filePath : fileList) {
  281. File fileToDelete = new File(repo.getWorkTree(), filePath);
  282. if (fileToDelete.exists())
  283. FileUtils.delete(fileToDelete, FileUtils.RECURSIVE
  284. | FileUtils.RETRY);
  285. }
  286. }
  287. } finally {
  288. dc.unlock();
  289. }
  290. RevWalk rw = new RevWalk(repo);
  291. RevCommit commit = rw.parseCommit(repo.resolve(Constants.HEAD));
  292. rw.release();
  293. return commit;
  294. }
  295. /**
  296. * @return the commit if we had to do a commit, otherwise null
  297. * @throws GitAPIException
  298. * @throws IOException
  299. */
  300. private RevCommit continueRebase() throws GitAPIException, IOException {
  301. // if there are still conflicts, we throw a specific Exception
  302. DirCache dc = repo.readDirCache();
  303. boolean hasUnmergedPaths = dc.hasUnmergedPaths();
  304. if (hasUnmergedPaths)
  305. throw new UnmergedPathsException();
  306. // determine whether we need to commit
  307. TreeWalk treeWalk = new TreeWalk(repo);
  308. treeWalk.reset();
  309. treeWalk.setRecursive(true);
  310. treeWalk.addTree(new DirCacheIterator(dc));
  311. ObjectId id = repo.resolve(Constants.HEAD + "^{tree}");
  312. if (id == null)
  313. throw new NoHeadException(
  314. JGitText.get().cannotRebaseWithoutCurrentHead);
  315. treeWalk.addTree(id);
  316. treeWalk.setFilter(TreeFilter.ANY_DIFF);
  317. boolean needsCommit = treeWalk.next();
  318. treeWalk.release();
  319. if (needsCommit) {
  320. CommitCommand commit = new Git(repo).commit();
  321. commit.setMessage(readFile(rebaseDir, MESSAGE));
  322. commit.setAuthor(parseAuthor());
  323. return commit.call();
  324. }
  325. return null;
  326. }
  327. private PersonIdent parseAuthor() throws IOException {
  328. File authorScriptFile = new File(rebaseDir, AUTHOR_SCRIPT);
  329. byte[] raw;
  330. try {
  331. raw = IO.readFully(authorScriptFile);
  332. } catch (FileNotFoundException notFound) {
  333. return null;
  334. }
  335. return parseAuthor(raw);
  336. }
  337. private RebaseResult stop(RevCommit commitToPick) throws IOException {
  338. PersonIdent author = commitToPick.getAuthorIdent();
  339. String authorScript = toAuthorScript(author);
  340. createFile(rebaseDir, AUTHOR_SCRIPT, authorScript);
  341. createFile(rebaseDir, MESSAGE, commitToPick.getFullMessage());
  342. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  343. DiffFormatter df = new DiffFormatter(bos);
  344. df.setRepository(repo);
  345. df.format(commitToPick.getParent(0), commitToPick);
  346. createFile(rebaseDir, PATCH, new String(bos.toByteArray(),
  347. Constants.CHARACTER_ENCODING));
  348. createFile(rebaseDir, STOPPED_SHA, repo.newObjectReader().abbreviate(
  349. commitToPick).name());
  350. return new RebaseResult(commitToPick);
  351. }
  352. String toAuthorScript(PersonIdent author) {
  353. StringBuilder sb = new StringBuilder(100);
  354. sb.append(GIT_AUTHOR_NAME);
  355. sb.append("='");
  356. sb.append(author.getName());
  357. sb.append("'\n");
  358. sb.append(GIT_AUTHOR_EMAIL);
  359. sb.append("='");
  360. sb.append(author.getEmailAddress());
  361. sb.append("'\n");
  362. // the command line uses the "external String"
  363. // representation for date and timezone
  364. sb.append(GIT_AUTHOR_DATE);
  365. sb.append("='");
  366. String externalString = author.toExternalString();
  367. sb
  368. .append(externalString.substring(externalString
  369. .lastIndexOf('>') + 2));
  370. sb.append("'\n");
  371. return sb.toString();
  372. }
  373. /**
  374. * Removes the number of lines given in the parameter from the
  375. * <code>git-rebase-todo</code> file but preserves comments and other lines
  376. * that can not be parsed as steps
  377. *
  378. * @param numSteps
  379. * @throws IOException
  380. */
  381. private void popSteps(int numSteps) throws IOException {
  382. if (numSteps == 0)
  383. return;
  384. List<String> todoLines = new ArrayList<String>();
  385. List<String> poppedLines = new ArrayList<String>();
  386. File todoFile = new File(rebaseDir, GIT_REBASE_TODO);
  387. File doneFile = new File(rebaseDir, DONE);
  388. BufferedReader br = new BufferedReader(new InputStreamReader(
  389. new FileInputStream(todoFile), Constants.CHARACTER_ENCODING));
  390. try {
  391. // check if the line starts with a action tag (pick, skip...)
  392. while (poppedLines.size() < numSteps) {
  393. String popCandidate = br.readLine();
  394. if (popCandidate == null)
  395. break;
  396. if (popCandidate.charAt(0) == '#')
  397. continue;
  398. int spaceIndex = popCandidate.indexOf(' ');
  399. boolean pop = false;
  400. if (spaceIndex >= 0) {
  401. String actionToken = popCandidate.substring(0, spaceIndex);
  402. pop = Action.parse(actionToken) != null;
  403. }
  404. if (pop)
  405. poppedLines.add(popCandidate);
  406. else
  407. todoLines.add(popCandidate);
  408. }
  409. String readLine = br.readLine();
  410. while (readLine != null) {
  411. todoLines.add(readLine);
  412. readLine = br.readLine();
  413. }
  414. } finally {
  415. br.close();
  416. }
  417. BufferedWriter todoWriter = new BufferedWriter(new OutputStreamWriter(
  418. new FileOutputStream(todoFile), Constants.CHARACTER_ENCODING));
  419. try {
  420. for (String writeLine : todoLines) {
  421. todoWriter.write(writeLine);
  422. todoWriter.newLine();
  423. }
  424. } finally {
  425. todoWriter.close();
  426. }
  427. if (poppedLines.size() > 0) {
  428. // append here
  429. BufferedWriter doneWriter = new BufferedWriter(
  430. new OutputStreamWriter(
  431. new FileOutputStream(doneFile, true),
  432. Constants.CHARACTER_ENCODING));
  433. try {
  434. for (String writeLine : poppedLines) {
  435. doneWriter.write(writeLine);
  436. doneWriter.newLine();
  437. }
  438. } finally {
  439. doneWriter.close();
  440. }
  441. }
  442. }
  443. private RebaseResult initFilesAndRewind() throws RefNotFoundException,
  444. IOException, NoHeadException, JGitInternalException {
  445. // we need to store everything into files so that we can implement
  446. // --skip, --continue, and --abort
  447. // first of all, we determine the commits to be applied
  448. List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
  449. Ref head = repo.getRef(Constants.HEAD);
  450. if (head == null || head.getObjectId() == null)
  451. throw new RefNotFoundException(MessageFormat.format(
  452. JGitText.get().refNotResolved, Constants.HEAD));
  453. String headName;
  454. if (head.isSymbolic())
  455. headName = head.getTarget().getName();
  456. else
  457. headName = "detached HEAD";
  458. ObjectId headId = head.getObjectId();
  459. if (headId == null)
  460. throw new RefNotFoundException(MessageFormat.format(
  461. JGitText.get().refNotResolved, Constants.HEAD));
  462. RevCommit headCommit = walk.lookupCommit(headId);
  463. monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
  464. ProgressMonitor.UNKNOWN);
  465. LogCommand cmd = new Git(repo).log().addRange(upstreamCommit,
  466. headCommit);
  467. Iterable<RevCommit> commitsToUse = cmd.call();
  468. for (RevCommit commit : commitsToUse) {
  469. cherryPickList.add(commit);
  470. }
  471. // if the upstream commit is in a direct line to the current head,
  472. // the log command will not report any commits; in this case,
  473. // we create the cherry-pick list ourselves
  474. if (cherryPickList.isEmpty()) {
  475. Iterable<RevCommit> parents = new Git(repo).log().add(
  476. upstreamCommit).call();
  477. for (RevCommit parent : parents) {
  478. if (parent.equals(headCommit))
  479. break;
  480. if (parent.getParentCount() != 1)
  481. throw new JGitInternalException(
  482. JGitText.get().canOnlyCherryPickCommitsWithOneParent);
  483. cherryPickList.add(parent);
  484. }
  485. }
  486. // nothing to do: return with UP_TO_DATE_RESULT
  487. if (cherryPickList.isEmpty())
  488. return RebaseResult.UP_TO_DATE_RESULT;
  489. Collections.reverse(cherryPickList);
  490. // create the folder for the meta information
  491. FileUtils.mkdir(rebaseDir);
  492. createFile(repo.getDirectory(), Constants.ORIG_HEAD, headId.name());
  493. createFile(rebaseDir, REBASE_HEAD, headId.name());
  494. createFile(rebaseDir, HEAD_NAME, headName);
  495. createFile(rebaseDir, ONTO, upstreamCommit.name());
  496. createFile(rebaseDir, INTERACTIVE, "");
  497. BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
  498. new FileOutputStream(new File(rebaseDir, GIT_REBASE_TODO)),
  499. Constants.CHARACTER_ENCODING));
  500. fw.write("# Created by EGit: rebasing " + upstreamCommit.name()
  501. + " onto " + headId.name());
  502. fw.newLine();
  503. try {
  504. StringBuilder sb = new StringBuilder();
  505. ObjectReader reader = walk.getObjectReader();
  506. for (RevCommit commit : cherryPickList) {
  507. sb.setLength(0);
  508. sb.append(Action.PICK.toToken());
  509. sb.append(" ");
  510. sb.append(reader.abbreviate(commit).name());
  511. sb.append(" ");
  512. sb.append(commit.getShortMessage());
  513. fw.write(sb.toString());
  514. fw.newLine();
  515. }
  516. } finally {
  517. fw.close();
  518. }
  519. monitor.endTask();
  520. // we rewind to the upstream commit
  521. monitor.beginTask(MessageFormat.format(JGitText.get().rewinding,
  522. upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
  523. checkoutCommit(upstreamCommit);
  524. monitor.endTask();
  525. return null;
  526. }
  527. /**
  528. * checks if we can fast-forward and returns the new head if it is possible
  529. *
  530. * @param newCommit
  531. * @return the new head, or null
  532. * @throws RefNotFoundException
  533. * @throws IOException
  534. */
  535. public RevCommit tryFastForward(RevCommit newCommit)
  536. throws RefNotFoundException, IOException {
  537. Ref head = repo.getRef(Constants.HEAD);
  538. if (head == null || head.getObjectId() == null)
  539. throw new RefNotFoundException(MessageFormat.format(
  540. JGitText.get().refNotResolved, Constants.HEAD));
  541. ObjectId headId = head.getObjectId();
  542. if (headId == null)
  543. throw new RefNotFoundException(MessageFormat.format(
  544. JGitText.get().refNotResolved, Constants.HEAD));
  545. RevCommit headCommit = walk.lookupCommit(headId);
  546. if (walk.isMergedInto(newCommit, headCommit))
  547. return newCommit;
  548. String headName;
  549. if (head.isSymbolic())
  550. headName = head.getTarget().getName();
  551. else
  552. headName = "detached HEAD";
  553. return tryFastForward(headName, headCommit, newCommit);
  554. }
  555. private RevCommit tryFastForward(String headName, RevCommit oldCommit,
  556. RevCommit newCommit) throws IOException, JGitInternalException {
  557. boolean tryRebase = false;
  558. for (RevCommit parentCommit : newCommit.getParents())
  559. if (parentCommit.equals(oldCommit))
  560. tryRebase = true;
  561. if (!tryRebase)
  562. return null;
  563. CheckoutCommand co = new CheckoutCommand(repo);
  564. try {
  565. co.setName(newCommit.name()).call();
  566. if (headName.startsWith(Constants.R_HEADS)) {
  567. RefUpdate rup = repo.updateRef(headName);
  568. rup.setExpectedOldObjectId(oldCommit);
  569. rup.setNewObjectId(newCommit);
  570. rup.setRefLogMessage("Fast-foward from " + oldCommit.name()
  571. + " to " + newCommit.name(), false);
  572. Result res = rup.update(walk);
  573. switch (res) {
  574. case FAST_FORWARD:
  575. case NO_CHANGE:
  576. case FORCED:
  577. break;
  578. default:
  579. throw new IOException("Could not fast-forward");
  580. }
  581. }
  582. return newCommit;
  583. } catch (RefAlreadyExistsException e) {
  584. throw new JGitInternalException(e.getMessage(), e);
  585. } catch (RefNotFoundException e) {
  586. throw new JGitInternalException(e.getMessage(), e);
  587. } catch (InvalidRefNameException e) {
  588. throw new JGitInternalException(e.getMessage(), e);
  589. }
  590. }
  591. private void checkParameters() throws WrongRepositoryStateException {
  592. if (this.operation != Operation.BEGIN) {
  593. // these operations are only possible while in a rebasing state
  594. switch (repo.getRepositoryState()) {
  595. case REBASING_INTERACTIVE:
  596. break;
  597. default:
  598. throw new WrongRepositoryStateException(MessageFormat.format(
  599. JGitText.get().wrongRepositoryState, repo
  600. .getRepositoryState().name()));
  601. }
  602. } else
  603. switch (repo.getRepositoryState()) {
  604. case SAFE:
  605. if (this.upstreamCommit == null)
  606. throw new JGitInternalException(MessageFormat
  607. .format(JGitText.get().missingRequiredParameter,
  608. "upstream"));
  609. return;
  610. default:
  611. throw new WrongRepositoryStateException(MessageFormat.format(
  612. JGitText.get().wrongRepositoryState, repo
  613. .getRepositoryState().name()));
  614. }
  615. }
  616. private void createFile(File parentDir, String name, String content)
  617. throws IOException {
  618. File file = new File(parentDir, name);
  619. FileOutputStream fos = new FileOutputStream(file);
  620. try {
  621. fos.write(content.getBytes(Constants.CHARACTER_ENCODING));
  622. fos.write('\n');
  623. } finally {
  624. fos.close();
  625. }
  626. }
  627. private RebaseResult abort() throws IOException {
  628. try {
  629. String commitId = readFile(repo.getDirectory(), Constants.ORIG_HEAD);
  630. monitor.beginTask(MessageFormat.format(
  631. JGitText.get().abortingRebase, commitId),
  632. ProgressMonitor.UNKNOWN);
  633. RevCommit commit = walk.parseCommit(repo.resolve(commitId));
  634. // no head in order to reset --hard
  635. DirCacheCheckout dco = new DirCacheCheckout(repo, repo
  636. .lockDirCache(), commit.getTree());
  637. dco.setFailOnConflict(false);
  638. dco.checkout();
  639. walk.release();
  640. } finally {
  641. monitor.endTask();
  642. }
  643. try {
  644. String headName = readFile(rebaseDir, HEAD_NAME);
  645. if (headName.startsWith(Constants.R_REFS)) {
  646. monitor.beginTask(MessageFormat.format(
  647. JGitText.get().resettingHead, headName),
  648. ProgressMonitor.UNKNOWN);
  649. // update the HEAD
  650. RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false);
  651. Result res = refUpdate.link(headName);
  652. switch (res) {
  653. case FAST_FORWARD:
  654. case FORCED:
  655. case NO_CHANGE:
  656. break;
  657. default:
  658. throw new JGitInternalException(
  659. JGitText.get().abortingRebaseFailed);
  660. }
  661. }
  662. // cleanup the files
  663. FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
  664. return new RebaseResult(Status.ABORTED);
  665. } finally {
  666. monitor.endTask();
  667. }
  668. }
  669. private String readFile(File directory, String fileName) throws IOException {
  670. byte[] content = IO.readFully(new File(directory, fileName));
  671. // strip off the last LF
  672. int end = content.length;
  673. while (0 < end && content[end - 1] == '\n')
  674. end--;
  675. return RawParseUtils.decode(content, 0, end);
  676. }
  677. private void checkoutCommit(RevCommit commit) throws IOException {
  678. try {
  679. RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
  680. DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
  681. repo.lockDirCache(), commit.getTree());
  682. dco.setFailOnConflict(true);
  683. dco.checkout();
  684. // update the HEAD
  685. RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
  686. refUpdate.setExpectedOldObjectId(head);
  687. refUpdate.setNewObjectId(commit);
  688. Result res = refUpdate.forceUpdate();
  689. switch (res) {
  690. case FAST_FORWARD:
  691. case NO_CHANGE:
  692. case FORCED:
  693. break;
  694. default:
  695. throw new IOException("Could not rewind to upstream commit");
  696. }
  697. } finally {
  698. walk.release();
  699. monitor.endTask();
  700. }
  701. }
  702. private List<Step> loadSteps() throws IOException {
  703. byte[] buf = IO.readFully(new File(rebaseDir, GIT_REBASE_TODO));
  704. int ptr = 0;
  705. int tokenBegin = 0;
  706. ArrayList<Step> r = new ArrayList<Step>();
  707. while (ptr < buf.length) {
  708. tokenBegin = ptr;
  709. ptr = RawParseUtils.nextLF(buf, ptr);
  710. int nextSpace = 0;
  711. int tokenCount = 0;
  712. Step current = null;
  713. while (tokenCount < 3 && nextSpace < ptr) {
  714. switch (tokenCount) {
  715. case 0:
  716. nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  717. String actionToken = new String(buf, tokenBegin, nextSpace
  718. - tokenBegin - 1);
  719. tokenBegin = nextSpace;
  720. if (actionToken.charAt(0) == '#') {
  721. tokenCount = 3;
  722. break;
  723. }
  724. Action action = Action.parse(actionToken);
  725. if (action != null)
  726. current = new Step(Action.parse(actionToken));
  727. break;
  728. case 1:
  729. if (current == null)
  730. break;
  731. nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  732. String commitToken = new String(buf, tokenBegin, nextSpace
  733. - tokenBegin - 1);
  734. tokenBegin = nextSpace;
  735. current.commit = AbbreviatedObjectId
  736. .fromString(commitToken);
  737. break;
  738. case 2:
  739. if (current == null)
  740. break;
  741. nextSpace = ptr;
  742. int length = ptr - tokenBegin;
  743. current.shortMessage = new byte[length];
  744. System.arraycopy(buf, tokenBegin, current.shortMessage, 0,
  745. length);
  746. r.add(current);
  747. break;
  748. }
  749. tokenCount++;
  750. }
  751. }
  752. return r;
  753. }
  754. /**
  755. * @param upstream
  756. * the upstream commit
  757. * @return {@code this}
  758. */
  759. public RebaseCommand setUpstream(RevCommit upstream) {
  760. this.upstreamCommit = upstream;
  761. return this;
  762. }
  763. /**
  764. * @param upstream
  765. * id of the upstream commit
  766. * @return {@code this}
  767. */
  768. public RebaseCommand setUpstream(AnyObjectId upstream) {
  769. try {
  770. this.upstreamCommit = walk.parseCommit(upstream);
  771. } catch (IOException e) {
  772. throw new JGitInternalException(MessageFormat.format(
  773. JGitText.get().couldNotReadObjectWhileParsingCommit,
  774. upstream.name()), e);
  775. }
  776. return this;
  777. }
  778. /**
  779. * @param upstream
  780. * the upstream branch
  781. * @return {@code this}
  782. * @throws RefNotFoundException
  783. */
  784. public RebaseCommand setUpstream(String upstream)
  785. throws RefNotFoundException {
  786. try {
  787. ObjectId upstreamId = repo.resolve(upstream);
  788. if (upstreamId == null)
  789. throw new RefNotFoundException(MessageFormat.format(JGitText
  790. .get().refNotResolved, upstream));
  791. upstreamCommit = walk.parseCommit(repo.resolve(upstream));
  792. return this;
  793. } catch (IOException ioe) {
  794. throw new JGitInternalException(ioe.getMessage(), ioe);
  795. }
  796. }
  797. /**
  798. * @param operation
  799. * the operation to perform
  800. * @return {@code this}
  801. */
  802. public RebaseCommand setOperation(Operation operation) {
  803. this.operation = operation;
  804. return this;
  805. }
  806. /**
  807. * @param monitor
  808. * a progress monitor
  809. * @return this instance
  810. */
  811. public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
  812. this.monitor = monitor;
  813. return this;
  814. }
  815. static enum Action {
  816. PICK("pick"); // later add SQUASH, EDIT, etc.
  817. private final String token;
  818. private Action(String token) {
  819. this.token = token;
  820. }
  821. public String toToken() {
  822. return this.token;
  823. }
  824. static Action parse(String token) {
  825. if (token.equals("pick") || token.equals("p"))
  826. return PICK;
  827. throw new JGitInternalException(
  828. MessageFormat
  829. .format(
  830. "Unknown or unsupported command \"{0}\", only \"pick\" is allowed",
  831. token));
  832. }
  833. }
  834. static class Step {
  835. Action action;
  836. AbbreviatedObjectId commit;
  837. byte[] shortMessage;
  838. Step(Action action) {
  839. this.action = action;
  840. }
  841. }
  842. PersonIdent parseAuthor(byte[] raw) {
  843. if (raw.length == 0)
  844. return null;
  845. Map<String, String> keyValueMap = new HashMap<String, String>();
  846. for (int p = 0; p < raw.length;) {
  847. int end = RawParseUtils.nextLF(raw, p);
  848. if (end == p)
  849. break;
  850. int equalsIndex = RawParseUtils.next(raw, p, '=');
  851. if (equalsIndex == end)
  852. break;
  853. String key = RawParseUtils.decode(raw, p, equalsIndex - 1);
  854. String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2);
  855. p = end;
  856. keyValueMap.put(key, value);
  857. }
  858. String name = keyValueMap.get(GIT_AUTHOR_NAME);
  859. String email = keyValueMap.get(GIT_AUTHOR_EMAIL);
  860. String time = keyValueMap.get(GIT_AUTHOR_DATE);
  861. // the time is saved as <seconds since 1970> <timezone offset>
  862. long when = Long.parseLong(time.substring(0, time.indexOf(' '))) * 1000;
  863. String tzOffsetString = time.substring(time.indexOf(' ') + 1);
  864. int multiplier = -1;
  865. if (tzOffsetString.charAt(0) == '+')
  866. multiplier = 1;
  867. int hours = Integer.parseInt(tzOffsetString.substring(1, 3));
  868. int minutes = Integer.parseInt(tzOffsetString.substring(3, 5));
  869. // this is in format (+/-)HHMM (hours and minutes)
  870. // we need to convert into minutes
  871. int tz = (hours * 60 + minutes) * multiplier;
  872. if (name != null && email != null)
  873. return new PersonIdent(name, email, when, tz);
  874. return null;
  875. }
  876. }