Bug: 394432 Change-Id: I373128c0ba949f9b24248874f77f3d68b50ccfd1 Signed-off-by: Chris Aniszczyk <zx@twitter.com>tags/v2.2.0.201212191850-r
@@ -42,8 +42,8 @@ | |||
*/ | |||
package org.eclipse.jgit.pgm; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertArrayEquals; | |||
import static org.junit.Assert.assertEquals; | |||
import org.eclipse.jgit.api.Git; | |||
import org.eclipse.jgit.lib.CLIRepositoryTestCase; | |||
@@ -82,7 +82,8 @@ public class MergeTest extends CLIRepositoryTestCase { | |||
git.commit().setMessage("commit").call(); | |||
git.checkout().setName("side").call(); | |||
assertEquals("Fast-forward", execute("git merge master")[0]); | |||
assertArrayEquals(new String[] { "Updating 6fd41be..26a81a1", | |||
"Fast-forward", "" }, execute("git merge master")); | |||
} | |||
@Test | |||
@@ -120,4 +121,58 @@ public class MergeTest extends CLIRepositoryTestCase { | |||
"" }, | |||
execute("git merge master --squash")); | |||
} | |||
@Test | |||
public void testNoFastForward() throws Exception { | |||
git.branchCreate().setName("side").call(); | |||
writeTrashFile("file", "master"); | |||
git.add().addFilepattern("file").call(); | |||
git.commit().setMessage("commit").call(); | |||
git.checkout().setName("side").call(); | |||
assertEquals("Merge made by the 'recursive' strategy.", | |||
execute("git merge master --no-ff")[0]); | |||
assertArrayEquals(new String[] { | |||
"commit 6db23724012376e8407fc24b5da4277a9601be81", // | |||
"Author: GIT_COMMITTER_NAME <GIT_COMMITTER_EMAIL>", // | |||
"Date: Sat Aug 15 20:12:58 2009 -0330", // | |||
"", // | |||
" Merge branch 'master' into side", // | |||
"", // | |||
"commit 6fd41be26b7ee41584dd997f665deb92b6c4c004", // | |||
"Author: GIT_COMMITTER_NAME <GIT_COMMITTER_EMAIL>", // | |||
"Date: Sat Aug 15 20:12:58 2009 -0330", // | |||
"", // | |||
" initial commit", // | |||
"", // | |||
"commit 26a81a1c6a105551ba703a8b6afc23994cacbae1", // | |||
"Author: GIT_COMMITTER_NAME <GIT_COMMITTER_EMAIL>", // | |||
"Date: Sat Aug 15 20:12:58 2009 -0330", // | |||
"", // | |||
" commit", // | |||
"", // | |||
"" | |||
}, execute("git log")); | |||
} | |||
@Test | |||
public void testNoFastForwardAndSquash() throws Exception { | |||
assertEquals("fatal: You cannot combine --squash with --no-ff.", | |||
execute("git merge master --no-ff --squash")[0]); | |||
} | |||
@Test | |||
public void testFastForwardOnly() throws Exception { | |||
git.branchCreate().setName("side").call(); | |||
writeTrashFile("file", "master"); | |||
git.add().addFilepattern("file").call(); | |||
git.commit().setMessage("commit#1").call(); | |||
git.checkout().setName("side").call(); | |||
writeTrashFile("file", "side"); | |||
git.add().addFilepattern("file").call(); | |||
git.commit().setMessage("commit#2").call(); | |||
assertEquals("fatal: Not possible to fast-forward, aborting.", | |||
execute("git merge master --ff-only")[0]); | |||
} | |||
} |
@@ -18,6 +18,7 @@ branchNotFound=branch '{0}' not found. | |||
cacheTreePathInfo="{0}": {1} entries, {2} children | |||
cannotBeRenamed={0} cannot be renamed | |||
cannotChekoutNoHeadsAdvertisedByRemote=cannot checkout; no HEAD advertised by remote | |||
cannotCombineSquashWithNoff=You cannot combine --squash with --no-ff. | |||
cannotCreateCommand=Cannot create command {0} | |||
cannotCreateOutputStream=cannot create output stream | |||
cannotDeatchHEAD=Cannot detach HEAD | |||
@@ -55,6 +56,7 @@ failedToLockTag=Failed to lock tag {0}: {1} | |||
fatalError=fatal: {0} | |||
fatalThisProgramWillDestroyTheRepository=fatal: This program will destroy the repository\nfatal:\nfatal:\nfatal: {0}\nfatal:\nfatal: To continue, add {1} to the command line\nfatal: | |||
fileIsRequired=argument file is required | |||
ffNotPossibleAborting=Not possible to fast-forward, aborting. | |||
forcedUpdate=forced update | |||
fromURI=From {0} | |||
initializedEmptyGitRepositoryIn=Initialized empty Git repository in {0} | |||
@@ -160,6 +162,7 @@ unknownMergeStrategy=unknown merge strategy {0} specified | |||
unmergedPaths=Unmerged paths: | |||
unsupportedOperation=Unsupported operation: {0} | |||
untrackedFiles=Untracked files: | |||
updating=Updating {0}..{1} | |||
usage_Blame=Show what revision and author last modified each line | |||
usage_CommandLineClientForamazonsS3Service=Command line client for Amazon's S3 service | |||
usage_CommitAll=commit all modified and deleted files |
@@ -86,6 +86,7 @@ public class CLIText extends TranslationBundle { | |||
/***/ public String configFileNotFound; | |||
/***/ public String cannotBeRenamed; | |||
/***/ public String cannotChekoutNoHeadsAdvertisedByRemote; | |||
/***/ public String cannotCombineSquashWithNoff; | |||
/***/ public String cannotCreateCommand; | |||
/***/ public String cannotCreateOutputStream; | |||
/***/ public String cannotDeatchHEAD; | |||
@@ -122,6 +123,7 @@ public class CLIText extends TranslationBundle { | |||
/***/ public String fatalError; | |||
/***/ public String fatalThisProgramWillDestroyTheRepository; | |||
/***/ public String fileIsRequired; | |||
/***/ public String ffNotPossibleAborting; | |||
/***/ public String forcedUpdate; | |||
/***/ public String fromURI; | |||
/***/ public String initializedEmptyGitRepositoryIn; | |||
@@ -221,5 +223,6 @@ public class CLIText extends TranslationBundle { | |||
/***/ public String unmergedPaths; | |||
/***/ public String unsupportedOperation; | |||
/***/ public String untrackedFiles; | |||
/***/ public String updating; | |||
/***/ public String warningNoCommitGivenOnCommandLine; | |||
} |
@@ -43,14 +43,22 @@ | |||
package org.eclipse.jgit.pgm; | |||
import java.io.IOException; | |||
import java.text.MessageFormat; | |||
import java.util.Map; | |||
import org.eclipse.jgit.api.Git; | |||
import org.eclipse.jgit.api.MergeCommand; | |||
import org.eclipse.jgit.api.MergeResult; | |||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode; | |||
import org.eclipse.jgit.lib.AnyObjectId; | |||
import org.eclipse.jgit.lib.Constants; | |||
import org.eclipse.jgit.lib.ObjectId; | |||
import org.eclipse.jgit.lib.Ref; | |||
import org.eclipse.jgit.merge.MergeStrategy; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.revwalk.RevWalk; | |||
import org.kohsuke.args4j.Argument; | |||
import org.kohsuke.args4j.Option; | |||
@@ -68,8 +76,23 @@ class Merge extends TextBuiltin { | |||
@Argument(required = true) | |||
private String ref; | |||
@Option(name = "--ff") | |||
private FastForwardMode ff = FastForwardMode.FF; | |||
@Option(name = "--no-ff") | |||
void noff(@SuppressWarnings("unused") final boolean ignored) { | |||
ff = FastForwardMode.NO_FF; | |||
} | |||
@Option(name = "--ff-only") | |||
void ffonly(@SuppressWarnings("unused") final boolean ignored) { | |||
ff = FastForwardMode.FF_ONLY; | |||
} | |||
@Override | |||
protected void run() throws Exception { | |||
if (squash && ff == FastForwardMode.NO_FF) | |||
throw die(CLIText.get().cannotCombineSquashWithNoff); | |||
// determine the merge strategy | |||
if (strategyName != null) { | |||
mergeStrategy = MergeStrategy.get(strategyName); | |||
@@ -79,14 +102,21 @@ class Merge extends TextBuiltin { | |||
} | |||
// determine the other revision we want to merge with HEAD | |||
final Ref srcRef = db.getRef(ref); | |||
final ObjectId src = db.resolve(ref + "^{commit}"); | |||
if (src == null) | |||
throw die(MessageFormat.format( | |||
CLIText.get().refDoesNotExistOrNoCommit, ref)); | |||
Ref oldHead = db.getRef(Constants.HEAD); | |||
Git git = new Git(db); | |||
MergeResult result = git.merge().setStrategy(mergeStrategy) | |||
.setSquash(squash).include(src).call(); | |||
MergeCommand mergeCmd = git.merge().setStrategy(mergeStrategy) | |||
.setSquash(squash).setFastForward(ff); | |||
if (srcRef != null) | |||
mergeCmd.include(srcRef); | |||
else | |||
mergeCmd.include(src); | |||
MergeResult result = mergeCmd.call(); | |||
switch (result.getMergeStatus()) { | |||
case ALREADY_UP_TO_DATE: | |||
@@ -95,6 +125,10 @@ class Merge extends TextBuiltin { | |||
outw.println(CLIText.get().alreadyUpToDate); | |||
break; | |||
case FAST_FORWARD: | |||
ObjectId oldHeadId = oldHead.getObjectId(); | |||
outw.println(MessageFormat.format(CLIText.get().updating, oldHeadId | |||
.abbreviate(7).name(), result.getNewHead().abbreviate(7) | |||
.name())); | |||
outw.println(result.getMergeStatus().toString()); | |||
break; | |||
case CONFLICTING: | |||
@@ -119,15 +153,32 @@ class Merge extends TextBuiltin { | |||
} | |||
break; | |||
case MERGED: | |||
outw.println(MessageFormat.format(CLIText.get().mergeMadeBy, | |||
mergeStrategy.getName())); | |||
String name; | |||
if (!isMergedInto(oldHead, src)) | |||
name = mergeStrategy.getName(); | |||
else | |||
name = "recursive"; | |||
outw.println(MessageFormat.format(CLIText.get().mergeMadeBy, name)); | |||
break; | |||
case MERGED_SQUASHED: | |||
outw.println(CLIText.get().mergedSquashed); | |||
break; | |||
case ABORTED: | |||
throw die(CLIText.get().ffNotPossibleAborting); | |||
case NOT_SUPPORTED: | |||
outw.println(MessageFormat.format( | |||
CLIText.get().unsupportedOperation, result.toString())); | |||
} | |||
} | |||
private boolean isMergedInto(Ref oldHead, AnyObjectId src) | |||
throws IOException { | |||
RevWalk revWalk = new RevWalk(db); | |||
ObjectId oldHeadObjectId = oldHead.getPeeledObjectId(); | |||
if (oldHeadObjectId == null) | |||
oldHeadObjectId = oldHead.getObjectId(); | |||
RevCommit oldHeadCommit = revWalk.lookupCommit(oldHeadObjectId); | |||
RevCommit srcCommit = revWalk.lookupCommit(src); | |||
return revWalk.isMergedInto(oldHeadCommit, srcCommit); | |||
} | |||
} |
@@ -31,6 +31,7 @@ branchNameInvalid=Branch name {0} is not allowed | |||
cachedPacksPreventsIndexCreation=Using cached packs prevents index creation | |||
cannotBeCombined=Cannot be combined. | |||
cannotBeRecursiveWhenTreesAreIncluded=TreeWalk shouldn't be recursive when tree objects are included. | |||
cannotCombineSquashWithNoff=Cannot combine --squash with --no-ff. | |||
cannotCombineTreeFilterWithRevFilter=Cannot combine TreeFilter {0} with RevFilter {1}. | |||
cannotCommitOnARepoWithState=Cannot commit on a repo with state: {0} | |||
cannotCommitWriteTo=Cannot commit write to {0} |
@@ -99,6 +99,30 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
private boolean squash; | |||
private FastForwardMode fastForwardMode = FastForwardMode.FF; | |||
/** | |||
* The modes available for fast forward merges (corresponding to the --ff, | |||
* --no-ff and --ff-only options). | |||
*/ | |||
public enum FastForwardMode { | |||
/** | |||
* Corresponds to the default --ff option (for a fast forward update the | |||
* branch pointer only). | |||
*/ | |||
FF, | |||
/** | |||
* Corresponds to the --no-ff option (create a merge commit even for a | |||
* fast forward). | |||
*/ | |||
NO_FF, | |||
/** | |||
* Corresponds to the --ff-only option (abort unless the merge is a fast | |||
* forward). | |||
*/ | |||
FF_ONLY; | |||
} | |||
/** | |||
* @param repo | |||
*/ | |||
@@ -118,14 +142,7 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
ConcurrentRefUpdateException, CheckoutConflictException, | |||
InvalidMergeHeadsException, WrongRepositoryStateException, NoMessageException { | |||
checkCallable(); | |||
if (commits.size() != 1) | |||
throw new InvalidMergeHeadsException( | |||
commits.isEmpty() ? JGitText.get().noMergeHeadSpecified | |||
: MessageFormat.format( | |||
JGitText.get().mergeStrategyDoesNotSupportHeads, | |||
mergeStrategy.getName(), | |||
Integer.valueOf(commits.size()))); | |||
checkParameters(); | |||
RevWalk revWalk = null; | |||
DirCacheCheckout dco = null; | |||
@@ -179,7 +196,8 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
return new MergeResult(headCommit, srcCommit, new ObjectId[] { | |||
headCommit, srcCommit }, | |||
MergeStatus.ALREADY_UP_TO_DATE, mergeStrategy, null, null); | |||
} else if (revWalk.isMergedInto(headCommit, srcCommit)) { | |||
} else if (revWalk.isMergedInto(headCommit, srcCommit) | |||
&& fastForwardMode == FastForwardMode.FF) { | |||
// FAST_FORWARD detected: skip doing a real merge but only | |||
// update HEAD | |||
refLogMessage.append(": " + MergeStatus.FAST_FORWARD); | |||
@@ -210,6 +228,11 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
headCommit, srcCommit }, mergeStatus, mergeStrategy, | |||
null, msg); | |||
} else { | |||
if (fastForwardMode == FastForwardMode.FF_ONLY) { | |||
return new MergeResult(headCommit, srcCommit, | |||
new ObjectId[] { headCommit, srcCommit }, | |||
MergeStatus.ABORTED, mergeStrategy, null, null); | |||
} | |||
String mergeMessage = ""; | |||
if (!squash) { | |||
mergeMessage = new MergeMessageFormatter().format( | |||
@@ -241,7 +264,10 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
} else | |||
noProblems = merger.merge(headCommit, srcCommit); | |||
refLogMessage.append(": Merge made by "); | |||
refLogMessage.append(mergeStrategy.getName()); | |||
if (!revWalk.isMergedInto(headCommit, srcCommit)) | |||
refLogMessage.append(mergeStrategy.getName()); | |||
else | |||
refLogMessage.append("recursive"); | |||
refLogMessage.append('.'); | |||
if (noProblems) { | |||
dco = new DirCacheCheckout(repo, | |||
@@ -305,6 +331,21 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
} | |||
} | |||
private void checkParameters() throws InvalidMergeHeadsException { | |||
if (squash && fastForwardMode == FastForwardMode.NO_FF) { | |||
throw new JGitInternalException( | |||
JGitText.get().cannotCombineSquashWithNoff); | |||
} | |||
if (commits.size() != 1) | |||
throw new InvalidMergeHeadsException( | |||
commits.isEmpty() ? JGitText.get().noMergeHeadSpecified | |||
: MessageFormat.format( | |||
JGitText.get().mergeStrategyDoesNotSupportHeads, | |||
mergeStrategy.getName(), | |||
Integer.valueOf(commits.size()))); | |||
} | |||
private void updateHead(StringBuilder refLogMessage, ObjectId newHeadId, | |||
ObjectId oldHeadID) throws IOException, | |||
ConcurrentRefUpdateException { | |||
@@ -392,4 +433,19 @@ public class MergeCommand extends GitCommand<MergeResult> { | |||
this.squash = squash; | |||
return this; | |||
} | |||
/** | |||
* Sets the fast forward mode. | |||
* | |||
* @param fastForwardMode | |||
* corresponds to the --ff/--no-ff/--ff-only options. --ff is the | |||
* default option. | |||
* @return {@code this} | |||
* @since 2.2 | |||
*/ | |||
public MergeCommand setFastForward(FastForwardMode fastForwardMode) { | |||
checkCallable(); | |||
this.fastForwardMode = fastForwardMode; | |||
return this; | |||
} | |||
} |
@@ -152,6 +152,20 @@ public class MergeResult { | |||
return false; | |||
} | |||
}, | |||
/** | |||
* @since 2.2 | |||
*/ | |||
ABORTED { | |||
@Override | |||
public String toString() { | |||
return "Aborted"; | |||
} | |||
@Override | |||
public boolean isSuccessful() { | |||
return false; | |||
} | |||
}, | |||
/** */ | |||
NOT_SUPPORTED { | |||
@Override |
@@ -91,6 +91,7 @@ public class JGitText extends TranslationBundle { | |||
/***/ public String cachedPacksPreventsIndexCreation; | |||
/***/ public String cannotBeCombined; | |||
/***/ public String cannotBeRecursiveWhenTreesAreIncluded; | |||
/***/ public String cannotCombineSquashWithNoff; | |||
/***/ public String cannotCombineTreeFilterWithRevFilter; | |||
/***/ public String cannotCommitOnARepoWithState; | |||
/***/ public String cannotCommitWriteTo; |