summaryrefslogtreecommitdiffstatsabout
diff options
context:
space:
mode:
authorMathias Kinzler2010-11-22 10:26:00 (EST)
committer Chris Aniszczyk2010-11-22 10:58:36 (EST)
commite5b96a7848d680cf50123a44cbc147db91d798d3 (patch)
tree6ce0488c869ebcb9248f3f1cdb90544b88d3645c
parentbd98a0a9a52973704467cda892e99711524de48b (diff)
downloadjgit-e5b96a7848d680cf50123a44cbc147db91d798d3.zip
jgit-e5b96a7848d680cf50123a44cbc147db91d798d3.tar.gz
jgit-e5b96a7848d680cf50123a44cbc147db91d798d3.tar.bz2
Initial implementation of a Rebase commandrefs/changes/64/1864/10
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>
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java265
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java620
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java104
6 files changed, 1016 insertions, 0 deletions
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
index 0000000..aee2cc4
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
@@ -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();
+ }
+ }
+}
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
index ab4ec61..e05500e 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
@@ -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}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
index 2eb316e..9c4717a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
@@ -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;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
index 9b62210..2ed173a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -244,6 +244,19 @@ public class Git {
}
/**
+ * 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
*/
public Repository getRepository() {
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
index 0000000..bda7f26
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -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
index 0000000..bdbddda
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java
@@ -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;
+ }
+}