backfill: default to grabbing edge blobs too

Commit 302aff09223f (backfill: accept revision arguments, 2026-03-26) added support for accepting revision arguments to backfill. This allows users to do things like git backfill --remotes ^v2.3.0 and then run many commands without triggering on-demand downloads of blobs. However, if they have topics based on v2.3.0, they will likely still trigger on-demand downloads. Consider, for example, the command git log -p v2.3.0..topic This would still trigger on-demand blob loadings after the backfill command above, because the commit(s) with A as a parent will need to diff against the blobs in A. In fact, multiple commands need blobs from the lower boundary of the revision range: * git log -p A..B # After backfill A..B * git replay --onto TARGET A..B # After backfill TARGET^! A..B * git checkout A && git merge B # After backfill A...B Add an extra --[no-]include-edges flag to allow grabbing blobs from edge commits. Since the point of backfill is to prevent on-demand blob loading and these are common commands, default to --include-edges. Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Apr 15, 2026 at 23:58 UTC a1ad4a0fca14cdeb55ab9fb065551b15cafa8a4f
3 files changed +119 -8
Documentation/git-backfill.adoc
+8 -1
@@ -9,7 +9,7 @@ git-backfill - Download missing objects in a partial clone
9 SYNOPSIS
10 --------
11 [synopsis]
12 -git backfill [--min-batch-size=<n>] [--[no-]sparse] [<revision-range>]
12 +git backfill [--min-batch-size=<n>] [--[no-]sparse] [--[no-]include-edges] [<revision-range>]
13
14 DESCRIPTION
15 -----------
@@ -63,6 +63,13 @@ OPTIONS
63 current sparse-checkout. If the sparse-checkout feature is enabled,
64 then `--sparse` is assumed and can be disabled with `--no-sparse`.
65
66 +`--include-edges`::
67 +`--no-include-edges`::
68 + Include blobs from boundary commits in the backfill. Useful in
69 + preparation for commands like `git log -p A..B` or `git replay
70 + --onto TARGET A..B`, where A..B normally excludes A but you need
71 + the blobs from A as well. `--include-edges` is the default.
72 +
73 `<revision-range>`::
74 Backfill only blobs reachable from commits in the specified
75 revision range. When no _<revision-range>_ is specified, it
builtin/backfill.c
+7 -1
@@ -26,7 +26,7 @@
26 #include "path-walk.h"
27
28 static const char * const builtin_backfill_usage[] = {
29 - N_("git backfill [--min-batch-size=<n>] [--[no-]sparse] [<revision-range>]"),
29 + N_("git backfill [--min-batch-size=<n>] [--[no-]sparse] [--[no-]include-edges] [<revision-range>]"),
30 NULL
31 };
32
@@ -35,6 +35,7 @@ struct backfill_context {
35 struct oid_array current_batch;
36 size_t min_batch_size;
37 int sparse;
38 + int include_edges;
39 struct rev_info revs;
40 };
41
@@ -116,6 +117,8 @@ static int do_backfill(struct backfill_context *ctx)
117 /* Walk from HEAD if otherwise unspecified. */
118 if (!ctx->revs.pending.nr)
119 add_head_to_pending(&ctx->revs);
120 + if (ctx->include_edges)
121 + ctx->revs.edge_hint = 1;
122
123 info.blobs = 1;
124 info.tags = info.commits = info.trees = 0;
@@ -143,12 +146,15 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit
146 .min_batch_size = 50000,
147 .sparse = -1,
148 .revs = REV_INFO_INIT,
149 + .include_edges = 1,
150 };
151 struct option options[] = {
152 OPT_UNSIGNED(0, "min-batch-size", &ctx.min_batch_size,
153 N_("Minimum number of objects to request at a time")),
154 OPT_BOOL(0, "sparse", &ctx.sparse,
155 N_("Restrict the missing objects to the current sparse-checkout")),
156 + OPT_BOOL(0, "include-edges", &ctx.include_edges,
157 + N_("Include blobs from boundary commits in the backfill")),
158 OPT_END(),
159 };
160 struct repo_config_values *cfg = repo_config_values(the_repository);
t/t5620-backfill.sh
+104 -6
@@ -257,11 +257,12 @@ test_expect_success 'backfill with revision range' '
257 git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
258 test_line_count = 48 missing &&
259
260 - git -C backfill-revs backfill HEAD~2..HEAD &&
260 + GIT_TRACE2_EVENT="$(pwd)/backfill-trace" git -C backfill-revs backfill HEAD~2..HEAD &&
261
262 - # 30 objects downloaded.
262 + # 36 objects downloaded, 12 still missing
263 + test_trace2_data promisor fetch_count 36 <backfill-trace &&
264 git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
264 - test_line_count = 18 missing
265 + test_line_count = 12 missing
266 '
267
268 test_expect_success 'backfill with revisions over stdin' '
@@ -279,11 +280,12 @@ test_expect_success 'backfill with revisions over stdin' '
280 ^HEAD~2
281 EOF
282
282 - git -C backfill-revs backfill --stdin <in &&
283 + GIT_TRACE2_EVENT="$(pwd)/backfill-trace" git -C backfill-revs backfill --stdin <in &&
284
284 - # 30 objects downloaded.
285 + # 36 objects downloaded, 12 still missing
286 + test_trace2_data promisor fetch_count 36 <backfill-trace &&
287 git -C backfill-revs rev-list --quiet --objects --missing=print HEAD >missing &&
286 - test_line_count = 18 missing
288 + test_line_count = 12 missing
289 '
290
291 test_expect_success 'backfill with prefix pathspec' '
@@ -398,6 +400,102 @@ test_expect_success 'backfill with --since' '
400 test_line_count = 6 missing
401 '
402
403 +test_expect_success 'backfill range with include-edges enables fetch-free git-log' '
404 + git clone --no-checkout --filter=blob:none \
405 + --single-branch --branch=main \
406 + "file://$(pwd)/srv.bare" backfill-log &&
407 +
408 + # Backfill the range with default include edges.
409 + git -C backfill-log backfill HEAD~2..HEAD &&
410 +
411 + # git log -p needs edge blobs for the "before" side of
412 + # diffs. With edge inclusion, all needed blobs are local.
413 + GIT_TRACE2_EVENT="$(pwd)/log-trace" git \
414 + -C backfill-log log -p HEAD~2..HEAD >log-output &&
415 +
416 + # No promisor fetches should have been needed.
417 + ! grep "fetch_count" log-trace
418 +'
419 +
420 +test_expect_success 'backfill range without include edges causes on-demand fetches in git-log' '
421 + git clone --no-checkout --filter=blob:none \
422 + --single-branch --branch=main \
423 + "file://$(pwd)/srv.bare" backfill-log-no-bdy &&
424 +
425 + # Backfill WITHOUT include edges -- file.3 v1 blobs are missing.
426 + git -C backfill-log-no-bdy backfill --no-include-edges HEAD~2..HEAD &&
427 +
428 + # git log -p HEAD~2..HEAD computes diff of commit 7 against
429 + # commit 6. It needs file.3 v1 (the "before" side), which was
430 + # not backfilled. This triggers on-demand promisor fetches.
431 + GIT_TRACE2_EVENT="$(pwd)/log-no-bdy-trace" git \
432 + -C backfill-log-no-bdy log -p HEAD~2..HEAD >log-output &&
433 +
434 + grep "fetch_count" log-no-bdy-trace
435 +'
436 +
437 +test_expect_success 'backfill range enables fetch-free replay' '
438 + # Create a repo with a branch to replay.
439 + git init replay-src &&
440 + (
441 + cd replay-src &&
442 + git config uploadpack.allowfilter 1 &&
443 + git config uploadpack.allowanysha1inwant 1 &&
444 + test_commit base &&
445 + git checkout -b topic &&
446 + test_commit topic-change &&
447 + git checkout main &&
448 + test_commit main-change
449 + ) &&
450 + git clone --bare --filter=blob:none \
451 + "file://$(pwd)/replay-src" replay-dest.git &&
452 +
453 + # Backfill the replay range: --onto main, replaying topic~1..topic.
454 + # For replay, we need TARGET^! plus the range.
455 + main_oid=$(git -C replay-dest.git rev-parse main) &&
456 + topic_oid=$(git -C replay-dest.git rev-parse topic) &&
457 + base_oid=$(git -C replay-dest.git rev-parse topic~1) &&
458 + git -C replay-dest.git backfill \
459 + "$main_oid^!" "$base_oid..$topic_oid" &&
460 +
461 + # Now replay should complete without any promisor fetches.
462 + GIT_TRACE2_EVENT="$(pwd)/replay-trace" git -C replay-dest.git \
463 + replay --onto main topic~1..topic >replay-out &&
464 +
465 + ! grep "fetch_count" replay-trace
466 +'
467 +
468 +test_expect_success 'backfill enables fetch-free merge' '
469 + # Create a repo with two branches to merge.
470 + git init merge-src &&
471 + (
472 + cd merge-src &&
473 + git config uploadpack.allowfilter 1 &&
474 + git config uploadpack.allowanysha1inwant 1 &&
475 + test_commit merge-base &&
476 + git checkout -b side &&
477 + test_commit side-change &&
478 + git checkout main &&
479 + test_commit main-side-change
480 + ) &&
481 + git clone --filter=blob:none \
482 + "file://$(pwd)/merge-src" merge-dest &&
483 +
484 + # The clone checked out main, fetching its blobs.
485 + # Backfill the three endpoint commits needed for merge.
486 + main_oid=$(git -C merge-dest rev-parse origin/main) &&
487 + side_oid=$(git -C merge-dest rev-parse origin/side) &&
488 + mbase=$(git -C merge-dest merge-base origin/main origin/side) &&
489 + git -C merge-dest backfill --no-include-edges \
490 + "$main_oid^!" "$side_oid^!" "$mbase^!" &&
491 +
492 + # Merge should complete without promisor fetches.
493 + GIT_TRACE2_EVENT="$(pwd)/merge-trace" git -C merge-dest \
494 + merge origin/side -m "test merge" &&
495 +
496 + ! grep "fetch_count" merge-trace
497 +'
498 +
499 . "$TEST_DIRECTORY"/lib-httpd.sh
500 start_httpd
501