Merge branch 'ds/maintenance-prefetch-fix'

The prefetch task in "git maintenance" assumed that "git fetch"
from any remote would fetch all its local branches, which would
fetch too much if the user is interested in only a subset of
branches there.

* ds/maintenance-prefetch-fix:
  maintenance: respect remote.*.skipFetchAll
  maintenance: use 'git fetch --prefetch'
  fetch: add --prefetch option
  maintenance: simplify prefetch logic
This commit is contained in:
Junio C Hamano 2021-04-30 13:50:25 +09:00
commit d250f90359
6 changed files with 134 additions and 40 deletions

View File

@ -110,6 +110,11 @@ ifndef::git-pull[]
setting `fetch.writeCommitGraph`.
endif::git-pull[]
--prefetch::
Modify the configured refspec to place all refs into the
`refs/prefetch/` namespace. See the `prefetch` task in
linkgit:git-maintenance[1].
-p::
--prune::
Before fetching, remove any remote-tracking references that no

View File

@ -92,10 +92,8 @@ commit-graph::
prefetch::
The `prefetch` task updates the object directory with the latest
objects from all registered remotes. For each remote, a `git fetch`
command is run. The refmap is custom to avoid updating local or remote
branches (those in `refs/heads` or `refs/remotes`). Instead, the
remote refs are stored in `refs/prefetch/<remote>/`. Also, tags are
not updated.
command is run. The configured refspec is modified to place all
requested refs within `refs/prefetch/`. Also, tags are not updated.
+
This is done to avoid disrupting the remote-tracking branches. The end users
expect these refs to stay unmoved unless they initiate a fetch. With prefetch

View File

