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>tags/v0.10.1
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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} |
@@ -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; | |||
} |
@@ -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 | |||
*/ |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |