diff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt index 8bc36af4b1..fa0a3151b3 100644 --- a/Documentation/fetch-options.txt +++ b/Documentation/fetch-options.txt @@ -68,11 +68,16 @@ endif::git-pull[] -f:: --force:: - When 'git fetch' is used with `:` - refspec, it refuses to update the local branch - `` unless the remote branch `` it - fetches is a descendant of ``. This option - overrides that check. + When 'git fetch' is used with `:` refspec it may + refuse to update the local branch as discussed +ifdef::git-pull[] + in the `` part of the linkgit:git-fetch[1] + documentation. +endif::git-pull[] +ifndef::git-pull[] + in the `` part below. +endif::git-pull[] + This option overrides that check. -k:: --keep:: diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt index 55277a9781..f345bd30fc 100644 --- a/Documentation/git-push.txt +++ b/Documentation/git-push.txt @@ -74,22 +74,57 @@ without any `` on the command line. Otherwise, missing `:` means to update the same ref as the ``. + The object referenced by is used to update the reference -on the remote side. By default this is only allowed if is not -a tag (annotated or lightweight), and then only if it can fast-forward -. By having the optional leading `+`, you can tell Git to update -the ref even if it is not allowed by default (e.g., it is not a -fast-forward.) This does *not* attempt to merge into . See -EXAMPLES below for details. +on the remote side. Whether this is allowed depends on where in +`refs/*` the reference lives as described in detail below, in +those sections "update" means any modifications except deletes, which +as noted after the next few sections are treated differently. + -`tag ` means the same as `refs/tags/:refs/tags/`. +The `refs/heads/*` namespace will only accept commit objects, and +updates only if they can be fast-forwarded. + -Pushing an empty allows you to delete the ref from -the remote repository. +The `refs/tags/*` namespace will accept any kind of object (as +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 allows you to delete the 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) 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 already exists on the remote side. ++ +`tag ` means the same as `refs/tags/:refs/tags/`. --all:: Push all branches (i.e. refs under `refs/heads/`); cannot be diff --git a/Documentation/gitrevisions.txt b/Documentation/gitrevisions.txt index 1f6cceaefb..d407b7dee1 100644 --- a/Documentation/gitrevisions.txt +++ b/Documentation/gitrevisions.txt @@ -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 also specify a range of revisions explicitly. -In addition, some Git commands (such as linkgit:git-show[1]) also take -revision parameters which denote other objects than commits, e.g. blobs -("files") or trees ("directories of files"). +In addition, some Git commands (such as linkgit:git-show[1] and +linkgit:git-push[1]) can also take revision parameters which denote +other objects than commits, e.g. blobs ("files") or trees +("directories of files"). include::revisions.txt[] diff --git a/Documentation/pull-fetch-param.txt b/Documentation/pull-fetch-param.txt index f1fb08dc68..293c6b967d 100644 --- a/Documentation/pull-fetch-param.txt +++ b/Documentation/pull-fetch-param.txt @@ -33,11 +33,40 @@ name. it requests fetching everything up to the given tag. + The remote ref that matches -is fetched, and if is not an empty string, the local -ref that matches it is fast-forwarded using . -If the optional plus `+` is used, the local ref -is updated even if it does not result in a fast-forward -update. +is fetched, and if is not an empty string, an attempt +is made to update the local ref that matches it. ++ +Whether that update is allowed without `--force` depends on the ref +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 `...` +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] When the remote branch you want to fetch is known to diff --git a/builtin/fetch.c b/builtin/fetch.c index dc0931fb46..0696abfc2a 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -115,7 +115,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, @@ -668,12 +668,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(the_repository, diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 539c25aada..7a8f56db53 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -969,7 +969,7 @@ test_force_push_tag () { tag_type_description=$1 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_child testrepo child1 && mk_child testrepo child2 && @@ -1009,7 +1009,32 @@ test_force_push_tag () { } 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' ' mk_empty testrepo && diff --git a/t/t5612-clone-refspec.sh b/t/t5612-clone-refspec.sh index 5582b3d5fd..e36ac01661 100755 --- a/t/t5612-clone-refspec.sh +++ b/t/t5612-clone-refspec.sh @@ -103,7 +103,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 @@ -114,7 +114,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 &&