Merge branch 'ab/fetch-tags-noclobber'

The rules used by "git push" and "git fetch" to determine if a ref
can or cannot be updated were inconsistent; specifically, fetching
to update existing tags were allowed even though tags are supposed
to be unmoving anchoring points.  "git fetch" was taught to forbid
updates to existing tags without the "--force" option.

* ab/fetch-tags-noclobber:
  fetch: stop clobbering existing tags without --force
  fetch: document local ref updates with/without --force
  push doc: correct lies about how push refspecs work
  push doc: move mention of "tag <tag>" later in the prose
  push doc: remove confusing mention of remote merger
  fetch tests: add a test for clobbering tag behavior
  push tests: use spaces in interpolated string
  push tests: make use of unused $1 in test description
  fetch: change "branch" to "reference" in --force -h output
maint
Junio C Hamano 2018-09-17 13:54:00 -07:00
commit d39cab3989
7 changed files with 134 additions and 33 deletions

View File

@ -68,11 +68,16 @@ endif::git-pull[]


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


-k:: -k::
--keep:: --keep::

View File

@ -74,22 +74,57 @@ without any `<refspec>` on the command line. Otherwise, missing
`:<dst>` means to update the same ref as the `<src>`. `:<dst>` means to update the same ref as the `<src>`.
+ +
The object referenced by <src> is used to update the <dst> reference The object referenced by <src> is used to update the <dst> reference
on the remote side. By default this is only allowed if <dst> is not on the remote side. Whether this is allowed depends on where in
a tag (annotated or lightweight), and then only if it can fast-forward `refs/*` the <dst> reference lives as described in detail below, in
<dst>. By having the optional leading `+`, you can tell Git to update those sections "update" means any modifications except deletes, which
the <dst> ref even if it is not allowed by default (e.g., it is not a as noted after the next few sections are treated differently.
fast-forward.) This does *not* attempt to merge <src> into <dst>. See
EXAMPLES below for details.
+ +
`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`. The `refs/heads/*` namespace will only accept commit objects, and
updates only if they can be fast-forwarded.
+ +
Pushing an empty <src> allows you to delete the <dst> ref from The `refs/tags/*` namespace will accept any kind of object (as
the remote repository. commits, trees and blobs can be tagged), and any updates to them will
be rejected.
+
It's possible to push any type of object to any namespace outside of
`refs/{tags,heads}/*`. In the case of tags and commits, these will be
treated as if they were the commits inside `refs/heads/*` for the
purposes of whether the update is allowed.
+
I.e. a fast-forward of commits and tags outside `refs/{tags,heads}/*`
is allowed, even in cases where what's being fast-forwarded is not a
commit, but a tag object which happens to point to a new commit which
is a fast-forward of the commit the last tag (or commit) it's
replacing. Replacing a tag with an entirely different tag is also
allowed, if it points to the same commit, as well as pushing a peeled
tag, i.e. pushing the commit that existing tag object points to, or a
new tag object which an existing commit points to.
+
Tree and blob objects outside of `refs/{tags,heads}/*` will be treated
the same way as if they were inside `refs/tags/*`, any update of them
will be rejected.
+
All of the rules described above about what's not allowed as an update
can be overridden by adding an the optional leading `+` to a refspec
(or using `--force` command line option). The only exception to this
is that no amount of forcing will make the `refs/heads/*` namespace
accept a non-commit object. Hooks and configuration can also override
or amend these rules, see e.g. `receive.denyNonFastForwards` in
linkgit:git-config[1] and`pre-receive` and `update` in
linkgit:githooks[5].
+
Pushing an empty <src> allows you to delete the <dst> ref from the
remote repository. Deletions are always accepted without a leading `+`
in the refspec (or `--force`), except when forbidden by configuration
or hooks. See `receive.denyDeletes` in linkgit:git-config[1] and
`pre-receive` and `update` in linkgit:githooks[5].
+ +
The special refspec `:` (or `+:` to allow non-fast-forward updates) The special refspec `:` (or `+:` to allow non-fast-forward updates)
directs Git to push "matching" branches: for every branch that exists on directs Git to push "matching" branches: for every branch that exists on
the local side, the remote side is updated if a branch of the same name the local side, the remote side is updated if a branch of the same name
already exists on the remote side. already exists on the remote side.
+
`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`.


--all:: --all::
Push all branches (i.e. refs under `refs/heads/`); cannot be Push all branches (i.e. refs under `refs/heads/`); cannot be

View File

@ -19,9 +19,10 @@ walk the revision graph (such as linkgit:git-log[1]), all commits which are
reachable from that commit. For commands that walk the revision graph one can reachable from that commit. For commands that walk the revision graph one can
also specify a range of revisions explicitly. also specify a range of revisions explicitly.


In addition, some Git commands (such as linkgit:git-show[1]) also take In addition, some Git commands (such as linkgit:git-show[1] and
revision parameters which denote other objects than commits, e.g. blobs linkgit:git-push[1]) can also take revision parameters which denote
("files") or trees ("directories of files"). other objects than commits, e.g. blobs ("files") or trees
("directories of files").


include::revisions.txt[] include::revisions.txt[]



View File

