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" (87po6ahx87.fsf@evledraar.gmail.com;
https://public-inbox.org/git/87po6ahx87.fsf@evledraar.gmail.com/) for
more background info.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Ævar Arnfjörð Bjarmason 2018-02-09 20:32:15 +00:00 committed by Junio C Hamano
parent e249ce0ccd
commit 97716d217c
8 changed files with 191 additions and 5 deletions

View File

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

View File

@ -73,7 +73,19 @@ ifndef::git-pull[]
are fetched due to an explicit refspec (either on the command
line or in the remote configuration, for example if the remote
was cloned with the --mirror option), then they are also
subject to pruning.
subject to pruning. Supplying `--prune-tags` is a shorthand for
providing the tag refspec.
+
See the PRUNING section below for more details.
-P::
--prune-tags::
Before fetching, remove any local tags that no longer exist on
the remote if `--prune` is enabled. This option should be used
more carefully, unlike `--prune` it will remove any local
references (local tags) that have been created. This option is
a shorthand for providing the explicit tag refspec along with
`--prune`, see the discussion about that in its documentation.
+
See the PRUNING section below for more details.

View File

@ -148,6 +148,53 @@ So be careful when using this with a refspec like
`refs/tags/*:refs/tags/*`, or any other refspec which might map
references from multiple remotes to the same local namespace.
Since keeping up-to-date with both branches and tags on the remote is
a common use-case the `--prune-tags` option can be supplied along with
`--prune` to prune local tags that don't exist on the remote, and
force-update those tags that differ. Tag pruning can also be enabled
with `fetch.pruneTags` or `remote.<name>.pruneTags` in the config. See
linkgit:git-config[1].
The `--prune-tags` option is equivalent to having
`refs/tags/*:refs/tags/*` declared in the refspecs of the remote. This
can lead to some seemingly strange interactions:
------------------------------------------------
# These both fetch tags
$ git fetch --no-tags origin 'refs/tags/*:refs/tags/*'
$ git fetch --no-tags --prune-tags origin
------------------------------------------------
The reason it doesn't error out when provided without `--prune` or its
config versions is for flexibility of the configured versions, and to
maintain a 1=1 mapping between what the command line flags do, and
what the configuration versions do.
It's reasonable to e.g. configure `fetch.pruneTags=true` in
`~/.gitconfig` to have tags pruned whenever `git fetch --prune` is
run, without making every invocation of `git fetch` without `--prune`
an error.
Another special case of `--prune-tags` is that
`refs/tags/*:refs/tags/*` will not be implicitly provided if an URL is
being fetched. I.e.:
------------------------------------------------
$ git fetch <url> --prune --prune-tags
------------------------------------------------
Will prune no tags, as opposed to:
------------------------------------------------
$ git fetch origin --prune --prune-tags
------------------------------------------------
To prune tags given a URL supply the refspec explicitly:
------------------------------------------------
$ git fetch <url> --prune 'refs/tags/*:refs/tags/*'
------------------------------------------------
OUTPUT
------

View File

@ -38,6 +38,10 @@ static int fetch_prune_config = -1; /* unspecified */
static int prune = -1; /* unspecified */
#define PRUNE_BY_DEFAULT 0 /* do we prune by default? */
static int fetch_prune_tags_config = -1; /* unspecified */
static int prune_tags = -1; /* unspecified */
#define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */
static int all, append, dry_run, force, keep, multiple, update_head_ok, verbosity, deepen_relative;
static int progress = -1;
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)
return 0;
}
if (!strcmp(k, "fetch.prunetags")) {
fetch_prune_tags_config = git_config_bool(k, v);
return 0;
}
if (!strcmp(k, "submodule.recurse")) {
int r = git_config_bool(k, v) ?
RECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF;
@ -126,6 +135,8 @@ static struct option builtin_fetch_options[] = {
N_("number of submodules fetched in parallel")),
OPT_BOOL('p', "prune", &prune,
N_("prune remote-tracking branches no longer on remote")),
OPT_BOOL('P', "prune-tags", &prune_tags,
N_("prune local tags no longer on remote and clobber changed tags")),
{ OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, N_("on-demand"),
N_("control recursive fetching of submodules"),
PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules },
@ -1212,6 +1223,8 @@ static void add_options_to_argv(struct argv_array *argv)
argv_array_push(argv, "--dry-run");
if (prune != -1)
argv_array_push(argv, prune ? "--prune" : "--no-prune");
if (prune_tags != -1)
argv_array_push(argv, prune_tags ? "--prune-tags" : "--no-prune-tags");
if (update_head_ok)
argv_array_push(argv, "--update-head-ok");
if (force)
@ -1265,7 +1278,7 @@ static int fetch_multiple(struct string_list *list)
return result;
}
static int fetch_one(struct remote *remote, int argc, const char **argv)
static int fetch_one(struct remote *remote, int argc, const char **argv, int prune_tags_ok)
{
static const char **refs = NULL;
struct refspec *refspec;
@ -1288,6 +1301,19 @@ static int fetch_one(struct remote *remote, int argc, const char **argv)
prune = PRUNE_BY_DEFAULT;
}
if (prune_tags < 0) {
/* no command line request */
if (0 <= remote->prune_tags)
prune_tags = remote->prune_tags;
else if (0 <= fetch_prune_tags_config)
prune_tags = fetch_prune_tags_config;
else
prune_tags = PRUNE_TAGS_BY_DEFAULT;
}
if (prune_tags_ok && prune_tags && remote_is_configured(remote, 0))
add_prune_tags_to_fetch_refspec(remote);
if (argc > 0) {
int j = 0;
int i;
@ -1368,7 +1394,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
} else if (argc == 0) {
/* No arguments -- use default remote */
remote = remote_get(NULL);
result = fetch_one(remote, argc, argv);
result = fetch_one(remote, argc, argv, 1);
} else if (multiple) {
/* All arguments are assumed to be remotes or groups */
for (i = 0; i < argc; i++)
@ -1386,7 +1412,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
} else {
/* Zero or one remotes */
remote = remote_get(argv[0]);
result = fetch_one(remote, argc-1, argv+1);
result = fetch_one(remote, argc-1, argv+1, argc == 1);
}
}

