path-walk: always emit directly-requested objects

We are preparing to integrate the path-walk API with some --filter options in 'git pack-objects', but there is a subtle issue that is revealed when those are put together and the test suite is run with GIT_TEST_PACK_PATH_WALK=1. When a filter reduces the set of requested objects, this results in filtering out directly-requested objects, such as in the download of needed blobs in a blobless partial clone. The root cause is that the scan of pending objects in the path-walk API respects the filters set in the path_walk_info instead of overriding them for pending objects. We can tell that a path is part of the directly-referenced objects if its path name starts with '/' (other paths, including root trees never have this starting character). Create a path_is_for_direct_objects() to make this meaning clear, especially as we add more references in the future as we integrate the path-walk API with partial clone filter options. Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed May 22, 2026 at 18:24 UTC 7a7070eebce16f20548bd4685362bca7df6af431
3 files changed +39 -15
Documentation/technical/api-path-walk.adoc
+7
@@ -48,6 +48,13 @@ commits.
48 applications could disable some options to make it simpler to walk
49 the objects or to have fewer calls to `path_fn`.
50 +
51 +Note that objects directly requested as pending objects (such as targets
52 +of lightweight tags or other ref tips) are always emitted to `path_fn`,
53 +even when the corresponding type flag is disabled. Only objects
54 +discovered during the tree walk are subject to these type filters. This
55 +ensures that objects specifically requested through the revision input
56 +are never silently dropped.
57 ++
58 While it is possible to walk only commits in this way, consumers would be
59 better off using the revision walk API instead.
60
path-walk.c
+27 -15
@@ -248,6 +248,17 @@ static int add_tree_entries(struct path_walk_context *ctx,
248 return 0;
249 }
250
251 +/*
252 + * Paths starting with '/' (e.g., "/tags", "/tagged-blobs") hold objects that
253 + * were directly requested by 'pending' objects rather than discovered during
254 + * tree traversal.
255 + */
256 +static int path_is_for_direct_objects(const char *path)
257 +{
258 + ASSERT(path);
259 + return path[0] == '/';
260 +}
261 +
262 /*
263 * For each path in paths_to_explore, walk the trees another level
264 * and add any found blobs to the batch (but only if they exist and
@@ -306,14 +317,19 @@ static int walk_path(struct path_walk_context *ctx,
317
318 if (list->type == OBJ_BLOB &&
319 ctx->revs->prune_data.nr &&
320 + !path_is_for_direct_objects(path) &&
321 !match_pathspec(ctx->repo->index, &ctx->revs->prune_data,
322 path, strlen(path), 0,
323 NULL, 0))
324 return 0;
325
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) ||
326 + /*
327 + * Evaluate function pointer on this data, if requested.
328 + * Ignore object type filters for tagged objects (path starts
329 + * with `/`).
330 + */
331 + if ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||
332 + (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||
333 (list->type == OBJ_TAG && ctx->info->tags))
334 ret = ctx->info->path_fn(path, &list->oids, list->type,
335 ctx->info->path_fn_data);
@@ -374,10 +390,8 @@ static int setup_pending_objects(struct path_walk_info *info,
390
391 if (info->tags)
392 CALLOC_ARRAY(tags, 1);
377 - if (info->blobs)
378 - CALLOC_ARRAY(tagged_blobs, 1);
379 - if (info->trees)
380 - root_tree_list = strmap_get(&ctx->paths_to_lists, root_path);
393 + CALLOC_ARRAY(tagged_blobs, 1);
394 + root_tree_list = strmap_get(&ctx->paths_to_lists, root_path);
395
396 /*
397 * Pending objects include:
@@ -421,8 +435,6 @@ static int setup_pending_objects(struct path_walk_info *info,
435
436 switch (obj->type) {
437 case OBJ_TREE:
424 - if (!info->trees)
425 - continue;
438 if (pending->path) {
439 char *path = *pending->path ? xstrfmt("%s/", pending->path)
440 : xstrdup("");
@@ -435,8 +447,6 @@ static int setup_pending_objects(struct path_walk_info *info,
447 break;
448
449 case OBJ_BLOB:
438 - if (!info->blobs)
439 - continue;
450 if (pending->path)
451 add_path_to_list(ctx, pending->path, OBJ_BLOB, &obj->oid, 1);
452 else
@@ -532,15 +542,17 @@ int walk_objects_by_path(struct path_walk_info *info)
542 push_to_stack(&ctx, root_path);
543
544 /*
535 - * Set these values before preparing the walk to catch
536 - * lightweight tags pointing to non-commits and indexed objects.
545 + * Ensure that prepare_revision_walk() keeps all pending objects
546 + * even through an object type filter.
547 */
538 - info->revs->blob_objects = info->blobs;
539 - info->revs->tree_objects = info->trees;
548 + info->revs->blob_objects = info->revs->tree_objects = 1;
549
550 if (prepare_revision_walk(info->revs))
551 die(_("failed to setup revision walk"));
552
553 + info->revs->blob_objects = info->blobs;
554 + info->revs->tree_objects = info->trees;
555 +
556 /*
557 * Walk trees to mark them as UNINTERESTING.
558 * This is particularly important when 'edge_aggressive' is set.
path-walk.h
+5
@@ -36,6 +36,11 @@ struct path_walk_info {
36 /**
37 * Initialize which object types the path_fn should be called on. This
38 * could also limit the walk to skip blobs if not set.
39 + *
40 + * Note: even when 'blobs' or 'trees' is disabled, objects that are
41 + * directly requested as pending objects will still be emitted to
42 + * path_fn. Only objects discovered during the tree walk are filtered by
43 + * these flags.
44 */
45 int commits;
46 int trees;