fetch --prune: prune only based on explicit refspecs

The old behavior of "fetch --prune" was to prune whatever was being fetched. In particular, "fetch --prune --tags" caused tags not only to be fetched, but also to be pruned. This is inappropriate because there is only one tags namespace that is shared among the local repository and all remotes. Therefore, if the user defines a local tag and then runs "git fetch --prune --tags", then the local tag is deleted. Moreover, "--prune" and "--tags" can also be configured via fetch.prune / remote.<name>.prune and remote.<name>.tagopt, making it even less obvious that an invocation of "git fetch" could result in tag lossage. Since the command "git remote update" invokes "git fetch", it had the same problem. The command "git remote prune", on the other hand, disregarded the setting of remote.<name>.tagopt, and so its behavior was inconsistent with that of the other commands. So the old behavior made it too easy to lose tags. To fix this problem, change "fetch --prune" to prune references based only on refspecs specified explicitly by the user, either on the command line or via remote.<name>.fetch. Thus, tags are no longer made subject to pruning by the --tags option or the remote.<name>.tagopt setting. However, tags *are* still subject to pruning if they are fetched as part of a refspec, and that is good. For example: * On the command line, git fetch --prune 'refs/tags/*:refs/tags/*' causes tags, and only tags, to be fetched and pruned, and is therefore a simple way for the user to get the equivalent of the old behavior of "--prune --tag". * For a remote that was configured with the "--mirror" option, the configuration is set to include [remote "name"] fetch = +refs/*:refs/* , which causes tags to be subject to pruning along with all other references. This is the behavior that will typically be desired for a mirror. Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael Haggerty committed Oct 30, 2013 at 06:33 UTC 0838bf47b3b37f6ff13b8de183316483958bf3bb
4 files changed +30 -42
Documentation/config.txt
+2 -2
@@ -2087,8 +2087,8 @@ remote.<name>.vcs::
2087
2088 remote.<name>.prune::
2089 When set to true, fetching from this remote by default will also
2090 - remove any remote-tracking branches which no longer exist on the
2091 - remote (as if the `--prune` option was give on the command line).
2090 + remove any remote-tracking references that no longer exist on the
2091 + remote (as if the `--prune` option was given on the command line).
2092 Overrides `fetch.prune` settings, if any.
2093
2094 remotes.<group>::
Documentation/fetch-options.txt
+14 -5
@@ -41,8 +41,14 @@ ifndef::git-pull[]
41
42 -p::
43 --prune::
44 - After fetching, remove any remote-tracking branches which
45 - no longer exist on the remote.
44 + After fetching, remove any remote-tracking references that no
45 + longer exist on the remote. Tags are not subject to pruning
46 + if they are fetched only because of the default tag
47 + auto-following or due to a --tags option. However, if tags
48 + are fetched due to an explicit refspec (either on the command
49 + line or in the remote configuration, for example if the remote
50 + was cloned with the --mirror option), then they are also
51 + subject to pruning.
52 endif::git-pull[]
53
54 ifdef::git-pull[]
@@ -61,9 +67,12 @@ endif::git-pull[]
67 ifndef::git-pull[]
68 -t::
69 --tags::
64 - Request that all tags be fetched from the remote in addition
65 - to whatever else is being fetched. Its effect is similar to
66 - that of the refspec `refs/tags/*:refs/tags/*`.
70 + Fetch all tags from the remote (i.e., fetch remote tags
71 + `refs/tags/*` into local tags with the same name), in addition
72 + to whatever else would otherwise be fetched. Using this
73 + option alone does not subject tags to pruning, even if --prune
74 + is used (though tags may be pruned anyway if they are also the
75 + destination of an explicit refspec; see '--prune').
76
77 --recurse-submodules[=yes|on-demand|no]::
78 This option controls if and under what conditions new commits of
builtin/fetch.c
+9 -30
@@ -850,38 +850,17 @@ static int do_fetch(struct transport *transport,
850 goto cleanup;
851 }
852 if (prune) {
853 - struct refspec *prune_refspecs;
854 - int prune_refspec_count;
855 -
853 + /*
854 + * We only prune based on refspecs specified
855 + * explicitly (via command line or configuration); we
856 + * don't care whether --tags was specified.
857 + */
858 if (ref_count) {
857 - prune_refspecs = refs;
858 - prune_refspec_count = ref_count;
859 - } else {
860 - prune_refspecs = transport->remote->fetch;
861 - prune_refspec_count = transport->remote->fetch_refspec_nr;
862 - }
863 -
864 - if (tags == TAGS_SET) {
865 - /*
866 - * --tags was specified. Pretend that the user also
867 - * gave us the canonical tags refspec
868 - */
869 - const char *tags_str = "refs/tags/*:refs/tags/*";
870 - struct refspec *tags_refspec, *refspec;
871 -
872 - /* Copy the refspec and add the tags to it */
873 - refspec = xcalloc(prune_refspec_count + 1, sizeof(*refspec));
874 - tags_refspec = parse_fetch_refspec(1, &tags_str);
875 - memcpy(refspec, prune_refspecs, prune_refspec_count * sizeof(*refspec));
876 - memcpy(&refspec[prune_refspec_count], tags_refspec, sizeof(*refspec));
877 -
878 - prune_refs(refspec, prune_refspec_count + 1, ref_map);
879 -
880 - /* The rest of the strings belong to fetch_one */
881 - free_refspec(1, tags_refspec);
882 - free(refspec);
859 + prune_refs(refs, ref_count, ref_map);
860 } else {
884 - prune_refs(prune_refspecs, prune_refspec_count, ref_map);
861 + prune_refs(transport->remote->fetch,
862 + transport->remote->fetch_refspec_nr,
863 + ref_map);
864 }
865 }
866 free_refs(ref_map);
t/t5510-fetch.sh
+5 -5
@@ -113,7 +113,7 @@ test_expect_success 'fetch --prune with a namespace keeps other namespaces' '
113 git rev-parse origin/master
114 '
115
116 -test_expect_success 'fetch --prune --tags prunes tags and branches' '
116 +test_expect_success 'fetch --prune --tags prunes branches but not tags' '
117 cd "$D" &&
118 git clone . prune-tags &&
119 cd prune-tags &&
@@ -125,10 +125,10 @@ test_expect_success 'fetch --prune --tags prunes tags and branches' '
125 git fetch --prune --tags origin &&
126 git rev-parse origin/master &&
127 test_must_fail git rev-parse origin/fake-remote &&
128 - test_must_fail git rev-parse sometag
128 + git rev-parse sometag
129 '
130
131 -test_expect_success 'fetch --prune --tags with branch does not delete other remote-tracking branches' '
131 +test_expect_success 'fetch --prune --tags with branch does not prune other things' '
132 cd "$D" &&
133 git clone . prune-tags-branch &&
134 cd prune-tags-branch &&
@@ -137,7 +137,7 @@ test_expect_success 'fetch --prune --tags with branch does not delete other remo
137
138 git fetch --prune --tags origin master &&
139 git rev-parse origin/extrabranch &&
140 - test_must_fail git rev-parse sometag
140 + git rev-parse sometag
141 '
142
143 test_expect_success 'fetch --prune --tags with refspec prunes based on refspec' '
@@ -151,7 +151,7 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
151 git fetch --prune --tags origin refs/heads/foo/*:refs/remotes/origin/foo/* &&
152 test_must_fail git rev-parse refs/remotes/origin/foo/otherbranch &&
153 git rev-parse origin/extrabranch &&
154 - test_must_fail git rev-parse sometag
154 + git rev-parse sometag
155 '
156
157 test_expect_success 'fetch tags when there is no tags' '