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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  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. if (this.operation == Operation.SKIP)
  195. newHead = checkoutCurrentHead();
  196. ObjectReader or = repo.newObjectReader();
  197. List<Step> steps = loadSteps();
  198. for (Step step : steps) {
  199. popSteps(1);
  200. Collection<ObjectId> ids = or.resolve(step.commit);
  201. if (ids.size() != 1)
  202. throw new JGitInternalException(
  203. "Could not resolve uniquely the abbreviated object ID");
  204. RevCommit commitToPick = walk
  205. .parseCommit(ids.iterator().next());
  206. if (monitor.isCancelled())
  207. return new RebaseResult(commitToPick);
  208. monitor.beginTask(MessageFormat.format(
  209. JGitText.get().applyingCommit, commitToPick
  210. .getShortMessage()), ProgressMonitor.UNKNOWN);
  211. // TODO if the first parent of commitToPick is the current HEAD,
  212. // we should fast-forward instead of cherry-pick to avoid
  213. // unnecessary object rewriting
  214. newHead = new Git(repo).cherryPick().include(commitToPick)
  215. .call();
  216. monitor.endTask();
  217. if (newHead == null) {
  218. return stop(commitToPick);
  219. }
  220. }
  221. if (newHead != null) {
  222. // point the previous head (if any) to the new commit
  223. String headName = readFile(rebaseDir, HEAD_NAME);
  224. if (headName.startsWith(Constants.R_REFS)) {
  225. RefUpdate rup = repo.updateRef(headName);
  226. rup.setNewObjectId(newHead);
  227. Result res = rup.forceUpdate();
  228. switch (res) {
  229. case FAST_FORWARD:
  230. case FORCED:
  231. case NO_CHANGE:
  232. break;
  233. default:
  234. throw new JGitInternalException("Updating HEAD failed");
  235. }
  236. rup = repo.updateRef(Constants.HEAD);
  237. res = rup.link(headName);
  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. }
  247. FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
  248. return new RebaseResult(Status.OK);
  249. }
  250. return new RebaseResult(Status.UP_TO_DATE);
  251. } catch (IOException ioe) {
  252. throw new JGitInternalException(ioe.getMessage(), ioe);
  253. }
  254. }
  255. private RevCommit checkoutCurrentHead() throws IOException,
  256. NoHeadException, JGitInternalException {
  257. ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}");
  258. if (headTree == null)
  259. throw new NoHeadException(
  260. JGitText.get().cannotRebaseWithoutCurrentHead);
  261. DirCache dc = repo.lockDirCache();
  262. try {
  263. DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree);
  264. dco.setFailOnConflict(false);
  265. boolean needsDeleteFiles = dco.checkout();
  266. if (needsDeleteFiles) {
  267. List<String> fileList = dco.getToBeDeleted();
  268. for (String filePath : fileList) {
  269. File fileToDelete = new File(repo.getWorkTree(), filePath);
  270. if (fileToDelete.exists())
  271. FileUtils.delete(fileToDelete, FileUtils.RECURSIVE
  272. | FileUtils.RETRY);
  273. }
  274. }
  275. } finally {
  276. dc.unlock();
  277. }
  278. RevWalk rw = new RevWalk(repo);
  279. RevCommit commit = rw.parseCommit(repo.resolve(Constants.HEAD));
  280. rw.release();
  281. return commit;
  282. }
  283. /**
  284. * @return the commit if we had to do a commit, otherwise null
  285. * @throws GitAPIException
  286. * @throws IOException
  287. */
  288. private RevCommit continueRebase() throws GitAPIException, IOException {
  289. // if there are still conflicts, we throw a specific Exception
  290. DirCache dc = repo.readDirCache();
  291. boolean hasUnmergedPaths = dc.hasUnmergedPaths();
  292. if (hasUnmergedPaths)
  293. throw new UnmergedPathsException();
  294. // determine whether we need to commit
  295. TreeWalk treeWalk = new TreeWalk(repo);
  296. treeWalk.reset();
  297. treeWalk.setRecursive(true);
  298. treeWalk.addTree(new DirCacheIterator(dc));
  299. ObjectId id = repo.resolve(Constants.HEAD + "^{tree}");
  300. if (id == null)
  301. throw new NoHeadException(
  302. JGitText.get().cannotRebaseWithoutCurrentHead);
  303. treeWalk.addTree(id);
  304. treeWalk.setFilter(TreeFilter.ANY_DIFF);
  305. boolean needsCommit = treeWalk.next();
  306. treeWalk.release();
  307. if (needsCommit) {
  308. CommitCommand commit = new Git(repo).commit();
  309. commit.setMessage(readFile(rebaseDir, MESSAGE));
  310. commit.setAuthor(parseAuthor());
  311. return commit.call();
  312. }
  313. return null;
  314. }
  315. private PersonIdent parseAuthor() throws IOException {
  316. File authorScriptFile = new File(rebaseDir, AUTHOR_SCRIPT);
  317. byte[] raw;
  318. try {
  319. raw = IO.readFully(authorScriptFile);
  320. } catch (FileNotFoundException notFound) {
  321. return null;
  322. }
  323. return parseAuthor(raw);
  324. }
  325. private RebaseResult stop(RevCommit commitToPick) throws IOException {
  326. PersonIdent author = commitToPick.getAuthorIdent();
  327. String authorScript = toAuthorScript(author);
  328. createFile(rebaseDir, AUTHOR_SCRIPT, authorScript);
  329. createFile(rebaseDir, MESSAGE, commitToPick.getFullMessage());
  330. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  331. DiffFormatter df = new DiffFormatter(bos);
  332. df.setRepository(repo);
  333. df.format(commitToPick.getParent(0), commitToPick);
  334. createFile(rebaseDir, PATCH, new String(bos.toByteArray(),
  335. Constants.CHARACTER_ENCODING));
  336. createFile(rebaseDir, STOPPED_SHA, repo.newObjectReader().abbreviate(
  337. commitToPick).name());
  338. return new RebaseResult(commitToPick);
  339. }
  340. String toAuthorScript(PersonIdent author) {
  341. StringBuilder sb = new StringBuilder(100);
  342. sb.append(GIT_AUTHOR_NAME);
  343. sb.append("='");
  344. sb.append(author.getName());
  345. sb.append("'\n");
  346. sb.append(GIT_AUTHOR_EMAIL);
  347. sb.append("='");
  348. sb.append(author.getEmailAddress());
  349. sb.append("'\n");
  350. // the command line uses the "external String"
  351. // representation for date and timezone
  352. sb.append(GIT_AUTHOR_DATE);
  353. sb.append("='");
  354. String externalString = author.toExternalString();
  355. sb
  356. .append(externalString.substring(externalString
  357. .lastIndexOf('>') + 2));
  358. sb.append("'\n");
  359. return sb.toString();
  360. }
  361. /**
  362. * Removes the number of lines given in the parameter from the
  363. * <code>git-rebase-todo</code> file but preserves comments and other lines
  364. * that can not be parsed as steps
  365. *
  366. * @param numSteps
  367. * @throws IOException
  368. */
  369. private void popSteps(int numSteps) throws IOException {
  370. if (numSteps == 0)
  371. return;
  372. List<String> todoLines = new ArrayList<String>();
  373. List<String> poppedLines = new ArrayList<String>();
  374. File todoFile = new File(rebaseDir, GIT_REBASE_TODO);
  375. File doneFile = new File(rebaseDir, DONE);
  376. BufferedReader br = new BufferedReader(new InputStreamReader(
  377. new FileInputStream(todoFile), Constants.CHARACTER_ENCODING));
  378. try {
  379. // check if the line starts with a action tag (pick, skip...)
  380. while (poppedLines.size() < numSteps) {
  381. String popCandidate = br.readLine();
  382. if (popCandidate == null)
  383. break;
  384. if (popCandidate.charAt(0) == '#')
  385. continue;
  386. int spaceIndex = popCandidate.indexOf(' ');
  387. boolean pop = false;
  388. if (spaceIndex >= 0) {
  389. String actionToken = popCandidate.substring(0, spaceIndex);
  390. pop = Action.parse(actionToken) != null;
  391. }
  392. if (pop)
  393. poppedLines.add(popCandidate);
  394. else
  395. todoLines.add(popCandidate);
  396. }
  397. String readLine = br.readLine();
  398. while (readLine != null) {
  399. todoLines.add(readLine);
  400. readLine = br.readLine();
  401. }
  402. } finally {
  403. br.close();
  404. }
  405. BufferedWriter todoWriter = new BufferedWriter(new OutputStreamWriter(
  406. new FileOutputStream(todoFile), Constants.CHARACTER_ENCODING));
  407. try {
  408. for (String writeLine : todoLines) {
  409. todoWriter.write(writeLine);
  410. todoWriter.newLine();
  411. }
  412. } finally {
  413. todoWriter.close();
  414. }
  415. if (poppedLines.size() > 0) {
  416. // append here
  417. BufferedWriter doneWriter = new BufferedWriter(
  418. new OutputStreamWriter(
  419. new FileOutputStream(doneFile, true),
  420. Constants.CHARACTER_ENCODING));
  421. try {
  422. for (String writeLine : poppedLines) {
  423. doneWriter.write(writeLine);
  424. doneWriter.newLine();
  425. }
  426. } finally {
  427. doneWriter.close();
  428. }
  429. }
  430. }
  431. private RebaseResult initFilesAndRewind() throws RefNotFoundException,
  432. IOException, NoHeadException, JGitInternalException {
  433. // we need to store everything into files so that we can implement
  434. // --skip, --continue, and --abort
  435. // first of all, we determine the commits to be applied
  436. List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
  437. Ref head = repo.getRef(Constants.HEAD);
  438. if (head == null || head.getObjectId() == null)
  439. throw new RefNotFoundException(MessageFormat.format(
  440. JGitText.get().refNotResolved, Constants.HEAD));
  441. String headName;
  442. if (head.isSymbolic())
  443. headName = head.getTarget().getName();
  444. else
  445. headName = "detached HEAD";
  446. ObjectId headId = head.getObjectId();
  447. if (headId == null)
  448. throw new RefNotFoundException(MessageFormat.format(
  449. JGitText.get().refNotResolved, Constants.HEAD));
  450. RevCommit headCommit = walk.lookupCommit(headId);
  451. monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
  452. ProgressMonitor.UNKNOWN);
  453. LogCommand cmd = new Git(repo).log().addRange(upstreamCommit,
  454. headCommit);
  455. Iterable<RevCommit> commitsToUse = cmd.call();
  456. for (RevCommit commit : commitsToUse) {
  457. cherryPickList.add(commit);
  458. }
  459. // nothing to do: return with UP_TO_DATE_RESULT
  460. if (cherryPickList.isEmpty())
  461. return RebaseResult.UP_TO_DATE_RESULT;
  462. Collections.reverse(cherryPickList);
  463. // create the folder for the meta information
  464. FileUtils.mkdir(rebaseDir);
  465. createFile(repo.getDirectory(), Constants.ORIG_HEAD, headId.name());
  466. createFile(rebaseDir, REBASE_HEAD, headId.name());
  467. createFile(rebaseDir, HEAD_NAME, headName);
  468. createFile(rebaseDir, ONTO, upstreamCommit.name());
  469. createFile(rebaseDir, INTERACTIVE, "");
  470. BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
  471. new FileOutputStream(new File(rebaseDir, GIT_REBASE_TODO)),
  472. Constants.CHARACTER_ENCODING));
  473. fw.write("# Created by EGit: rebasing " + upstreamCommit.name()
  474. + " onto " + headId.name());
  475. fw.newLine();
  476. try {
  477. StringBuilder sb = new StringBuilder();
  478. ObjectReader reader = walk.getObjectReader();
  479. for (RevCommit commit : cherryPickList) {
  480. sb.setLength(0);
  481. sb.append(Action.PICK.toToken());
  482. sb.append(" ");
  483. sb.append(reader.abbreviate(commit).name());
  484. sb.append(" ");
  485. sb.append(commit.getShortMessage());
  486. fw.write(sb.toString());
  487. fw.newLine();
  488. }
  489. } finally {
  490. fw.close();
  491. }
  492. monitor.endTask();
  493. // we rewind to the upstream commit
  494. monitor.beginTask(MessageFormat.format(JGitText.get().rewinding,
  495. upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
  496. checkoutCommit(upstreamCommit);
  497. monitor.endTask();
  498. return null;
  499. }
  500. private void checkParameters() throws WrongRepositoryStateException {
  501. if (this.operation != Operation.BEGIN) {
  502. // these operations are only possible while in a rebasing state
  503. switch (repo.getRepositoryState()) {
  504. case REBASING_INTERACTIVE:
  505. break;
  506. default:
  507. throw new WrongRepositoryStateException(MessageFormat.format(
  508. JGitText.get().wrongRepositoryState, repo
  509. .getRepositoryState().name()));
  510. }
  511. } else
  512. switch (repo.getRepositoryState()) {
  513. case SAFE:
  514. if (this.upstreamCommit == null)
  515. throw new JGitInternalException(MessageFormat
  516. .format(JGitText.get().missingRequiredParameter,
  517. "upstream"));
  518. return;
  519. default:
  520. throw new WrongRepositoryStateException(MessageFormat.format(
  521. JGitText.get().wrongRepositoryState, repo
  522. .getRepositoryState().name()));
  523. }
  524. }
  525. private void createFile(File parentDir, String name, String content)
  526. throws IOException {
  527. File file = new File(parentDir, name);
  528. FileOutputStream fos = new FileOutputStream(file);
  529. try {
  530. fos.write(content.getBytes(Constants.CHARACTER_ENCODING));
  531. fos.write('\n');
  532. } finally {
  533. fos.close();
  534. }
  535. }
  536. private RebaseResult abort() throws IOException {
  537. try {
  538. String commitId = readFile(repo.getDirectory(), Constants.ORIG_HEAD);
  539. monitor.beginTask(MessageFormat.format(
  540. JGitText.get().abortingRebase, commitId),
  541. ProgressMonitor.UNKNOWN);
  542. RevCommit commit = walk.parseCommit(repo.resolve(commitId));
  543. // no head in order to reset --hard
  544. DirCacheCheckout dco = new DirCacheCheckout(repo, repo
  545. .lockDirCache(), commit.getTree());
  546. dco.setFailOnConflict(false);
  547. dco.checkout();
  548. walk.release();
  549. } finally {
  550. monitor.endTask();
  551. }
  552. try {
  553. String headName = readFile(rebaseDir, HEAD_NAME);
  554. if (headName.startsWith(Constants.R_REFS)) {
  555. monitor.beginTask(MessageFormat.format(
  556. JGitText.get().resettingHead, headName),
  557. ProgressMonitor.UNKNOWN);
  558. // update the HEAD
  559. RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false);
  560. Result res = refUpdate.link(headName);
  561. switch (res) {
  562. case FAST_FORWARD:
  563. case FORCED:
  564. case NO_CHANGE:
  565. break;
  566. default:
  567. throw new JGitInternalException(
  568. JGitText.get().abortingRebaseFailed);
  569. }
  570. }
  571. // cleanup the files
  572. FileUtils.delete(rebaseDir, FileUtils.RECURSIVE);
  573. return new RebaseResult(Status.ABORTED);
  574. } finally {
  575. monitor.endTask();
  576. }
  577. }
  578. private String readFile(File directory, String fileName) throws IOException {
  579. byte[] content = IO.readFully(new File(directory, fileName));
  580. // strip off the last LF
  581. int end = content.length;
  582. while (0 < end && content[end - 1] == '\n')
  583. end--;
  584. return RawParseUtils.decode(content, 0, end);
  585. }
  586. private void checkoutCommit(RevCommit commit) throws IOException {
  587. try {
  588. RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
  589. DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
  590. repo.lockDirCache(), commit.getTree());
  591. dco.setFailOnConflict(true);
  592. dco.checkout();
  593. // update the HEAD
  594. RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
  595. refUpdate.setExpectedOldObjectId(head);
  596. refUpdate.setNewObjectId(commit);
  597. Result res = refUpdate.forceUpdate();
  598. switch (res) {
  599. case FAST_FORWARD:
  600. case NO_CHANGE:
  601. case FORCED:
  602. break;
  603. default:
  604. throw new IOException("Could not rewind to upstream commit");
  605. }
  606. } finally {
  607. walk.release();
  608. monitor.endTask();
  609. }
  610. }
  611. private List<Step> loadSteps() throws IOException {
  612. byte[] buf = IO.readFully(new File(rebaseDir, GIT_REBASE_TODO));
  613. int ptr = 0;
  614. int tokenBegin = 0;
  615. ArrayList<Step> r = new ArrayList<Step>();
  616. while (ptr < buf.length) {
  617. tokenBegin = ptr;
  618. ptr = RawParseUtils.nextLF(buf, ptr);
  619. int nextSpace = 0;
  620. int tokenCount = 0;
  621. Step current = null;
  622. while (tokenCount < 3 && nextSpace < ptr) {
  623. switch (tokenCount) {
  624. case 0:
  625. nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  626. String actionToken = new String(buf, tokenBegin, nextSpace
  627. - tokenBegin - 1);
  628. tokenBegin = nextSpace;
  629. if (actionToken.charAt(0) == '#') {
  630. tokenCount = 3;
  631. break;
  632. }
  633. Action action = Action.parse(actionToken);
  634. if (action != null)
  635. current = new Step(Action.parse(actionToken));
  636. break;
  637. case 1:
  638. if (current == null)
  639. break;
  640. nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  641. String commitToken = new String(buf, tokenBegin, nextSpace
  642. - tokenBegin - 1);
  643. tokenBegin = nextSpace;
  644. current.commit = AbbreviatedObjectId
  645. .fromString(commitToken);
  646. break;
  647. case 2:
  648. if (current == null)
  649. break;
  650. nextSpace = ptr;
  651. int length = ptr - tokenBegin;
  652. current.shortMessage = new byte[length];
  653. System.arraycopy(buf, tokenBegin, current.shortMessage, 0,
  654. length);
  655. r.add(current);
  656. break;
  657. }
  658. tokenCount++;
  659. }
  660. }
  661. return r;
  662. }
  663. /**
  664. * @param upstream
  665. * the upstream commit
  666. * @return {@code this}
  667. */
  668. public RebaseCommand setUpstream(RevCommit upstream) {
  669. this.upstreamCommit = upstream;
  670. return this;
  671. }
  672. /**
  673. * @param upstream
  674. * the upstream branch
  675. * @return {@code this}
  676. * @throws RefNotFoundException
  677. */
  678. public RebaseCommand setUpstream(String upstream)
  679. throws RefNotFoundException {
  680. try {
  681. ObjectId upstreamId = repo.resolve(upstream);
  682. if (upstreamId == null)
  683. throw new RefNotFoundException(MessageFormat.format(JGitText
  684. .get().refNotResolved, upstream));
  685. upstreamCommit = walk.parseCommit(repo.resolve(upstream));
  686. return this;
  687. } catch (IOException ioe) {
  688. throw new JGitInternalException(ioe.getMessage(), ioe);
  689. }
  690. }
  691. /**
  692. * @param operation
  693. * the operation to perform
  694. * @return {@code this}
  695. */
  696. public RebaseCommand setOperation(Operation operation) {
  697. this.operation = operation;
  698. return this;
  699. }
  700. /**
  701. * @param monitor
  702. * a progress monitor
  703. * @return this instance
  704. */
  705. public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
  706. this.monitor = monitor;
  707. return this;
  708. }
  709. static enum Action {
  710. PICK("pick"); // later add SQUASH, EDIT, etc.
  711. private final String token;
  712. private Action(String token) {
  713. this.token = token;
  714. }
  715. public String toToken() {
  716. return this.token;
  717. }
  718. static Action parse(String token) {
  719. if (token.equals("pick") || token.equals("p"))
  720. return PICK;
  721. throw new JGitInternalException(
  722. MessageFormat
  723. .format(
  724. "Unknown or unsupported command \"{0}\", only \"pick\" is allowed",
  725. token));
  726. }
  727. }
  728. static class Step {
  729. Action action;
  730. AbbreviatedObjectId commit;
  731. byte[] shortMessage;
  732. Step(Action action) {
  733. this.action = action;
  734. }
  735. }
  736. PersonIdent parseAuthor(byte[] raw) {
  737. if (raw.length == 0)
  738. return null;
  739. Map<String, String> keyValueMap = new HashMap<String, String>();
  740. for (int p = 0; p < raw.length;) {
  741. int end = RawParseUtils.nextLF(raw, p);
  742. if (end == p)
  743. break;
  744. int equalsIndex = RawParseUtils.next(raw, p, '=');
  745. if (equalsIndex == end)
  746. break;
  747. String key = RawParseUtils.decode(raw, p, equalsIndex - 1);
  748. String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2);
  749. p = end;
  750. keyValueMap.put(key, value);
  751. }
  752. String name = keyValueMap.get(GIT_AUTHOR_NAME);
  753. String email = keyValueMap.get(GIT_AUTHOR_EMAIL);
  754. String time = keyValueMap.get(GIT_AUTHOR_DATE);
  755. // the time is saved as <seconds since 1970> <timezone offset>
  756. long when = Long.parseLong(time.substring(0, time.indexOf(' '))) * 1000;
  757. String tzOffsetString = time.substring(time.indexOf(' ') + 1);
  758. int multiplier = -1;
  759. if (tzOffsetString.charAt(0) == '+')
  760. multiplier = 1;
  761. int hours = Integer.parseInt(tzOffsetString.substring(1, 3));
  762. int minutes = Integer.parseInt(tzOffsetString.substring(3, 5));
  763. // this is in format (+/-)HHMM (hours and minutes)
  764. // we need to convert into minutes
  765. int tz = (hours * 60 + minutes) * multiplier;
  766. if (name != null && email != null)
  767. return new PersonIdent(name, email, when, tz);
  768. return null;
  769. }
  770. }