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

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