]> source.dussan.org Git - jgit.git/commitdiff
Initial implementation of a Rebase command 64/1864/10
authorMathias Kinzler <mathias.kinzler@sap.com>
Mon, 22 Nov 2010 15:26:00 +0000 (16:26 +0100)
committerChris Aniszczyk <caniszczyk@gmail.com>
Mon, 22 Nov 2010 15:58:36 +0000 (09:58 -0600)
This is a first iteration to implement Rebase. At the moment, this
does not implement --continue and --skip, so if the first
conflict is found, the only option is to --abort the command.

Bug: 328217
Change-Id: I24d60c0214e71e5572955f8261e10a42e9e95298
Signed-off-by: Mathias Kinzler <mathias.kinzler@sap.com>
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java [new file with mode: 0644]
org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java [new file with mode: 0644]

diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
new file mode 100644 (file)
index 0000000..aee2cc4
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import org.eclipse.jgit.api.RebaseCommand.Operation;
+import org.eclipse.jgit.api.RebaseResult.Status;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class RebaseCommandTest extends RepositoryTestCase {
+       private void createBranch(ObjectId objectId, String branchName)
+                       throws IOException {
+               RefUpdate updateRef = db.updateRef(branchName);
+               updateRef.setNewObjectId(objectId);
+               updateRef.update();
+       }
+
+       private void checkoutBranch(String branchName)
+                       throws IllegalStateException, IOException {
+               RevWalk walk = new RevWalk(db);
+               RevCommit head = walk.parseCommit(db.resolve(Constants.HEAD));
+               RevCommit branch = walk.parseCommit(db.resolve(branchName));
+               DirCacheCheckout dco = new DirCacheCheckout(db, head.getTree().getId(),
+                               db.lockDirCache(), branch.getTree().getId());
+               dco.setFailOnConflict(true);
+               dco.checkout();
+               walk.release();
+               // update the HEAD
+               RefUpdate refUpdate = db.updateRef(Constants.HEAD);
+               refUpdate.link(branchName);
+       }
+
+       public void testFastForwardWithNewFile() throws Exception {
+               Git git = new Git(db);
+
+               // create file1 on master
+               writeTrashFile("file1", "file1");
+               git.add().addFilepattern("file1").call();
+               RevCommit first = git.commit().setMessage("Add file1").call();
+
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               // create a topic branch
+               createBranch(first, "refs/heads/topic");
+               // create file2 on master
+               writeTrashFile("file2", "file2");
+               git.add().addFilepattern("file2").call();
+               git.commit().setMessage("Add file2").call();
+               assertTrue(new File(db.getWorkTree(), "file2").exists());
+
+               checkoutBranch("refs/heads/topic");
+               assertFalse(new File(db.getWorkTree(), "file2").exists());
+
+               RebaseResult res = git.rebase().setUpstream("refs/heads/master").call();
+               assertEquals(Status.UP_TO_DATE, res.getStatus());
+       }
+
+       public void testConflictFreeWithSingleFile() throws Exception {
+               Git git = new Git(db);
+
+               // create file1 on master
+               File theFile = writeTrashFile("file1", "1\n2\n3\n");
+               git.add().addFilepattern("file1").call();
+               RevCommit second = git.commit().setMessage("Add file1").call();
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               // change first line in master and commit
+               writeTrashFile("file1", "1master\n2\n3\n");
+               checkFile(theFile, "1master\n2\n3\n");
+               git.add().addFilepattern("file1").call();
+               RevCommit lastMasterChange = git.commit().setMessage(
+                               "change file1 in master").call();
+
+               // create a topic branch based on second commit
+               createBranch(second, "refs/heads/topic");
+               checkoutBranch("refs/heads/topic");
+               // we have the old content again
+               checkFile(theFile, "1\n2\n3\n");
+
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               // change third line in topic branch
+               writeTrashFile("file1", "1\n2\n3\ntopic\n");
+               git.add().addFilepattern("file1").call();
+               git.commit().setMessage("change file1 in topic").call();
+
+               RebaseResult res = git.rebase().setUpstream("refs/heads/master").call();
+               assertEquals(Status.OK, res.getStatus());
+               checkFile(theFile, "1master\n2\n3\ntopic\n");
+               // our old branch should be checked out again
+               assertEquals("refs/heads/topic", db.getFullBranch());
+               assertEquals(lastMasterChange, new RevWalk(db).parseCommit(
+                               db.resolve(Constants.HEAD)).getParent(0));
+       }
+
+       public void testFilesAddedFromTwoBranches() throws Exception {
+               Git git = new Git(db);
+
+               // create file1 on master
+               writeTrashFile("file1", "file1");
+               git.add().addFilepattern("file1").call();
+               RevCommit masterCommit = git.commit().setMessage("Add file1 to master")
+                               .call();
+
+               // create a branch named file2 and add file2
+               createBranch(masterCommit, "refs/heads/file2");
+               checkoutBranch("refs/heads/file2");
+               writeTrashFile("file2", "file2");
+               git.add().addFilepattern("file2").call();
+               RevCommit addFile2 = git.commit().setMessage(
+                               "Add file2 to branch file2").call();
+
+               // create a branch named file3 and add file3
+               createBranch(masterCommit, "refs/heads/file3");
+               checkoutBranch("refs/heads/file3");
+               writeTrashFile("file3", "file3");
+               git.add().addFilepattern("file3").call();
+               git.commit().setMessage("Add file3 to branch file3").call();
+
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               assertFalse(new File(db.getWorkTree(), "file2").exists());
+               assertTrue(new File(db.getWorkTree(), "file3").exists());
+
+               RebaseResult res = git.rebase().setUpstream("refs/heads/file2").call();
+               assertEquals(Status.OK, res.getStatus());
+
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               assertTrue(new File(db.getWorkTree(), "file2").exists());
+               assertTrue(new File(db.getWorkTree(), "file3").exists());
+
+               // our old branch should be checked out again
+               assertEquals("refs/heads/file3", db.getFullBranch());
+               assertEquals(addFile2, new RevWalk(db).parseCommit(
+                               db.resolve(Constants.HEAD)).getParent(0));
+
+               checkoutBranch("refs/heads/file2");
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               assertTrue(new File(db.getWorkTree(), "file2").exists());
+               assertFalse(new File(db.getWorkTree(), "file3").exists());
+       }
+
+       public void testAbortOnConflict() throws Exception {
+               Git git = new Git(db);
+
+               // create file1 on master
+               File theFile = writeTrashFile("file1", "1\n2\n3\n");
+               git.add().addFilepattern("file1").call();
+               RevCommit second = git.commit().setMessage("Add file1").call();
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               // change first line in master and commit
+               writeTrashFile("file1", "1master\n2\n3\n");
+               checkFile(theFile, "1master\n2\n3\n");
+               git.add().addFilepattern("file1").call();
+               git.commit().setMessage("change file1 in master").call();
+
+               // create a topic branch based on second commit
+               createBranch(second, "refs/heads/topic");
+               checkoutBranch("refs/heads/topic");
+               // we have the old content again
+               checkFile(theFile, "1\n2\n3\n");
+
+               assertTrue(new File(db.getWorkTree(), "file1").exists());
+               // add a line (non-conflicting)
+               writeTrashFile("file1", "1\n2\n3\n4\n");
+               git.add().addFilepattern("file1").call();
+               git.commit().setMessage("add a line to file1 in topic").call();
+
+               // change first line (conflicting)
+               writeTrashFile("file1", "1topic\n2\n3\n4\n");
+               git.add().addFilepattern("file1").call();
+               git.commit().setMessage("change file1 in topic").call();
+
+               // change second line (not conflicting)
+               writeTrashFile("file1", "1topic\n2topic\n3\n4\n");
+               git.add().addFilepattern("file1").call();
+               RevCommit lastTopicCommit = git.commit().setMessage(
+                               "change file1 in topic again").call();
+
+               RebaseResult res = git.rebase().setUpstream("refs/heads/master").call();
+               assertEquals(Status.STOPPED, res.getStatus());
+               checkFile(theFile,
+                               "<<<<<<< OURS\n1master\n=======\n1topic\n>>>>>>> THEIRS\n2\n3\n4\n");
+
+               assertEquals(RepositoryState.REBASING_MERGE, db.getRepositoryState());
+               // the first one should be included, so we should have left two picks in
+               // the file
+               assertEquals(countPicks(), 2);
+               // abort should reset to topic branch
+               res = git.rebase().setOperation(Operation.ABORT).call();
+               assertEquals(res.getStatus(), Status.ABORTED);
+               assertEquals("refs/heads/topic", db.getFullBranch());
+               checkFile(theFile, "1topic\n2topic\n3\n4\n");
+               RevWalk rw = new RevWalk(db);
+               assertEquals(lastTopicCommit, rw
+                               .parseCommit(db.resolve(Constants.HEAD)));
+       }
+
+       private int countPicks() throws IOException {
+               int count = 0;
+               File todoFile = new File(db.getDirectory(),
+                               "rebase-merge/git-rebase-todo");
+               BufferedReader br = new BufferedReader(new InputStreamReader(
+                               new FileInputStream(todoFile), "UTF-8"));
+               try {
+                       String line = br.readLine();
+                       while (line != null) {
+                               if (line.startsWith("pick "))
+                                       count++;
+                               line = br.readLine();
+                       }
+                       return count;
+               } finally {
+                       br.close();
+               }
+       }
+}
index ab4ec6132599c56e06b2f33b314b61b558c700ac..e05500e0a57115b9b799302aeedc69cbc8d63ce6 100644 (file)
@@ -8,6 +8,7 @@ URINotSupported=URI not supported: {0}
 URLNotFound={0} not found
 aNewObjectIdIsRequired=A NewObjectId is required.
 abbreviationLengthMustBeNonNegative=Abbreviation length must not be negative.
