merge: refuse to create too cool a merge by default
authorJunio C Hamano <gitster@pobox.com>
Fri, 18 Mar 2016 20:21:09 +0000 (13:21 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 23 Mar 2016 19:04:48 +0000 (12:04 -0700)
While it makes sense to allow merging unrelated histories of two
projects that started independently into one, in the way "gitk" was
merged to "git" itself aka "the coolest merge ever", such a merge is
still an unusual event. Worse, if somebody creates an independent
history by starting from a tarball of an established project and
sends a pull request to the original project, "git merge" however
happily creates such a merge without any sign of something unusual
is happening.

Teach "git merge" to refuse to create such a merge by default,
unless the user passes a new "--allow-unrelated-histories" option to
tell it that the user is aware that two unrelated projects are
merged.

Because such a "two project merge" is a rare event, a configuration
option to always allow such a merge is not added.

We could add the same option to "git pull" and have it passed
through to underlying "git merge". I do not have a fundamental
opposition against such a feature, but this commit does not do so
and instead leaves it as low-hanging fruit for others, because such
a "two project merge" would be done after fetching the other project
into some location in the working tree of an existing project and
making sure how well they fit together, it is sufficient to allow a
local merge without such an option pass-through from "git pull" to
"git merge". Many tests that are updated by this patch does the
pass-through manually by turning:

git pull something

into its equivalent:

git fetch something &&
git merge --allow-unrelated-histories FETCH_HEAD

If somebody is inclined to add such an option, updated tests in this
change need to be adjusted back to:

git pull --allow-unrelated-histories something

Signed-off-by: Junio C Hamano <gitster@pobox.com>
12 files changed:
Documentation/git-merge.txt
builtin/merge.c
t/t3033-merge-toplevel.sh
t/t3412-rebase-root.sh
t/t5500-fetch-pack.sh
t/t6009-rev-list-parent.sh
t/t6010-merge-base.sh
t/t6012-rev-list-simplify.sh
t/t6026-merge-attr.sh
t/t6029-merge-subtree.sh
t/t6101-rev-parse-parents.sh
t/t9400-git-cvsserver-server.sh
index 07f7295ec8b603fcbd907e780d4ed1109ac5b358..689aa4c57cca66c7cd92235248eee2b32ff20451 100644 (file)
@@ -98,6 +98,19 @@ commit or stash your changes before running 'git merge'.
 'git merge --abort' is equivalent to 'git reset --merge' when
 `MERGE_HEAD` is present.
 
+--allow-unrelated-histories::
+       By default, `git merge` command refuses to merge histories
+       that do not share a common ancestor.  This option can be
+       used to override this safety when merging histories of two
+       projects that started their lives independently.  As that is
+       a very rare occasion, no configuration variable to enable
+       this by default exists and will not be added, and the list
+       of options at the top of this documentation does not mention
+       this option.  Also `git pull` does not pass this option down
+       to `git merge` (instead, you `git fetch` first, examine what
+       you will be merging and then `git merge` locally with this
+       option).
+
 <commit>...::
        Commits, usually other branch heads, to merge into our branch.
        Specifying more than one commit will create a merge with
index 101ffeff4c942636e0ca688357a4b8ec8aa2a431..e3db41b77a4ecafb2e2d02562b1d460ac68bd551 100644 (file)
@@ -64,6 +64,7 @@ static int option_renormalize;
 static int verbosity;
 static int allow_rerere_auto;
 static int abort_current_merge;
+static int allow_unrelated_histories;
 static int show_progress = -1;
 static int default_to_upstream = 1;
 static const char *sign_commit;
@@ -221,6 +222,8 @@ static struct option builtin_merge_options[] = {
        OPT__VERBOSITY(&verbosity),
        OPT_BOOL(0, "abort", &abort_current_merge,
                N_("abort the current in-progress merge")),
+       OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories,
+                N_("allow merging unrelated histories")),
        OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1),
        { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"),
          N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
@@ -1397,9 +1400,12 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
        update_ref("updating ORIG_HEAD", "ORIG_HEAD", head_commit->object.oid.hash,
                   NULL, 0, UPDATE_REFS_DIE_ON_ERR);
 
-       if (remoteheads && !common)
-               ; /* No common ancestors found. We need a real merge. */
-       else if (!remoteheads ||
+       if (remoteheads && !common) {
+               /* No common ancestors found. */
+               if (!allow_unrelated_histories)
+                       die(_("refusing to merge unrelated histories"));
+               /* otherwise, we need a real merge. */
+       } else if (!remoteheads ||
                 (!remoteheads->next && !common->next &&
                  common->item == remoteheads->item)) {
                /*
index 46aadc410bc470d9279ed1fd3ee72583d0963d30..c1379b00c2efc99a700c75f5ce13b2248706cf4b 100755 (executable)
@@ -19,6 +19,8 @@ test_expect_success setup '
        test_commit three &&
        git checkout right &&
        test_commit four &&
+       git checkout --orphan five &&
+       test_commit five &&
        git checkout master
 '
 
@@ -133,4 +135,18 @@ test_expect_success 'merge FETCH_HEAD octopus non-fast-forward' '
        test_cmp expect actual
 '
 
+# two-project merge
+test_expect_success 'refuse two-project merge by default' '
+       t3033_reset &&
+       git reset --hard four &&
+       test_must_fail git merge five
+'
+
+test_expect_success 'two-project merge with --allow-unrelated-histories' '
+       t3033_reset &&
+       git reset --hard four &&
+       git merge --allow-unrelated-histories five &&
+       git diff --exit-code five
+'
+
 test_done
index 0b521057283bf106da8ce55f25f61c7ec7e3ad35..73a39f2923aa7536ce65ea3ee514805122be79b1 100755 (executable)
@@ -133,7 +133,7 @@ test_expect_success 'set up second root and merge' '
        rm A B C &&
        test_commit 6 D &&
        git checkout other &&
-       git merge third
+       git merge --allow-unrelated-histories third
 '
 
 cat > expect-third <<'EOF'
index 3a9b77576fb57828cd574017883c7920c2d91aa3..256958023277de8b5c1c3924170a0640ffb92972 100755 (executable)
@@ -259,7 +259,8 @@ test_expect_success 'clone shallow object count' '
 test_expect_success 'pull in shallow repo with missing merge base' '
        (
                cd shallow &&
-               test_must_fail git pull --depth 4 .. A
+               git fetch --depth 4 .. A
+               test_must_fail git merge --allow-unrelated-histories FETCH_HEAD
        )
 '
 
@@ -279,9 +280,10 @@ test_expect_success 'clone shallow depth count' '
 test_expect_success 'clone shallow object count' '
        (
                cd shallow &&
+               git prune &&
                git count-objects -v
        ) > count.shallow &&
-       grep "^count: 55" count.shallow
+       grep "^count: 54" count.shallow
 '
 
 test_expect_success 'fetch --no-shallow on full repo' '
index 66cda17ef342a2791ce7bac873eecfd4cf319faf..20e3e2554a132552b5e398a2c449cedbef6ccb1e 100755 (executable)
@@ -47,7 +47,9 @@ test_expect_success 'setup roots, merges and octopuses' '
        git checkout -b yetanotherbranch four &&
        test_commit eight &&
        git checkout master &&
-       test_merge normalmerge newroot &&
+       test_tick &&
+       git merge --allow-unrelated-histories -m normalmerge newroot &&
+       git tag normalmerge &&
        test_tick &&
        git merge -m tripus sidebranch anotherbranch &&
        git tag tripus &&
index 39b3238da211957eeec36a9c7b911cd5942de6dd..e0c5f44cac0290c6e15c5360b0cc7c1f4d486614 100755 (executable)
@@ -215,11 +215,13 @@ test_expect_success 'criss-cross merge-base for octopus-step' '
        git reset --hard E &&
        test_commit CC2 &&
        test_tick &&
-       git merge -s ours CC1 &&
+       # E is a root commit unrelated to MMR root on which CC1 is based
+       git merge -s ours --allow-unrelated-histories CC1 &&
        test_commit CC-o &&
        test_commit CCB &&
        git reset --hard CC1 &&
-       git merge -s ours CC2 &&
+       # E is a root commit unrelated to MMR root on which CC1 is based
+       git merge -s ours --allow-unrelated-histories CC2 &&
        test_commit CCA &&
 
        git rev-parse CC1 CC2 >expected &&
index b89cd6b07a3df8f10c89e97ddf5cad7338e065bc..2a0fbb87b1d63f1a07f8a3934e1546c695a73f56 100755 (executable)
@@ -71,7 +71,7 @@ test_expect_success setup '
        note J &&
 
        git checkout master &&
-       test_tick && git merge -m "Coolest" unrelated &&
+       test_tick && git merge --allow-unrelated-histories -m "Coolest" unrelated &&
        note K &&
 
        echo "Immaterial" >elif &&
index 04c0509c476de87bfb679f1c50556ca4c3a3d615..ef0cbceafe855cfb7b0ce4d1cb5050875d587253 100755 (executable)
@@ -176,7 +176,8 @@ test_expect_success 'up-to-date merge without common ancestor' '
        test_tick &&
        (
                cd repo1 &&
-               git pull ../repo2 master
+               git fetch ../repo2 master &&
+               git merge --allow-unrelated-histories FETCH_HEAD
        )
 '
 
index 73fc240e8548911c65c4dffc4da0c6ff8ab4f27b..3e692454a719324a7ed3c12238416f45df080efa 100755 (executable)
@@ -49,7 +49,7 @@ test_expect_success 'setup' '
 
 test_expect_success 'initial merge' '
        git remote add -f gui ../git-gui &&
-       git merge -s ours --no-commit gui/master &&
+       git merge -s ours --no-commit --allow-unrelated-histories gui/master &&
        git read-tree --prefix=git-gui/ -u gui/master &&
        git commit -m "Merge git-gui as our subdirectory" &&
        git checkout -b work &&
index 10b1452766b613932f66421f6c9bfafecc093177..1c6952d04936b190c9cf42024519273b00747113 100755 (executable)
@@ -19,7 +19,7 @@ test_expect_success 'setup' '
        git checkout --orphan tmp &&
        test_commit start2 &&
        git checkout master &&
-       git merge -m next start2 &&
+       git merge -m next --allow-unrelated-histories start2 &&
        test_commit final &&
 
        test_seq 40 |
index 6146c3fec293915b4599748e7a787539a7715e1a..bc19811dde8308a31bed705b0d465bc736318a96 100755 (executable)
@@ -45,7 +45,8 @@ test_expect_success 'setup' '
   touch secondrootfile &&
   git add secondrootfile &&
   git commit -m "second root") &&
-  git pull secondroot master &&
+  git fetch secondroot master &&
+  git merge --allow-unrelated-histories FETCH_HEAD &&
   git clone -q --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 &&
   GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
   GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log" &&