]> source.dussan.org Git - jgit.git/commitdiff
Add support for --no-ff while merging 95/8395/6
authorTomasz Zarna <tomasz.zarna@tasktop.com>
Mon, 29 Oct 2012 15:21:59 +0000 (16:21 +0100)
committerTomasz Zarna <tomasz.zarna@tasktop.com>
Fri, 16 Nov 2012 10:04:13 +0000 (11:04 +0100)
Bug: 394432
Change-Id: I373128c0ba949f9b24248874f77f3d68b50ccfd1
Signed-off-by: Chris Aniszczyk <zx@twitter.com>
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

index 87a2c855334efa5699e103272674cd0324f602b5..28a107bcb1efe42d2a17e4bfe7509abe9dc9332f 100644 (file)
@@ -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]);
+       }
 }
index 6f5c8eb68581a5ccb6b155a4adde0365cf942b2d..cb1574f26cc92defc102f14c994022b652de637a 100644 (file)
@@ -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
index c1cac71cfa11441645eef0f00c083aaa59469a5b..5a0b8b287536ee0a7cae580b7c7dc2f921a51cb0 100644 (file)
@@ -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;
 }
index 728866d1824dd37f6ffd607685f7b0412aeeb07b..e79a8ff0a8fc2583424516ed8c2543c704d256bf 100644 (file)
 
 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);
+       }
 }
index 8018ab9baac746de85391c3f277ee2c8876b7cf4..70f1313a4089b689ae648a8ffad9f06a6fb98c19 100644 (file)
@@ -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}
index 3ca861c06cc166f1137776d3d16ce69cadd32b76..a884c70cb098ddbbf23391aad86d93a17214de87 100644 (file)
@@ -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;
+       }
 }
index 484039e7081e5f4446386c2baf5e97d57f72197f..8485a15440b3fd222beafcd87a1cf3389cd58021 100644 (file)
@@ -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
index f6c48c92c606260a6cee7ee783e6deb9eaefa98d..e115a73ce6db580fb9f4726f55998c6c01651058 100644 (file)
@@ -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;