path-walk: support wildcard pathspecs for blob filtering

Previously, walk_objects_by_path() silently ignored pathspecs containing wildcards or magic by clearing them. This caused all blobs to be downloaded regardless of the given pathspec. Wildcard pathspecs like "d/file.*.txt" are useful for narrowing which blobs to process (e.g., during 'git backfill'). Support wildcard pathspecs by making two changes: 1. Add an 'exact_pathspecs' flag to path_walk_context. When the pathspec has no wildcards or magic, set this flag and use the existing fast-path prefix matching in add_tree_entries(). When wildcards are present, skip that block since prefix matching cannot handle glob patterns. 2. Add a match_pathspec() check in walk_path() to filter out blobs whose full path does not match the pathspec. This provides the actual blob-level filtering for wildcard pathspecs. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Mar 26, 2026 at 15:14 UTC 3f20c21a1ceeb796e121147a53ba10d28041b1fe
2 files changed +16 -13
path-walk.c
+13 -9
@@ -63,6 +63,8 @@ struct path_walk_context {
63 */
64 struct prio_queue path_stack;
65 struct strset path_stack_pushed;
66 +
67 + unsigned exact_pathspecs:1;
68 };
69
70 static int compare_by_type(const void *one, const void *two, void *cb_data)
@@ -207,7 +209,7 @@ static int add_tree_entries(struct path_walk_context *ctx,
209 match != MATCHED)
210 continue;
211 }
210 - if (ctx->revs->prune_data.nr) {
212 + if (ctx->revs->prune_data.nr && ctx->exact_pathspecs) {
213 struct pathspec *pd = &ctx->revs->prune_data;
214 bool found = false;
215 int did_strip_suffix = strbuf_strip_suffix(&path, "/");
@@ -302,6 +304,13 @@ static int walk_path(struct path_walk_context *ctx,
304 return 0;
305 }
306
307 + if (list->type == OBJ_BLOB &&
308 + ctx->revs->prune_data.nr &&
309 + !match_pathspec(ctx->repo->index, &ctx->revs->prune_data,
310 + path, strlen(path), 0,
311 + NULL, 0))
312 + return 0;
313 +
314 /* Evaluate function pointer on this data, if requested. */
315 if ((list->type == OBJ_TREE && ctx->info->trees) ||
316 (list->type == OBJ_BLOB && ctx->info->blobs) ||
@@ -510,14 +519,9 @@ int walk_objects_by_path(struct path_walk_info *info)
519 info->revs->tag_objects = 1;
520
521 if (ctx.revs->prune_data.nr) {
513 - /*
514 - * Only exact prefix pathspecs are currently supported.
515 - * Clear any wildcard or magic pathspecs to avoid
516 - * incorrect prefix matching.
517 - */
518 - if (ctx.revs->prune_data.has_wildcard ||
519 - ctx.revs->prune_data.magic)
520 - clear_pathspec(&ctx.revs->prune_data);
522 + if (!ctx.revs->prune_data.has_wildcard &&
523 + !ctx.revs->prune_data.magic)
524 + ctx.exact_pathspecs = 1;
525 }
526
527 /* Insert a single list for the root tree into the paths. */
t/t5620-backfill.sh
+3 -4
@@ -307,12 +307,11 @@ test_expect_success 'backfill with wildcard pathspec' '
307 git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
308 test_line_count = 48 missing &&
309
310 - # TODO: The wildcard pathspec should limit downloaded blobs,
311 - # but currently all blobs are downloaded.
312 - git -C backfill-path backfill HEAD -- "d/file.*.txt" &&
310 + git -C backfill-path backfill HEAD -- "d/file.*.txt" 2>err &&
311 + test_must_be_empty err &&
312
313 git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
315 - test_line_count = 0 missing
314 + test_line_count = 40 missing
315 '
316
317 test_expect_success 'backfill with --all' '