Skip to content

Commit 97716d2

Browse files
avargitster
authored andcommitted
fetch: add a --prune-tags option and fetch.pruneTags config
Add a --prune-tags option to git-fetch, along with fetch.pruneTags config option and a -P shorthand (-p is --prune). This allows for doing any of: git fetch -p -P git fetch --prune --prune-tags git fetch -p -P origin git fetch --prune --prune-tags origin Or simply: git config fetch.prune true && git config fetch.pruneTags true && git fetch Instead of the much more verbose: git fetch --prune origin 'refs/tags/*:refs/tags/*' '+refs/heads/*:refs/remotes/origin/*' Before this feature it was painful to support the use-case of pulling from a repo which is having both its branches *and* tags deleted regularly, and have our local references to reflect upstream. At work we create deployment tags in the repo for each rollout, and there's *lots* of those, so they're archived within weeks for performance reasons. Without this change it's hard to centrally configure such repos in /etc/gitconfig (on servers that are only used for working with them). You need to set fetch.prune=true globally, and then for each repo: git -C {} config --replace-all remote.origin.fetch "refs/tags/*:refs/tags/*" "^\+*refs/tags/\*:refs/tags/\*$" Now I can simply set fetch.pruneTags=true in /etc/gitconfig as well, and users running "git pull" will automatically get the pruning semantics I want. Even though "git remote" has corresponding "prune" and "update --prune" subcommands I'm intentionally not adding a corresponding prune-tags or "update --prune --prune-tags" mode to that command. It's advertised (as noted in my recent "git remote doc: correct dangerous lies about what prune does") as only modifying remote tracking references, whereas any --prune-tags option is always going to modify what from the user's perspective is a local copy of the tag, since there's no such thing as a remote tracking tag. Ideally add_prune_tags_to_fetch_refspec() would be something that would use ALLOC_GROW() to grow the 'fetch` member of the 'remote' struct. Instead I'm realloc-ing remote->fetch and adding the tag_refspec to the end. The reason is that parse_{fetch,push}_refspec which allocate the refspec (ultimately remote->fetch) struct are called many places that don't have access to a 'remote' struct. It would be hard to change all their callsites to be amenable to carry around the bookkeeping variables required for dynamic allocation. All the other callers of the API first incrementally construct the string version of the refspec in remote->fetch_refspec via add_fetch_refspec(), before finally calling parse_fetch_refspec() via some variation of remote_get(). It's less of a pain to deal with the one special case that needs to modify already constructed refspecs than to chase down and change all the other callsites. The API I'm adding is intentionally not generalized because if we add more of these we'd probably want to re-visit how this is done. See my "Re: [BUG] git remote prune removes local tags, depending on fetch config" ([email protected]; https://public-inbox.org/git/[email protected]/) for more background info. Signed-off-by: Ævar Arnfjörð Bjarmason <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e249ce0 commit 97716d2

File tree

8 files changed

+191
-5
lines changed

8 files changed

+191
-5
lines changed

Documentation/config.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,14 @@ fetch.prune::
14011401
option was given on the command line. See also `remote.<name>.prune`
14021402
and the PRUNING section of linkgit:git-fetch[1].
14031403

1404+
fetch.pruneTags::
1405+
If true, fetch will automatically behave as if the
1406+
`refs/tags/*:refs/tags/*` refspec was provided when pruning,
1407+
if not set already. This allows for setting both this option
1408+
and `fetch.prune` to maintain a 1=1 mapping to upstream
1409+
refs. See also `remote.<name>.pruneTags` and the PRUNING
1410+
section of linkgit:git-fetch[1].
1411+
14041412
fetch.output::
14051413
Control how ref update status is printed. Valid values are
14061414
`full` and `compact`. Default value is `full`. See section
@@ -2945,6 +2953,12 @@ remote.<name>.prune::
29452953
remove any remote-tracking references that no longer exist on the
29462954
remote (as if the `--prune` option was given on the command line).
29472955
Overrides `fetch.prune` settings, if any.
2956+
2957+
remote.<name>.pruneTags::
2958+
When set to true, fetching from this remote by default will also
2959+
remove any local tags that no longer exist on the remote if pruning
2960+
is activated in general via `remote.<name>.prune`, `fetch.prune` or
2961+
`--prune`. Overrides `fetch.pruneTags` settings, if any.
29482962
+
29492963
See also `remote.<name>.prune` and the PRUNING section of
29502964
linkgit:git-fetch[1].

Documentation/fetch-options.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,19 @@ ifndef::git-pull[]
7373
are fetched due to an explicit refspec (either on the command
7474
line or in the remote configuration, for example if the remote
7575
was cloned with the --mirror option), then they are also
76-
subject to pruning.
76+
subject to pruning. Supplying `--prune-tags` is a shorthand for
77+
providing the tag refspec.
78+
+
79+
See the PRUNING section below for more details.
80+
81+
-P::
82+
--prune-tags::
83+
Before fetching, remove any local tags that no longer exist on
84+
the remote if `--prune` is enabled. This option should be used
85+
more carefully, unlike `--prune` it will remove any local
86+
references (local tags) that have been created. This option is
87+
a shorthand for providing the explicit tag refspec along with
88+
`--prune`, see the discussion about that in its documentation.
7789
+
7890
See the PRUNING section below for more details.
7991

Documentation/git-fetch.txt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,53 @@ So be careful when using this with a refspec like
148148
`refs/tags/*:refs/tags/*`, or any other refspec which might map
149149
references from multiple remotes to the same local namespace.
150150

151+
Since keeping up-to-date with both branches and tags on the remote is
152+
a common use-case the `--prune-tags` option can be supplied along with
153+
`--prune` to prune local tags that don't exist on the remote, and
154+
force-update those tags that differ. Tag pruning can also be enabled
155+
with `fetch.pruneTags` or `remote.<name>.pruneTags` in the config. See
156+
linkgit:git-config[1].
157+
158+
The `--prune-tags` option is equivalent to having
159+
`refs/tags/*:refs/tags/*` declared in the refspecs of the remote. This
160+
can lead to some seemingly strange interactions:
161+
162+
------------------------------------------------
163+
# These both fetch tags
164+
$ git fetch --no-tags origin 'refs/tags/*:refs/tags/*'
165+
$ git fetch --no-tags --prune-tags origin
166+
------------------------------------------------
167+
168+
The reason it doesn't error out when provided without `--prune` or its
169+
config versions is for flexibility of the configured versions, and to
170+
maintain a 1=1 mapping between what the command line flags do, and
171+
what the configuration versions do.
172+
173+
It's reasonable to e.g. configure `fetch.pruneTags=true` in
174+
`~/.gitconfig` to have tags pruned whenever `git fetch --prune` is
175+
run, without making every invocation of `git fetch` without `--prune`
176+
an error.
177+
178+
Another special case of `--prune-tags` is that
179+
`refs/tags/*:refs/tags/*` will not be implicitly provided if an URL is
180+
being fetched. I.e.:
181+
182+
------------------------------------------------
183+
$ git fetch <url> --prune --prune-tags
184+
------------------------------------------------
185+
186+
Will prune no tags, as opposed to:
187+
188+
------------------------------------------------
189+
$ git fetch origin --prune --prune-tags
190+
------------------------------------------------
191+
192+
To prune tags given a URL supply the refspec explicitly:
193+
194+
------------------------------------------------
195+
$ git fetch <url> --prune 'refs/tags/*:refs/tags/*'
196+
------------------------------------------------
197+
151198
OUTPUT
152199
------
153200

builtin/fetch.c

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ static int fetch_prune_config = -1; /* unspecified */
3838
static int prune = -1; /* unspecified */
3939
#define PRUNE_BY_DEFAULT 0 /* do we prune by default? */
4040

