path-walk: support blobless filter

The 'git pack-objects' command can opt-in to using the path-walk API for scanning the objects. Currently, this option is dynamically disabled if combined with '--filter=<X>', even when using a simple filter such as 'blob:none' to signal a blobless packfile. This is a common scenario for repos at scale, so is worth integrating. Also, users can opt-in to the '--path-walk' option by default through the pack.usePathWalk=true config option. When using that in a blobless partial clone, the following warning can appear even though the user did not specify either option directly: warning: cannot use --filter with --path-walk Teach the path-walk API to handle the 'blob:none' object filter natively. When revs->filter.choice is LOFC_BLOB_NONE, the path-walk sets info->blobs to 0 (skipping all blob objects) and clears the filter from revs so that prepare_revision_walk() does not reject the configuration. This check is implemented in the static prepare_filters() method, which will simultaneously check if the input filters are compatible and will make the appropriate mutations to the path_walk_info and filters if the path_walk_info is non-NULL. This allows us to use this logic both in the API method path_walk_filter_compatible() for use in builtin/pack-objects.c and as a prep step in walk_objects_by_path(). Update the test helper (test-path-walk) to accept --filter=<spec> as a test-tool option (before '--'), applying it to revs after setup_revisions() to avoid the --objects requirement check. We can also revert recent GIT_TEST_PACK_PATH_WALK overrides in t5620. Also switch test-path-walk from REV_INFO_INIT with manual repo assignment to repo_init_revisions(), which properly initializes the filter_spec strbuf needed for filter parsing. Add tests for blob:none with --all and with a single branch. The performance test p5315 shows the impact of this change when using blobless filters: Test HEAD~1 HEAD --------------------------------------------------------------------- 5315.6: repack (blob:none) 13.53 13.87 +2.5% 5315.7: repack size (blob:none) 137.7M 137.8M +0.1% 5315.8: repack (blob:none, --path-walk) 13.51 23.43 +73.4% 5315.9: repack size (blob:none, --path-walk) 137.7M 115.2M -16.3% These performance tests were run on the Git repository. The --path-walk feature shows meaningful space savings (16% smaller for blobless packs) at the cost of increased computation time due to the two compression passes. This data demonstrates that the feature is engaged and provides real compression benefits when --no-reuse-delta forces fresh deltas. Co-Authored-by: Taylor Blau <me@ttaylorr.com> 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>

