merge: allow fast-forward when merging a tracked tag

Long time ago at fab47d05 ("merge: force edit and no-ff mode when merging a tag object", 2011-11-07), "git merge" was made to always create a merge commit when merging a tag, even when the side branch being merged is a descendant of the current branch. This default is good for merges made by upstream maintainers to integrate work signed by downstream contributors, but will leave pointless no-ff merges when downstream contributors pull a newer release tag to make their long-running topic branches catch up with the upstream. When there is no local work left on the topic, such a merge should simply fast-forward to the commit pointed at by the release tag. Update the default (again) for "git merge" that merges a tag object to (1) --no-ff (i.e. create a merge commit even when side branch fast forwards) if the tag being merged is not at its expected place in refs/tags/ hierarchy and (2) --ff (i.e. allow fast-forward update when able) otherwise. Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Feb 14, 2018 at 10:18 UTC adcc94a0aa7055be4133ebda8b25f4af63285c6d
4 files changed +79 -7
Documentation/merge-options.txt
+2 -1
@@ -35,7 +35,8 @@ set to `no` at the beginning of them.
35 --no-ff::
36 Create a merge commit even when the merge resolves as a
37 fast-forward. This is the default behaviour when merging an
38 - annotated (and possibly signed) tag.
38 + annotated (and possibly signed) tag that is not stored in
39 + its natural place in 'refs/tags/' hierarchy.
40
41 --ff-only::
42 Refuse to merge and exit with a non-zero status unless the
builtin/merge.c
+39 -4
@@ -33,6 +33,7 @@
33 #include "sequencer.h"
34 #include "string-list.h"
35 #include "packfile.h"
36 +#include "tag.h"
37
38 #define DEFAULT_TWOHEAD (1<<0)
39 #define DEFAULT_OCTOPUS (1<<1)
@@ -1125,6 +1126,43 @@ static struct commit_list *collect_parents(struct commit *head_commit,
1126 return remoteheads;
1127 }
1128
1129 +static int merging_a_throwaway_tag(struct commit *commit)
1130 +{
1131 + char *tag_ref;
1132 + struct object_id oid;
1133 + int is_throwaway_tag = 0;
1134 +
1135 + /* Are we merging a tag? */
1136 + if (!merge_remote_util(commit) ||
1137 + !merge_remote_util(commit)->obj ||
1138 + merge_remote_util(commit)->obj->type != OBJ_TAG)
1139 + return is_throwaway_tag;
1140 +
1141 + /*
1142 + * Now we know we are merging a tag object. Are we downstream
1143 + * and following the tags from upstream? If so, we must have
1144 + * the tag object pointed at by "refs/tags/$T" where $T is the
1145 + * tagname recorded in the tag object. We want to allow such
1146 + * a "just to catch up" merge to fast-forward.
1147 + *
1148 + * Otherwise, we are playing an integrator's role, making a
1149 + * merge with a throw-away tag from a contributor with
1150 + * something like "git pull $contributor $signed_tag".
1151 + * We want to forbid such a merge from fast-forwarding
1152 + * by default; otherwise we would not keep the signature
1153 + * anywhere.
1154 + */
1155 + tag_ref = xstrfmt("refs/tags/%s",
1156 + ((struct tag *)merge_remote_util(commit)->obj)->tag);
1157 + if (!read_ref(tag_ref, &oid) &&
1158 + !oidcmp(&oid, &merge_remote_util(commit)->obj->oid))
1159 + is_throwaway_tag = 0;
1160 + else
1161 + is_throwaway_tag = 1;
1162 + free(tag_ref);
1163 + return is_throwaway_tag;
1164 +}
1165 +
1166 int cmd_merge(int argc, const char **argv, const char *prefix)
1167 {
1168 struct object_id result_tree, stash, head_oid;
@@ -1322,10 +1360,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
1360 oid_to_hex(&commit->object.oid));
1361 setenv(buf.buf, merge_remote_util(commit)->name, 1);
1362 strbuf_reset(&buf);
1325 - if (fast_forward != FF_ONLY &&
1326 - merge_remote_util(commit) &&
1327 - merge_remote_util(commit)->obj &&
1328 - merge_remote_util(commit)->obj->type == OBJ_TAG)
1363 + if (fast_forward != FF_ONLY && merging_a_throwaway_tag(commit))
1364 fast_forward = FF_NO;
1365 }
1366
t/t6200-fmt-merge-msg.sh
+1 -1
@@ -512,7 +512,7 @@ test_expect_success 'merge-msg with "merging" an annotated tag' '
512
513 test_when_finished "git reset --hard" &&
514 annote=$(git rev-parse annote) &&
515 - git merge --no-commit $annote &&
515 + git merge --no-commit --no-ff $annote &&
516 {
517 cat <<-EOF
518 Merge tag '\''$annote'\''
t/t7600-merge.sh
+37 -1
@@ -700,6 +700,42 @@ test_expect_success 'merge --no-ff --edit' '
700 test_cmp expected actual
701 '
702
703 +test_expect_success 'merge annotated/signed tag w/o tracking' '
704 + test_when_finished "rm -rf dst; git tag -d anno1" &&
705 + git tag -a -m "anno c1" anno1 c1 &&
706 + git init dst &&
707 + git rev-parse c1 >dst/expect &&
708 + (
709 + # c0 fast-forwards to c1 but because this repository
710 + # is not a "downstream" whose refs/tags follows along
711 + # tag from the "upstream", this pull defaults to --no-ff
712 + cd dst &&
713 + git pull .. c0 &&
714 + git pull .. anno1 &&
715 + git rev-parse HEAD^2 >actual &&
716 + test_cmp expect actual
717 + )
718 +'
719 +
720 +test_expect_success 'merge annotated/signed tag w/ tracking' '
721 + test_when_finished "rm -rf dst; git tag -d anno1" &&
722 + git tag -a -m "anno c1" anno1 c1 &&
723 + git init dst &&
724 + git rev-parse c1 >dst/expect &&
725 + (
726 + # c0 fast-forwards to c1 and because this repository
727 + # is a "downstream" whose refs/tags follows along
728 + # tag from the "upstream", this pull defaults to --ff
729 + cd dst &&
730 + git remote add origin .. &&
731 + git pull origin c0 &&
732 + git fetch origin &&
733 + git merge anno1 &&
734 + git rev-parse HEAD >actual &&
735 + test_cmp expect actual
736 + )
737 +'
738 +
739 test_expect_success GPG 'merge --ff-only tag' '
740 git reset --hard c0 &&
741 git commit --allow-empty -m "A newer commit" &&
@@ -718,7 +754,7 @@ test_expect_success GPG 'merge --no-edit tag should skip editor' '
754 git tag -f -s -m "A newer commit" signed &&
755 git reset --hard c0 &&
756
721 - EDITOR=false git merge --no-edit signed &&
757 + EDITOR=false git merge --no-edit --no-ff signed &&
758 git rev-parse signed^0 >expect &&
759 git rev-parse HEAD^2 >actual &&
760 test_cmp expect actual