@ -33,11 +33,40 @@ name.
it requests fetching everything up to the given tag. it requests fetching everything up to the given tag.
+ +
The remote ref that matches <src> The remote ref that matches <src>
is fetched, and if <dst> is not an empty string, the local is fetched, and if <dst> is not an empty string, an attempt
ref that matches it is fast-forwarded using <src>. is made to update the local ref that matches it.
If the optional plus `+` is used, the local ref +
is updated even if it does not result in a fast-forward Whether that update is allowed without `--force` depends on the ref
update. namespace it's being fetched to, the type of object being fetched, and
whether the update is considered to be a fast-forward. Generally, the
same rules apply for fetching as when pushing, see the `<refspec>...`
section of linkgit:git-push[1] for what those are. Exceptions to those
rules particular to 'git fetch' are noted below.
+
Until Git version 2.20, and unlike when pushing with
linkgit:git-push[1], any updates to `refs/tags/*` would be accepted
without `+` in the refspec (or `--force`). The receiving promiscuously
considered all tag updates from a remote to be forced fetches. Since
Git version 2.20, fetching to update `refs/tags/*` work the same way
as when pushing. I.e. any updates will be rejected without `+` in the
refspec (or `--force`).
+
Unlike when pushing with linkgit:git-push[1], any updates outside of
`refs/{tags,heads}/*` will be accepted without `+` in the refspec (or
`--force`), whether that's swapping e.g. a tree object for a blob, or
a commit for another commit that's doesn't have the previous commit as
an ancestor etc.
+
Unlike when pushing with linkgit:git-push[1], there is no
configuration which'll amend these rules, and nothing like a
`pre-fetch` hook analogous to the `pre-receive` hook.
+
As with pushing with linkgit:git-push[1], all of the rules described
above about what's not allowed as an update can be overridden by
adding an the optional leading `+` to a refspec (or using `--force`
command line option). The only exception to this is that no amount of
forcing will make the `refs/heads/*` namespace accept a non-commit
object.
+ +
[NOTE] [NOTE]
When the remote branch you want to fetch is known to When the remote branch you want to fetch is known to

View File

@ -115,7 +115,7 @@ static struct option builtin_fetch_options[] = {
N_("append to .git/FETCH_HEAD instead of overwriting")), N_("append to .git/FETCH_HEAD instead of overwriting")),
OPT_STRING(0, "upload-pack", &upload_pack, N_("path"), OPT_STRING(0, "upload-pack", &upload_pack, N_("path"),
N_("path to upload pack on remote end")), 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, OPT_BOOL('m', "multiple", &multiple,
N_("fetch from multiple remotes")), N_("fetch from multiple remotes")),
OPT_SET_INT('t', "tags", &tags, OPT_SET_INT('t', "tags", &tags,
@ -668,12 +668,18 @@ static int update_local_ref(struct ref *ref,


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


current = lookup_commit_reference_gently(the_repository, current = lookup_commit_reference_gently(the_repository,

View File

@ -969,7 +969,7 @@ test_force_push_tag () {
tag_type_description=$1 tag_type_description=$1
tag_args=$2 tag_args=$2


test_expect_success 'force pushing required to update lightweight tag' " test_expect_success "force pushing required to update $tag_type_description" "
mk_test testrepo heads/master && mk_test testrepo heads/master &&
mk_child testrepo child1 && mk_child testrepo child1 &&
mk_child testrepo child2 && mk_child testrepo child2 &&
@ -1009,7 +1009,32 @@ test_force_push_tag () {
} }


test_force_push_tag "lightweight tag" "-f" test_force_push_tag "lightweight tag" "-f"
test_force_push_tag "annotated tag" "-f -a -m'msg'" test_force_push_tag "annotated tag" "-f -a -m'tag message'"

test_force_fetch_tag () {
tag_type_description=$1
tag_args=$2

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 &&
(
cd testrepo &&
git tag testTag &&
git -C ../child1 fetch origin tag testTag &&
>file1 &&
git add file1 &&
git commit -m 'file1' &&
git tag $tag_args testTag &&
test_must_fail git -C ../child1 fetch origin tag testTag &&
git -C ../child1 fetch origin '+refs/tags/*:refs/tags/*'
)
"
}

test_force_fetch_tag "lightweight tag" "-f"
test_force_fetch_tag "annotated tag" "-f -a -m'tag message'"


test_expect_success 'push --porcelain' ' test_expect_success 'push --porcelain' '
mk_empty testrepo && mk_empty testrepo &&

View File

@ -103,7 +103,7 @@ test_expect_success 'clone with --no-tags' '
test_expect_success '--single-branch while HEAD pointing at master' ' test_expect_success '--single-branch while HEAD pointing at master' '
( (
cd dir_master && cd dir_master &&
git fetch && git fetch --force &&
git for-each-ref refs/remotes/origin | git for-each-ref refs/remotes/origin |
sed -e "/HEAD$/d" \ sed -e "/HEAD$/d" \
-e "s|/remotes/origin/|/heads/|" >../actual -e "s|/remotes/origin/|/heads/|" >../actual
@ -114,7 +114,7 @@ test_expect_success '--single-branch while HEAD pointing at master' '
test_cmp expect actual && test_cmp expect actual &&
( (
cd dir_master && cd dir_master &&
git fetch --tags && git fetch --tags --force &&
git for-each-ref refs/tags >../actual git for-each-ref refs/tags >../actual
) && ) &&
git for-each-ref refs/tags >expect && git for-each-ref refs/tags >expect &&