Skip to content

Commit

Permalink
fetch: stop clobbering existing tags without --force
Browse files Browse the repository at this point in the history
Change "fetch" to treat "+" in refspecs (aka --force) to mean we
should clobber a local tag of the same name.

This changes the long-standing behavior of "fetch" added in
853a369 ("[PATCH] Multi-head fetch.", 2005-08-20), before this
change all tag fetches effectively had --force enabled. The original
rationale in that change was:

    > Tags need not be pointing at commits so there is no way to
    > guarantee "fast-forward" anyway.

That comment and the rest of the history of "fetch" shows that the
"+" (--force) part of refpecs was only conceived for branch updates,
while tags have accepted any changes from upstream unconditionally and
clobbered the local tag object. Changing this behavior has been
discussed as early as 2011[1].

I the current behavior doesn't make sense, it easily results in local
tags accidentally being clobbered. Ideally we'd namespace our tags
per-remote, but as with my 97716d2 ("fetch: add a --prune-tags
option and fetch.pruneTags config", 2018-02-09) it's easier to work
around the current implementation than to fix the root cause, so this
implements suggestion #1 from [1], "fetch" now only clobbers the tag
if either "+" is provided as part of the refspec, or if "--force" is
provided on the command-line.

This also makes it nicely symmetrical with how "tag" itself
works. We'll now refuse to clobber any existing tags unless "--force"
is supplied, whether that clobbering would happen by clobbering a
local tag with "tag", or by fetching it from the remote with "fetch".

It's still not at all nicely symmetrical with how "git push" works, as
discussed in the updated pull-fetch-param.txt documentation, but this
change brings them more into line with one another. I don't think
there's any reason "fetch" couldn't fully converge with the behavior
used by "push", but that's a topic for another change.

One of the tests added in 31b808a ("clone --single: limit the fetch
refspec to fetched branch", 2012-09-20) is being changed to use
--force where a clone would clobber a tag. This changes nothing about
the existing behavior of the test.

1. https://public-inbox.org/git/20111123221658.GA22313@sigill.intra.peff.net/

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
avar authored and gitster committed May 8, 2018
1 parent 6ed3f67 commit 400a083
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 22 deletions.
15 changes: 10 additions & 5 deletions Documentation/fetch-options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ endif::git-pull[]

-f::
--force::
When 'git fetch' is used with `<rbranch>:<lbranch>`
refspec, it refuses to update the local branch
`<lbranch>` unless the remote branch `<rbranch>` it
fetches is a descendant of `<lbranch>`. This option
overrides that check.
When 'git fetch' is used with `<src>:<dst>` refspec it might
refuse to update the local branch as discussed
ifdef::git-pull[]
in the `<refspec>` part of the linkgit:git-fetch[1]
documentation.
endif::git-pull[]
ifndef::git-pull[]
in the `<refspec>` part below.
endif::git-pull[]
This option overrides that check.

-k::
--keep::
Expand Down
22 changes: 16 additions & 6 deletions Documentation/pull-fetch-param.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,22 @@ name.
`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;
it requests fetching everything up to the given tag.
+
The remote ref that matches <src>
is fetched, and if <dst> is not empty string, the local
ref that matches it is fast-forwarded using <src>.
If the optional plus `+` is used, the local ref
is updated even if it does not result in a fast-forward
update.
The remote ref that matches <src> is fetched, and if <dst> is not
empty string, an attempt is made to update the local ref that matches
it.
+
Whether that update is allowed is confusingly not the inverse of
whether a server will accept a push as described in the `<refspec>...`
section of linkgit:git-push[1]. If it's a commit under `refs/heads/*`
only fast-forwards are allowed, but unlike what linkgit:git-push[1]
will accept clobbering any ref pointing to blobs, trees etc. in any
other namespace will be accepted, but commits in any ref
namespace. Those apply the same fast-forward rule. An exception to
this is that as of Git version 2.18 any object under `refs/tags/*` is
protected from updates.
+
If the optional plus `+` is used, the local ref is updated if the
update would have otherwise been rejected.
+
[NOTE]
When the remote branch you want to fetch is known to
Expand Down
20 changes: 13 additions & 7 deletions builtin/fetch.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ static struct option builtin_fetch_options[] = {
N_("append to .git/FETCH_HEAD instead of overwriting")),
OPT_STRING(0, "upload-pack", &upload_pack, N_("path"),
N_("path to upload pack on remote end")),
OPT__FORCE(&force, N_("force overwrite of local branch"), 0),
OPT__FORCE(&force, N_("force overwrite of local reference"), 0),
OPT_BOOL('m', "multiple", &multiple,
N_("fetch from multiple remotes")),
OPT_SET_INT('t', "tags", &tags,
Expand Down Expand Up @@ -664,12 +664,18 @@ static int update_local_ref(struct ref *ref,

if (!is_null_oid(&ref->old_oid) &&
starts_with(ref->name, "refs/tags/")) {
int r;
r = s_update_ref("updating tag", ref, 0);
format_display(display, r ? '!' : 't', _("[tag update]"),
r ? _("unable to update local ref") : NULL,
remote, pretty_ref, summary_width);
return r;
if (force || ref->force) {
int r;
r = s_update_ref("updating tag", ref, 0);
format_display(display, r ? '!' : 't', _("[tag update]"),
r ? _("unable to update local ref") : NULL,
remote, pretty_ref, summary_width);
return r;
} else {
format_display(display, '!', _("[rejected]"), _("would clobber existing tag"),
remote, pretty_ref, summary_width);
return 1;
}
}

current = lookup_commit_reference_gently(&ref->old_oid, 1);
Expand Down
5 changes: 3 additions & 2 deletions t/t5516-fetch-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,7 @@ test_force_fetch_tag () {
tag_type_description=$1
tag_args=$2

test_expect_success "fetch will clobber an existing $tag_type_description" "
test_expect_success "fetch will not clobber an existing $tag_type_description without --force" "
mk_test testrepo heads/master &&
mk_child testrepo child1 &&
mk_child testrepo child2 &&
Expand All @@ -1019,7 +1019,8 @@ test_force_fetch_tag () {
git add file1 &&
git commit -m 'file1' &&
git tag $tag_args Tag &&
git -C ../child1 fetch origin tag Tag
test_must_fail git -C ../child1 fetch origin tag Tag &&
git -C ../child1 fetch origin '+refs/tags/*:refs/tags/*'
)
"
}
Expand Down
4 changes: 2 additions & 2 deletions t/t5612-clone-refspec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ test_expect_success 'clone with --no-tags' '
test_expect_success '--single-branch while HEAD pointing at master' '
(
cd dir_master &&
git fetch &&
git fetch --force &&
git for-each-ref refs/remotes/origin |
sed -e "/HEAD$/d" \
-e "s|/remotes/origin/|/heads/|" >../actual
Expand All @@ -115,7 +115,7 @@ test_expect_success '--single-branch while HEAD pointing at master' '
test_cmp expect actual &&
(
cd dir_master &&
git fetch --tags &&
git fetch --tags --force &&
git for-each-ref refs/tags >../actual
) &&
git for-each-ref refs/tags >expect &&
Expand Down

0 comments on commit 400a083

Please sign in to comment.