fetch: add --prefetch option

The --prefetch option will be used by the 'prefetch' maintenance task instead of sending refspecs explicitly across the command-line. The intention is to modify the refspec to place all results in refs/prefetch/ instead of anywhere else. Create helper method filter_prefetch_refspec() to modify a given refspec to fit the rules expected of the prefetch task: * Negative refspecs are preserved. * Refspecs without a destination are removed. * Refspecs whose source starts with "refs/tags/" are removed. * Other refspecs are placed within "refs/prefetch/". Finally, we add the 'force' option to ensure that prefetch refs are replaced as necessary. There are some interesting cases that are worth testing. An earlier version of this change dropped the "i--" from the loop that deletes a refspec item and shifts the remaining entries down. This allowed some refspecs to not be modified. The subtle part about the first --prefetch test is that the "refs/tags/*" refspec appears directly before the "refs/heads/bogus/*" refspec. Without that "i--", this ordering would remove the "refs/tags/*" refspec and leave the last one unmodified, placing the result in "refs/heads/*". It is possible to have an empty refspec. This is typically the case for remotes other than the origin, where users want to fetch a specific tag or branch. To correctly test this case, we need to further remove the upstream remote for the local branch. Thus, we are testing a refspec that will be deleted, leaving nothing to fetch. Helped-by: Tom Saeger <tom.saeger@oracle.com> Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com> Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Apr 16, 2021 at 12:49 UTC 2e03115d0c253843953ef9d113c72e0375892df4
3 files changed +106 -1
Documentation/fetch-options.txt
+5
@@ -110,6 +110,11 @@ ifndef::git-pull[]
110 setting `fetch.writeCommitGraph`.
111 endif::git-pull[]
112
113 +--prefetch::
114 + Modify the configured refspec to place all refs into the
115 + `refs/prefetch/` namespace. See the `prefetch` task in
116 + linkgit:git-maintenance[1].
117 +
118 -p::
119 --prune::
120 Before fetching, remove any remote-tracking references that no
builtin/fetch.c
+58 -1
@@ -48,6 +48,7 @@ enum {
48 static int fetch_prune_config = -1; /* unspecified */
49 static int fetch_show_forced_updates = 1;
50 static uint64_t forced_updates_ms = 0;
51 +static int prefetch = 0;
52 static int prune = -1; /* unspecified */
53 #define PRUNE_BY_DEFAULT 0 /* do we prune by default? */
54
@@ -158,6 +159,8 @@ static struct option builtin_fetch_options[] = {
159 N_("do not fetch all tags (--no-tags)"), TAGS_UNSET),
160 OPT_INTEGER('j', "jobs", &max_jobs,
161 N_("number of submodules fetched in parallel")),
162 + OPT_BOOL(0, "prefetch", &prefetch,
163 + N_("modify the refspec to place all refs within refs/prefetch/")),
164 OPT_BOOL('p', "prune", &prune,
165 N_("prune remote-tracking branches no longer on remote")),
166 OPT_BOOL('P', "prune-tags", &prune_tags,
@@ -436,6 +439,56 @@ static void find_non_local_tags(const struct ref *refs,
439 oidset_clear(&fetch_oids);
440 }
441
442 +static void filter_prefetch_refspec(struct refspec *rs)
443 +{
444 + int i;
445 +
446 + if (!prefetch)
447 + return;
448 +
449 + for (i = 0; i < rs->nr; i++) {
450 + struct strbuf new_dst = STRBUF_INIT;
451 + char *old_dst;
452 + const char *sub = NULL;
453 +
454 + if (rs->items[i].negative)
455 + continue;
456 + if (!rs->items[i].dst ||
457 + (rs->items[i].src &&
458 + !strncmp(rs->items[i].src, "refs/tags/", 10))) {
459 + int j;
460 +
461 + free(rs->items[i].src);
462 + free(rs->items[i].dst);
463 +
464 + for (j = i + 1; j < rs->nr; j++) {
465 + rs->items[j - 1] = rs->items[j];
466 + rs->raw[j - 1] = rs->raw[j];
467 + }
468 + rs->nr--;
469 + i--;
470 + continue;
471 + }
472 +
473 + old_dst = rs->items[i].dst;
474 + strbuf_addstr(&new_dst, "refs/prefetch/");
475 +
476 + /*
477 + * If old_dst starts with "refs/", then place
478 + * sub after that prefix. Otherwise, start at
479 + * the beginning of the string.
480 + */
481 + if (!skip_prefix(old_dst, "refs/", &sub))
482 + sub = old_dst;
483 + strbuf_addstr(&new_dst, sub);
484 +
485 + rs->items[i].dst = strbuf_detach(&new_dst, NULL);
486 + rs->items[i].force = 1;
487 +
488 + free(old_dst);
489 + }
490 +}
491 +
492 static struct ref *get_ref_map(struct remote *remote,
493 const struct ref *remote_refs,
494 struct refspec *rs,
@@ -452,6 +505,10 @@ static struct ref *get_ref_map(struct remote *remote,
505 struct hashmap existing_refs;
506 int existing_refs_populated = 0;
507
508 + filter_prefetch_refspec(rs);
509 + if (remote)
510 + filter_prefetch_refspec(&remote->fetch);
511 +
512 if (rs->nr) {
513 struct refspec *fetch_refspec;
514
@@ -520,7 +577,7 @@ static struct ref *get_ref_map(struct remote *remote,
577 if (has_merge &&
578 !strcmp(branch->remote_name, remote->name))
579 add_merge_config(&ref_map, remote_refs, branch, &tail);
523 - } else {
580 + } else if (!prefetch) {
581 ref_map = get_remote_ref(remote_refs, "HEAD");
582 if (!ref_map)
583 die(_("Couldn't find remote ref HEAD"));
t/t5582-fetch-negative-refspec.sh
+43
@@ -240,4 +240,47 @@ test_expect_success "push with matching +: and negative refspec" '
240 git -C two push -v one
241 '
242
243 +test_expect_success '--prefetch correctly modifies refspecs' '
244 + git -C one config --unset-all remote.origin.fetch &&
245 + git -C one config --add remote.origin.fetch ^refs/heads/bogus/ignore &&
246 + git -C one config --add remote.origin.fetch "refs/tags/*:refs/tags/*" &&
247 + git -C one config --add remote.origin.fetch "refs/heads/bogus/*:bogus/*" &&
248 +
249 + git tag -a -m never never-fetch-tag HEAD &&
250 +
251 + git branch bogus/fetched HEAD~1 &&
252 + git branch bogus/ignore HEAD &&
253 +
254 + git -C one fetch --prefetch --no-tags &&
255 + test_must_fail git -C one rev-parse never-fetch-tag &&
256 + git -C one rev-parse refs/prefetch/bogus/fetched &&
257 + test_must_fail git -C one rev-parse refs/prefetch/bogus/ignore &&
258 +
259 + # correctly handle when refspec set becomes empty
260 + # after removing the refs/tags/* refspec.
261 + git -C one config --unset-all remote.origin.fetch &&
262 + git -C one config --add remote.origin.fetch "refs/tags/*:refs/tags/*" &&
263 +
264 + git -C one fetch --prefetch --no-tags &&
265 + test_must_fail git -C one rev-parse never-fetch-tag &&
266 +
267 + # The refspec for refs that are not fully qualified
268 + # are filtered multiple times.
269 + git -C one rev-parse refs/prefetch/bogus/fetched &&
270 + test_must_fail git -C one rev-parse refs/prefetch/bogus/ignore
271 +'
272 +
273 +test_expect_success '--prefetch succeeds when refspec becomes empty' '
274 + git checkout bogus/fetched &&
275 + test_commit extra &&
276 +
277 + git -C one config --unset-all remote.origin.fetch &&
278 + git -C one config --unset branch.main.remote &&
279 + git -C one config remote.origin.fetch "+refs/tags/extra" &&
280 + git -C one config remote.origin.skipfetchall true &&
281 + git -C one config remote.origin.tagopt "--no-tags" &&
282 +
283 + git -C one fetch --prefetch
284 +'
285 +
286 test_done