+abortingRebase=Aborting rebase: resetting to {0}
 advertisementCameBefore=advertisement of {0}^{} came before {1}
 advertisementOfCameBefore=advertisement of {0}^{} came before {1}
 amazonS3ActionFailed={0} of '{1}' failed: {2} {3}
@@ -15,6 +16,7 @@ amazonS3ActionFailedGivingUp={0} of '{1}' failed: Giving up after {2} attempts.
 ambiguousObjectAbbreviation=Object abbreviation {0} is ambiguous
 anExceptionOccurredWhileTryingToAddTheIdOfHEAD=An exception occurred while trying to add the Id of HEAD
 anSSHSessionHasBeenAlreadyCreated=An SSH session has been already created
+applyingCommit=Applying {0}
 atLeastOnePathIsRequired=At least one path is required.
 atLeastOnePatternIsRequired=At least one pattern is required.
 atLeastTwoFiltersNeeded=At least two filters needed.
@@ -258,6 +260,7 @@ missingDeltaBase=delta base
 missingForwardImageInGITBinaryPatch=Missing forward-image in GIT binary patch
 missingObject=Missing {0} {1}
 missingPrerequisiteCommits=missing prerequisite commits:
+missingRequiredParameter=Parameter "{0}" is missing
 missingSecretkey=Missing secretkey.
 mixedStagesNotAllowed=Mixed stages not allowed
 multipleMergeBasesFor=Multiple merge bases for:\n  {0}\n  {1} found:\n  {2}\n  {3}