41+
static int fetch_prune_tags_config = -1; /* unspecified */
42+
static int prune_tags = -1; /* unspecified */
43+
#define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */
44+
4145
static int all, append, dry_run, force, keep, multiple, update_head_ok, verbosity, deepen_relative;
4246
static int progress = -1;
4347
static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen;
@@ -64,6 +68,11 @@ static int git_fetch_config(const char *k, const char *v, void *cb)
6468
return 0;
6569
}
6670

71+
if (!strcmp(k, "fetch.prunetags")) {
72+
fetch_prune_tags_config = git_config_bool(k, v);
73+
return 0;
74+
}
75+
6776
if (!strcmp(k, "submodule.recurse")) {
6877
int r = git_config_bool(k, v) ?
6978
RECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF;
@@ -126,6 +135,8 @@ static struct option builtin_fetch_options[] = {
126135
N_("number of submodules fetched in parallel")),
127136
OPT_BOOL('p', "prune", &prune,
128137
N_("prune remote-tracking branches no longer on remote")),
138+
OPT_BOOL('P', "prune-tags", &prune_tags,
139+
N_("prune local tags no longer on remote and clobber changed tags")),
129140
{ OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, N_("on-demand"),
130141
N_("control recursive fetching of submodules"),
131142
PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules },
@@ -1212,6 +1223,8 @@ static void add_options_to_argv(struct argv_array *argv)
12121223
argv_array_push(argv, "--dry-run");
12131224
if (prune != -1)
12141225
argv_array_push(argv, prune ? "--prune" : "--no-prune");
1226+
if (prune_tags != -1)
1227+
argv_array_push(argv, prune_tags ? "--prune-tags" : "--no-prune-tags");
12151228
if (update_head_ok)
12161229
argv_array_push(argv, "--update-head-ok");
12171230
if (force)
@@ -1265,7 +1278,7 @@ static int fetch_multiple(struct string_list *list)
12651278
return result;
12661279
}
12671280