@ -48,6 +48,7 @@ enum {
static int fetch_prune_config = -1; /* unspecified */
static int fetch_show_forced_updates = 1;
static uint64_t forced_updates_ms = 0;
static int prefetch = 0;
static int prune = -1; /* unspecified */
#define PRUNE_BY_DEFAULT 0 /* do we prune by default? */
@ -158,6 +159,8 @@ static struct option builtin_fetch_options[] = {
N_("do not fetch all tags (--no-tags)"), TAGS_UNSET),
OPT_INTEGER('j', "jobs", &max_jobs,
N_("number of submodules fetched in parallel")),
OPT_BOOL(0, "prefetch", &prefetch,
N_("modify the refspec to place all refs within refs/prefetch/")),
OPT_BOOL('p', "prune", &prune,
N_("prune remote-tracking branches no longer on remote")),
OPT_BOOL('P', "prune-tags", &prune_tags,
@ -436,6 +439,56 @@ static void find_non_local_tags(const struct ref *refs,
oidset_clear(&fetch_oids);
}
static void filter_prefetch_refspec(struct refspec *rs)
{
int i;
if (!prefetch)
return;
for (i = 0; i < rs->nr; i++) {
struct strbuf new_dst = STRBUF_INIT;
char *old_dst;
const char *sub = NULL;
if (rs->items[i].negative)
continue;
if (!rs->items[i].dst ||
(rs->items[i].src &&
!strncmp(rs->items[i].src, "refs/tags/", 10))) {
int j;
free(rs->items[i].src);
free(rs->items[i].dst);
for (j = i + 1; j < rs->nr; j++) {
rs->items[j - 1] = rs->items[j];
rs->raw[j - 1] = rs->raw[j];
}
rs->nr--;
i--;
continue;
}
old_dst = rs->items[i].dst;
strbuf_addstr(&new_dst, "refs/prefetch/");
/*
* If old_dst starts with "refs/", then place
* sub after that prefix. Otherwise, start at
* the beginning of the string.
*/
if (!skip_prefix(old_dst, "refs/", &sub))
sub = old_dst;
strbuf_addstr(&new_dst, sub);
rs->items[i].dst = strbuf_detach(&new_dst, NULL);
rs->items[i].force = 1;
free(old_dst);
}
}
static struct ref *get_ref_map(struct remote *remote,
const struct ref *remote_refs,
struct refspec *rs,
@ -452,6 +505,10 @@ static struct ref *get_ref_map(struct remote *remote,
struct hashmap existing_refs;
int existing_refs_populated = 0;
filter_prefetch_refspec(rs);
if (remote)
filter_prefetch_refspec(&remote->fetch);
if (rs->nr) {
struct refspec *fetch_refspec;
@ -520,7 +577,7 @@ static struct ref *get_ref_map(struct remote *remote,
if (has_merge &&
!strcmp(branch->remote_name, remote->name))
add_merge_config(&ref_map, remote_refs, branch, &tail);
} else {
} else if (!prefetch) {
ref_map = get_remote_ref(remote_refs, "HEAD");
if (!ref_map)
die(_("Couldn't find remote ref HEAD"));

View File

@ -873,55 +873,40 @@ static int maintenance_task_commit_graph(struct maintenance_run_opts *opts)
return 0;
}
static int fetch_remote(const char *remote, struct maintenance_run_opts *opts)
static int fetch_remote(struct remote *remote, void *cbdata)
{
struct maintenance_run_opts *opts = cbdata;
struct child_process child = CHILD_PROCESS_INIT;
if (remote->skip_default_update)
return 0;
child.git_cmd = 1;
strvec_pushl(&child.args, "fetch", remote, "--prune", "--no-tags",
strvec_pushl(&child.args, "fetch", remote->name,
"--prefetch", "--prune", "--no-tags",
"--no-write-fetch-head", "--recurse-submodules=no",
"--refmap=", NULL);
NULL);
if (opts->quiet)
strvec_push(&child.args, "--quiet");
strvec_pushf(&child.args, "+refs/heads/*:refs/prefetch/%s/*", remote);
return !!run_command(&child);
}
static int append_remote(struct remote *remote, void *cbdata)
{
struct string_list *remotes = (struct string_list *)cbdata;
string_list_append(remotes, remote->name);
return 0;
}
static int maintenance_task_prefetch(struct maintenance_run_opts *opts)
{
int result = 0;
struct string_list_item *item;
struct string_list remotes = STRING_LIST_INIT_DUP;
git_config_set_multivar_gently("log.excludedecoration",
"refs/prefetch/",
"refs/prefetch/",
CONFIG_FLAGS_FIXED_VALUE |
CONFIG_FLAGS_MULTI_REPLACE);
if (for_each_remote(append_remote, &remotes)) {
error(_("failed to fill remotes"));
result = 1;
goto cleanup;
if (for_each_remote(fetch_remote, opts)) {
error(_("failed to prefetch remotes"));
return 1;
}
for_each_string_list_item(item, &remotes)
result |= fetch_remote(item->string, opts);
cleanup:
string_list_clear(&remotes, 0);
return result;
return 0;
}
static int maintenance_task_gc(struct maintenance_run_opts *opts)

View File

@ -240,4 +240,47 @@ test_expect_success "push with matching +: and negative refspec" '
git -C two push -v one
'
test_expect_success '--prefetch correctly modifies refspecs' '
git -C one config --unset-all remote.origin.fetch &&
git -C one config --add remote.origin.fetch ^refs/heads/bogus/ignore &&
git -C one config --add remote.origin.fetch "refs/tags/*:refs/tags/*" &&
git -C one config --add remote.origin.fetch "refs/heads/bogus/*:bogus/*" &&
git tag -a -m never never-fetch-tag HEAD &&
git branch bogus/fetched HEAD~1 &&
git branch bogus/ignore HEAD &&
git -C one fetch --prefetch --no-tags &&
test_must_fail git -C one rev-parse never-fetch-tag &&
git -C one rev-parse refs/prefetch/bogus/fetched &&
test_must_fail git -C one rev-parse refs/prefetch/bogus/ignore &&
# correctly handle when refspec set becomes empty
# after removing the refs/tags/* refspec.
git -C one config --unset-all remote.origin.fetch &&
git -C one config --add remote.origin.fetch "refs/tags/*:refs/tags/*" &&
git -C one fetch --prefetch --no-tags &&
test_must_fail git -C one rev-parse never-fetch-tag &&
# The refspec for refs that are not fully qualified
# are filtered multiple times.
git -C one rev-parse refs/prefetch/bogus/fetched &&
test_must_fail git -C one rev-parse refs/prefetch/bogus/ignore
'
test_expect_success '--prefetch succeeds when refspec becomes empty' '
git checkout bogus/fetched &&
test_commit extra &&
git -C one config --unset-all remote.origin.fetch &&
git -C one config --unset branch.main.remote &&
git -C one config remote.origin.fetch "+refs/tags/extra" &&
git -C one config remote.origin.skipfetchall true &&
git -C one config remote.origin.tagopt "--no-tags" &&
git -C one fetch --prefetch
'
test_done

View File

@ -141,19 +141,25 @@ test_expect_success 'prefetch multiple remotes' '
test_commit -C clone1 one &&
test_commit -C clone2 two &&
GIT_TRACE2_EVENT="$(pwd)/run-prefetch.txt" git maintenance run --task=prefetch 2>/dev/null &&
fetchargs="--prune --no-tags --no-write-fetch-head --recurse-submodules=no --refmap= --quiet" &&
test_subcommand git fetch remote1 $fetchargs +refs/heads/\\*:refs/prefetch/remote1/\\* <run-prefetch.txt &&
test_subcommand git fetch remote2 $fetchargs +refs/heads/\\*:refs/prefetch/remote2/\\* <run-prefetch.txt &&
fetchargs="--prefetch --prune --no-tags --no-write-fetch-head --recurse-submodules=no --quiet" &&
test_subcommand git fetch remote1 $fetchargs <run-prefetch.txt &&
test_subcommand git fetch remote2 $fetchargs <run-prefetch.txt &&
test_path_is_missing .git/refs/remotes &&
git log prefetch/remote1/one &&
git log prefetch/remote2/two &&
git log prefetch/remotes/remote1/one &&
git log prefetch/remotes/remote2/two &&
git fetch --all &&
test_cmp_rev refs/remotes/remote1/one refs/prefetch/remote1/one &&
test_cmp_rev refs/remotes/remote2/two refs/prefetch/remote2/two &&
test_cmp_rev refs/remotes/remote1/one refs/prefetch/remotes/remote1/one &&
test_cmp_rev refs/remotes/remote2/two refs/prefetch/remotes/remote2/two &&
test_cmp_config refs/prefetch/ log.excludedecoration &&
git log --oneline --decorate --all >log &&
! grep "prefetch" log
! grep "prefetch" log &&
test_when_finished git config --unset remote.remote1.skipFetchAll &&
git config remote.remote1.skipFetchAll true &&
GIT_TRACE2_EVENT="$(pwd)/skip-remote1.txt" git maintenance run --task=prefetch 2>/dev/null &&
test_subcommand ! git fetch remote1 $fetchargs <skip-remote1.txt &&
test_subcommand git fetch remote2 $fetchargs <skip-remote1.txt
'
test_expect_success 'prefetch and existing log.excludeDecoration values' '