Derrick Stolee committed May 22, 2026 at 18:24 UTC 6d87f0e8a3bf3d718c7aaf3657cf3ce73c3bd026
7 files changed +113 -14
Documentation/git-pack-objects.adoc
+3 -3
@@ -402,9 +402,9 @@ will be automatically changed to version `1`.
402 of filenames that cause collisions in Git's default name-hash
403 algorithm.
404 +
405 -Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The
406 -`--use-bitmap-index` option will be ignored in the presence of
407 -`--path-walk.`
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>` form `blob:none`.
408
409
410 DELTA ISLANDS
builtin/pack-objects.c
+1 -1
@@ -5177,7 +5177,7 @@ int cmd_pack_objects(int argc,
5177
5178 if (path_walk) {
5179 const char *option = NULL;
5180 - if (filter_options.choice)
5180 + if (!path_walk_filter_compatible(&filter_options))
5181 option = "--filter";
5182 else if (use_delta_islands)
5183 option = "--delta-islands";
path-walk.c
+30
@@ -9,6 +9,7 @@
9 #include "hashmap.h"
10 #include "hex.h"
11 #include "list-objects.h"
12 +#include "list-objects-filter-options.h"
13 #include "object.h"
14 #include "oid-array.h"
15 #include "path.h"
@@ -495,6 +496,32 @@ static int setup_pending_objects(struct path_walk_info *info,
496 return 0;
497 }
498
499 +static int prepare_filters(struct path_walk_info *info,
500 + struct list_objects_filter_options *options)
501 +{
502 + switch (options->choice) {
503 + case LOFC_DISABLED:
504 + return 1;
505 +
506 + case LOFC_BLOB_NONE:
507 + if (info) {
508 + info->blobs = 0;
509 + list_objects_filter_release(options);
510 + }
511 + return 1;
512 +
513 + default:
514 + error(_("object filter '%s' not supported by the path-walk API"),
515 + list_objects_filter_spec(options));
516 + return 0;
517 + }
518 +}
519 +
520 +int path_walk_filter_compatible(struct list_objects_filter_options *options)
521 +{
522 + return prepare_filters(NULL, options);
523 +}
524 +
525 /**
526 * Given the configuration of 'info', walk the commits based on 'info->revs' and
527 * call 'info->path_fn' on each discovered path.
@@ -522,6 +549,9 @@ int walk_objects_by_path(struct path_walk_info *info)
549
550 trace2_region_enter("path-walk", "commit-walk", info->revs->repo);
551
552 + if (!prepare_filters(info, &info->revs->filter))
553 + return -1;
554 +
555 CALLOC_ARRAY(commit_list, 1);
556 commit_list->type = OBJ_COMMIT;
557
path-walk.h
+7
@@ -90,3 +90,10 @@ void path_walk_info_clear(struct path_walk_info *info);
90 * Returns nonzero on an error.
91 */
92 int walk_objects_by_path(struct path_walk_info *info);
93 +
94 +struct list_objects_filter_options;
95 +/**
96 + * Given a set of options for filtering objects, return 1 if the options
97 + * are compatible with the path-walk API and 0 otherwise.
98 + */
99 +int path_walk_filter_compatible(struct list_objects_filter_options *options);
t/helper/test-path-walk.c
+10 -1
@@ -4,6 +4,7 @@
4 #include "dir.h"
5 #include "environment.h"
6 #include "hex.h"
7 +#include "list-objects-filter-options.h"
8 #include "object-name.h"
9 #include "object.h"
10 #include "pretty.h"
@@ -71,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)
72 struct rev_info revs = REV_INFO_INIT;
73 struct path_walk_info info = PATH_WALK_INFO_INIT;
74 struct path_walk_test_data data = { 0 };
75 + struct list_objects_filter_options filter_options =
76 + LIST_OBJECTS_FILTER_INIT;
77 struct option options[] = {
78 OPT_BOOL(0, "blobs", &info.blobs,
79 N_("toggle inclusion of blob objects")),
@@ -86,11 +89,12 @@ int cmd__path_walk(int argc, const char **argv)
89 N_("toggle aggressive edge walk")),
90 OPT_BOOL(0, "stdin-pl", &stdin_pl,
91 N_("read a pattern list over stdin")),
92 + OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),
93 OPT_END(),
94 };
95
96 setup_git_directory();
93 - revs.repo = the_repository;
97 + repo_init_revisions(the_repository, &revs, NULL);
98
99 argc = parse_options(argc, argv, NULL,
100 options, path_walk_usage,
@@ -101,6 +105,10 @@ int cmd__path_walk(int argc, const char **argv)
105 else
106 usage(path_walk_usage[0]);
107
108 + /* Apply the filter after setup_revisions to avoid the --objects check. */
109 + if (filter_options.choice)
110 + list_objects_filter_copy(&revs.filter, &filter_options);
111 +
112 info.revs = &revs;
113 info.path_fn = emit_block;
114 info.path_fn_data = &data;
@@ -129,6 +137,7 @@ int cmd__path_walk(int argc, const char **argv)
137 free(info.pl);
138 }
139
140 + list_objects_filter_release(&filter_options);
141 release_revisions(&revs);
142 return res;
143 }
t/t5620-backfill.sh
-9
@@ -298,9 +298,6 @@ test_expect_success 'backfill with prefix pathspec' '
298 git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
299 test_line_count = 48 missing &&
300
301 - # If we enable --path-walk here, we will get a warning overs stderr
302 - # due to incompatibilities with --filter.
303 - GIT_TEST_PACK_PATH_WALK=0 \
301 git -C backfill-path backfill HEAD -- d/f 2>err &&
302 test_must_be_empty err &&
303
@@ -318,9 +315,6 @@ test_expect_success 'backfill with multiple pathspecs' '
315 git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
316 test_line_count = 48 missing &&
317
321 - # If we enable --path-walk here, we will get a warning overs stderr
322 - # due to incompatibilities with --filter.
323 - GIT_TEST_PACK_PATH_WALK=0 \
318 git -C backfill-path backfill HEAD -- d/f a 2>err &&
319 test_must_be_empty err &&
320
@@ -338,9 +332,6 @@ test_expect_success 'backfill with wildcard pathspec' '
332 git -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&
333 test_line_count = 48 missing &&
334
341 - # If we enable --path-walk here, we will get a warning overs stderr
342 - # due to incompatibilities with --filter.
343 - GIT_TEST_PACK_PATH_WALK=0 \
335 git -C backfill-path backfill HEAD -- "d/file.*.txt" 2>err &&
336 test_must_be_empty err &&
337
t/t6601-path-walk.sh
+62
@@ -415,4 +415,66 @@ test_expect_success 'trees are reported exactly once' '
415 test_line_count = 1 out-filtered
416 '
417
418 +test_expect_success 'all, blob:none filter' '
419 + test-tool path-walk --filter=blob:none -- --all >out &&
420 +
421 + cat >expect <<-EOF &&
422 + 0:commit::$(git rev-parse topic)
423 + 0:commit::$(git rev-parse base)
424 + 0:commit::$(git rev-parse base~1)
425 + 0:commit::$(git rev-parse base~2)
426 + 1:tag:/tags:$(git rev-parse refs/tags/first)
427 + 1:tag:/tags:$(git rev-parse refs/tags/second.1)
428 + 1:tag:/tags:$(git rev-parse refs/tags/second.2)
429 + 1:tag:/tags:$(git rev-parse refs/tags/third)
430 + 1:tag:/tags:$(git rev-parse refs/tags/fourth)
431 + 1:tag:/tags:$(git rev-parse refs/tags/tree-tag)
432 + 1:tag:/tags:$(git rev-parse refs/tags/blob-tag)
433 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})
434 + 2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})
435 + 3:tree::$(git rev-parse topic^{tree})
436 + 3:tree::$(git rev-parse base^{tree})
437 + 3:tree::$(git rev-parse base~1^{tree})
438 + 3:tree::$(git rev-parse base~2^{tree})
439 + 3:tree::$(git rev-parse refs/tags/tree-tag^{})
440 + 3:tree::$(git rev-parse refs/tags/tree-tag2^{})
441 + 4:tree:a/:$(git rev-parse base:a)
442 + 5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)
443 + 6:tree:left/:$(git rev-parse base:left)
444 + 6:tree:left/:$(git rev-parse base~2:left)
445 + 7:tree:right/:$(git rev-parse topic:right)
446 + 7:tree:right/:$(git rev-parse base~1:right)
447 + 7:tree:right/:$(git rev-parse base~2:right)
448 + blobs:2
449 + commits:4
450 + tags:7
451 + trees:13
452 + EOF
453 +
454 + test_cmp_sorted expect out
455 +'
456 +
457 +test_expect_success 'topic only, blob:none filter' '
458 + test-tool path-walk --filter=blob:none -- topic >out &&
459 +
460 + cat >expect <<-EOF &&
461 + 0:commit::$(git rev-parse topic)
462 + 0:commit::$(git rev-parse base~1)
463 + 0:commit::$(git rev-parse base~2)
464 + 1:tree::$(git rev-parse topic^{tree})
465 + 1:tree::$(git rev-parse base~1^{tree})
466 + 1:tree::$(git rev-parse base~2^{tree})
467 + 2:tree:left/:$(git rev-parse base~2:left)
468 + 3:tree:right/:$(git rev-parse topic:right)
469 + 3:tree:right/:$(git rev-parse base~1:right)
470 + 3:tree:right/:$(git rev-parse base~2:right)
471 + blobs:0
472 + commits:3
473 + tags:0
474 + trees:7
475 + EOF
476 +
477 + test_cmp_sorted expect out
478 +'
479 +
480 test_done