1268-
static int fetch_one(struct remote *remote, int argc, const char **argv)
1281+
static int fetch_one(struct remote *remote, int argc, const char **argv, int prune_tags_ok)
12691282
{
12701283
static const char **refs = NULL;
12711284
struct refspec *refspec;
@@ -1288,6 +1301,19 @@ static int fetch_one(struct remote *remote, int argc, const char **argv)
12881301
prune = PRUNE_BY_DEFAULT;
12891302
}
12901303

1304+
if (prune_tags < 0) {
1305+
/* no command line request */
1306+
if (0 <= remote->prune_tags)
1307+
prune_tags = remote->prune_tags;
1308+
else if (0 <= fetch_prune_tags_config)
1309+
prune_tags = fetch_prune_tags_config;
1310+
else
1311+
prune_tags = PRUNE_TAGS_BY_DEFAULT;
1312+
}
1313+
1314+
if (prune_tags_ok && prune_tags && remote_is_configured(remote, 0))
1315+
add_prune_tags_to_fetch_refspec(remote);
1316+
12911317
if (argc > 0) {
12921318
int j = 0;
12931319
int i;
@@ -1368,7 +1394,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
13681394
} else if (argc == 0) {
13691395
/* No arguments -- use default remote */
13701396
remote = remote_get(NULL);
1371-
result = fetch_one(remote, argc, argv);
1397+
result = fetch_one(remote, argc, argv, 1);
13721398
} else if (multiple) {
13731399
/* All arguments are assumed to be remotes or groups */
13741400
for (i = 0; i < argc; i++)
@@ -1386,7 +1412,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
13861412
} else {
13871413
/* Zero or one remotes */
13881414
remote = remote_get(argv[0]);
1389-
result = fetch_one(remote, argc-1, argv+1);
1415+
result = fetch_one(remote, argc-1, argv+1, argc == 1);
13901416
}
13911417
}
13921418

contrib/completion/git-completion.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1468,7 +1468,7 @@ __git_fetch_recurse_submodules="yes on-demand no"
14681468
__git_fetch_options="
14691469
--quiet --verbose --append --upload-pack --force --keep --depth=
14701470
--tags --no-tags --all --prune --dry-run --recurse-submodules=
1471-
--unshallow --update-shallow
1471+
--unshallow --update-shallow --prune-tags
14721472
"
14731473

14741474
_git_fetch ()

remote.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ static void add_fetch_refspec(struct remote *remote, const char *ref)
104104
remote->fetch_refspec[remote->fetch_refspec_nr++] = ref;
105105
}
106106