@@ -292,6 +295,7 @@ objectAtPathDoesNotHaveId=Object at path "{0}" does not have an id assigned. All
 objectIsCorrupt=Object {0} is corrupt: {1}
 objectIsNotA=Object {0} is not a {1}.
 objectNotFoundIn=Object {0} not found in {1}.
+obtainingCommitsForCherryPick=Obtaining commits that need to be cherry-picked
 offsetWrittenDeltaBaseForObjectNotFoundInAPack=Offset-written delta base for object not found in a pack
 onlyAlreadyUpToDateAndFastForwardMergesAreAvailable=only already-up-to-date and fast forward merges are available
 onlyOneFetchSupported=Only one fetch supported
@@ -359,7 +363,9 @@ repositoryState_rebaseOrApplyMailbox=Rebase/Apply mailbox
 repositoryState_rebaseWithMerge=Rebase w/merge
 requiredHashFunctionNotAvailable=Required hash function {0} not available.
 resolvingDeltas=Resolving deltas
+resettingHead=Resetting head to {0}
 resultLengthIncorrect=result length incorrect
+rewinding=Rewinding to commit {0}
 searchForReuse=Finding sources
 sequenceTooLargeForDiffAlgorithm=Sequence too large for difference algorithm.
 serviceNotPermitted={0} not permitted
@@ -436,3 +442,4 @@ writingNotPermitted=Writing not permitted
 writingNotSupported=Writing {0} not supported.
 writingObjects=Writing objects
 wrongDecompressedLength=wrong decompressed length