View File

@ -1468,7 +1468,7 @@ __git_fetch_recurse_submodules="yes on-demand no"
__git_fetch_options="
--quiet --verbose --append --upload-pack --force --keep --depth=
--tags --no-tags --all --prune --dry-run --recurse-submodules=
--unshallow --update-shallow
--unshallow --update-shallow --prune-tags
"
_git_fetch ()

View File

@ -104,6 +104,17 @@ static void add_fetch_refspec(struct remote *remote, const char *ref)
remote->fetch_refspec[remote->fetch_refspec_nr++] = ref;
}
void add_prune_tags_to_fetch_refspec(struct remote *remote)
{
int nr = remote->fetch_refspec_nr;
int bufsize = nr + 1;
int size = sizeof(struct refspec);
remote->fetch = xrealloc(remote->fetch, size * bufsize);
memcpy(&remote->fetch[nr], tag_refspec, size);
add_fetch_refspec(remote, xstrdup(TAG_REFSPEC));
}
static void add_url(struct remote *remote, const char *url)
{
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)
ret = xcalloc(1, sizeof(struct remote));
ret->prune = -1; /* unspecified */
ret->prune_tags = -1; /* unspecified */
ALLOC_GROW(remotes, remotes_nr + 1, remotes_alloc);
remotes[remotes_nr++] = ret;
ret->name = xstrndup(name, len);
@ -392,6 +404,8 @@ static int handle_config(const char *key, const char *value, void *cb)
remote->skip_default_update = git_config_bool(key, value);
else if (!strcmp(subkey, "prune"))
remote->prune = git_config_bool(key, value);
else if (!strcmp(subkey, "prunetags"))
remote->prune_tags = git_config_bool(key, value);
else if (!strcmp(subkey, "url")) {
const char *v;
if (git_config_string(&v, key, value))

View File

@ -47,6 +47,7 @@ struct remote {
int skip_default_update;
int mirror;
int prune;
int prune_tags;
const char *receivepack;
const char *uploadpack;
@ -299,4 +300,6 @@ void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
#define TAG_REFSPEC "refs/tags/*:refs/tags/*"
void add_prune_tags_to_fetch_refspec(struct remote *remote);
#endif

View File

@ -589,6 +589,15 @@ test_configured_prune_type () {
new_cmdline=$(printf "%s" "$cmdline" | perl -pe 's[origin(?!/)]["'"$remote_url"'"]g')
fi
if test "$fetch_prune_tags" = 'true' ||
test "$remote_origin_prune_tags" = 'true'
then
if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
then
new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
fi
fi
cmdline="$new_cmdline"
fi
@ -702,6 +711,67 @@ test_configured_prune true true unset unset kept pruned \
test_configured_prune true true unset unset pruned pruned \
"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
# --prune-tags on its own does nothing, needs --prune as well, same
# for for fetch.pruneTags without fetch.prune
test_configured_prune unset unset unset unset kept kept "--prune-tags"
test_configured_prune unset unset true unset kept kept ""
test_configured_prune unset unset unset true kept kept ""
# These will prune the tags
test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
test_configured_prune true unset true unset pruned pruned ""
test_configured_prune unset true unset true pruned pruned ""
# remote.<name>.pruneTags overrides fetch.pruneTags, just like
# remote.<name>.prune overrides fetch.prune if set.
test_configured_prune true unset true unset pruned pruned ""
test_configured_prune false true false true pruned pruned ""
test_configured_prune true false true false kept kept ""
# When --prune-tags is supplied it's ignored if an explicit refspec is
# given, same for the configuration options.
test_configured_prune unset unset unset unset pruned kept \
"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
test_configured_prune unset unset true unset pruned kept \
"--prune origin +refs/heads/*:refs/remotes/origin/*"
test_configured_prune unset unset unset true pruned kept \
"--prune origin +refs/heads/*:refs/remotes/origin/*"
# Pruning that also takes place if a file:// url replaces a named
# remote, with the exception of --prune-tags on the command-line
# (arbitrary limitation).
#
# However, because there's no implicit
# +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
# command-line negates --prune-tags, the branches will not be pruned.
test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
test_configured_prune_type unset unset unset unset kept kept "origin --prune --prune-tags" "link"
test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
test_configured_prune_type unset unset unset unset kept kept "--prune --prune-tags origin" "link"
test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
test_configured_prune_type true unset true unset pruned pruned "origin" "name"
test_configured_prune_type true unset true unset kept pruned "origin" "link"
test_configured_prune_type unset true true unset pruned pruned "origin" "name"
test_configured_prune_type unset true true unset kept pruned "origin" "link"
test_configured_prune_type unset true unset true pruned pruned "origin" "name"
test_configured_prune_type unset true unset true kept pruned "origin" "link"
# Interaction between --prune-tags and no "fetch" config in the remote
# at all.
test_expect_success 'remove remote.origin.fetch "one"' '
(
cd one &&
git config --unset-all remote.origin.fetch
)
'
test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "name"
test_configured_prune_type unset unset unset unset kept kept "origin --prune --prune-tags" "link"
test_expect_success 'all boundary commits are excluded' '
test_commit base &&
test_commit oneside &&