Skip to content

Commit 2e03115

Browse files
derrickstoleegitster
authored andcommitted
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 <[email protected]> Helped-by: Ramsay Jones <[email protected]> Signed-off-by: Derrick Stolee <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent a039a1f commit 2e03115

File tree

3 files changed

+106
-1
lines changed

3 files changed

+106
-1
lines changed

Documentation/fetch-options.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ ifndef::git-pull[]
110110
setting `fetch.writeCommitGraph`.
111111
endif::git-pull[]
112112

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+
113118
-p::
114119
--prune::
115120
Before fetching, remove any remote-tracking references that no

builtin/fetch.c

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ enum {
4848
static int fetch_prune_config = -1; /* unspecified */
4949
static int fetch_show_forced_updates = 1;
5050
static uint64_t forced_updates_ms = 0;
51+
static int prefetch = 0;
5152
static int prune = -1; /* unspecified */
5253
#define PRUNE_BY_DEFAULT 0 /* do we prune by default? */
5354

@@ -158,6 +159,8 @@ static struct option builtin_fetch_options[] = {
158159
N_("do not fetch all tags (--no-tags)"), TAGS_UNSET),
159160
OPT_INTEGER('j', "jobs", &max_jobs,
160161
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/")),
161164
OPT_BOOL('p', "prune", &prune,
162165
N_("prune remote-tracking branches no longer on remote")),
163166
OPT_BOOL('P', "prune-tags", &prune_tags,
@@ -436,6 +439,56 @@ static void find_non_local_tags(const struct ref *refs,
436439
oidset_clear(&fetch_oids);
437440
}
438441

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+
439492
static struct ref *get_ref_map(struct remote *remote,
440493
const struct ref *remote_refs,
441494
struct refspec *rs,
@@ -452,6 +505,10 @@ static struct ref *get_ref_map(struct remote *remote,
452505
struct hashmap existing_refs;
453506
int existing_refs_populated = 0;
454507

508+
filter_prefetch_refspec(rs);
509+
if (remote)
510+
filter_prefetch_refspec(&remote->fetch);
511+
455512
if (rs->nr) {
456513
struct refspec *fetch_refspec;
457514

@@ -520,7 +577,7 @@ static struct ref *get_ref_map(struct remote *remote,
520577
if (has_merge &&
521578
!strcmp(branch->remote_name, remote->name))
522579
add_merge_config(&ref_map, remote_refs, branch, &tail);
523-
} else {
580+
} else if (!prefetch) {
524581
ref_map = get_remote_ref(remote_refs, "HEAD");
525582
if (!ref_map)
526583
die(_("Couldn't find remote ref HEAD"));

t/t5582-fetch-negative-refspec.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,47 @@ test_expect_success "push with matching +: and negative refspec" '
240240
git -C two push -v one
241241
'
242242

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+
243286
test_done

0 commit comments

Comments
 (0)