107+
void add_prune_tags_to_fetch_refspec(struct remote *remote)
108+
{
109+
int nr = remote->fetch_refspec_nr;
110+
int bufsize = nr + 1;
111+
int size = sizeof(struct refspec);
112+
113+
remote->fetch = xrealloc(remote->fetch, size * bufsize);
114+
memcpy(&remote->fetch[nr], tag_refspec, size);
115+
add_fetch_refspec(remote, xstrdup(TAG_REFSPEC));
116+
}
117+
107118
static void add_url(struct remote *remote, const char *url)
108119
{
109120
ALLOC_GROW(remote->url, remote->url_nr + 1, remote->url_alloc);
@@ -174,6 +185,7 @@ static struct remote *make_remote(const char *name, int len)
174185

175186
ret = xcalloc(1, sizeof(struct remote));
176187
ret->prune = -1; /* unspecified */
188+
ret->prune_tags = -1; /* unspecified */
177189
ALLOC_GROW(remotes, remotes_nr + 1, remotes_alloc);
178190
remotes[remotes_nr++] = ret;
179191
ret->name = xstrndup(name, len);
@@ -392,6 +404,8 @@ static int handle_config(const char *key, const char *value, void *cb)
392404
remote->skip_default_update = git_config_bool(key, value);
393405
else if (!strcmp(subkey, "prune"))
394406
remote->prune = git_config_bool(key, value);
407+
else if (!strcmp(subkey, "prunetags"))
408+
remote->prune_tags = git_config_bool(key, value);
395409
else if (!strcmp(subkey, "url")) {
396410
const char *v;
397411
if (git_config_string(&v, key, value))

remote.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ struct remote {
4747
int skip_default_update;
4848
int mirror;
4949
int prune;
50+
int prune_tags;
5051

5152
const char *receivepack;
5253
const char *uploadpack;
@@ -299,4 +300,6 @@ void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
299300

300301
#define TAG_REFSPEC "refs/tags/*:refs/tags/*"
301302

303+
void add_prune_tags_to_fetch_refspec(struct remote *remote);
304+
302305
#endif

t/t5510-fetch.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,15 @@ test_configured_prune_type () {
589589
new_cmdline=$(printf "%s" "$cmdline" | perl -pe 's[origin(?!/)]["'"$remote_url"'"]g')
590590
fi
591591

592+
if test "$fetch_prune_tags" = 'true' ||
593+
test "$remote_origin_prune_tags" = 'true'
594+
then
595+
if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
596+
then
597+
new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
598+
fi
599+
fi
600+
592601
cmdline="$new_cmdline"
593602
fi
594603

@@ -702,6 +711,67 @@ test_configured_prune true true unset unset kept pruned \
702711
test_configured_prune true true unset unset pruned pruned \
703712
"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
704713

714+
# --prune-tags on its own does nothing, needs --prune as well, same
715+
# for for fetch.pruneTags without fetch.prune
716+
test_configured_prune unset unset unset unset kept kept "--prune-tags"
717+
test_configured_prune unset unset true unset kept kept ""
718+
test_configured_prune unset unset unset true kept kept ""
719+
720+
# These will prune the tags
721+
test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
722+
test_configured_prune true unset true unset pruned pruned ""
723+
test_configured_prune unset true unset true pruned pruned ""
724+
725+
# remote.<name>.pruneTags overrides fetch.pruneTags, just like
726+
# remote.<name>.prune overrides fetch.prune if set.
727+
test_configured_prune true unset true unset pruned pruned ""
728+
test_configured_prune false true false true pruned pruned ""
729+
test_configured_prune true false true false kept kept ""
730+
731+
# When --prune-tags is supplied it's ignored if an explicit refspec is
732+
# given, same for the configuration options.
733+
test_configured_prune unset unset unset unset pruned kept \
734+
"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
735+
test_configured_prune unset unset true unset pruned kept \
736+
"--prune origin +refs/heads/*:refs/remotes/origin/*"
737+
test_configured_prune unset unset unset true pruned kept \
738+
"--prune origin +refs/heads/*:refs/remotes/origin/*"
739+
740+
# Pruning that also takes place if a file:// url replaces a named
741+
# remote, with the exception of --prune-tags on the command-line
742+
# (arbitrary limitation).
743+
#
744+
# However, because there's no implicit
745+
# +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
746+
# command-line negates --prune-tags, the branches will not be pruned.
747+
test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
748+
test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
749+
test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
750+
test_configured_prune_type unset unset unset unset kept kept "origin --prune --prune-tags" "link"
751+
test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
752+
test_configured_prune_type unset unset unset unset kept kept "--prune --prune-tags origin" "link"
753+
test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
754+
test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
755+
test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
756+
test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
757+
test_configured_prune_type true unset true unset pruned pruned "origin" "name"
758+
test_configured_prune_type true unset true unset kept pruned "origin" "link"
759+
test_configured_prune_type unset true true unset pruned pruned "origin" "name"
760+
test_configured_prune_type unset true true unset kept pruned "origin" "link"
761+
test_configured_prune_type unset true unset true pruned pruned "origin" "name"
762+
test_configured_prune_type unset true unset true kept pruned "origin" "link"
763+
764+
# Interaction between --prune-tags and no "fetch" config in the remote
765+
# at all.
766+
test_expect_success 'remove remote.origin.fetch "one"' '
767+
(
768+
cd one &&
769+
git config --unset-all remote.origin.fetch
770+
)
771+
'
772+
test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "name"
773+
test_configured_prune_type unset unset unset unset kept kept "origin --prune --prune-tags" "link"
774+
705775
test_expect_success 'all boundary commits are excluded' '
706776
test_commit base &&
707777
test_commit oneside &&

0 commit comments

Comments
 (0)