path-walk: support `tree:0` filter

The `tree:0` object filter omits all trees and blobs from the result, keeping only commits and tags. Consequently, this filter type should has a fairly straightforward integration with path-walk, as the decision to include an object depends only on its type and does not depend on any path-sensitive state. Mapping it onto `path_walk_info` is direct: set `info->trees = 0` and `info->blobs = 0` in `prepare_filters()` when the `LOFC_TREE_DEPTH` choice is requested with depth zero. The existing code already plumbs those flags through the rest of the walk: - 'walk_objects_by_path()' sets `revs->blob_objects = info->blobs` and `revs->tree_objects = info->trees` before `prepare_revision_walk()`, so the revision walk doesn't try to enumerate trees or blobs itself. - The commit-walk loop short-circuits the root-tree fetch with "if (!info->trees && !info->blobs) continue;", so we never even look up the root tree, let alone descend into it. - `setup_pending_objects()` skips pending trees and blobs based on the same flags. This means the path-walk doesn't allocate or expand any tree structures at all under `tree:0`, which matches the intended behavior of the filter. However, this requires first fixing some issues with how the path-walk API handles directly-requested trees _and_ trees requested through lightweight tags. These changes create substantial updates to t6601-path-walk.sh, which the previous change highlighted as a problem by tagging otherwise-unreachable trees and having them not appear in the output. Non-zero tree-depth filters are not supported. Those depend on the depth at which a tree is visited, which is a path-walk concept the filter machinery doesn't currently share with the path-walk API. Reject them in `prepare_filters()` with a helpful error and let pack-objects fall back to the regular traversal, the same way it already does for unsupported filters. Add coverage in t6601 for both `--all` and a single-branch case to confirm that no trees or blobs are emitted, and a separate test that `tree:1` is rejected with the expected error message. Place the new tests before "setup sparse filter blob" so they run on the original set of refs, before the orphan branch that the sparse-tree tests create. Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed May 22, 2026 at 18:24 UTC 5111520e2a140b80f60565c579b694b8d95eee01
3 files changed +152 -70
Documentation/git-pack-objects.adoc
+2 -2
@@ -404,8 +404,8 @@ will be automatically changed to version `1`.
404 +
405 Incompatible with `--delta-islands`. The `--use-bitmap-index` option is
406 ignored in the presence of `--path-walk`. The `--path-walk` option
407 -supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and
408 -`sparse:<oid>`.
407 +supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,
408 +`tree:0`, and `sparse:<oid>`.
409
410
411 DELTA ISLANDS
path-walk.c
+43 -10
@@ -390,11 +390,18 @@ static int walk_path(struct path_walk_context *ctx,
390 ctx->info->path_fn_data);
391 }
392
393 - /* Expand data for children. */
394 - if (list->type == OBJ_TREE) {
393 + /*
394 + * Expand tree children, except when the set is directly requested
395 + * _and_ we are otherwise filtering out trees.
396 + */
397 + if (list->type == OBJ_TREE &&
398 + (!path_is_for_direct_objects(path) || ctx->info->trees)) {
399 + /* Use root path if expanding from tagged/direct trees. */
400 + const char *expand_path = !strcmp(path, "/tagged-trees")
401 + ? root_path : path;
402 for (size_t i = 0; i < list->oids.nr; i++) {
403 ret |= add_tree_entries(ctx,
397 - path,
404 + expand_path,
405 &list->oids.oid[i]);
406 }
407 }
@@ -442,12 +449,12 @@ static int setup_pending_objects(struct path_walk_info *info,
449 {
450 struct type_and_oid_list *tags = NULL;
451 struct type_and_oid_list *tagged_blobs = NULL;
445 - struct type_and_oid_list *root_tree_list = NULL;
452 + struct type_and_oid_list *tagged_trees = NULL;
453
454 if (info->tags)
455 CALLOC_ARRAY(tags, 1);
456 CALLOC_ARRAY(tagged_blobs, 1);
450 - root_tree_list = strmap_get(&ctx->paths_to_lists, root_path);
457 + CALLOC_ARRAY(tagged_trees, 1);
458
459 /*
460 * Pending objects include:
@@ -491,14 +498,15 @@ static int setup_pending_objects(struct path_walk_info *info,
498
499 switch (obj->type) {
500 case OBJ_TREE:
494 - if (pending->path) {
495 - char *path = *pending->path ? xstrfmt("%s/", pending->path)
496 - : xstrdup("");
501 + if (pending->path && *pending->path) {
502 + char *path = xstrfmt("%s/", pending->path);
503 add_path_to_list(ctx, path, OBJ_TREE, &obj->oid, 1);
504 free(path);
505 + } else if (!pending->path || !info->trees) {
506 + oid_array_append(&tagged_trees->oids, &obj->oid);
507 } else {
500 - /* assume a root tree, such as a lightweight tag. */
501 - oid_array_append(&root_tree_list->oids, &obj->oid);
508 + add_path_to_list(ctx, root_path, OBJ_TREE,
509 + &obj->oid, 1);
510 }
511 break;
512
@@ -535,6 +543,18 @@ static int setup_pending_objects(struct path_walk_info *info,
543 free(tagged_blobs);
544 }
545 }
546 + if (tagged_trees) {
547 + if (tagged_trees->oids.nr) {
548 + const char *tagged_tree_path = "/tagged-trees";
549 + tagged_trees->type = OBJ_TREE;
550 + tagged_trees->maybe_interesting = 1;
551 + strmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);
552 + push_to_stack(ctx, tagged_tree_path);
553 + } else {
554 + oid_array_clear(&tagged_trees->oids);
555 + free(tagged_trees);
556 + }
557 + }
558 if (tags) {
559 if (tags->oids.nr) {
560 const char *tag_path = "/tags";
@@ -575,6 +595,19 @@ static int prepare_filters(struct path_walk_info *info,
595 }
596 return 1;
597
598 + case LOFC_TREE_DEPTH:
599 + if (options->tree_exclude_depth) {
600 + error(_("tree:%lu filter not supported by the path-walk API"),
601 + options->tree_exclude_depth);
602 + return 0;
603 + }
604 + if (info) {
605 + info->trees = 0;
606 + info->blobs = 0;
607 + list_objects_filter_release(options);
608 + }
609 + return 1;
610 +
611 case LOFC_SPARSE_OID:
612 if (info) {
613 struct object_id sparse_oid;
t/t6601-path-walk.sh
+107 -58
@@ -77,23 +77,23 @@ test_expect_success 'all' '
77 3:tree::$(git rev-parse base^{tree})
78 3:tree::$(git rev-parse base~1^{tree})
79 3:tree::$(git rev-parse base~2^{tree})
80 - 3:tree::$(git rev-parse refs/tags/tree-tag^{})
81 - 3:tree::$(git rev-parse refs/tags/tree-tag2^{})
80 4:blob:a:$(git rev-parse base~2:a)
83 - 5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)
84 - 6:tree:a/:$(git rev-parse base:a)
85 - 7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
86 - 8:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)
87 - 9:tree:left/:$(git rev-parse base:left)
88 - 9:tree:left/:$(git rev-parse base~2:left)
89 - 10:blob:left/b:$(git rev-parse base~2:left/b)
90 - 10:blob:left/b:$(git rev-parse base:left/b)
91 - 11:tree:right/:$(git rev-parse topic:right)
92 - 11:tree:right/:$(git rev-parse base~1:right)
93 - 11:tree:right/:$(git rev-parse base~2:right)
94 - 12:blob:right/c:$(git rev-parse base~2:right/c)
95 - 12:blob:right/c:$(git rev-parse topic:right/c)
96 - 13:blob:right/d:$(git rev-parse base~1:right/d)
81 + 5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})
82 + 5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})
83 + 6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)
84 + 7:tree:a/:$(git rev-parse base:a)
85 + 8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
86 + 9:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)
87 + 10:tree:left/:$(git rev-parse base:left)
88 + 10:tree:left/:$(git rev-parse base~2:left)
89 + 11:blob:left/b:$(git rev-parse base~2:left/b)
90 + 11:blob:left/b:$(git rev-parse base:left/b)
91 + 12:tree:right/:$(git rev-parse topic:right)
92 + 12:tree:right/:$(git rev-parse base~1:right)
93 + 12:tree:right/:$(git rev-parse base~2:right)
94 + 13:blob:right/c:$(git rev-parse base~2:right/c)
95 + 13:blob:right/c:$(git rev-parse topic:right/c)
96 + 14:blob:right/d:$(git rev-parse base~1:right/d)
97 blobs:10
98 commits:4
99 tags:7
@@ -471,15 +471,15 @@ test_expect_success 'all, blob:none filter' '
471 3:tree::$(git rev-parse base^{tree})
472 3:tree::$(git rev-parse base~1^{tree})
473 3:tree::$(git rev-parse base~2^{tree})
474 - 3:tree::$(git rev-parse refs/tags/tree-tag^{})
475 - 3:tree::$(git rev-parse refs/tags/tree-tag2^{})
476 - 4:tree:a/:$(git rev-parse base:a)
477 - 5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
478 - 6:tree:left/:$(git rev-parse base:left)
479 - 6:tree:left/:$(git rev-parse base~2:left)
480 - 7:tree:right/:$(git rev-parse topic:right)
481 - 7:tree:right/:$(git rev-parse base~1:right)
482 - 7:tree:right/:$(git rev-parse base~2:right)
474 + 4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})
475 + 4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})
476 + 5:tree:a/:$(git rev-parse base:a)
477 + 6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
478 + 7:tree:left/:$(git rev-parse base:left)
479 + 7:tree:left/:$(git rev-parse base~2:left)
480 + 8:tree:right/:$(git rev-parse topic:right)
481 + 8:tree:right/:$(git rev-parse base~1:right)
482 + 8:tree:right/:$(git rev-parse base~2:right)
483 blobs:2
484 commits:4
485 tags:7
@@ -533,15 +533,15 @@ test_expect_success 'all, blob:limit=0 filter' '
533 3:tree::$(git rev-parse base^{tree})
534 3:tree::$(git rev-parse base~1^{tree})
535 3:tree::$(git rev-parse base~2^{tree})
536 - 3:tree::$(git rev-parse refs/tags/tree-tag^{})
537 - 3:tree::$(git rev-parse refs/tags/tree-tag2^{})
538 - 4:tree:a/:$(git rev-parse base:a)
539 - 5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
540 - 6:tree:left/:$(git rev-parse base:left)
541 - 6:tree:left/:$(git rev-parse base~2:left)
542 - 7:tree:right/:$(git rev-parse topic:right)
543 - 7:tree:right/:$(git rev-parse base~1:right)
544 - 7:tree:right/:$(git rev-parse base~2:right)
536 + 4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})
537 + 4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})
538 + 5:tree:a/:$(git rev-parse base:a)
539 + 6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
540 + 7:tree:left/:$(git rev-parse base:left)
541 + 7:tree:left/:$(git rev-parse base~2:left)
542 + 8:tree:right/:$(git rev-parse topic:right)
543 + 8:tree:right/:$(git rev-parse base~1:right)
544 + 8:tree:right/:$(git rev-parse base~2:right)
545 blobs:2
546 commits:4
547 tags:7
@@ -572,19 +572,19 @@ test_expect_success 'all, blob:limit=3 filter' '
572 3:tree::$(git rev-parse base^{tree})
573 3:tree::$(git rev-parse base~1^{tree})
574 3:tree::$(git rev-parse base~2^{tree})
575 - 3:tree::$(git rev-parse refs/tags/tree-tag^{})
576 - 3:tree::$(git rev-parse refs/tags/tree-tag2^{})
575 4:blob:a:$(git rev-parse base~2:a)
578 - 5:tree:a/:$(git rev-parse base:a)
579 - 6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
580 - 7:tree:left/:$(git rev-parse base:left)
581 - 7:tree:left/:$(git rev-parse base~2:left)
582 - 8:blob:left/b:$(git rev-parse base~2:left/b)
583 - 9:tree:right/:$(git rev-parse topic:right)
584 - 9:tree:right/:$(git rev-parse base~1:right)
585 - 9:tree:right/:$(git rev-parse base~2:right)
586 - 10:blob:right/c:$(git rev-parse base~2:right/c)
587 - 11:blob:right/d:$(git rev-parse base~1:right/d)
576 + 5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})
577 + 5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})
578 + 6:tree:a/:$(git rev-parse base:a)
579 + 7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
580 + 8:tree:left/:$(git rev-parse base:left)
581 + 8:tree:left/:$(git rev-parse base~2:left)
582 + 9:blob:left/b:$(git rev-parse base~2:left/b)
583 + 10:tree:right/:$(git rev-parse topic:right)
584 + 10:tree:right/:$(git rev-parse base~1:right)
585 + 10:tree:right/:$(git rev-parse base~2:right)
586 + 11:blob:right/c:$(git rev-parse base~2:right/c)
587 + 12:blob:right/d:$(git rev-parse base~1:right/d)
588 blobs:6
589 commits:4
590 tags:7
@@ -594,6 +594,55 @@ test_expect_success 'all, blob:limit=3 filter' '
594 test_cmp_sorted expect out
595 '
596
597 +test_expect_success 'all, tree:0 filter' '
598 + test-tool path-walk --filter=tree:0 -- --all >out &&
599 +
600 + cat >expect <<-EOF &&
601 + 0:commit::$(git rev-parse topic)
602 + 0:commit::$(git rev-parse base)
603 + 0:commit::$(git rev-parse base~1)
604 + 0:commit::$(git rev-parse base~2)
605 + 1:tag:/tags:$(git rev-parse refs/tags/first)
606 + 1:tag:/tags:$(git rev-parse refs/tags/second.1)
607 + 1:tag:/tags:$(git rev-parse refs/tags/second.2)
608 + 1:tag:/tags:$(git rev-parse refs/tags/third)
609 + 1:tag:/tags:$(git rev-parse refs/tags/fourth)
610 + 1:tag:/tags:$(git rev-parse refs/tags/tree-tag)
611 + 1:tag:/tags:$(git rev-parse refs/tags/blob-tag)
612 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})
613 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})
614 + 3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})
615 + 3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)
616 + blobs:2
617 + commits:4
618 + tags:7
619 + trees:2
620 + EOF
621 +
622 + test_cmp_sorted expect out
623 +'
624 +
625 +test_expect_success 'topic only, tree:0 filter' '
626 + test-tool path-walk --filter=tree:0 -- topic >out &&
627 +
628 + cat >expect <<-EOF &&
629 + 0:commit::$(git rev-parse topic)
630 + 0:commit::$(git rev-parse base~1)
631 + 0:commit::$(git rev-parse base~2)
632 + blobs:0
633 + commits:3
634 + tags:0
635 + trees:0
636 + EOF
637 +
638 + test_cmp_sorted expect out
639 +'
640 +
641 +test_expect_success 'tree:1 filter is rejected' '
642 + test_must_fail test-tool path-walk --filter=tree:1 -- --all 2>err &&
643 + test_grep "tree:1 filter not supported by the path-walk API" err
644 +'
645 +
646 test_expect_success 'setup sparse filter blob' '
647 # Cone-mode patterns: include root, exclude all dirs, include left/
648 cat >patterns <<-\EOF &&
@@ -625,19 +674,19 @@ test_expect_success 'all, sparse:oid filter' '
674 3:tree::$(git rev-parse base^{tree})
675 3:tree::$(git rev-parse base~1^{tree})
676 3:tree::$(git rev-parse base~2^{tree})
628 - 3:tree::$(git rev-parse refs/tags/tree-tag^{})
629 - 3:tree::$(git rev-parse refs/tags/tree-tag2^{})
677 4:blob:a:$(git rev-parse base~2:a)
631 - 5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)
632 - 6:tree:a/:$(git rev-parse base:a)
633 - 7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
634 - 8:tree:left/:$(git rev-parse base:left)
635 - 8:tree:left/:$(git rev-parse base~2:left)
636 - 9:blob:left/b:$(git rev-parse base~2:left/b)
637 - 9:blob:left/b:$(git rev-parse base:left/b)
638 - 10:tree:right/:$(git rev-parse topic:right)
639 - 10:tree:right/:$(git rev-parse base~1:right)
640 - 10:tree:right/:$(git rev-parse base~2:right)
678 + 5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})
679 + 5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})
680 + 6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)
681 + 7:tree:a/:$(git rev-parse base:a)
682 + 8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
683 + 9:tree:left/:$(git rev-parse base:left)
684 + 9:tree:left/:$(git rev-parse base~2:left)
685 + 10:blob:left/b:$(git rev-parse base~2:left/b)
686 + 10:blob:left/b:$(git rev-parse base:left/b)
687 + 11:tree:right/:$(git rev-parse topic:right)
688 + 11:tree:right/:$(git rev-parse base~1:right)
689 + 11:tree:right/:$(git rev-parse base~2:right)
690 blobs:6
691 commits:4
692 tags:7