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>
This commit is contained in:
Junio C Hamano 2018-02-14 10:18:55 -08:00
parent b2e45c695d
commit adcc94a0aa
4 changed files with 79 additions and 7 deletions

View File

@ -35,7 +35,8 @@ set to `no` at the beginning of them.
--no-ff::
Create a merge commit even when the merge resolves as a
fast-forward. This is the default behaviour when merging an
annotated (and possibly signed) tag.
annotated (and possibly signed) tag that is not stored in
its natural place in 'refs/tags/' hierarchy.
--ff-only::
Refuse to merge and exit with a non-zero status unless the

View File

@ -33,6 +33,7 @@
#include "sequencer.h"
#include "string-list.h"
#include "packfile.h"
#include "tag.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@ -1125,6 +1126,43 @@ static struct commit_list *collect_parents(struct commit *head_commit,
return remoteheads;
}
static int merging_a_throwaway_tag(struct commit *commit)
{
char *tag_ref;
struct object_id oid;
int is_throwaway_tag = 0;
/* Are we merging a tag? */
if (!merge_remote_util(commit) ||
!merge_remote_util(commit)->obj ||
merge_remote_util(commit)->obj->type != OBJ_TAG)
return is_throwaway_tag;
/*
* Now we know we are merging a tag object. Are we downstream
* and following the tags from upstream? If so, we must have
* the tag object pointed at by "refs/tags/$T" where $T is the
* tagname recorded in the tag object. We want to allow such
* a "just to catch up" merge to fast-forward.
*
* Otherwise, we are playing an integrator's role, making a
* merge with a throw-away tag from a contributor with
* something like "git pull $contributor $signed_tag".
* We want to forbid such a merge from fast-forwarding
* by default; otherwise we would not keep the signature
* anywhere.
*/
tag_ref = xstrfmt("refs/tags/%s",
((struct tag *)merge_remote_util(commit)->obj)->tag);
if (!read_ref(tag_ref, &oid) &&
!oidcmp(&oid, &merge_remote_util(commit)->obj->oid))
is_throwaway_tag = 0;
else
is_throwaway_tag = 1;
free(tag_ref);
return is_throwaway_tag;
}
int cmd_merge(int argc, const char **argv, const char *prefix)
{
struct object_id result_tree, stash, head_oid;
@ -1322,10 +1360,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
oid_to_hex(&commit->object.oid));
setenv(buf.buf, merge_remote_util(commit)->name, 1);
strbuf_reset(&buf);
if (fast_forward != FF_ONLY &&
merge_remote_util(commit) &&
merge_remote_util(commit)->obj &&
merge_remote_util(commit)->obj->type == OBJ_TAG)
if (fast_forward != FF_ONLY && merging_a_throwaway_tag(commit))
fast_forward = FF_NO;
}

View File

@ -512,7 +512,7 @@ test_expect_success 'merge-msg with "merging" an annotated tag' '
test_when_finished "git reset --hard" &&
annote=$(git rev-parse annote) &&
git merge --no-commit $annote &&
git merge --no-commit --no-ff $annote &&
{
cat <<-EOF
Merge tag '\''$annote'\''

View File

@ -700,6 +700,42 @@ test_expect_success 'merge --no-ff --edit' '
test_cmp expected actual
'
test_expect_success 'merge annotated/signed tag w/o tracking' '
test_when_finished "rm -rf dst; git tag -d anno1" &&
git tag -a -m "anno c1" anno1 c1 &&
git init dst &&
git rev-parse c1 >dst/expect &&
(
# c0 fast-forwards to c1 but because this repository
# is not a "downstream" whose refs/tags follows along
# tag from the "upstream", this pull defaults to --no-ff
cd dst &&
git pull .. c0 &&
git pull .. anno1 &&
git rev-parse HEAD^2 >actual &&
test_cmp expect actual
)
'
test_expect_success 'merge annotated/signed tag w/ tracking' '
test_when_finished "rm -rf dst; git tag -d anno1" &&
git tag -a -m "anno c1" anno1 c1 &&
git init dst &&
git rev-parse c1 >dst/expect &&
(
# c0 fast-forwards to c1 and because this repository
# is a "downstream" whose refs/tags follows along
# tag from the "upstream", this pull defaults to --ff
cd dst &&
git remote add origin .. &&
git pull origin c0 &&
git fetch origin &&
git merge anno1 &&
git rev-parse HEAD >actual &&
test_cmp expect actual
)
'
test_expect_success GPG 'merge --ff-only tag' '
git reset --hard c0 &&
git commit --allow-empty -m "A newer commit" &&
@ -718,7 +754,7 @@ test_expect_success GPG 'merge --no-edit tag should skip editor' '
git tag -f -s -m "A newer commit" signed &&
git reset --hard c0 &&
EDITOR=false git merge --no-edit signed &&
EDITOR=false git merge --no-edit --no-ff signed &&
git rev-parse signed^0 >expect &&
git rev-parse HEAD^2 >actual &&
test_cmp expect actual