Просмотр исходного кода

Initial implementation of a Rebase command

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
Mathias Kinzler 13 лет назад
Родитель
Сommit
e5b96a7848

+ 265
- 0
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();
}
}
}

+ 7
- 0
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}

+ 7
- 0
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;
}

+ 13
- 0
org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java Просмотреть файл

@@ -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
*/

+ 620
- 0
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;
}
}
}

+ 104
- 0
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;
}
}

Загрузка…
Отмена
Сохранить