+wrongRepositoryState=Wrong Repository State: {0}
index 2eb316e85ce5601fcc6a01e1c86f842102ce2926..9c4717a814d1a4a3ac653e5371bc6ccc478a92e3 100644 (file)
@@ -68,12 +68,14 @@ public class JGitText extends TranslationBundle {
        /***/ public String URLNotFound;
        /***/ public String aNewObjectIdIsRequired;
        /***/ public String abbreviationLengthMustBeNonNegative;
+       /***/ public String abortingRebase;
        /***/ public String advertisementCameBefore;
        /***/ public String advertisementOfCameBefore;
        /***/ public String amazonS3ActionFailed;
        /***/ public String amazonS3ActionFailedGivingUp;
        /***/ public String ambiguousObjectAbbreviation;
        /***/ public String anExceptionOccurredWhileTryingToAddTheIdOfHEAD;
+       /***/ public String applyingCommit;
        /***/ public String anSSHSessionHasBeenAlreadyCreated;
        /***/ public String atLeastOnePathIsRequired;
        /***/ public String atLeastOnePatternIsRequired;
@@ -318,6 +320,7 @@ public class JGitText extends TranslationBundle {
        /***/ public String missingForwardImageInGITBinaryPatch;
        /***/ public String missingObject;
        /***/ public String missingPrerequisiteCommits;
+       /***/ public String missingRequiredParameter;
        /***/ public String missingSecretkey;
        /***/ public String mixedStagesNotAllowed;
        /***/ public String multipleMergeBasesFor;
@@ -352,6 +355,7 @@ public class JGitText extends TranslationBundle {
        /***/ public String objectIsCorrupt;
        /***/ public String objectIsNotA;
        /***/ public String objectNotFoundIn;
+       /***/ public String obtainingCommitsForCherryPick;
        /***/ public String offsetWrittenDeltaBaseForObjectNotFoundInAPack;
        /***/ public String onlyAlreadyUpToDateAndFastForwardMergesAreAvailable;
        /***/ public String onlyOneFetchSupported;
@@ -418,8 +422,10 @@ public class JGitText extends TranslationBundle {
        /***/ public String repositoryState_rebaseOrApplyMailbox;
        /***/ public String repositoryState_rebaseWithMerge;
        /***/ public String requiredHashFunctionNotAvailable;
+       /***/ public String resettingHead;      
        /***/ public String resolvingDeltas;
        /***/ public String resultLengthIncorrect;
+       /***/ public String rewinding;
        /***/ public String searchForReuse;
        /***/ public String sequenceTooLargeForDiffAlgorithm;
        /***/ public String serviceNotPermitted;
@@ -496,4 +502,5 @@ public class JGitText extends TranslationBundle {
        /***/ public String writingNotSupported;
        /***/ public String writingObjects;
        /***/ public String wrongDecompressedLength;
+       /***/ public String wrongRepositoryState;
 }
index 9b6221097ae5d7140d42769bea56fed4c1f2d14a..2ed173ada1efa67042b3805be2d9572ef062941a 100644 (file)
@@ -243,6 +243,19 @@ public class Git {
                return new CherryPickCommand(repo);
        }
 
+       /**
+        * Returns a command object to execute a {@code Rebase} command
+        *
+        * @see <a
+        *      href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html"
+        *      >Git documentation about rebase</a>
+        * @return a {@link RebaseCommand} used to collect all optional parameters
+        *         and to finally execute the {@code rebase} command
+        */
+       public RebaseCommand rebase() {
+               return new RebaseCommand(repo);
+       }
+
        /**
         * @return the git repository this class is interacting with
         */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
new file mode 100644 (file)
index 0000000..bda7f26
--- /dev/null
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.JGitText;
+import org.eclipse.jgit.api.RebaseResult.Status;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.NoHeadException;
+import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * A class used to execute a {@code Rebase} command. It has setters for all
+ * supported options and arguments of this command and a {@link #call()} method
+ * to finally execute the command. Each instance of this class should only be
+ * used for one invocation of the command (means: one call to {@link #call()})
+ * <p>
+ *
+ * @see <a
+ *      href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html"
+ *      >Git documentation about Rebase</a>
+ */
+public class RebaseCommand extends GitCommand<RebaseResult> {
+       /**
+        * The available operations
+        */
+       public enum Operation {
+               /**
+                * Initiates rebase
+                */
+               BEGIN,
+               /**
+                * Continues after a conflict resolution
+                */
+               CONTINUE,
+               /**
+                * Skips the "current" commit
+                */
+               SKIP,
+               /**
+                * Aborts and resets the current rebase
+                */
+               ABORT;
+       }
+
+       private Operation operation = Operation.BEGIN;
+
+       private RevCommit upstreamCommit;
+
+       private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
+
+       private final RevWalk walk;
+
+       private final File rebaseDir;
+
+       /**
+        * @param repo
+        */
+       protected RebaseCommand(Repository repo) {
+               super(repo);
+               walk = new RevWalk(repo);
+               rebaseDir = new File(repo.getDirectory(), "rebase-merge");
+       }
+
+       /**
+        * Executes the {@code Rebase} command with all the options and parameters
+        * collected by the setter methods of this class. Each instance of this
+        * class should only be used for one invocation of the command. Don't call
+        * this method twice on an instance.
+        *
+        * @return an object describing the result of this command
+        */
+       public RebaseResult call() throws NoHeadException, RefNotFoundException,
+                       JGitInternalException, GitAPIException {
+               checkCallable();
+               checkParameters();
+               try {
+                       switch (operation) {
+                       case ABORT:
+                               try {
+                                       return abort();
+                               } catch (IOException ioe) {
+                                       throw new JGitInternalException(ioe.getMessage(), ioe);
+                               }
+                       case SKIP:
+                               // fall through
+                       case CONTINUE:
+                               String upstreamCommitName = readFile(rebaseDir, "onto");
+                               this.upstreamCommit = walk.parseCommit(repo
+                                               .resolve(upstreamCommitName));
+                               break;
+                       case BEGIN:
+                               RebaseResult res = initFilesAndRewind();
+                               if (res != null)
+                                       return res;
+                       }
+
+                       if (monitor.isCancelled())
+                               return abort();
+
+                       if (this.operation == Operation.CONTINUE)
+                               throw new UnsupportedOperationException(
+                                               "--continue Not yet implemented");
+
+                       if (this.operation == Operation.SKIP)
+                               throw new UnsupportedOperationException(
+                                               "--skip Not yet implemented");
+
+                       RevCommit newHead = null;
+
+                       List<Step> steps = loadSteps();
+                       ObjectReader or = repo.newObjectReader();
+                       int stepsToPop = 0;
+
+                       for (Step step : steps) {
+                               if (step.action != Action.PICK)
+                                       continue;
+                               Collection<ObjectId> ids = or.resolve(step.commit);
+                               if (ids.size() != 1)
+                                       throw new JGitInternalException(
+                                                       "Could not resolve uniquely the abbreviated object ID");
+                               RevCommit commitToPick = walk
+                                               .parseCommit(ids.iterator().next());
+                               if (monitor.isCancelled())
+                                       return new RebaseResult(commitToPick);
+                               monitor.beginTask(MessageFormat.format(
+                                               JGitText.get().applyingCommit, commitToPick
+                                                               .getShortMessage()), ProgressMonitor.UNKNOWN);
+                               // TODO if the first parent of commitToPick is the current HEAD,
+                               // we should fast-forward instead of cherry-pick to avoid
+                               // unnecessary object rewriting
+                               newHead = new Git(repo).cherryPick().include(commitToPick)
+                                               .call();
+                               monitor.endTask();
+                               if (newHead == null) {
+                                       popSteps(stepsToPop);
+                                       return new RebaseResult(commitToPick);
+                               }
+                               stepsToPop++;
+                       }
+                       if (newHead != null) {
+                               // point the previous head (if any) to the new commit
+                               String headName = readFile(rebaseDir, "head-name");
+                               if (headName.startsWith(Constants.R_REFS)) {
+                                       RefUpdate rup = repo.updateRef(headName);
+                                       rup.setNewObjectId(newHead);
+                                       rup.forceUpdate();
+                                       rup = repo.updateRef(Constants.HEAD);
+                                       rup.link(headName);
+                               }
+                               deleteRecursive(rebaseDir);
+                               return new RebaseResult(Status.OK);
+                       }
+                       return new RebaseResult(Status.UP_TO_DATE);
+               } catch (IOException ioe) {
+                       throw new JGitInternalException(ioe.getMessage(), ioe);
+               }
+       }
+
+       /**
+        * Removes the number of lines given in the parameter from the
+        * <code>git-rebase-todo</code> file but preserves comments and other lines
+        * that can not be parsed as steps
+        *
+        * @param numSteps
+        * @throws IOException
+        */
+       private void popSteps(int numSteps) throws IOException {
+               if (numSteps == 0)
+                       return;
+               List<String> lines = new ArrayList<String>();
+               File file = new File(rebaseDir, "git-rebase-todo");
+               BufferedReader br = new BufferedReader(new InputStreamReader(
+                               new FileInputStream(file), "UTF-8"));
+               int popped = 0;
+               try {
+                       // check if the line starts with a action tag (pick, skip...)
+                       while (popped < numSteps) {
+                               String popCandidate = br.readLine();
+                               if (popCandidate == null)
+                                       break;
+                               int spaceIndex = popCandidate.indexOf(' ');
+                               boolean pop = false;
+                               if (spaceIndex >= 0) {
+                                       String actionToken = popCandidate.substring(0, spaceIndex);
+                                       pop = Action.parse(actionToken) != null;
+                               }
+                               if (pop)
+                                       popped++;
+                               else
+                                       lines.add(popCandidate);
+                       }
+                       String readLine = br.readLine();
+                       while (readLine != null) {
+                               lines.add(readLine);
+                               readLine = br.readLine();
+                       }
+               } finally {
+                       br.close();
+               }
+
+               BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
+                               new FileOutputStream(file), "UTF-8"));
+               try {
+                       for (String writeLine : lines) {
+                               bw.write(writeLine);
+                               bw.newLine();
+                       }
+               } finally {
+                       bw.close();
+               }
+       }
+
+       private RebaseResult initFilesAndRewind() throws RefNotFoundException,
+                       IOException, NoHeadException, JGitInternalException {
+               // we need to store everything into files so that we can implement
+               // --skip, --continue, and --abort
+
+               // first of all, we determine the commits to be applied
+               List<RevCommit> cherryPickList = new ArrayList<RevCommit>();
+
+               Ref head = repo.getRef(Constants.HEAD);
+               if (head == null || head.getObjectId() == null)
+                       throw new RefNotFoundException(MessageFormat.format(
+                                       JGitText.get().refNotResolved, Constants.HEAD));
+
+               String headName;
+               if (head.isSymbolic())
+                       headName = head.getTarget().getName();
+               else
+                       headName = "detached HEAD";
+               ObjectId headId = head.getObjectId();
+               if (headId == null)
+                       throw new RefNotFoundException(MessageFormat.format(
+                                       JGitText.get().refNotResolved, Constants.HEAD));
+               RevCommit headCommit = walk.lookupCommit(headId);
+               monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick,
+                               ProgressMonitor.UNKNOWN);
+               LogCommand cmd = new Git(repo).log().addRange(upstreamCommit,
+                               headCommit);
+               Iterable<RevCommit> commitsToUse = cmd.call();
+               for (RevCommit commit : commitsToUse) {
+                       cherryPickList.add(commit);
+               }
+
+               // nothing to do: return with UP_TO_DATE_RESULT
+               if (cherryPickList.isEmpty())
+                       return RebaseResult.UP_TO_DATE_RESULT;
+
+               Collections.reverse(cherryPickList);
+               // create the folder for the meta information
+               rebaseDir.mkdir();
+
+               createFile(repo.getDirectory(), "ORIG_HEAD", headId.name());
+               createFile(rebaseDir, "head", headId.name());
+               createFile(rebaseDir, "head-name", headName);
+               createFile(rebaseDir, "onto", upstreamCommit.name());
+               BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(
+                               new FileOutputStream(new File(rebaseDir, "git-rebase-todo")),
+                               "UTF-8"));
+               fw.write("# Created by EGit: rebasing " + upstreamCommit.name()
+                               + " onto " + headId.name());
+               fw.newLine();
+               try {
+                       StringBuilder sb = new StringBuilder();
+                       ObjectReader reader = walk.getObjectReader();
+                       for (RevCommit commit : cherryPickList) {
+                               sb.setLength(0);
+                               sb.append(Action.PICK.toToken());
+                               sb.append(" ");
+                               sb.append(reader.abbreviate(commit).name());
+                               sb.append(" ");
+                               sb.append(commit.getShortMessage());
+                               fw.write(sb.toString());
+                               fw.newLine();
+                       }
+               } finally {
+                       fw.close();
+               }
+
+               monitor.endTask();
+               // we rewind to the upstream commit
+               monitor.beginTask(MessageFormat.format(JGitText.get().rewinding,
+                               upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN);
+               checkoutCommit(upstreamCommit);
+               monitor.endTask();
+               return null;
+       }
+
+       private void checkParameters() throws WrongRepositoryStateException {
+               if (this.operation != Operation.BEGIN) {
+                       // these operations are only possible while in a rebasing state
+                       switch (repo.getRepositoryState()) {
+                       case REBASING:
+                               // fall through
+                       case REBASING_INTERACTIVE:
+                               // fall through
+                       case REBASING_MERGE:
+                               // fall through
+                       case REBASING_REBASING:
+                               break;
+                       default:
+                               throw new WrongRepositoryStateException(MessageFormat.format(
+                                               JGitText.get().wrongRepositoryState, repo
+                                                               .getRepositoryState().name()));
+                       }
+               } else
+                       switch (repo.getRepositoryState()) {
+                       case SAFE:
+                               if (this.upstreamCommit == null)
+                                       throw new JGitInternalException(MessageFormat
+                                                       .format(JGitText.get().missingRequiredParameter,
+                                                                       "upstream"));
+                               return;
+                       default:
+                               throw new WrongRepositoryStateException(MessageFormat.format(
+                                               JGitText.get().wrongRepositoryState, repo
+                                                               .getRepositoryState().name()));
+
+                       }
+       }
+
+       private void createFile(File parentDir, String name, String content)
+                       throws IOException {
+               File file = new File(parentDir, name);
+               FileOutputStream fos = new FileOutputStream(file);
+               try {
+                       fos.write(content.getBytes("UTF-8"));
+               } finally {
+                       fos.close();
+               }
+       }
+
+       private RebaseResult abort() throws IOException {
+               try {
+                       String commitId = readFile(repo.getDirectory(), "ORIG_HEAD");
+                       monitor.beginTask(MessageFormat.format(
+                                       JGitText.get().abortingRebase, commitId),
+                                       ProgressMonitor.UNKNOWN);
+
+                       RevCommit commit = walk.parseCommit(repo.resolve(commitId));
+                       // no head in order to reset --hard
+                       DirCacheCheckout dco = new DirCacheCheckout(repo, repo
+                                       .lockDirCache(), commit.getTree());
+                       dco.setFailOnConflict(false);
+                       dco.checkout();
+                       walk.release();
+               } finally {
+                       monitor.endTask();
+               }
+               try {
+                       String headName = readFile(rebaseDir, "head-name");
+                       if (headName.startsWith(Constants.R_REFS)) {
+                               monitor.beginTask(MessageFormat.format(
+                                               JGitText.get().resettingHead, headName),
+                                               ProgressMonitor.UNKNOWN);
+
+                               // update the HEAD
+                               RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false);
+                               Result res = refUpdate.link(headName);
+                               switch (res) {
+                               case FAST_FORWARD:
+                               case FORCED:
+                               case NO_CHANGE:
+                                       break;
+                               default:
+                                       throw new IOException("Could not abort rebase");
+                               }
+                       }
+                       // cleanup the files
+                       deleteRecursive(rebaseDir);
+                       return new RebaseResult(Status.ABORTED);
+
+               } finally {
+                       monitor.endTask();
+               }
+       }
+
+       private void deleteRecursive(File fileOrFolder) throws IOException {
+               File[] children = fileOrFolder.listFiles();
+               if (children != null) {
+                       for (File child : children)
+                               deleteRecursive(child);
+               }
+               if (!fileOrFolder.delete())
+                       throw new IOException("Could not delete " + fileOrFolder.getPath());
+       }
+
+       private String readFile(File directory, String fileName) throws IOException {
+               return RawParseUtils
+                               .decode(IO.readFully(new File(directory, fileName)));
+       }
+
+       private void checkoutCommit(RevCommit commit) throws IOException {
+               try {
+                       RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD));
+                       DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(),
+                                       repo.lockDirCache(), commit.getTree());
+                       dco.setFailOnConflict(true);
+                       dco.checkout();
+                       // update the HEAD
+                       RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true);
+                       refUpdate.setExpectedOldObjectId(head);
+                       refUpdate.setNewObjectId(commit);
+                       Result res = refUpdate.forceUpdate();
+                       switch (res) {
+                       case FAST_FORWARD:
+                       case NO_CHANGE:
+                       case FORCED:
+                               break;
+                       default:
+                               throw new IOException("Could not rewind to upstream commit");
+                       }
+               } finally {
+                       walk.release();
+                       monitor.endTask();
+               }
+       }
+
+       private List<Step> loadSteps() throws IOException {
+               byte[] buf = IO.readFully(new File(rebaseDir, "git-rebase-todo"));
+               int ptr = 0;
+               int tokenBegin = 0;
+               ArrayList<Step> r = new ArrayList<Step>();
+               while (ptr < buf.length) {
+                       tokenBegin = ptr;
+                       ptr = RawParseUtils.nextLF(buf, ptr);
+                       int nextSpace = 0;
+                       int tokenCount = 0;
+                       Step current = null;
+                       while (tokenCount < 3 && nextSpace < ptr) {
+                               switch (tokenCount) {
+                               case 0:
+                                       nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
+                                       String actionToken = new String(buf, tokenBegin, nextSpace
+                                                       - tokenBegin - 1);
+                                       tokenBegin = nextSpace;
+                                       Action action = Action.parse(actionToken);
+                                       if (action != null)
+                                               current = new Step(Action.parse(actionToken));
+                                       break;
+                               case 1:
+                                       if (current == null)
+                                               break;
+                                       nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
+                                       String commitToken = new String(buf, tokenBegin, nextSpace
+                                                       - tokenBegin - 1);
+                                       tokenBegin = nextSpace;
+                                       current.commit = AbbreviatedObjectId
+                                                       .fromString(commitToken);
+                                       break;
+                               case 2:
+                                       if (current == null)
+                                               break;
+                                       nextSpace = ptr;
+                                       int length = ptr - tokenBegin;
+                                       current.shortMessage = new byte[length];
+                                       System.arraycopy(buf, tokenBegin, current.shortMessage, 0,
+                                                       length);
+                                       r.add(current);
+                                       break;
+                               }
+                               tokenCount++;
+                       }
+               }
+               return r;
+       }
+
+       /**
+        * @param upstream
+        *            the upstream commit
+        * @return {@code this}
+        */
+       public RebaseCommand setUpstream(RevCommit upstream) {
+               this.upstreamCommit = upstream;
+               return this;
+       }
+
+       /**
+        * @param upstream
+        *            the upstream branch
+        * @return {@code this}
+        * @throws RefNotFoundException
+        */
+       public RebaseCommand setUpstream(String upstream)
+                       throws RefNotFoundException {
+               try {
+                       ObjectId upstreamId = repo.resolve(upstream);
+                       if (upstreamId == null)
+                               throw new RefNotFoundException(MessageFormat.format(JGitText
+                                               .get().refNotResolved, upstream));
+                       upstreamCommit = walk.parseCommit(repo.resolve(upstream));
+                       return this;
+               } catch (IOException ioe) {
+                       throw new JGitInternalException(ioe.getMessage(), ioe);
+               }
+       }
+
+       /**
+        * @param operation
+        *            the operation to perform
+        * @return {@code this}
+        */
+       public RebaseCommand setOperation(Operation operation) {
+               this.operation = operation;
+               return this;
+       }
+
+       /**
+        * @param monitor
+        *            a progress monitor
+        * @return this instance
+        */
+       public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
+               this.monitor = monitor;
+               return this;
+       }
+
+       static enum Action {
+               PICK("pick"); // later add SQUASH, EDIT, etc.
+
+               private final String token;
+
+               private Action(String token) {
+                       this.token = token;
+               }
+
+               public String toToken() {
+                       return this.token;
+               }
+
+               static Action parse(String token) {
+                       if (token.equals("pick") || token.equals("p"))
+                               return PICK;
+                       return null;
+               }
+       }
+
+       static class Step {
+               Action action;
+
+               AbbreviatedObjectId commit;
+
+               byte[] shortMessage;
+
+               Step(Action action) {
+                       this.action = action;
+               }
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
new file mode 100644 (file)
index 0000000..bdbddda
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * The result of a {@link RebaseCommand} execution
+ */
+public class RebaseResult {
+       /**
+        * The overall status
+        */
+       public enum Status {
+               /**
+                * Rebase was successful, HEAD points to the new commit
+                */
+               OK,
+               /**
+                * Aborted; the original HEAD was restored
+                */
+               ABORTED,
+               /**
+                * Stopped due to a conflict; must either abort or resolve or skip
+                */
+               STOPPED,
+               /**
+                * Already up-to-date
+                */
+               UP_TO_DATE;
+       }
+
+       static final RebaseResult UP_TO_DATE_RESULT = new RebaseResult(
+                       Status.UP_TO_DATE);
+
+       private final Status mySatus;
+
+       private final RevCommit currentCommit;
+
+       RebaseResult(Status status) {
+               this.mySatus = status;
+               currentCommit = null;
+       }
+
+       RebaseResult(RevCommit commit) {
+               this.mySatus = Status.STOPPED;
+               currentCommit = commit;
+       }
+
+       /**
+        * @return the overall status
+        */
+       public Status getStatus() {
+               return mySatus;
+       }
+
+       /**
+        * @return the current commit if status is {@link Status#STOPPED}, otherwise
+        *         <code>null</code>
+        */
+       public RevCommit getCurrentCommit() {
+               return currentCommit;
+       }
+}