Merge branch 'jc/allow-ff-merging-kept-tags'

Since Git 1.7.9, "git merge" defaulted to --no-ff (i.e. even when
the side branch being merged is a descendant of the current commit,
create a merge commit instead of fast-forwarding) when merging a
tag object.  This was appropriate default for integrators who pull
signed tags from their downstream contributors, but caused an
unnecessary merges when used by downstream contributors who
habitually "catch up" their topic branches with tagged releases
from the upstream.  Update "git merge" to default to --no-ff only
when merging a tag object that does *not* sit at its usual place in
refs/tags/ hierarchy, and allow fast-forwarding otherwise, to
mitigate the problem.

* jc/allow-ff-merging-kept-tags:
  merge: allow fast-forward when merging a tracked tag
This commit is contained in:
Junio C Hamano 2018-03-06 14:54:04 -08:00
commit f88590e675
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