{"thread":{"id":"65575","subject":"[PATCH 0/7] pack-objects: integrate --path-walk and some --filter options","startedAt":"2026-05-02T14:15:58Z","lastAt":"2026-05-24T23:44:20Z","messageCount":96,"participants":["Derrick Stolee via GitGitGadget","Junio C Hamano","Derrick Stolee","Taylor Blau via GitGitGadget","Taylor Blau"],"isPatch":true,"patchVersion":1,"patchTotal":7},"messages":[{"id":"542585","messageId":"pull.2101.git.1777731354.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":null,"subject":"[PATCH 0/7] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:47Z","receivedAt":"2026-05-02T14:15:58Z","isPatch":true,"body":"The 'git pack-objects' command has a '--path-walk' option that uses the\npath-walk API instead of a typical revision walk to group objects into\nchunks by path name instead of relying solely on name-hashes to group\nsimilar files together. (It also does a second compression pass looking for\nbetter deltas after the first pass that is focused within chunks per path.)\n\nThe '--path-walk' feature was not previously integrated with the '--filter'\nfeature, so a warning would appear and disable the path-walk API when a\nfilter is given. This patch series integrates these together in the\nfollowing ways:\n\n * --filter=blob:none updates the path-walk API options to skip blobs.\n * --filter=blob:limit=<size> adds a scan to a list of blob objects to\n   remove objects that are too large.\n * --filter=sparse:<oid> adds a scan to the chunks to validate that the\n   paths match the sparse-checkout patterns.\n\nIn particular, this last check is significantly faster than the previous\nalgorithm because it can check all objects at a given path simultaneously\ninstead of checking all sparse-checkout patterns for each discovered blob\nobject.\n\nA subtlety must be added here, in that we must change how we mark an object\nas \"seen\" during the path-walk. We may need to add an object to multiple\npaths and only mark it as \"seen\" if it indeed matched the sparse-checkout\npatterns as the path is accepted for emitting to the callback. This adds a\nnew filter that the \"seen\" objects must also be removed from later chunks to\navoid sending the same object as grouped to multiple chunks.\n\nThere's also a subtle detail here in that the path-walk API also prunes tree\npaths based on cone-mode sparse-checkouts, to enable 'git backfill --sparse'\noperating quickly for small sparse-checkout scopes. But the\n--filter=sparse:<oid> feature doesn't prune trees!\n\nAs a future step, I do plan to recommend that we add a treesparse:<oid>\nsetting that does allow us to trim the tree set by cone-mode sparse\npatterns. At the time that partial clone filters were being created, cone\nmode sparse-checkout didn't exist and neither did the sparse index. Those\nfeatures together make a smaller tree set possible, assuming the user never\nneeds to change their scope. This would be a significant change so it is not\nimplemented here, though the git pack-objects integration would be quick\nafter this series completes.\n\nNeither the sparse:<oid> or hypothetical treesparse:<oid> options are or\nshould necessarily be supported by Git servers. It's too expensive to\ncompute dynamically and it doesn't work well with reachability bitmaps. What\nbecomes possible with this change is that it becomes reasonably fast to\nconstruct bundles with these filters that can bootstrap a working\nenvironment with the full history of all files within a given\nsparse-checkout scope.\n\n\nPerformance Results\n===================\n\nSince the '--path-walk' option is ignored in today's Git version when a\n'--filter' is added, the performance matches the behavior without\n'--path-walk'. For the tables below, you can compare the rows against each\nother (time and then packfile size) for the mode without and then with\n'--path-walk' as a representation of \"before\" and \"after\". (These tables are\nrepeated in the commit messages as new implementations improve specific\nrows.)\n\nI chose a number of open source repositories of various sizes and shapes:\n\n\ngit/git\n=======\n\nTest                                              HEAD\n-------------------------------------------------------------------\n5315.2: repack (no filter)                       27.73\n5315.3: repack size (no filter)                 250.6M\n5315.4: repack (no filter, --path-walk)          35.19\n5315.5: repack size (no filter, --path-walk)    220.1M\n5315.6: repack (blob:none)                       13.42\n5315.7: repack size (blob:none)                 137.6M\n5315.8: repack (blob:none, --path-walk)          20.98\n5315.9: repack size (blob:none, --path-walk)    115.2M\n5315.10: repack (sparse:oid)                     72.53\n5315.11: repack size (sparse:oid)               187.5M\n5315.12: repack (sparse:oid, --path-walk)        29.00\n5315.13: repack size (sparse:oid, --path-walk)  161.0M\n\n\n\nnodejs/node\n===========\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                       75.53\n5315.3: repack size (no filter)                   0.9G\n5315.4: repack (no filter, --path-walk)          80.54\n5315.5: repack size (no filter, --path-walk)    885.7M\n5315.6: repack (blob:none)                       12.65\n5315.7: repack size (blob:none)                 148.6M\n5315.8: repack (blob:none, --path-walk)          17.60\n5315.9: repack size (blob:none, --path-walk)    134.6M\n5315.10: repack (sparse:oid)                    518.84\n5315.11: repack size (sparse:oid)               153.4M\n5315.12: repack (sparse:oid, --path-walk)        27.99\n5315.13: repack size (sparse:oid, --path-walk)  139.4M\n\n\n\nmicrosoft/fluentui\n==================\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                      146.77\n5315.3: repack size (no filter)                 562.1M\n5315.4: repack (no filter, --path-walk)          72.82\n5315.5: repack size (no filter, --path-walk)    172.6M\n5315.6: repack (blob:none)                        4.84\n5315.7: repack size (blob:none)                  62.7M\n5315.8: repack (blob:none, --path-walk)           5.19\n5315.9: repack size (blob:none, --path-walk)     59.9M\n5315.10: repack (sparse:oid)                     59.95\n5315.11: repack size (sparse:oid)                85.6M\n5315.12: repack (sparse:oid, --path-walk)        15.16\n5315.13: repack size (sparse:oid, --path-walk)   72.5M\n\n\n\nmicrosoftdocs/azure-devops-docs\n===============================\n\nTest                                               HEAD\n---------------------------------------------------------------------\n5315.2: repack (no filter)                        4.41\n5315.3: repack size (no filter)                   1.6G\n5315.4: repack (no filter, --path-walk)           6.00\n5315.5: repack size (no filter, --path-walk)      1.6G\n5315.6: repack (blob:none)                        1.35\n5315.7: repack size (blob:none)                  60.0M\n5315.8: repack (blob:none, --path-walk)           1.23\n5315.9: repack size (blob:none, --path-walk)     60.0M\n5315.10: repack (sparse:oid)                    138.24\n5315.11: repack size (sparse:oid)                84.4M\n5315.12: repack (sparse:oid, --path-walk)         1.86\n5315.13: repack size (sparse:oid, --path-walk)   84.4M\n\n\n\nPerformance conclusions\n=======================\n\nAs seen in earlier series around the '--path-walk' feature, the space\nsavings can be valuable but is not always guaranteed. When the space savings\ndoesn't happen, then the time spent is generally slower because of the\ntwo-pass mechanism. The microsoftdocs/azure-devops-docs repo demonstrates\nthis case quite clearly.\n\nHowever, even in these cases the 'sparse:<oid>' filters are much faster\nbecause of the ability to check an entire set of objects against the\nsparse-checkout patterns only once.\n\nThanks, -Stolee\n\nP.S. I've CC'd the folks who were on the original path-walk feature thread\n[1]\n\n[1]\nhttps://lore.kernel.org/git/pull.1819.git.1741571455.gitgitgadget@gmail.com/\n\nDerrick Stolee (7):\n  pack-objects: pass --objects with --path-walk\n  t/perf: add pack-objects filter and path-walk benchmark\n  path-walk: support blobless filter\n  backfill: die on incompatible filter options\n  path-walk: support blob size limit filter\n  path-walk: add pl_sparse_trees to control tree pruning\n  pack-objects: support sparse:oid filter with path-walk\n\n builtin/backfill.c                     |   5 +\n builtin/pack-objects.c                 |   9 +-\n path-walk.c                            | 154 ++++++++++++-\n path-walk.h                            |  21 ++\n t/helper/test-path-walk.c              |  17 +-\n t/perf/p5315-pack-objects-filter.sh    | 129 +++++++++++\n t/t5317-pack-objects-filter-objects.sh | 125 ++++++++++\n t/t5620-backfill.sh                    |   8 +\n t/t6601-path-walk.sh                   | 306 +++++++++++++++++++++++++\n 9 files changed, 758 insertions(+), 16 deletions(-)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\n\nbase-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2101%2Fderrickstolee%2Fpath-walk-filters-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2101/derrickstolee/path-walk-filters-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2101\n-- \ngitgitgadget\n"},{"id":"542586","messageId":"29d2797440412bfec85be7b6a6439350b9ebf5e8.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 1/7] pack-objects: pass --objects with --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:48Z","receivedAt":"2026-05-02T14:15:59Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen 'git pack-objects' has the --path-walk option enabled, it uses a\ndifferent set of revision walk parameters than normal. For once,\n--objects was previously assumed by the path-walk API and was not needed\nto be added. We also needed --boundary to allow discovering\nUNINTERESTING objects to use as delta bases.\n\nWe will be updating the path-walk API soon to work with some filter\noptions. However, the revision machinery will trigger a fatal error:\n\n  fatal: object filtering requires --objects\n\nThe fix is easy: add the --objects option as an argument. This has no\neffect on the path-walk API but does simplify the revision option\nparsing for the objects filter.\n\nWe can remove the comment about \"removing\" the options because they were\nnever removed and instead not added. We still need to disable using\nbitmaps.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex dd2480a73d..4338962904 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5190,10 +5190,7 @@ int cmd_pack_objects(int argc,\n \t}\n \tif (path_walk) {\n \t\tstrvec_push(&rp, \"--boundary\");\n-\t\t /*\n-\t\t  * We must disable the bitmaps because we are removing\n-\t\t  * the --objects / --objects-edge[-aggressive] options.\n-\t\t  */\n+\t\tstrvec_push(&rp, \"--objects\");\n \t\tuse_bitmap_index = 0;\n \t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n-- \ngitgitgadget\n\n"},{"id":"542587","messageId":"f3646218153ecfe5d534033cb54b1a43733ca3dd.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 2/7] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:49Z","receivedAt":"2026-05-02T14:16:01Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd p5315-pack-objects-filter.sh to measure the performance of\n'git pack-objects --revs --all' under different filter and traversal\ncombinations:\n\n * no filter (baseline)\n * --filter=blob:none (blobless)\n * --filter=sparse:oid=<oid> (cone-mode sparse)\n\nEach filter scenario is tested both with and without --path-walk,\nproducing paired measurements that show the impact of the path-walk\ntraversal for each filter type as we integrate the --path-walk feature\nwith different --filter options. It currently has no integration so\nfalls back to the standard revision walk. Thus, there are no significant\ndifferences in the current results other than a full repack (and even\nthen, the --path-walk feature is not incredibly different for the\ndefault Git repository):\n\nTest                                             HEAD\n-----------------------------------------------------\n5315.2: repack (no filter)                      27.91\n5315.3: repack size (no filter)                250.7M\n5315.4: repack (no filter, --path-walk)         34.92\n5315.5: repack size (no filter, --path-walk)   220.0M\n5315.6: repack (blob:none)                      13.63\n5315.7: repack size (blob:none)                137.6M\n5315.8: repack (blob:none, --path-walk)         13.48\n5315.9: repack size (blob:none, --path-walk)   137.7M\n5315.10: repack (sparse:oid)                    72.67\n5315.11: repack size (sparse:oid)              187.4M\n5315.12: repack (sparse:oid, --path-walk)       72.47\n5315.13: repack size (sparse:oid, --path-walk) 187.4M\n\nThe sparse filter definition is built automatically by sampling\ndepth-2 directories from the test repository, making the test work\non any repo passed via GIT_PERF_LARGE_REPO. For repos that lack\ndepth-2 directories, a single top-level directory is used; for flat\nrepos, the sparse tests are skipped via prerequisite.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5315-pack-objects-filter.sh | 129 ++++++++++++++++++++++++++++\n 1 file changed, 129 insertions(+)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\ndiff --git a/t/perf/p5315-pack-objects-filter.sh b/t/perf/p5315-pack-objects-filter.sh\nnew file mode 100755\nindex 0000000000..b009039c89\n--- /dev/null\n+++ b/t/perf/p5315-pack-objects-filter.sh\n@@ -0,0 +1,129 @@\n+#!/bin/sh\n+\n+test_description='Tests pack-objects performance with filters and --path-walk'\n+. ./perf-lib.sh\n+\n+test_perf_large_repo\n+\n+test_expect_success 'setup filter inputs' '\n+\t# Sample a few depth-2 directories from the test repo to build\n+\t# a cone-mode sparse-checkout definition.  The sampling picks\n+\t# directories at evenly-spaced positions so the choice is stable\n+\t# and scales to repos of any shape.\n+\n+\tgit ls-tree -d --name-only HEAD >top-dirs &&\n+\ttop_nr=$(wc -l <top-dirs) &&\n+\n+\t>depth2-dirs &&\n+\twhile read tdir\n+\tdo\n+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null |\n+\t\tsed \"s|^|$tdir/|\" >>depth2-dirs || return 1\n+\tdone <top-dirs &&\n+\n+\td2_nr=$(wc -l <depth2-dirs) &&\n+\n+\tif test \"$d2_nr\" -ge 2\n+\tthen\n+\t\t# Pick two directories from evenly-spaced positions.\n+\t\tfirst=$(sed -n \"1p\" depth2-dirs) &&\n+\t\tmid=$(sed -n \"$((d2_nr / 2 + 1))p\" depth2-dirs) &&\n+\n+\t\tp1=$(dirname \"$first\") &&\n+\t\tp2=$(dirname \"$mid\") &&\n+\n+\t\t# Build cone-mode sparse-checkout patterns.\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$p1/\" &&\n+\t\t\techo \"!/$p1/*/\" &&\n+\t\t\tif test \"$p1\" != \"$p2\"\n+\t\t\tthen\n+\t\t\t\techo \"/$p2/\" &&\n+\t\t\t\techo \"!/$p2/*/\"\n+\t\t\tfi &&\n+\t\t\techo \"/$first/\" &&\n+\t\t\tif test \"$first\" != \"$mid\"\n+\t\t\tthen\n+\t\t\t\techo \"/$mid/\"\n+\t\t\tfi\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first $mid\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\telif test \"$top_nr\" -ge 1\n+\tthen\n+\t\t# Fallback: use a single top-level directory.\n+\t\tfirst=$(sed -n \"1p\" top-dirs) &&\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$first/\"\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\tfi\n+'\n+\n+test_perf 'repack (no filter)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (no filter, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"542588","messageId":"b7e72d5dbb8c00e3ee35da3e9853faddf8e42748.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 3/7] path-walk: support blobless filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:50Z","receivedAt":"2026-05-02T14:16:02Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git pack-objects' command can opt-in to using the path-walk API for\nscanning the objects. Currently, this option is dynamically disabled if\ncombined with '--filter=<X>', even when using a simple filter such as\n'blob:none' to signal a blobless packfile. This is a common scenario for\nrepos at scale, so is worth integrating.\n\nAlso, users can opt-in to the '--path-walk' option by default through\nthe pack.usePathWalk=true config option. When using that in a blobless\npartial clone, the following warning can appear even though the user did\nnot specify either option directly:\n\n  warning: cannot use --filter with --path-walk\n\nTeach the path-walk API to handle the 'blob:none' object filter\nnatively. When revs->filter.choice is LOFC_BLOB_NONE, the path-walk\nsets info->blobs to 0 (skipping all blob objects) and clears the\nfilter from revs so that prepare_revision_walk() does not reject the\nconfiguration.\n\nThis check is implemented in the static prepare_filters() method, which\nwill simultaneously check if the input filters are compatible and will\nmake the appropriate mutations to the path_walk_info and filters if the\npath_walk_info is non-NULL. This allows us to use this logic both in the\nAPI method path_walk_filter_compatible() for use in\nbuiltin/pack-objects.c and as a prep step in walk_objects_by_path().\n\nUpdate the test helper (test-path-walk) to accept --filter=<spec>\nas a test-tool option (before '--'), applying it to revs after\nsetup_revisions() to avoid the --objects requirement check.\n\nAlso switch test-path-walk from REV_INFO_INIT with manual repo\nassignment to repo_init_revisions(), which properly initializes\nthe filter_spec strbuf needed for filter parsing.\n\nAdd tests for blob:none with --all and with a single branch.\n\nThe performance test p5315 shows the impact of this change when using\nblobless filters:\n\nTest                                           HEAD~1     HEAD\n---------------------------------------------------------------------\n5315.6: repack (blob:none)                      13.53   13.87  +2.5%\n5315.7: repack size (blob:none)                137.7M  137.8M  +0.1%\n5315.8: repack (blob:none, --path-walk)         13.51   23.43 +73.4%\n5315.9: repack size (blob:none, --path-walk)   137.7M  115.2M -16.3%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (16% smaller for blobless packs)\nat the cost of increased computation time due to the two compression\npasses. This data demonstrates that the feature is engaged and provides\nreal compression benefits when --no-reuse-delta forces fresh deltas.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c    |  2 +-\n path-walk.c               | 30 ++++++++++++++++++++\n path-walk.h               |  7 +++++\n t/helper/test-path-walk.c | 11 ++++++-\n t/t6601-path-walk.sh      | 60 +++++++++++++++++++++++++++++++++++++++\n 5 files changed, 108 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 4338962904..bc9fb5b457 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5177,7 +5177,7 @@ int cmd_pack_objects(int argc,\n \n \tif (path_walk) {\n \t\tconst char *option = NULL;\n-\t\tif (filter_options.choice)\n+\t\tif (!path_walk_filter_compatible(&filter_options))\n \t\t\toption = \"--filter\";\n \t\telse if (use_delta_islands)\n \t\t\toption = \"--delta-islands\";\ndiff --git a/path-walk.c b/path-walk.c\nindex 6e426af433..a4dd197c37 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -9,6 +9,7 @@\n #include \"hashmap.h\"\n #include \"hex.h\"\n #include \"list-objects.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -485,6 +486,32 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tswitch (options->choice) {\n+\tcase LOFC_DISABLED:\n+\t\treturn 1;\n+\n+\tcase LOFC_BLOB_NONE:\n+\t\tif (info) {\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n+\tdefault:\n+\t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n+\t\t      list_objects_filter_spec(options));\n+\t\treturn 0;\n+\t}\n+}\n+\n+int path_walk_filter_compatible(struct list_objects_filter_options *options)\n+{\n+\treturn prepare_filters(NULL, options);\n+}\n+\n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\n  * call 'info->path_fn' on each discovered path.\n@@ -512,6 +539,9 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tif (!prepare_filters(info, &info->revs->filter))\n+\t\treturn -1;\n+\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 5ef5a8440e..be8d27b398 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -85,3 +85,10 @@ void path_walk_info_clear(struct path_walk_info *info);\n  * Returns nonzero on an error.\n  */\n int walk_objects_by_path(struct path_walk_info *info);\n+\n+struct list_objects_filter_options;\n+/**\n+ * Given a set of options for filtering objects, return 1 if the options\n+ * are compatible with the path-walk API and 0 otherwise.\n+ */\n+int path_walk_filter_compatible(struct list_objects_filter_options *options);\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex fe63002c2b..88f86ae0dc 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -4,6 +4,7 @@\n #include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object-name.h\"\n #include \"object.h\"\n #include \"pretty.h\"\n@@ -71,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n+\tstruct list_objects_filter_options filter_options =\n+\t\tLIST_OBJECTS_FILTER_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n@@ -86,11 +89,12 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n \n \tsetup_git_directory();\n-\trevs.repo = the_repository;\n+\trepo_init_revisions(the_repository, &revs, NULL);\n \n \targc = parse_options(argc, argv, NULL,\n \t\t\t     options, path_walk_usage,\n@@ -101,6 +105,10 @@ int cmd__path_walk(int argc, const char **argv)\n \telse\n \t\tusage(path_walk_usage[0]);\n \n+\t/* Apply the filter after setup_revisions to avoid the --objects check. */\n+\tif (filter_options.choice)\n+\t\tlist_objects_filter_copy(&revs.filter, &filter_options);\n+\n \tinfo.revs = &revs;\n \tinfo.path_fn = emit_block;\n \tinfo.path_fn_data = &data;\n@@ -129,6 +137,7 @@ int cmd__path_walk(int argc, const char **argv)\n \t\tfree(info.pl);\n \t}\n \n+\tlist_objects_filter_release(&filter_options);\n \trelease_revisions(&revs);\n \treturn res;\n }\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 56bd1e3c5b..94df309987 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -415,4 +415,64 @@ test_expect_success 'trees are reported exactly once' '\n \ttest_line_count = 1 out-filtered\n '\n \n+test_expect_success 'all, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:tree::$(git rev-parse topic^{tree})\n+\t2:tree::$(git rev-parse base^{tree})\n+\t2:tree::$(git rev-parse base~1^{tree})\n+\t2:tree::$(git rev-parse base~2^{tree})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t5:tree:left/:$(git rev-parse base:left)\n+\t5:tree:left/:$(git rev-parse base~2:left)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:tree:left/:$(git rev-parse base~2:left)\n+\t3:tree:right/:$(git rev-parse topic:right)\n+\t3:tree:right/:$(git rev-parse base~1:right)\n+\t3:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"542589","messageId":"22db528d0c5e6db71664f071dca97ab218f4ba45.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 4/7] backfill: die on incompatible filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:51Z","receivedAt":"2026-05-02T14:16:04Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git backfill' command uses the path-walk API in a critical way: it\nuses the objects output from the command to find the batches of missing\nobjects that should be requested from the server. Unlike 'git\npack-objects', we cannot fall back to another mechanism.\n\nThe previous change added the path_walk_filter_compatible() method that\nwe can reuse here. Use it during argument validation in cmd_backfill().\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c  | 2 ++\n t/t5620-backfill.sh | 8 ++++++++\n 2 files changed, 10 insertions(+)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex d794dd842f..51eaa42169 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -144,6 +144,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n \n \tif (argc > 1)\n \t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n+\tif (!path_walk_filter_compatible(&ctx.revs.filter))\n+\t\tdie(_(\"cannot backfill with these filter options\"));\n \n \trepo_config(repo, git_default_config, NULL);\n \ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex f3b5e39493..3580e10b9c 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -15,6 +15,14 @@ test_expect_success 'backfill rejects unexpected arguments' '\n \ttest_grep \"unrecognized argument: --unexpected-arg\" err\n '\n \n+test_expect_success 'backfill rejects incompatible filter options' '\n+\ttest_must_fail git backfill --objects --filter=tree:1 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err &&\n+\n+\ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err\n+'\n+\n # We create objects in the 'src' repo.\n test_expect_success 'setup repo for object creation' '\n \techo \"{print \\$1}\" >print_1.awk &&\n-- \ngitgitgadget\n\n"},{"id":"542590","messageId":"2e786160bdc283617de5b4c901c76773f1ed3a41.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 5/7] path-walk: support blob size limit filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:52Z","receivedAt":"2026-05-02T14:16:06Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nExtend the path-walk API to handle the 'blob:limit=<size>' object\nfilter natively. This filter omits blobs whose size is equal to or\ngreater than the given limit, matching the semantics used by the\nlist-objects-filter machinery.\n\nWhen revs->filter.choice is LOFC_BLOB_LIMIT, the prepare_filters()\nmethod stores the limit value in info->blob_limit and clears the filter\nfrom revs. If the limit is zero, this degenerates to blob:none (all\nblobs excluded), so info->blobs is set to 0 instead.\n\nDuring walk_path(), blob batches are filtered before being delivered to\nthe callback: each blob's size is checked via odb_read_object_info(),\nand only blobs strictly smaller than the limit are included. Blobs whose\nsize cannot be determined (e.g. missing in a partial clone) are\nconservatively included, matching the existing filter behavior. Empty\nbatches after filtering are skipped entirely.\n\nThe check for inclusion in the path batch looks a little strange at\nfirst glance. We use odb_read_object_info() to read the object's size.\nBased on all of the assumptions to this point, this _should_ return\nOBJ_BLOB. Since we are focused on the size filter, we use a\nshort-circuited OR (||) to skip the size check if that method returns a\ndifferent object type.\n\nNotice that this inspection of object sizes requires the content to be\npresent in the repository. The odb_read_object_info() call will download\na missing blob on-demand. This means that the use of the path-walk API\nwithin 'git backfill' would not operate nicely with this filter type.\nThe intention of that command is to download missing blobs in batches.\nDownloading objects one-by-one would go against the point. Update the\nvalidation in 'git backfill' to add its own compatibility check on top\nof path_walk_filter_compatible().\n\nAdd tests for blob:limit=0 (equivalent to blob:none) and blob:limit=3\n(which exercises partial filtering within a batch where some blobs are\nkept and others are excluded).\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c   |  2 ++\n path-walk.c          | 38 +++++++++++++++++++--\n path-walk.h          |  8 +++++\n t/t5620-backfill.sh  |  2 +-\n t/t6601-path-walk.sh | 78 ++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 124 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 51eaa42169..7ef9dc305e 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -146,6 +146,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n \t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n \tif (!path_walk_filter_compatible(&ctx.revs.filter))\n \t\tdie(_(\"cannot backfill with these filter options\"));\n+\tif (ctx.revs.filter.blob_limit_value)\n+\t\tdie(_(\"cannot backfill with blob size limits\"));\n \n \trepo_config(repo, git_default_config, NULL);\n \ndiff --git a/path-walk.c b/path-walk.c\nindex a4dd197c37..0e7dab7a6a 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -315,9 +316,29 @@ static int walk_path(struct path_walk_context *ctx,\n \t/* Evaluate function pointer on this data, if requested. */\n \tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n \t    (list->type == OBJ_BLOB && ctx->info->blobs) ||\n-\t    (list->type == OBJ_TAG && ctx->info->tags))\n-\t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n-\t\t\t\t\tctx->info->path_fn_data);\n+\t    (list->type == OBJ_TAG && ctx->info->tags)) {\n+\t\tstruct oid_array *oids = &list->oids;\n+\t\tstruct oid_array filtered = OID_ARRAY_INIT;\n+\n+\t\tif (list->type == OBJ_BLOB && ctx->info->blob_limit) {\n+\t\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\t\tunsigned long size;\n+\n+\t\t\t\tif (odb_read_object_info(ctx->repo->objects,\n+\t\t\t\t\t\t\t &list->oids.oid[i],\n+\t\t\t\t\t\t\t &size) != OBJ_BLOB ||\n+\t\t\t\t    size < ctx->info->blob_limit)\n+\t\t\t\t\toid_array_append(&filtered,\n+\t\t\t\t\t\t\t &list->oids.oid[i]);\n+\t\t\t}\n+\t\t\toids = &filtered;\n+\t\t}\n+\n+\t\tif (oids->nr)\n+\t\t\tret = ctx->info->path_fn(path, oids, list->type,\n+\t\t\t\t\t\t ctx->info->path_fn_data);\n+\t\toid_array_clear(&filtered);\n+\t}\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -500,6 +521,17 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_BLOB_LIMIT:\n+\t\tif (info) {\n+\t\t\tif (!options->blob_limit_value) {\n+\t\t\t\tinfo->blobs = 0;\n+\t\t\t} else {\n+\t\t\t\tinfo->blob_limit = options->blob_limit_value;\n+\t\t\t}\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/path-walk.h b/path-walk.h\nindex be8d27b398..bcb81b70a1 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -42,6 +42,14 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If non-zero, specifies a maximum blob size. Blobs with a\n+\t * size equal to or greater than this limit will be omitted\n+\t * from the walk. Blobs smaller than the limit (or blobs\n+\t * whose size cannot be determined) are still visited.\n+\t */\n+\tunsigned long blob_limit;\n+\n \t/**\n \t * When 'prune_all_uninteresting' is set and a path has all objects\n \t * marked as UNINTERESTING, then the path-walk will not visit those\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 3580e10b9c..3c8a75192a 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -20,7 +20,7 @@ test_expect_success 'backfill rejects incompatible filter options' '\n \ttest_grep \"cannot backfill with these filter options\" err &&\n \n \ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n-\ttest_grep \"cannot backfill with these filter options\" err\n+\ttest_grep \"cannot backfill with blob size limits\" err\n '\n \n # We create objects in the 'src' repo.\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 94df309987..d9be7b9cd2 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -475,4 +475,82 @@ test_expect_success 'topic only, blob:none filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, blob:limit=0 filter' '\n+\ttest-tool path-walk --filter=blob:limit=0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:tree::$(git rev-parse topic^{tree})\n+\t2:tree::$(git rev-parse base^{tree})\n+\t2:tree::$(git rev-parse base~1^{tree})\n+\t2:tree::$(git rev-parse base~2^{tree})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t5:tree:left/:$(git rev-parse base:left)\n+\t5:tree:left/:$(git rev-parse base~2:left)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, blob:limit=3 filter' '\n+\ttest-tool path-walk --filter=blob:limit=3 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:tree::$(git rev-parse topic^{tree})\n+\t2:tree::$(git rev-parse base^{tree})\n+\t2:tree::$(git rev-parse base~1^{tree})\n+\t2:tree::$(git rev-parse base~2^{tree})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t3:blob:a:$(git rev-parse base~2:a)\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:blob:left/b:$(git rev-parse base~2:left/b)\n+\t8:tree:right/:$(git rev-parse topic:right)\n+\t8:tree:right/:$(git rev-parse base~1:right)\n+\t8:tree:right/:$(git rev-parse base~2:right)\n+\t9:blob:right/c:$(git rev-parse base~2:right/c)\n+\t10:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:4\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"542591","messageId":"c5aca53b238a9d0895a3505a088a5a770fee929d.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 6/7] path-walk: add pl_sparse_trees to control tree pruning","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:53Z","receivedAt":"2026-05-02T14:16:07Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe path-walk API prunes trees and blobs when a sparse-checkout pattern\nlist is provided, which is the correct behavior for 'git backfill\n--sparse' since it only needs to fill in objects at paths within the\nsparse cone.\n\nHowever, a future change will use the path-walk API with a sparse:<oid>\nfilter that restricts only blobs while retaining all reachable trees.\nTo support both behaviors, add a 'pl_sparse_trees' flag to\npath_walk_info. When set (as in 'git backfill --sparse' and the\n--stdin-pl test helper mode), the sparse patterns prune both trees and\nblobs. When unset, only blobs are filtered and all trees are walked and\nreported.\n\nAdditionally, move the SEEN flag assignment in add_tree_entries() to\nafter the sparse pattern and pathspec checks. Previously, SEEN was set\nimmediately upon discovering an object, before checking whether its path\nmatched the sparse patterns. When the same object ID appeared at\nmultiple paths (e.g. sibling directories with identical contents), the\nfirst path to be visited would mark the object as SEEN. If that path was\noutside the sparse cone, the object would be skipped there but also\nnever discovered at its in-cone path.\n\nBy deferring the SEEN flag until after the checks pass, objects that are\nskipped due to sparse filtering remain discoverable at other paths where\nthey may be in scope.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c        |  1 +\n path-walk.c               |  5 +++--\n path-walk.h               |  6 ++++++\n t/helper/test-path-walk.c |  6 +++++-\n t/t6601-path-walk.sh      | 37 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 52 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 7ef9dc305e..b661c34f7c 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -85,6 +85,7 @@ static int do_backfill(struct backfill_context *ctx)\n \n \tif (ctx->sparse) {\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = 1;\n \t\tif (get_sparse_checkout_patterns(info.pl)) {\n \t\t\tpath_walk_info_clear(&info);\n \t\t\treturn error(_(\"problem loading sparse-checkout\"));\ndiff --git a/path-walk.c b/path-walk.c\nindex 0e7dab7a6a..c25392b901 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -183,7 +183,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t/* Skip this object if already seen. */\n \t\tif (o->flags & SEEN)\n \t\t\tcontinue;\n-\t\to->flags |= SEEN;\n \n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n@@ -204,7 +203,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\t\t\t\t  ctx->repo->index);\n \n \t\t\tif (ctx->info->pl->use_cone_patterns &&\n-\t\t\t    match == NOT_MATCHED)\n+\t\t\t    match == NOT_MATCHED &&\n+\t\t\t    (type == OBJ_BLOB || ctx->info->pl_sparse_trees))\n \t\t\t\tcontinue;\n \t\t\telse if (!ctx->info->pl->use_cone_patterns &&\n \t\t\t\t type == OBJ_BLOB &&\n@@ -239,6 +239,7 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\tcontinue;\n \t\t}\n \n+\t\to->flags |= SEEN;\n \t\tadd_path_to_list(ctx, path.buf, type, &entry.oid,\n \t\t\t\t !(o->flags & UNINTERESTING));\n \ndiff --git a/path-walk.h b/path-walk.h\nindex bcb81b70a1..5fa3ff46b4 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -72,8 +72,14 @@ struct path_walk_info {\n \t * of the cone. If not in cone mode, then all tree paths will be\n \t * explored but the path_fn will only be called when the path matches\n \t * the sparse-checkout patterns.\n+\t *\n+\t * When 'pl_sparse_trees' is zero, the sparse patterns only restrict\n+\t * blobs and all trees are included in the walk output. This matches\n+\t * the behavior of the sparse:oid object filter. When nonzero, trees\n+\t * are also pruned by the sparse patterns (as used by backfill).\n \t */\n \tstruct pattern_list *pl;\n+\tint pl_sparse_trees;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 88f86ae0dc..3f2b50a9aa 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -68,7 +68,7 @@ static int emit_block(const char *path, struct oid_array *oids,\n \n int cmd__path_walk(int argc, const char **argv)\n {\n-\tint res, stdin_pl = 0;\n+\tint res, stdin_pl = 0, pl_sparse_trees = -1;\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n@@ -89,6 +89,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_BOOL(0, \"pl-sparse-trees\", &pl_sparse_trees,\n+\t\t\t N_(\"toggle pruning of trees by sparse patterns\")),\n \t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n@@ -116,6 +118,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tif (stdin_pl) {\n \t\tstruct strbuf in = STRBUF_INIT;\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = (pl_sparse_trees >= 0) ?\n+\t\t\tpl_sparse_trees : 1;\n \n \t\tinfo.pl->use_cone_patterns = 1;\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex d9be7b9cd2..1126afaea1 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -206,6 +206,43 @@ test_expect_success 'base & topic, sparse' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'base & topic, sparse, no tree pruning' '\n+\tcat >patterns <<-EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\n+\ttest-tool path-walk --stdin-pl --no-pl-sparse-trees \\\n+\t\t-- base topic <patterns >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:blob:left/b:$(git rev-parse base:left/b)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:3\n+\tcommits:4\n+\ttags:0\n+\ttrees:10\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n-- \ngitgitgadget\n\n"},{"id":"542592","messageId":"859bee39ebe62f832a9779d445717ff703075dac.1777731354.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH 7/7] pack-objects: support sparse:oid filter with path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T14:15:54Z","receivedAt":"2026-05-02T14:16:09Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe --filter=sparse:<oid> option to 'git pack-objects' allows focusing\nan object set to a sparse-checkout definition. This reduces the set of\nmatching blobs while retaining all reachable trees. No server currently\nsupports fetching with this filter because it is expensive to compute\nand reachability bitmaps do not help without a significant effort to\nextend the bitmap feature to store bitmaps for each supported sparse-\ncheckout definition.\n\nWithout focusing on serving fetches and clones with these filters, there\nare still benefits that could be realized by making this faster. With\nthe sparse index, it's more realistic now than ever to be able to\noperate a local clone that was bootstrapped by a packfile created with\na sparse filter, because the missing trees are not needed to move a\nsparse-checkout from one commit to another or to view the history of any\npath in scope. Such clones could perhaps be bootstrapped by partial\nbundles.\n\nPreviously, constructing these sparse packs has been incredibly\ncomputationally inefficient. The revision walk that explores which\nobjects are in scope spends a lot of time checking each object to see if\nit matches the sparse-checkout patterns, causing quadratic behavior\n(number of objects times number of sparse-checkout patterns). This\nimproves somewhat when using cone-mode sparse-checkout patterns that can\nuse hashtables and prefix matches to determine containment. However, the\ncheck per object is still too expensive for most cases.\n\nThis is where the path-walk feature comes in. We can proceed as normal\nby placing objects in bins by path and _then_ check a group of objects\nall at once. Since sparse:<oid> only restricts blobs, the path-walk must\ninclude all reachable trees while using the cone-mode patterns to skip\nblobs at paths outside the sparse scope. This establishes a baseline for\na potential future \"treesparse:<oid>\" filter that would also restrict\ntrees, but introducing such a new filter is deferred to a later change.\n\nThe implementation here is focused around loading the sparse-checkout\npatterns from the provided object ID and checking that the patterns are\nindeed cone-mode patterns. We can then load the correct pattern list\ninto the path walk context and use the logic that already exists from\nbff45557675 (backfill: add --sparse option, 2025-02-03), though that\nfeature loads sparse-checkout patterns from the worktree's local\nsettings and also restricts tree objects. We use a combination of errors\nand warnings to signal problems during this load. The difference is that\nerrors are likely fatal for the non-path-walk version while the warnings\nare probably just implementation details for the path-walk version and\nthe 'git pack-objects' command can fall back to the revision walk\nversion.\n\nNow that the SEEN flag is deferred until after pattern checks (from the\nprevious commit), handle the case where a tree with a shared OID appears\nat both an out-of-cone and in-cone path. When trees are not being pruned\n(pl_sparse_trees == 0), the path-walk re-walks the tree at the in-cone\npath so that in-cone blobs within it are discovered. The new tests in\nt5317 and t6601 demonstrate this behavior and would fail without these\nchanges.\n\nThe performance test p5315 shows the impact of this change when using\nsparse filters:\n\nTest                                              HEAD~1     HEAD\n----------------------------------------------------------------------\n5315.10: repack (sparse:oid)                      77.98    77.47  -0.7%\n5315.11: repack size (sparse:oid)                187.5M   187.4M  -0.0%\n5315.12: repack (sparse:oid, --path-walk)         77.91    31.41 -59.7%\n5315.13: repack size (sparse:oid, --path-walk)   187.5M   161.1M -14.1%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (14% smaller for sparse packs)\nand dramatic time savings (60% faster) by leveraging the path-walk's\nability to skip blobs outside the sparse scope.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c                 |   2 +\n path-walk.c                            |  81 ++++++++++++++-\n t/t5317-pack-objects-filter-objects.sh | 125 +++++++++++++++++++++++\n t/t6601-path-walk.sh                   | 131 +++++++++++++++++++++++++\n 4 files changed, 335 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex bc9fb5b457..ba00d8148a 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4777,6 +4777,8 @@ static void get_object_list_path_walk(struct rev_info *revs)\n \tresult = walk_objects_by_path(&info);\n \ttrace2_region_leave(\"pack-objects\", \"path-walk\", revs->repo);\n \n+\tpath_walk_info_clear(&info);\n+\n \tif (result)\n \t\tdie(_(\"failed to pack objects via path-walk\"));\n }\ndiff --git a/path-walk.c b/path-walk.c\nindex c25392b901..700617ee2f 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"object-name.h\"\n #include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n@@ -180,10 +181,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\treturn -1;\n \t\t}\n \n-\t\t/* Skip this object if already seen. */\n-\t\tif (o->flags & SEEN)\n-\t\t\tcontinue;\n-\n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n \n@@ -194,6 +191,40 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\tif (type == OBJ_TREE)\n \t\t\tstrbuf_addch(&path, '/');\n \n+\t\tif (o->flags & SEEN) {\n+\t\t\t/*\n+\t\t\t * A tree with a shared OID may appear at multiple\n+\t\t\t * paths. Even though we already added this tree to\n+\t\t\t * the output at some other path, we still need to\n+\t\t\t * walk into it at this in-cone path to discover\n+\t\t\t * blobs that were not found at the earlier\n+\t\t\t * out-of-cone path.\n+\t\t\t *\n+\t\t\t * Only do this for paths not yet in our map, to\n+\t\t\t * avoid duplicate entries when the same tree OID\n+\t\t\t * appears at the same path across multiple commits.\n+\t\t\t */\n+\t\t\tif (type == OBJ_TREE && ctx->info->pl &&\n+\t\t\t    ctx->info->pl->use_cone_patterns &&\n+\t\t\t    !ctx->info->pl_sparse_trees &&\n+\t\t\t    !strmap_contains(&ctx->paths_to_lists, path.buf)) {\n+\t\t\t\tint dtype;\n+\t\t\t\tenum pattern_match_result m;\n+\t\t\t\tm = path_matches_pattern_list(path.buf, path.len,\n+\t\t\t\t\t\t\t      path.buf + base_len,\n+\t\t\t\t\t\t\t      &dtype,\n+\t\t\t\t\t\t\t      ctx->info->pl,\n+\t\t\t\t\t\t\t      ctx->repo->index);\n+\t\t\t\tif (m != NOT_MATCHED) {\n+\t\t\t\t\tadd_path_to_list(ctx, path.buf, type,\n+\t\t\t\t\t\t\t &entry.oid,\n+\t\t\t\t\t\t\t !(o->flags & UNINTERESTING));\n+\t\t\t\t\tpush_to_stack(ctx, path.buf);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (ctx->info->pl) {\n \t\t\tint dtype;\n \t\t\tenum pattern_match_result match;\n@@ -533,6 +564,48 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_SPARSE_OID:\n+\t\tif (info) {\n+\t\t\tstruct object_id sparse_oid;\n+\t\t\tstruct repository *repo = info->revs->repo;\n+\n+\t\t\tif (info->pl) {\n+\t\t\t\twarning(_(\"sparse filter cannot be combined with existing sparse patterns\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (repo_get_oid_with_flags(repo,\n+\t\t\t\t\t\t    options->sparse_oid_name,\n+\t\t\t\t\t\t    &sparse_oid,\n+\t\t\t\t\t\t    GET_OID_BLOB)) {\n+\t\t\t\terror(_(\"unable to access sparse blob in '%s'\"),\n+\t\t\t\t      options->sparse_oid_name);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tCALLOC_ARRAY(info->pl, 1);\n+\t\t\tinfo->pl->use_cone_patterns = 1;\n+\n+\t\t\tif (add_patterns_from_blob_to_list(&sparse_oid, \"\", 0,\n+\t\t\t\t\t\t\t   info->pl) < 0) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\terror(_(\"unable to parse sparse filter data in '%s'\"),\n+\t\t\t\t      oid_to_hex(&sparse_oid));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (!info->pl->use_cone_patterns) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh\nindex 501d715b9a..dddb79ba62 100755\n--- a/t/t5317-pack-objects-filter-objects.sh\n+++ b/t/t5317-pack-objects-filter-objects.sh\n@@ -478,4 +478,129 @@ test_expect_success 'verify pack-objects w/ --missing=allow-any' '\n \tEOF\n '\n \n+# Test that --path-walk produces the same object set as standard traversal\n+# when using sparse:oid filters with cone-mode patterns.\n+#\n+# The sparse:oid filter restricts only blobs, not trees. Both standard\n+# and path-walk should produce identical sets of blobs, commits, and trees.\n+\n+test_expect_success 'setup pw_sparse for path-walk comparison' '\n+\tgit init pw_sparse &&\n+\tmkdir -p pw_sparse/inc/sub pw_sparse/exc/sub &&\n+\n+\tfor n in 1 2\n+\tdo\n+\t\techo \"inc $n\" >pw_sparse/inc/file$n &&\n+\t\techo \"inc sub $n\" >pw_sparse/inc/sub/file$n &&\n+\t\techo \"exc $n\" >pw_sparse/exc/file$n &&\n+\t\techo \"exc sub $n\" >pw_sparse/exc/sub/file$n &&\n+\t\techo \"root $n\" >pw_sparse/root$n || return 1\n+\tdone &&\n+\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"first\" &&\n+\n+\techo \"inc 1 modified\" >pw_sparse/inc/file1 &&\n+\techo \"exc 1 modified\" >pw_sparse/exc/file1 &&\n+\techo \"root 1 modified\" >pw_sparse/root1 &&\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"second\" &&\n+\n+\t# Cone-mode sparse pattern: include root + inc/\n+\tprintf \"/*\\n!/*/\\n/inc/\\n\" |\n+\tgit -C pw_sparse hash-object -w --stdin >sparse_oid\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk produces same blobs' '\n+\toid=$(cat sparse_oid) &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >standard.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../standard.pack &&\n+\tgit -C pw_sparse verify-pack -v ../standard.pack >standard_verify &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >pathwalk.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../pathwalk.pack &&\n+\tgit -C pw_sparse verify-pack -v ../pathwalk.pack >pathwalk_verify &&\n+\n+\t# Blobs must match exactly\n+\tgrep -E \"^[0-9a-f]{40} blob\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_blobs &&\n+\ttest_cmp standard_blobs pathwalk_blobs &&\n+\n+\t# Commits must match exactly\n+\tgrep -E \"^[0-9a-f]{40} commit\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_commits &&\n+\tgrep -E \"^[0-9a-f]{40} commit\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_commits &&\n+\ttest_cmp standard_commits pathwalk_commits\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk includes all trees' '\n+\t# The sparse:oid filter restricts only blobs, not trees.\n+\t# Both standard and path-walk should include the same trees.\n+\tgrep -E \"^[0-9a-f]{40} tree\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_trees &&\n+\tgrep -E \"^[0-9a-f]{40} tree\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_trees &&\n+\n+\ttest_cmp standard_trees pathwalk_trees\n+'\n+\n+# Test the edge case where the same tree/blob OID appears at both an\n+# in-cone and out-of-cone path. When sibling directories have identical\n+# contents, they share a tree OID. The path-walk defers marking objects\n+# SEEN until after checking sparse patterns, so an object at an out-of-cone\n+# path can still be discovered at an in-cone path.\n+\n+test_expect_success 'setup pw_shared for shared OID across cone boundary' '\n+\tgit init pw_shared &&\n+\tmkdir pw_shared/aaa pw_shared/zzz &&\n+\techo \"shared content\" >pw_shared/aaa/file &&\n+\techo \"shared content\" >pw_shared/zzz/file &&\n+\techo \"root file\" >pw_shared/rootfile &&\n+\tgit -C pw_shared add . &&\n+\tgit -C pw_shared commit -m \"aaa and zzz share tree OID\" &&\n+\n+\t# Verify they share a tree OID\n+\taaa_tree=$(git -C pw_shared rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git -C pw_shared rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tprintf \"/*\\n!/*/\\n/zzz/\\n\" |\n+\tgit -C pw_shared hash-object -w --stdin >shared_sparse_oid\n+'\n+\n+test_expect_success 'shared tree OID: --path-walk blobs match standard' '\n+\toid=$(cat shared_sparse_oid) &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >shared_std.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_std.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_std.pack >shared_std_verify &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >shared_pw.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_pw.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_pw.pack >shared_pw_verify &&\n+\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_std_verify |\n+\tawk \"{print \\$1}\" | sort >shared_std_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_pw_verify |\n+\tawk \"{print \\$1}\" | sort >shared_pw_blobs &&\n+\ttest_cmp shared_std_blobs shared_pw_blobs\n+'\n+\n test_done\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 1126afaea1..520269dfc6 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -590,4 +590,135 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'setup sparse filter blob' '\n+\t# Cone-mode patterns: include root, exclude all dirs, include left/\n+\tcat >patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\tsparse_oid=$(git hash-object -w -t blob patterns)\n+'\n+\n+test_expect_success 'all, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:blob:left/b:$(git rev-parse base:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+# Demonstrate the SEEN flag ordering issue: when the same tree/blob OID\n+# appears at two sibling paths where one is in-cone and the other is\n+# out-of-cone, the path-walk must still discover blobs at the in-cone\n+# path even when the shared tree OID was first encountered out-of-cone.\n+# Since sparse:oid includes all trees, the out-of-cone tree (aaa/) is\n+# walked first, and its blob is skipped. The path-walk then re-walks\n+# the same tree OID at the in-cone path (zzz/) to find the blob there.\n+\n+test_expect_success 'setup shared tree OID across cone boundary' '\n+\tgit checkout --orphan shared-tree &&\n+\tgit rm -rf . &&\n+\tmkdir aaa zzz &&\n+\techo \"shared content\" >aaa/file &&\n+\techo \"shared content\" >zzz/file &&\n+\techo \"root file\" >rootfile &&\n+\tgit add aaa zzz rootfile &&\n+\tgit commit -m \"aaa and zzz have same tree OID\" &&\n+\n+\t# Verify they really share a tree OID\n+\taaa_tree=$(git rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tcat >shared-patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/zzz/\n+\tEOF\n+\tshared_sparse_oid=$(git hash-object -w -t blob shared-patterns)\n+'\n+\n+test_expect_success 'sparse:oid with shared tree OID across cone boundary' '\n+\ttest-tool path-walk \\\n+\t\t--filter=sparse:oid=$shared_sparse_oid \\\n+\t\t-- shared-tree >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse shared-tree)\n+\t1:tree::$(git rev-parse shared-tree^{tree})\n+\t2:blob:rootfile:$(git rev-parse shared-tree:rootfile)\n+\t3:tree:aaa/:$(git rev-parse shared-tree:aaa)\n+\t4:tree:zzz/:$(git rev-parse shared-tree:zzz)\n+\t5:blob:zzz/file:$(git rev-parse shared-tree:zzz/file)\n+\tblobs:2\n+\tcommits:1\n+\ttags:0\n+\ttrees:3\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"542628","messageId":"xmqqbjewrtek.fsf@gitster.g","threadId":"65575","inReplyTo":"22db528d0c5e6db71664f071dca97ab218f4ba45.1777731354.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 4/7] backfill: die on incompatible filter options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-03T22:59:47Z","receivedAt":"2026-05-03T22:59:49Z","isPatch":true,"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> The 'git backfill' command uses the path-walk API in a critical way: it\n> uses the objects output from the command to find the batches of missing\n> objects that should be requested from the server. Unlike 'git\n> pack-objects', we cannot fall back to another mechanism.\n>\n> The previous change added the path_walk_filter_compatible() method that\n> we can reuse here. Use it during argument validation in cmd_backfill().\n>\n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  builtin/backfill.c  | 2 ++\n>  t/t5620-backfill.sh | 8 ++++++++\n>  2 files changed, 10 insertions(+)\n\nAnother topic adds a helper function to check for many incompatible\noptions and calls it from here.  When I merged this topic, I made an\nsemi-evil merge to move this call to that function (with necessary\nadjustment to the parameter).  Please sanity check the resolution I\nmade in 'seen'.  Thanks.\n\n\n> diff --git a/builtin/backfill.c b/builtin/backfill.c\n> index d794dd842f..51eaa42169 100644\n> --- a/builtin/backfill.c\n> +++ b/builtin/backfill.c\n> @@ -144,6 +144,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n>  \n>  \tif (argc > 1)\n>  \t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n> +\tif (!path_walk_filter_compatible(&ctx.revs.filter))\n> +\t\tdie(_(\"cannot backfill with these filter options\"));\n>  \n>  \trepo_config(repo, git_default_config, NULL);\n>  \n> diff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\n> index f3b5e39493..3580e10b9c 100755\n> --- a/t/t5620-backfill.sh\n> +++ b/t/t5620-backfill.sh\n> @@ -15,6 +15,14 @@ test_expect_success 'backfill rejects unexpected arguments' '\n>  \ttest_grep \"unrecognized argument: --unexpected-arg\" err\n>  '\n>  \n> +test_expect_success 'backfill rejects incompatible filter options' '\n> +\ttest_must_fail git backfill --objects --filter=tree:1 2>err &&\n> +\ttest_grep \"cannot backfill with these filter options\" err &&\n> +\n> +\ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n> +\ttest_grep \"cannot backfill with these filter options\" err\n> +'\n> +\n>  # We create objects in the 'src' repo.\n>  test_expect_success 'setup repo for object creation' '\n>  \techo \"{print \\$1}\" >print_1.awk &&\n"},{"id":"542638","messageId":"xmqqo6iwq9qs.fsf@gitster.g","threadId":"65575","inReplyTo":"29d2797440412bfec85be7b6a6439350b9ebf5e8.1777731354.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/7] pack-objects: pass --objects with --path-walk","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-04T00:49:47Z","receivedAt":"2026-05-04T00:49:50Z","isPatch":true,"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> When 'git pack-objects' has the --path-walk option enabled, it uses a\n> different set of revision walk parameters than normal. For once,\n\n\"once\" -> \"one\" (or \"instance\")?\n\n> --objects was previously assumed by the path-walk API and was not needed\n> to be added. We also needed --boundary to allow discovering\n> UNINTERESTING objects to use as delta bases.\n>\n> We will be updating the path-walk API soon to work with some filter\n> options. However, the revision machinery will trigger a fatal error:\n>\n>   fatal: object filtering requires --objects\n>\n> The fix is easy: add the --objects option as an argument. This has no\n> effect on the path-walk API but does simplify the revision option\n> parsing for the objects filter.\n>\n> We can remove the comment about \"removing\" the options because they were\n> never removed and instead not added. We still need to disable using\n> bitmaps.\n\nIn the old code, there was a valid reason why bitmaps were not used\n(i.e., \"--objects\" not enabled), but that no longer holds (i.e., now\nwe add \"--objects\" ourselves).  Do we need to give an updated\nrationale to keep bitmap disabled?\n\n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  builtin/pack-objects.c | 5 +----\n>  1 file changed, 1 insertion(+), 4 deletions(-)\n>\n> diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\n> index dd2480a73d..4338962904 100644\n> --- a/builtin/pack-objects.c\n> +++ b/builtin/pack-objects.c\n> @@ -5190,10 +5190,7 @@ int cmd_pack_objects(int argc,\n>  \t}\n>  \tif (path_walk) {\n>  \t\tstrvec_push(&rp, \"--boundary\");\n> -\t\t /*\n> -\t\t  * We must disable the bitmaps because we are removing\n> -\t\t  * the --objects / --objects-edge[-aggressive] options.\n> -\t\t  */\n> +\t\tstrvec_push(&rp, \"--objects\");\n>  \t\tuse_bitmap_index = 0;\n>  \t} else if (thin) {\n>  \t\tuse_internal_rev_list = 1;\n"},{"id":"542661","messageId":"79609516-ea40-447f-a1e8-5ee71d309208@gmail.com","threadId":"65575","inReplyTo":"xmqqo6iwq9qs.fsf@gitster.g","subject":"Re: [PATCH 1/7] pack-objects: pass --objects with --path-walk","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-04T12:01:02Z","receivedAt":"2026-05-04T12:01:05Z","isPatch":true,"body":"On 5/3/2026 8:49 PM, Junio C Hamano wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> From: Derrick Stolee <stolee@gmail.com>\n>>\n>> When 'git pack-objects' has the --path-walk option enabled, it uses a\n>> different set of revision walk parameters than normal. For once,\n> \n> \"once\" -> \"one\" (or \"instance\")?\n\nYes, \"one\". Sorry for the typo.\n\n>> --objects was previously assumed by the path-walk API and was not needed\n>> to be added. We also needed --boundary to allow discovering\n>> UNINTERESTING objects to use as delta bases.\n>>\n>> We will be updating the path-walk API soon to work with some filter\n>> options. However, the revision machinery will trigger a fatal error:\n>>\n>>   fatal: object filtering requires --objects\n>>\n>> The fix is easy: add the --objects option as an argument. This has no\n>> effect on the path-walk API but does simplify the revision option\n>> parsing for the objects filter.\n>>\n>> We can remove the comment about \"removing\" the options because they were\n>> never removed and instead not added. We still need to disable using\n>> bitmaps.\n> \n> In the old code, there was a valid reason why bitmaps were not used\n> (i.e., \"--objects\" not enabled), but that no longer holds (i.e., now\n> we add \"--objects\" ourselves).  Do we need to give an updated\n> rationale to keep bitmap disabled?\n\n>>  \tif (path_walk) {\n>>  \t\tstrvec_push(&rp, \"--boundary\");\n>> -\t\t /*\n>> -\t\t  * We must disable the bitmaps because we are removing\n>> -\t\t  * the --objects / --objects-edge[-aggressive] options.\n>> -\t\t  */\n>> +\t\tstrvec_push(&rp, \"--objects\");\n>>  \t\tuse_bitmap_index = 0;\n>>  \t} else if (thin) {\nThis old comment is perhaps confusing things. The important thing here\nis to disable bitmaps with 'use_bitmap_index = 0;' (though perhaps not\nfor long [1]).\n\n[1] https://lore.kernel.org/git/f50f8df01a9f216d5b4388b2fe4ff58077b574f3.1777853408.git.me@ttaylorr.com/\n\nThe path-walk API itself disables the objects walk for the revision\nmachinery in walk_objects_by_path():\n\n\tinfo->revs->blob_objects = info->revs->tree_objects = 0;\n\nThis allows the path-walk API to rely on the revision walk for a\n_commits only_ walk and then have the path-walk API handle the trees\nand blobs.\n\nThe reason we need to add \"--objects\" now is to allow for parsing the\n\"--filter\" option without the revision logic complaining.\n\nThanks,\n-Stolee\n"},{"id":"542662","messageId":"eb8b4a7b-e241-4249-bae1-8f631bf7bd2d@gmail.com","threadId":"65575","inReplyTo":"xmqqbjewrtek.fsf@gitster.g","subject":"Re: [PATCH 4/7] backfill: die on incompatible filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-04T12:09:52Z","receivedAt":"2026-05-04T12:09:54Z","isPatch":true,"body":"On 5/3/2026 6:59 PM, Junio C Hamano wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> From: Derrick Stolee <stolee@gmail.com>\n>>\n>> The 'git backfill' command uses the path-walk API in a critical way: it\n>> uses the objects output from the command to find the batches of missing\n>> objects that should be requested from the server. Unlike 'git\n>> pack-objects', we cannot fall back to another mechanism.\n>>\n>> The previous change added the path_walk_filter_compatible() method that\n>> we can reuse here. Use it during argument validation in cmd_backfill().\n>>\n>> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n>> ---\n>>  builtin/backfill.c  | 2 ++\n>>  t/t5620-backfill.sh | 8 ++++++++\n>>  2 files changed, 10 insertions(+)\n> \n> Another topic adds a helper function to check for many incompatible\n> options and calls it from here.  When I merged this topic, I made an\n> semi-evil merge to move this call to that function (with necessary\n> adjustment to the parameter).  Please sanity check the resolution I\n> made in 'seen'.  Thanks.\nThanks for alerting me about this. I do think there is an error in\nyour merge. Hopefully the tests I wrote in this series caught the\nmistake.\n\nHere are the last lines in your copy of\nreject_unsupported_rev_list_options():\n\n\tif (revs->filter.choice)\n\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n\t\t    \"--filter\");\n\tif (!path_walk_filter_compatible(&revs->filter))\n\t\tdie(_(\"cannot backfill with these filter options\"));\n\tif (revs->filter.blob_limit_value)\n\t\tdie(_(\"cannot backfill with blob size limits\"));\n\nThe last two options are correct, but they can't do anything\nbecause the first one causes the command to fail immediately.\n\nThis should have caused failures in t5620-backfill.sh, specifically\nthe test 'backfill rejects incompatible filter options'. Indeed, I\nget this error output when running on that commit:\n\n+ test_grep cannot backfill with these filter options err\n+ eval last_arg=${2}\n+ last_arg=err\n+ test -f err\n+ test 2 -lt 2\n+ test x! = xcannot backfill with these filter options\n+ test x! = xcannot backfill with these filter options\n+ grep cannot backfill with these filter options err\n+ echo error: 'grep cannot backfill with these filter options err' didn't find a match in:\nerror: 'grep cannot backfill with these filter options err' didn't find a match in:\n+ test -s err\n+ cat err\nfatal: '--filter' cannot be used with 'git backfill'\n+ return 1\n\nThis does make it clear that I should add a new test in t5620 that\ntests the 'sparse:<oid>' filter now that it is compatible, which I\nmissed in v1.\n\nThanks,\n-Stolee\n\n"},{"id":"542718","messageId":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.git.1777731354.gitgitgadget@gmail.com","subject":"[PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:09Z","receivedAt":"2026-05-04T20:21:22Z","isPatch":true,"body":"NOTE: This series is based on en/backfill-fixes-and-edges.\n\nThe 'git pack-objects' command has a '--path-walk' option that uses the\npath-walk API instead of a typical revision walk to group objects into\nchunks by path name instead of relying solely on name-hashes to group\nsimilar files together. (It also does a second compression pass looking for\nbetter deltas after the first pass that is focused within chunks per path.)\n\nThe '--path-walk' feature was not previously integrated with the '--filter'\nfeature, so a warning would appear and disable the path-walk API when a\nfilter is given. This patch series integrates these together in the\nfollowing ways:\n\n * --filter=blob:none updates the path-walk API options to skip blobs.\n * --filter=blob:limit=<size> adds a scan to a list of blob objects to\n   remove objects that are too large.\n * --filter=sparse:<oid> adds a scan to the chunks to validate that the\n   paths match the sparse-checkout patterns.\n\nIn particular, this last check is significantly faster than the previous\nalgorithm because it can check all objects at a given path simultaneously\ninstead of checking all sparse-checkout patterns for each discovered blob\nobject.\n\nA subtlety must be added here, in that we must change how we mark an object\nas \"seen\" during the path-walk. We may need to add an object to multiple\npaths and only mark it as \"seen\" if it indeed matched the sparse-checkout\npatterns as the path is accepted for emitting to the callback. This adds a\nnew filter that the \"seen\" objects must also be removed from later chunks to\navoid sending the same object as grouped to multiple chunks.\n\nThere's also a subtle detail here in that the path-walk API also prunes tree\npaths based on cone-mode sparse-checkouts, to enable 'git backfill --sparse'\noperating quickly for small sparse-checkout scopes. But the\n--filter=sparse:<oid> feature doesn't prune trees!\n\nAs a future step, I do plan to recommend that we add a treesparse:<oid>\nsetting that does allow us to trim the tree set by cone-mode sparse\npatterns. At the time that partial clone filters were being created, cone\nmode sparse-checkout didn't exist and neither did the sparse index. Those\nfeatures together make a smaller tree set possible, assuming the user never\nneeds to change their scope. This would be a significant change so it is not\nimplemented here, though the git pack-objects integration would be quick\nafter this series completes.\n\nNeither the sparse:<oid> or hypothetical treesparse:<oid> options are or\nshould necessarily be supported by Git servers. It's too expensive to\ncompute dynamically and it doesn't work well with reachability bitmaps. What\nbecomes possible with this change is that it becomes reasonably fast to\nconstruct bundles with these filters that can bootstrap a working\nenvironment with the full history of all files within a given\nsparse-checkout scope.\n\n\nPerformance Results\n===================\n\nSince the '--path-walk' option is ignored in today's Git version when a\n'--filter' is added, the performance matches the behavior without\n'--path-walk'. For the tables below, you can compare the rows against each\nother (time and then packfile size) for the mode without and then with\n'--path-walk' as a representation of \"before\" and \"after\". (These tables are\nrepeated in the commit messages as new implementations improve specific\nrows.)\n\nI chose a number of open source repositories of various sizes and shapes:\n\n\ngit/git\n=======\n\nTest                                              HEAD\n-------------------------------------------------------------------\n5315.2: repack (no filter)                       27.73\n5315.3: repack size (no filter)                 250.6M\n5315.4: repack (no filter, --path-walk)          35.19\n5315.5: repack size (no filter, --path-walk)    220.1M\n5315.6: repack (blob:none)                       13.42\n5315.7: repack size (blob:none)                 137.6M\n5315.8: repack (blob:none, --path-walk)          20.98\n5315.9: repack size (blob:none, --path-walk)    115.2M\n5315.10: repack (sparse:oid)                     72.53\n5315.11: repack size (sparse:oid)               187.5M\n5315.12: repack (sparse:oid, --path-walk)        29.00\n5315.13: repack size (sparse:oid, --path-walk)  161.0M\n\n\n\nnodejs/node\n===========\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                       75.53\n5315.3: repack size (no filter)                   0.9G\n5315.4: repack (no filter, --path-walk)          80.54\n5315.5: repack size (no filter, --path-walk)    885.7M\n5315.6: repack (blob:none)                       12.65\n5315.7: repack size (blob:none)                 148.6M\n5315.8: repack (blob:none, --path-walk)          17.60\n5315.9: repack size (blob:none, --path-walk)    134.6M\n5315.10: repack (sparse:oid)                    518.84\n5315.11: repack size (sparse:oid)               153.4M\n5315.12: repack (sparse:oid, --path-walk)        27.99\n5315.13: repack size (sparse:oid, --path-walk)  139.4M\n\n\n\nmicrosoft/fluentui\n==================\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                      146.77\n5315.3: repack size (no filter)                 562.1M\n5315.4: repack (no filter, --path-walk)          72.82\n5315.5: repack size (no filter, --path-walk)    172.6M\n5315.6: repack (blob:none)                        4.84\n5315.7: repack size (blob:none)                  62.7M\n5315.8: repack (blob:none, --path-walk)           5.19\n5315.9: repack size (blob:none, --path-walk)     59.9M\n5315.10: repack (sparse:oid)                     59.95\n5315.11: repack size (sparse:oid)                85.6M\n5315.12: repack (sparse:oid, --path-walk)        15.16\n5315.13: repack size (sparse:oid, --path-walk)   72.5M\n\n\n\nmicrosoftdocs/azure-devops-docs\n===============================\n\nTest                                               HEAD\n---------------------------------------------------------------------\n5315.2: repack (no filter)                        4.41\n5315.3: repack size (no filter)                   1.6G\n5315.4: repack (no filter, --path-walk)           6.00\n5315.5: repack size (no filter, --path-walk)      1.6G\n5315.6: repack (blob:none)                        1.35\n5315.7: repack size (blob:none)                  60.0M\n5315.8: repack (blob:none, --path-walk)           1.23\n5315.9: repack size (blob:none, --path-walk)     60.0M\n5315.10: repack (sparse:oid)                    138.24\n5315.11: repack size (sparse:oid)                84.4M\n5315.12: repack (sparse:oid, --path-walk)         1.86\n5315.13: repack size (sparse:oid, --path-walk)   84.4M\n\n\n\nPerformance conclusions\n=======================\n\nAs seen in earlier series around the '--path-walk' feature, the space\nsavings can be valuable but is not always guaranteed. When the space savings\ndoesn't happen, then the time spent is generally slower because of the\ntwo-pass mechanism. The microsoftdocs/azure-devops-docs repo demonstrates\nthis case quite clearly.\n\nHowever, even in these cases the 'sparse:<oid>' filters are much faster\nbecause of the ability to check an entire set of objects against the\nsparse-checkout patterns only once.\n\nThanks, -Stolee\n\n\nUPDATES IN V2\n=============\n\n * Rebased onto en/backfill-fixes-and-edges to properly integrate with the\n   incompatible rev-list options logic in that series.\n * Updated documentation as behavior changes. Credit to Taylor Blau for\n   presenting these suggestions in his RFC [2].\n * Added three patches of Taylor's to extend more filter options.\n\nP.S. I've CC'd the folks who were on the original path-walk feature thread\n[1]\n\n[1]\nhttps://lore.kernel.org/git/pull.1819.git.1741571455.gitgitgadget@gmail.com/\n\n[2] https://lore.kernel.org/git/cover.1777853408.git.me@ttaylorr.com/\n\nDerrick Stolee (7):\n  pack-objects: pass --objects with --path-walk\n  t/perf: add pack-objects filter and path-walk benchmark\n  path-walk: support blobless filter\n  backfill: die on incompatible filter options\n  path-walk: support blob size limit filter\n  path-walk: add pl_sparse_trees to control tree pruning\n  pack-objects: support sparse:oid filter with path-walk\n\nTaylor Blau (3):\n  path-walk: support `tree:0` filter\n  path-walk: support `object:type` filter\n  path-walk: support `combine` filter\n\n Documentation/git-backfill.adoc        |   4 +\n Documentation/git-pack-objects.adoc    |   8 +-\n builtin/backfill.c                     |   8 +-\n builtin/pack-objects.c                 |   9 +-\n path-walk.c                            | 201 +++++++++-\n path-walk.h                            |  21 ++\n t/helper/test-path-walk.c              |  17 +-\n t/perf/p5315-pack-objects-filter.sh    | 129 +++++++\n t/t5317-pack-objects-filter-objects.sh | 125 ++++++\n t/t5620-backfill.sh                    |   8 +\n t/t6601-path-walk.sh                   | 502 +++++++++++++++++++++++++\n 11 files changed, 1007 insertions(+), 25 deletions(-)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\n\nbase-commit: a1ad4a0fca14cdeb55ab9fb065551b15cafa8a4f\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2101%2Fderrickstolee%2Fpath-walk-filters-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2101/derrickstolee/path-walk-filters-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2101\n\nRange-diff vs v1:\n\n  1:  29d2797440 =  1:  b361680338 pack-objects: pass --objects with --path-walk\n  2:  f364621815 =  2:  9ea81aa726 t/perf: add pack-objects filter and path-walk benchmark\n  3:  b7e72d5dbb !  3:  ed4d277a2c path-walk: support blobless filter\n     @@ Commit message\n          passes. This data demonstrates that the feature is engaged and provides\n          real compression benefits when --no-reuse-delta forces fresh deltas.\n      \n     +    Co-Authored-by: Taylor Blau <me@ttaylorr.com>\n     +    Signed-off-by: Taylor Blau <me@ttaylorr.com>\n          Signed-off-by: Derrick Stolee <stolee@gmail.com>\n      \n     + ## Documentation/git-pack-objects.adoc ##\n     +@@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n     + \tof filenames that cause collisions in Git's default name-hash\n     + \talgorithm.\n     + +\n     +-Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n     +-`--use-bitmap-index` option will be ignored in the presence of\n     +-`--path-walk.`\n     ++Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     ++ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     ++supports the `--filter=<spec>` form `blob:none`.\n     + \n     + \n     + DELTA ISLANDS\n     +\n       ## builtin/pack-objects.c ##\n      @@ builtin/pack-objects.c: int cmd_pack_objects(int argc,\n       \n  4:  22db528d0c !  4:  da4c19af42 backfill: die on incompatible filter options\n     @@ Commit message\n          Signed-off-by: Derrick Stolee <stolee@gmail.com>\n      \n       ## builtin/backfill.c ##\n     -@@ builtin/backfill.c: int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n     - \n     - \tif (argc > 1)\n     - \t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n     -+\tif (!path_walk_filter_compatible(&ctx.revs.filter))\n     +@@ builtin/backfill.c: static void reject_unsupported_rev_list_options(struct rev_info *revs)\n     + \tif (revs->explicit_diff_merges)\n     + \t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n     + \t\t    \"--diff-merges\");\n     +-\tif (revs->filter.choice)\n     +-\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n     +-\t\t    \"--filter\");\n     ++\tif (!path_walk_filter_compatible(&revs->filter))\n      +\t\tdie(_(\"cannot backfill with these filter options\"));\n     + }\n       \n     - \trepo_config(repo, git_default_config, NULL);\n     - \n     + static int do_backfill(struct backfill_context *ctx)\n      \n       ## t/t5620-backfill.sh ##\n      @@ t/t5620-backfill.sh: test_expect_success 'backfill rejects unexpected arguments' '\n  5:  2e786160bd !  5:  d309345fec path-walk: support blob size limit filter\n     @@ Commit message\n          (which exercises partial filtering within a batch where some blobs are\n          kept and others are excluded).\n      \n     +    Co-authored-by: Taylor Blau <me@ttaylorr.com>\n     +    Signed-off-by: Taylor Blau <me@ttaylorr.com>\n          Signed-off-by: Derrick Stolee <stolee@gmail.com>\n      \n     + ## Documentation/git-pack-objects.adoc ##\n     +@@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n     + +\n     + Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     + ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     +-supports the `--filter=<spec>` form `blob:none`.\n     ++supports the `--filter=<spec>` form `blob:none` and `blob:limit=<n>`.\n     + \n     + \n     + DELTA ISLANDS\n     +\n       ## builtin/backfill.c ##\n     -@@ builtin/backfill.c: int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n     - \t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n     - \tif (!path_walk_filter_compatible(&ctx.revs.filter))\n     +@@ builtin/backfill.c: static void reject_unsupported_rev_list_options(struct rev_info *revs)\n     + \t\t    \"--diff-merges\");\n     + \tif (!path_walk_filter_compatible(&revs->filter))\n       \t\tdie(_(\"cannot backfill with these filter options\"));\n     -+\tif (ctx.revs.filter.blob_limit_value)\n     ++\tif (revs->filter.blob_limit_value)\n      +\t\tdie(_(\"cannot backfill with blob size limits\"));\n     + }\n       \n     - \trepo_config(repo, git_default_config, NULL);\n     - \n     + static int do_backfill(struct backfill_context *ctx)\n      \n       ## path-walk.c ##\n      @@\n  6:  c5aca53b23 =  6:  55ed02772b path-walk: add pl_sparse_trees to control tree pruning\n  7:  859bee39eb !  7:  dcb1a29d3a pack-objects: support sparse:oid filter with path-walk\n     @@ Commit message\n          and dramatic time savings (60% faster) by leveraging the path-walk's\n          ability to skip blobs outside the sparse scope.\n      \n     +    Co-authored-by: Taylor Blau <me@ttaylorr.com>\n     +    Signed-off-by: Taylor Blaue <me@ttaylorr.com>\n          Signed-off-by: Derrick Stolee <stolee@gmail.com>\n      \n     + ## Documentation/git-backfill.adoc ##\n     +@@ Documentation/git-backfill.adoc: OPTIONS\n     + +\n     + You may also use commit-limiting options understood by\n     + linkgit:git-rev-list[1] such as `--first-parent`, `--since`, or pathspecs.\n     +++\n     ++Most `--filter=<spec>` options don't work with the purpose of\n     ++`git backfill`, but the `sparse:<oid>` filter is integrated to provide a\n     ++focused set of paths to download, distinct from the `--sparse` option.\n     + \n     + SEE ALSO\n     + --------\n     +\n     + ## Documentation/git-pack-objects.adoc ##\n     +@@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n     + +\n     + Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     + ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     +-supports the `--filter=<spec>` form `blob:none` and `blob:limit=<n>`.\n     ++supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`, and\n     ++`sparse:<oid>`.\n     + \n     + \n     + DELTA ISLANDS\n     +\n       ## builtin/pack-objects.c ##\n      @@ builtin/pack-objects.c: static void get_object_list_path_walk(struct rev_info *revs)\n       \tresult = walk_objects_by_path(&info);\n  -:  ---------- >  8:  772cdcaeed path-walk: support `tree:0` filter\n  -:  ---------- >  9:  c557f3c59d path-walk: support `object:type` filter\n  -:  ---------- > 10:  5423273edf path-walk: support `combine` filter\n\n-- \ngitgitgadget\n"},{"id":"542719","messageId":"b3616803382f6dedd12137a0cec9695e3d5474ea.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 01/10] pack-objects: pass --objects with --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:10Z","receivedAt":"2026-05-04T20:21:24Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen 'git pack-objects' has the --path-walk option enabled, it uses a\ndifferent set of revision walk parameters than normal. For once,\n--objects was previously assumed by the path-walk API and was not needed\nto be added. We also needed --boundary to allow discovering\nUNINTERESTING objects to use as delta bases.\n\nWe will be updating the path-walk API soon to work with some filter\noptions. However, the revision machinery will trigger a fatal error:\n\n  fatal: object filtering requires --objects\n\nThe fix is easy: add the --objects option as an argument. This has no\neffect on the path-walk API but does simplify the revision option\nparsing for the objects filter.\n\nWe can remove the comment about \"removing\" the options because they were\nnever removed and instead not added. We still need to disable using\nbitmaps.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex dd2480a73d..4338962904 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5190,10 +5190,7 @@ int cmd_pack_objects(int argc,\n \t}\n \tif (path_walk) {\n \t\tstrvec_push(&rp, \"--boundary\");\n-\t\t /*\n-\t\t  * We must disable the bitmaps because we are removing\n-\t\t  * the --objects / --objects-edge[-aggressive] options.\n-\t\t  */\n+\t\tstrvec_push(&rp, \"--objects\");\n \t\tuse_bitmap_index = 0;\n \t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n-- \ngitgitgadget\n\n"},{"id":"542720","messageId":"9ea81aa7269c6fac61723ff46323549f35f5d06d.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 02/10] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:11Z","receivedAt":"2026-05-04T20:21:25Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd p5315-pack-objects-filter.sh to measure the performance of\n'git pack-objects --revs --all' under different filter and traversal\ncombinations:\n\n * no filter (baseline)\n * --filter=blob:none (blobless)\n * --filter=sparse:oid=<oid> (cone-mode sparse)\n\nEach filter scenario is tested both with and without --path-walk,\nproducing paired measurements that show the impact of the path-walk\ntraversal for each filter type as we integrate the --path-walk feature\nwith different --filter options. It currently has no integration so\nfalls back to the standard revision walk. Thus, there are no significant\ndifferences in the current results other than a full repack (and even\nthen, the --path-walk feature is not incredibly different for the\ndefault Git repository):\n\nTest                                             HEAD\n-----------------------------------------------------\n5315.2: repack (no filter)                      27.91\n5315.3: repack size (no filter)                250.7M\n5315.4: repack (no filter, --path-walk)         34.92\n5315.5: repack size (no filter, --path-walk)   220.0M\n5315.6: repack (blob:none)                      13.63\n5315.7: repack size (blob:none)                137.6M\n5315.8: repack (blob:none, --path-walk)         13.48\n5315.9: repack size (blob:none, --path-walk)   137.7M\n5315.10: repack (sparse:oid)                    72.67\n5315.11: repack size (sparse:oid)              187.4M\n5315.12: repack (sparse:oid, --path-walk)       72.47\n5315.13: repack size (sparse:oid, --path-walk) 187.4M\n\nThe sparse filter definition is built automatically by sampling\ndepth-2 directories from the test repository, making the test work\non any repo passed via GIT_PERF_LARGE_REPO. For repos that lack\ndepth-2 directories, a single top-level directory is used; for flat\nrepos, the sparse tests are skipped via prerequisite.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5315-pack-objects-filter.sh | 129 ++++++++++++++++++++++++++++\n 1 file changed, 129 insertions(+)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\ndiff --git a/t/perf/p5315-pack-objects-filter.sh b/t/perf/p5315-pack-objects-filter.sh\nnew file mode 100755\nindex 0000000000..b009039c89\n--- /dev/null\n+++ b/t/perf/p5315-pack-objects-filter.sh\n@@ -0,0 +1,129 @@\n+#!/bin/sh\n+\n+test_description='Tests pack-objects performance with filters and --path-walk'\n+. ./perf-lib.sh\n+\n+test_perf_large_repo\n+\n+test_expect_success 'setup filter inputs' '\n+\t# Sample a few depth-2 directories from the test repo to build\n+\t# a cone-mode sparse-checkout definition.  The sampling picks\n+\t# directories at evenly-spaced positions so the choice is stable\n+\t# and scales to repos of any shape.\n+\n+\tgit ls-tree -d --name-only HEAD >top-dirs &&\n+\ttop_nr=$(wc -l <top-dirs) &&\n+\n+\t>depth2-dirs &&\n+\twhile read tdir\n+\tdo\n+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null |\n+\t\tsed \"s|^|$tdir/|\" >>depth2-dirs || return 1\n+\tdone <top-dirs &&\n+\n+\td2_nr=$(wc -l <depth2-dirs) &&\n+\n+\tif test \"$d2_nr\" -ge 2\n+\tthen\n+\t\t# Pick two directories from evenly-spaced positions.\n+\t\tfirst=$(sed -n \"1p\" depth2-dirs) &&\n+\t\tmid=$(sed -n \"$((d2_nr / 2 + 1))p\" depth2-dirs) &&\n+\n+\t\tp1=$(dirname \"$first\") &&\n+\t\tp2=$(dirname \"$mid\") &&\n+\n+\t\t# Build cone-mode sparse-checkout patterns.\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$p1/\" &&\n+\t\t\techo \"!/$p1/*/\" &&\n+\t\t\tif test \"$p1\" != \"$p2\"\n+\t\t\tthen\n+\t\t\t\techo \"/$p2/\" &&\n+\t\t\t\techo \"!/$p2/*/\"\n+\t\t\tfi &&\n+\t\t\techo \"/$first/\" &&\n+\t\t\tif test \"$first\" != \"$mid\"\n+\t\t\tthen\n+\t\t\t\techo \"/$mid/\"\n+\t\t\tfi\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first $mid\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\telif test \"$top_nr\" -ge 1\n+\tthen\n+\t\t# Fallback: use a single top-level directory.\n+\t\tfirst=$(sed -n \"1p\" top-dirs) &&\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$first/\"\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\tfi\n+'\n+\n+test_perf 'repack (no filter)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (no filter, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"542721","messageId":"ed4d277a2c7dff0566acaf7b902e7474214fbab4.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 03/10] path-walk: support blobless filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:12Z","receivedAt":"2026-05-04T20:21:26Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git pack-objects' command can opt-in to using the path-walk API for\nscanning the objects. Currently, this option is dynamically disabled if\ncombined with '--filter=<X>', even when using a simple filter such as\n'blob:none' to signal a blobless packfile. This is a common scenario for\nrepos at scale, so is worth integrating.\n\nAlso, users can opt-in to the '--path-walk' option by default through\nthe pack.usePathWalk=true config option. When using that in a blobless\npartial clone, the following warning can appear even though the user did\nnot specify either option directly:\n\n  warning: cannot use --filter with --path-walk\n\nTeach the path-walk API to handle the 'blob:none' object filter\nnatively. When revs->filter.choice is LOFC_BLOB_NONE, the path-walk\nsets info->blobs to 0 (skipping all blob objects) and clears the\nfilter from revs so that prepare_revision_walk() does not reject the\nconfiguration.\n\nThis check is implemented in the static prepare_filters() method, which\nwill simultaneously check if the input filters are compatible and will\nmake the appropriate mutations to the path_walk_info and filters if the\npath_walk_info is non-NULL. This allows us to use this logic both in the\nAPI method path_walk_filter_compatible() for use in\nbuiltin/pack-objects.c and as a prep step in walk_objects_by_path().\n\nUpdate the test helper (test-path-walk) to accept --filter=<spec>\nas a test-tool option (before '--'), applying it to revs after\nsetup_revisions() to avoid the --objects requirement check.\n\nAlso switch test-path-walk from REV_INFO_INIT with manual repo\nassignment to repo_init_revisions(), which properly initializes\nthe filter_spec strbuf needed for filter parsing.\n\nAdd tests for blob:none with --all and with a single branch.\n\nThe performance test p5315 shows the impact of this change when using\nblobless filters:\n\nTest                                           HEAD~1     HEAD\n---------------------------------------------------------------------\n5315.6: repack (blob:none)                      13.53   13.87  +2.5%\n5315.7: repack size (blob:none)                137.7M  137.8M  +0.1%\n5315.8: repack (blob:none, --path-walk)         13.51   23.43 +73.4%\n5315.9: repack size (blob:none, --path-walk)   137.7M  115.2M -16.3%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (16% smaller for blobless packs)\nat the cost of increased computation time due to the two compression\npasses. This data demonstrates that the feature is engaged and provides\nreal compression benefits when --no-reuse-delta forces fresh deltas.\n\nCo-Authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  6 +--\n builtin/pack-objects.c              |  2 +-\n path-walk.c                         | 30 +++++++++++++++\n path-walk.h                         |  7 ++++\n t/helper/test-path-walk.c           | 11 +++++-\n t/t6601-path-walk.sh                | 60 +++++++++++++++++++++++++++++\n 6 files changed, 111 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex b78175fbe1..917045d5c3 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -402,9 +402,9 @@ will be automatically changed to version `1`.\n \tof filenames that cause collisions in Git's default name-hash\n \talgorithm.\n +\n-Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n-`--use-bitmap-index` option will be ignored in the presence of\n-`--path-walk.`\n+Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n+ignored in the presence of `--path-walk`. Whe `--path-walk` option\n+supports the `--filter=<spec>` form `blob:none`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 4338962904..bc9fb5b457 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5177,7 +5177,7 @@ int cmd_pack_objects(int argc,\n \n \tif (path_walk) {\n \t\tconst char *option = NULL;\n-\t\tif (filter_options.choice)\n+\t\tif (!path_walk_filter_compatible(&filter_options))\n \t\t\toption = \"--filter\";\n \t\telse if (use_delta_islands)\n \t\t\toption = \"--delta-islands\";\ndiff --git a/path-walk.c b/path-walk.c\nindex 6e426af433..a4dd197c37 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -9,6 +9,7 @@\n #include \"hashmap.h\"\n #include \"hex.h\"\n #include \"list-objects.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -485,6 +486,32 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tswitch (options->choice) {\n+\tcase LOFC_DISABLED:\n+\t\treturn 1;\n+\n+\tcase LOFC_BLOB_NONE:\n+\t\tif (info) {\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n+\tdefault:\n+\t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n+\t\t      list_objects_filter_spec(options));\n+\t\treturn 0;\n+\t}\n+}\n+\n+int path_walk_filter_compatible(struct list_objects_filter_options *options)\n+{\n+\treturn prepare_filters(NULL, options);\n+}\n+\n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\n  * call 'info->path_fn' on each discovered path.\n@@ -512,6 +539,9 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tif (!prepare_filters(info, &info->revs->filter))\n+\t\treturn -1;\n+\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 5ef5a8440e..be8d27b398 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -85,3 +85,10 @@ void path_walk_info_clear(struct path_walk_info *info);\n  * Returns nonzero on an error.\n  */\n int walk_objects_by_path(struct path_walk_info *info);\n+\n+struct list_objects_filter_options;\n+/**\n+ * Given a set of options for filtering objects, return 1 if the options\n+ * are compatible with the path-walk API and 0 otherwise.\n+ */\n+int path_walk_filter_compatible(struct list_objects_filter_options *options);\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex fe63002c2b..88f86ae0dc 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -4,6 +4,7 @@\n #include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object-name.h\"\n #include \"object.h\"\n #include \"pretty.h\"\n@@ -71,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n+\tstruct list_objects_filter_options filter_options =\n+\t\tLIST_OBJECTS_FILTER_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n@@ -86,11 +89,12 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n \n \tsetup_git_directory();\n-\trevs.repo = the_repository;\n+\trepo_init_revisions(the_repository, &revs, NULL);\n \n \targc = parse_options(argc, argv, NULL,\n \t\t\t     options, path_walk_usage,\n@@ -101,6 +105,10 @@ int cmd__path_walk(int argc, const char **argv)\n \telse\n \t\tusage(path_walk_usage[0]);\n \n+\t/* Apply the filter after setup_revisions to avoid the --objects check. */\n+\tif (filter_options.choice)\n+\t\tlist_objects_filter_copy(&revs.filter, &filter_options);\n+\n \tinfo.revs = &revs;\n \tinfo.path_fn = emit_block;\n \tinfo.path_fn_data = &data;\n@@ -129,6 +137,7 @@ int cmd__path_walk(int argc, const char **argv)\n \t\tfree(info.pl);\n \t}\n \n+\tlist_objects_filter_release(&filter_options);\n \trelease_revisions(&revs);\n \treturn res;\n }\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 56bd1e3c5b..94df309987 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -415,4 +415,64 @@ test_expect_success 'trees are reported exactly once' '\n \ttest_line_count = 1 out-filtered\n '\n \n+test_expect_success 'all, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:tree::$(git rev-parse topic^{tree})\n+\t2:tree::$(git rev-parse base^{tree})\n+\t2:tree::$(git rev-parse base~1^{tree})\n+\t2:tree::$(git rev-parse base~2^{tree})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t5:tree:left/:$(git rev-parse base:left)\n+\t5:tree:left/:$(git rev-parse base~2:left)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:tree:left/:$(git rev-parse base~2:left)\n+\t3:tree:right/:$(git rev-parse topic:right)\n+\t3:tree:right/:$(git rev-parse base~1:right)\n+\t3:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"542722","messageId":"da4c19af42ccdf550b52a247aa688d20393b983a.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 04/10] backfill: die on incompatible filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:13Z","receivedAt":"2026-05-04T20:21:28Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git backfill' command uses the path-walk API in a critical way: it\nuses the objects output from the command to find the batches of missing\nobjects that should be requested from the server. Unlike 'git\npack-objects', we cannot fall back to another mechanism.\n\nThe previous change added the path_walk_filter_compatible() method that\nwe can reuse here. Use it during argument validation in cmd_backfill().\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c  | 5 ++---\n t/t5620-backfill.sh | 8 ++++++++\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 7ffab2ea74..b80f9ebe69 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -96,9 +96,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \tif (revs->explicit_diff_merges)\n \t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n \t\t    \"--diff-merges\");\n-\tif (revs->filter.choice)\n-\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n-\t\t    \"--filter\");\n+\tif (!path_walk_filter_compatible(&revs->filter))\n+\t\tdie(_(\"cannot backfill with these filter options\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..ede89f8c33 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -15,6 +15,14 @@ test_expect_success 'backfill rejects unexpected arguments' '\n \ttest_grep \"unrecognized argument: --unexpected-arg\" err\n '\n \n+test_expect_success 'backfill rejects incompatible filter options' '\n+\ttest_must_fail git backfill --objects --filter=tree:1 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err &&\n+\n+\ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err\n+'\n+\n # We create objects in the 'src' repo.\n test_expect_success 'setup repo for object creation' '\n \techo \"{print \\$1}\" >print_1.awk &&\n-- \ngitgitgadget\n\n"},{"id":"542723","messageId":"d309345fece6fe293b392197b717b947b48590fc.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 05/10] path-walk: support blob size limit filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:14Z","receivedAt":"2026-05-04T20:21:29Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nExtend the path-walk API to handle the 'blob:limit=<size>' object\nfilter natively. This filter omits blobs whose size is equal to or\ngreater than the given limit, matching the semantics used by the\nlist-objects-filter machinery.\n\nWhen revs->filter.choice is LOFC_BLOB_LIMIT, the prepare_filters()\nmethod stores the limit value in info->blob_limit and clears the filter\nfrom revs. If the limit is zero, this degenerates to blob:none (all\nblobs excluded), so info->blobs is set to 0 instead.\n\nDuring walk_path(), blob batches are filtered before being delivered to\nthe callback: each blob's size is checked via odb_read_object_info(),\nand only blobs strictly smaller than the limit are included. Blobs whose\nsize cannot be determined (e.g. missing in a partial clone) are\nconservatively included, matching the existing filter behavior. Empty\nbatches after filtering are skipped entirely.\n\nThe check for inclusion in the path batch looks a little strange at\nfirst glance. We use odb_read_object_info() to read the object's size.\nBased on all of the assumptions to this point, this _should_ return\nOBJ_BLOB. Since we are focused on the size filter, we use a\nshort-circuited OR (||) to skip the size check if that method returns a\ndifferent object type.\n\nNotice that this inspection of object sizes requires the content to be\npresent in the repository. The odb_read_object_info() call will download\na missing blob on-demand. This means that the use of the path-walk API\nwithin 'git backfill' would not operate nicely with this filter type.\nThe intention of that command is to download missing blobs in batches.\nDownloading objects one-by-one would go against the point. Update the\nvalidation in 'git backfill' to add its own compatibility check on top\nof path_walk_filter_compatible().\n\nAdd tests for blob:limit=0 (equivalent to blob:none) and blob:limit=3\n(which exercises partial filtering within a batch where some blobs are\nkept and others are excluded).\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n builtin/backfill.c                  |  2 +\n path-walk.c                         | 38 ++++++++++++--\n path-walk.h                         |  8 +++\n t/t5620-backfill.sh                 |  2 +-\n t/t6601-path-walk.sh                | 78 +++++++++++++++++++++++++++++\n 6 files changed, 125 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 917045d5c3..3821bf7e22 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,7 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n-supports the `--filter=<spec>` form `blob:none`.\n+supports the `--filter=<spec>` form `blob:none` and `blob:limit=<n>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex b80f9ebe69..5254a42711 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -98,6 +98,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \t\t    \"--diff-merges\");\n \tif (!path_walk_filter_compatible(&revs->filter))\n \t\tdie(_(\"cannot backfill with these filter options\"));\n+\tif (revs->filter.blob_limit_value)\n+\t\tdie(_(\"cannot backfill with blob size limits\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/path-walk.c b/path-walk.c\nindex a4dd197c37..0e7dab7a6a 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -315,9 +316,29 @@ static int walk_path(struct path_walk_context *ctx,\n \t/* Evaluate function pointer on this data, if requested. */\n \tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n \t    (list->type == OBJ_BLOB && ctx->info->blobs) ||\n-\t    (list->type == OBJ_TAG && ctx->info->tags))\n-\t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n-\t\t\t\t\tctx->info->path_fn_data);\n+\t    (list->type == OBJ_TAG && ctx->info->tags)) {\n+\t\tstruct oid_array *oids = &list->oids;\n+\t\tstruct oid_array filtered = OID_ARRAY_INIT;\n+\n+\t\tif (list->type == OBJ_BLOB && ctx->info->blob_limit) {\n+\t\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\t\tunsigned long size;\n+\n+\t\t\t\tif (odb_read_object_info(ctx->repo->objects,\n+\t\t\t\t\t\t\t &list->oids.oid[i],\n+\t\t\t\t\t\t\t &size) != OBJ_BLOB ||\n+\t\t\t\t    size < ctx->info->blob_limit)\n+\t\t\t\t\toid_array_append(&filtered,\n+\t\t\t\t\t\t\t &list->oids.oid[i]);\n+\t\t\t}\n+\t\t\toids = &filtered;\n+\t\t}\n+\n+\t\tif (oids->nr)\n+\t\t\tret = ctx->info->path_fn(path, oids, list->type,\n+\t\t\t\t\t\t ctx->info->path_fn_data);\n+\t\toid_array_clear(&filtered);\n+\t}\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -500,6 +521,17 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_BLOB_LIMIT:\n+\t\tif (info) {\n+\t\t\tif (!options->blob_limit_value) {\n+\t\t\t\tinfo->blobs = 0;\n+\t\t\t} else {\n+\t\t\t\tinfo->blob_limit = options->blob_limit_value;\n+\t\t\t}\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/path-walk.h b/path-walk.h\nindex be8d27b398..bcb81b70a1 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -42,6 +42,14 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If non-zero, specifies a maximum blob size. Blobs with a\n+\t * size equal to or greater than this limit will be omitted\n+\t * from the walk. Blobs smaller than the limit (or blobs\n+\t * whose size cannot be determined) are still visited.\n+\t */\n+\tunsigned long blob_limit;\n+\n \t/**\n \t * When 'prune_all_uninteresting' is set and a path has all objects\n \t * marked as UNINTERESTING, then the path-walk will not visit those\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex ede89f8c33..d2ea68e065 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -20,7 +20,7 @@ test_expect_success 'backfill rejects incompatible filter options' '\n \ttest_grep \"cannot backfill with these filter options\" err &&\n \n \ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n-\ttest_grep \"cannot backfill with these filter options\" err\n+\ttest_grep \"cannot backfill with blob size limits\" err\n '\n \n # We create objects in the 'src' repo.\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 94df309987..d9be7b9cd2 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -475,4 +475,82 @@ test_expect_success 'topic only, blob:none filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, blob:limit=0 filter' '\n+\ttest-tool path-walk --filter=blob:limit=0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:tree::$(git rev-parse topic^{tree})\n+\t2:tree::$(git rev-parse base^{tree})\n+\t2:tree::$(git rev-parse base~1^{tree})\n+\t2:tree::$(git rev-parse base~2^{tree})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t5:tree:left/:$(git rev-parse base:left)\n+\t5:tree:left/:$(git rev-parse base~2:left)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, blob:limit=3 filter' '\n+\ttest-tool path-walk --filter=blob:limit=3 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:tree::$(git rev-parse topic^{tree})\n+\t2:tree::$(git rev-parse base^{tree})\n+\t2:tree::$(git rev-parse base~1^{tree})\n+\t2:tree::$(git rev-parse base~2^{tree})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t3:blob:a:$(git rev-parse base~2:a)\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:blob:left/b:$(git rev-parse base~2:left/b)\n+\t8:tree:right/:$(git rev-parse topic:right)\n+\t8:tree:right/:$(git rev-parse base~1:right)\n+\t8:tree:right/:$(git rev-parse base~2:right)\n+\t9:blob:right/c:$(git rev-parse base~2:right/c)\n+\t10:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:4\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"542724","messageId":"55ed02772b5c0aecebfb2a99416555d647d0cb77.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 06/10] path-walk: add pl_sparse_trees to control tree pruning","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:15Z","receivedAt":"2026-05-04T20:21:30Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe path-walk API prunes trees and blobs when a sparse-checkout pattern\nlist is provided, which is the correct behavior for 'git backfill\n--sparse' since it only needs to fill in objects at paths within the\nsparse cone.\n\nHowever, a future change will use the path-walk API with a sparse:<oid>\nfilter that restricts only blobs while retaining all reachable trees.\nTo support both behaviors, add a 'pl_sparse_trees' flag to\npath_walk_info. When set (as in 'git backfill --sparse' and the\n--stdin-pl test helper mode), the sparse patterns prune both trees and\nblobs. When unset, only blobs are filtered and all trees are walked and\nreported.\n\nAdditionally, move the SEEN flag assignment in add_tree_entries() to\nafter the sparse pattern and pathspec checks. Previously, SEEN was set\nimmediately upon discovering an object, before checking whether its path\nmatched the sparse patterns. When the same object ID appeared at\nmultiple paths (e.g. sibling directories with identical contents), the\nfirst path to be visited would mark the object as SEEN. If that path was\noutside the sparse cone, the object would be skipped there but also\nnever discovered at its in-cone path.\n\nBy deferring the SEEN flag until after the checks pass, objects that are\nskipped due to sparse filtering remain discoverable at other paths where\nthey may be in scope.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c        |  1 +\n path-walk.c               |  5 +++--\n path-walk.h               |  6 ++++++\n t/helper/test-path-walk.c |  6 +++++-\n t/t6601-path-walk.sh      | 37 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 52 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 5254a42711..e71e0f4742 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -109,6 +109,7 @@ static int do_backfill(struct backfill_context *ctx)\n \n \tif (ctx->sparse) {\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = 1;\n \t\tif (get_sparse_checkout_patterns(info.pl)) {\n \t\t\tpath_walk_info_clear(&info);\n \t\t\treturn error(_(\"problem loading sparse-checkout\"));\ndiff --git a/path-walk.c b/path-walk.c\nindex 0e7dab7a6a..c25392b901 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -183,7 +183,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t/* Skip this object if already seen. */\n \t\tif (o->flags & SEEN)\n \t\t\tcontinue;\n-\t\to->flags |= SEEN;\n \n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n@@ -204,7 +203,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\t\t\t\t  ctx->repo->index);\n \n \t\t\tif (ctx->info->pl->use_cone_patterns &&\n-\t\t\t    match == NOT_MATCHED)\n+\t\t\t    match == NOT_MATCHED &&\n+\t\t\t    (type == OBJ_BLOB || ctx->info->pl_sparse_trees))\n \t\t\t\tcontinue;\n \t\t\telse if (!ctx->info->pl->use_cone_patterns &&\n \t\t\t\t type == OBJ_BLOB &&\n@@ -239,6 +239,7 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\tcontinue;\n \t\t}\n \n+\t\to->flags |= SEEN;\n \t\tadd_path_to_list(ctx, path.buf, type, &entry.oid,\n \t\t\t\t !(o->flags & UNINTERESTING));\n \ndiff --git a/path-walk.h b/path-walk.h\nindex bcb81b70a1..5fa3ff46b4 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -72,8 +72,14 @@ struct path_walk_info {\n \t * of the cone. If not in cone mode, then all tree paths will be\n \t * explored but the path_fn will only be called when the path matches\n \t * the sparse-checkout patterns.\n+\t *\n+\t * When 'pl_sparse_trees' is zero, the sparse patterns only restrict\n+\t * blobs and all trees are included in the walk output. This matches\n+\t * the behavior of the sparse:oid object filter. When nonzero, trees\n+\t * are also pruned by the sparse patterns (as used by backfill).\n \t */\n \tstruct pattern_list *pl;\n+\tint pl_sparse_trees;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 88f86ae0dc..3f2b50a9aa 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -68,7 +68,7 @@ static int emit_block(const char *path, struct oid_array *oids,\n \n int cmd__path_walk(int argc, const char **argv)\n {\n-\tint res, stdin_pl = 0;\n+\tint res, stdin_pl = 0, pl_sparse_trees = -1;\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n@@ -89,6 +89,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_BOOL(0, \"pl-sparse-trees\", &pl_sparse_trees,\n+\t\t\t N_(\"toggle pruning of trees by sparse patterns\")),\n \t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n@@ -116,6 +118,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tif (stdin_pl) {\n \t\tstruct strbuf in = STRBUF_INIT;\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = (pl_sparse_trees >= 0) ?\n+\t\t\tpl_sparse_trees : 1;\n \n \t\tinfo.pl->use_cone_patterns = 1;\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex d9be7b9cd2..1126afaea1 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -206,6 +206,43 @@ test_expect_success 'base & topic, sparse' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'base & topic, sparse, no tree pruning' '\n+\tcat >patterns <<-EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\n+\ttest-tool path-walk --stdin-pl --no-pl-sparse-trees \\\n+\t\t-- base topic <patterns >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:blob:left/b:$(git rev-parse base:left/b)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:3\n+\tcommits:4\n+\ttags:0\n+\ttrees:10\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n-- \ngitgitgadget\n\n"},{"id":"542725","messageId":"dcb1a29d3a12d1f358b3b24937370c943c0be22e.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 07/10] pack-objects: support sparse:oid filter with path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:16Z","receivedAt":"2026-05-04T20:21:31Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe --filter=sparse:<oid> option to 'git pack-objects' allows focusing\nan object set to a sparse-checkout definition. This reduces the set of\nmatching blobs while retaining all reachable trees. No server currently\nsupports fetching with this filter because it is expensive to compute\nand reachability bitmaps do not help without a significant effort to\nextend the bitmap feature to store bitmaps for each supported sparse-\ncheckout definition.\n\nWithout focusing on serving fetches and clones with these filters, there\nare still benefits that could be realized by making this faster. With\nthe sparse index, it's more realistic now than ever to be able to\noperate a local clone that was bootstrapped by a packfile created with\na sparse filter, because the missing trees are not needed to move a\nsparse-checkout from one commit to another or to view the history of any\npath in scope. Such clones could perhaps be bootstrapped by partial\nbundles.\n\nPreviously, constructing these sparse packs has been incredibly\ncomputationally inefficient. The revision walk that explores which\nobjects are in scope spends a lot of time checking each object to see if\nit matches the sparse-checkout patterns, causing quadratic behavior\n(number of objects times number of sparse-checkout patterns). This\nimproves somewhat when using cone-mode sparse-checkout patterns that can\nuse hashtables and prefix matches to determine containment. However, the\ncheck per object is still too expensive for most cases.\n\nThis is where the path-walk feature comes in. We can proceed as normal\nby placing objects in bins by path and _then_ check a group of objects\nall at once. Since sparse:<oid> only restricts blobs, the path-walk must\ninclude all reachable trees while using the cone-mode patterns to skip\nblobs at paths outside the sparse scope. This establishes a baseline for\na potential future \"treesparse:<oid>\" filter that would also restrict\ntrees, but introducing such a new filter is deferred to a later change.\n\nThe implementation here is focused around loading the sparse-checkout\npatterns from the provided object ID and checking that the patterns are\nindeed cone-mode patterns. We can then load the correct pattern list\ninto the path walk context and use the logic that already exists from\nbff45557675 (backfill: add --sparse option, 2025-02-03), though that\nfeature loads sparse-checkout patterns from the worktree's local\nsettings and also restricts tree objects. We use a combination of errors\nand warnings to signal problems during this load. The difference is that\nerrors are likely fatal for the non-path-walk version while the warnings\nare probably just implementation details for the path-walk version and\nthe 'git pack-objects' command can fall back to the revision walk\nversion.\n\nNow that the SEEN flag is deferred until after pattern checks (from the\nprevious commit), handle the case where a tree with a shared OID appears\nat both an out-of-cone and in-cone path. When trees are not being pruned\n(pl_sparse_trees == 0), the path-walk re-walks the tree at the in-cone\npath so that in-cone blobs within it are discovered. The new tests in\nt5317 and t6601 demonstrate this behavior and would fail without these\nchanges.\n\nThe performance test p5315 shows the impact of this change when using\nsparse filters:\n\nTest                                              HEAD~1     HEAD\n----------------------------------------------------------------------\n5315.10: repack (sparse:oid)                      77.98    77.47  -0.7%\n5315.11: repack size (sparse:oid)                187.5M   187.4M  -0.0%\n5315.12: repack (sparse:oid, --path-walk)         77.91    31.41 -59.7%\n5315.13: repack size (sparse:oid, --path-walk)   187.5M   161.1M -14.1%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (14% smaller for sparse packs)\nand dramatic time savings (60% faster) by leveraging the path-walk's\nability to skip blobs outside the sparse scope.\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blaue <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-backfill.adoc        |   4 +\n Documentation/git-pack-objects.adoc    |   3 +-\n builtin/pack-objects.c                 |   2 +\n path-walk.c                            |  81 ++++++++++++++-\n t/t5317-pack-objects-filter-objects.sh | 125 +++++++++++++++++++++++\n t/t6601-path-walk.sh                   | 131 +++++++++++++++++++++++++\n 6 files changed, 341 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc\nindex c0a3b80615..82d6a1969d 100644\n--- a/Documentation/git-backfill.adoc\n+++ b/Documentation/git-backfill.adoc\n@@ -80,6 +80,10 @@ OPTIONS\n +\n You may also use commit-limiting options understood by\n linkgit:git-rev-list[1] such as `--first-parent`, `--since`, or pathspecs.\n++\n+Most `--filter=<spec>` options don't work with the purpose of\n+`git backfill`, but the `sparse:<oid>` filter is integrated to provide a\n+focused set of paths to download, distinct from the `--sparse` option.\n \n SEE ALSO\n --------\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 3821bf7e22..026061a5bc 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n-supports the `--filter=<spec>` form `blob:none` and `blob:limit=<n>`.\n+supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`, and\n+`sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex bc9fb5b457..ba00d8148a 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4777,6 +4777,8 @@ static void get_object_list_path_walk(struct rev_info *revs)\n \tresult = walk_objects_by_path(&info);\n \ttrace2_region_leave(\"pack-objects\", \"path-walk\", revs->repo);\n \n+\tpath_walk_info_clear(&info);\n+\n \tif (result)\n \t\tdie(_(\"failed to pack objects via path-walk\"));\n }\ndiff --git a/path-walk.c b/path-walk.c\nindex c25392b901..700617ee2f 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"object-name.h\"\n #include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n@@ -180,10 +181,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\treturn -1;\n \t\t}\n \n-\t\t/* Skip this object if already seen. */\n-\t\tif (o->flags & SEEN)\n-\t\t\tcontinue;\n-\n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n \n@@ -194,6 +191,40 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\tif (type == OBJ_TREE)\n \t\t\tstrbuf_addch(&path, '/');\n \n+\t\tif (o->flags & SEEN) {\n+\t\t\t/*\n+\t\t\t * A tree with a shared OID may appear at multiple\n+\t\t\t * paths. Even though we already added this tree to\n+\t\t\t * the output at some other path, we still need to\n+\t\t\t * walk into it at this in-cone path to discover\n+\t\t\t * blobs that were not found at the earlier\n+\t\t\t * out-of-cone path.\n+\t\t\t *\n+\t\t\t * Only do this for paths not yet in our map, to\n+\t\t\t * avoid duplicate entries when the same tree OID\n+\t\t\t * appears at the same path across multiple commits.\n+\t\t\t */\n+\t\t\tif (type == OBJ_TREE && ctx->info->pl &&\n+\t\t\t    ctx->info->pl->use_cone_patterns &&\n+\t\t\t    !ctx->info->pl_sparse_trees &&\n+\t\t\t    !strmap_contains(&ctx->paths_to_lists, path.buf)) {\n+\t\t\t\tint dtype;\n+\t\t\t\tenum pattern_match_result m;\n+\t\t\t\tm = path_matches_pattern_list(path.buf, path.len,\n+\t\t\t\t\t\t\t      path.buf + base_len,\n+\t\t\t\t\t\t\t      &dtype,\n+\t\t\t\t\t\t\t      ctx->info->pl,\n+\t\t\t\t\t\t\t      ctx->repo->index);\n+\t\t\t\tif (m != NOT_MATCHED) {\n+\t\t\t\t\tadd_path_to_list(ctx, path.buf, type,\n+\t\t\t\t\t\t\t &entry.oid,\n+\t\t\t\t\t\t\t !(o->flags & UNINTERESTING));\n+\t\t\t\t\tpush_to_stack(ctx, path.buf);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (ctx->info->pl) {\n \t\t\tint dtype;\n \t\t\tenum pattern_match_result match;\n@@ -533,6 +564,48 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_SPARSE_OID:\n+\t\tif (info) {\n+\t\t\tstruct object_id sparse_oid;\n+\t\t\tstruct repository *repo = info->revs->repo;\n+\n+\t\t\tif (info->pl) {\n+\t\t\t\twarning(_(\"sparse filter cannot be combined with existing sparse patterns\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (repo_get_oid_with_flags(repo,\n+\t\t\t\t\t\t    options->sparse_oid_name,\n+\t\t\t\t\t\t    &sparse_oid,\n+\t\t\t\t\t\t    GET_OID_BLOB)) {\n+\t\t\t\terror(_(\"unable to access sparse blob in '%s'\"),\n+\t\t\t\t      options->sparse_oid_name);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tCALLOC_ARRAY(info->pl, 1);\n+\t\t\tinfo->pl->use_cone_patterns = 1;\n+\n+\t\t\tif (add_patterns_from_blob_to_list(&sparse_oid, \"\", 0,\n+\t\t\t\t\t\t\t   info->pl) < 0) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\terror(_(\"unable to parse sparse filter data in '%s'\"),\n+\t\t\t\t      oid_to_hex(&sparse_oid));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (!info->pl->use_cone_patterns) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh\nindex 501d715b9a..dddb79ba62 100755\n--- a/t/t5317-pack-objects-filter-objects.sh\n+++ b/t/t5317-pack-objects-filter-objects.sh\n@@ -478,4 +478,129 @@ test_expect_success 'verify pack-objects w/ --missing=allow-any' '\n \tEOF\n '\n \n+# Test that --path-walk produces the same object set as standard traversal\n+# when using sparse:oid filters with cone-mode patterns.\n+#\n+# The sparse:oid filter restricts only blobs, not trees. Both standard\n+# and path-walk should produce identical sets of blobs, commits, and trees.\n+\n+test_expect_success 'setup pw_sparse for path-walk comparison' '\n+\tgit init pw_sparse &&\n+\tmkdir -p pw_sparse/inc/sub pw_sparse/exc/sub &&\n+\n+\tfor n in 1 2\n+\tdo\n+\t\techo \"inc $n\" >pw_sparse/inc/file$n &&\n+\t\techo \"inc sub $n\" >pw_sparse/inc/sub/file$n &&\n+\t\techo \"exc $n\" >pw_sparse/exc/file$n &&\n+\t\techo \"exc sub $n\" >pw_sparse/exc/sub/file$n &&\n+\t\techo \"root $n\" >pw_sparse/root$n || return 1\n+\tdone &&\n+\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"first\" &&\n+\n+\techo \"inc 1 modified\" >pw_sparse/inc/file1 &&\n+\techo \"exc 1 modified\" >pw_sparse/exc/file1 &&\n+\techo \"root 1 modified\" >pw_sparse/root1 &&\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"second\" &&\n+\n+\t# Cone-mode sparse pattern: include root + inc/\n+\tprintf \"/*\\n!/*/\\n/inc/\\n\" |\n+\tgit -C pw_sparse hash-object -w --stdin >sparse_oid\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk produces same blobs' '\n+\toid=$(cat sparse_oid) &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >standard.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../standard.pack &&\n+\tgit -C pw_sparse verify-pack -v ../standard.pack >standard_verify &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >pathwalk.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../pathwalk.pack &&\n+\tgit -C pw_sparse verify-pack -v ../pathwalk.pack >pathwalk_verify &&\n+\n+\t# Blobs must match exactly\n+\tgrep -E \"^[0-9a-f]{40} blob\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_blobs &&\n+\ttest_cmp standard_blobs pathwalk_blobs &&\n+\n+\t# Commits must match exactly\n+\tgrep -E \"^[0-9a-f]{40} commit\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_commits &&\n+\tgrep -E \"^[0-9a-f]{40} commit\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_commits &&\n+\ttest_cmp standard_commits pathwalk_commits\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk includes all trees' '\n+\t# The sparse:oid filter restricts only blobs, not trees.\n+\t# Both standard and path-walk should include the same trees.\n+\tgrep -E \"^[0-9a-f]{40} tree\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_trees &&\n+\tgrep -E \"^[0-9a-f]{40} tree\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_trees &&\n+\n+\ttest_cmp standard_trees pathwalk_trees\n+'\n+\n+# Test the edge case where the same tree/blob OID appears at both an\n+# in-cone and out-of-cone path. When sibling directories have identical\n+# contents, they share a tree OID. The path-walk defers marking objects\n+# SEEN until after checking sparse patterns, so an object at an out-of-cone\n+# path can still be discovered at an in-cone path.\n+\n+test_expect_success 'setup pw_shared for shared OID across cone boundary' '\n+\tgit init pw_shared &&\n+\tmkdir pw_shared/aaa pw_shared/zzz &&\n+\techo \"shared content\" >pw_shared/aaa/file &&\n+\techo \"shared content\" >pw_shared/zzz/file &&\n+\techo \"root file\" >pw_shared/rootfile &&\n+\tgit -C pw_shared add . &&\n+\tgit -C pw_shared commit -m \"aaa and zzz share tree OID\" &&\n+\n+\t# Verify they share a tree OID\n+\taaa_tree=$(git -C pw_shared rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git -C pw_shared rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tprintf \"/*\\n!/*/\\n/zzz/\\n\" |\n+\tgit -C pw_shared hash-object -w --stdin >shared_sparse_oid\n+'\n+\n+test_expect_success 'shared tree OID: --path-walk blobs match standard' '\n+\toid=$(cat shared_sparse_oid) &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >shared_std.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_std.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_std.pack >shared_std_verify &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >shared_pw.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_pw.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_pw.pack >shared_pw_verify &&\n+\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_std_verify |\n+\tawk \"{print \\$1}\" | sort >shared_std_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_pw_verify |\n+\tawk \"{print \\$1}\" | sort >shared_pw_blobs &&\n+\ttest_cmp shared_std_blobs shared_pw_blobs\n+'\n+\n test_done\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 1126afaea1..520269dfc6 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -590,4 +590,135 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'setup sparse filter blob' '\n+\t# Cone-mode patterns: include root, exclude all dirs, include left/\n+\tcat >patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\tsparse_oid=$(git hash-object -w -t blob patterns)\n+'\n+\n+test_expect_success 'all, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:blob:left/b:$(git rev-parse base:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+# Demonstrate the SEEN flag ordering issue: when the same tree/blob OID\n+# appears at two sibling paths where one is in-cone and the other is\n+# out-of-cone, the path-walk must still discover blobs at the in-cone\n+# path even when the shared tree OID was first encountered out-of-cone.\n+# Since sparse:oid includes all trees, the out-of-cone tree (aaa/) is\n+# walked first, and its blob is skipped. The path-walk then re-walks\n+# the same tree OID at the in-cone path (zzz/) to find the blob there.\n+\n+test_expect_success 'setup shared tree OID across cone boundary' '\n+\tgit checkout --orphan shared-tree &&\n+\tgit rm -rf . &&\n+\tmkdir aaa zzz &&\n+\techo \"shared content\" >aaa/file &&\n+\techo \"shared content\" >zzz/file &&\n+\techo \"root file\" >rootfile &&\n+\tgit add aaa zzz rootfile &&\n+\tgit commit -m \"aaa and zzz have same tree OID\" &&\n+\n+\t# Verify they really share a tree OID\n+\taaa_tree=$(git rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tcat >shared-patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/zzz/\n+\tEOF\n+\tshared_sparse_oid=$(git hash-object -w -t blob shared-patterns)\n+'\n+\n+test_expect_success 'sparse:oid with shared tree OID across cone boundary' '\n+\ttest-tool path-walk \\\n+\t\t--filter=sparse:oid=$shared_sparse_oid \\\n+\t\t-- shared-tree >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse shared-tree)\n+\t1:tree::$(git rev-parse shared-tree^{tree})\n+\t2:blob:rootfile:$(git rev-parse shared-tree:rootfile)\n+\t3:tree:aaa/:$(git rev-parse shared-tree:aaa)\n+\t4:tree:zzz/:$(git rev-parse shared-tree:zzz)\n+\t5:blob:zzz/file:$(git rev-parse shared-tree:zzz/file)\n+\tblobs:2\n+\tcommits:1\n+\ttags:0\n+\ttrees:3\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"542726","messageId":"772cdcaeed69f81d6aa3f60d36f761e58aeb8cf3.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 08/10] path-walk: support `tree:0` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:17Z","receivedAt":"2026-05-04T20:21:32Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `tree:0` object filter omits all trees and blobs from the result,\nkeeping only commits and tags. Consequently, this filter type should\nhas a fairly straightforward integration with path-walk, as the decision\nto include an object depends only on its type and does not depend on any\npath-sensitive state.\n\nMapping it onto `path_walk_info` is direct: set `info->trees = 0` and\n`info->blobs = 0` in `prepare_filters()` when the `LOFC_TREE_DEPTH`\nchoice is requested with depth zero. The existing code already plumbs\nthose flags through the rest of the walk:\n\n - 'walk_objects_by_path()' sets `revs->blob_objects = info->blobs` and\n   `revs->tree_objects = info->trees` before `prepare_revision_walk()`,\n   so the revision walk doesn't try to enumerate trees or blobs itself.\n\n - The commit-walk loop short-circuits the root-tree fetch with\n   \"if (!info->trees && !info->blobs) continue;\", so we never even\n   look up the root tree, let alone descend into it.\n\n - `setup_pending_objects()` skips pending trees and blobs based on\n   the same flags.\n\nThis means the path-walk doesn't allocate or expand any tree structures\nat all under `tree:0`, which matches the intended behavior of the\nfilter.\n\nNon-zero tree-depth filters are not supported. Those depend on the depth\nat which a tree is visited, which is a path-walk concept the filter\nmachinery doesn't currently share with the path-walk API. Reject them in\n`prepare_filters()` with a helpful error and let pack-objects fall back\nto the regular traversal, the same way it already does for unsupported\nfilters.\n\nAdd coverage in t6601 for both `--all` and a single-branch case to\nconfirm that no trees or blobs are emitted, and a separate test that\n`tree:1` is rejected with the expected error message. Place the new\ntests before \"setup sparse filter blob\" so they run on the original set\nof refs, before the orphan branch that the sparse-tree tests create.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  4 +--\n path-walk.c                         | 13 +++++++++\n t/t6601-path-walk.sh                | 45 +++++++++++++++++++++++++++++\n 3 files changed, 60 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 026061a5bc..3e26f57b92 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,8 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n-supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`, and\n-`sparse:<oid>`.\n+supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`,\n+`tree:0`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex 700617ee2f..36a1e5b967 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -564,6 +564,19 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_TREE_DEPTH:\n+\t\tif (options->tree_exclude_depth) {\n+\t\t\terror(_(\"tree:%lu filter not supported by the path-walk API\"),\n+\t\t\t      options->tree_exclude_depth);\n+\t\t\treturn 0;\n+\t\t}\n+\t\tif (info) {\n+\t\t\tinfo->trees = 0;\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 520269dfc6..72e09211e6 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -590,6 +590,51 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\tblobs:0\n+\tcommits:4\n+\ttags:7\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'tree:1 filter is rejected' '\n+\ttest_must_fail test-tool path-walk --filter=tree:1 -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n\n"},{"id":"542727","messageId":"c557f3c59d8cca51c0669d5463a7e032f9f8ab72.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 09/10] path-walk: support `object:type` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:18Z","receivedAt":"2026-05-04T20:21:34Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `object:type` filter accepts only objects of a single type; it is\nthe second member of the object-info-only filter family that bitmap\ntraversal already supports.\n\nLike `blob:none` and `tree:0`, it can be evaluated with nothing more\nthan the object's type, which is exactly the granularity path-walk's\nexisting info->{commits,trees,blobs,tags} flags already control.\n\nMap `LOFC_OBJECT_TYPE` in `prepare_filters()` by AND-ing each flag\nagainst the filtered type. A single `object:type=X` filter\napplied to the default info (all flags = 1) leaves `info->X = 1` and\nall the others 0, which is what we want.\n\nUsing an AND rather than straight assignment prepares us for a\nsubsequent change to implement combined object filters.\n\nThe path-walk machinery is mostly already wired for the per-type\ndistinction:\n\n - `walk_path()` calls `path_fn` for a batch only when the corresponding\n   `info->X` flag is set, so unwanted types are silently not reported.\n\n - `add_tree_entries()` skips tree entries of type `OBJ_BLOB` when\n   `info->blobs` is unset, so we don't even allocate paths for them.\n\n - The commit-walk loop short-circuits the root-tree fetch when\n   `!info->trees && !info->blobs`, so commit-only filters don't descend\n   into trees at all.\n\nBut there are a couple of side effects of the \"trees off, blobs on\" case\nthat need fixing:\n\n 1. 'setup_pending_objects()' previously skipped pending trees as soon\n    as `info->trees` was zero. For 'object:type=blob' the call site\n    needs those pending trees: a lightweight tag pointing to a tree, or\n    an annotated tag whose peeled target is a tree, can both reach\n    blobs that are otherwise unreachable from any commit's root tree.\n    Loosen the gate to \"if (!info->trees && !info->blobs) continue\" and\n    similarly retrieve the root_tree_list whenever either trees or\n    blobs are wanted.\n\n 2. The revision machinery's `handle_commit()` drops pending trees when\n    `revs->tree_objects` is zero (see the 'OBJ_TREE' handler in\n    revision.c), so by the time path-walk sees the pending list\n    after `prepare_revision_walk()` the tree-bearing pendings would\n    already be gone. Fix this by setting\n\n        revs->tree_objects = info->trees || info->blobs\n\n    so pending trees survive `prepare_revision_walk()` whenever we\n    need to walk into them. Path-walk still resets tree_objects to\n    zero immediately after `prepare_revision_walk()` returns, so the\n    rev-walk itself never enumerates trees redundantly with\n    path-walk's own descent.\n\nAdd coverage in t6601 for each of the four `object:type` values. The\n'object:type=blob' test in particular asserts that file2 and child/file\n(both reachable only through tag-pointed trees) show up in the output,\nexercising the pending-tree fix.\n\nUpdate Documentation/git-pack-objects.adoc to add object:type to\nthe list of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n path-walk.c                         | 23 +++++++-\n t/t6601-path-walk.sh                | 86 +++++++++++++++++++++++++++++\n 3 files changed, 107 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 3e26f57b92..bd7c808eef 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,7 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`,\n-`tree:0`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex 36a1e5b967..b9902abbb7 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -430,7 +430,7 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\tCALLOC_ARRAY(tags, 1);\n \tif (info->blobs)\n \t\tCALLOC_ARRAY(tagged_blobs, 1);\n-\tif (info->trees)\n+\tif (info->trees || info->blobs)\n \t\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n \n \t/*\n@@ -475,7 +475,7 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (!info->trees)\n+\t\t\tif (!info->trees && !info->blobs)\n \t\t\t\tcontinue;\n \t\t\tif (pending->path) {\n \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n@@ -577,6 +577,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_OBJECT_TYPE:\n+\t\tif (info) {\n+\t\t\tinfo->commits &= options->object_type == OBJ_COMMIT;\n+\t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n+\t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n+\t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\n@@ -683,9 +693,16 @@ int walk_objects_by_path(struct path_walk_info *info)\n \t/*\n \t * Set these values before preparing the walk to catch\n \t * lightweight tags pointing to non-commits and indexed objects.\n+\t *\n+\t * Keep tree_objects set whenever blobs are wanted: blobs may\n+\t * be reachable through trees that show up as pending objects\n+\t * (e.g., via lightweight tags pointing to trees, or annotated\n+\t * tags whose peeled target is a tree). Without tree_objects,\n+\t * prepare_revision_walk() would discard those pending trees\n+\t * and we would never descend into them.\n \t */\n \tinfo->revs->blob_objects = info->blobs;\n-\tinfo->revs->tree_objects = info->trees;\n+\tinfo->revs->tree_objects = info->trees || info->blobs;\n \n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 72e09211e6..13016e62ab 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -635,6 +635,92 @@ test_expect_success 'tree:1 filter is rejected' '\n \ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n '\n \n+test_expect_success 'all, object:type=commit filter' '\n+\ttest-tool path-walk --filter=object:type=commit -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:4\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tag filter' '\n+\ttest-tool path-walk --filter=object:type=tag -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tag:/tags:$(git rev-parse refs/tags/first)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t0:tag:/tags:$(git rev-parse refs/tags/third)\n+\t0:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t0:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t0:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\tblobs:0\n+\tcommits:0\n+\ttags:7\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tree filter' '\n+\ttest-tool path-walk --filter=object:type=tree -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tree::$(git rev-parse topic^{tree})\n+\t0:tree::$(git rev-parse base^{tree})\n+\t0:tree::$(git rev-parse base~1^{tree})\n+\t0:tree::$(git rev-parse base~2^{tree})\n+\t0:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t0:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t1:tree:a/:$(git rev-parse base:a)\n+\t2:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t3:tree:left/:$(git rev-parse base:left)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:tree:right/:$(git rev-parse topic:right)\n+\t4:tree:right/:$(git rev-parse base~1:right)\n+\t4:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=blob filter' '\n+\ttest-tool path-walk --filter=object:type=blob -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t3:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n+\t4:blob:left/b:$(git rev-parse base:left/b)\n+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:blob:right/c:$(git rev-parse base~2:right/c)\n+\t5:blob:right/c:$(git rev-parse topic:right/c)\n+\t6:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:10\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n\n"},{"id":"542728","messageId":"5423273edf7836b75fb79e2339f27bfcad994c15.1777926079.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v2 10/10] path-walk: support `combine` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-04T20:21:19Z","receivedAt":"2026-05-04T20:21:35Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `combine` filter takes the intersection of its children, that is:\nobjects are shown only when all child filters would admit the object.\n\nThe preceding patches added support for many individual filter types.\nEnable users to compose these filters by implementing support for the\n`combine` filter type.\n\nMapping intersection onto path_walk_info works because every supported\nchild filter is a monotonic restriction:\n\n - `blob:none`, `tree:0` unconditionally clear `info->blobs` and (for\n   `tree:0`) `info->trees`; clearing an already-cleared flag is a\n   no-op.\n\n - `object:type=X` is now expressed as an AND of each type flag with the\n   filtered type, so applying multiple such filters only refines the\n   existing set rather than overwrites it.\n\n - `blob:limit=N` has to compose too: the intersection of \"size < L1\"\n   and \"size < L2\" is \"size < min(L1, L2)\".\n\n   Update the `LOFC_BLOB_LIMIT` handler to take the running minimum when\n   `info->blob_limit` is already set, so a combined filter with, e.g.,\n   both \"blob:limit=10\" and \"blob:limit=5\" produces a limit of 5\n   regardless of ordering.\n\n - `sparse:oid` is left unchanged. A `combine` filter that includes a\n   `sparse:oid` is allowed at most once, since the existing handler\n   refuses to overwrite `info->pl`. Two `sparse:oid` filters in a single\n   `combine` would be unusual and are rejected with a warning, matching\n   the standalone `sparse:oid` behavior.\n\nImplementation-wise, the existing `prepare_filters()` called\n`list_objects_filter_release()` inside each case branch. That works fine\nfor top-level filters, but `combine` filters need to recurse over its\n  child filters without releasing each one in turn (since the parent's\n  release iterates the sub array). Split `prepare_filters()` into a\n  recursive helper that performs only the mutation, plus a thin wrapper\n  that calls the helper and then releases the top-level filter once.\n\nThe `LOFC_COMBINE` case in the helper just walks `sub_nr` and recurses;\nchild filters are released by the wrapper's single\n`list_objects_filter_release()` call on the parent (which itself\nrecursively releases each sub-filter, the same way it always has).\n\nIf any sub-filter is unsupported (e.g. \"tree:1\", \"sparse:<path>\", or a\nnot-yet-supported choice), the recursion bubbles a failure up and the\nexisting pack-objects/backfill fallback paths kick in.\n\nAdd coverage in t6601:\n\n  - \"combine:blob:none+tree:0\" collapses to \"tree:0\"\n\n  - \"combine:object:type=blob+blob:limit=3\" yields only the blobs\n    smaller than three bytes\n\n  - \"combine:object:type=blob+object:type=tree\" intersects to empty\n\n  - \"combine:tree:1+blob:none\" reports the \"tree:1\" error.\n\nUpdate Documentation/git-pack-objects.adoc to add combine to the\nlist of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  3 +-\n path-walk.c                         | 31 +++++++++-----\n t/t6601-path-walk.sh                | 65 +++++++++++++++++++++++++++++\n 3 files changed, 88 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex bd7c808eef..c12b63a040 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,8 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`,\n-`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter\n+types can be combined with the `combine:<spec>+<spec>` form.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex b9902abbb7..6d66da3dc3 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -539,28 +539,26 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n-static int prepare_filters(struct path_walk_info *info,\n-\t\t\t   struct list_objects_filter_options *options)\n+static int prepare_filters_one(struct path_walk_info *info,\n+\t\t\t       struct list_objects_filter_options *options)\n {\n \tswitch (options->choice) {\n \tcase LOFC_DISABLED:\n \t\treturn 1;\n \n \tcase LOFC_BLOB_NONE:\n-\t\tif (info) {\n+\t\tif (info)\n \t\t\tinfo->blobs = 0;\n-\t\t\tlist_objects_filter_release(options);\n-\t\t}\n \t\treturn 1;\n \n \tcase LOFC_BLOB_LIMIT:\n \t\tif (info) {\n \t\t\tif (!options->blob_limit_value) {\n \t\t\t\tinfo->blobs = 0;\n-\t\t\t} else {\n+\t\t\t} else if (!info->blob_limit ||\n+\t\t\t\t   options->blob_limit_value < info->blob_limit) {\n \t\t\t\tinfo->blob_limit = options->blob_limit_value;\n \t\t\t}\n-\t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n \n@@ -573,7 +571,6 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tinfo->trees = 0;\n \t\t\tinfo->blobs = 0;\n-\t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n \n@@ -583,7 +580,6 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n \t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n \t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n-\t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n \n@@ -624,8 +620,13 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n \t\t\t\treturn 0;\n \t\t\t}\n+\t\t}\n+\t\treturn 1;\n \n-\t\t\tlist_objects_filter_release(options);\n+\tcase LOFC_COMBINE:\n+\t\tfor (size_t i = 0; i < options->sub_nr; i++) {\n+\t\t\tif (!prepare_filters_one(info, &options->sub[i]))\n+\t\t\t\treturn 0;\n \t\t}\n \t\treturn 1;\n \n@@ -636,6 +637,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t}\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tif (!prepare_filters_one(info, options))\n+\t\treturn 0;\n+\tif (info)\n+\t\tlist_objects_filter_release(options);\n+\treturn 1;\n+}\n+\n int path_walk_filter_compatible(struct list_objects_filter_options *options)\n {\n \treturn prepare_filters(NULL, options);\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 13016e62ab..a7d5f0de4e 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -721,6 +721,71 @@ test_expect_success 'all, object:type=blob filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, combine:blob:none+tree:0 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:blob:none+tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\tblobs:0\n+\tcommits:4\n+\ttags:7\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine:object:type=blob+blob:limit=3 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+blob:limit=3 \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:a:$(git rev-parse base~2:a)\n+\t1:blob:left/b:$(git rev-parse base~2:left/b)\n+\t2:blob:right/c:$(git rev-parse base~2:right/c)\n+\t3:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:4\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine of disjoint object:types is empty' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+object:type=tree \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'combine: rejects unsupported subfilters' '\n+\ttest_must_fail test-tool path-walk \\\n+\t\t--filter=combine:tree:1+blob:none -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n"},{"id":"542763","messageId":"f5d8d4aa-2453-45ef-bc96-2b94bdf55c7e@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-05T16:18:28Z","receivedAt":"2026-05-05T16:18:32Z","isPatch":true,"body":"On 5/4/2026 4:21 PM, Derrick Stolee via GitGitGadget wrote:\n> NOTE: This series is based on en/backfill-fixes-and-edges.\n> \n> The 'git pack-objects' command has a '--path-walk' option that uses the\n> path-walk API instead of a typical revision walk to group objects into\n> chunks by path name instead of relying solely on name-hashes to group\n> similar files together. (It also does a second compression pass looking for\n> better deltas after the first pass that is focused within chunks per path.)\n> \n> The '--path-walk' feature was not previously integrated with the '--filter'\n> feature, so a warning would appear and disable the path-walk API when a\n> filter is given. This patch series integrates these together in the\n> following ways:\n> \n>  * --filter=blob:none updates the path-walk API options to skip blobs.\n>  * --filter=blob:limit=<size> adds a scan to a list of blob objects to\n>    remove objects that are too large.\n>  * --filter=sparse:<oid> adds a scan to the chunks to validate that the\n>    paths match the sparse-checkout patterns.\n\n(I need to update this cover letter to include the new filters.)\n\nOne thing I discovered when testing Taylor's series is that this series\nintroduces new test failures when run with GIT_TEST_PACK_PATH_WALK=1.\nIt's probably due to new cases that are fragile to the difference\nbetween delta compression algorithms, but are now exposed after the\nfilters are no longer disabling --path-walk even with that test var.\n\nI'll make sure these are fixed in the next version.\n\nThanks,\n-Stolee\n\n"},{"id":"542765","messageId":"afo+mEITFBSLevqV@nand.local","threadId":"65575","inReplyTo":"f5d8d4aa-2453-45ef-bc96-2b94bdf55c7e@gmail.com","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-05T19:01:44Z","receivedAt":"2026-05-05T19:01:48Z","isPatch":true,"body":"On Tue, May 05, 2026 at 12:18:28PM -0400, Derrick Stolee wrote:\n> One thing I discovered when testing Taylor's series is that this series\n> introduces new test failures when run with GIT_TEST_PACK_PATH_WALK=1.\n> It's probably due to new cases that are fragile to the difference\n> between delta compression algorithms, but are now exposed after the\n> filters are no longer disabling --path-walk even with that test var.\n>\n> I'll make sure these are fixed in the next version.\n\nThanks for looking into it.\n\nIt looks like this bisects (at least in t5310) to \"path-walk: support\nblobless filter\", which is 03/10 in this series. I suspect that there\nare other failures that are indeed due to delta selection sensitivity as\nyou note, but in this case it looks like we are actually not sending the\nright set of objects:\n\n    + git clone --no-local --bare --filter=blob:none . partial-clone.git\n    Cloning into bare repository 'partial-clone.git'...\n    [...]\n    fatal: bad object 782f60206c837dcd3d441e106549ad6f58de55b5\n    fatal: remote did not send all necessary objects\n    error: last command exited with $?=128\n    not ok 26 - partial clone from bitmapped repository\n\nI think this is a consequence of us not sending directly-referenced\nblobs with `--filter=blob:none` when running the filters through\n`--path-walk`. Something like:\n\n--- 8< ---\ndiff --git a/path-walk.c b/path-walk.c\nindex a4dd197c37e..dbad01287e2 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -159,8 +159,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\tif (S_ISGITLINK(entry.mode))\n \t\t\tcontinue;\n\n-\t\t/* If the caller doesn't want blobs, then don't bother. */\n-\t\tif (!ctx->info->blobs && type == OBJ_BLOB)\n+\t\tif ((!ctx->info->blobs || ctx->info->prune_tree_blobs) &&\n+\t\t    type == OBJ_BLOB)\n \t\t\tcontinue;\n\n \t\tif (type == OBJ_TREE) {\n@@ -495,7 +495,7 @@ static int prepare_filters(struct path_walk_info *info,\n\n \tcase LOFC_BLOB_NONE:\n \t\tif (info) {\n-\t\t\tinfo->blobs = 0;\n+\t\t\tinfo->prune_tree_blobs = 1;\n \t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n--- >8 ---\n\nfixes t5310 for me. I haven't looked into any of the other failures yet\nsince you mentioned that you're looking into them, but let me know if\nyou want to tag-team any of these.\n\n(As a related side-note, I noticed that GIT_TEST_PACK_PATH_WALK=1 is not\ncurrently in the TEST-vars CI build.  I'm not sure if there are\nhistorical reasons for leaving it out, but if not I think it would be\nworthwhile to add it.)\n\nThanks,\nTaylor\n"},{"id":"542780","messageId":"07b36bd8-376b-4a98-a735-0c0f75452c24@gmail.com","threadId":"65575","inReplyTo":"afo+mEITFBSLevqV@nand.local","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-05T19:44:56Z","receivedAt":"2026-05-05T19:44:59Z","isPatch":true,"body":"On 5/5/2026 3:01 PM, Taylor Blau wrote:\n> On Tue, May 05, 2026 at 12:18:28PM -0400, Derrick Stolee wrote:\n>> One thing I discovered when testing Taylor's series is that this series\n>> introduces new test failures when run with GIT_TEST_PACK_PATH_WALK=1.\n>> It's probably due to new cases that are fragile to the difference\n>> between delta compression algorithms, but are now exposed after the\n>> filters are no longer disabling --path-walk even with that test var.\n>>\n>> I'll make sure these are fixed in the next version.\n> \n> Thanks for looking into it.\n> \n> It looks like this bisects (at least in t5310) to \"path-walk: support\n> blobless filter\", which is 03/10 in this series. I suspect that there\n> are other failures that are indeed due to delta selection sensitivity as\n> you note, but in this case it looks like we are actually not sending the\n> right set of objects:\n> \n>     + git clone --no-local --bare --filter=blob:none . partial-clone.git\n>     Cloning into bare repository 'partial-clone.git'...\n>     [...]\n>     fatal: bad object 782f60206c837dcd3d441e106549ad6f58de55b5\n>     fatal: remote did not send all necessary objects\n>     error: last command exited with $?=128\n>     not ok 26 - partial clone from bitmapped repository\n> \n> I think this is a consequence of us not sending directly-referenced\n> blobs with `--filter=blob:none` when running the filters through\n> `--path-walk`. Something like:\n> \n> --- 8< ---\n> diff --git a/path-walk.c b/path-walk.c\n> index a4dd197c37e..dbad01287e2 100644\n> --- a/path-walk.c\n> +++ b/path-walk.c\n> @@ -159,8 +159,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n>  \t\tif (S_ISGITLINK(entry.mode))\n>  \t\t\tcontinue;\n> \n> -\t\t/* If the caller doesn't want blobs, then don't bother. */\n> -\t\tif (!ctx->info->blobs && type == OBJ_BLOB)\n> +\t\tif ((!ctx->info->blobs || ctx->info->prune_tree_blobs) &&\n> +\t\t    type == OBJ_BLOB)\n>  \t\t\tcontinue;\n> \n>  \t\tif (type == OBJ_TREE) {\n> @@ -495,7 +495,7 @@ static int prepare_filters(struct path_walk_info *info,\n> \n>  \tcase LOFC_BLOB_NONE:\n>  \t\tif (info) {\n> -\t\t\tinfo->blobs = 0;\n> +\t\t\tinfo->prune_tree_blobs = 1;\n>  \t\t\tlist_objects_filter_release(options);\n>  \t\t}\n>  \t\treturn 1;\n> --- >8 ---\n\nThanks for this suggestion. I got pulled away from my investigation, so\nwasn't to this point yet.\n> fixes t5310 for me. I haven't looked into any of the other failures yet\n> since you mentioned that you're looking into them, but let me know if\n> you want to tag-team any of these.\n> \n> (As a related side-note, I noticed that GIT_TEST_PACK_PATH_WALK=1 is not\n> currently in the TEST-vars CI build.  I'm not sure if there are\n> historical reasons for leaving it out, but if not I think it would be\n> worthwhile to add it.)\nI think the initial idea was that the feature was too niche to add it to\nthe CI builds right away. Your series is going to make it a lot more\nimportant, so adding this to CI builds may be valuable.\n\nThanks,\n-Stolee\n\n"},{"id":"542784","messageId":"afpWOGhWJrRdPtcy@nand.local","threadId":"65575","inReplyTo":"07b36bd8-376b-4a98-a735-0c0f75452c24@gmail.com","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-05T20:42:32Z","receivedAt":"2026-05-05T20:42:34Z","isPatch":true,"body":"On Tue, May 05, 2026 at 03:44:56PM -0400, Derrick Stolee wrote:\n> Thanks for this suggestion. I got pulled away from my investigation, so\n> wasn't to this point yet.\n\nNo problem. One of those things that in the course of responding to your\nemail, I had written enough to fix at least one of the tests. I hope I\ndidn't step on any toes as a consequence.\n\n> I think the initial idea was that the feature was too niche to add it to\n> the CI builds right away. Your series is going to make it a lot more\n> important, so adding this to CI builds may be valuable.\n\nSounds good to me. I imagine that this makes more sense to place as a\npreparatory patch in your series, but LMK if you would rather I place it\nin mine.\n\nThanks,\nTaylor\n"},{"id":"542840","messageId":"f5a08ae2-3eec-4939-b6d3-c31f8968dd09@gmail.com","threadId":"65575","inReplyTo":"afpWOGhWJrRdPtcy@nand.local","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-07T11:40:55Z","receivedAt":"2026-05-07T11:40:57Z","isPatch":true,"body":"On 5/5/26 4:42 PM, Taylor Blau wrote:\n> On Tue, May 05, 2026 at 03:44:56PM -0400, Derrick Stolee wrote:\n>> Thanks for this suggestion. I got pulled away from my investigation, so\n>> wasn't to this point yet.\n> \n> No problem. One of those things that in the course of responding to your\n> email, I had written enough to fix at least one of the tests. I hope I\n> didn't step on any toes as a consequence.\n> \n>> I think the initial idea was that the feature was too niche to add it to\n>> the CI builds right away. Your series is going to make it a lot more\n>> important, so adding this to CI builds may be valuable.\n> \n> Sounds good to me. I imagine that this makes more sense to place as a\n> preparatory patch in your series, but LMK if you would rather I place it\n> in mine.\n\nI've got some changes that make everything work, and they plug into\nthe right patches at the right time. I need to do a few more rounds\nof tweaking, but I should have a new version of the series ready by\ntomorrow with these fixes.\n\nIn summary, we've got a few things going on:\n\n1. At the base of the topic, the backfill tests fail because they\n    assert that stderr is empty and the warning about combining\n    filters comes up. This goes away with the integration, but it\n    was causing the tests to fail patch-by-patch.\n\n2. In partial clone situations, as you deduced, the direct object\n    requests were getting filtered out. This starts with the blob\n    requests with blob:none and then the tree requests are added\n    in with the tree:0 filter.\n\n3. When getting to your bitmap patches, there are some subtleties\n    around the --path-walk option adding --boundary to the revision\n    walk but that changes the object set and breaks tests. I need\n    to think more about the solution here, but I'll include it as\n    a reply to my v3 cover letter when the rest of this series is\n    re-rolled.\n\nThanks,\n-Stolee\n\n"},{"id":"543007","messageId":"xmqq8q9qtzly.fsf@gitster.g","threadId":"65575","inReplyTo":"07b36bd8-376b-4a98-a735-0c0f75452c24@gmail.com","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-11T03:05:29Z","receivedAt":"2026-05-11T03:05:32Z","isPatch":true,"body":"Derrick Stolee <stolee@gmail.com> writes:\n\n> On 5/5/2026 3:01 PM, Taylor Blau wrote:\n>> On Tue, May 05, 2026 at 12:18:28PM -0400, Derrick Stolee wrote:\n>> ...\n>> I think this is a consequence of us not sending directly-referenced\n>> blobs with `--filter=blob:none` when running the filters through\n>> `--path-walk`. Something like:\n>> ...\n> Thanks for this suggestion. I got pulled away from my investigation, so\n> wasn't to this point yet.\n>> fixes t5310 for me. I haven't looked into any of the other failures yet\n>> since you mentioned that you're looking into them, but let me know if\n>> you want to tag-team any of these.\n>> \n>> (As a related side-note, I noticed that GIT_TEST_PACK_PATH_WALK=1 is not\n>> currently in the TEST-vars CI build.  I'm not sure if there are\n>> historical reasons for leaving it out, but if not I think it would be\n>> worthwhile to add it.)\n> I think the initial idea was that the feature was too niche to add it to\n> the CI builds right away. Your series is going to make it a lot more\n> important, so adding this to CI builds may be valuable.\n\nShould I expect a new [v3] iteration anytime soon?  Not getting\nantsy, but just going through the \"What's cooking\" report to see if\nthere are things I should immediately advance.\n\nThanks.\n"},{"id":"543059","messageId":"f16da7bb-0aaa-4cf7-b10e-1fa334b235b4@gmail.com","threadId":"65575","inReplyTo":"xmqq8q9qtzly.fsf@gitster.g","subject":"Re: [PATCH v2 00/10] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-11T13:58:45Z","receivedAt":"2026-05-11T13:58:47Z","isPatch":true,"body":"On 5/10/2026 11:05 PM, Junio C Hamano wrote:\n> Derrick Stolee <stolee@gmail.com> writes:\n> \n>> On 5/5/2026 3:01 PM, Taylor Blau wrote:\n>>> On Tue, May 05, 2026 at 12:18:28PM -0400, Derrick Stolee wrote:\n>>> ...\n>>> I think this is a consequence of us not sending directly-referenced\n>>> blobs with `--filter=blob:none` when running the filters through\n>>> `--path-walk`. Something like:\n>>> ...\n>> Thanks for this suggestion. I got pulled away from my investigation, so\n>> wasn't to this point yet.\n>>> fixes t5310 for me. I haven't looked into any of the other failures yet\n>>> since you mentioned that you're looking into them, but let me know if\n>>> you want to tag-team any of these.\n>>>\n>>> (As a related side-note, I noticed that GIT_TEST_PACK_PATH_WALK=1 is not\n>>> currently in the TEST-vars CI build.  I'm not sure if there are\n>>> historical reasons for leaving it out, but if not I think it would be\n>>> worthwhile to add it.)\n>> I think the initial idea was that the feature was too niche to add it to\n>> the CI builds right away. Your series is going to make it a lot more\n>> important, so adding this to CI builds may be valuable.\n> \n> Should I expect a new [v3] iteration anytime soon?  Not getting\n> antsy, but just going through the \"What's cooking\" report to see if\n> there are things I should immediately advance.\n\nI hope to have a new version today. It was a more substantial change\ngetting things working correctly and in the right points in time. Lots\nof rebases and rewrites have been happening.\n\nThanks,\n-Stolee\n"},{"id":"543070","messageId":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v2.git.1777926079.gitgitgadget@gmail.com","subject":"[PATCH v3 00/12] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:12:57Z","receivedAt":"2026-05-11T18:13:12Z","isPatch":true,"body":"NOTE: This series is based on en/backfill-fixes-and-edges.\n\nThe 'git pack-objects' command has a '--path-walk' option that uses the\npath-walk API instead of a typical revision walk to group objects into\nchunks by path name instead of relying solely on name-hashes to group\nsimilar files together. (It also does a second compression pass looking for\nbetter deltas after the first pass that is focused within chunks per path.)\n\nThe '--path-walk' feature was not previously integrated with the '--filter'\nfeature, so a warning would appear and disable the path-walk API when a\nfilter is given. This patch series integrates these together in the\nfollowing ways:\n\n * --filter=blob:none updates the path-walk API options to skip blobs.\n * --filter=blob:limit=<size> adds a scan to a list of blob objects to\n   remove objects that are too large.\n * --filter=sparse:<oid> adds a scan to the chunks to validate that the\n   paths match the sparse-checkout patterns.\n\nIn particular, this last check is significantly faster than the previous\nalgorithm because it can check all objects at a given path simultaneously\ninstead of checking all sparse-checkout patterns for each discovered blob\nobject.\n\nA subtlety must be added here, in that we must change how we mark an object\nas \"seen\" during the path-walk. We may need to add an object to multiple\npaths and only mark it as \"seen\" if it indeed matched the sparse-checkout\npatterns as the path is accepted for emitting to the callback. This adds a\nnew filter that the \"seen\" objects must also be removed from later chunks to\navoid sending the same object as grouped to multiple chunks.\n\nThere's also a subtle detail here in that the path-walk API also prunes tree\npaths based on cone-mode sparse-checkouts, to enable 'git backfill --sparse'\noperating quickly for small sparse-checkout scopes. But the\n--filter=sparse:<oid> feature doesn't prune trees!\n\nAs a future step, I do plan to recommend that we add a treesparse:<oid>\nsetting that does allow us to trim the tree set by cone-mode sparse\npatterns. At the time that partial clone filters were being created, cone\nmode sparse-checkout didn't exist and neither did the sparse index. Those\nfeatures together make a smaller tree set possible, assuming the user never\nneeds to change their scope. This would be a significant change so it is not\nimplemented here, though the git pack-objects integration would be quick\nafter this series completes.\n\nNeither the sparse:<oid> or hypothetical treesparse:<oid> options are or\nshould necessarily be supported by Git servers. It's too expensive to\ncompute dynamically and it doesn't work well with reachability bitmaps. What\nbecomes possible with this change is that it becomes reasonably fast to\nconstruct bundles with these filters that can bootstrap a working\nenvironment with the full history of all files within a given\nsparse-checkout scope.\n\n\nPerformance Results\n===================\n\nSince the '--path-walk' option is ignored in today's Git version when a\n'--filter' is added, the performance matches the behavior without\n'--path-walk'. For the tables below, you can compare the rows against each\nother (time and then packfile size) for the mode without and then with\n'--path-walk' as a representation of \"before\" and \"after\". (These tables are\nrepeated in the commit messages as new implementations improve specific\nrows.)\n\nI chose a number of open source repositories of various sizes and shapes:\n\n\ngit/git\n=======\n\nTest                                              HEAD\n-------------------------------------------------------------------\n5315.2: repack (no filter)                       27.73\n5315.3: repack size (no filter)                 250.6M\n5315.4: repack (no filter, --path-walk)          35.19\n5315.5: repack size (no filter, --path-walk)    220.1M\n5315.6: repack (blob:none)                       13.42\n5315.7: repack size (blob:none)                 137.6M\n5315.8: repack (blob:none, --path-walk)          20.98\n5315.9: repack size (blob:none, --path-walk)    115.2M\n5315.10: repack (sparse:oid)                     72.53\n5315.11: repack size (sparse:oid)               187.5M\n5315.12: repack (sparse:oid, --path-walk)        29.00\n5315.13: repack size (sparse:oid, --path-walk)  161.0M\n\n\n\nnodejs/node\n===========\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                       75.53\n5315.3: repack size (no filter)                   0.9G\n5315.4: repack (no filter, --path-walk)          80.54\n5315.5: repack size (no filter, --path-walk)    885.7M\n5315.6: repack (blob:none)                       12.65\n5315.7: repack size (blob:none)                 148.6M\n5315.8: repack (blob:none, --path-walk)          17.60\n5315.9: repack size (blob:none, --path-walk)    134.6M\n5315.10: repack (sparse:oid)                    518.84\n5315.11: repack size (sparse:oid)               153.4M\n5315.12: repack (sparse:oid, --path-walk)        27.99\n5315.13: repack size (sparse:oid, --path-walk)  139.4M\n\n\n\nmicrosoft/fluentui\n==================\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                      146.77\n5315.3: repack size (no filter)                 562.1M\n5315.4: repack (no filter, --path-walk)          72.82\n5315.5: repack size (no filter, --path-walk)    172.6M\n5315.6: repack (blob:none)                        4.84\n5315.7: repack size (blob:none)                  62.7M\n5315.8: repack (blob:none, --path-walk)           5.19\n5315.9: repack size (blob:none, --path-walk)     59.9M\n5315.10: repack (sparse:oid)                     59.95\n5315.11: repack size (sparse:oid)                85.6M\n5315.12: repack (sparse:oid, --path-walk)        15.16\n5315.13: repack size (sparse:oid, --path-walk)   72.5M\n\n\n\nmicrosoftdocs/azure-devops-docs\n===============================\n\nTest                                               HEAD\n---------------------------------------------------------------------\n5315.2: repack (no filter)                        4.41\n5315.3: repack size (no filter)                   1.6G\n5315.4: repack (no filter, --path-walk)           6.00\n5315.5: repack size (no filter, --path-walk)      1.6G\n5315.6: repack (blob:none)                        1.35\n5315.7: repack size (blob:none)                  60.0M\n5315.8: repack (blob:none, --path-walk)           1.23\n5315.9: repack size (blob:none, --path-walk)     60.0M\n5315.10: repack (sparse:oid)                    138.24\n5315.11: repack size (sparse:oid)                84.4M\n5315.12: repack (sparse:oid, --path-walk)         1.86\n5315.13: repack size (sparse:oid, --path-walk)   84.4M\n\n\n\nPerformance conclusions\n=======================\n\nAs seen in earlier series around the '--path-walk' feature, the space\nsavings can be valuable but is not always guaranteed. When the space savings\ndoesn't happen, then the time spent is generally slower because of the\ntwo-pass mechanism. The microsoftdocs/azure-devops-docs repo demonstrates\nthis case quite clearly.\n\nHowever, even in these cases the 'sparse:<oid>' filters are much faster\nbecause of the ability to check an entire set of objects against the\nsparse-checkout patterns only once.\n\nThanks, -Stolee\n\n\nUPDATES IN V2\n=============\n\n * Rebased onto en/backfill-fixes-and-edges to properly integrate with the\n   incompatible rev-list options logic in that series.\n * Updated documentation as behavior changes. Credit to Taylor Blau for\n   presenting these suggestions in his RFC [2].\n * Added three patches of Taylor's to extend more filter options.\n\n\nUPDATES IN V3\n=============\n\nUpon realizing that the tests were not passing with\nGIT_TEST_PACK_PATH_WALK=1, I spent a lot of time reworking each patch to\npass all tests with that variable enabled. This led to a lot of meaningful\nchanges:\n\n * A new patch updates t5620-backfill.sh because they are currently failing\n   due to a check for quiet stderr checks. These changes are reverted later\n   when the filters are integrated so the warning stops being written.\n * I move the logic for the path-walk API emitting \"directly requested\"\n   objects (non-commits in the 'pending' list). This is substantial enough\n   to be its own patch.\n * The filtering logic is pulled entirely within the path-walk API instead\n   of needing integration within builtin/pack-objects.c.\n * The tree:0 filter had a lot to be desired when fetching missing objects,\n   so is substantially updated.\n * The object-type filter requires a change to the typical direct-request\n   behavior, including a new 'strict_types' member that prevents ever\n   allowing objects against type.\n * The combine filter needed better logic around multiple blob size limits,\n   to take the smaller of the two.\n * The t6601-path-walk.sh script has many test updates to better reflect the\n   new behavior, as required by the other partial clone tests under\n   GIT_TEST_PACK_PATH_WALK=1.\n * Doc updates for 'form' to 'forms' when multiple forms are supported.\n\nI've also updated Taylor's bitmap-related patches into three commits on top\nof this series (see [3]).\n\nP.S. I've CC'd the folks who were on the original path-walk feature thread\n[1]\n\n[1]\nhttps://lore.kernel.org/git/pull.1819.git.1741571455.gitgitgadget@gmail.com/\n\n[2] https://lore.kernel.org/git/cover.1777853408.git.me@ttaylorr.com/\n\n[3]\nhttps://github.com/derrickstolee/git/compare/path-walk-filters...derrickstolee:git:path-walk-bitmaps\n\nDerrick Stolee (9):\n  t5620: make test work with path-walk var\n  pack-objects: pass --objects with --path-walk\n  t/perf: add pack-objects filter and path-walk benchmark\n  path-walk: always emit directly-requested objects\n  path-walk: support blobless filter\n  backfill: die on incompatible filter options\n  path-walk: support blob size limit filter\n  path-walk: add pl_sparse_trees to control tree pruning\n  pack-objects: support sparse:oid filter with path-walk\n\nTaylor Blau (3):\n  path-walk: support `tree:0` filter\n  path-walk: support `object:type` filter\n  path-walk: support `combine` filter\n\n Documentation/git-backfill.adoc            |   4 +\n Documentation/git-pack-objects.adoc        |   8 +-\n Documentation/technical/api-path-walk.adoc |   7 +\n builtin/backfill.c                         |   8 +-\n builtin/pack-objects.c                     |  23 +-\n path-walk.c                                | 260 ++++++++++-\n path-walk.h                                |  31 ++\n t/helper/test-path-walk.c                  |  17 +-\n t/perf/p5315-pack-objects-filter.sh        | 129 ++++++\n t/t5317-pack-objects-filter-objects.sh     | 125 +++++\n t/t5620-backfill.sh                        |   8 +\n t/t6601-path-walk.sh                       | 516 +++++++++++++++++++++\n 12 files changed, 1093 insertions(+), 43 deletions(-)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\n\nbase-commit: a1ad4a0fca14cdeb55ab9fb065551b15cafa8a4f\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2101%2Fderrickstolee%2Fpath-walk-filters-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2101/derrickstolee/path-walk-filters-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2101\n\nRange-diff vs v2:\n\n  -:  ---------- >  1:  0840110116 t5620: make test work with path-walk var\n  1:  b361680338 =  2:  5cc6383380 pack-objects: pass --objects with --path-walk\n  2:  9ea81aa726 =  3:  77329cf8f4 t/perf: add pack-objects filter and path-walk benchmark\n  -:  ---------- >  4:  50933cccf5 path-walk: always emit directly-requested objects\n  3:  ed4d277a2c !  5:  b2deb7f819 path-walk: support blobless filter\n     @@ Commit message\n      \n          Update the test helper (test-path-walk) to accept --filter=<spec>\n          as a test-tool option (before '--'), applying it to revs after\n     -    setup_revisions() to avoid the --objects requirement check.\n     +    setup_revisions() to avoid the --objects requirement check. We can also\n     +    revert recent GIT_TEST_PACK_PATH_WALK overrides in t5620.\n      \n          Also switch test-path-walk from REV_INFO_INIT with manual repo\n          assignment to repo_init_revisions(), which properly initializes\n     @@ t/helper/test-path-walk.c: int cmd__path_walk(int argc, const char **argv)\n       \treturn res;\n       }\n      \n     + ## t/t5620-backfill.sh ##\n     +@@ t/t5620-backfill.sh: test_expect_success 'backfill with prefix pathspec' '\n     + \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n     + \ttest_line_count = 48 missing &&\n     + \n     +-\t# If we enable --path-walk here, we will get a warning overs stderr\n     +-\t# due to incompatibilities with --filter.\n     +-\tGIT_TEST_PACK_PATH_WALK=0 \\\n     + \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n     + \ttest_must_be_empty err &&\n     + \n     +@@ t/t5620-backfill.sh: test_expect_success 'backfill with multiple pathspecs' '\n     + \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n     + \ttest_line_count = 48 missing &&\n     + \n     +-\t# If we enable --path-walk here, we will get a warning overs stderr\n     +-\t# due to incompatibilities with --filter.\n     +-\tGIT_TEST_PACK_PATH_WALK=0 \\\n     + \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n     + \ttest_must_be_empty err &&\n     + \n     +@@ t/t5620-backfill.sh: test_expect_success 'backfill with wildcard pathspec' '\n     + \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n     + \ttest_line_count = 48 missing &&\n     + \n     +-\t# If we enable --path-walk here, we will get a warning overs stderr\n     +-\t# due to incompatibilities with --filter.\n     +-\tGIT_TEST_PACK_PATH_WALK=0 \\\n     + \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n     + \ttest_must_be_empty err &&\n     + \n     +\n       ## t/t6601-path-walk.sh ##\n      @@ t/t6601-path-walk.sh: test_expect_success 'trees are reported exactly once' '\n       \ttest_line_count = 1 out-filtered\n     @@ t/t6601-path-walk.sh: test_expect_success 'trees are reported exactly once' '\n      +\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n     -+\t2:tree::$(git rev-parse topic^{tree})\n     -+\t2:tree::$(git rev-parse base^{tree})\n     -+\t2:tree::$(git rev-parse base~1^{tree})\n     -+\t2:tree::$(git rev-parse base~2^{tree})\n     -+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n     -+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     -+\t3:tree:a/:$(git rev-parse base:a)\n     -+\t4:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     -+\t5:tree:left/:$(git rev-parse base:left)\n     -+\t5:tree:left/:$(git rev-parse base~2:left)\n     -+\t6:tree:right/:$(git rev-parse topic:right)\n     -+\t6:tree:right/:$(git rev-parse base~1:right)\n     -+\t6:tree:right/:$(git rev-parse base~2:right)\n     -+\tblobs:0\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n     ++\t3:tree::$(git rev-parse topic^{tree})\n     ++\t3:tree::$(git rev-parse base^{tree})\n     ++\t3:tree::$(git rev-parse base~1^{tree})\n     ++\t3:tree::$(git rev-parse base~2^{tree})\n     ++\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     ++\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t4:tree:a/:$(git rev-parse base:a)\n     ++\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t6:tree:left/:$(git rev-parse base:left)\n     ++\t6:tree:left/:$(git rev-parse base~2:left)\n     ++\t7:tree:right/:$(git rev-parse topic:right)\n     ++\t7:tree:right/:$(git rev-parse base~1:right)\n     ++\t7:tree:right/:$(git rev-parse base~2:right)\n     ++\tblobs:2\n      +\tcommits:4\n      +\ttags:7\n      +\ttrees:13\n  4:  da4c19af42 =  6:  da191e29e6 backfill: die on incompatible filter options\n  5:  d309345fec !  7:  a1ab70405d path-walk: support blob size limit filter\n     @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n       ignored in the presence of `--path-walk`. Whe `--path-walk` option\n      -supports the `--filter=<spec>` form `blob:none`.\n     -+supports the `--filter=<spec>` form `blob:none` and `blob:limit=<n>`.\n     ++supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n       \n       \n       DELTA ISLANDS\n     @@ path-walk.c\n       #include \"oid-array.h\"\n       #include \"path.h\"\n      @@ path-walk.c: static int walk_path(struct path_walk_context *ctx,\n     - \t/* Evaluate function pointer on this data, if requested. */\n     - \tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n     - \t    (list->type == OBJ_BLOB && ctx->info->blobs) ||\n     + \t/*\n     + \t * Evaluate function pointer on this data, if requested.\n     + \t * Ignore object type filters for tagged objects (path starts\n     +-\t * with `/`).\n     ++\t * with `/`), first for blobs and then other types.\n     + \t */\n     +-\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n     +-\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n      -\t    (list->type == OBJ_TAG && ctx->info->tags))\n     --\t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n     --\t\t\t\t\tctx->info->path_fn_data);\n     -+\t    (list->type == OBJ_TAG && ctx->info->tags)) {\n     -+\t\tstruct oid_array *oids = &list->oids;\n     ++\tif (list->type == OBJ_BLOB &&\n     ++\t    ctx->info->blob_limit &&\n     ++\t    !path_is_for_direct_objects(path)) {\n      +\t\tstruct oid_array filtered = OID_ARRAY_INIT;\n      +\n     -+\t\tif (list->type == OBJ_BLOB && ctx->info->blob_limit) {\n     -+\t\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n     -+\t\t\t\tunsigned long size;\n     ++\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n     ++\t\t\tunsigned long size;\n      +\n     -+\t\t\t\tif (odb_read_object_info(ctx->repo->objects,\n     -+\t\t\t\t\t\t\t &list->oids.oid[i],\n     -+\t\t\t\t\t\t\t &size) != OBJ_BLOB ||\n     -+\t\t\t\t    size < ctx->info->blob_limit)\n     -+\t\t\t\t\toid_array_append(&filtered,\n     -+\t\t\t\t\t\t\t &list->oids.oid[i]);\n     -+\t\t\t}\n     -+\t\t\toids = &filtered;\n     ++\t\t\tif (odb_read_object_info(ctx->repo->objects,\n     ++\t\t\t\t\t\t &list->oids.oid[i],\n     ++\t\t\t\t\t\t &size) != OBJ_BLOB ||\n     ++\t\t\t\tsize < ctx->info->blob_limit)\n     ++\t\t\t\toid_array_append(&filtered,\n     ++\t\t\t\t\t\t &list->oids.oid[i]);\n      +\t\t}\n      +\n     -+\t\tif (oids->nr)\n     -+\t\t\tret = ctx->info->path_fn(path, oids, list->type,\n     ++\t\tif (filtered.nr)\n     ++\t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n      +\t\t\t\t\t\t ctx->info->path_fn_data);\n      +\t\toid_array_clear(&filtered);\n     ++\t} else if (path_is_for_direct_objects(path) ||\n     ++\t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n     ++\t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n     ++\t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n     + \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n     + \t\t\t\t\tctx->info->path_fn_data);\n      +\t}\n       \n       \t/* Expand data for children. */\n     @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \n      +\tcase LOFC_BLOB_LIMIT:\n      +\t\tif (info) {\n     -+\t\t\tif (!options->blob_limit_value) {\n     ++\t\t\tif (!options->blob_limit_value)\n      +\t\t\t\tinfo->blobs = 0;\n     -+\t\t\t} else {\n     ++\t\t\telse\n      +\t\t\t\tinfo->blob_limit = options->blob_limit_value;\n     -+\t\t\t}\n      +\t\t\tlist_objects_filter_release(options);\n      +\t\t}\n      +\t\treturn 1;\n     @@ path-walk.h: struct path_walk_info {\n       \n      +\t/**\n      +\t * If non-zero, specifies a maximum blob size. Blobs with a\n     -+\t * size equal to or greater than this limit will be omitted\n     -+\t * from the walk. Blobs smaller than the limit (or blobs\n     -+\t * whose size cannot be determined) are still visited.\n     ++\t * size equal to or greater than this limit will not be\n     ++\t * emitted unless included in 'pending'.\n      +\t */\n      +\tunsigned long blob_limit;\n      +\n     @@ t/t6601-path-walk.sh: test_expect_success 'topic only, blob:none filter' '\n      +\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n     -+\t2:tree::$(git rev-parse topic^{tree})\n     -+\t2:tree::$(git rev-parse base^{tree})\n     -+\t2:tree::$(git rev-parse base~1^{tree})\n     -+\t2:tree::$(git rev-parse base~2^{tree})\n     -+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n     -+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     -+\t3:tree:a/:$(git rev-parse base:a)\n     -+\t4:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     -+\t5:tree:left/:$(git rev-parse base:left)\n     -+\t5:tree:left/:$(git rev-parse base~2:left)\n     -+\t6:tree:right/:$(git rev-parse topic:right)\n     -+\t6:tree:right/:$(git rev-parse base~1:right)\n     -+\t6:tree:right/:$(git rev-parse base~2:right)\n     -+\tblobs:0\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n     ++\t3:tree::$(git rev-parse topic^{tree})\n     ++\t3:tree::$(git rev-parse base^{tree})\n     ++\t3:tree::$(git rev-parse base~1^{tree})\n     ++\t3:tree::$(git rev-parse base~2^{tree})\n     ++\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     ++\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t4:tree:a/:$(git rev-parse base:a)\n     ++\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t6:tree:left/:$(git rev-parse base:left)\n     ++\t6:tree:left/:$(git rev-parse base~2:left)\n     ++\t7:tree:right/:$(git rev-parse topic:right)\n     ++\t7:tree:right/:$(git rev-parse base~1:right)\n     ++\t7:tree:right/:$(git rev-parse base~2:right)\n     ++\tblobs:2\n      +\tcommits:4\n      +\ttags:7\n      +\ttrees:13\n     @@ t/t6601-path-walk.sh: test_expect_success 'topic only, blob:none filter' '\n      +\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n     -+\t2:tree::$(git rev-parse topic^{tree})\n     -+\t2:tree::$(git rev-parse base^{tree})\n     -+\t2:tree::$(git rev-parse base~1^{tree})\n     -+\t2:tree::$(git rev-parse base~2^{tree})\n     -+\t2:tree::$(git rev-parse refs/tags/tree-tag^{})\n     -+\t2:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     -+\t3:blob:a:$(git rev-parse base~2:a)\n     -+\t4:tree:a/:$(git rev-parse base:a)\n     -+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     -+\t6:tree:left/:$(git rev-parse base:left)\n     -+\t6:tree:left/:$(git rev-parse base~2:left)\n     -+\t7:blob:left/b:$(git rev-parse base~2:left/b)\n     -+\t8:tree:right/:$(git rev-parse topic:right)\n     -+\t8:tree:right/:$(git rev-parse base~1:right)\n     -+\t8:tree:right/:$(git rev-parse base~2:right)\n     -+\t9:blob:right/c:$(git rev-parse base~2:right/c)\n     -+\t10:blob:right/d:$(git rev-parse base~1:right/d)\n     -+\tblobs:4\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n     ++\t3:tree::$(git rev-parse topic^{tree})\n     ++\t3:tree::$(git rev-parse base^{tree})\n     ++\t3:tree::$(git rev-parse base~1^{tree})\n     ++\t3:tree::$(git rev-parse base~2^{tree})\n     ++\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     ++\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t4:blob:a:$(git rev-parse base~2:a)\n     ++\t5:tree:a/:$(git rev-parse base:a)\n     ++\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t7:tree:left/:$(git rev-parse base:left)\n     ++\t7:tree:left/:$(git rev-parse base~2:left)\n     ++\t8:blob:left/b:$(git rev-parse base~2:left/b)\n     ++\t9:tree:right/:$(git rev-parse topic:right)\n     ++\t9:tree:right/:$(git rev-parse base~1:right)\n     ++\t9:tree:right/:$(git rev-parse base~2:right)\n     ++\t10:blob:right/c:$(git rev-parse base~2:right/c)\n     ++\t11:blob:right/d:$(git rev-parse base~1:right/d)\n     ++\tblobs:6\n      +\tcommits:4\n      +\ttags:7\n      +\ttrees:13\n  6:  55ed02772b =  8:  2360a5be81 path-walk: add pl_sparse_trees to control tree pruning\n  7:  dcb1a29d3a !  9:  d9f5a98830 pack-objects: support sparse:oid filter with path-walk\n     @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1\n       +\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n       ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     --supports the `--filter=<spec>` form `blob:none` and `blob:limit=<n>`.\n     -+supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`, and\n     +-supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n     ++supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n      +`sparse:<oid>`.\n       \n       \n       DELTA ISLANDS\n      \n       ## builtin/pack-objects.c ##\n     +@@ builtin/pack-objects.c: static int add_objects_by_path(const char *path,\n     + \treturn 0;\n     + }\n     + \n     +-static void get_object_list_path_walk(struct rev_info *revs)\n     ++static int get_object_list_path_walk(struct rev_info *revs)\n     + {\n     + \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n     + \tunsigned int processed = 0;\n      @@ builtin/pack-objects.c: static void get_object_list_path_walk(struct rev_info *revs)\n       \tresult = walk_objects_by_path(&info);\n       \ttrace2_region_leave(\"pack-objects\", \"path-walk\", revs->repo);\n       \n     +-\tif (result)\n     +-\t\tdie(_(\"failed to pack objects via path-walk\"));\n      +\tpath_walk_info_clear(&info);\n      +\n     - \tif (result)\n     - \t\tdie(_(\"failed to pack objects via path-walk\"));\n     ++\treturn result;\n       }\n     + \n     + static void get_object_list(struct rev_info *revs, struct strvec *argv)\n     +@@ builtin/pack-objects.c: static void get_object_list(struct rev_info *revs, struct strvec *argv)\n     + \t\tfn_show_object = show_object;\n     + \n     + \tif (path_walk) {\n     +-\t\tget_object_list_path_walk(revs);\n     +-\t} else {\n     ++\t\tif (get_object_list_path_walk(revs)) {\n     ++\t\t\twarning(_(\"failed to pack objects via path-walk\"));\n     ++\t\t\tpath_walk = 0;\n     ++\t\t}\n     ++\t}\n     ++\n     ++\tif (!path_walk) {\n     + \t\tif (prepare_revision_walk(revs))\n     + \t\t\tdie(_(\"revision walk setup failed\"));\n     + \t\tmark_edges_uninteresting(revs, show_edge, sparse);\n      \n       ## path-walk.c ##\n      @@\n  8:  772cdcaeed ! 10:  c9efff0ea8 path-walk: support `tree:0` filter\n     @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1\n       +\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n       ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     --supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`, and\n     +-supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n      -`sparse:<oid>`.\n     -+supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`,\n     ++supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n      +`tree:0`, and `sparse:<oid>`.\n       \n       \n       DELTA ISLANDS\n      \n       ## path-walk.c ##\n     +@@ path-walk.c: static int walk_path(struct path_walk_context *ctx,\n     + \t\t\t\t\tctx->info->path_fn_data);\n     + \t}\n     + \n     +-\t/* Expand data for children. */\n     +-\tif (list->type == OBJ_TREE) {\n     ++\t/* Expand data for children, unless this is a direct-object path. */\n     ++\tif (list->type == OBJ_TREE && !path_is_for_direct_objects(path)) {\n     + \t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n     + \t\t\tret |= add_tree_entries(ctx,\n     + \t\t\t\t\t    path,\n     +@@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n     + {\n     + \tstruct type_and_oid_list *tags = NULL;\n     + \tstruct type_and_oid_list *tagged_blobs = NULL;\n     ++\tstruct type_and_oid_list *tagged_trees = NULL;\n     + \tstruct type_and_oid_list *root_tree_list = NULL;\n     + \n     + \tif (info->tags)\n     + \t\tCALLOC_ARRAY(tags, 1);\n     + \tCALLOC_ARRAY(tagged_blobs, 1);\n     ++\tif (!info->trees)\n     ++\t\tCALLOC_ARRAY(tagged_trees, 1);\n     + \troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n     + \n     + \t/*\n     +@@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n     + \n     + \t\tswitch (obj->type) {\n     + \t\tcase OBJ_TREE:\n     +-\t\t\tif (pending->path) {\n     ++\t\t\tif (tagged_trees) {\n     ++\t\t\t\t/*\n     ++\t\t\t\t * Trees are disabled but pending trees\n     ++\t\t\t\t * should still be emitted. Collect them\n     ++\t\t\t\t * into a \"/tagged-trees\" list that\n     ++\t\t\t\t * bypasses the object type filter.\n     ++\t\t\t\t */\n     ++\t\t\t\toid_array_append(&tagged_trees->oids, &obj->oid);\n     ++\t\t\t} else if (pending->path) {\n     + \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n     + \t\t\t\t\t\t\t    : xstrdup(\"\");\n     + \t\t\t\tadd_path_to_list(ctx, path, OBJ_TREE, &obj->oid, 1);\n     +@@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n     + \t\t\tfree(tagged_blobs);\n     + \t\t}\n     + \t}\n     ++\tif (tagged_trees) {\n     ++\t\tif (tagged_trees->oids.nr) {\n     ++\t\t\tconst char *tagged_tree_path = \"/tagged-trees\";\n     ++\t\t\ttagged_trees->type = OBJ_TREE;\n     ++\t\t\ttagged_trees->maybe_interesting = 1;\n     ++\t\t\tstrmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);\n     ++\t\t\tpush_to_stack(ctx, tagged_tree_path);\n     ++\t\t} else {\n     ++\t\t\toid_array_clear(&tagged_trees->oids);\n     ++\t\t\tfree(tagged_trees);\n     ++\t\t}\n     ++\t}\n     + \tif (tags) {\n     + \t\tif (tags->oids.nr) {\n     + \t\t\tconst char *tag_path = \"/tags\";\n      @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\t}\n       \t\treturn 1;\n     @@ t/t6601-path-walk.sh: test_expect_success 'all, blob:limit=3 filter' '\n      +\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n     -+\tblobs:0\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n     ++\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n     ++\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n     ++\tblobs:2\n      +\tcommits:4\n      +\ttags:7\n     -+\ttrees:0\n     ++\ttrees:2\n      +\tEOF\n      +\n      +\ttest_cmp_sorted expect out\n  9:  c557f3c59d ! 11:  b221ea4712 path-walk: support `object:type` filter\n     @@ Documentation/git-pack-objects.adoc\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n       ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     - supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`,\n     + supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n      -`tree:0`, and `sparse:<oid>`.\n      +`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n       \n     @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1\n       DELTA ISLANDS\n      \n       ## path-walk.c ##\n     -@@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n     - \t\tCALLOC_ARRAY(tags, 1);\n     - \tif (info->blobs)\n     - \t\tCALLOC_ARRAY(tagged_blobs, 1);\n     --\tif (info->trees)\n     -+\tif (info->trees || info->blobs)\n     - \t\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n     - \n     - \t/*\n     -@@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n     - \n     - \t\tswitch (obj->type) {\n     - \t\tcase OBJ_TREE:\n     --\t\t\tif (!info->trees)\n     -+\t\t\tif (!info->trees && !info->blobs)\n     - \t\t\t\tcontinue;\n     - \t\t\tif (pending->path) {\n     - \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n     +@@ path-walk.c: static int walk_path(struct path_walk_context *ctx,\n     + \t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n     + \t\t\t\t\t\t ctx->info->path_fn_data);\n     + \t\toid_array_clear(&filtered);\n     +-\t} else if (path_is_for_direct_objects(path) ||\n     ++\t} else if ((!ctx->info->strict_types && path_is_for_direct_objects(path)) ||\n     + \t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n     + \t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n     + \t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n      @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\t}\n       \t\treturn 1;\n     @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n      +\t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n      +\t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n      +\t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n     ++\t\t\tinfo->strict_types = 1;\n      +\t\t\tlist_objects_filter_release(options);\n      +\t\t}\n      +\t\treturn 1;\n     @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \tcase LOFC_SPARSE_OID:\n       \t\tif (info) {\n       \t\t\tstruct object_id sparse_oid;\n     -@@ path-walk.c: int walk_objects_by_path(struct path_walk_info *info)\n     - \t/*\n     - \t * Set these values before preparing the walk to catch\n     - \t * lightweight tags pointing to non-commits and indexed objects.\n     -+\t *\n     -+\t * Keep tree_objects set whenever blobs are wanted: blobs may\n     -+\t * be reachable through trees that show up as pending objects\n     -+\t * (e.g., via lightweight tags pointing to trees, or annotated\n     -+\t * tags whose peeled target is a tree). Without tree_objects,\n     -+\t * prepare_revision_walk() would discard those pending trees\n     -+\t * and we would never descend into them.\n     - \t */\n     - \tinfo->revs->blob_objects = info->blobs;\n     --\tinfo->revs->tree_objects = info->trees;\n     -+\tinfo->revs->tree_objects = info->trees || info->blobs;\n     +\n     + ## path-walk.h ##\n     +@@ path-walk.h: struct path_walk_info {\n     + \tint blobs;\n     + \tint tags;\n       \n     - \tif (prepare_revision_walk(info->revs))\n     - \t\tdie(_(\"failed to setup revision walk\"));\n     ++\t/**\n     ++\t * If 'strict_types' is 0, then direct object requests will no longer\n     ++\t * override the object type restrictions.\n     ++\t */\n     ++\tint strict_types;\n     ++\n     + \t/**\n     + \t * If non-zero, specifies a maximum blob size. Blobs with a\n     + \t * size equal to or greater than this limit will not be\n      \n       ## t/t6601-path-walk.sh ##\n      @@ t/t6601-path-walk.sh: test_expect_success 'tree:1 filter is rejected' '\n     @@ t/t6601-path-walk.sh: test_expect_success 'tree:1 filter is rejected' '\n      +\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n      +\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n      +\t1:blob:a:$(git rev-parse base~2:a)\n     -+\t2:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n     -+\t3:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n     -+\t4:blob:left/b:$(git rev-parse base:left/b)\n     -+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n     -+\t5:blob:right/c:$(git rev-parse base~2:right/c)\n     -+\t5:blob:right/c:$(git rev-parse topic:right/c)\n     -+\t6:blob:right/d:$(git rev-parse base~1:right/d)\n     -+\tblobs:10\n     ++\t2:blob:left/b:$(git rev-parse base:left/b)\n     ++\t2:blob:left/b:$(git rev-parse base~2:left/b)\n     ++\t3:blob:right/c:$(git rev-parse base~2:right/c)\n     ++\t3:blob:right/c:$(git rev-parse topic:right/c)\n     ++\t4:blob:right/d:$(git rev-parse base~1:right/d)\n     ++\tblobs:8\n      +\tcommits:0\n      +\ttags:0\n      +\ttrees:0\n 10:  5423273edf ! 12:  16bd3c3161 path-walk: support `combine` filter\n     @@ Commit message\n             the standalone `sparse:oid` behavior.\n      \n          Implementation-wise, the existing `prepare_filters()` called\n     -    `list_objects_filter_release()` inside each case branch. That works fine\n     -    for top-level filters, but `combine` filters need to recurse over its\n     -      child filters without releasing each one in turn (since the parent's\n     -      release iterates the sub array). Split `prepare_filters()` into a\n     -      recursive helper that performs only the mutation, plus a thin wrapper\n     -      that calls the helper and then releases the top-level filter once.\n     +    `list_objects_filter_release()` inside each case branch. That works fine for\n     +    top-level filters, but `combine` filters need to recurse over its child\n     +    filters without releasing each one in turn (since the parent's release\n     +    iterates the sub array). Split `prepare_filters()` into a recursive helper\n     +    that performs only the mutation, plus a thin wrapper that calls the helper\n     +    and then releases the top-level filter once.\n      \n          The `LOFC_COMBINE` case in the helper just walks `sub_nr` and recurses;\n          child filters are released by the wrapper's single\n     -    `list_objects_filter_release()` call on the parent (which itself\n     -    recursively releases each sub-filter, the same way it always has).\n     +    `list_objects_filter_release()` call on the parent (which itself recursively\n     +    releases each sub-filter, the same way it always has).\n      \n          If any sub-filter is unsupported (e.g. \"tree:1\", \"sparse:<path>\", or a\n          not-yet-supported choice), the recursion bubbles a failure up and the\n     @@ Documentation/git-pack-objects.adoc\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n       ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     - supports the `--filter=<spec>` form `blob:none`, `blob:limit=<n>`,\n     + supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n      -`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n      +`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter\n      +types can be combined with the `combine:<spec>+<spec>` form.\n     @@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n       {\n       \tswitch (options->choice) {\n       \tcase LOFC_DISABLED:\n     - \t\treturn 1;\n     - \n     - \tcase LOFC_BLOB_NONE:\n     --\t\tif (info) {\n     -+\t\tif (info)\n     - \t\t\tinfo->blobs = 0;\n     --\t\t\tlist_objects_filter_release(options);\n     --\t\t}\n     - \t\treturn 1;\n     - \n     - \tcase LOFC_BLOB_LIMIT:\n     +@@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\tif (info) {\n     - \t\t\tif (!options->blob_limit_value) {\n     + \t\t\tif (!options->blob_limit_value)\n       \t\t\t\tinfo->blobs = 0;\n     --\t\t\t} else {\n     -+\t\t\t} else if (!info->blob_limit ||\n     -+\t\t\t\t   options->blob_limit_value < info->blob_limit) {\n     +-\t\t\telse\n     ++\t\t\telse if (!info->blob_limit ||\n     ++\t\t\t\t info->blob_limit > options->blob_limit_value)\n       \t\t\t\tinfo->blob_limit = options->blob_limit_value;\n     - \t\t\t}\n     --\t\t\tlist_objects_filter_release(options);\n     + \t\t\tlist_objects_filter_release(options);\n       \t\t}\n     - \t\treturn 1;\n     - \n      @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\tif (info) {\n       \t\t\tinfo->trees = 0;\n     @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\t}\n       \t\treturn 1;\n       \n     -@@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n     - \t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n     - \t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n     - \t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n     --\t\t\tlist_objects_filter_release(options);\n     - \t\t}\n     - \t\treturn 1;\n     - \n      @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n       \t\t\t\treturn 0;\n     @@ t/t6601-path-walk.sh: test_expect_success 'all, object:type=blob filter' '\n      +\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n      +\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n     -+\tblobs:0\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n     ++\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n     ++\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n     ++\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n     ++\tblobs:2\n      +\tcommits:4\n      +\ttags:7\n     -+\ttrees:0\n     ++\ttrees:2\n      +\tEOF\n      +\n      +\ttest_cmp_sorted expect out\n     @@ t/t6601-path-walk.sh: test_expect_success 'all, object:type=blob filter' '\n      +\t\t-- --all >out &&\n      +\n      +\tcat >expect <<-EOF &&\n     -+\t0:blob:a:$(git rev-parse base~2:a)\n     -+\t1:blob:left/b:$(git rev-parse base~2:left/b)\n     -+\t2:blob:right/c:$(git rev-parse base~2:right/c)\n     -+\t3:blob:right/d:$(git rev-parse base~1:right/d)\n     -+\tblobs:4\n     ++\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n     ++\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n     ++\t1:blob:a:$(git rev-parse base~2:a)\n     ++\t2:blob:left/b:$(git rev-parse base~2:left/b)\n     ++\t3:blob:right/c:$(git rev-parse base~2:right/c)\n     ++\t4:blob:right/d:$(git rev-parse base~1:right/d)\n     ++\tblobs:6\n      +\tcommits:0\n      +\ttags:0\n      +\ttrees:0\n\n-- \ngitgitgadget\n"},{"id":"543069","messageId":"0840110116a967d39d8b63dbfe822ea9b2500088.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 01/12] t5620: make test work with path-walk var","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:12:58Z","receivedAt":"2026-05-11T18:13:13Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe GIT_TEST_PACK_PATH_WALK test variable allows enabling the\n--path-walk option to 'git pack-objects' by default. This sometimes\nengages the warning that --path-walk is incompatible with the --filter\noption. These tests in t5620 fail due to this warning over stderr in\nthis case. Disable this variable for this moment until these options\nwork together.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t5620-backfill.sh | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..e174290787 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -298,6 +298,9 @@ test_expect_success 'backfill with prefix pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -315,6 +318,9 @@ test_expect_success 'backfill with multiple pathspecs' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -332,6 +338,9 @@ test_expect_success 'backfill with wildcard pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n \ttest_must_be_empty err &&\n \n-- \ngitgitgadget\n\n"},{"id":"543071","messageId":"5cc63833800d12b09ebce882ad3e25f61c9c1940.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 02/12] pack-objects: pass --objects with --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:12:59Z","receivedAt":"2026-05-11T18:13:15Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen 'git pack-objects' has the --path-walk option enabled, it uses a\ndifferent set of revision walk parameters than normal. For once,\n--objects was previously assumed by the path-walk API and was not needed\nto be added. We also needed --boundary to allow discovering\nUNINTERESTING objects to use as delta bases.\n\nWe will be updating the path-walk API soon to work with some filter\noptions. However, the revision machinery will trigger a fatal error:\n\n  fatal: object filtering requires --objects\n\nThe fix is easy: add the --objects option as an argument. This has no\neffect on the path-walk API but does simplify the revision option\nparsing for the objects filter.\n\nWe can remove the comment about \"removing\" the options because they were\nnever removed and instead not added. We still need to disable using\nbitmaps.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex dd2480a73d..4338962904 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5190,10 +5190,7 @@ int cmd_pack_objects(int argc,\n \t}\n \tif (path_walk) {\n \t\tstrvec_push(&rp, \"--boundary\");\n-\t\t /*\n-\t\t  * We must disable the bitmaps because we are removing\n-\t\t  * the --objects / --objects-edge[-aggressive] options.\n-\t\t  */\n+\t\tstrvec_push(&rp, \"--objects\");\n \t\tuse_bitmap_index = 0;\n \t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n-- \ngitgitgadget\n\n"},{"id":"543072","messageId":"77329cf8f45b948f3986d004be72035c75ce40da.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 03/12] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:00Z","receivedAt":"2026-05-11T18:13:16Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd p5315-pack-objects-filter.sh to measure the performance of\n'git pack-objects --revs --all' under different filter and traversal\ncombinations:\n\n * no filter (baseline)\n * --filter=blob:none (blobless)\n * --filter=sparse:oid=<oid> (cone-mode sparse)\n\nEach filter scenario is tested both with and without --path-walk,\nproducing paired measurements that show the impact of the path-walk\ntraversal for each filter type as we integrate the --path-walk feature\nwith different --filter options. It currently has no integration so\nfalls back to the standard revision walk. Thus, there are no significant\ndifferences in the current results other than a full repack (and even\nthen, the --path-walk feature is not incredibly different for the\ndefault Git repository):\n\nTest                                             HEAD\n-----------------------------------------------------\n5315.2: repack (no filter)                      27.91\n5315.3: repack size (no filter)                250.7M\n5315.4: repack (no filter, --path-walk)         34.92\n5315.5: repack size (no filter, --path-walk)   220.0M\n5315.6: repack (blob:none)                      13.63\n5315.7: repack size (blob:none)                137.6M\n5315.8: repack (blob:none, --path-walk)         13.48\n5315.9: repack size (blob:none, --path-walk)   137.7M\n5315.10: repack (sparse:oid)                    72.67\n5315.11: repack size (sparse:oid)              187.4M\n5315.12: repack (sparse:oid, --path-walk)       72.47\n5315.13: repack size (sparse:oid, --path-walk) 187.4M\n\nThe sparse filter definition is built automatically by sampling\ndepth-2 directories from the test repository, making the test work\non any repo passed via GIT_PERF_LARGE_REPO. For repos that lack\ndepth-2 directories, a single top-level directory is used; for flat\nrepos, the sparse tests are skipped via prerequisite.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5315-pack-objects-filter.sh | 129 ++++++++++++++++++++++++++++\n 1 file changed, 129 insertions(+)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\ndiff --git a/t/perf/p5315-pack-objects-filter.sh b/t/perf/p5315-pack-objects-filter.sh\nnew file mode 100755\nindex 0000000000..b009039c89\n--- /dev/null\n+++ b/t/perf/p5315-pack-objects-filter.sh\n@@ -0,0 +1,129 @@\n+#!/bin/sh\n+\n+test_description='Tests pack-objects performance with filters and --path-walk'\n+. ./perf-lib.sh\n+\n+test_perf_large_repo\n+\n+test_expect_success 'setup filter inputs' '\n+\t# Sample a few depth-2 directories from the test repo to build\n+\t# a cone-mode sparse-checkout definition.  The sampling picks\n+\t# directories at evenly-spaced positions so the choice is stable\n+\t# and scales to repos of any shape.\n+\n+\tgit ls-tree -d --name-only HEAD >top-dirs &&\n+\ttop_nr=$(wc -l <top-dirs) &&\n+\n+\t>depth2-dirs &&\n+\twhile read tdir\n+\tdo\n+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null |\n+\t\tsed \"s|^|$tdir/|\" >>depth2-dirs || return 1\n+\tdone <top-dirs &&\n+\n+\td2_nr=$(wc -l <depth2-dirs) &&\n+\n+\tif test \"$d2_nr\" -ge 2\n+\tthen\n+\t\t# Pick two directories from evenly-spaced positions.\n+\t\tfirst=$(sed -n \"1p\" depth2-dirs) &&\n+\t\tmid=$(sed -n \"$((d2_nr / 2 + 1))p\" depth2-dirs) &&\n+\n+\t\tp1=$(dirname \"$first\") &&\n+\t\tp2=$(dirname \"$mid\") &&\n+\n+\t\t# Build cone-mode sparse-checkout patterns.\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$p1/\" &&\n+\t\t\techo \"!/$p1/*/\" &&\n+\t\t\tif test \"$p1\" != \"$p2\"\n+\t\t\tthen\n+\t\t\t\techo \"/$p2/\" &&\n+\t\t\t\techo \"!/$p2/*/\"\n+\t\t\tfi &&\n+\t\t\techo \"/$first/\" &&\n+\t\t\tif test \"$first\" != \"$mid\"\n+\t\t\tthen\n+\t\t\t\techo \"/$mid/\"\n+\t\t\tfi\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first $mid\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\telif test \"$top_nr\" -ge 1\n+\tthen\n+\t\t# Fallback: use a single top-level directory.\n+\t\tfirst=$(sed -n \"1p\" top-dirs) &&\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$first/\"\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\tfi\n+'\n+\n+test_perf 'repack (no filter)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (no filter, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"543073","messageId":"50933cccf508f71f47d955d5aceacad0db612fc5.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 04/12] path-walk: always emit directly-requested objects","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:01Z","receivedAt":"2026-05-11T18:13:18Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWe are preparing to integrate the path-walk API with some --filter options\nin 'git pack-objects', but there is a subtle issue that is revealed when\nthose are put together and the test suite is run with\nGIT_TEST_PACK_PATH_WALK=1.\n\nWhen a filter reduces the set of requested objects, this results in\nfiltering out directly-requested objects, such as in the download of needed\nblobs in a blobless partial clone.\n\nThe root cause is that the scan of pending objects in the path-walk API\nrespects the filters set in the path_walk_info instead of overriding them\nfor pending objects.\n\nWe can tell that a path is part of the directly-referenced objects if its\npath name starts with '/' (other paths, including root trees never have this\nstarting character). Create a path_is_for_direct_objects() to make this\nmeaning clear, especially as we add more references in the future as we\nintegrate the path-walk API with partial clone filter options.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.adoc |  7 ++++\n path-walk.c                                | 41 ++++++++++++++--------\n path-walk.h                                |  5 +++\n 3 files changed, 38 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.adoc b/Documentation/technical/api-path-walk.adoc\nindex a67de1b143..6e17b13d61 100644\n--- a/Documentation/technical/api-path-walk.adoc\n+++ b/Documentation/technical/api-path-walk.adoc\n@@ -48,6 +48,13 @@ commits.\n \tapplications could disable some options to make it simpler to walk\n \tthe objects or to have fewer calls to `path_fn`.\n +\n+Note that objects directly requested as pending objects (such as targets\n+of lightweight tags or other ref tips) are always emitted to `path_fn`,\n+even when the corresponding type flag is disabled. Only objects\n+discovered during the tree walk are subject to these type filters. This\n+ensures that objects specifically requested through the revision input\n+are never silently dropped.\n++\n While it is possible to walk only commits in this way, consumers would be\n better off using the revision walk API instead.\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 6e426af433..59a7670c5b 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -248,6 +248,16 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \treturn 0;\n }\n \n+/*\n+ * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n+ * were directly requested by 'pending' objects rather than discovered during\n+ * tree traversal.\n+ */\n+static int path_is_for_direct_objects(const char *path)\n+{\n+\treturn path[0] == '/';\n+}\n+\n /*\n  * For each path in paths_to_explore, walk the trees another level\n  * and add any found blobs to the batch (but only if they exist and\n@@ -306,14 +316,19 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tif (list->type == OBJ_BLOB &&\n \t    ctx->revs->prune_data.nr &&\n+\t    !path_is_for_direct_objects(path) &&\n \t    !match_pathspec(ctx->repo->index, &ctx->revs->prune_data,\n \t\t\t   path, strlen(path), 0,\n \t\t\t   NULL, 0))\n \t\treturn 0;\n \n-\t/* Evaluate function pointer on this data, if requested. */\n-\tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n-\t    (list->type == OBJ_BLOB && ctx->info->blobs) ||\n+\t/*\n+\t * Evaluate function pointer on this data, if requested.\n+\t * Ignore object type filters for tagged objects (path starts\n+\t * with `/`).\n+\t */\n+\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n+\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n \t    (list->type == OBJ_TAG && ctx->info->tags))\n \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n \t\t\t\t\tctx->info->path_fn_data);\n@@ -374,10 +389,8 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \tif (info->tags)\n \t\tCALLOC_ARRAY(tags, 1);\n-\tif (info->blobs)\n-\t\tCALLOC_ARRAY(tagged_blobs, 1);\n-\tif (info->trees)\n-\t\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n+\tCALLOC_ARRAY(tagged_blobs, 1);\n+\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n \n \t/*\n \t * Pending objects include:\n@@ -421,8 +434,6 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (!info->trees)\n-\t\t\t\tcontinue;\n \t\t\tif (pending->path) {\n \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n \t\t\t\t\t\t\t    : xstrdup(\"\");\n@@ -435,8 +446,6 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\t\tbreak;\n \n \t\tcase OBJ_BLOB:\n-\t\t\tif (!info->blobs)\n-\t\t\t\tcontinue;\n \t\t\tif (pending->path)\n \t\t\t\tadd_path_to_list(ctx, pending->path, OBJ_BLOB, &obj->oid, 1);\n \t\t\telse\n@@ -532,15 +541,17 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tpush_to_stack(&ctx, root_path);\n \n \t/*\n-\t * Set these values before preparing the walk to catch\n-\t * lightweight tags pointing to non-commits and indexed objects.\n+\t * Ensure that prepare_revision_walk() keeps all pending objects\n+\t * even through an object type filter.\n \t */\n-\tinfo->revs->blob_objects = info->blobs;\n-\tinfo->revs->tree_objects = info->trees;\n+\tinfo->revs->blob_objects = info->revs->tree_objects = 1;\n \n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n+\tinfo->revs->blob_objects = info->blobs;\n+\tinfo->revs->tree_objects = info->trees;\n+\n \t/*\n \t * Walk trees to mark them as UNINTERESTING.\n \t * This is particularly important when 'edge_aggressive' is set.\ndiff --git a/path-walk.h b/path-walk.h\nindex 5ef5a8440e..657eeda8ec 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -36,6 +36,11 @@ struct path_walk_info {\n \t/**\n \t * Initialize which object types the path_fn should be called on. This\n \t * could also limit the walk to skip blobs if not set.\n+\t *\n+\t * Note: even when 'blobs' or 'trees' is disabled, objects that are\n+\t * directly requested as pending objects will still be emitted to\n+\t * path_fn. Only objects discovered during the tree walk are filtered by\n+\t * these flags.\n \t */\n \tint commits;\n \tint trees;\n-- \ngitgitgadget\n\n"},{"id":"543074","messageId":"b2deb7f81939d6ed03f0ea5c8be5ec515c1c99ab.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 05/12] path-walk: support blobless filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:02Z","receivedAt":"2026-05-11T18:13:19Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git pack-objects' command can opt-in to using the path-walk API for\nscanning the objects. Currently, this option is dynamically disabled if\ncombined with '--filter=<X>', even when using a simple filter such as\n'blob:none' to signal a blobless packfile. This is a common scenario for\nrepos at scale, so is worth integrating.\n\nAlso, users can opt-in to the '--path-walk' option by default through\nthe pack.usePathWalk=true config option. When using that in a blobless\npartial clone, the following warning can appear even though the user did\nnot specify either option directly:\n\n  warning: cannot use --filter with --path-walk\n\nTeach the path-walk API to handle the 'blob:none' object filter\nnatively. When revs->filter.choice is LOFC_BLOB_NONE, the path-walk\nsets info->blobs to 0 (skipping all blob objects) and clears the\nfilter from revs so that prepare_revision_walk() does not reject the\nconfiguration.\n\nThis check is implemented in the static prepare_filters() method, which\nwill simultaneously check if the input filters are compatible and will\nmake the appropriate mutations to the path_walk_info and filters if the\npath_walk_info is non-NULL. This allows us to use this logic both in the\nAPI method path_walk_filter_compatible() for use in\nbuiltin/pack-objects.c and as a prep step in walk_objects_by_path().\n\nUpdate the test helper (test-path-walk) to accept --filter=<spec>\nas a test-tool option (before '--'), applying it to revs after\nsetup_revisions() to avoid the --objects requirement check. We can also\nrevert recent GIT_TEST_PACK_PATH_WALK overrides in t5620.\n\nAlso switch test-path-walk from REV_INFO_INIT with manual repo\nassignment to repo_init_revisions(), which properly initializes\nthe filter_spec strbuf needed for filter parsing.\n\nAdd tests for blob:none with --all and with a single branch.\n\nThe performance test p5315 shows the impact of this change when using\nblobless filters:\n\nTest                                           HEAD~1     HEAD\n---------------------------------------------------------------------\n5315.6: repack (blob:none)                      13.53   13.87  +2.5%\n5315.7: repack size (blob:none)                137.7M  137.8M  +0.1%\n5315.8: repack (blob:none, --path-walk)         13.51   23.43 +73.4%\n5315.9: repack size (blob:none, --path-walk)   137.7M  115.2M -16.3%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (16% smaller for blobless packs)\nat the cost of increased computation time due to the two compression\npasses. This data demonstrates that the feature is engaged and provides\nreal compression benefits when --no-reuse-delta forces fresh deltas.\n\nCo-Authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  6 +--\n builtin/pack-objects.c              |  2 +-\n path-walk.c                         | 30 ++++++++++++++\n path-walk.h                         |  7 ++++\n t/helper/test-path-walk.c           | 11 ++++-\n t/t5620-backfill.sh                 |  9 -----\n t/t6601-path-walk.sh                | 62 +++++++++++++++++++++++++++++\n 7 files changed, 113 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex b78175fbe1..917045d5c3 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -402,9 +402,9 @@ will be automatically changed to version `1`.\n \tof filenames that cause collisions in Git's default name-hash\n \talgorithm.\n +\n-Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n-`--use-bitmap-index` option will be ignored in the presence of\n-`--path-walk.`\n+Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n+ignored in the presence of `--path-walk`. Whe `--path-walk` option\n+supports the `--filter=<spec>` form `blob:none`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 4338962904..bc9fb5b457 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5177,7 +5177,7 @@ int cmd_pack_objects(int argc,\n \n \tif (path_walk) {\n \t\tconst char *option = NULL;\n-\t\tif (filter_options.choice)\n+\t\tif (!path_walk_filter_compatible(&filter_options))\n \t\t\toption = \"--filter\";\n \t\telse if (use_delta_islands)\n \t\t\toption = \"--delta-islands\";\ndiff --git a/path-walk.c b/path-walk.c\nindex 59a7670c5b..cc587268e7 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -9,6 +9,7 @@\n #include \"hashmap.h\"\n #include \"hex.h\"\n #include \"list-objects.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -494,6 +495,32 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tswitch (options->choice) {\n+\tcase LOFC_DISABLED:\n+\t\treturn 1;\n+\n+\tcase LOFC_BLOB_NONE:\n+\t\tif (info) {\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n+\tdefault:\n+\t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n+\t\t      list_objects_filter_spec(options));\n+\t\treturn 0;\n+\t}\n+}\n+\n+int path_walk_filter_compatible(struct list_objects_filter_options *options)\n+{\n+\treturn prepare_filters(NULL, options);\n+}\n+\n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\n  * call 'info->path_fn' on each discovered path.\n@@ -521,6 +548,9 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tif (!prepare_filters(info, &info->revs->filter))\n+\t\treturn -1;\n+\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 657eeda8ec..a1736ecb2b 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -90,3 +90,10 @@ void path_walk_info_clear(struct path_walk_info *info);\n  * Returns nonzero on an error.\n  */\n int walk_objects_by_path(struct path_walk_info *info);\n+\n+struct list_objects_filter_options;\n+/**\n+ * Given a set of options for filtering objects, return 1 if the options\n+ * are compatible with the path-walk API and 0 otherwise.\n+ */\n+int path_walk_filter_compatible(struct list_objects_filter_options *options);\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex fe63002c2b..88f86ae0dc 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -4,6 +4,7 @@\n #include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object-name.h\"\n #include \"object.h\"\n #include \"pretty.h\"\n@@ -71,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n+\tstruct list_objects_filter_options filter_options =\n+\t\tLIST_OBJECTS_FILTER_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n@@ -86,11 +89,12 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n \n \tsetup_git_directory();\n-\trevs.repo = the_repository;\n+\trepo_init_revisions(the_repository, &revs, NULL);\n \n \targc = parse_options(argc, argv, NULL,\n \t\t\t     options, path_walk_usage,\n@@ -101,6 +105,10 @@ int cmd__path_walk(int argc, const char **argv)\n \telse\n \t\tusage(path_walk_usage[0]);\n \n+\t/* Apply the filter after setup_revisions to avoid the --objects check. */\n+\tif (filter_options.choice)\n+\t\tlist_objects_filter_copy(&revs.filter, &filter_options);\n+\n \tinfo.revs = &revs;\n \tinfo.path_fn = emit_block;\n \tinfo.path_fn_data = &data;\n@@ -129,6 +137,7 @@ int cmd__path_walk(int argc, const char **argv)\n \t\tfree(info.pl);\n \t}\n \n+\tlist_objects_filter_release(&filter_options);\n \trelease_revisions(&revs);\n \treturn res;\n }\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex e174290787..94f35ce190 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -298,9 +298,6 @@ test_expect_success 'backfill with prefix pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -318,9 +315,6 @@ test_expect_success 'backfill with multiple pathspecs' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -338,9 +332,6 @@ test_expect_success 'backfill with wildcard pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n \ttest_must_be_empty err &&\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 56bd1e3c5b..b0ee31ee2d 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -415,4 +415,66 @@ test_expect_success 'trees are reported exactly once' '\n \ttest_line_count = 1 out-filtered\n '\n \n+test_expect_success 'all, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:tree:right/:$(git rev-parse topic:right)\n+\t7:tree:right/:$(git rev-parse base~1:right)\n+\t7:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:tree:left/:$(git rev-parse base~2:left)\n+\t3:tree:right/:$(git rev-parse topic:right)\n+\t3:tree:right/:$(git rev-parse base~1:right)\n+\t3:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543076","messageId":"da191e29e68bd46a56adea35ba1e7a41ff2a15c5.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 06/12] backfill: die on incompatible filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:03Z","receivedAt":"2026-05-11T18:13:20Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git backfill' command uses the path-walk API in a critical way: it\nuses the objects output from the command to find the batches of missing\nobjects that should be requested from the server. Unlike 'git\npack-objects', we cannot fall back to another mechanism.\n\nThe previous change added the path_walk_filter_compatible() method that\nwe can reuse here. Use it during argument validation in cmd_backfill().\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c  | 5 ++---\n t/t5620-backfill.sh | 8 ++++++++\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 7ffab2ea74..b80f9ebe69 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -96,9 +96,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \tif (revs->explicit_diff_merges)\n \t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n \t\t    \"--diff-merges\");\n-\tif (revs->filter.choice)\n-\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n-\t\t    \"--filter\");\n+\tif (!path_walk_filter_compatible(&revs->filter))\n+\t\tdie(_(\"cannot backfill with these filter options\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..ede89f8c33 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -15,6 +15,14 @@ test_expect_success 'backfill rejects unexpected arguments' '\n \ttest_grep \"unrecognized argument: --unexpected-arg\" err\n '\n \n+test_expect_success 'backfill rejects incompatible filter options' '\n+\ttest_must_fail git backfill --objects --filter=tree:1 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err &&\n+\n+\ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err\n+'\n+\n # We create objects in the 'src' repo.\n test_expect_success 'setup repo for object creation' '\n \techo \"{print \\$1}\" >print_1.awk &&\n-- \ngitgitgadget\n\n"},{"id":"543075","messageId":"a1ab70405d6209a11b2eb752c154361060f1675d.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 07/12] path-walk: support blob size limit filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:04Z","receivedAt":"2026-05-11T18:13:22Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nExtend the path-walk API to handle the 'blob:limit=<size>' object\nfilter natively. This filter omits blobs whose size is equal to or\ngreater than the given limit, matching the semantics used by the\nlist-objects-filter machinery.\n\nWhen revs->filter.choice is LOFC_BLOB_LIMIT, the prepare_filters()\nmethod stores the limit value in info->blob_limit and clears the filter\nfrom revs. If the limit is zero, this degenerates to blob:none (all\nblobs excluded), so info->blobs is set to 0 instead.\n\nDuring walk_path(), blob batches are filtered before being delivered to\nthe callback: each blob's size is checked via odb_read_object_info(),\nand only blobs strictly smaller than the limit are included. Blobs whose\nsize cannot be determined (e.g. missing in a partial clone) are\nconservatively included, matching the existing filter behavior. Empty\nbatches after filtering are skipped entirely.\n\nThe check for inclusion in the path batch looks a little strange at\nfirst glance. We use odb_read_object_info() to read the object's size.\nBased on all of the assumptions to this point, this _should_ return\nOBJ_BLOB. Since we are focused on the size filter, we use a\nshort-circuited OR (||) to skip the size check if that method returns a\ndifferent object type.\n\nNotice that this inspection of object sizes requires the content to be\npresent in the repository. The odb_read_object_info() call will download\na missing blob on-demand. This means that the use of the path-walk API\nwithin 'git backfill' would not operate nicely with this filter type.\nThe intention of that command is to download missing blobs in batches.\nDownloading objects one-by-one would go against the point. Update the\nvalidation in 'git backfill' to add its own compatibility check on top\nof path_walk_filter_compatible().\n\nAdd tests for blob:limit=0 (equivalent to blob:none) and blob:limit=3\n(which exercises partial filtering within a batch where some blobs are\nkept and others are excluded).\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n builtin/backfill.c                  |  2 +\n path-walk.c                         | 41 +++++++++++++--\n path-walk.h                         |  7 +++\n t/t5620-backfill.sh                 |  2 +-\n t/t6601-path-walk.sh                | 82 +++++++++++++++++++++++++++++\n 6 files changed, 130 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 917045d5c3..c193917731 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,7 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n-supports the `--filter=<spec>` form `blob:none`.\n+supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex b80f9ebe69..5254a42711 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -98,6 +98,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \t\t    \"--diff-merges\");\n \tif (!path_walk_filter_compatible(&revs->filter))\n \t\tdie(_(\"cannot backfill with these filter options\"));\n+\tif (revs->filter.blob_limit_value)\n+\t\tdie(_(\"cannot backfill with blob size limits\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/path-walk.c b/path-walk.c\nindex cc587268e7..16fdfd7c5a 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -326,13 +327,35 @@ static int walk_path(struct path_walk_context *ctx,\n \t/*\n \t * Evaluate function pointer on this data, if requested.\n \t * Ignore object type filters for tagged objects (path starts\n-\t * with `/`).\n+\t * with `/`), first for blobs and then other types.\n \t */\n-\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n-\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n-\t    (list->type == OBJ_TAG && ctx->info->tags))\n+\tif (list->type == OBJ_BLOB &&\n+\t    ctx->info->blob_limit &&\n+\t    !path_is_for_direct_objects(path)) {\n+\t\tstruct oid_array filtered = OID_ARRAY_INIT;\n+\n+\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\tunsigned long size;\n+\n+\t\t\tif (odb_read_object_info(ctx->repo->objects,\n+\t\t\t\t\t\t &list->oids.oid[i],\n+\t\t\t\t\t\t &size) != OBJ_BLOB ||\n+\t\t\t\tsize < ctx->info->blob_limit)\n+\t\t\t\toid_array_append(&filtered,\n+\t\t\t\t\t\t &list->oids.oid[i]);\n+\t\t}\n+\n+\t\tif (filtered.nr)\n+\t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n+\t\t\t\t\t\t ctx->info->path_fn_data);\n+\t\toid_array_clear(&filtered);\n+\t} else if (path_is_for_direct_objects(path) ||\n+\t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n+\t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n+\t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n \t\t\t\t\tctx->info->path_fn_data);\n+\t}\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -509,6 +532,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_BLOB_LIMIT:\n+\t\tif (info) {\n+\t\t\tif (!options->blob_limit_value)\n+\t\t\t\tinfo->blobs = 0;\n+\t\t\telse\n+\t\t\t\tinfo->blob_limit = options->blob_limit_value;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/path-walk.h b/path-walk.h\nindex a1736ecb2b..60ceb65433 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -47,6 +47,13 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If non-zero, specifies a maximum blob size. Blobs with a\n+\t * size equal to or greater than this limit will not be\n+\t * emitted unless included in 'pending'.\n+\t */\n+\tunsigned long blob_limit;\n+\n \t/**\n \t * When 'prune_all_uninteresting' is set and a path has all objects\n \t * marked as UNINTERESTING, then the path-walk will not visit those\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex ede89f8c33..d2ea68e065 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -20,7 +20,7 @@ test_expect_success 'backfill rejects incompatible filter options' '\n \ttest_grep \"cannot backfill with these filter options\" err &&\n \n \ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n-\ttest_grep \"cannot backfill with these filter options\" err\n+\ttest_grep \"cannot backfill with blob size limits\" err\n '\n \n # We create objects in the 'src' repo.\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex b0ee31ee2d..45f366d738 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -477,4 +477,86 @@ test_expect_success 'topic only, blob:none filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, blob:limit=0 filter' '\n+\ttest-tool path-walk --filter=blob:limit=0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:tree:right/:$(git rev-parse topic:right)\n+\t7:tree:right/:$(git rev-parse base~1:right)\n+\t7:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, blob:limit=3 filter' '\n+\ttest-tool path-walk --filter=blob:limit=3 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:tree:right/:$(git rev-parse topic:right)\n+\t9:tree:right/:$(git rev-parse base~1:right)\n+\t9:tree:right/:$(git rev-parse base~2:right)\n+\t10:blob:right/c:$(git rev-parse base~2:right/c)\n+\t11:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543077","messageId":"2360a5be812b6f8f7e9ccb36e8b5f3347ec646f5.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 08/12] path-walk: add pl_sparse_trees to control tree pruning","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:05Z","receivedAt":"2026-05-11T18:13:23Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe path-walk API prunes trees and blobs when a sparse-checkout pattern\nlist is provided, which is the correct behavior for 'git backfill\n--sparse' since it only needs to fill in objects at paths within the\nsparse cone.\n\nHowever, a future change will use the path-walk API with a sparse:<oid>\nfilter that restricts only blobs while retaining all reachable trees.\nTo support both behaviors, add a 'pl_sparse_trees' flag to\npath_walk_info. When set (as in 'git backfill --sparse' and the\n--stdin-pl test helper mode), the sparse patterns prune both trees and\nblobs. When unset, only blobs are filtered and all trees are walked and\nreported.\n\nAdditionally, move the SEEN flag assignment in add_tree_entries() to\nafter the sparse pattern and pathspec checks. Previously, SEEN was set\nimmediately upon discovering an object, before checking whether its path\nmatched the sparse patterns. When the same object ID appeared at\nmultiple paths (e.g. sibling directories with identical contents), the\nfirst path to be visited would mark the object as SEEN. If that path was\noutside the sparse cone, the object would be skipped there but also\nnever discovered at its in-cone path.\n\nBy deferring the SEEN flag until after the checks pass, objects that are\nskipped due to sparse filtering remain discoverable at other paths where\nthey may be in scope.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c        |  1 +\n path-walk.c               |  5 +++--\n path-walk.h               |  6 ++++++\n t/helper/test-path-walk.c |  6 +++++-\n t/t6601-path-walk.sh      | 37 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 52 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 5254a42711..e71e0f4742 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -109,6 +109,7 @@ static int do_backfill(struct backfill_context *ctx)\n \n \tif (ctx->sparse) {\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = 1;\n \t\tif (get_sparse_checkout_patterns(info.pl)) {\n \t\t\tpath_walk_info_clear(&info);\n \t\t\treturn error(_(\"problem loading sparse-checkout\"));\ndiff --git a/path-walk.c b/path-walk.c\nindex 16fdfd7c5a..21cc40c392 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -183,7 +183,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t/* Skip this object if already seen. */\n \t\tif (o->flags & SEEN)\n \t\t\tcontinue;\n-\t\to->flags |= SEEN;\n \n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n@@ -204,7 +203,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\t\t\t\t  ctx->repo->index);\n \n \t\t\tif (ctx->info->pl->use_cone_patterns &&\n-\t\t\t    match == NOT_MATCHED)\n+\t\t\t    match == NOT_MATCHED &&\n+\t\t\t    (type == OBJ_BLOB || ctx->info->pl_sparse_trees))\n \t\t\t\tcontinue;\n \t\t\telse if (!ctx->info->pl->use_cone_patterns &&\n \t\t\t\t type == OBJ_BLOB &&\n@@ -239,6 +239,7 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\tcontinue;\n \t\t}\n \n+\t\to->flags |= SEEN;\n \t\tadd_path_to_list(ctx, path.buf, type, &entry.oid,\n \t\t\t\t !(o->flags & UNINTERESTING));\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 60ceb65433..7e57ae5f65 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -76,8 +76,14 @@ struct path_walk_info {\n \t * of the cone. If not in cone mode, then all tree paths will be\n \t * explored but the path_fn will only be called when the path matches\n \t * the sparse-checkout patterns.\n+\t *\n+\t * When 'pl_sparse_trees' is zero, the sparse patterns only restrict\n+\t * blobs and all trees are included in the walk output. This matches\n+\t * the behavior of the sparse:oid object filter. When nonzero, trees\n+\t * are also pruned by the sparse patterns (as used by backfill).\n \t */\n \tstruct pattern_list *pl;\n+\tint pl_sparse_trees;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 88f86ae0dc..3f2b50a9aa 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -68,7 +68,7 @@ static int emit_block(const char *path, struct oid_array *oids,\n \n int cmd__path_walk(int argc, const char **argv)\n {\n-\tint res, stdin_pl = 0;\n+\tint res, stdin_pl = 0, pl_sparse_trees = -1;\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n@@ -89,6 +89,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_BOOL(0, \"pl-sparse-trees\", &pl_sparse_trees,\n+\t\t\t N_(\"toggle pruning of trees by sparse patterns\")),\n \t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n@@ -116,6 +118,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tif (stdin_pl) {\n \t\tstruct strbuf in = STRBUF_INIT;\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = (pl_sparse_trees >= 0) ?\n+\t\t\tpl_sparse_trees : 1;\n \n \t\tinfo.pl->use_cone_patterns = 1;\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 45f366d738..02ad83dfb0 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -206,6 +206,43 @@ test_expect_success 'base & topic, sparse' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'base & topic, sparse, no tree pruning' '\n+\tcat >patterns <<-EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\n+\ttest-tool path-walk --stdin-pl --no-pl-sparse-trees \\\n+\t\t-- base topic <patterns >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:blob:left/b:$(git rev-parse base:left/b)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:3\n+\tcommits:4\n+\ttags:0\n+\ttrees:10\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n-- \ngitgitgadget\n\n"},{"id":"543078","messageId":"d9f5a98830390cc719808f2dc8deee8db666fafd.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 09/12] pack-objects: support sparse:oid filter with path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:06Z","receivedAt":"2026-05-11T18:13:25Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe --filter=sparse:<oid> option to 'git pack-objects' allows focusing\nan object set to a sparse-checkout definition. This reduces the set of\nmatching blobs while retaining all reachable trees. No server currently\nsupports fetching with this filter because it is expensive to compute\nand reachability bitmaps do not help without a significant effort to\nextend the bitmap feature to store bitmaps for each supported sparse-\ncheckout definition.\n\nWithout focusing on serving fetches and clones with these filters, there\nare still benefits that could be realized by making this faster. With\nthe sparse index, it's more realistic now than ever to be able to\noperate a local clone that was bootstrapped by a packfile created with\na sparse filter, because the missing trees are not needed to move a\nsparse-checkout from one commit to another or to view the history of any\npath in scope. Such clones could perhaps be bootstrapped by partial\nbundles.\n\nPreviously, constructing these sparse packs has been incredibly\ncomputationally inefficient. The revision walk that explores which\nobjects are in scope spends a lot of time checking each object to see if\nit matches the sparse-checkout patterns, causing quadratic behavior\n(number of objects times number of sparse-checkout patterns). This\nimproves somewhat when using cone-mode sparse-checkout patterns that can\nuse hashtables and prefix matches to determine containment. However, the\ncheck per object is still too expensive for most cases.\n\nThis is where the path-walk feature comes in. We can proceed as normal\nby placing objects in bins by path and _then_ check a group of objects\nall at once. Since sparse:<oid> only restricts blobs, the path-walk must\ninclude all reachable trees while using the cone-mode patterns to skip\nblobs at paths outside the sparse scope. This establishes a baseline for\na potential future \"treesparse:<oid>\" filter that would also restrict\ntrees, but introducing such a new filter is deferred to a later change.\n\nThe implementation here is focused around loading the sparse-checkout\npatterns from the provided object ID and checking that the patterns are\nindeed cone-mode patterns. We can then load the correct pattern list\ninto the path walk context and use the logic that already exists from\nbff45557675 (backfill: add --sparse option, 2025-02-03), though that\nfeature loads sparse-checkout patterns from the worktree's local\nsettings and also restricts tree objects. We use a combination of errors\nand warnings to signal problems during this load. The difference is that\nerrors are likely fatal for the non-path-walk version while the warnings\nare probably just implementation details for the path-walk version and\nthe 'git pack-objects' command can fall back to the revision walk\nversion.\n\nNow that the SEEN flag is deferred until after pattern checks (from the\nprevious commit), handle the case where a tree with a shared OID appears\nat both an out-of-cone and in-cone path. When trees are not being pruned\n(pl_sparse_trees == 0), the path-walk re-walks the tree at the in-cone\npath so that in-cone blobs within it are discovered. The new tests in\nt5317 and t6601 demonstrate this behavior and would fail without these\nchanges.\n\nThe performance test p5315 shows the impact of this change when using\nsparse filters:\n\nTest                                              HEAD~1     HEAD\n----------------------------------------------------------------------\n5315.10: repack (sparse:oid)                      77.98    77.47  -0.7%\n5315.11: repack size (sparse:oid)                187.5M   187.4M  -0.0%\n5315.12: repack (sparse:oid, --path-walk)         77.91    31.41 -59.7%\n5315.13: repack size (sparse:oid, --path-walk)   187.5M   161.1M -14.1%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (14% smaller for sparse packs)\nand dramatic time savings (60% faster) by leveraging the path-walk's\nability to skip blobs outside the sparse scope.\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blaue <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-backfill.adoc        |   4 +\n Documentation/git-pack-objects.adoc    |   3 +-\n builtin/pack-objects.c                 |  16 ++-\n path-walk.c                            |  81 ++++++++++++++-\n t/t5317-pack-objects-filter-objects.sh | 125 +++++++++++++++++++++++\n t/t6601-path-walk.sh                   | 131 +++++++++++++++++++++++++\n 6 files changed, 350 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc\nindex c0a3b80615..82d6a1969d 100644\n--- a/Documentation/git-backfill.adoc\n+++ b/Documentation/git-backfill.adoc\n@@ -80,6 +80,10 @@ OPTIONS\n +\n You may also use commit-limiting options understood by\n linkgit:git-rev-list[1] such as `--first-parent`, `--since`, or pathspecs.\n++\n+Most `--filter=<spec>` options don't work with the purpose of\n+`git backfill`, but the `sparse:<oid>` filter is integrated to provide a\n+focused set of paths to download, distinct from the `--sparse` option.\n \n SEE ALSO\n --------\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex c193917731..f0887ab5b9 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n-supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n+supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n+`sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex bc9fb5b457..b783dc62bc 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4754,7 +4754,7 @@ static int add_objects_by_path(const char *path,\n \treturn 0;\n }\n \n-static void get_object_list_path_walk(struct rev_info *revs)\n+static int get_object_list_path_walk(struct rev_info *revs)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tunsigned int processed = 0;\n@@ -4777,8 +4777,9 @@ static void get_object_list_path_walk(struct rev_info *revs)\n \tresult = walk_objects_by_path(&info);\n \ttrace2_region_leave(\"pack-objects\", \"path-walk\", revs->repo);\n \n-\tif (result)\n-\t\tdie(_(\"failed to pack objects via path-walk\"));\n+\tpath_walk_info_clear(&info);\n+\n+\treturn result;\n }\n \n static void get_object_list(struct rev_info *revs, struct strvec *argv)\n@@ -4841,8 +4842,13 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)\n \t\tfn_show_object = show_object;\n \n \tif (path_walk) {\n-\t\tget_object_list_path_walk(revs);\n-\t} else {\n+\t\tif (get_object_list_path_walk(revs)) {\n+\t\t\twarning(_(\"failed to pack objects via path-walk\"));\n+\t\t\tpath_walk = 0;\n+\t\t}\n+\t}\n+\n+\tif (!path_walk) {\n \t\tif (prepare_revision_walk(revs))\n \t\t\tdie(_(\"revision walk setup failed\"));\n \t\tmark_edges_uninteresting(revs, show_edge, sparse);\ndiff --git a/path-walk.c b/path-walk.c\nindex 21cc40c392..1172385402 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"object-name.h\"\n #include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n@@ -180,10 +181,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\treturn -1;\n \t\t}\n \n-\t\t/* Skip this object if already seen. */\n-\t\tif (o->flags & SEEN)\n-\t\t\tcontinue;\n-\n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n \n@@ -194,6 +191,40 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\tif (type == OBJ_TREE)\n \t\t\tstrbuf_addch(&path, '/');\n \n+\t\tif (o->flags & SEEN) {\n+\t\t\t/*\n+\t\t\t * A tree with a shared OID may appear at multiple\n+\t\t\t * paths. Even though we already added this tree to\n+\t\t\t * the output at some other path, we still need to\n+\t\t\t * walk into it at this in-cone path to discover\n+\t\t\t * blobs that were not found at the earlier\n+\t\t\t * out-of-cone path.\n+\t\t\t *\n+\t\t\t * Only do this for paths not yet in our map, to\n+\t\t\t * avoid duplicate entries when the same tree OID\n+\t\t\t * appears at the same path across multiple commits.\n+\t\t\t */\n+\t\t\tif (type == OBJ_TREE && ctx->info->pl &&\n+\t\t\t    ctx->info->pl->use_cone_patterns &&\n+\t\t\t    !ctx->info->pl_sparse_trees &&\n+\t\t\t    !strmap_contains(&ctx->paths_to_lists, path.buf)) {\n+\t\t\t\tint dtype;\n+\t\t\t\tenum pattern_match_result m;\n+\t\t\t\tm = path_matches_pattern_list(path.buf, path.len,\n+\t\t\t\t\t\t\t      path.buf + base_len,\n+\t\t\t\t\t\t\t      &dtype,\n+\t\t\t\t\t\t\t      ctx->info->pl,\n+\t\t\t\t\t\t\t      ctx->repo->index);\n+\t\t\t\tif (m != NOT_MATCHED) {\n+\t\t\t\t\tadd_path_to_list(ctx, path.buf, type,\n+\t\t\t\t\t\t\t &entry.oid,\n+\t\t\t\t\t\t\t !(o->flags & UNINTERESTING));\n+\t\t\t\t\tpush_to_stack(ctx, path.buf);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (ctx->info->pl) {\n \t\t\tint dtype;\n \t\t\tenum pattern_match_result match;\n@@ -543,6 +574,48 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_SPARSE_OID:\n+\t\tif (info) {\n+\t\t\tstruct object_id sparse_oid;\n+\t\t\tstruct repository *repo = info->revs->repo;\n+\n+\t\t\tif (info->pl) {\n+\t\t\t\twarning(_(\"sparse filter cannot be combined with existing sparse patterns\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (repo_get_oid_with_flags(repo,\n+\t\t\t\t\t\t    options->sparse_oid_name,\n+\t\t\t\t\t\t    &sparse_oid,\n+\t\t\t\t\t\t    GET_OID_BLOB)) {\n+\t\t\t\terror(_(\"unable to access sparse blob in '%s'\"),\n+\t\t\t\t      options->sparse_oid_name);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tCALLOC_ARRAY(info->pl, 1);\n+\t\t\tinfo->pl->use_cone_patterns = 1;\n+\n+\t\t\tif (add_patterns_from_blob_to_list(&sparse_oid, \"\", 0,\n+\t\t\t\t\t\t\t   info->pl) < 0) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\terror(_(\"unable to parse sparse filter data in '%s'\"),\n+\t\t\t\t      oid_to_hex(&sparse_oid));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (!info->pl->use_cone_patterns) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh\nindex 501d715b9a..dddb79ba62 100755\n--- a/t/t5317-pack-objects-filter-objects.sh\n+++ b/t/t5317-pack-objects-filter-objects.sh\n@@ -478,4 +478,129 @@ test_expect_success 'verify pack-objects w/ --missing=allow-any' '\n \tEOF\n '\n \n+# Test that --path-walk produces the same object set as standard traversal\n+# when using sparse:oid filters with cone-mode patterns.\n+#\n+# The sparse:oid filter restricts only blobs, not trees. Both standard\n+# and path-walk should produce identical sets of blobs, commits, and trees.\n+\n+test_expect_success 'setup pw_sparse for path-walk comparison' '\n+\tgit init pw_sparse &&\n+\tmkdir -p pw_sparse/inc/sub pw_sparse/exc/sub &&\n+\n+\tfor n in 1 2\n+\tdo\n+\t\techo \"inc $n\" >pw_sparse/inc/file$n &&\n+\t\techo \"inc sub $n\" >pw_sparse/inc/sub/file$n &&\n+\t\techo \"exc $n\" >pw_sparse/exc/file$n &&\n+\t\techo \"exc sub $n\" >pw_sparse/exc/sub/file$n &&\n+\t\techo \"root $n\" >pw_sparse/root$n || return 1\n+\tdone &&\n+\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"first\" &&\n+\n+\techo \"inc 1 modified\" >pw_sparse/inc/file1 &&\n+\techo \"exc 1 modified\" >pw_sparse/exc/file1 &&\n+\techo \"root 1 modified\" >pw_sparse/root1 &&\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"second\" &&\n+\n+\t# Cone-mode sparse pattern: include root + inc/\n+\tprintf \"/*\\n!/*/\\n/inc/\\n\" |\n+\tgit -C pw_sparse hash-object -w --stdin >sparse_oid\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk produces same blobs' '\n+\toid=$(cat sparse_oid) &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >standard.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../standard.pack &&\n+\tgit -C pw_sparse verify-pack -v ../standard.pack >standard_verify &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >pathwalk.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../pathwalk.pack &&\n+\tgit -C pw_sparse verify-pack -v ../pathwalk.pack >pathwalk_verify &&\n+\n+\t# Blobs must match exactly\n+\tgrep -E \"^[0-9a-f]{40} blob\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_blobs &&\n+\ttest_cmp standard_blobs pathwalk_blobs &&\n+\n+\t# Commits must match exactly\n+\tgrep -E \"^[0-9a-f]{40} commit\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_commits &&\n+\tgrep -E \"^[0-9a-f]{40} commit\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_commits &&\n+\ttest_cmp standard_commits pathwalk_commits\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk includes all trees' '\n+\t# The sparse:oid filter restricts only blobs, not trees.\n+\t# Both standard and path-walk should include the same trees.\n+\tgrep -E \"^[0-9a-f]{40} tree\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_trees &&\n+\tgrep -E \"^[0-9a-f]{40} tree\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_trees &&\n+\n+\ttest_cmp standard_trees pathwalk_trees\n+'\n+\n+# Test the edge case where the same tree/blob OID appears at both an\n+# in-cone and out-of-cone path. When sibling directories have identical\n+# contents, they share a tree OID. The path-walk defers marking objects\n+# SEEN until after checking sparse patterns, so an object at an out-of-cone\n+# path can still be discovered at an in-cone path.\n+\n+test_expect_success 'setup pw_shared for shared OID across cone boundary' '\n+\tgit init pw_shared &&\n+\tmkdir pw_shared/aaa pw_shared/zzz &&\n+\techo \"shared content\" >pw_shared/aaa/file &&\n+\techo \"shared content\" >pw_shared/zzz/file &&\n+\techo \"root file\" >pw_shared/rootfile &&\n+\tgit -C pw_shared add . &&\n+\tgit -C pw_shared commit -m \"aaa and zzz share tree OID\" &&\n+\n+\t# Verify they share a tree OID\n+\taaa_tree=$(git -C pw_shared rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git -C pw_shared rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tprintf \"/*\\n!/*/\\n/zzz/\\n\" |\n+\tgit -C pw_shared hash-object -w --stdin >shared_sparse_oid\n+'\n+\n+test_expect_success 'shared tree OID: --path-walk blobs match standard' '\n+\toid=$(cat shared_sparse_oid) &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >shared_std.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_std.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_std.pack >shared_std_verify &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >shared_pw.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_pw.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_pw.pack >shared_pw_verify &&\n+\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_std_verify |\n+\tawk \"{print \\$1}\" | sort >shared_std_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_pw_verify |\n+\tawk \"{print \\$1}\" | sort >shared_pw_blobs &&\n+\ttest_cmp shared_std_blobs shared_pw_blobs\n+'\n+\n test_done\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 02ad83dfb0..ac294867a5 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -596,4 +596,135 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'setup sparse filter blob' '\n+\t# Cone-mode patterns: include root, exclude all dirs, include left/\n+\tcat >patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\tsparse_oid=$(git hash-object -w -t blob patterns)\n+'\n+\n+test_expect_success 'all, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:blob:left/b:$(git rev-parse base:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+# Demonstrate the SEEN flag ordering issue: when the same tree/blob OID\n+# appears at two sibling paths where one is in-cone and the other is\n+# out-of-cone, the path-walk must still discover blobs at the in-cone\n+# path even when the shared tree OID was first encountered out-of-cone.\n+# Since sparse:oid includes all trees, the out-of-cone tree (aaa/) is\n+# walked first, and its blob is skipped. The path-walk then re-walks\n+# the same tree OID at the in-cone path (zzz/) to find the blob there.\n+\n+test_expect_success 'setup shared tree OID across cone boundary' '\n+\tgit checkout --orphan shared-tree &&\n+\tgit rm -rf . &&\n+\tmkdir aaa zzz &&\n+\techo \"shared content\" >aaa/file &&\n+\techo \"shared content\" >zzz/file &&\n+\techo \"root file\" >rootfile &&\n+\tgit add aaa zzz rootfile &&\n+\tgit commit -m \"aaa and zzz have same tree OID\" &&\n+\n+\t# Verify they really share a tree OID\n+\taaa_tree=$(git rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tcat >shared-patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/zzz/\n+\tEOF\n+\tshared_sparse_oid=$(git hash-object -w -t blob shared-patterns)\n+'\n+\n+test_expect_success 'sparse:oid with shared tree OID across cone boundary' '\n+\ttest-tool path-walk \\\n+\t\t--filter=sparse:oid=$shared_sparse_oid \\\n+\t\t-- shared-tree >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse shared-tree)\n+\t1:tree::$(git rev-parse shared-tree^{tree})\n+\t2:blob:rootfile:$(git rev-parse shared-tree:rootfile)\n+\t3:tree:aaa/:$(git rev-parse shared-tree:aaa)\n+\t4:tree:zzz/:$(git rev-parse shared-tree:zzz)\n+\t5:blob:zzz/file:$(git rev-parse shared-tree:zzz/file)\n+\tblobs:2\n+\tcommits:1\n+\ttags:0\n+\ttrees:3\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543079","messageId":"c9efff0ea8d72d2d084957e1b099801b8d8727f3.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 10/12] path-walk: support `tree:0` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:07Z","receivedAt":"2026-05-11T18:13:26Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `tree:0` object filter omits all trees and blobs from the result,\nkeeping only commits and tags. Consequently, this filter type should\nhas a fairly straightforward integration with path-walk, as the decision\nto include an object depends only on its type and does not depend on any\npath-sensitive state.\n\nMapping it onto `path_walk_info` is direct: set `info->trees = 0` and\n`info->blobs = 0` in `prepare_filters()` when the `LOFC_TREE_DEPTH`\nchoice is requested with depth zero. The existing code already plumbs\nthose flags through the rest of the walk:\n\n - 'walk_objects_by_path()' sets `revs->blob_objects = info->blobs` and\n   `revs->tree_objects = info->trees` before `prepare_revision_walk()`,\n   so the revision walk doesn't try to enumerate trees or blobs itself.\n\n - The commit-walk loop short-circuits the root-tree fetch with\n   \"if (!info->trees && !info->blobs) continue;\", so we never even\n   look up the root tree, let alone descend into it.\n\n - `setup_pending_objects()` skips pending trees and blobs based on\n   the same flags.\n\nThis means the path-walk doesn't allocate or expand any tree structures\nat all under `tree:0`, which matches the intended behavior of the\nfilter.\n\nNon-zero tree-depth filters are not supported. Those depend on the depth\nat which a tree is visited, which is a path-walk concept the filter\nmachinery doesn't currently share with the path-walk API. Reject them in\n`prepare_filters()` with a helpful error and let pack-objects fall back\nto the regular traversal, the same way it already does for unsupported\nfilters.\n\nAdd coverage in t6601 for both `--all` and a single-branch case to\nconfirm that no trees or blobs are emitted, and a separate test that\n`tree:1` is rejected with the expected error message. Place the new\ntests before \"setup sparse filter blob\" so they run on the original set\nof refs, before the orphan branch that the sparse-tree tests create.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  4 +--\n path-walk.c                         | 42 +++++++++++++++++++++++--\n t/t6601-path-walk.sh                | 49 +++++++++++++++++++++++++++++\n 3 files changed, 90 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex f0887ab5b9..4ebda0fa9e 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,8 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n-supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n-`sparse:<oid>`.\n+supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n+`tree:0`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex 1172385402..f6f95da550 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -389,8 +389,8 @@ static int walk_path(struct path_walk_context *ctx,\n \t\t\t\t\tctx->info->path_fn_data);\n \t}\n \n-\t/* Expand data for children. */\n-\tif (list->type == OBJ_TREE) {\n+\t/* Expand data for children, unless this is a direct-object path. */\n+\tif (list->type == OBJ_TREE && !path_is_for_direct_objects(path)) {\n \t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n \t\t\tret |= add_tree_entries(ctx,\n \t\t\t\t\t    path,\n@@ -441,11 +441,14 @@ static int setup_pending_objects(struct path_walk_info *info,\n {\n \tstruct type_and_oid_list *tags = NULL;\n \tstruct type_and_oid_list *tagged_blobs = NULL;\n+\tstruct type_and_oid_list *tagged_trees = NULL;\n \tstruct type_and_oid_list *root_tree_list = NULL;\n \n \tif (info->tags)\n \t\tCALLOC_ARRAY(tags, 1);\n \tCALLOC_ARRAY(tagged_blobs, 1);\n+\tif (!info->trees)\n+\t\tCALLOC_ARRAY(tagged_trees, 1);\n \troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n \n \t/*\n@@ -490,7 +493,15 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (pending->path) {\n+\t\t\tif (tagged_trees) {\n+\t\t\t\t/*\n+\t\t\t\t * Trees are disabled but pending trees\n+\t\t\t\t * should still be emitted. Collect them\n+\t\t\t\t * into a \"/tagged-trees\" list that\n+\t\t\t\t * bypasses the object type filter.\n+\t\t\t\t */\n+\t\t\t\toid_array_append(&tagged_trees->oids, &obj->oid);\n+\t\t\t} else if (pending->path) {\n \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n \t\t\t\t\t\t\t    : xstrdup(\"\");\n \t\t\t\tadd_path_to_list(ctx, path, OBJ_TREE, &obj->oid, 1);\n@@ -534,6 +545,18 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\t\tfree(tagged_blobs);\n \t\t}\n \t}\n+\tif (tagged_trees) {\n+\t\tif (tagged_trees->oids.nr) {\n+\t\t\tconst char *tagged_tree_path = \"/tagged-trees\";\n+\t\t\ttagged_trees->type = OBJ_TREE;\n+\t\t\ttagged_trees->maybe_interesting = 1;\n+\t\t\tstrmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);\n+\t\t\tpush_to_stack(ctx, tagged_tree_path);\n+\t\t} else {\n+\t\t\toid_array_clear(&tagged_trees->oids);\n+\t\t\tfree(tagged_trees);\n+\t\t}\n+\t}\n \tif (tags) {\n \t\tif (tags->oids.nr) {\n \t\t\tconst char *tag_path = \"/tags\";\n@@ -574,6 +597,19 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_TREE_DEPTH:\n+\t\tif (options->tree_exclude_depth) {\n+\t\t\terror(_(\"tree:%lu filter not supported by the path-walk API\"),\n+\t\t\t      options->tree_exclude_depth);\n+\t\t\treturn 0;\n+\t\t}\n+\t\tif (info) {\n+\t\t\tinfo->trees = 0;\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex ac294867a5..deaeab9a3e 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -596,6 +596,55 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:2\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'tree:1 filter is rejected' '\n+\ttest_must_fail test-tool path-walk --filter=tree:1 -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n\n"},{"id":"543080","messageId":"b221ea47125c1e095167acbc066fbb7baffca55a.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 11/12] path-walk: support `object:type` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:08Z","receivedAt":"2026-05-11T18:13:28Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `object:type` filter accepts only objects of a single type; it is\nthe second member of the object-info-only filter family that bitmap\ntraversal already supports.\n\nLike `blob:none` and `tree:0`, it can be evaluated with nothing more\nthan the object's type, which is exactly the granularity path-walk's\nexisting info->{commits,trees,blobs,tags} flags already control.\n\nMap `LOFC_OBJECT_TYPE` in `prepare_filters()` by AND-ing each flag\nagainst the filtered type. A single `object:type=X` filter\napplied to the default info (all flags = 1) leaves `info->X = 1` and\nall the others 0, which is what we want.\n\nUsing an AND rather than straight assignment prepares us for a\nsubsequent change to implement combined object filters.\n\nThe path-walk machinery is mostly already wired for the per-type\ndistinction:\n\n - `walk_path()` calls `path_fn` for a batch only when the corresponding\n   `info->X` flag is set, so unwanted types are silently not reported.\n\n - `add_tree_entries()` skips tree entries of type `OBJ_BLOB` when\n   `info->blobs` is unset, so we don't even allocate paths for them.\n\n - The commit-walk loop short-circuits the root-tree fetch when\n   `!info->trees && !info->blobs`, so commit-only filters don't descend\n   into trees at all.\n\nBut there are a couple of side effects of the \"trees off, blobs on\" case\nthat need fixing:\n\n 1. 'setup_pending_objects()' previously skipped pending trees as soon\n    as `info->trees` was zero. For 'object:type=blob' the call site\n    needs those pending trees: a lightweight tag pointing to a tree, or\n    an annotated tag whose peeled target is a tree, can both reach\n    blobs that are otherwise unreachable from any commit's root tree.\n    Loosen the gate to \"if (!info->trees && !info->blobs) continue\" and\n    similarly retrieve the root_tree_list whenever either trees or\n    blobs are wanted.\n\n 2. The revision machinery's `handle_commit()` drops pending trees when\n    `revs->tree_objects` is zero (see the 'OBJ_TREE' handler in\n    revision.c), so by the time path-walk sees the pending list\n    after `prepare_revision_walk()` the tree-bearing pendings would\n    already be gone. Fix this by setting\n\n        revs->tree_objects = info->trees || info->blobs\n\n    so pending trees survive `prepare_revision_walk()` whenever we\n    need to walk into them. Path-walk still resets tree_objects to\n    zero immediately after `prepare_revision_walk()` returns, so the\n    rev-walk itself never enumerates trees redundantly with\n    path-walk's own descent.\n\nAdd coverage in t6601 for each of the four `object:type` values. The\n'object:type=blob' test in particular asserts that file2 and child/file\n(both reachable only through tag-pointed trees) show up in the output,\nexercising the pending-tree fix.\n\nUpdate Documentation/git-pack-objects.adoc to add object:type to\nthe list of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n path-walk.c                         | 13 ++++-\n path-walk.h                         |  6 +++\n t/t6601-path-walk.sh                | 84 +++++++++++++++++++++++++++++\n 4 files changed, 103 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 4ebda0fa9e..e96d2c00b3 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,7 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n-`tree:0`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex f6f95da550..839505839b 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -381,7 +381,7 @@ static int walk_path(struct path_walk_context *ctx,\n \t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n \t\t\t\t\t\t ctx->info->path_fn_data);\n \t\toid_array_clear(&filtered);\n-\t} else if (path_is_for_direct_objects(path) ||\n+\t} else if ((!ctx->info->strict_types && path_is_for_direct_objects(path)) ||\n \t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n \t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n \t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n@@ -610,6 +610,17 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_OBJECT_TYPE:\n+\t\tif (info) {\n+\t\t\tinfo->commits &= options->object_type == OBJ_COMMIT;\n+\t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n+\t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n+\t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n+\t\t\tinfo->strict_types = 1;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/path-walk.h b/path-walk.h\nindex 7e57ae5f65..a2652b2d46 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -47,6 +47,12 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If 'strict_types' is 0, then direct object requests will no longer\n+\t * override the object type restrictions.\n+\t */\n+\tint strict_types;\n+\n \t/**\n \t * If non-zero, specifies a maximum blob size. Blobs with a\n \t * size equal to or greater than this limit will not be\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex deaeab9a3e..0515aaddbf 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -645,6 +645,90 @@ test_expect_success 'tree:1 filter is rejected' '\n \ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n '\n \n+test_expect_success 'all, object:type=commit filter' '\n+\ttest-tool path-walk --filter=object:type=commit -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:4\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tag filter' '\n+\ttest-tool path-walk --filter=object:type=tag -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tag:/tags:$(git rev-parse refs/tags/first)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t0:tag:/tags:$(git rev-parse refs/tags/third)\n+\t0:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t0:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t0:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\tblobs:0\n+\tcommits:0\n+\ttags:7\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tree filter' '\n+\ttest-tool path-walk --filter=object:type=tree -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tree::$(git rev-parse topic^{tree})\n+\t0:tree::$(git rev-parse base^{tree})\n+\t0:tree::$(git rev-parse base~1^{tree})\n+\t0:tree::$(git rev-parse base~2^{tree})\n+\t0:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t0:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t1:tree:a/:$(git rev-parse base:a)\n+\t2:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t3:tree:left/:$(git rev-parse base:left)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:tree:right/:$(git rev-parse topic:right)\n+\t4:tree:right/:$(git rev-parse base~1:right)\n+\t4:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=blob filter' '\n+\ttest-tool path-walk --filter=object:type=blob -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:left/b:$(git rev-parse base:left/b)\n+\t2:blob:left/b:$(git rev-parse base~2:left/b)\n+\t3:blob:right/c:$(git rev-parse base~2:right/c)\n+\t3:blob:right/c:$(git rev-parse topic:right/c)\n+\t4:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:8\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n\n"},{"id":"543081","messageId":"16bd3c3161bb695466c3ba1fc23bc90fee763246.1778523189.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v3 12/12] path-walk: support `combine` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-11T18:13:09Z","receivedAt":"2026-05-11T18:13:29Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `combine` filter takes the intersection of its children, that is:\nobjects are shown only when all child filters would admit the object.\n\nThe preceding patches added support for many individual filter types.\nEnable users to compose these filters by implementing support for the\n`combine` filter type.\n\nMapping intersection onto path_walk_info works because every supported\nchild filter is a monotonic restriction:\n\n - `blob:none`, `tree:0` unconditionally clear `info->blobs` and (for\n   `tree:0`) `info->trees`; clearing an already-cleared flag is a\n   no-op.\n\n - `object:type=X` is now expressed as an AND of each type flag with the\n   filtered type, so applying multiple such filters only refines the\n   existing set rather than overwrites it.\n\n - `blob:limit=N` has to compose too: the intersection of \"size < L1\"\n   and \"size < L2\" is \"size < min(L1, L2)\".\n\n   Update the `LOFC_BLOB_LIMIT` handler to take the running minimum when\n   `info->blob_limit` is already set, so a combined filter with, e.g.,\n   both \"blob:limit=10\" and \"blob:limit=5\" produces a limit of 5\n   regardless of ordering.\n\n - `sparse:oid` is left unchanged. A `combine` filter that includes a\n   `sparse:oid` is allowed at most once, since the existing handler\n   refuses to overwrite `info->pl`. Two `sparse:oid` filters in a single\n   `combine` would be unusual and are rejected with a warning, matching\n   the standalone `sparse:oid` behavior.\n\nImplementation-wise, the existing `prepare_filters()` called\n`list_objects_filter_release()` inside each case branch. That works fine for\ntop-level filters, but `combine` filters need to recurse over its child\nfilters without releasing each one in turn (since the parent's release\niterates the sub array). Split `prepare_filters()` into a recursive helper\nthat performs only the mutation, plus a thin wrapper that calls the helper\nand then releases the top-level filter once.\n\nThe `LOFC_COMBINE` case in the helper just walks `sub_nr` and recurses;\nchild filters are released by the wrapper's single\n`list_objects_filter_release()` call on the parent (which itself recursively\nreleases each sub-filter, the same way it always has).\n\nIf any sub-filter is unsupported (e.g. \"tree:1\", \"sparse:<path>\", or a\nnot-yet-supported choice), the recursion bubbles a failure up and the\nexisting pack-objects/backfill fallback paths kick in.\n\nAdd coverage in t6601:\n\n  - \"combine:blob:none+tree:0\" collapses to \"tree:0\"\n\n  - \"combine:object:type=blob+blob:limit=3\" yields only the blobs\n    smaller than three bytes\n\n  - \"combine:object:type=blob+object:type=tree\" intersects to empty\n\n  - \"combine:tree:1+blob:none\" reports the \"tree:1\" error.\n\nUpdate Documentation/git-pack-objects.adoc to add combine to the\nlist of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  3 +-\n path-walk.c                         | 25 ++++++++--\n t/t6601-path-walk.sh                | 71 +++++++++++++++++++++++++++++\n 3 files changed, 93 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex e96d2c00b3..2fd79f13ad 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,8 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. Whe `--path-walk` option\n supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n-`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter\n+types can be combined with the `combine:<spec>+<spec>` form.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex 839505839b..3c67b359f9 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -573,8 +573,8 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n-static int prepare_filters(struct path_walk_info *info,\n-\t\t\t   struct list_objects_filter_options *options)\n+static int prepare_filters_one(struct path_walk_info *info,\n+\t\t\t       struct list_objects_filter_options *options)\n {\n \tswitch (options->choice) {\n \tcase LOFC_DISABLED:\n@@ -591,7 +591,8 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tif (!options->blob_limit_value)\n \t\t\t\tinfo->blobs = 0;\n-\t\t\telse\n+\t\t\telse if (!info->blob_limit ||\n+\t\t\t\t info->blob_limit > options->blob_limit_value)\n \t\t\t\tinfo->blob_limit = options->blob_limit_value;\n \t\t\tlist_objects_filter_release(options);\n \t\t}\n@@ -606,7 +607,6 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tinfo->trees = 0;\n \t\t\tinfo->blobs = 0;\n-\t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n \n@@ -658,8 +658,13 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n \t\t\t\treturn 0;\n \t\t\t}\n+\t\t}\n+\t\treturn 1;\n \n-\t\t\tlist_objects_filter_release(options);\n+\tcase LOFC_COMBINE:\n+\t\tfor (size_t i = 0; i < options->sub_nr; i++) {\n+\t\t\tif (!prepare_filters_one(info, &options->sub[i]))\n+\t\t\t\treturn 0;\n \t\t}\n \t\treturn 1;\n \n@@ -670,6 +675,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t}\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tif (!prepare_filters_one(info, options))\n+\t\treturn 0;\n+\tif (info)\n+\t\tlist_objects_filter_release(options);\n+\treturn 1;\n+}\n+\n int path_walk_filter_compatible(struct list_objects_filter_options *options)\n {\n \treturn prepare_filters(NULL, options);\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 0515aaddbf..643d630d2a 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -729,6 +729,77 @@ test_expect_success 'all, object:type=blob filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, combine:blob:none+tree:0 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:blob:none+tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:2\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine:object:type=blob+blob:limit=3 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+blob:limit=3 \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:left/b:$(git rev-parse base~2:left/b)\n+\t3:blob:right/c:$(git rev-parse base~2:right/c)\n+\t4:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine of disjoint object:types is empty' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+object:type=tree \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'combine: rejects unsupported subfilters' '\n+\ttest_must_fail test-tool path-walk \\\n+\t\t--filter=combine:tree:1+blob:none -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n"},{"id":"543082","messageId":"agIiL3jBvBBLHJWD@nand.local","threadId":"65575","inReplyTo":"b2deb7f81939d6ed03f0ea5c8be5ec515c1c99ab.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 05/12] path-walk: support blobless filter","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-11T18:38:39Z","receivedAt":"2026-05-11T18:38:44Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:13:02PM +0000, Derrick Stolee via GitGitGadget wrote:\n> diff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\n> index b78175fbe1..917045d5c3 100644\n> --- a/Documentation/git-pack-objects.adoc\n> +++ b/Documentation/git-pack-objects.adoc\n> @@ -402,9 +402,9 @@ will be automatically changed to version `1`.\n>  \tof filenames that cause collisions in Git's default name-hash\n>  \talgorithm.\n>  +\n> -Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n> -`--use-bitmap-index` option will be ignored in the presence of\n> -`--path-walk.`\n> +Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n> +ignored in the presence of `--path-walk`. Whe `--path-walk` option\n> +supports the `--filter=<spec>` form `blob:none`.\n\ns/Whe/The/ ?\n\nThanks,\nTaylor\n"},{"id":"543086","messageId":"0b4a8595-63e3-427c-8920-af4271ae1714@gmail.com","threadId":"65575","inReplyTo":"agIiL3jBvBBLHJWD@nand.local","subject":"Re: [PATCH v3 05/12] path-walk: support blobless filter","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-11T19:44:52Z","receivedAt":"2026-05-11T19:44:54Z","isPatch":true,"body":"On 5/11/2026 2:38 PM, Taylor Blau wrote:\n> On Mon, May 11, 2026 at 06:13:02PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> diff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\n>> index b78175fbe1..917045d5c3 100644\n>> --- a/Documentation/git-pack-objects.adoc\n>> +++ b/Documentation/git-pack-objects.adoc\n>> @@ -402,9 +402,9 @@ will be automatically changed to version `1`.\n>>  \tof filenames that cause collisions in Git's default name-hash\n>>  \talgorithm.\n>>  +\n>> -Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n>> -`--use-bitmap-index` option will be ignored in the presence of\n>> -`--path-walk.`\n>> +Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n>> +ignored in the presence of `--path-walk`. Whe `--path-walk` option\n>> +supports the `--filter=<spec>` form `blob:none`.\n> \n> s/Whe/The/ ?\nYes, of course. This typo will follow the remaining patches in the context\nlines. Will fix in v4.\n\nThanks,\n-Stolee\n\n"},{"id":"543116","messageId":"agJ8V4wKF6NQI0fO@nand.local","threadId":"65575","inReplyTo":"0840110116a967d39d8b63dbfe822ea9b2500088.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 01/12] t5620: make test work with path-walk var","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:03:19Z","receivedAt":"2026-05-12T01:03:21Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:12:58PM +0000, Derrick Stolee via GitGitGadget wrote:\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> The GIT_TEST_PACK_PATH_WALK test variable allows enabling the\n> --path-walk option to 'git pack-objects' by default. This sometimes\n> engages the warning that --path-walk is incompatible with the --filter\n> option. These tests in t5620 fail due to this warning over stderr in\n> this case. Disable this variable for this moment until these options\n> work together.\n\nMakes sense. I think it would be equally OK to set this at the\ntop-level similar to c51f5a6437c (t5326: test multi-pack bitmap\nbehavior, 2021-08-31), but this is equally fine. Especially so if there\nare other tests in the script that we do want to exercise under\n--path-walk.\n\nLooks good.\n\nThanks,\nTaylor\n"},{"id":"543117","messageId":"agJ8pTOLq3bT6rrX@nand.local","threadId":"65575","inReplyTo":"5cc63833800d12b09ebce882ad3e25f61c9c1940.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 02/12] pack-objects: pass --objects with --path-walk","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:04:37Z","receivedAt":"2026-05-12T01:04:39Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:12:59PM +0000, Derrick Stolee via GitGitGadget wrote:\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> When 'git pack-objects' has the --path-walk option enabled, it uses a\n> different set of revision walk parameters than normal. For once,\n\ns/once/one/ ? Not sure.\n\n> --objects was previously assumed by the path-walk API and was not needed\n\ns/was not/did not/ ? Also not sure.\n\n> ---\n>  builtin/pack-objects.c | 5 +----\n>  1 file changed, 1 insertion(+), 4 deletions(-)\n\nLooks good.\n\nThanks,\nTaylor\n"},{"id":"543118","messageId":"agJ+ORhy0O6fpJJm@nand.local","threadId":"65575","inReplyTo":"77329cf8f45b948f3986d004be72035c75ce40da.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 03/12] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:11:21Z","receivedAt":"2026-05-12T01:11:23Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:13:00PM +0000, Derrick Stolee via GitGitGadget wrote:\n> ---\n>  t/perf/p5315-pack-objects-filter.sh | 129 ++++++++++++++++++++++++++++\n>  1 file changed, 129 insertions(+)\n>  create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\nThis is great, thank you for adding these as part of the series! :-)\n\n> diff --git a/t/perf/p5315-pack-objects-filter.sh b/t/perf/p5315-pack-objects-filter.sh\n> new file mode 100755\n> index 0000000000..b009039c89\n> --- /dev/null\n> +++ b/t/perf/p5315-pack-objects-filter.sh\n> @@ -0,0 +1,129 @@\n> +#!/bin/sh\n> +\n> +test_description='Tests pack-objects performance with filters and --path-walk'\n> +. ./perf-lib.sh\n> +\n> +test_perf_large_repo\n> +\n> +test_expect_success 'setup filter inputs' '\n> +\t# Sample a few depth-2 directories from the test repo to build\n> +\t# a cone-mode sparse-checkout definition.  The sampling picks\n> +\t# directories at evenly-spaced positions so the choice is stable\n> +\t# and scales to repos of any shape.\n> +\n> +\tgit ls-tree -d --name-only HEAD >top-dirs &&\n> +\ttop_nr=$(wc -l <top-dirs) &&\n> +\n> +\t>depth2-dirs &&\n> +\twhile read tdir\n> +\tdo\n> +\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null |\n> +\t\tsed \"s|^|$tdir/|\" >>depth2-dirs || return 1\n> +\tdone <top-dirs &&\n\nNot a huge deal, but I think we should avoid having ls-tree on the\nleft-hand side of the pipe here. I would perhaps write this as:\n\n    while read tdir\n    do\n        git ls-tree -d --name-only \"HEAD:$tdir\" || return 1\n    done <top-dirs >depth2-dirs.raw &&\n    sed -e \"s|^|$tdir/|\" depth2-dirs.raw >depth2-dirs &&\n\n(Moving sed outside of the main loop cuts down on the process count, but\nisn't a huge deal. I think the main thing I wanted to suggest here was\navoiding 'git ls-tree ... | sed'.)\n\nThe rest of the test looks great to me.\n\nThanks,\nTaylor\n"},{"id":"543121","messageId":"agKBJJvBP/PndPMP@nand.local","threadId":"65575","inReplyTo":"50933cccf508f71f47d955d5aceacad0db612fc5.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 04/12] path-walk: always emit directly-requested objects","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:23:48Z","receivedAt":"2026-05-12T01:23:50Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:13:01PM +0000, Derrick Stolee via GitGitGadget wrote:\n> We can tell that a path is part of the directly-referenced objects if its\n> path name starts with '/' (other paths, including root trees never have this\n> starting character). Create a path_is_for_direct_objects() to make this\n> meaning clear, especially as we add more references in the future as we\n> integrate the path-walk API with partial clone filter options.\n\nI don't know that I have anything in the way of a better suggestion, but\nI can't help but feel like the `path_is_for_direct_objects()` check is\nsomewhat brittle as-is.\n\nI am not familiar enough with the path-walk.c internals to come up with\na good suggestion off the top of my head, but I figured I'd raise it\nhere in case you had thoughts on alternatives.\n> diff --git a/path-walk.c b/path-walk.c\n> index 6e426af433..59a7670c5b 100644\n> --- a/path-walk.c\n> +++ b/path-walk.c\n> @@ -248,6 +248,16 @@ static int add_tree_entries(struct path_walk_context *ctx,\n>  \treturn 0;\n>  }\n>\n> +/*\n> + * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n> + * were directly requested by 'pending' objects rather than discovered during\n> + * tree traversal.\n> + */\n> +static int path_is_for_direct_objects(const char *path)\n> +{\n> +\treturn path[0] == '/';\n\nIf we do end up keeping this approach, should we have a NULL check on\npath itself here? I think that could even be an ASSERT(), since\nsomething has gone wrong if we have a NULL at this point, but I'd rather\ndie by an assertion rather than a segfault here if so.\n\nThanks,\nTaylor\n"},{"id":"543122","messageId":"agKB2bNgtQcztA42@nand.local","threadId":"65575","inReplyTo":"da191e29e68bd46a56adea35ba1e7a41ff2a15c5.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 06/12] backfill: die on incompatible filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:26:49Z","receivedAt":"2026-05-12T01:26:51Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:13:03PM +0000, Derrick Stolee via GitGitGadget wrote:\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> The 'git backfill' command uses the path-walk API in a critical way: it\n> uses the objects output from the command to find the batches of missing\n> objects that should be requested from the server. Unlike 'git\n> pack-objects', we cannot fall back to another mechanism.\n>\n> The previous change added the path_walk_filter_compatible() method that\n> we can reuse here. Use it during argument validation in cmd_backfill().\n\nMakes sense.\n\n> @@ -96,9 +96,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n>  \tif (revs->explicit_diff_merges)\n>  \t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n>  \t\t    \"--diff-merges\");\n> -\tif (revs->filter.choice)\n> -\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n> -\t\t    \"--filter\");\n> +\tif (!path_walk_filter_compatible(&revs->filter))\n> +\t\tdie(_(\"cannot backfill with these filter options\"));\n\nI was going to suggest that we indicate the type of object filter which\nwas incompatible, but that gets a little tricky if the incompatible\nfilter is a child of a LOFC_COMBINE filter.\n\nResolving that does not seem worth our while, so I think that what you\nwrote here is more than sufficient.\n\nThanks,\nTaylor\n"},{"id":"543123","messageId":"agKDbUoZeiux3jdj@nand.local","threadId":"65575","inReplyTo":"a1ab70405d6209a11b2eb752c154361060f1675d.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 07/12] path-walk: support blob size limit filter","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:33:33Z","receivedAt":"2026-05-12T01:33:36Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:13:04PM +0000, Derrick Stolee via GitGitGadget wrote:\n> [...] Blobs whose size cannot be determined (e.g. missing in a partial\n> clone) are conservatively included, matching the existing filter\n> behavior.\n\nMakes sense, but...\n\n> Notice that this inspection of object sizes requires the content to be\n> present in the repository. The odb_read_object_info() call will download\n> a missing blob on-demand.\n\n... this says that we do download missing blobs on-demand. Should we be\n(temporarily) disabling 'fetch_if_missing' for this phase, or using\nodb_read_object_info_extended() with the OBJECT_INFO_SKIP_FETCH_OBJECT\nbit set?\n\nI don't know enough about 'git backfill' to know whether the current\nbehavior is more reasonable than the above suggestion, so please let me\nknow if I'm missing something here!\n\nThe rest looks good to me.\n\nThanks,\nTaylor\n"},{"id":"543125","messageId":"agKFRV4TjInVOCaO@nand.local","threadId":"65575","inReplyTo":"c9efff0ea8d72d2d084957e1b099801b8d8727f3.1778523189.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 10/12] path-walk: support `tree:0` filter","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:41:25Z","receivedAt":"2026-05-12T01:41:27Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:13:07PM +0000, Taylor Blau via GitGitGadget wrote:\n> @@ -534,6 +545,18 @@ static int setup_pending_objects(struct path_walk_info *info,\n>  \t\t\tfree(tagged_blobs);\n>  \t\t}\n>  \t}\n> +\tif (tagged_trees) {\n> +\t\tif (tagged_trees->oids.nr) {\n> +\t\t\tconst char *tagged_tree_path = \"/tagged-trees\";\n> +\t\t\ttagged_trees->type = OBJ_TREE;\n> +\t\t\ttagged_trees->maybe_interesting = 1;\n> +\t\t\tstrmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);\n> +\t\t\tpush_to_stack(ctx, tagged_tree_path);\n> +\t\t} else {\n> +\t\t\toid_array_clear(&tagged_trees->oids);\n> +\t\t\tfree(tagged_trees);\n> +\t\t}\n> +\t}\n>  \tif (tags) {\n>  \t\tif (tags->oids.nr) {\n>  \t\t\tconst char *tag_path = \"/tags\";\n\nIt looks like there is some prior art here for enumerating a sentinel\npath for \"/tags\", but I am curious why we did the same for\ndirectly-listed trees in the presence of --filter=tree:0.\n\nThanks,\nTaylor\n"},{"id":"543126","messageId":"agKFxFbMrlYn1GWt@nand.local","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 00/12] pack-objects: integrate --path-walk and some --filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-12T01:43:32Z","receivedAt":"2026-05-12T01:43:34Z","isPatch":true,"body":"On Mon, May 11, 2026 at 06:12:57PM +0000, Derrick Stolee via GitGitGadget wrote:\n> Derrick Stolee (9):\n>   t5620: make test work with path-walk var\n>   pack-objects: pass --objects with --path-walk\n>   t/perf: add pack-objects filter and path-walk benchmark\n>   path-walk: always emit directly-requested objects\n>   path-walk: support blobless filter\n>   backfill: die on incompatible filter options\n>   path-walk: support blob size limit filter\n>   path-walk: add pl_sparse_trees to control tree pruning\n>   pack-objects: support sparse:oid filter with path-walk\n>\n> Taylor Blau (3):\n>   path-walk: support `tree:0` filter\n>   path-walk: support `object:type` filter\n>   path-walk: support `combine` filter\n\nThanks so much for putting this together! I'm really excited to see this\nall moving along, and am eager to get this deployed at GitHub ;-).\n\nI left a handful of notes throughout the series. There are a couple of\ntypos I noted that I think merit a reroll, but most of my remaining\ncomments are more for my own edification than identifying anything to\nchange within your series.\n\nThanks in advance for taking a look at them. I think that the next\nreroll ought to be pretty close to the finish line.\n\nThanks,\nTaylor\n"},{"id":"543256","messageId":"b5098cab-48e7-479d-bd76-b9f17ce4a5b5@gmail.com","threadId":"65575","inReplyTo":"agJ+ORhy0O6fpJJm@nand.local","subject":"Re: [PATCH v3 03/12] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-13T18:23:06Z","receivedAt":"2026-05-13T18:23:09Z","isPatch":true,"body":"On 5/11/2026 9:11 PM, Taylor Blau wrote:\n> On Mon, May 11, 2026 at 06:13:00PM +0000, Derrick Stolee via GitGitGadget wrote:\n\n>> +\tgit ls-tree -d --name-only HEAD >top-dirs &&\n>> +\ttop_nr=$(wc -l <top-dirs) &&\n\n(I'll come back to this)\n\n>> +\t>depth2-dirs &&\n>> +\twhile read tdir\n>> +\tdo\n>> +\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null |\n>> +\t\tsed \"s|^|$tdir/|\" >>depth2-dirs || return 1\n>> +\tdone <top-dirs &&\n> \n> Not a huge deal, but I think we should avoid having ls-tree on the\n> left-hand side of the pipe here. I would perhaps write this as:\n> \n>     while read tdir\n>     do\n>         git ls-tree -d --name-only \"HEAD:$tdir\" || return 1\n>     done <top-dirs >depth2-dirs.raw &&\n>     sed -e \"s|^|$tdir/|\" depth2-dirs.raw >depth2-dirs &&\n> \n> (Moving sed outside of the main loop cuts down on the process count, but\n> isn't a huge deal. I think the main thing I wanted to suggest here was\n> avoiding 'git ls-tree ... | sed'.)\n\nThis is a good find, because the structure of this loop is hiding\na break when a \"directory\" emitted by 'git ls-tree -d' is actually\na submodule entry.\n\nI've modified the loop as well as the first ls-tree command to be\nmore resilient to these cases.\n\nThanks,\n-Stolee\n\n"},{"id":"543257","messageId":"b7465fa5-48c9-4c94-b886-0055f0696371@gmail.com","threadId":"65575","inReplyTo":"agKBJJvBP/PndPMP@nand.local","subject":"Re: [PATCH v3 04/12] path-walk: always emit directly-requested objects","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-13T18:29:11Z","receivedAt":"2026-05-13T18:29:13Z","isPatch":true,"body":"On 5/11/2026 9:23 PM, Taylor Blau wrote:\n> On Mon, May 11, 2026 at 06:13:01PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> We can tell that a path is part of the directly-referenced objects if its\n>> path name starts with '/' (other paths, including root trees never have this\n>> starting character). Create a path_is_for_direct_objects() to make this\n>> meaning clear, especially as we add more references in the future as we\n>> integrate the path-walk API with partial clone filter options.\n> \n> I don't know that I have anything in the way of a better suggestion, but\n> I can't help but feel like the `path_is_for_direct_objects()` check is\n> somewhat brittle as-is.\n> \n> I am not familiar enough with the path-walk.c internals to come up with\n> a good suggestion off the top of my head, but I figured I'd raise it\n> here in case you had thoughts on alternatives.\n\nThe brittle-ness is due to how trees and blobs without known paths are\nadded to sets that are emitted using distinct paths. The starting '/'\ncharacter does prevent the \"/tagged-blobs\" and \"/tagged-trees\" sets\nfrom ever colliding with valid paths that were discovered starting from\nroot trees (with path \"\").\n\nI could imagine a world where we store a more robust struct that has a\nflag member to indicate that these objects were not discovered via\nnormal tree walking. Changing that could lead to this method not doing\nwhat we expect it to do. But we are not in that future.\n\n>> diff --git a/path-walk.c b/path-walk.c\n>> index 6e426af433..59a7670c5b 100644\n>> --- a/path-walk.c\n>> +++ b/path-walk.c\n>> @@ -248,6 +248,16 @@ static int add_tree_entries(struct path_walk_context *ctx,\n>>  \treturn 0;\n>>  }\n>>\n>> +/*\n>> + * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n>> + * were directly requested by 'pending' objects rather than discovered during\n>> + * tree traversal.\n>> + */\n>> +static int path_is_for_direct_objects(const char *path)\n>> +{\n>> +\treturn path[0] == '/';\n> \n> If we do end up keeping this approach, should we have a NULL check on\n> path itself here? I think that could even be an ASSERT(), since\n> something has gone wrong if we have a NULL at this point, but I'd rather\n> die by an assertion rather than a segfault here if so.\nThe ASSERT() is a good idea to prevent incorrect use.\n\nThanks,\n-Stolee\n"},{"id":"543258","messageId":"130ecbf0-7456-48da-ac20-d5364a1b5577@gmail.com","threadId":"65575","inReplyTo":"agKDbUoZeiux3jdj@nand.local","subject":"Re: [PATCH v3 07/12] path-walk: support blob size limit filter","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-13T18:35:40Z","receivedAt":"2026-05-13T18:35:42Z","isPatch":true,"body":"On 5/11/2026 9:33 PM, Taylor Blau wrote:\n> On Mon, May 11, 2026 at 06:13:04PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> [...] Blobs whose size cannot be determined (e.g. missing in a partial\n>> clone) are conservatively included, matching the existing filter\n>> behavior.\n> \n> Makes sense, but...\n> \n>> Notice that this inspection of object sizes requires the content to be\n>> present in the repository. The odb_read_object_info() call will download\n>> a missing blob on-demand.\n> \n> ... this says that we do download missing blobs on-demand. Should we be\n> (temporarily) disabling 'fetch_if_missing' for this phase, or using\n> odb_read_object_info_extended() with the OBJECT_INFO_SKIP_FETCH_OBJECT\n> bit set?\n\nI don't know that we'd want to set this everywhere. The case that is\ntechnically correct, but probably not ideal, is someone running 'git\npack-objects --filter=blob:<size>' from a blobless partial clone. They\nare asking for something inefficient to create, but we _can_ still\ncreate it. \n> I don't know enough about 'git backfill' to know whether the current\n> behavior is more reasonable than the above suggestion, so please let me\n> know if I'm missing something here!\nFor 'git backfill', a size limit filter doesn't make sense as it's all\nabout the client making decisions about how to download batches of\nobjects based on the local tree data. Filtering on size isn't something\nthat the client can infer in advance of downloading everything. Further,\nthe direct blob requests to the server can't be filtered by size, by\ndesign.\n\nThanks,\n-Stolee\n\n"},{"id":"543269","messageId":"bf1e14e2-626f-432c-a77c-706eea0fcf29@gmail.com","threadId":"65575","inReplyTo":"agKFRV4TjInVOCaO@nand.local","subject":"Re: [PATCH v3 10/12] path-walk: support `tree:0` filter","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-13T19:46:16Z","receivedAt":"2026-05-13T19:46:19Z","isPatch":true,"body":"On 5/11/2026 9:41 PM, Taylor Blau wrote:\n> On Mon, May 11, 2026 at 06:13:07PM +0000, Taylor Blau via GitGitGadget wrote:\n>> @@ -534,6 +545,18 @@ static int setup_pending_objects(struct path_walk_info *info,\n>>  \t\t\tfree(tagged_blobs);\n>>  \t\t}\n>>  \t}\n>> +\tif (tagged_trees) {\n>> +\t\tif (tagged_trees->oids.nr) {\n>> +\t\t\tconst char *tagged_tree_path = \"/tagged-trees\";\n>> +\t\t\ttagged_trees->type = OBJ_TREE;\n>> +\t\t\ttagged_trees->maybe_interesting = 1;\n>> +\t\t\tstrmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);\n>> +\t\t\tpush_to_stack(ctx, tagged_tree_path);\n>> +\t\t} else {\n>> +\t\t\toid_array_clear(&tagged_trees->oids);\n>> +\t\t\tfree(tagged_trees);\n>> +\t\t}\n>> +\t}\n>>  \tif (tags) {\n>>  \t\tif (tags->oids.nr) {\n>>  \t\t\tconst char *tag_path = \"/tags\";\n> \n> It looks like there is some prior art here for enumerating a sentinel\n> path for \"/tags\", but I am curious why we did the same for\n> directly-listed trees in the presence of --filter=tree:0.\n\nIt turns out that this confusion is actually due to some major\nflaws in how the path-walk API was treating tagged trees. The\ntest setup didn't catch this because the tagged trees were also\nroot trees from commits, so this wasn't caught before.\n\nThis means that in the next version, there will be a setup\npatch that updates the test setup (and demonstrates the error\nthat those changes don't impact the rest of the tests) but\nthe new version of this patch will be substantial.\n\nThanks,\n-Stolee\n\n"},{"id":"543270","messageId":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v3.git.1778523189.gitgitgadget@gmail.com","subject":"[PATCH v4 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:42Z","receivedAt":"2026-05-13T21:18:58Z","isPatch":true,"body":"NOTE: This series is based on en/backfill-fixes-and-edges.\n\nThe 'git pack-objects' command has a '--path-walk' option that uses the\npath-walk API instead of a typical revision walk to group objects into\nchunks by path name instead of relying solely on name-hashes to group\nsimilar files together. (It also does a second compression pass looking for\nbetter deltas after the first pass that is focused within chunks per path.)\n\nThe '--path-walk' feature was not previously integrated with the '--filter'\nfeature, so a warning would appear and disable the path-walk API when a\nfilter is given. This patch series integrates these together in the\nfollowing ways:\n\n * --filter=blob:none updates the path-walk API options to skip blobs.\n * --filter=blob:limit=<size> adds a scan to a list of blob objects to\n   remove objects that are too large.\n * --filter=sparse:<oid> adds a scan to the chunks to validate that the\n   paths match the sparse-checkout patterns.\n\nIn particular, this last check is significantly faster than the previous\nalgorithm because it can check all objects at a given path simultaneously\ninstead of checking all sparse-checkout patterns for each discovered blob\nobject.\n\nA subtlety must be added here, in that we must change how we mark an object\nas \"seen\" during the path-walk. We may need to add an object to multiple\npaths and only mark it as \"seen\" if it indeed matched the sparse-checkout\npatterns as the path is accepted for emitting to the callback. This adds a\nnew filter that the \"seen\" objects must also be removed from later chunks to\navoid sending the same object as grouped to multiple chunks.\n\nThere's also a subtle detail here in that the path-walk API also prunes tree\npaths based on cone-mode sparse-checkouts, to enable 'git backfill --sparse'\noperating quickly for small sparse-checkout scopes. But the\n--filter=sparse:<oid> feature doesn't prune trees!\n\nAs a future step, I do plan to recommend that we add a treesparse:<oid>\nsetting that does allow us to trim the tree set by cone-mode sparse\npatterns. At the time that partial clone filters were being created, cone\nmode sparse-checkout didn't exist and neither did the sparse index. Those\nfeatures together make a smaller tree set possible, assuming the user never\nneeds to change their scope. This would be a significant change so it is not\nimplemented here, though the git pack-objects integration would be quick\nafter this series completes.\n\nNeither the sparse:<oid> or hypothetical treesparse:<oid> options are or\nshould necessarily be supported by Git servers. It's too expensive to\ncompute dynamically and it doesn't work well with reachability bitmaps. What\nbecomes possible with this change is that it becomes reasonably fast to\nconstruct bundles with these filters that can bootstrap a working\nenvironment with the full history of all files within a given\nsparse-checkout scope.\n\n\nPerformance Results\n===================\n\nSince the '--path-walk' option is ignored in today's Git version when a\n'--filter' is added, the performance matches the behavior without\n'--path-walk'. For the tables below, you can compare the rows against each\nother (time and then packfile size) for the mode without and then with\n'--path-walk' as a representation of \"before\" and \"after\". (These tables are\nrepeated in the commit messages as new implementations improve specific\nrows.)\n\nI chose a number of open source repositories of various sizes and shapes:\n\n\ngit/git\n=======\n\nTest                                              HEAD\n-------------------------------------------------------------------\n5315.2: repack (no filter)                       27.73\n5315.3: repack size (no filter)                 250.6M\n5315.4: repack (no filter, --path-walk)          35.19\n5315.5: repack size (no filter, --path-walk)    220.1M\n5315.6: repack (blob:none)                       13.42\n5315.7: repack size (blob:none)                 137.6M\n5315.8: repack (blob:none, --path-walk)          20.98\n5315.9: repack size (blob:none, --path-walk)    115.2M\n5315.10: repack (sparse:oid)                     72.53\n5315.11: repack size (sparse:oid)               187.5M\n5315.12: repack (sparse:oid, --path-walk)        29.00\n5315.13: repack size (sparse:oid, --path-walk)  161.0M\n\n\n\nnodejs/node\n===========\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                       75.53\n5315.3: repack size (no filter)                   0.9G\n5315.4: repack (no filter, --path-walk)          80.54\n5315.5: repack size (no filter, --path-walk)    885.7M\n5315.6: repack (blob:none)                       12.65\n5315.7: repack size (blob:none)                 148.6M\n5315.8: repack (blob:none, --path-walk)          17.60\n5315.9: repack size (blob:none, --path-walk)    134.6M\n5315.10: repack (sparse:oid)                    518.84\n5315.11: repack size (sparse:oid)               153.4M\n5315.12: repack (sparse:oid, --path-walk)        27.99\n5315.13: repack size (sparse:oid, --path-walk)  139.4M\n\n\n\nmicrosoft/fluentui\n==================\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                      146.77\n5315.3: repack size (no filter)                 562.1M\n5315.4: repack (no filter, --path-walk)          72.82\n5315.5: repack size (no filter, --path-walk)    172.6M\n5315.6: repack (blob:none)                        4.84\n5315.7: repack size (blob:none)                  62.7M\n5315.8: repack (blob:none, --path-walk)           5.19\n5315.9: repack size (blob:none, --path-walk)     59.9M\n5315.10: repack (sparse:oid)                     59.95\n5315.11: repack size (sparse:oid)                85.6M\n5315.12: repack (sparse:oid, --path-walk)        15.16\n5315.13: repack size (sparse:oid, --path-walk)   72.5M\n\n\n\nmicrosoftdocs/azure-devops-docs\n===============================\n\nTest                                               HEAD\n---------------------------------------------------------------------\n5315.2: repack (no filter)                        4.41\n5315.3: repack size (no filter)                   1.6G\n5315.4: repack (no filter, --path-walk)           6.00\n5315.5: repack size (no filter, --path-walk)      1.6G\n5315.6: repack (blob:none)                        1.35\n5315.7: repack size (blob:none)                  60.0M\n5315.8: repack (blob:none, --path-walk)           1.23\n5315.9: repack size (blob:none, --path-walk)     60.0M\n5315.10: repack (sparse:oid)                    138.24\n5315.11: repack size (sparse:oid)                84.4M\n5315.12: repack (sparse:oid, --path-walk)         1.86\n5315.13: repack size (sparse:oid, --path-walk)   84.4M\n\n\n\nPerformance conclusions\n=======================\n\nAs seen in earlier series around the '--path-walk' feature, the space\nsavings can be valuable but is not always guaranteed. When the space savings\ndoesn't happen, then the time spent is generally slower because of the\ntwo-pass mechanism. The microsoftdocs/azure-devops-docs repo demonstrates\nthis case quite clearly.\n\nHowever, even in these cases the 'sparse:<oid>' filters are much faster\nbecause of the ability to check an entire set of objects against the\nsparse-checkout patterns only once.\n\nThanks, -Stolee\n\n\nUPDATES IN V2\n=============\n\n * Rebased onto en/backfill-fixes-and-edges to properly integrate with the\n   incompatible rev-list options logic in that series.\n * Updated documentation as behavior changes. Credit to Taylor Blau for\n   presenting these suggestions in his RFC [2].\n * Added three patches of Taylor's to extend more filter options.\n\n\nUPDATES IN V3\n=============\n\nUpon realizing that the tests were not passing with\nGIT_TEST_PACK_PATH_WALK=1, I spent a lot of time reworking each patch to\npass all tests with that variable enabled. This led to a lot of meaningful\nchanges:\n\n * A new patch updates t5620-backfill.sh because they are currently failing\n   due to a check for quiet stderr checks. These changes are reverted later\n   when the filters are integrated so the warning stops being written.\n * I move the logic for the path-walk API emitting \"directly requested\"\n   objects (non-commits in the 'pending' list). This is substantial enough\n   to be its own patch.\n * The filtering logic is pulled entirely within the path-walk API instead\n   of needing integration within builtin/pack-objects.c.\n * The tree:0 filter had a lot to be desired when fetching missing objects,\n   so is substantially updated.\n * The object-type filter requires a change to the typical direct-request\n   behavior, including a new 'strict_types' member that prevents ever\n   allowing objects against type.\n * The combine filter needed better logic around multiple blob size limits,\n   to take the smaller of the two.\n * The t6601-path-walk.sh script has many test updates to better reflect the\n   new behavior, as required by the other partial clone tests under\n   GIT_TEST_PACK_PATH_WALK=1.\n * Doc updates for 'form' to 'forms' when multiple forms are supported.\n\nI've also updated Taylor's bitmap-related patches into three commits on top\nof this series (see [3]).\n\n\nUPDATES IN V4\n=============\n\nThanks, Taylor for the careful review.\n\n * Several typos are fixed.\n * The performance test is corrected for issues around piping Git commands\n   and made more robust to the existence of submodules.\n * BIG: The tree:0 patch is significantly updated in this version. Taylor\n   correctly smelled a problem with the new logic to emit the /tagged-trees\n   object set, and that signaled that those trees were previously never\n   emitted. I update the test to demonstrate that changing the data shape\n   (including tagged trees that are otherwise-unreachable) doesn't change\n   the test behavior, signaling a bug. The behavior change details all the\n   complexities of visiting only directly-requested trees under a tree:0\n   filter and recursing on all trees in other cases.\n\nP.S. I've CC'd the folks who were on the original path-walk feature thread\n[1]\n\n[1]\nhttps://lore.kernel.org/git/pull.1819.git.1741571455.gitgitgadget@gmail.com/\n\n[2] https://lore.kernel.org/git/cover.1777853408.git.me@ttaylorr.com/\n\n[3]\nhttps://github.com/derrickstolee/git/compare/path-walk-filters...derrickstolee:git:path-walk-bitmaps\n\nDerrick Stolee (10):\n  t5620: make test work with path-walk var\n  pack-objects: pass --objects with --path-walk\n  t/perf: add pack-objects filter and path-walk benchmark\n  path-walk: always emit directly-requested objects\n  path-walk: support blobless filter\n  backfill: die on incompatible filter options\n  path-walk: support blob size limit filter\n  path-walk: add pl_sparse_trees to control tree pruning\n  pack-objects: support sparse:oid filter with path-walk\n  t6601: tag otherwise-unreachable trees\n\nTaylor Blau (3):\n  path-walk: support `tree:0` filter\n  path-walk: support `object:type` filter\n  path-walk: support `combine` filter\n\n Documentation/git-backfill.adoc            |   4 +\n Documentation/git-pack-objects.adoc        |   8 +-\n Documentation/technical/api-path-walk.adoc |   7 +\n builtin/backfill.c                         |   8 +-\n builtin/pack-objects.c                     |  23 +-\n path-walk.c                                | 270 ++++++++--\n path-walk.h                                |  31 ++\n t/helper/test-path-walk.c                  |  17 +-\n t/perf/p5315-pack-objects-filter.sh        | 131 +++++\n t/t5317-pack-objects-filter-objects.sh     | 125 +++++\n t/t5620-backfill.sh                        |   8 +\n t/t6601-path-walk.sh                       | 572 +++++++++++++++++++--\n 12 files changed, 1126 insertions(+), 78 deletions(-)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\n\nbase-commit: a1ad4a0fca14cdeb55ab9fb065551b15cafa8a4f\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2101%2Fderrickstolee%2Fpath-walk-filters-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2101/derrickstolee/path-walk-filters-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2101\n\nRange-diff vs v3:\n\n  1:  0840110116 =  1:  0840110116 t5620: make test work with path-walk var\n  2:  5cc6383380 !  2:  d7c87545f3 pack-objects: pass --objects with --path-walk\n     @@ Commit message\n          pack-objects: pass --objects with --path-walk\n      \n          When 'git pack-objects' has the --path-walk option enabled, it uses a\n     -    different set of revision walk parameters than normal. For once,\n     -    --objects was previously assumed by the path-walk API and was not needed\n     -    to be added. We also needed --boundary to allow discovering\n     -    UNINTERESTING objects to use as delta bases.\n     +    different set of revision walk parameters than normal. For one,\n     +    --objects was previously assumed by the path-walk API and could be\n     +    omitted. We also needed --boundary to allow discovering UNINTERESTING\n     +    objects to use as delta bases.\n      \n          We will be updating the path-walk API soon to work with some filter\n          options. However, the revision machinery will trigger a fatal error:\n  3:  77329cf8f4 !  3:  fb8a0f9c43 t/perf: add pack-objects filter and path-walk benchmark\n     @@ t/perf/p5315-pack-objects-filter.sh (new)\n      +\t# directories at evenly-spaced positions so the choice is stable\n      +\t# and scales to repos of any shape.\n      +\n     -+\tgit ls-tree -d --name-only HEAD >top-dirs &&\n     ++\tgit ls-tree -d HEAD >top-entries &&\n     ++\tgrep \"^040000\" top-entries |\n     ++\t\tawk \"{print \\$4;}\" >top-dirs &&\n      +\ttop_nr=$(wc -l <top-dirs) &&\n      +\n      +\t>depth2-dirs &&\n      +\twhile read tdir\n      +\tdo\n     -+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null |\n     -+\t\tsed \"s|^|$tdir/|\" >>depth2-dirs || return 1\n     -+\tdone <top-dirs &&\n     ++\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n     ++\tdone <top-dirs >depth2-dirs.raw &&\n     ++\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n      +\n      +\td2_nr=$(wc -l <depth2-dirs) &&\n      +\n  4:  50933cccf5 !  4:  e77c8a6bbc path-walk: always emit directly-requested objects\n     @@ path-walk.c: static int add_tree_entries(struct path_walk_context *ctx,\n      + */\n      +static int path_is_for_direct_objects(const char *path)\n      +{\n     ++\tASSERT(path);\n      +\treturn path[0] == '/';\n      +}\n      +\n  5:  b2deb7f819 !  5:  f4904f81e0 path-walk: support blobless filter\n     @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1\n      -`--use-bitmap-index` option will be ignored in the presence of\n      -`--path-walk.`\n      +Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     -+ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     ++ignored in the presence of `--path-walk`. The `--path-walk` option\n      +supports the `--filter=<spec>` form `blob:none`.\n       \n       \n  6:  da191e29e6 =  6:  f37467e46f backfill: die on incompatible filter options\n  7:  a1ab70405d !  7:  133c1b156c path-walk: support blob size limit filter\n     @@ Documentation/git-pack-objects.adoc\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       +\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     - ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     + ignored in the presence of `--path-walk`. The `--path-walk` option\n      -supports the `--filter=<spec>` form `blob:none`.\n      +supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n       \n  8:  2360a5be81 =  8:  0f517be8e3 path-walk: add pl_sparse_trees to control tree pruning\n  9:  d9f5a98830 !  9:  b4dc09ab69 pack-objects: support sparse:oid filter with path-walk\n     @@ Documentation/git-pack-objects.adoc\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       +\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     - ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     + ignored in the presence of `--path-walk`. The `--path-walk` option\n      -supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n      +supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n      +`sparse:<oid>`.\n  -:  ---------- > 10:  0b1eed0790 t6601: tag otherwise-unreachable trees\n 10:  c9efff0ea8 ! 11:  b23244c4c2 path-walk: support `tree:0` filter\n     @@ Commit message\n          at all under `tree:0`, which matches the intended behavior of the\n          filter.\n      \n     +    However, this requires first fixing some issues with how the path-walk\n     +    API handles directly-requested trees _and_ trees requested through\n     +    lightweight tags. These changes create substantial updates to\n     +    t6601-path-walk.sh, which the previous change highlighted as a problem\n     +    by tagging otherwise-unreachable trees and having them not appear in the\n     +    output.\n     +\n          Non-zero tree-depth filters are not supported. Those depend on the depth\n          at which a tree is visited, which is a path-walk concept the filter\n          machinery doesn't currently share with the path-walk API. Reject them in\n     @@ Documentation/git-pack-objects.adoc\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       +\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     - ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     + ignored in the presence of `--path-walk`. The `--path-walk` option\n      -supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n      -`sparse:<oid>`.\n      +supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n     @@ path-walk.c: static int walk_path(struct path_walk_context *ctx,\n       \n      -\t/* Expand data for children. */\n      -\tif (list->type == OBJ_TREE) {\n     -+\t/* Expand data for children, unless this is a direct-object path. */\n     -+\tif (list->type == OBJ_TREE && !path_is_for_direct_objects(path)) {\n     ++\t/*\n     ++\t * Expand tree children, except when the set is directly requested\n     ++\t * _and_ we are otherwise filtering out trees.\n     ++\t */\n     ++\tif (list->type == OBJ_TREE &&\n     ++\t    (!path_is_for_direct_objects(path) || ctx->info->trees)) {\n     ++\t\t/* Use root path if expanding from tagged/direct trees. */\n     ++\t\tconst char *expand_path = !strcmp(path, \"/tagged-trees\")\n     ++\t\t\t\t\t  ? root_path : path;\n       \t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n       \t\t\tret |= add_tree_entries(ctx,\n     - \t\t\t\t\t    path,\n     +-\t\t\t\t\t    path,\n     ++\t\t\t\t\t    expand_path,\n     + \t\t\t\t\t    &list->oids.oid[i]);\n     + \t\t}\n     + \t}\n      @@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n       {\n       \tstruct type_and_oid_list *tags = NULL;\n       \tstruct type_and_oid_list *tagged_blobs = NULL;\n     +-\tstruct type_and_oid_list *root_tree_list = NULL;\n      +\tstruct type_and_oid_list *tagged_trees = NULL;\n     - \tstruct type_and_oid_list *root_tree_list = NULL;\n       \n       \tif (info->tags)\n       \t\tCALLOC_ARRAY(tags, 1);\n       \tCALLOC_ARRAY(tagged_blobs, 1);\n     -+\tif (!info->trees)\n     -+\t\tCALLOC_ARRAY(tagged_trees, 1);\n     - \troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n     +-\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n     ++\tCALLOC_ARRAY(tagged_trees, 1);\n       \n       \t/*\n     + \t * Pending objects include:\n      @@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n       \n       \t\tswitch (obj->type) {\n       \t\tcase OBJ_TREE:\n      -\t\t\tif (pending->path) {\n     -+\t\t\tif (tagged_trees) {\n     -+\t\t\t\t/*\n     -+\t\t\t\t * Trees are disabled but pending trees\n     -+\t\t\t\t * should still be emitted. Collect them\n     -+\t\t\t\t * into a \"/tagged-trees\" list that\n     -+\t\t\t\t * bypasses the object type filter.\n     -+\t\t\t\t */\n     -+\t\t\t\toid_array_append(&tagged_trees->oids, &obj->oid);\n     -+\t\t\t} else if (pending->path) {\n     - \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n     - \t\t\t\t\t\t\t    : xstrdup(\"\");\n     +-\t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n     +-\t\t\t\t\t\t\t    : xstrdup(\"\");\n     ++\t\t\tif (pending->path && *pending->path) {\n     ++\t\t\t\tchar *path = xstrfmt(\"%s/\", pending->path);\n       \t\t\t\tadd_path_to_list(ctx, path, OBJ_TREE, &obj->oid, 1);\n     + \t\t\t\tfree(path);\n     ++\t\t\t} else if (!pending->path || !info->trees) {\n     ++\t\t\t\toid_array_append(&tagged_trees->oids, &obj->oid);\n     + \t\t\t} else {\n     +-\t\t\t\t/* assume a root tree, such as a lightweight tag. */\n     +-\t\t\t\toid_array_append(&root_tree_list->oids, &obj->oid);\n     ++\t\t\t\tadd_path_to_list(ctx, root_path, OBJ_TREE,\n     ++\t\t\t\t\t\t &obj->oid, 1);\n     + \t\t\t}\n     + \t\t\tbreak;\n     + \n      @@ path-walk.c: static int setup_pending_objects(struct path_walk_info *info,\n       \t\t\tfree(tagged_blobs);\n       \t\t}\n     @@ path-walk.c: static int prepare_filters(struct path_walk_info *info,\n       \t\t\tstruct object_id sparse_oid;\n      \n       ## t/t6601-path-walk.sh ##\n     +@@ t/t6601-path-walk.sh: test_expect_success 'all' '\n     + \t3:tree::$(git rev-parse base^{tree})\n     + \t3:tree::$(git rev-parse base~1^{tree})\n     + \t3:tree::$(git rev-parse base~2^{tree})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     + \t4:blob:a:$(git rev-parse base~2:a)\n     +-\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n     +-\t6:tree:a/:$(git rev-parse base:a)\n     +-\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     +-\t8:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n     +-\t9:tree:left/:$(git rev-parse base:left)\n     +-\t9:tree:left/:$(git rev-parse base~2:left)\n     +-\t10:blob:left/b:$(git rev-parse base~2:left/b)\n     +-\t10:blob:left/b:$(git rev-parse base:left/b)\n     +-\t11:tree:right/:$(git rev-parse topic:right)\n     +-\t11:tree:right/:$(git rev-parse base~1:right)\n     +-\t11:tree:right/:$(git rev-parse base~2:right)\n     +-\t12:blob:right/c:$(git rev-parse base~2:right/c)\n     +-\t12:blob:right/c:$(git rev-parse topic:right/c)\n     +-\t13:blob:right/d:$(git rev-parse base~1:right/d)\n     ++\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n     ++\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n     ++\t7:tree:a/:$(git rev-parse base:a)\n     ++\t8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t9:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n     ++\t10:tree:left/:$(git rev-parse base:left)\n     ++\t10:tree:left/:$(git rev-parse base~2:left)\n     ++\t11:blob:left/b:$(git rev-parse base~2:left/b)\n     ++\t11:blob:left/b:$(git rev-parse base:left/b)\n     ++\t12:tree:right/:$(git rev-parse topic:right)\n     ++\t12:tree:right/:$(git rev-parse base~1:right)\n     ++\t12:tree:right/:$(git rev-parse base~2:right)\n     ++\t13:blob:right/c:$(git rev-parse base~2:right/c)\n     ++\t13:blob:right/c:$(git rev-parse topic:right/c)\n     ++\t14:blob:right/d:$(git rev-parse base~1:right/d)\n     + \tblobs:10\n     + \tcommits:4\n     + \ttags:7\n     +@@ t/t6601-path-walk.sh: test_expect_success 'all, blob:none filter' '\n     + \t3:tree::$(git rev-parse base^{tree})\n     + \t3:tree::$(git rev-parse base~1^{tree})\n     + \t3:tree::$(git rev-parse base~2^{tree})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     +-\t4:tree:a/:$(git rev-parse base:a)\n     +-\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     +-\t6:tree:left/:$(git rev-parse base:left)\n     +-\t6:tree:left/:$(git rev-parse base~2:left)\n     +-\t7:tree:right/:$(git rev-parse topic:right)\n     +-\t7:tree:right/:$(git rev-parse base~1:right)\n     +-\t7:tree:right/:$(git rev-parse base~2:right)\n     ++\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n     ++\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t5:tree:a/:$(git rev-parse base:a)\n     ++\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t7:tree:left/:$(git rev-parse base:left)\n     ++\t7:tree:left/:$(git rev-parse base~2:left)\n     ++\t8:tree:right/:$(git rev-parse topic:right)\n     ++\t8:tree:right/:$(git rev-parse base~1:right)\n     ++\t8:tree:right/:$(git rev-parse base~2:right)\n     + \tblobs:2\n     + \tcommits:4\n     + \ttags:7\n     +@@ t/t6601-path-walk.sh: test_expect_success 'all, blob:limit=0 filter' '\n     + \t3:tree::$(git rev-parse base^{tree})\n     + \t3:tree::$(git rev-parse base~1^{tree})\n     + \t3:tree::$(git rev-parse base~2^{tree})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     +-\t4:tree:a/:$(git rev-parse base:a)\n     +-\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     +-\t6:tree:left/:$(git rev-parse base:left)\n     +-\t6:tree:left/:$(git rev-parse base~2:left)\n     +-\t7:tree:right/:$(git rev-parse topic:right)\n     +-\t7:tree:right/:$(git rev-parse base~1:right)\n     +-\t7:tree:right/:$(git rev-parse base~2:right)\n     ++\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n     ++\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t5:tree:a/:$(git rev-parse base:a)\n     ++\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t7:tree:left/:$(git rev-parse base:left)\n     ++\t7:tree:left/:$(git rev-parse base~2:left)\n     ++\t8:tree:right/:$(git rev-parse topic:right)\n     ++\t8:tree:right/:$(git rev-parse base~1:right)\n     ++\t8:tree:right/:$(git rev-parse base~2:right)\n     + \tblobs:2\n     + \tcommits:4\n     + \ttags:7\n     +@@ t/t6601-path-walk.sh: test_expect_success 'all, blob:limit=3 filter' '\n     + \t3:tree::$(git rev-parse base^{tree})\n     + \t3:tree::$(git rev-parse base~1^{tree})\n     + \t3:tree::$(git rev-parse base~2^{tree})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     + \t4:blob:a:$(git rev-parse base~2:a)\n     +-\t5:tree:a/:$(git rev-parse base:a)\n     +-\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     +-\t7:tree:left/:$(git rev-parse base:left)\n     +-\t7:tree:left/:$(git rev-parse base~2:left)\n     +-\t8:blob:left/b:$(git rev-parse base~2:left/b)\n     +-\t9:tree:right/:$(git rev-parse topic:right)\n     +-\t9:tree:right/:$(git rev-parse base~1:right)\n     +-\t9:tree:right/:$(git rev-parse base~2:right)\n     +-\t10:blob:right/c:$(git rev-parse base~2:right/c)\n     +-\t11:blob:right/d:$(git rev-parse base~1:right/d)\n     ++\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n     ++\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t6:tree:a/:$(git rev-parse base:a)\n     ++\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t8:tree:left/:$(git rev-parse base:left)\n     ++\t8:tree:left/:$(git rev-parse base~2:left)\n     ++\t9:blob:left/b:$(git rev-parse base~2:left/b)\n     ++\t10:tree:right/:$(git rev-parse topic:right)\n     ++\t10:tree:right/:$(git rev-parse base~1:right)\n     ++\t10:tree:right/:$(git rev-parse base~2:right)\n     ++\t11:blob:right/c:$(git rev-parse base~2:right/c)\n     ++\t12:blob:right/d:$(git rev-parse base~1:right/d)\n     + \tblobs:6\n     + \tcommits:4\n     + \ttags:7\n      @@ t/t6601-path-walk.sh: test_expect_success 'all, blob:limit=3 filter' '\n       \ttest_cmp_sorted expect out\n       '\n     @@ t/t6601-path-walk.sh: test_expect_success 'all, blob:limit=3 filter' '\n       test_expect_success 'setup sparse filter blob' '\n       \t# Cone-mode patterns: include root, exclude all dirs, include left/\n       \tcat >patterns <<-\\EOF &&\n     +@@ t/t6601-path-walk.sh: test_expect_success 'all, sparse:oid filter' '\n     + \t3:tree::$(git rev-parse base^{tree})\n     + \t3:tree::$(git rev-parse base~1^{tree})\n     + \t3:tree::$(git rev-parse base~2^{tree})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n     +-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     + \t4:blob:a:$(git rev-parse base~2:a)\n     +-\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n     +-\t6:tree:a/:$(git rev-parse base:a)\n     +-\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     +-\t8:tree:left/:$(git rev-parse base:left)\n     +-\t8:tree:left/:$(git rev-parse base~2:left)\n     +-\t9:blob:left/b:$(git rev-parse base~2:left/b)\n     +-\t9:blob:left/b:$(git rev-parse base:left/b)\n     +-\t10:tree:right/:$(git rev-parse topic:right)\n     +-\t10:tree:right/:$(git rev-parse base~1:right)\n     +-\t10:tree:right/:$(git rev-parse base~2:right)\n     ++\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n     ++\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n     ++\t7:tree:a/:$(git rev-parse base:a)\n     ++\t8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t9:tree:left/:$(git rev-parse base:left)\n     ++\t9:tree:left/:$(git rev-parse base~2:left)\n     ++\t10:blob:left/b:$(git rev-parse base~2:left/b)\n     ++\t10:blob:left/b:$(git rev-parse base:left/b)\n     ++\t11:tree:right/:$(git rev-parse topic:right)\n     ++\t11:tree:right/:$(git rev-parse base~1:right)\n     ++\t11:tree:right/:$(git rev-parse base~2:right)\n     + \tblobs:6\n     + \tcommits:4\n     + \ttags:7\n 11:  b221ea4712 ! 12:  7e1e503361 path-walk: support `object:type` filter\n     @@ Commit message\n       ## Documentation/git-pack-objects.adoc ##\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     - ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     + ignored in the presence of `--path-walk`. The `--path-walk` option\n       supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n      -`tree:0`, and `sparse:<oid>`.\n      +`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n     @@ t/t6601-path-walk.sh: test_expect_success 'tree:1 filter is rejected' '\n      +\t0:tree::$(git rev-parse base^{tree})\n      +\t0:tree::$(git rev-parse base~1^{tree})\n      +\t0:tree::$(git rev-parse base~2^{tree})\n     -+\t0:tree::$(git rev-parse refs/tags/tree-tag^{})\n     -+\t0:tree::$(git rev-parse refs/tags/tree-tag2^{})\n     -+\t1:tree:a/:$(git rev-parse base:a)\n     -+\t2:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     -+\t3:tree:left/:$(git rev-parse base:left)\n     -+\t3:tree:left/:$(git rev-parse base~2:left)\n     -+\t4:tree:right/:$(git rev-parse topic:right)\n     -+\t4:tree:right/:$(git rev-parse base~1:right)\n     -+\t4:tree:right/:$(git rev-parse base~2:right)\n     ++\t1:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n     ++\t1:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n     ++\t2:tree:a/:$(git rev-parse base:a)\n     ++\t3:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n     ++\t4:tree:left/:$(git rev-parse base:left)\n     ++\t4:tree:left/:$(git rev-parse base~2:left)\n     ++\t5:tree:right/:$(git rev-parse topic:right)\n     ++\t5:tree:right/:$(git rev-parse base~1:right)\n     ++\t5:tree:right/:$(git rev-parse base~2:right)\n      +\tblobs:0\n      +\tcommits:0\n      +\ttags:0\n 12:  16bd3c3161 ! 13:  a615b1a707 path-walk: support `combine` filter\n     @@ Commit message\n       ## Documentation/git-pack-objects.adoc ##\n      @@ Documentation/git-pack-objects.adoc: will be automatically changed to version `1`.\n       Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n     - ignored in the presence of `--path-walk`. Whe `--path-walk` option\n     + ignored in the presence of `--path-walk`. The `--path-walk` option\n       supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n      -`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n      +`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter\n\n-- \ngitgitgadget\n"},{"id":"543271","messageId":"0840110116a967d39d8b63dbfe822ea9b2500088.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 01/13] t5620: make test work with path-walk var","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:43Z","receivedAt":"2026-05-13T21:19:00Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe GIT_TEST_PACK_PATH_WALK test variable allows enabling the\n--path-walk option to 'git pack-objects' by default. This sometimes\nengages the warning that --path-walk is incompatible with the --filter\noption. These tests in t5620 fail due to this warning over stderr in\nthis case. Disable this variable for this moment until these options\nwork together.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t5620-backfill.sh | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..e174290787 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -298,6 +298,9 @@ test_expect_success 'backfill with prefix pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -315,6 +318,9 @@ test_expect_success 'backfill with multiple pathspecs' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -332,6 +338,9 @@ test_expect_success 'backfill with wildcard pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n \ttest_must_be_empty err &&\n \n-- \ngitgitgadget\n\n"},{"id":"543272","messageId":"d7c87545f38a2457031bb2e75a51b464a1525ed6.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 02/13] pack-objects: pass --objects with --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:44Z","receivedAt":"2026-05-13T21:19:02Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen 'git pack-objects' has the --path-walk option enabled, it uses a\ndifferent set of revision walk parameters than normal. For one,\n--objects was previously assumed by the path-walk API and could be\nomitted. We also needed --boundary to allow discovering UNINTERESTING\nobjects to use as delta bases.\n\nWe will be updating the path-walk API soon to work with some filter\noptions. However, the revision machinery will trigger a fatal error:\n\n  fatal: object filtering requires --objects\n\nThe fix is easy: add the --objects option as an argument. This has no\neffect on the path-walk API but does simplify the revision option\nparsing for the objects filter.\n\nWe can remove the comment about \"removing\" the options because they were\nnever removed and instead not added. We still need to disable using\nbitmaps.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex dd2480a73d..4338962904 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5190,10 +5190,7 @@ int cmd_pack_objects(int argc,\n \t}\n \tif (path_walk) {\n \t\tstrvec_push(&rp, \"--boundary\");\n-\t\t /*\n-\t\t  * We must disable the bitmaps because we are removing\n-\t\t  * the --objects / --objects-edge[-aggressive] options.\n-\t\t  */\n+\t\tstrvec_push(&rp, \"--objects\");\n \t\tuse_bitmap_index = 0;\n \t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n-- \ngitgitgadget\n\n"},{"id":"543273","messageId":"fb8a0f9c43d4e41712839a93c4db6a294a7b5285.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 03/13] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:45Z","receivedAt":"2026-05-13T21:19:04Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd p5315-pack-objects-filter.sh to measure the performance of\n'git pack-objects --revs --all' under different filter and traversal\ncombinations:\n\n * no filter (baseline)\n * --filter=blob:none (blobless)\n * --filter=sparse:oid=<oid> (cone-mode sparse)\n\nEach filter scenario is tested both with and without --path-walk,\nproducing paired measurements that show the impact of the path-walk\ntraversal for each filter type as we integrate the --path-walk feature\nwith different --filter options. It currently has no integration so\nfalls back to the standard revision walk. Thus, there are no significant\ndifferences in the current results other than a full repack (and even\nthen, the --path-walk feature is not incredibly different for the\ndefault Git repository):\n\nTest                                             HEAD\n-----------------------------------------------------\n5315.2: repack (no filter)                      27.91\n5315.3: repack size (no filter)                250.7M\n5315.4: repack (no filter, --path-walk)         34.92\n5315.5: repack size (no filter, --path-walk)   220.0M\n5315.6: repack (blob:none)                      13.63\n5315.7: repack size (blob:none)                137.6M\n5315.8: repack (blob:none, --path-walk)         13.48\n5315.9: repack size (blob:none, --path-walk)   137.7M\n5315.10: repack (sparse:oid)                    72.67\n5315.11: repack size (sparse:oid)              187.4M\n5315.12: repack (sparse:oid, --path-walk)       72.47\n5315.13: repack size (sparse:oid, --path-walk) 187.4M\n\nThe sparse filter definition is built automatically by sampling\ndepth-2 directories from the test repository, making the test work\non any repo passed via GIT_PERF_LARGE_REPO. For repos that lack\ndepth-2 directories, a single top-level directory is used; for flat\nrepos, the sparse tests are skipped via prerequisite.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5315-pack-objects-filter.sh | 131 ++++++++++++++++++++++++++++\n 1 file changed, 131 insertions(+)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\ndiff --git a/t/perf/p5315-pack-objects-filter.sh b/t/perf/p5315-pack-objects-filter.sh\nnew file mode 100755\nindex 0000000000..21056abfc0\n--- /dev/null\n+++ b/t/perf/p5315-pack-objects-filter.sh\n@@ -0,0 +1,131 @@\n+#!/bin/sh\n+\n+test_description='Tests pack-objects performance with filters and --path-walk'\n+. ./perf-lib.sh\n+\n+test_perf_large_repo\n+\n+test_expect_success 'setup filter inputs' '\n+\t# Sample a few depth-2 directories from the test repo to build\n+\t# a cone-mode sparse-checkout definition.  The sampling picks\n+\t# directories at evenly-spaced positions so the choice is stable\n+\t# and scales to repos of any shape.\n+\n+\tgit ls-tree -d HEAD >top-entries &&\n+\tgrep \"^040000\" top-entries |\n+\t\tawk \"{print \\$4;}\" >top-dirs &&\n+\ttop_nr=$(wc -l <top-dirs) &&\n+\n+\t>depth2-dirs &&\n+\twhile read tdir\n+\tdo\n+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n+\tdone <top-dirs >depth2-dirs.raw &&\n+\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n+\n+\td2_nr=$(wc -l <depth2-dirs) &&\n+\n+\tif test \"$d2_nr\" -ge 2\n+\tthen\n+\t\t# Pick two directories from evenly-spaced positions.\n+\t\tfirst=$(sed -n \"1p\" depth2-dirs) &&\n+\t\tmid=$(sed -n \"$((d2_nr / 2 + 1))p\" depth2-dirs) &&\n+\n+\t\tp1=$(dirname \"$first\") &&\n+\t\tp2=$(dirname \"$mid\") &&\n+\n+\t\t# Build cone-mode sparse-checkout patterns.\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$p1/\" &&\n+\t\t\techo \"!/$p1/*/\" &&\n+\t\t\tif test \"$p1\" != \"$p2\"\n+\t\t\tthen\n+\t\t\t\techo \"/$p2/\" &&\n+\t\t\t\techo \"!/$p2/*/\"\n+\t\t\tfi &&\n+\t\t\techo \"/$first/\" &&\n+\t\t\tif test \"$first\" != \"$mid\"\n+\t\t\tthen\n+\t\t\t\techo \"/$mid/\"\n+\t\t\tfi\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first $mid\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\telif test \"$top_nr\" -ge 1\n+\tthen\n+\t\t# Fallback: use a single top-level directory.\n+\t\tfirst=$(sed -n \"1p\" top-dirs) &&\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$first/\"\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\tfi\n+'\n+\n+test_perf 'repack (no filter)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (no filter, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"543274","messageId":"e77c8a6bbc22da3428751f81ff5ee79aa5364237.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 04/13] path-walk: always emit directly-requested objects","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:46Z","receivedAt":"2026-05-13T21:19:05Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWe are preparing to integrate the path-walk API with some --filter options\nin 'git pack-objects', but there is a subtle issue that is revealed when\nthose are put together and the test suite is run with\nGIT_TEST_PACK_PATH_WALK=1.\n\nWhen a filter reduces the set of requested objects, this results in\nfiltering out directly-requested objects, such as in the download of needed\nblobs in a blobless partial clone.\n\nThe root cause is that the scan of pending objects in the path-walk API\nrespects the filters set in the path_walk_info instead of overriding them\nfor pending objects.\n\nWe can tell that a path is part of the directly-referenced objects if its\npath name starts with '/' (other paths, including root trees never have this\nstarting character). Create a path_is_for_direct_objects() to make this\nmeaning clear, especially as we add more references in the future as we\nintegrate the path-walk API with partial clone filter options.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.adoc |  7 ++++\n path-walk.c                                | 42 ++++++++++++++--------\n path-walk.h                                |  5 +++\n 3 files changed, 39 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.adoc b/Documentation/technical/api-path-walk.adoc\nindex a67de1b143..6e17b13d61 100644\n--- a/Documentation/technical/api-path-walk.adoc\n+++ b/Documentation/technical/api-path-walk.adoc\n@@ -48,6 +48,13 @@ commits.\n \tapplications could disable some options to make it simpler to walk\n \tthe objects or to have fewer calls to `path_fn`.\n +\n+Note that objects directly requested as pending objects (such as targets\n+of lightweight tags or other ref tips) are always emitted to `path_fn`,\n+even when the corresponding type flag is disabled. Only objects\n+discovered during the tree walk are subject to these type filters. This\n+ensures that objects specifically requested through the revision input\n+are never silently dropped.\n++\n While it is possible to walk only commits in this way, consumers would be\n better off using the revision walk API instead.\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 6e426af433..05bfc1c114 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -248,6 +248,17 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \treturn 0;\n }\n \n+/*\n+ * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n+ * were directly requested by 'pending' objects rather than discovered during\n+ * tree traversal.\n+ */\n+static int path_is_for_direct_objects(const char *path)\n+{\n+\tASSERT(path);\n+\treturn path[0] == '/';\n+}\n+\n /*\n  * For each path in paths_to_explore, walk the trees another level\n  * and add any found blobs to the batch (but only if they exist and\n@@ -306,14 +317,19 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tif (list->type == OBJ_BLOB &&\n \t    ctx->revs->prune_data.nr &&\n+\t    !path_is_for_direct_objects(path) &&\n \t    !match_pathspec(ctx->repo->index, &ctx->revs->prune_data,\n \t\t\t   path, strlen(path), 0,\n \t\t\t   NULL, 0))\n \t\treturn 0;\n \n-\t/* Evaluate function pointer on this data, if requested. */\n-\tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n-\t    (list->type == OBJ_BLOB && ctx->info->blobs) ||\n+\t/*\n+\t * Evaluate function pointer on this data, if requested.\n+\t * Ignore object type filters for tagged objects (path starts\n+\t * with `/`).\n+\t */\n+\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n+\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n \t    (list->type == OBJ_TAG && ctx->info->tags))\n \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n \t\t\t\t\tctx->info->path_fn_data);\n@@ -374,10 +390,8 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \tif (info->tags)\n \t\tCALLOC_ARRAY(tags, 1);\n-\tif (info->blobs)\n-\t\tCALLOC_ARRAY(tagged_blobs, 1);\n-\tif (info->trees)\n-\t\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n+\tCALLOC_ARRAY(tagged_blobs, 1);\n+\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n \n \t/*\n \t * Pending objects include:\n@@ -421,8 +435,6 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (!info->trees)\n-\t\t\t\tcontinue;\n \t\t\tif (pending->path) {\n \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n \t\t\t\t\t\t\t    : xstrdup(\"\");\n@@ -435,8 +447,6 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\t\tbreak;\n \n \t\tcase OBJ_BLOB:\n-\t\t\tif (!info->blobs)\n-\t\t\t\tcontinue;\n \t\t\tif (pending->path)\n \t\t\t\tadd_path_to_list(ctx, pending->path, OBJ_BLOB, &obj->oid, 1);\n \t\t\telse\n@@ -532,15 +542,17 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tpush_to_stack(&ctx, root_path);\n \n \t/*\n-\t * Set these values before preparing the walk to catch\n-\t * lightweight tags pointing to non-commits and indexed objects.\n+\t * Ensure that prepare_revision_walk() keeps all pending objects\n+\t * even through an object type filter.\n \t */\n-\tinfo->revs->blob_objects = info->blobs;\n-\tinfo->revs->tree_objects = info->trees;\n+\tinfo->revs->blob_objects = info->revs->tree_objects = 1;\n \n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n+\tinfo->revs->blob_objects = info->blobs;\n+\tinfo->revs->tree_objects = info->trees;\n+\n \t/*\n \t * Walk trees to mark them as UNINTERESTING.\n \t * This is particularly important when 'edge_aggressive' is set.\ndiff --git a/path-walk.h b/path-walk.h\nindex 5ef5a8440e..657eeda8ec 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -36,6 +36,11 @@ struct path_walk_info {\n \t/**\n \t * Initialize which object types the path_fn should be called on. This\n \t * could also limit the walk to skip blobs if not set.\n+\t *\n+\t * Note: even when 'blobs' or 'trees' is disabled, objects that are\n+\t * directly requested as pending objects will still be emitted to\n+\t * path_fn. Only objects discovered during the tree walk are filtered by\n+\t * these flags.\n \t */\n \tint commits;\n \tint trees;\n-- \ngitgitgadget\n\n"},{"id":"543275","messageId":"f4904f81e0caae12ac1cb8bb30ed58fdb8889c50.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 05/13] path-walk: support blobless filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:47Z","receivedAt":"2026-05-13T21:19:07Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git pack-objects' command can opt-in to using the path-walk API for\nscanning the objects. Currently, this option is dynamically disabled if\ncombined with '--filter=<X>', even when using a simple filter such as\n'blob:none' to signal a blobless packfile. This is a common scenario for\nrepos at scale, so is worth integrating.\n\nAlso, users can opt-in to the '--path-walk' option by default through\nthe pack.usePathWalk=true config option. When using that in a blobless\npartial clone, the following warning can appear even though the user did\nnot specify either option directly:\n\n  warning: cannot use --filter with --path-walk\n\nTeach the path-walk API to handle the 'blob:none' object filter\nnatively. When revs->filter.choice is LOFC_BLOB_NONE, the path-walk\nsets info->blobs to 0 (skipping all blob objects) and clears the\nfilter from revs so that prepare_revision_walk() does not reject the\nconfiguration.\n\nThis check is implemented in the static prepare_filters() method, which\nwill simultaneously check if the input filters are compatible and will\nmake the appropriate mutations to the path_walk_info and filters if the\npath_walk_info is non-NULL. This allows us to use this logic both in the\nAPI method path_walk_filter_compatible() for use in\nbuiltin/pack-objects.c and as a prep step in walk_objects_by_path().\n\nUpdate the test helper (test-path-walk) to accept --filter=<spec>\nas a test-tool option (before '--'), applying it to revs after\nsetup_revisions() to avoid the --objects requirement check. We can also\nrevert recent GIT_TEST_PACK_PATH_WALK overrides in t5620.\n\nAlso switch test-path-walk from REV_INFO_INIT with manual repo\nassignment to repo_init_revisions(), which properly initializes\nthe filter_spec strbuf needed for filter parsing.\n\nAdd tests for blob:none with --all and with a single branch.\n\nThe performance test p5315 shows the impact of this change when using\nblobless filters:\n\nTest                                           HEAD~1     HEAD\n---------------------------------------------------------------------\n5315.6: repack (blob:none)                      13.53   13.87  +2.5%\n5315.7: repack size (blob:none)                137.7M  137.8M  +0.1%\n5315.8: repack (blob:none, --path-walk)         13.51   23.43 +73.4%\n5315.9: repack size (blob:none, --path-walk)   137.7M  115.2M -16.3%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (16% smaller for blobless packs)\nat the cost of increased computation time due to the two compression\npasses. This data demonstrates that the feature is engaged and provides\nreal compression benefits when --no-reuse-delta forces fresh deltas.\n\nCo-Authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  6 +--\n builtin/pack-objects.c              |  2 +-\n path-walk.c                         | 30 ++++++++++++++\n path-walk.h                         |  7 ++++\n t/helper/test-path-walk.c           | 11 ++++-\n t/t5620-backfill.sh                 |  9 -----\n t/t6601-path-walk.sh                | 62 +++++++++++++++++++++++++++++\n 7 files changed, 113 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex b78175fbe1..2994faf988 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -402,9 +402,9 @@ will be automatically changed to version `1`.\n \tof filenames that cause collisions in Git's default name-hash\n \talgorithm.\n +\n-Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n-`--use-bitmap-index` option will be ignored in the presence of\n-`--path-walk.`\n+Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n+ignored in the presence of `--path-walk`. The `--path-walk` option\n+supports the `--filter=<spec>` form `blob:none`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 4338962904..bc9fb5b457 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5177,7 +5177,7 @@ int cmd_pack_objects(int argc,\n \n \tif (path_walk) {\n \t\tconst char *option = NULL;\n-\t\tif (filter_options.choice)\n+\t\tif (!path_walk_filter_compatible(&filter_options))\n \t\t\toption = \"--filter\";\n \t\telse if (use_delta_islands)\n \t\t\toption = \"--delta-islands\";\ndiff --git a/path-walk.c b/path-walk.c\nindex 05bfc1c114..bd81508163 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -9,6 +9,7 @@\n #include \"hashmap.h\"\n #include \"hex.h\"\n #include \"list-objects.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -495,6 +496,32 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tswitch (options->choice) {\n+\tcase LOFC_DISABLED:\n+\t\treturn 1;\n+\n+\tcase LOFC_BLOB_NONE:\n+\t\tif (info) {\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n+\tdefault:\n+\t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n+\t\t      list_objects_filter_spec(options));\n+\t\treturn 0;\n+\t}\n+}\n+\n+int path_walk_filter_compatible(struct list_objects_filter_options *options)\n+{\n+\treturn prepare_filters(NULL, options);\n+}\n+\n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\n  * call 'info->path_fn' on each discovered path.\n@@ -522,6 +549,9 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tif (!prepare_filters(info, &info->revs->filter))\n+\t\treturn -1;\n+\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 657eeda8ec..a1736ecb2b 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -90,3 +90,10 @@ void path_walk_info_clear(struct path_walk_info *info);\n  * Returns nonzero on an error.\n  */\n int walk_objects_by_path(struct path_walk_info *info);\n+\n+struct list_objects_filter_options;\n+/**\n+ * Given a set of options for filtering objects, return 1 if the options\n+ * are compatible with the path-walk API and 0 otherwise.\n+ */\n+int path_walk_filter_compatible(struct list_objects_filter_options *options);\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex fe63002c2b..88f86ae0dc 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -4,6 +4,7 @@\n #include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object-name.h\"\n #include \"object.h\"\n #include \"pretty.h\"\n@@ -71,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n+\tstruct list_objects_filter_options filter_options =\n+\t\tLIST_OBJECTS_FILTER_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n@@ -86,11 +89,12 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n \n \tsetup_git_directory();\n-\trevs.repo = the_repository;\n+\trepo_init_revisions(the_repository, &revs, NULL);\n \n \targc = parse_options(argc, argv, NULL,\n \t\t\t     options, path_walk_usage,\n@@ -101,6 +105,10 @@ int cmd__path_walk(int argc, const char **argv)\n \telse\n \t\tusage(path_walk_usage[0]);\n \n+\t/* Apply the filter after setup_revisions to avoid the --objects check. */\n+\tif (filter_options.choice)\n+\t\tlist_objects_filter_copy(&revs.filter, &filter_options);\n+\n \tinfo.revs = &revs;\n \tinfo.path_fn = emit_block;\n \tinfo.path_fn_data = &data;\n@@ -129,6 +137,7 @@ int cmd__path_walk(int argc, const char **argv)\n \t\tfree(info.pl);\n \t}\n \n+\tlist_objects_filter_release(&filter_options);\n \trelease_revisions(&revs);\n \treturn res;\n }\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex e174290787..94f35ce190 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -298,9 +298,6 @@ test_expect_success 'backfill with prefix pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -318,9 +315,6 @@ test_expect_success 'backfill with multiple pathspecs' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -338,9 +332,6 @@ test_expect_success 'backfill with wildcard pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n \ttest_must_be_empty err &&\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 56bd1e3c5b..b0ee31ee2d 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -415,4 +415,66 @@ test_expect_success 'trees are reported exactly once' '\n \ttest_line_count = 1 out-filtered\n '\n \n+test_expect_success 'all, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:tree:right/:$(git rev-parse topic:right)\n+\t7:tree:right/:$(git rev-parse base~1:right)\n+\t7:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:tree:left/:$(git rev-parse base~2:left)\n+\t3:tree:right/:$(git rev-parse topic:right)\n+\t3:tree:right/:$(git rev-parse base~1:right)\n+\t3:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543277","messageId":"f37467e46f3c23b90fc5c772ae24164aafab58c5.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 06/13] backfill: die on incompatible filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:48Z","receivedAt":"2026-05-13T21:19:09Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git backfill' command uses the path-walk API in a critical way: it\nuses the objects output from the command to find the batches of missing\nobjects that should be requested from the server. Unlike 'git\npack-objects', we cannot fall back to another mechanism.\n\nThe previous change added the path_walk_filter_compatible() method that\nwe can reuse here. Use it during argument validation in cmd_backfill().\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c  | 5 ++---\n t/t5620-backfill.sh | 8 ++++++++\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 7ffab2ea74..b80f9ebe69 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -96,9 +96,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \tif (revs->explicit_diff_merges)\n \t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n \t\t    \"--diff-merges\");\n-\tif (revs->filter.choice)\n-\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n-\t\t    \"--filter\");\n+\tif (!path_walk_filter_compatible(&revs->filter))\n+\t\tdie(_(\"cannot backfill with these filter options\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..ede89f8c33 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -15,6 +15,14 @@ test_expect_success 'backfill rejects unexpected arguments' '\n \ttest_grep \"unrecognized argument: --unexpected-arg\" err\n '\n \n+test_expect_success 'backfill rejects incompatible filter options' '\n+\ttest_must_fail git backfill --objects --filter=tree:1 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err &&\n+\n+\ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err\n+'\n+\n # We create objects in the 'src' repo.\n test_expect_success 'setup repo for object creation' '\n \techo \"{print \\$1}\" >print_1.awk &&\n-- \ngitgitgadget\n\n"},{"id":"543276","messageId":"133c1b156cc8b2e5d0c46b3b44066ebcddaffe64.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 07/13] path-walk: support blob size limit filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:49Z","receivedAt":"2026-05-13T21:19:11Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nExtend the path-walk API to handle the 'blob:limit=<size>' object\nfilter natively. This filter omits blobs whose size is equal to or\ngreater than the given limit, matching the semantics used by the\nlist-objects-filter machinery.\n\nWhen revs->filter.choice is LOFC_BLOB_LIMIT, the prepare_filters()\nmethod stores the limit value in info->blob_limit and clears the filter\nfrom revs. If the limit is zero, this degenerates to blob:none (all\nblobs excluded), so info->blobs is set to 0 instead.\n\nDuring walk_path(), blob batches are filtered before being delivered to\nthe callback: each blob's size is checked via odb_read_object_info(),\nand only blobs strictly smaller than the limit are included. Blobs whose\nsize cannot be determined (e.g. missing in a partial clone) are\nconservatively included, matching the existing filter behavior. Empty\nbatches after filtering are skipped entirely.\n\nThe check for inclusion in the path batch looks a little strange at\nfirst glance. We use odb_read_object_info() to read the object's size.\nBased on all of the assumptions to this point, this _should_ return\nOBJ_BLOB. Since we are focused on the size filter, we use a\nshort-circuited OR (||) to skip the size check if that method returns a\ndifferent object type.\n\nNotice that this inspection of object sizes requires the content to be\npresent in the repository. The odb_read_object_info() call will download\na missing blob on-demand. This means that the use of the path-walk API\nwithin 'git backfill' would not operate nicely with this filter type.\nThe intention of that command is to download missing blobs in batches.\nDownloading objects one-by-one would go against the point. Update the\nvalidation in 'git backfill' to add its own compatibility check on top\nof path_walk_filter_compatible().\n\nAdd tests for blob:limit=0 (equivalent to blob:none) and blob:limit=3\n(which exercises partial filtering within a batch where some blobs are\nkept and others are excluded).\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n builtin/backfill.c                  |  2 +\n path-walk.c                         | 41 +++++++++++++--\n path-walk.h                         |  7 +++\n t/t5620-backfill.sh                 |  2 +-\n t/t6601-path-walk.sh                | 82 +++++++++++++++++++++++++++++\n 6 files changed, 130 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 2994faf988..85ae48b699 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,7 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n-supports the `--filter=<spec>` form `blob:none`.\n+supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex b80f9ebe69..5254a42711 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -98,6 +98,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \t\t    \"--diff-merges\");\n \tif (!path_walk_filter_compatible(&revs->filter))\n \t\tdie(_(\"cannot backfill with these filter options\"));\n+\tif (revs->filter.blob_limit_value)\n+\t\tdie(_(\"cannot backfill with blob size limits\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/path-walk.c b/path-walk.c\nindex bd81508163..04b924d4de 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -327,13 +328,35 @@ static int walk_path(struct path_walk_context *ctx,\n \t/*\n \t * Evaluate function pointer on this data, if requested.\n \t * Ignore object type filters for tagged objects (path starts\n-\t * with `/`).\n+\t * with `/`), first for blobs and then other types.\n \t */\n-\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n-\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n-\t    (list->type == OBJ_TAG && ctx->info->tags))\n+\tif (list->type == OBJ_BLOB &&\n+\t    ctx->info->blob_limit &&\n+\t    !path_is_for_direct_objects(path)) {\n+\t\tstruct oid_array filtered = OID_ARRAY_INIT;\n+\n+\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\tunsigned long size;\n+\n+\t\t\tif (odb_read_object_info(ctx->repo->objects,\n+\t\t\t\t\t\t &list->oids.oid[i],\n+\t\t\t\t\t\t &size) != OBJ_BLOB ||\n+\t\t\t\tsize < ctx->info->blob_limit)\n+\t\t\t\toid_array_append(&filtered,\n+\t\t\t\t\t\t &list->oids.oid[i]);\n+\t\t}\n+\n+\t\tif (filtered.nr)\n+\t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n+\t\t\t\t\t\t ctx->info->path_fn_data);\n+\t\toid_array_clear(&filtered);\n+\t} else if (path_is_for_direct_objects(path) ||\n+\t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n+\t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n+\t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n \t\t\t\t\tctx->info->path_fn_data);\n+\t}\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -510,6 +533,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_BLOB_LIMIT:\n+\t\tif (info) {\n+\t\t\tif (!options->blob_limit_value)\n+\t\t\t\tinfo->blobs = 0;\n+\t\t\telse\n+\t\t\t\tinfo->blob_limit = options->blob_limit_value;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/path-walk.h b/path-walk.h\nindex a1736ecb2b..60ceb65433 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -47,6 +47,13 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If non-zero, specifies a maximum blob size. Blobs with a\n+\t * size equal to or greater than this limit will not be\n+\t * emitted unless included in 'pending'.\n+\t */\n+\tunsigned long blob_limit;\n+\n \t/**\n \t * When 'prune_all_uninteresting' is set and a path has all objects\n \t * marked as UNINTERESTING, then the path-walk will not visit those\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex ede89f8c33..d2ea68e065 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -20,7 +20,7 @@ test_expect_success 'backfill rejects incompatible filter options' '\n \ttest_grep \"cannot backfill with these filter options\" err &&\n \n \ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n-\ttest_grep \"cannot backfill with these filter options\" err\n+\ttest_grep \"cannot backfill with blob size limits\" err\n '\n \n # We create objects in the 'src' repo.\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex b0ee31ee2d..45f366d738 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -477,4 +477,86 @@ test_expect_success 'topic only, blob:none filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, blob:limit=0 filter' '\n+\ttest-tool path-walk --filter=blob:limit=0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:tree:right/:$(git rev-parse topic:right)\n+\t7:tree:right/:$(git rev-parse base~1:right)\n+\t7:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, blob:limit=3 filter' '\n+\ttest-tool path-walk --filter=blob:limit=3 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:tree:right/:$(git rev-parse topic:right)\n+\t9:tree:right/:$(git rev-parse base~1:right)\n+\t9:tree:right/:$(git rev-parse base~2:right)\n+\t10:blob:right/c:$(git rev-parse base~2:right/c)\n+\t11:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543278","messageId":"0f517be8e3853e05d313258133db4eab229d3835.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 08/13] path-walk: add pl_sparse_trees to control tree pruning","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:50Z","receivedAt":"2026-05-13T21:19:12Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe path-walk API prunes trees and blobs when a sparse-checkout pattern\nlist is provided, which is the correct behavior for 'git backfill\n--sparse' since it only needs to fill in objects at paths within the\nsparse cone.\n\nHowever, a future change will use the path-walk API with a sparse:<oid>\nfilter that restricts only blobs while retaining all reachable trees.\nTo support both behaviors, add a 'pl_sparse_trees' flag to\npath_walk_info. When set (as in 'git backfill --sparse' and the\n--stdin-pl test helper mode), the sparse patterns prune both trees and\nblobs. When unset, only blobs are filtered and all trees are walked and\nreported.\n\nAdditionally, move the SEEN flag assignment in add_tree_entries() to\nafter the sparse pattern and pathspec checks. Previously, SEEN was set\nimmediately upon discovering an object, before checking whether its path\nmatched the sparse patterns. When the same object ID appeared at\nmultiple paths (e.g. sibling directories with identical contents), the\nfirst path to be visited would mark the object as SEEN. If that path was\noutside the sparse cone, the object would be skipped there but also\nnever discovered at its in-cone path.\n\nBy deferring the SEEN flag until after the checks pass, objects that are\nskipped due to sparse filtering remain discoverable at other paths where\nthey may be in scope.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c        |  1 +\n path-walk.c               |  5 +++--\n path-walk.h               |  6 ++++++\n t/helper/test-path-walk.c |  6 +++++-\n t/t6601-path-walk.sh      | 37 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 52 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 5254a42711..e71e0f4742 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -109,6 +109,7 @@ static int do_backfill(struct backfill_context *ctx)\n \n \tif (ctx->sparse) {\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = 1;\n \t\tif (get_sparse_checkout_patterns(info.pl)) {\n \t\t\tpath_walk_info_clear(&info);\n \t\t\treturn error(_(\"problem loading sparse-checkout\"));\ndiff --git a/path-walk.c b/path-walk.c\nindex 04b924d4de..225857bbc8 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -183,7 +183,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t/* Skip this object if already seen. */\n \t\tif (o->flags & SEEN)\n \t\t\tcontinue;\n-\t\to->flags |= SEEN;\n \n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n@@ -204,7 +203,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\t\t\t\t  ctx->repo->index);\n \n \t\t\tif (ctx->info->pl->use_cone_patterns &&\n-\t\t\t    match == NOT_MATCHED)\n+\t\t\t    match == NOT_MATCHED &&\n+\t\t\t    (type == OBJ_BLOB || ctx->info->pl_sparse_trees))\n \t\t\t\tcontinue;\n \t\t\telse if (!ctx->info->pl->use_cone_patterns &&\n \t\t\t\t type == OBJ_BLOB &&\n@@ -239,6 +239,7 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\tcontinue;\n \t\t}\n \n+\t\to->flags |= SEEN;\n \t\tadd_path_to_list(ctx, path.buf, type, &entry.oid,\n \t\t\t\t !(o->flags & UNINTERESTING));\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 60ceb65433..7e57ae5f65 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -76,8 +76,14 @@ struct path_walk_info {\n \t * of the cone. If not in cone mode, then all tree paths will be\n \t * explored but the path_fn will only be called when the path matches\n \t * the sparse-checkout patterns.\n+\t *\n+\t * When 'pl_sparse_trees' is zero, the sparse patterns only restrict\n+\t * blobs and all trees are included in the walk output. This matches\n+\t * the behavior of the sparse:oid object filter. When nonzero, trees\n+\t * are also pruned by the sparse patterns (as used by backfill).\n \t */\n \tstruct pattern_list *pl;\n+\tint pl_sparse_trees;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 88f86ae0dc..3f2b50a9aa 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -68,7 +68,7 @@ static int emit_block(const char *path, struct oid_array *oids,\n \n int cmd__path_walk(int argc, const char **argv)\n {\n-\tint res, stdin_pl = 0;\n+\tint res, stdin_pl = 0, pl_sparse_trees = -1;\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n@@ -89,6 +89,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_BOOL(0, \"pl-sparse-trees\", &pl_sparse_trees,\n+\t\t\t N_(\"toggle pruning of trees by sparse patterns\")),\n \t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n@@ -116,6 +118,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tif (stdin_pl) {\n \t\tstruct strbuf in = STRBUF_INIT;\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = (pl_sparse_trees >= 0) ?\n+\t\t\tpl_sparse_trees : 1;\n \n \t\tinfo.pl->use_cone_patterns = 1;\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 45f366d738..02ad83dfb0 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -206,6 +206,43 @@ test_expect_success 'base & topic, sparse' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'base & topic, sparse, no tree pruning' '\n+\tcat >patterns <<-EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\n+\ttest-tool path-walk --stdin-pl --no-pl-sparse-trees \\\n+\t\t-- base topic <patterns >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:blob:left/b:$(git rev-parse base:left/b)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:3\n+\tcommits:4\n+\ttags:0\n+\ttrees:10\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n-- \ngitgitgadget\n\n"},{"id":"543279","messageId":"b4dc09ab6903251164f85ea2827448564cca2f7c.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 09/13] pack-objects: support sparse:oid filter with path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:51Z","receivedAt":"2026-05-13T21:19:14Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe --filter=sparse:<oid> option to 'git pack-objects' allows focusing\nan object set to a sparse-checkout definition. This reduces the set of\nmatching blobs while retaining all reachable trees. No server currently\nsupports fetching with this filter because it is expensive to compute\nand reachability bitmaps do not help without a significant effort to\nextend the bitmap feature to store bitmaps for each supported sparse-\ncheckout definition.\n\nWithout focusing on serving fetches and clones with these filters, there\nare still benefits that could be realized by making this faster. With\nthe sparse index, it's more realistic now than ever to be able to\noperate a local clone that was bootstrapped by a packfile created with\na sparse filter, because the missing trees are not needed to move a\nsparse-checkout from one commit to another or to view the history of any\npath in scope. Such clones could perhaps be bootstrapped by partial\nbundles.\n\nPreviously, constructing these sparse packs has been incredibly\ncomputationally inefficient. The revision walk that explores which\nobjects are in scope spends a lot of time checking each object to see if\nit matches the sparse-checkout patterns, causing quadratic behavior\n(number of objects times number of sparse-checkout patterns). This\nimproves somewhat when using cone-mode sparse-checkout patterns that can\nuse hashtables and prefix matches to determine containment. However, the\ncheck per object is still too expensive for most cases.\n\nThis is where the path-walk feature comes in. We can proceed as normal\nby placing objects in bins by path and _then_ check a group of objects\nall at once. Since sparse:<oid> only restricts blobs, the path-walk must\ninclude all reachable trees while using the cone-mode patterns to skip\nblobs at paths outside the sparse scope. This establishes a baseline for\na potential future \"treesparse:<oid>\" filter that would also restrict\ntrees, but introducing such a new filter is deferred to a later change.\n\nThe implementation here is focused around loading the sparse-checkout\npatterns from the provided object ID and checking that the patterns are\nindeed cone-mode patterns. We can then load the correct pattern list\ninto the path walk context and use the logic that already exists from\nbff45557675 (backfill: add --sparse option, 2025-02-03), though that\nfeature loads sparse-checkout patterns from the worktree's local\nsettings and also restricts tree objects. We use a combination of errors\nand warnings to signal problems during this load. The difference is that\nerrors are likely fatal for the non-path-walk version while the warnings\nare probably just implementation details for the path-walk version and\nthe 'git pack-objects' command can fall back to the revision walk\nversion.\n\nNow that the SEEN flag is deferred until after pattern checks (from the\nprevious commit), handle the case where a tree with a shared OID appears\nat both an out-of-cone and in-cone path. When trees are not being pruned\n(pl_sparse_trees == 0), the path-walk re-walks the tree at the in-cone\npath so that in-cone blobs within it are discovered. The new tests in\nt5317 and t6601 demonstrate this behavior and would fail without these\nchanges.\n\nThe performance test p5315 shows the impact of this change when using\nsparse filters:\n\nTest                                              HEAD~1     HEAD\n----------------------------------------------------------------------\n5315.10: repack (sparse:oid)                      77.98    77.47  -0.7%\n5315.11: repack size (sparse:oid)                187.5M   187.4M  -0.0%\n5315.12: repack (sparse:oid, --path-walk)         77.91    31.41 -59.7%\n5315.13: repack size (sparse:oid, --path-walk)   187.5M   161.1M -14.1%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (14% smaller for sparse packs)\nand dramatic time savings (60% faster) by leveraging the path-walk's\nability to skip blobs outside the sparse scope.\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blaue <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-backfill.adoc        |   4 +\n Documentation/git-pack-objects.adoc    |   3 +-\n builtin/pack-objects.c                 |  16 ++-\n path-walk.c                            |  81 ++++++++++++++-\n t/t5317-pack-objects-filter-objects.sh | 125 +++++++++++++++++++++++\n t/t6601-path-walk.sh                   | 131 +++++++++++++++++++++++++\n 6 files changed, 350 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc\nindex c0a3b80615..82d6a1969d 100644\n--- a/Documentation/git-backfill.adoc\n+++ b/Documentation/git-backfill.adoc\n@@ -80,6 +80,10 @@ OPTIONS\n +\n You may also use commit-limiting options understood by\n linkgit:git-rev-list[1] such as `--first-parent`, `--since`, or pathspecs.\n++\n+Most `--filter=<spec>` options don't work with the purpose of\n+`git backfill`, but the `sparse:<oid>` filter is integrated to provide a\n+focused set of paths to download, distinct from the `--sparse` option.\n \n SEE ALSO\n --------\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 85ae48b699..e38853391b 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n-supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n+supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n+`sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex bc9fb5b457..b783dc62bc 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4754,7 +4754,7 @@ static int add_objects_by_path(const char *path,\n \treturn 0;\n }\n \n-static void get_object_list_path_walk(struct rev_info *revs)\n+static int get_object_list_path_walk(struct rev_info *revs)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tunsigned int processed = 0;\n@@ -4777,8 +4777,9 @@ static void get_object_list_path_walk(struct rev_info *revs)\n \tresult = walk_objects_by_path(&info);\n \ttrace2_region_leave(\"pack-objects\", \"path-walk\", revs->repo);\n \n-\tif (result)\n-\t\tdie(_(\"failed to pack objects via path-walk\"));\n+\tpath_walk_info_clear(&info);\n+\n+\treturn result;\n }\n \n static void get_object_list(struct rev_info *revs, struct strvec *argv)\n@@ -4841,8 +4842,13 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)\n \t\tfn_show_object = show_object;\n \n \tif (path_walk) {\n-\t\tget_object_list_path_walk(revs);\n-\t} else {\n+\t\tif (get_object_list_path_walk(revs)) {\n+\t\t\twarning(_(\"failed to pack objects via path-walk\"));\n+\t\t\tpath_walk = 0;\n+\t\t}\n+\t}\n+\n+\tif (!path_walk) {\n \t\tif (prepare_revision_walk(revs))\n \t\t\tdie(_(\"revision walk setup failed\"));\n \t\tmark_edges_uninteresting(revs, show_edge, sparse);\ndiff --git a/path-walk.c b/path-walk.c\nindex 225857bbc8..ce38dcf1e9 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"object-name.h\"\n #include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n@@ -180,10 +181,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\treturn -1;\n \t\t}\n \n-\t\t/* Skip this object if already seen. */\n-\t\tif (o->flags & SEEN)\n-\t\t\tcontinue;\n-\n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n \n@@ -194,6 +191,40 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\tif (type == OBJ_TREE)\n \t\t\tstrbuf_addch(&path, '/');\n \n+\t\tif (o->flags & SEEN) {\n+\t\t\t/*\n+\t\t\t * A tree with a shared OID may appear at multiple\n+\t\t\t * paths. Even though we already added this tree to\n+\t\t\t * the output at some other path, we still need to\n+\t\t\t * walk into it at this in-cone path to discover\n+\t\t\t * blobs that were not found at the earlier\n+\t\t\t * out-of-cone path.\n+\t\t\t *\n+\t\t\t * Only do this for paths not yet in our map, to\n+\t\t\t * avoid duplicate entries when the same tree OID\n+\t\t\t * appears at the same path across multiple commits.\n+\t\t\t */\n+\t\t\tif (type == OBJ_TREE && ctx->info->pl &&\n+\t\t\t    ctx->info->pl->use_cone_patterns &&\n+\t\t\t    !ctx->info->pl_sparse_trees &&\n+\t\t\t    !strmap_contains(&ctx->paths_to_lists, path.buf)) {\n+\t\t\t\tint dtype;\n+\t\t\t\tenum pattern_match_result m;\n+\t\t\t\tm = path_matches_pattern_list(path.buf, path.len,\n+\t\t\t\t\t\t\t      path.buf + base_len,\n+\t\t\t\t\t\t\t      &dtype,\n+\t\t\t\t\t\t\t      ctx->info->pl,\n+\t\t\t\t\t\t\t      ctx->repo->index);\n+\t\t\t\tif (m != NOT_MATCHED) {\n+\t\t\t\t\tadd_path_to_list(ctx, path.buf, type,\n+\t\t\t\t\t\t\t &entry.oid,\n+\t\t\t\t\t\t\t !(o->flags & UNINTERESTING));\n+\t\t\t\t\tpush_to_stack(ctx, path.buf);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (ctx->info->pl) {\n \t\t\tint dtype;\n \t\t\tenum pattern_match_result match;\n@@ -544,6 +575,48 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_SPARSE_OID:\n+\t\tif (info) {\n+\t\t\tstruct object_id sparse_oid;\n+\t\t\tstruct repository *repo = info->revs->repo;\n+\n+\t\t\tif (info->pl) {\n+\t\t\t\twarning(_(\"sparse filter cannot be combined with existing sparse patterns\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (repo_get_oid_with_flags(repo,\n+\t\t\t\t\t\t    options->sparse_oid_name,\n+\t\t\t\t\t\t    &sparse_oid,\n+\t\t\t\t\t\t    GET_OID_BLOB)) {\n+\t\t\t\terror(_(\"unable to access sparse blob in '%s'\"),\n+\t\t\t\t      options->sparse_oid_name);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tCALLOC_ARRAY(info->pl, 1);\n+\t\t\tinfo->pl->use_cone_patterns = 1;\n+\n+\t\t\tif (add_patterns_from_blob_to_list(&sparse_oid, \"\", 0,\n+\t\t\t\t\t\t\t   info->pl) < 0) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\terror(_(\"unable to parse sparse filter data in '%s'\"),\n+\t\t\t\t      oid_to_hex(&sparse_oid));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (!info->pl->use_cone_patterns) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh\nindex 501d715b9a..dddb79ba62 100755\n--- a/t/t5317-pack-objects-filter-objects.sh\n+++ b/t/t5317-pack-objects-filter-objects.sh\n@@ -478,4 +478,129 @@ test_expect_success 'verify pack-objects w/ --missing=allow-any' '\n \tEOF\n '\n \n+# Test that --path-walk produces the same object set as standard traversal\n+# when using sparse:oid filters with cone-mode patterns.\n+#\n+# The sparse:oid filter restricts only blobs, not trees. Both standard\n+# and path-walk should produce identical sets of blobs, commits, and trees.\n+\n+test_expect_success 'setup pw_sparse for path-walk comparison' '\n+\tgit init pw_sparse &&\n+\tmkdir -p pw_sparse/inc/sub pw_sparse/exc/sub &&\n+\n+\tfor n in 1 2\n+\tdo\n+\t\techo \"inc $n\" >pw_sparse/inc/file$n &&\n+\t\techo \"inc sub $n\" >pw_sparse/inc/sub/file$n &&\n+\t\techo \"exc $n\" >pw_sparse/exc/file$n &&\n+\t\techo \"exc sub $n\" >pw_sparse/exc/sub/file$n &&\n+\t\techo \"root $n\" >pw_sparse/root$n || return 1\n+\tdone &&\n+\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"first\" &&\n+\n+\techo \"inc 1 modified\" >pw_sparse/inc/file1 &&\n+\techo \"exc 1 modified\" >pw_sparse/exc/file1 &&\n+\techo \"root 1 modified\" >pw_sparse/root1 &&\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"second\" &&\n+\n+\t# Cone-mode sparse pattern: include root + inc/\n+\tprintf \"/*\\n!/*/\\n/inc/\\n\" |\n+\tgit -C pw_sparse hash-object -w --stdin >sparse_oid\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk produces same blobs' '\n+\toid=$(cat sparse_oid) &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >standard.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../standard.pack &&\n+\tgit -C pw_sparse verify-pack -v ../standard.pack >standard_verify &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >pathwalk.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../pathwalk.pack &&\n+\tgit -C pw_sparse verify-pack -v ../pathwalk.pack >pathwalk_verify &&\n+\n+\t# Blobs must match exactly\n+\tgrep -E \"^[0-9a-f]{40} blob\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_blobs &&\n+\ttest_cmp standard_blobs pathwalk_blobs &&\n+\n+\t# Commits must match exactly\n+\tgrep -E \"^[0-9a-f]{40} commit\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_commits &&\n+\tgrep -E \"^[0-9a-f]{40} commit\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_commits &&\n+\ttest_cmp standard_commits pathwalk_commits\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk includes all trees' '\n+\t# The sparse:oid filter restricts only blobs, not trees.\n+\t# Both standard and path-walk should include the same trees.\n+\tgrep -E \"^[0-9a-f]{40} tree\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_trees &&\n+\tgrep -E \"^[0-9a-f]{40} tree\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_trees &&\n+\n+\ttest_cmp standard_trees pathwalk_trees\n+'\n+\n+# Test the edge case where the same tree/blob OID appears at both an\n+# in-cone and out-of-cone path. When sibling directories have identical\n+# contents, they share a tree OID. The path-walk defers marking objects\n+# SEEN until after checking sparse patterns, so an object at an out-of-cone\n+# path can still be discovered at an in-cone path.\n+\n+test_expect_success 'setup pw_shared for shared OID across cone boundary' '\n+\tgit init pw_shared &&\n+\tmkdir pw_shared/aaa pw_shared/zzz &&\n+\techo \"shared content\" >pw_shared/aaa/file &&\n+\techo \"shared content\" >pw_shared/zzz/file &&\n+\techo \"root file\" >pw_shared/rootfile &&\n+\tgit -C pw_shared add . &&\n+\tgit -C pw_shared commit -m \"aaa and zzz share tree OID\" &&\n+\n+\t# Verify they share a tree OID\n+\taaa_tree=$(git -C pw_shared rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git -C pw_shared rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tprintf \"/*\\n!/*/\\n/zzz/\\n\" |\n+\tgit -C pw_shared hash-object -w --stdin >shared_sparse_oid\n+'\n+\n+test_expect_success 'shared tree OID: --path-walk blobs match standard' '\n+\toid=$(cat shared_sparse_oid) &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >shared_std.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_std.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_std.pack >shared_std_verify &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >shared_pw.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_pw.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_pw.pack >shared_pw_verify &&\n+\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_std_verify |\n+\tawk \"{print \\$1}\" | sort >shared_std_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_pw_verify |\n+\tawk \"{print \\$1}\" | sort >shared_pw_blobs &&\n+\ttest_cmp shared_std_blobs shared_pw_blobs\n+'\n+\n test_done\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 02ad83dfb0..ac294867a5 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -596,4 +596,135 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'setup sparse filter blob' '\n+\t# Cone-mode patterns: include root, exclude all dirs, include left/\n+\tcat >patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\tsparse_oid=$(git hash-object -w -t blob patterns)\n+'\n+\n+test_expect_success 'all, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:blob:left/b:$(git rev-parse base:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+# Demonstrate the SEEN flag ordering issue: when the same tree/blob OID\n+# appears at two sibling paths where one is in-cone and the other is\n+# out-of-cone, the path-walk must still discover blobs at the in-cone\n+# path even when the shared tree OID was first encountered out-of-cone.\n+# Since sparse:oid includes all trees, the out-of-cone tree (aaa/) is\n+# walked first, and its blob is skipped. The path-walk then re-walks\n+# the same tree OID at the in-cone path (zzz/) to find the blob there.\n+\n+test_expect_success 'setup shared tree OID across cone boundary' '\n+\tgit checkout --orphan shared-tree &&\n+\tgit rm -rf . &&\n+\tmkdir aaa zzz &&\n+\techo \"shared content\" >aaa/file &&\n+\techo \"shared content\" >zzz/file &&\n+\techo \"root file\" >rootfile &&\n+\tgit add aaa zzz rootfile &&\n+\tgit commit -m \"aaa and zzz have same tree OID\" &&\n+\n+\t# Verify they really share a tree OID\n+\taaa_tree=$(git rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tcat >shared-patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/zzz/\n+\tEOF\n+\tshared_sparse_oid=$(git hash-object -w -t blob shared-patterns)\n+'\n+\n+test_expect_success 'sparse:oid with shared tree OID across cone boundary' '\n+\ttest-tool path-walk \\\n+\t\t--filter=sparse:oid=$shared_sparse_oid \\\n+\t\t-- shared-tree >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse shared-tree)\n+\t1:tree::$(git rev-parse shared-tree^{tree})\n+\t2:blob:rootfile:$(git rev-parse shared-tree:rootfile)\n+\t3:tree:aaa/:$(git rev-parse shared-tree:aaa)\n+\t4:tree:zzz/:$(git rev-parse shared-tree:zzz)\n+\t5:blob:zzz/file:$(git rev-parse shared-tree:zzz/file)\n+\tblobs:2\n+\tcommits:1\n+\ttags:0\n+\ttrees:3\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543280","messageId":"0b1eed07907270668713df5094c2198f6b2e600f.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 10/13] t6601: tag otherwise-unreachable trees","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:52Z","receivedAt":"2026-05-13T21:19:15Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe tests in t6601-path-walk.sh demonstrate the behavior of the\npath-walk API under different conditions. One thing that I noticed while\nupdating the behavior of directly-requested objects is that we don't\nactually emit tagged trees. This was previously not noticed due to those\ntagged trees actually being reachable from commits that we are including\nin the path-walk.\n\nUpdate the test setup to have tree-tag and tree-tag2 point to trees that\nare otherwise unreachable.\n\nIt is worth noting that this does not meaningfully change any of the\nother test cases, demontrating the bug.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t6601-path-walk.sh | 24 +++++++++++-------------\n 1 file changed, 11 insertions(+), 13 deletions(-)\n\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex ac294867a5..92c524d145 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -7,17 +7,15 @@ test_description='direct path-walk API tests'\n test_expect_success 'setup test repository' '\n \tgit checkout -b base &&\n \n-\t# Make some objects that will only be reachable\n-\t# via non-commit tags.\n-\tmkdir child &&\n-\techo file >child/file &&\n-\tgit add child &&\n-\tgit commit -m \"will abandon\" &&\n-\tgit tag -a -m \"tree\" tree-tag HEAD^{tree} &&\n-\techo file2 >file2 &&\n-\tgit add file2 &&\n-\tgit commit --amend -m \"will abandon\" &&\n-\tgit tag tree-tag2 HEAD^{tree} &&\n+\t# Create tree objects that are only reachable via tags,\n+\t# not from any commit in the history.\n+\tchild_blob_oid=$(echo \"child blob content\" | git hash-object -t blob -w --stdin) &&\n+\tchild_tree_oid=$(printf \"100644 blob %s\\tfile\\n\" \"$child_blob_oid\" | git mktree) &&\n+\ttree_tag_oid=$(printf \"040000 tree %s\\tchild\\n\" \"$child_tree_oid\" | git mktree) &&\n+\tgit tag -a -m \"tree\" tree-tag \"$tree_tag_oid\" &&\n+\tfile2_blob_oid=$(echo \"tagged tree file2\" | git hash-object -t blob -w --stdin) &&\n+\ttree_tag2_oid=$(printf \"040000 tree %s\\tchild\\n100644 blob %s\\tfile2\\n\" \"$child_tree_oid\" \"$file2_blob_oid\" | git mktree) &&\n+\tgit tag tree-tag2 \"$tree_tag2_oid\" &&\n \n \techo blob >file &&\n \tblob_oid=$(git hash-object -t blob -w --stdin <file) &&\n@@ -26,7 +24,7 @@ test_expect_success 'setup test repository' '\n \tblob2_oid=$(git hash-object -t blob -w --stdin <file2) &&\n \tgit tag blob-tag2 \"$blob2_oid\" &&\n \n-\trm -fr child file file2 &&\n+\trm -fr file file2 &&\n \n \tmkdir left &&\n \tmkdir right &&\n@@ -34,7 +32,7 @@ test_expect_success 'setup test repository' '\n \techo b >left/b &&\n \techo c >right/c &&\n \tgit add . &&\n-\tgit commit --amend -m \"first\" &&\n+\tgit commit -m \"first\" &&\n \tgit tag -m \"first\" first HEAD &&\n \n \techo d >right/d &&\n-- \ngitgitgadget\n\n"},{"id":"543281","messageId":"b23244c4c274aa2b8006ee71189e6eed2dde6489.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 11/13] path-walk: support `tree:0` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:53Z","receivedAt":"2026-05-13T21:19:17Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `tree:0` object filter omits all trees and blobs from the result,\nkeeping only commits and tags. Consequently, this filter type should\nhas a fairly straightforward integration with path-walk, as the decision\nto include an object depends only on its type and does not depend on any\npath-sensitive state.\n\nMapping it onto `path_walk_info` is direct: set `info->trees = 0` and\n`info->blobs = 0` in `prepare_filters()` when the `LOFC_TREE_DEPTH`\nchoice is requested with depth zero. The existing code already plumbs\nthose flags through the rest of the walk:\n\n - 'walk_objects_by_path()' sets `revs->blob_objects = info->blobs` and\n   `revs->tree_objects = info->trees` before `prepare_revision_walk()`,\n   so the revision walk doesn't try to enumerate trees or blobs itself.\n\n - The commit-walk loop short-circuits the root-tree fetch with\n   \"if (!info->trees && !info->blobs) continue;\", so we never even\n   look up the root tree, let alone descend into it.\n\n - `setup_pending_objects()` skips pending trees and blobs based on\n   the same flags.\n\nThis means the path-walk doesn't allocate or expand any tree structures\nat all under `tree:0`, which matches the intended behavior of the\nfilter.\n\nHowever, this requires first fixing some issues with how the path-walk\nAPI handles directly-requested trees _and_ trees requested through\nlightweight tags. These changes create substantial updates to\nt6601-path-walk.sh, which the previous change highlighted as a problem\nby tagging otherwise-unreachable trees and having them not appear in the\noutput.\n\nNon-zero tree-depth filters are not supported. Those depend on the depth\nat which a tree is visited, which is a path-walk concept the filter\nmachinery doesn't currently share with the path-walk API. Reject them in\n`prepare_filters()` with a helpful error and let pack-objects fall back\nto the regular traversal, the same way it already does for unsupported\nfilters.\n\nAdd coverage in t6601 for both `--all` and a single-branch case to\nconfirm that no trees or blobs are emitted, and a separate test that\n`tree:1` is rejected with the expected error message. Place the new\ntests before \"setup sparse filter blob\" so they run on the original set\nof refs, before the orphan branch that the sparse-tree tests create.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |   4 +-\n path-walk.c                         |  53 +++++++--\n t/t6601-path-walk.sh                | 165 ++++++++++++++++++----------\n 3 files changed, 152 insertions(+), 70 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex e38853391b..c86219be91 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,8 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n-supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n-`sparse:<oid>`.\n+supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n+`tree:0`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex ce38dcf1e9..cb67b8ce86 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -390,11 +390,18 @@ static int walk_path(struct path_walk_context *ctx,\n \t\t\t\t\tctx->info->path_fn_data);\n \t}\n \n-\t/* Expand data for children. */\n-\tif (list->type == OBJ_TREE) {\n+\t/*\n+\t * Expand tree children, except when the set is directly requested\n+\t * _and_ we are otherwise filtering out trees.\n+\t */\n+\tif (list->type == OBJ_TREE &&\n+\t    (!path_is_for_direct_objects(path) || ctx->info->trees)) {\n+\t\t/* Use root path if expanding from tagged/direct trees. */\n+\t\tconst char *expand_path = !strcmp(path, \"/tagged-trees\")\n+\t\t\t\t\t  ? root_path : path;\n \t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n \t\t\tret |= add_tree_entries(ctx,\n-\t\t\t\t\t    path,\n+\t\t\t\t\t    expand_path,\n \t\t\t\t\t    &list->oids.oid[i]);\n \t\t}\n \t}\n@@ -442,12 +449,12 @@ static int setup_pending_objects(struct path_walk_info *info,\n {\n \tstruct type_and_oid_list *tags = NULL;\n \tstruct type_and_oid_list *tagged_blobs = NULL;\n-\tstruct type_and_oid_list *root_tree_list = NULL;\n+\tstruct type_and_oid_list *tagged_trees = NULL;\n \n \tif (info->tags)\n \t\tCALLOC_ARRAY(tags, 1);\n \tCALLOC_ARRAY(tagged_blobs, 1);\n-\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n+\tCALLOC_ARRAY(tagged_trees, 1);\n \n \t/*\n \t * Pending objects include:\n@@ -491,14 +498,15 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (pending->path) {\n-\t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n-\t\t\t\t\t\t\t    : xstrdup(\"\");\n+\t\t\tif (pending->path && *pending->path) {\n+\t\t\t\tchar *path = xstrfmt(\"%s/\", pending->path);\n \t\t\t\tadd_path_to_list(ctx, path, OBJ_TREE, &obj->oid, 1);\n \t\t\t\tfree(path);\n+\t\t\t} else if (!pending->path || !info->trees) {\n+\t\t\t\toid_array_append(&tagged_trees->oids, &obj->oid);\n \t\t\t} else {\n-\t\t\t\t/* assume a root tree, such as a lightweight tag. */\n-\t\t\t\toid_array_append(&root_tree_list->oids, &obj->oid);\n+\t\t\t\tadd_path_to_list(ctx, root_path, OBJ_TREE,\n+\t\t\t\t\t\t &obj->oid, 1);\n \t\t\t}\n \t\t\tbreak;\n \n@@ -535,6 +543,18 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\t\tfree(tagged_blobs);\n \t\t}\n \t}\n+\tif (tagged_trees) {\n+\t\tif (tagged_trees->oids.nr) {\n+\t\t\tconst char *tagged_tree_path = \"/tagged-trees\";\n+\t\t\ttagged_trees->type = OBJ_TREE;\n+\t\t\ttagged_trees->maybe_interesting = 1;\n+\t\t\tstrmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);\n+\t\t\tpush_to_stack(ctx, tagged_tree_path);\n+\t\t} else {\n+\t\t\toid_array_clear(&tagged_trees->oids);\n+\t\t\tfree(tagged_trees);\n+\t\t}\n+\t}\n \tif (tags) {\n \t\tif (tags->oids.nr) {\n \t\t\tconst char *tag_path = \"/tags\";\n@@ -575,6 +595,19 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_TREE_DEPTH:\n+\t\tif (options->tree_exclude_depth) {\n+\t\t\terror(_(\"tree:%lu filter not supported by the path-walk API\"),\n+\t\t\t      options->tree_exclude_depth);\n+\t\t\treturn 0;\n+\t\t}\n+\t\tif (info) {\n+\t\t\tinfo->trees = 0;\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 92c524d145..566db7c7e3 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -77,23 +77,23 @@ test_expect_success 'all' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n \t4:blob:a:$(git rev-parse base~2:a)\n-\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n-\t6:tree:a/:$(git rev-parse base:a)\n-\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t8:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n-\t9:tree:left/:$(git rev-parse base:left)\n-\t9:tree:left/:$(git rev-parse base~2:left)\n-\t10:blob:left/b:$(git rev-parse base~2:left/b)\n-\t10:blob:left/b:$(git rev-parse base:left/b)\n-\t11:tree:right/:$(git rev-parse topic:right)\n-\t11:tree:right/:$(git rev-parse base~1:right)\n-\t11:tree:right/:$(git rev-parse base~2:right)\n-\t12:blob:right/c:$(git rev-parse base~2:right/c)\n-\t12:blob:right/c:$(git rev-parse topic:right/c)\n-\t13:blob:right/d:$(git rev-parse base~1:right/d)\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t7:tree:a/:$(git rev-parse base:a)\n+\t8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t9:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n+\t10:tree:left/:$(git rev-parse base:left)\n+\t10:tree:left/:$(git rev-parse base~2:left)\n+\t11:blob:left/b:$(git rev-parse base~2:left/b)\n+\t11:blob:left/b:$(git rev-parse base:left/b)\n+\t12:tree:right/:$(git rev-parse topic:right)\n+\t12:tree:right/:$(git rev-parse base~1:right)\n+\t12:tree:right/:$(git rev-parse base~2:right)\n+\t13:blob:right/c:$(git rev-parse base~2:right/c)\n+\t13:blob:right/c:$(git rev-parse topic:right/c)\n+\t14:blob:right/d:$(git rev-parse base~1:right/d)\n \tblobs:10\n \tcommits:4\n \ttags:7\n@@ -471,15 +471,15 @@ test_expect_success 'all, blob:none filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n-\t4:tree:a/:$(git rev-parse base:a)\n-\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t6:tree:left/:$(git rev-parse base:left)\n-\t6:tree:left/:$(git rev-parse base~2:left)\n-\t7:tree:right/:$(git rev-parse topic:right)\n-\t7:tree:right/:$(git rev-parse base~1:right)\n-\t7:tree:right/:$(git rev-parse base~2:right)\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:tree:right/:$(git rev-parse topic:right)\n+\t8:tree:right/:$(git rev-parse base~1:right)\n+\t8:tree:right/:$(git rev-parse base~2:right)\n \tblobs:2\n \tcommits:4\n \ttags:7\n@@ -533,15 +533,15 @@ test_expect_success 'all, blob:limit=0 filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n-\t4:tree:a/:$(git rev-parse base:a)\n-\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t6:tree:left/:$(git rev-parse base:left)\n-\t6:tree:left/:$(git rev-parse base~2:left)\n-\t7:tree:right/:$(git rev-parse topic:right)\n-\t7:tree:right/:$(git rev-parse base~1:right)\n-\t7:tree:right/:$(git rev-parse base~2:right)\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:tree:right/:$(git rev-parse topic:right)\n+\t8:tree:right/:$(git rev-parse base~1:right)\n+\t8:tree:right/:$(git rev-parse base~2:right)\n \tblobs:2\n \tcommits:4\n \ttags:7\n@@ -572,19 +572,19 @@ test_expect_success 'all, blob:limit=3 filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n \t4:blob:a:$(git rev-parse base~2:a)\n-\t5:tree:a/:$(git rev-parse base:a)\n-\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t7:tree:left/:$(git rev-parse base:left)\n-\t7:tree:left/:$(git rev-parse base~2:left)\n-\t8:blob:left/b:$(git rev-parse base~2:left/b)\n-\t9:tree:right/:$(git rev-parse topic:right)\n-\t9:tree:right/:$(git rev-parse base~1:right)\n-\t9:tree:right/:$(git rev-parse base~2:right)\n-\t10:blob:right/c:$(git rev-parse base~2:right/c)\n-\t11:blob:right/d:$(git rev-parse base~1:right/d)\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\t11:blob:right/c:$(git rev-parse base~2:right/c)\n+\t12:blob:right/d:$(git rev-parse base~1:right/d)\n \tblobs:6\n \tcommits:4\n \ttags:7\n@@ -594,6 +594,55 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:2\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'tree:1 filter is rejected' '\n+\ttest_must_fail test-tool path-walk --filter=tree:1 -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n@@ -625,19 +674,19 @@ test_expect_success 'all, sparse:oid filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n \t4:blob:a:$(git rev-parse base~2:a)\n-\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n-\t6:tree:a/:$(git rev-parse base:a)\n-\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t8:tree:left/:$(git rev-parse base:left)\n-\t8:tree:left/:$(git rev-parse base~2:left)\n-\t9:blob:left/b:$(git rev-parse base~2:left/b)\n-\t9:blob:left/b:$(git rev-parse base:left/b)\n-\t10:tree:right/:$(git rev-parse topic:right)\n-\t10:tree:right/:$(git rev-parse base~1:right)\n-\t10:tree:right/:$(git rev-parse base~2:right)\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t7:tree:a/:$(git rev-parse base:a)\n+\t8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t9:tree:left/:$(git rev-parse base:left)\n+\t9:tree:left/:$(git rev-parse base~2:left)\n+\t10:blob:left/b:$(git rev-parse base~2:left/b)\n+\t10:blob:left/b:$(git rev-parse base:left/b)\n+\t11:tree:right/:$(git rev-parse topic:right)\n+\t11:tree:right/:$(git rev-parse base~1:right)\n+\t11:tree:right/:$(git rev-parse base~2:right)\n \tblobs:6\n \tcommits:4\n \ttags:7\n-- \ngitgitgadget\n\n"},{"id":"543282","messageId":"7e1e503361e5d997d904ac101a776ecffdac6059.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 12/13] path-walk: support `object:type` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:54Z","receivedAt":"2026-05-13T21:19:18Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `object:type` filter accepts only objects of a single type; it is\nthe second member of the object-info-only filter family that bitmap\ntraversal already supports.\n\nLike `blob:none` and `tree:0`, it can be evaluated with nothing more\nthan the object's type, which is exactly the granularity path-walk's\nexisting info->{commits,trees,blobs,tags} flags already control.\n\nMap `LOFC_OBJECT_TYPE` in `prepare_filters()` by AND-ing each flag\nagainst the filtered type. A single `object:type=X` filter\napplied to the default info (all flags = 1) leaves `info->X = 1` and\nall the others 0, which is what we want.\n\nUsing an AND rather than straight assignment prepares us for a\nsubsequent change to implement combined object filters.\n\nThe path-walk machinery is mostly already wired for the per-type\ndistinction:\n\n - `walk_path()` calls `path_fn` for a batch only when the corresponding\n   `info->X` flag is set, so unwanted types are silently not reported.\n\n - `add_tree_entries()` skips tree entries of type `OBJ_BLOB` when\n   `info->blobs` is unset, so we don't even allocate paths for them.\n\n - The commit-walk loop short-circuits the root-tree fetch when\n   `!info->trees && !info->blobs`, so commit-only filters don't descend\n   into trees at all.\n\nBut there are a couple of side effects of the \"trees off, blobs on\" case\nthat need fixing:\n\n 1. 'setup_pending_objects()' previously skipped pending trees as soon\n    as `info->trees` was zero. For 'object:type=blob' the call site\n    needs those pending trees: a lightweight tag pointing to a tree, or\n    an annotated tag whose peeled target is a tree, can both reach\n    blobs that are otherwise unreachable from any commit's root tree.\n    Loosen the gate to \"if (!info->trees && !info->blobs) continue\" and\n    similarly retrieve the root_tree_list whenever either trees or\n    blobs are wanted.\n\n 2. The revision machinery's `handle_commit()` drops pending trees when\n    `revs->tree_objects` is zero (see the 'OBJ_TREE' handler in\n    revision.c), so by the time path-walk sees the pending list\n    after `prepare_revision_walk()` the tree-bearing pendings would\n    already be gone. Fix this by setting\n\n        revs->tree_objects = info->trees || info->blobs\n\n    so pending trees survive `prepare_revision_walk()` whenever we\n    need to walk into them. Path-walk still resets tree_objects to\n    zero immediately after `prepare_revision_walk()` returns, so the\n    rev-walk itself never enumerates trees redundantly with\n    path-walk's own descent.\n\nAdd coverage in t6601 for each of the four `object:type` values. The\n'object:type=blob' test in particular asserts that file2 and child/file\n(both reachable only through tag-pointed trees) show up in the output,\nexercising the pending-tree fix.\n\nUpdate Documentation/git-pack-objects.adoc to add object:type to\nthe list of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n path-walk.c                         | 13 ++++-\n path-walk.h                         |  6 +++\n t/t6601-path-walk.sh                | 84 +++++++++++++++++++++++++++++\n 4 files changed, 103 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex c86219be91..f2852ebd31 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,7 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n-`tree:0`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex cb67b8ce86..418972e753 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -382,7 +382,7 @@ static int walk_path(struct path_walk_context *ctx,\n \t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n \t\t\t\t\t\t ctx->info->path_fn_data);\n \t\toid_array_clear(&filtered);\n-\t} else if (path_is_for_direct_objects(path) ||\n+\t} else if ((!ctx->info->strict_types && path_is_for_direct_objects(path)) ||\n \t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n \t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n \t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n@@ -608,6 +608,17 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_OBJECT_TYPE:\n+\t\tif (info) {\n+\t\t\tinfo->commits &= options->object_type == OBJ_COMMIT;\n+\t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n+\t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n+\t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n+\t\t\tinfo->strict_types = 1;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/path-walk.h b/path-walk.h\nindex 7e57ae5f65..a2652b2d46 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -47,6 +47,12 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If 'strict_types' is 0, then direct object requests will no longer\n+\t * override the object type restrictions.\n+\t */\n+\tint strict_types;\n+\n \t/**\n \t * If non-zero, specifies a maximum blob size. Blobs with a\n \t * size equal to or greater than this limit will not be\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 566db7c7e3..0fd8e61c76 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -643,6 +643,90 @@ test_expect_success 'tree:1 filter is rejected' '\n \ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n '\n \n+test_expect_success 'all, object:type=commit filter' '\n+\ttest-tool path-walk --filter=object:type=commit -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:4\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tag filter' '\n+\ttest-tool path-walk --filter=object:type=tag -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tag:/tags:$(git rev-parse refs/tags/first)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t0:tag:/tags:$(git rev-parse refs/tags/third)\n+\t0:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t0:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t0:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\tblobs:0\n+\tcommits:0\n+\ttags:7\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tree filter' '\n+\ttest-tool path-walk --filter=object:type=tree -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tree::$(git rev-parse topic^{tree})\n+\t0:tree::$(git rev-parse base^{tree})\n+\t0:tree::$(git rev-parse base~1^{tree})\n+\t0:tree::$(git rev-parse base~2^{tree})\n+\t1:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t1:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t2:tree:a/:$(git rev-parse base:a)\n+\t3:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=blob filter' '\n+\ttest-tool path-walk --filter=object:type=blob -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:left/b:$(git rev-parse base:left/b)\n+\t2:blob:left/b:$(git rev-parse base~2:left/b)\n+\t3:blob:right/c:$(git rev-parse base~2:right/c)\n+\t3:blob:right/c:$(git rev-parse topic:right/c)\n+\t4:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:8\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n\n"},{"id":"543283","messageId":"a615b1a7078a6f092deb180d135f32c313094315.1778707135.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v4 13/13] path-walk: support `combine` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-13T21:18:55Z","receivedAt":"2026-05-13T21:19:19Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `combine` filter takes the intersection of its children, that is:\nobjects are shown only when all child filters would admit the object.\n\nThe preceding patches added support for many individual filter types.\nEnable users to compose these filters by implementing support for the\n`combine` filter type.\n\nMapping intersection onto path_walk_info works because every supported\nchild filter is a monotonic restriction:\n\n - `blob:none`, `tree:0` unconditionally clear `info->blobs` and (for\n   `tree:0`) `info->trees`; clearing an already-cleared flag is a\n   no-op.\n\n - `object:type=X` is now expressed as an AND of each type flag with the\n   filtered type, so applying multiple such filters only refines the\n   existing set rather than overwrites it.\n\n - `blob:limit=N` has to compose too: the intersection of \"size < L1\"\n   and \"size < L2\" is \"size < min(L1, L2)\".\n\n   Update the `LOFC_BLOB_LIMIT` handler to take the running minimum when\n   `info->blob_limit` is already set, so a combined filter with, e.g.,\n   both \"blob:limit=10\" and \"blob:limit=5\" produces a limit of 5\n   regardless of ordering.\n\n - `sparse:oid` is left unchanged. A `combine` filter that includes a\n   `sparse:oid` is allowed at most once, since the existing handler\n   refuses to overwrite `info->pl`. Two `sparse:oid` filters in a single\n   `combine` would be unusual and are rejected with a warning, matching\n   the standalone `sparse:oid` behavior.\n\nImplementation-wise, the existing `prepare_filters()` called\n`list_objects_filter_release()` inside each case branch. That works fine for\ntop-level filters, but `combine` filters need to recurse over its child\nfilters without releasing each one in turn (since the parent's release\niterates the sub array). Split `prepare_filters()` into a recursive helper\nthat performs only the mutation, plus a thin wrapper that calls the helper\nand then releases the top-level filter once.\n\nThe `LOFC_COMBINE` case in the helper just walks `sub_nr` and recurses;\nchild filters are released by the wrapper's single\n`list_objects_filter_release()` call on the parent (which itself recursively\nreleases each sub-filter, the same way it always has).\n\nIf any sub-filter is unsupported (e.g. \"tree:1\", \"sparse:<path>\", or a\nnot-yet-supported choice), the recursion bubbles a failure up and the\nexisting pack-objects/backfill fallback paths kick in.\n\nAdd coverage in t6601:\n\n  - \"combine:blob:none+tree:0\" collapses to \"tree:0\"\n\n  - \"combine:object:type=blob+blob:limit=3\" yields only the blobs\n    smaller than three bytes\n\n  - \"combine:object:type=blob+object:type=tree\" intersects to empty\n\n  - \"combine:tree:1+blob:none\" reports the \"tree:1\" error.\n\nUpdate Documentation/git-pack-objects.adoc to add combine to the\nlist of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  3 +-\n path-walk.c                         | 25 ++++++++--\n t/t6601-path-walk.sh                | 71 +++++++++++++++++++++++++++++\n 3 files changed, 93 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex f2852ebd31..8a27aa19fd 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,8 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n-`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter\n+types can be combined with the `combine:<spec>+<spec>` form.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex 418972e753..94ff90bd15 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -571,8 +571,8 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n-static int prepare_filters(struct path_walk_info *info,\n-\t\t\t   struct list_objects_filter_options *options)\n+static int prepare_filters_one(struct path_walk_info *info,\n+\t\t\t       struct list_objects_filter_options *options)\n {\n \tswitch (options->choice) {\n \tcase LOFC_DISABLED:\n@@ -589,7 +589,8 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tif (!options->blob_limit_value)\n \t\t\t\tinfo->blobs = 0;\n-\t\t\telse\n+\t\t\telse if (!info->blob_limit ||\n+\t\t\t\t info->blob_limit > options->blob_limit_value)\n \t\t\t\tinfo->blob_limit = options->blob_limit_value;\n \t\t\tlist_objects_filter_release(options);\n \t\t}\n@@ -604,7 +605,6 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tinfo->trees = 0;\n \t\t\tinfo->blobs = 0;\n-\t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n \n@@ -656,8 +656,13 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n \t\t\t\treturn 0;\n \t\t\t}\n+\t\t}\n+\t\treturn 1;\n \n-\t\t\tlist_objects_filter_release(options);\n+\tcase LOFC_COMBINE:\n+\t\tfor (size_t i = 0; i < options->sub_nr; i++) {\n+\t\t\tif (!prepare_filters_one(info, &options->sub[i]))\n+\t\t\t\treturn 0;\n \t\t}\n \t\treturn 1;\n \n@@ -668,6 +673,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t}\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tif (!prepare_filters_one(info, options))\n+\t\treturn 0;\n+\tif (info)\n+\t\tlist_objects_filter_release(options);\n+\treturn 1;\n+}\n+\n int path_walk_filter_compatible(struct list_objects_filter_options *options)\n {\n \treturn prepare_filters(NULL, options);\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 0fd8e61c76..e9fcd85e75 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -727,6 +727,77 @@ test_expect_success 'all, object:type=blob filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, combine:blob:none+tree:0 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:blob:none+tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:2\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine:object:type=blob+blob:limit=3 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+blob:limit=3 \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:left/b:$(git rev-parse base~2:left/b)\n+\t3:blob:right/c:$(git rev-parse base~2:right/c)\n+\t4:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine of disjoint object:types is empty' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+object:type=tree \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'combine: rejects unsupported subfilters' '\n+\ttest_must_fail test-tool path-walk \\\n+\t\t--filter=combine:tree:1+blob:none -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n"},{"id":"543711","messageId":"agzwsxV2KEkkaGPV@nand.local","threadId":"65575","inReplyTo":"e77c8a6bbc22da3428751f81ff5ee79aa5364237.1778707135.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 04/13] path-walk: always emit directly-requested objects","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-19T23:22:27Z","receivedAt":"2026-05-19T23:22:34Z","isPatch":true,"body":"On Wed, May 13, 2026 at 09:18:46PM +0000, Derrick Stolee via GitGitGadget wrote:\n> diff --git a/path-walk.c b/path-walk.c\n> index 6e426af433..05bfc1c114 100644\n> --- a/path-walk.c\n> +++ b/path-walk.c\n> @@ -248,6 +248,17 @@ static int add_tree_entries(struct path_walk_context *ctx,\n>  \treturn 0;\n>  }\n>\n> +/*\n> + * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n> + * were directly requested by 'pending' objects rather than discovered during\n> + * tree traversal.\n> + */\n> +static int path_is_for_direct_objects(const char *path)\n> +{\n> +\tASSERT(path);\n> +\treturn path[0] == '/';\n> +}\n> +\n\nHmm, I still find this a little brittle. I think that 'path' here is\ndoing a number of jobs: it serves as a strmap key, it's visible to the\ncaller, and now also a \"direct object\" marker.\n\nCould we instead store this explicitly on the type_and_oid_list, e.g. a\n\"direct\" flag? I'm not sure whether that type has the right scope for\nthis information. If not, I wonder if there is another way to store this\ninformation, since I worry that future callers may not know about this\nconvention and end up changing the result of the path-walk depending on\nhow they name their paths.\n\nThanks,\nTaylor\n"},{"id":"543714","messageId":"agz3fOHvVKGLMxgb@nand.local","threadId":"65575","inReplyTo":"fb8a0f9c43d4e41712839a93c4db6a294a7b5285.1778707135.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 03/13] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-19T23:51:24Z","receivedAt":"2026-05-19T23:51:26Z","isPatch":true,"body":"On Wed, May 13, 2026 at 09:18:45PM +0000, Derrick Stolee via GitGitGadget wrote:\n> +\t>depth2-dirs &&\n> +\twhile read tdir\n> +\tdo\n> +\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n> +\tdone <top-dirs >depth2-dirs.raw &&\n> +\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n\nUgh, I think that this was a bad suggestion on my part, since $tdir\nshould be empty at this point.\n\nCould we use --format here like so?\n\n    while read tdir\n    do\n        git ls-tree -d --format=\"$tdir/%(path)\" \"HEAD:$tdir\" || return 1\n    done 2>/dev/null\n\nI guess that breaks if $tdir contains a formatting atom, so perhaps we\nshould keep the spirit of the original (but using an intermediary file\ninstead of piping the output of Git to another command).\n\nSorry about that, I'm not sure why I thought that was a good idea when I\nwrote it :-<.\n\nThanks,\nTaylor\n"},{"id":"543715","messageId":"agz3/ZxZZHBKofR9@nand.local","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-19T23:53:33Z","receivedAt":"2026-05-19T23:53:36Z","isPatch":true,"body":"On Wed, May 13, 2026 at 09:18:42PM +0000, Derrick Stolee via GitGitGadget wrote:\n> UPDATES IN V4\n> =============\n>\n> Thanks, Taylor for the careful review.\n>\n>  * Several typos are fixed.\n>  * The performance test is corrected for issues around piping Git commands\n>    and made more robust to the existence of submodules.\n>  * BIG: The tree:0 patch is significantly updated in this version. Taylor\n>    correctly smelled a problem with the new logic to emit the /tagged-trees\n>    object set, and that signaled that those trees were previously never\n>    emitted. I update the test to demonstrate that changing the data shape\n>    (including tagged trees that are otherwise-unreachable) doesn't change\n>    the test behavior, signaling a bug. The behavior change details all the\n>    complexities of visiting only directly-requested trees under a tree:0\n>    filter and recursing on all trees in other cases.\n\nThanks for the new round; I gave this a lighter pass since I had\nreviewed v3 in detail and the range-diff here looks good. I focused in\non a few patches in particular, and left a couple of minor comments.\n\nMy main reservation is that the \"path starts with a '/' slash character\nwhen directly requested\" behavior feels brittle to me, and I am not sure\nif there is a cleaner way to express that.\n\nI'm curious what your thoughts are there. I think barring that things\nare near-complete here, though I did note one issue with the t/perf\nchanges (that is my fault for having a bad suggestion on the earlier\nround).\n\nThanks,\nTaylor\n"},{"id":"543860","messageId":"2485a7c4-c798-4886-ab64-a22fa70f55be@gmail.com","threadId":"65575","inReplyTo":"agz3fOHvVKGLMxgb@nand.local","subject":"Re: [PATCH v4 03/13] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-21T22:56:48Z","receivedAt":"2026-05-21T22:56:50Z","isPatch":true,"body":"On 5/19/26 7:51 PM, Taylor Blau wrote:\n> On Wed, May 13, 2026 at 09:18:45PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> +\t>depth2-dirs &&\n>> +\twhile read tdir\n>> +\tdo\n>> +\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n>> +\tdone <top-dirs >depth2-dirs.raw &&\n>> +\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n> \n> Ugh, I think that this was a bad suggestion on my part, since $tdir\n> should be empty at this point.\n> \n> Could we use --format here like so?\n\nThe --format option is clean. The full loop will look like this:\n\n\twhile read tdir\n\tdo\n\t\tgit ls-tree -d --format=\"$tdir/%(path)\" \"HEAD:$tdir\" || return 1\n\tdone <top-dirs >depth2-dirs &&\n\n> I guess that breaks if $tdir contains a formatting atom, so perhaps we\n> should keep the spirit of the original (but using an intermediary file\n> instead of piping the output of Git to another command).\n\nThanks,\n-Stolee\n\n\n"},{"id":"543861","messageId":"53d46614-7d9d-49ab-ace7-71367f6d2f40@gmail.com","threadId":"65575","inReplyTo":"agzwsxV2KEkkaGPV@nand.local","subject":"Re: [PATCH v4 04/13] path-walk: always emit directly-requested objects","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-21T23:00:39Z","receivedAt":"2026-05-21T23:00:43Z","isPatch":true,"body":"On 5/19/26 7:22 PM, Taylor Blau wrote:\n> On Wed, May 13, 2026 at 09:18:46PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> diff --git a/path-walk.c b/path-walk.c\n>> index 6e426af433..05bfc1c114 100644\n>> --- a/path-walk.c\n>> +++ b/path-walk.c\n>> @@ -248,6 +248,17 @@ static int add_tree_entries(struct path_walk_context *ctx,\n>>   \treturn 0;\n>>   }\n>>\n>> +/*\n>> + * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n>> + * were directly requested by 'pending' objects rather than discovered during\n>> + * tree traversal.\n>> + */\n>> +static int path_is_for_direct_objects(const char *path)\n>> +{\n>> +\tASSERT(path);\n>> +\treturn path[0] == '/';\n>> +}\n>> +\n> \n> Hmm, I still find this a little brittle. I think that 'path' here is\n> doing a number of jobs: it serves as a strmap key, it's visible to the\n> caller, and now also a \"direct object\" marker.\n> \n> Could we instead store this explicitly on the type_and_oid_list, e.g. a\n> \"direct\" flag? I'm not sure whether that type has the right scope for\n> this information. If not, I wonder if there is another way to store this\n> information, since I worry that future callers may not know about this\n> convention and end up changing the result of the path-walk depending on\n> how they name their paths.\n\nI don't find this as fragile as you do, because these \"direct\" paths\n_need_ to start with '/' to avoid collisions with other paths that may\nexist _and_ this meaning is internal to the data within the API. Callers\ncan't change this data, though they will see the paths themselves in the\ncallback function.\n\nAnd as I mentioned before, this is a memory-efficient storage of this\nindicator bit because it only consumes memory when it is \"on\" and the\nvast majority of cases where it is \"off\" it doesn't take any extra\nstorage.\n\nThanks,\n-Stolee\n\n"},{"id":"543862","messageId":"4d7a75e9-8260-4e33-a786-72e0aa3026ae@gmail.com","threadId":"65575","inReplyTo":"agz3/ZxZZHBKofR9@nand.local","subject":"Re: [PATCH v4 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-21T23:01:33Z","receivedAt":"2026-05-21T23:01:35Z","isPatch":true,"body":"On 5/19/26 7:53 PM, Taylor Blau wrote:\n> On Wed, May 13, 2026 at 09:18:42PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> UPDATES IN V4\n>> =============\n>>\n>> Thanks, Taylor for the careful review.\n>>\n>>   * Several typos are fixed.\n>>   * The performance test is corrected for issues around piping Git commands\n>>     and made more robust to the existence of submodules.\n>>   * BIG: The tree:0 patch is significantly updated in this version. Taylor\n>>     correctly smelled a problem with the new logic to emit the /tagged-trees\n>>     object set, and that signaled that those trees were previously never\n>>     emitted. I update the test to demonstrate that changing the data shape\n>>     (including tagged trees that are otherwise-unreachable) doesn't change\n>>     the test behavior, signaling a bug. The behavior change details all the\n>>     complexities of visiting only directly-requested trees under a tree:0\n>>     filter and recursing on all trees in other cases.\n> \n> Thanks for the new round; I gave this a lighter pass since I had\n> reviewed v3 in detail and the range-diff here looks good. I focused in\n> on a few patches in particular, and left a couple of minor comments.\n> \n> My main reservation is that the \"path starts with a '/' slash character\n> when directly requested\" behavior feels brittle to me, and I am not sure\n> if there is a cleaner way to express that.\n> \n> I'm curious what your thoughts are there. I think barring that things\n> are near-complete here, though I did note one issue with the t/perf\n> changes (that is my fault for having a bad suggestion on the earlier\n> round).\n\nI like the suggested change to t/perf but I don't share your concerns\naround the '/' character in the path (I go deeper into why in the\nthread).\n\nThanks,\n-Stolee\n\n"},{"id":"543930","messageId":"ahCF+PGcJ5pACQDW@nand.local","threadId":"65575","inReplyTo":"53d46614-7d9d-49ab-ace7-71367f6d2f40@gmail.com","subject":"Re: [PATCH v4 04/13] path-walk: always emit directly-requested objects","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-22T16:36:08Z","receivedAt":"2026-05-22T16:36:13Z","isPatch":true,"body":"On Thu, May 21, 2026 at 07:00:39PM -0400, Derrick Stolee wrote:\n> > Could we instead store this explicitly on the type_and_oid_list, e.g. a\n> > \"direct\" flag? I'm not sure whether that type has the right scope for\n> > this information. If not, I wonder if there is another way to store this\n> > information, since I worry that future callers may not know about this\n> > convention and end up changing the result of the path-walk depending on\n> > how they name their paths.\n>\n> I don't find this as fragile as you do, because these \"direct\" paths\n> _need_ to start with '/' to avoid collisions with other paths that may\n> exist _and_ this meaning is internal to the data within the API. Callers\n> can't change this data, though they will see the paths themselves in the\n> callback function.\n>\n> And as I mentioned before, this is a memory-efficient storage of this\n> indicator bit because it only consumes memory when it is \"on\" and the\n> vast majority of cases where it is \"off\" it doesn't take any extra\n> storage.\n\nAh, I didn't realize that we added '/' at the beginning to avoid\ncollisions. This approach makes sense to me!\n\n\nThanks,\nTaylor\n"},{"id":"543931","messageId":"ahCGL+AsIaR+63Pr@nand.local","threadId":"65575","inReplyTo":"4d7a75e9-8260-4e33-a786-72e0aa3026ae@gmail.com","subject":"Re: [PATCH v4 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-22T16:37:03Z","receivedAt":"2026-05-22T16:37:05Z","isPatch":true,"body":"On Thu, May 21, 2026 at 07:01:33PM -0400, Derrick Stolee wrote:\n> > I'm curious what your thoughts are there. I think barring that things\n> > are near-complete here, though I did note one issue with the t/perf\n> > changes (that is my fault for having a bad suggestion on the earlier\n> > round).\n>\n> I like the suggested change to t/perf but I don't share your concerns\n> around the '/' character in the path (I go deeper into why in the\n> thread).\n\nSounds good. I think a minor re-roll for that would be good, but I\nbetter understand your viewpoint around the '/' leading character now,\nso I think other than that we're good to go from my perspective.\n\nThanks,\nTaylor\n"},{"id":"543936","messageId":"6d8d0b30-99f2-49f1-bd82-3515aad9aacb@gmail.com","threadId":"65575","inReplyTo":"ahCGL+AsIaR+63Pr@nand.local","subject":"Re: [PATCH v4 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-05-22T18:24:02Z","receivedAt":"2026-05-22T18:24:04Z","isPatch":true,"body":"On 5/22/2026 12:37 PM, Taylor Blau wrote:\n> On Thu, May 21, 2026 at 07:01:33PM -0400, Derrick Stolee wrote:\n>>> I'm curious what your thoughts are there. I think barring that things\n>>> are near-complete here, though I did note one issue with the t/perf\n>>> changes (that is my fault for having a bad suggestion on the earlier\n>>> round).\n>>\n>> I like the suggested change to t/perf but I don't share your concerns\n>> around the '/' character in the path (I go deeper into why in the\n>> thread).\n> \n> Sounds good. I think a minor re-roll for that would be good, but I\n> better understand your viewpoint around the '/' leading character now,\n> so I think other than that we're good to go from my perspective.\n\nThanks! v5 is on its way with the update to the perf script.\n\nThanks,\n-Stolee\n\n\n"},{"id":"543937","messageId":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v4.git.1778707135.gitgitgadget@gmail.com","subject":"[PATCH v5 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:24Z","receivedAt":"2026-05-22T18:24:40Z","isPatch":true,"body":"NOTE: This series is based on en/backfill-fixes-and-edges.\n\nThe 'git pack-objects' command has a '--path-walk' option that uses the\npath-walk API instead of a typical revision walk to group objects into\nchunks by path name instead of relying solely on name-hashes to group\nsimilar files together. (It also does a second compression pass looking for\nbetter deltas after the first pass that is focused within chunks per path.)\n\nThe '--path-walk' feature was not previously integrated with the '--filter'\nfeature, so a warning would appear and disable the path-walk API when a\nfilter is given. This patch series integrates these together in the\nfollowing ways:\n\n * --filter=blob:none updates the path-walk API options to skip blobs.\n * --filter=blob:limit=<size> adds a scan to a list of blob objects to\n   remove objects that are too large.\n * --filter=sparse:<oid> adds a scan to the chunks to validate that the\n   paths match the sparse-checkout patterns.\n\nIn particular, this last check is significantly faster than the previous\nalgorithm because it can check all objects at a given path simultaneously\ninstead of checking all sparse-checkout patterns for each discovered blob\nobject.\n\nA subtlety must be added here, in that we must change how we mark an object\nas \"seen\" during the path-walk. We may need to add an object to multiple\npaths and only mark it as \"seen\" if it indeed matched the sparse-checkout\npatterns as the path is accepted for emitting to the callback. This adds a\nnew filter that the \"seen\" objects must also be removed from later chunks to\navoid sending the same object as grouped to multiple chunks.\n\nThere's also a subtle detail here in that the path-walk API also prunes tree\npaths based on cone-mode sparse-checkouts, to enable 'git backfill --sparse'\noperating quickly for small sparse-checkout scopes. But the\n--filter=sparse:<oid> feature doesn't prune trees!\n\nAs a future step, I do plan to recommend that we add a treesparse:<oid>\nsetting that does allow us to trim the tree set by cone-mode sparse\npatterns. At the time that partial clone filters were being created, cone\nmode sparse-checkout didn't exist and neither did the sparse index. Those\nfeatures together make a smaller tree set possible, assuming the user never\nneeds to change their scope. This would be a significant change so it is not\nimplemented here, though the git pack-objects integration would be quick\nafter this series completes.\n\nNeither the sparse:<oid> or hypothetical treesparse:<oid> options are or\nshould necessarily be supported by Git servers. It's too expensive to\ncompute dynamically and it doesn't work well with reachability bitmaps. What\nbecomes possible with this change is that it becomes reasonably fast to\nconstruct bundles with these filters that can bootstrap a working\nenvironment with the full history of all files within a given\nsparse-checkout scope.\n\n\nPerformance Results\n===================\n\nSince the '--path-walk' option is ignored in today's Git version when a\n'--filter' is added, the performance matches the behavior without\n'--path-walk'. For the tables below, you can compare the rows against each\nother (time and then packfile size) for the mode without and then with\n'--path-walk' as a representation of \"before\" and \"after\". (These tables are\nrepeated in the commit messages as new implementations improve specific\nrows.)\n\nI chose a number of open source repositories of various sizes and shapes:\n\n\ngit/git\n=======\n\nTest                                              HEAD\n-------------------------------------------------------------------\n5315.2: repack (no filter)                       27.73\n5315.3: repack size (no filter)                 250.6M\n5315.4: repack (no filter, --path-walk)          35.19\n5315.5: repack size (no filter, --path-walk)    220.1M\n5315.6: repack (blob:none)                       13.42\n5315.7: repack size (blob:none)                 137.6M\n5315.8: repack (blob:none, --path-walk)          20.98\n5315.9: repack size (blob:none, --path-walk)    115.2M\n5315.10: repack (sparse:oid)                     72.53\n5315.11: repack size (sparse:oid)               187.5M\n5315.12: repack (sparse:oid, --path-walk)        29.00\n5315.13: repack size (sparse:oid, --path-walk)  161.0M\n\n\n\nnodejs/node\n===========\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                       75.53\n5315.3: repack size (no filter)                   0.9G\n5315.4: repack (no filter, --path-walk)          80.54\n5315.5: repack size (no filter, --path-walk)    885.7M\n5315.6: repack (blob:none)                       12.65\n5315.7: repack size (blob:none)                 148.6M\n5315.8: repack (blob:none, --path-walk)          17.60\n5315.9: repack size (blob:none, --path-walk)    134.6M\n5315.10: repack (sparse:oid)                    518.84\n5315.11: repack size (sparse:oid)               153.4M\n5315.12: repack (sparse:oid, --path-walk)        27.99\n5315.13: repack size (sparse:oid, --path-walk)  139.4M\n\n\n\nmicrosoft/fluentui\n==================\n\nTest                                              HEAD\n--------------------------------------------------------------------\n5315.2: repack (no filter)                      146.77\n5315.3: repack size (no filter)                 562.1M\n5315.4: repack (no filter, --path-walk)          72.82\n5315.5: repack size (no filter, --path-walk)    172.6M\n5315.6: repack (blob:none)                        4.84\n5315.7: repack size (blob:none)                  62.7M\n5315.8: repack (blob:none, --path-walk)           5.19\n5315.9: repack size (blob:none, --path-walk)     59.9M\n5315.10: repack (sparse:oid)                     59.95\n5315.11: repack size (sparse:oid)                85.6M\n5315.12: repack (sparse:oid, --path-walk)        15.16\n5315.13: repack size (sparse:oid, --path-walk)   72.5M\n\n\n\nmicrosoftdocs/azure-devops-docs\n===============================\n\nTest                                               HEAD\n---------------------------------------------------------------------\n5315.2: repack (no filter)                        4.41\n5315.3: repack size (no filter)                   1.6G\n5315.4: repack (no filter, --path-walk)           6.00\n5315.5: repack size (no filter, --path-walk)      1.6G\n5315.6: repack (blob:none)                        1.35\n5315.7: repack size (blob:none)                  60.0M\n5315.8: repack (blob:none, --path-walk)           1.23\n5315.9: repack size (blob:none, --path-walk)     60.0M\n5315.10: repack (sparse:oid)                    138.24\n5315.11: repack size (sparse:oid)                84.4M\n5315.12: repack (sparse:oid, --path-walk)         1.86\n5315.13: repack size (sparse:oid, --path-walk)   84.4M\n\n\n\nPerformance conclusions\n=======================\n\nAs seen in earlier series around the '--path-walk' feature, the space\nsavings can be valuable but is not always guaranteed. When the space savings\ndoesn't happen, then the time spent is generally slower because of the\ntwo-pass mechanism. The microsoftdocs/azure-devops-docs repo demonstrates\nthis case quite clearly.\n\nHowever, even in these cases the 'sparse:<oid>' filters are much faster\nbecause of the ability to check an entire set of objects against the\nsparse-checkout patterns only once.\n\nThanks, -Stolee\n\n\nUPDATES IN V2\n=============\n\n * Rebased onto en/backfill-fixes-and-edges to properly integrate with the\n   incompatible rev-list options logic in that series.\n * Updated documentation as behavior changes. Credit to Taylor Blau for\n   presenting these suggestions in his RFC [2].\n * Added three patches of Taylor's to extend more filter options.\n\n\nUPDATES IN V3\n=============\n\nUpon realizing that the tests were not passing with\nGIT_TEST_PACK_PATH_WALK=1, I spent a lot of time reworking each patch to\npass all tests with that variable enabled. This led to a lot of meaningful\nchanges:\n\n * A new patch updates t5620-backfill.sh because they are currently failing\n   due to a check for quiet stderr checks. These changes are reverted later\n   when the filters are integrated so the warning stops being written.\n * I move the logic for the path-walk API emitting \"directly requested\"\n   objects (non-commits in the 'pending' list). This is substantial enough\n   to be its own patch.\n * The filtering logic is pulled entirely within the path-walk API instead\n   of needing integration within builtin/pack-objects.c.\n * The tree:0 filter had a lot to be desired when fetching missing objects,\n   so is substantially updated.\n * The object-type filter requires a change to the typical direct-request\n   behavior, including a new 'strict_types' member that prevents ever\n   allowing objects against type.\n * The combine filter needed better logic around multiple blob size limits,\n   to take the smaller of the two.\n * The t6601-path-walk.sh script has many test updates to better reflect the\n   new behavior, as required by the other partial clone tests under\n   GIT_TEST_PACK_PATH_WALK=1.\n * Doc updates for 'form' to 'forms' when multiple forms are supported.\n\nI've also updated Taylor's bitmap-related patches into three commits on top\nof this series (see [3]).\n\n\nUPDATES IN V4\n=============\n\nThanks, Taylor for the careful review.\n\n * Several typos are fixed.\n * The performance test is corrected for issues around piping Git commands\n   and made more robust to the existence of submodules.\n * BIG: The tree:0 patch is significantly updated in this version. Taylor\n   correctly smelled a problem with the new logic to emit the /tagged-trees\n   object set, and that signaled that those trees were previously never\n   emitted. I update the test to demonstrate that changing the data shape\n   (including tagged trees that are otherwise-unreachable) doesn't change\n   the test behavior, signaling a bug. The behavior change details all the\n   complexities of visiting only directly-requested trees under a tree:0\n   filter and recursing on all trees in other cases.\n\n\nUPDATES IN V5\n=============\n\nSmall adjustments to the performance test script.\n\nP.S. I've CC'd the folks who were on the original path-walk feature thread\n[1]\n\n[1]\nhttps://lore.kernel.org/git/pull.1819.git.1741571455.gitgitgadget@gmail.com/\n\n[2] https://lore.kernel.org/git/cover.1777853408.git.me@ttaylorr.com/\n\n[3]\nhttps://github.com/derrickstolee/git/compare/path-walk-filters...derrickstolee:git:path-walk-bitmaps\n\nDerrick Stolee (10):\n  t5620: make test work with path-walk var\n  pack-objects: pass --objects with --path-walk\n  t/perf: add pack-objects filter and path-walk benchmark\n  path-walk: always emit directly-requested objects\n  path-walk: support blobless filter\n  backfill: die on incompatible filter options\n  path-walk: support blob size limit filter\n  path-walk: add pl_sparse_trees to control tree pruning\n  pack-objects: support sparse:oid filter with path-walk\n  t6601: tag otherwise-unreachable trees\n\nTaylor Blau (3):\n  path-walk: support `tree:0` filter\n  path-walk: support `object:type` filter\n  path-walk: support `combine` filter\n\n Documentation/git-backfill.adoc            |   4 +\n Documentation/git-pack-objects.adoc        |   8 +-\n Documentation/technical/api-path-walk.adoc |   7 +\n builtin/backfill.c                         |   8 +-\n builtin/pack-objects.c                     |  23 +-\n path-walk.c                                | 270 ++++++++--\n path-walk.h                                |  31 ++\n t/helper/test-path-walk.c                  |  17 +-\n t/perf/p5315-pack-objects-filter.sh        | 129 +++++\n t/t5317-pack-objects-filter-objects.sh     | 125 +++++\n t/t5620-backfill.sh                        |   8 +\n t/t6601-path-walk.sh                       | 572 +++++++++++++++++++--\n 12 files changed, 1124 insertions(+), 78 deletions(-)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\n\nbase-commit: a1ad4a0fca14cdeb55ab9fb065551b15cafa8a4f\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2101%2Fderrickstolee%2Fpath-walk-filters-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2101/derrickstolee/path-walk-filters-v5\nPull-Request: https://github.com/gitgitgadget/git/pull/2101\n\nRange-diff vs v4:\n\n  1:  0840110116 =  1:  0840110116 t5620: make test work with path-walk var\n  2:  d7c87545f3 =  2:  d7c87545f3 pack-objects: pass --objects with --path-walk\n  3:  fb8a0f9c43 !  3:  697ef716d2 t/perf: add pack-objects filter and path-walk benchmark\n     @@ t/perf/p5315-pack-objects-filter.sh (new)\n      +\t\tawk \"{print \\$4;}\" >top-dirs &&\n      +\ttop_nr=$(wc -l <top-dirs) &&\n      +\n     -+\t>depth2-dirs &&\n      +\twhile read tdir\n      +\tdo\n     -+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n     -+\tdone <top-dirs >depth2-dirs.raw &&\n     -+\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n     ++\t\tgit ls-tree -d --format=\"$tdir/%(path)\" \"HEAD:$tdir\" || return 1\n     ++\tdone <top-dirs >depth2-dirs &&\n      +\n      +\td2_nr=$(wc -l <depth2-dirs) &&\n      +\n  4:  e77c8a6bbc =  4:  91845bcef0 path-walk: always emit directly-requested objects\n  5:  f4904f81e0 =  5:  fdb9361198 path-walk: support blobless filter\n  6:  f37467e46f =  6:  89726faf7e backfill: die on incompatible filter options\n  7:  133c1b156c =  7:  3884d4737f path-walk: support blob size limit filter\n  8:  0f517be8e3 =  8:  31b4ef0fa1 path-walk: add pl_sparse_trees to control tree pruning\n  9:  b4dc09ab69 =  9:  7d8f0aa036 pack-objects: support sparse:oid filter with path-walk\n 10:  0b1eed0790 = 10:  a68676d0de t6601: tag otherwise-unreachable trees\n 11:  b23244c4c2 = 11:  b0db73c6cc path-walk: support `tree:0` filter\n 12:  7e1e503361 = 12:  6845988f50 path-walk: support `object:type` filter\n 13:  a615b1a707 = 13:  d33d899251 path-walk: support `combine` filter\n\n-- \ngitgitgadget\n"},{"id":"543938","messageId":"0840110116a967d39d8b63dbfe822ea9b2500088.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 01/13] t5620: make test work with path-walk var","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:25Z","receivedAt":"2026-05-22T18:24:41Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe GIT_TEST_PACK_PATH_WALK test variable allows enabling the\n--path-walk option to 'git pack-objects' by default. This sometimes\nengages the warning that --path-walk is incompatible with the --filter\noption. These tests in t5620 fail due to this warning over stderr in\nthis case. Disable this variable for this moment until these options\nwork together.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t5620-backfill.sh | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..e174290787 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -298,6 +298,9 @@ test_expect_success 'backfill with prefix pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -315,6 +318,9 @@ test_expect_success 'backfill with multiple pathspecs' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -332,6 +338,9 @@ test_expect_success 'backfill with wildcard pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n+\t# If we enable --path-walk here, we will get a warning overs stderr\n+\t# due to incompatibilities with --filter.\n+\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n \ttest_must_be_empty err &&\n \n-- \ngitgitgadget\n\n"},{"id":"543939","messageId":"d7c87545f38a2457031bb2e75a51b464a1525ed6.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 02/13] pack-objects: pass --objects with --path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:26Z","receivedAt":"2026-05-22T18:24:42Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen 'git pack-objects' has the --path-walk option enabled, it uses a\ndifferent set of revision walk parameters than normal. For one,\n--objects was previously assumed by the path-walk API and could be\nomitted. We also needed --boundary to allow discovering UNINTERESTING\nobjects to use as delta bases.\n\nWe will be updating the path-walk API soon to work with some filter\noptions. However, the revision machinery will trigger a fatal error:\n\n  fatal: object filtering requires --objects\n\nThe fix is easy: add the --objects option as an argument. This has no\neffect on the path-walk API but does simplify the revision option\nparsing for the objects filter.\n\nWe can remove the comment about \"removing\" the options because they were\nnever removed and instead not added. We still need to disable using\nbitmaps.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/pack-objects.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex dd2480a73d..4338962904 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5190,10 +5190,7 @@ int cmd_pack_objects(int argc,\n \t}\n \tif (path_walk) {\n \t\tstrvec_push(&rp, \"--boundary\");\n-\t\t /*\n-\t\t  * We must disable the bitmaps because we are removing\n-\t\t  * the --objects / --objects-edge[-aggressive] options.\n-\t\t  */\n+\t\tstrvec_push(&rp, \"--objects\");\n \t\tuse_bitmap_index = 0;\n \t} else if (thin) {\n \t\tuse_internal_rev_list = 1;\n-- \ngitgitgadget\n\n"},{"id":"543941","messageId":"697ef716d234e432dfc5e4b54f35ca527d22aa5e.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 03/13] t/perf: add pack-objects filter and path-walk benchmark","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:27Z","receivedAt":"2026-05-22T18:24:43Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd p5315-pack-objects-filter.sh to measure the performance of\n'git pack-objects --revs --all' under different filter and traversal\ncombinations:\n\n * no filter (baseline)\n * --filter=blob:none (blobless)\n * --filter=sparse:oid=<oid> (cone-mode sparse)\n\nEach filter scenario is tested both with and without --path-walk,\nproducing paired measurements that show the impact of the path-walk\ntraversal for each filter type as we integrate the --path-walk feature\nwith different --filter options. It currently has no integration so\nfalls back to the standard revision walk. Thus, there are no significant\ndifferences in the current results other than a full repack (and even\nthen, the --path-walk feature is not incredibly different for the\ndefault Git repository):\n\nTest                                             HEAD\n-----------------------------------------------------\n5315.2: repack (no filter)                      27.91\n5315.3: repack size (no filter)                250.7M\n5315.4: repack (no filter, --path-walk)         34.92\n5315.5: repack size (no filter, --path-walk)   220.0M\n5315.6: repack (blob:none)                      13.63\n5315.7: repack size (blob:none)                137.6M\n5315.8: repack (blob:none, --path-walk)         13.48\n5315.9: repack size (blob:none, --path-walk)   137.7M\n5315.10: repack (sparse:oid)                    72.67\n5315.11: repack size (sparse:oid)              187.4M\n5315.12: repack (sparse:oid, --path-walk)       72.47\n5315.13: repack size (sparse:oid, --path-walk) 187.4M\n\nThe sparse filter definition is built automatically by sampling\ndepth-2 directories from the test repository, making the test work\non any repo passed via GIT_PERF_LARGE_REPO. For repos that lack\ndepth-2 directories, a single top-level directory is used; for flat\nrepos, the sparse tests are skipped via prerequisite.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/perf/p5315-pack-objects-filter.sh | 129 ++++++++++++++++++++++++++++\n 1 file changed, 129 insertions(+)\n create mode 100755 t/perf/p5315-pack-objects-filter.sh\n\ndiff --git a/t/perf/p5315-pack-objects-filter.sh b/t/perf/p5315-pack-objects-filter.sh\nnew file mode 100755\nindex 0000000000..445ff25be2\n--- /dev/null\n+++ b/t/perf/p5315-pack-objects-filter.sh\n@@ -0,0 +1,129 @@\n+#!/bin/sh\n+\n+test_description='Tests pack-objects performance with filters and --path-walk'\n+. ./perf-lib.sh\n+\n+test_perf_large_repo\n+\n+test_expect_success 'setup filter inputs' '\n+\t# Sample a few depth-2 directories from the test repo to build\n+\t# a cone-mode sparse-checkout definition.  The sampling picks\n+\t# directories at evenly-spaced positions so the choice is stable\n+\t# and scales to repos of any shape.\n+\n+\tgit ls-tree -d HEAD >top-entries &&\n+\tgrep \"^040000\" top-entries |\n+\t\tawk \"{print \\$4;}\" >top-dirs &&\n+\ttop_nr=$(wc -l <top-dirs) &&\n+\n+\twhile read tdir\n+\tdo\n+\t\tgit ls-tree -d --format=\"$tdir/%(path)\" \"HEAD:$tdir\" || return 1\n+\tdone <top-dirs >depth2-dirs &&\n+\n+\td2_nr=$(wc -l <depth2-dirs) &&\n+\n+\tif test \"$d2_nr\" -ge 2\n+\tthen\n+\t\t# Pick two directories from evenly-spaced positions.\n+\t\tfirst=$(sed -n \"1p\" depth2-dirs) &&\n+\t\tmid=$(sed -n \"$((d2_nr / 2 + 1))p\" depth2-dirs) &&\n+\n+\t\tp1=$(dirname \"$first\") &&\n+\t\tp2=$(dirname \"$mid\") &&\n+\n+\t\t# Build cone-mode sparse-checkout patterns.\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$p1/\" &&\n+\t\t\techo \"!/$p1/*/\" &&\n+\t\t\tif test \"$p1\" != \"$p2\"\n+\t\t\tthen\n+\t\t\t\techo \"/$p2/\" &&\n+\t\t\t\techo \"!/$p2/*/\"\n+\t\t\tfi &&\n+\t\t\techo \"/$first/\" &&\n+\t\t\tif test \"$first\" != \"$mid\"\n+\t\t\tthen\n+\t\t\t\techo \"/$mid/\"\n+\t\t\tfi\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first $mid\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\telif test \"$top_nr\" -ge 1\n+\tthen\n+\t\t# Fallback: use a single top-level directory.\n+\t\tfirst=$(sed -n \"1p\" top-dirs) &&\n+\t\t{\n+\t\t\techo \"/*\" &&\n+\t\t\techo \"!/*/\" &&\n+\t\t\techo \"/$first/\"\n+\t\t} >sparse-patterns &&\n+\n+\t\tgit hash-object -w sparse-patterns >sparse-oid &&\n+\t\techo \"Sparse cone: $first\" &&\n+\t\tcat sparse-patterns &&\n+\t\ttest_set_prereq SPARSE_OID\n+\tfi\n+'\n+\n+test_perf 'repack (no filter)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (no filter, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk </dev/null >pk\n+'\n+\n+test_size 'repack size (no filter, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (blob:none, --path-walk)' '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=blob:none </dev/null >pk\n+'\n+\n+test_size 'repack size (blob:none, --path-walk)' '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_perf 'repack (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\tgit pack-objects --stdout --no-reuse-delta --revs --all --path-walk \\\n+\t\t--filter=sparse:oid=$(cat sparse-oid) </dev/null >pk\n+'\n+\n+test_size 'repack size (sparse:oid, --path-walk)' \\\n+\t--prereq SPARSE_OID '\n+\ttest_file_size pk\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"543940","messageId":"91845bcef0692bf15b41af1dbf01dcc02baf42ee.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 04/13] path-walk: always emit directly-requested objects","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:28Z","receivedAt":"2026-05-22T18:24:44Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWe are preparing to integrate the path-walk API with some --filter options\nin 'git pack-objects', but there is a subtle issue that is revealed when\nthose are put together and the test suite is run with\nGIT_TEST_PACK_PATH_WALK=1.\n\nWhen a filter reduces the set of requested objects, this results in\nfiltering out directly-requested objects, such as in the download of needed\nblobs in a blobless partial clone.\n\nThe root cause is that the scan of pending objects in the path-walk API\nrespects the filters set in the path_walk_info instead of overriding them\nfor pending objects.\n\nWe can tell that a path is part of the directly-referenced objects if its\npath name starts with '/' (other paths, including root trees never have this\nstarting character). Create a path_is_for_direct_objects() to make this\nmeaning clear, especially as we add more references in the future as we\nintegrate the path-walk API with partial clone filter options.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/api-path-walk.adoc |  7 ++++\n path-walk.c                                | 42 ++++++++++++++--------\n path-walk.h                                |  5 +++\n 3 files changed, 39 insertions(+), 15 deletions(-)\n\ndiff --git a/Documentation/technical/api-path-walk.adoc b/Documentation/technical/api-path-walk.adoc\nindex a67de1b143..6e17b13d61 100644\n--- a/Documentation/technical/api-path-walk.adoc\n+++ b/Documentation/technical/api-path-walk.adoc\n@@ -48,6 +48,13 @@ commits.\n \tapplications could disable some options to make it simpler to walk\n \tthe objects or to have fewer calls to `path_fn`.\n +\n+Note that objects directly requested as pending objects (such as targets\n+of lightweight tags or other ref tips) are always emitted to `path_fn`,\n+even when the corresponding type flag is disabled. Only objects\n+discovered during the tree walk are subject to these type filters. This\n+ensures that objects specifically requested through the revision input\n+are never silently dropped.\n++\n While it is possible to walk only commits in this way, consumers would be\n better off using the revision walk API instead.\n \ndiff --git a/path-walk.c b/path-walk.c\nindex 6e426af433..05bfc1c114 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -248,6 +248,17 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \treturn 0;\n }\n \n+/*\n+ * Paths starting with '/' (e.g., \"/tags\", \"/tagged-blobs\") hold objects that\n+ * were directly requested by 'pending' objects rather than discovered during\n+ * tree traversal.\n+ */\n+static int path_is_for_direct_objects(const char *path)\n+{\n+\tASSERT(path);\n+\treturn path[0] == '/';\n+}\n+\n /*\n  * For each path in paths_to_explore, walk the trees another level\n  * and add any found blobs to the batch (but only if they exist and\n@@ -306,14 +317,19 @@ static int walk_path(struct path_walk_context *ctx,\n \n \tif (list->type == OBJ_BLOB &&\n \t    ctx->revs->prune_data.nr &&\n+\t    !path_is_for_direct_objects(path) &&\n \t    !match_pathspec(ctx->repo->index, &ctx->revs->prune_data,\n \t\t\t   path, strlen(path), 0,\n \t\t\t   NULL, 0))\n \t\treturn 0;\n \n-\t/* Evaluate function pointer on this data, if requested. */\n-\tif ((list->type == OBJ_TREE && ctx->info->trees) ||\n-\t    (list->type == OBJ_BLOB && ctx->info->blobs) ||\n+\t/*\n+\t * Evaluate function pointer on this data, if requested.\n+\t * Ignore object type filters for tagged objects (path starts\n+\t * with `/`).\n+\t */\n+\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n+\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n \t    (list->type == OBJ_TAG && ctx->info->tags))\n \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n \t\t\t\t\tctx->info->path_fn_data);\n@@ -374,10 +390,8 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \tif (info->tags)\n \t\tCALLOC_ARRAY(tags, 1);\n-\tif (info->blobs)\n-\t\tCALLOC_ARRAY(tagged_blobs, 1);\n-\tif (info->trees)\n-\t\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n+\tCALLOC_ARRAY(tagged_blobs, 1);\n+\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n \n \t/*\n \t * Pending objects include:\n@@ -421,8 +435,6 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (!info->trees)\n-\t\t\t\tcontinue;\n \t\t\tif (pending->path) {\n \t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n \t\t\t\t\t\t\t    : xstrdup(\"\");\n@@ -435,8 +447,6 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\t\tbreak;\n \n \t\tcase OBJ_BLOB:\n-\t\t\tif (!info->blobs)\n-\t\t\t\tcontinue;\n \t\t\tif (pending->path)\n \t\t\t\tadd_path_to_list(ctx, pending->path, OBJ_BLOB, &obj->oid, 1);\n \t\t\telse\n@@ -532,15 +542,17 @@ int walk_objects_by_path(struct path_walk_info *info)\n \tpush_to_stack(&ctx, root_path);\n \n \t/*\n-\t * Set these values before preparing the walk to catch\n-\t * lightweight tags pointing to non-commits and indexed objects.\n+\t * Ensure that prepare_revision_walk() keeps all pending objects\n+\t * even through an object type filter.\n \t */\n-\tinfo->revs->blob_objects = info->blobs;\n-\tinfo->revs->tree_objects = info->trees;\n+\tinfo->revs->blob_objects = info->revs->tree_objects = 1;\n \n \tif (prepare_revision_walk(info->revs))\n \t\tdie(_(\"failed to setup revision walk\"));\n \n+\tinfo->revs->blob_objects = info->blobs;\n+\tinfo->revs->tree_objects = info->trees;\n+\n \t/*\n \t * Walk trees to mark them as UNINTERESTING.\n \t * This is particularly important when 'edge_aggressive' is set.\ndiff --git a/path-walk.h b/path-walk.h\nindex 5ef5a8440e..657eeda8ec 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -36,6 +36,11 @@ struct path_walk_info {\n \t/**\n \t * Initialize which object types the path_fn should be called on. This\n \t * could also limit the walk to skip blobs if not set.\n+\t *\n+\t * Note: even when 'blobs' or 'trees' is disabled, objects that are\n+\t * directly requested as pending objects will still be emitted to\n+\t * path_fn. Only objects discovered during the tree walk are filtered by\n+\t * these flags.\n \t */\n \tint commits;\n \tint trees;\n-- \ngitgitgadget\n\n"},{"id":"543942","messageId":"fdb936119817f9bf36edd60dfb8d2ea8c7a660f0.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 05/13] path-walk: support blobless filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:29Z","receivedAt":"2026-05-22T18:24:45Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git pack-objects' command can opt-in to using the path-walk API for\nscanning the objects. Currently, this option is dynamically disabled if\ncombined with '--filter=<X>', even when using a simple filter such as\n'blob:none' to signal a blobless packfile. This is a common scenario for\nrepos at scale, so is worth integrating.\n\nAlso, users can opt-in to the '--path-walk' option by default through\nthe pack.usePathWalk=true config option. When using that in a blobless\npartial clone, the following warning can appear even though the user did\nnot specify either option directly:\n\n  warning: cannot use --filter with --path-walk\n\nTeach the path-walk API to handle the 'blob:none' object filter\nnatively. When revs->filter.choice is LOFC_BLOB_NONE, the path-walk\nsets info->blobs to 0 (skipping all blob objects) and clears the\nfilter from revs so that prepare_revision_walk() does not reject the\nconfiguration.\n\nThis check is implemented in the static prepare_filters() method, which\nwill simultaneously check if the input filters are compatible and will\nmake the appropriate mutations to the path_walk_info and filters if the\npath_walk_info is non-NULL. This allows us to use this logic both in the\nAPI method path_walk_filter_compatible() for use in\nbuiltin/pack-objects.c and as a prep step in walk_objects_by_path().\n\nUpdate the test helper (test-path-walk) to accept --filter=<spec>\nas a test-tool option (before '--'), applying it to revs after\nsetup_revisions() to avoid the --objects requirement check. We can also\nrevert recent GIT_TEST_PACK_PATH_WALK overrides in t5620.\n\nAlso switch test-path-walk from REV_INFO_INIT with manual repo\nassignment to repo_init_revisions(), which properly initializes\nthe filter_spec strbuf needed for filter parsing.\n\nAdd tests for blob:none with --all and with a single branch.\n\nThe performance test p5315 shows the impact of this change when using\nblobless filters:\n\nTest                                           HEAD~1     HEAD\n---------------------------------------------------------------------\n5315.6: repack (blob:none)                      13.53   13.87  +2.5%\n5315.7: repack size (blob:none)                137.7M  137.8M  +0.1%\n5315.8: repack (blob:none, --path-walk)         13.51   23.43 +73.4%\n5315.9: repack size (blob:none, --path-walk)   137.7M  115.2M -16.3%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (16% smaller for blobless packs)\nat the cost of increased computation time due to the two compression\npasses. This data demonstrates that the feature is engaged and provides\nreal compression benefits when --no-reuse-delta forces fresh deltas.\n\nCo-Authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  6 +--\n builtin/pack-objects.c              |  2 +-\n path-walk.c                         | 30 ++++++++++++++\n path-walk.h                         |  7 ++++\n t/helper/test-path-walk.c           | 11 ++++-\n t/t5620-backfill.sh                 |  9 -----\n t/t6601-path-walk.sh                | 62 +++++++++++++++++++++++++++++\n 7 files changed, 113 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex b78175fbe1..2994faf988 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -402,9 +402,9 @@ will be automatically changed to version `1`.\n \tof filenames that cause collisions in Git's default name-hash\n \talgorithm.\n +\n-Incompatible with `--delta-islands`, `--shallow`, or `--filter`. The\n-`--use-bitmap-index` option will be ignored in the presence of\n-`--path-walk.`\n+Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n+ignored in the presence of `--path-walk`. The `--path-walk` option\n+supports the `--filter=<spec>` form `blob:none`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 4338962904..bc9fb5b457 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -5177,7 +5177,7 @@ int cmd_pack_objects(int argc,\n \n \tif (path_walk) {\n \t\tconst char *option = NULL;\n-\t\tif (filter_options.choice)\n+\t\tif (!path_walk_filter_compatible(&filter_options))\n \t\t\toption = \"--filter\";\n \t\telse if (use_delta_islands)\n \t\t\toption = \"--delta-islands\";\ndiff --git a/path-walk.c b/path-walk.c\nindex 05bfc1c114..bd81508163 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -9,6 +9,7 @@\n #include \"hashmap.h\"\n #include \"hex.h\"\n #include \"list-objects.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -495,6 +496,32 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tswitch (options->choice) {\n+\tcase LOFC_DISABLED:\n+\t\treturn 1;\n+\n+\tcase LOFC_BLOB_NONE:\n+\t\tif (info) {\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n+\tdefault:\n+\t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n+\t\t      list_objects_filter_spec(options));\n+\t\treturn 0;\n+\t}\n+}\n+\n+int path_walk_filter_compatible(struct list_objects_filter_options *options)\n+{\n+\treturn prepare_filters(NULL, options);\n+}\n+\n /**\n  * Given the configuration of 'info', walk the commits based on 'info->revs' and\n  * call 'info->path_fn' on each discovered path.\n@@ -522,6 +549,9 @@ int walk_objects_by_path(struct path_walk_info *info)\n \n \ttrace2_region_enter(\"path-walk\", \"commit-walk\", info->revs->repo);\n \n+\tif (!prepare_filters(info, &info->revs->filter))\n+\t\treturn -1;\n+\n \tCALLOC_ARRAY(commit_list, 1);\n \tcommit_list->type = OBJ_COMMIT;\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 657eeda8ec..a1736ecb2b 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -90,3 +90,10 @@ void path_walk_info_clear(struct path_walk_info *info);\n  * Returns nonzero on an error.\n  */\n int walk_objects_by_path(struct path_walk_info *info);\n+\n+struct list_objects_filter_options;\n+/**\n+ * Given a set of options for filtering objects, return 1 if the options\n+ * are compatible with the path-walk API and 0 otherwise.\n+ */\n+int path_walk_filter_compatible(struct list_objects_filter_options *options);\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex fe63002c2b..88f86ae0dc 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -4,6 +4,7 @@\n #include \"dir.h\"\n #include \"environment.h\"\n #include \"hex.h\"\n+#include \"list-objects-filter-options.h\"\n #include \"object-name.h\"\n #include \"object.h\"\n #include \"pretty.h\"\n@@ -71,6 +72,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n+\tstruct list_objects_filter_options filter_options =\n+\t\tLIST_OBJECTS_FILTER_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"blobs\", &info.blobs,\n \t\t\t N_(\"toggle inclusion of blob objects\")),\n@@ -86,11 +89,12 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n \n \tsetup_git_directory();\n-\trevs.repo = the_repository;\n+\trepo_init_revisions(the_repository, &revs, NULL);\n \n \targc = parse_options(argc, argv, NULL,\n \t\t\t     options, path_walk_usage,\n@@ -101,6 +105,10 @@ int cmd__path_walk(int argc, const char **argv)\n \telse\n \t\tusage(path_walk_usage[0]);\n \n+\t/* Apply the filter after setup_revisions to avoid the --objects check. */\n+\tif (filter_options.choice)\n+\t\tlist_objects_filter_copy(&revs.filter, &filter_options);\n+\n \tinfo.revs = &revs;\n \tinfo.path_fn = emit_block;\n \tinfo.path_fn_data = &data;\n@@ -129,6 +137,7 @@ int cmd__path_walk(int argc, const char **argv)\n \t\tfree(info.pl);\n \t}\n \n+\tlist_objects_filter_release(&filter_options);\n \trelease_revisions(&revs);\n \treturn res;\n }\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex e174290787..94f35ce190 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -298,9 +298,6 @@ test_expect_success 'backfill with prefix pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -318,9 +315,6 @@ test_expect_success 'backfill with multiple pathspecs' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- d/f a 2>err &&\n \ttest_must_be_empty err &&\n \n@@ -338,9 +332,6 @@ test_expect_success 'backfill with wildcard pathspec' '\n \tgit -C backfill-path rev-list --quiet --objects --missing=print HEAD >missing &&\n \ttest_line_count = 48 missing &&\n \n-\t# If we enable --path-walk here, we will get a warning overs stderr\n-\t# due to incompatibilities with --filter.\n-\tGIT_TEST_PACK_PATH_WALK=0 \\\n \tgit -C backfill-path backfill HEAD -- \"d/file.*.txt\" 2>err &&\n \ttest_must_be_empty err &&\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 56bd1e3c5b..b0ee31ee2d 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -415,4 +415,66 @@ test_expect_success 'trees are reported exactly once' '\n \ttest_line_count = 1 out-filtered\n '\n \n+test_expect_success 'all, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:tree:right/:$(git rev-parse topic:right)\n+\t7:tree:right/:$(git rev-parse base~1:right)\n+\t7:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, blob:none filter' '\n+\ttest-tool path-walk --filter=blob:none -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:tree:left/:$(git rev-parse base~2:left)\n+\t3:tree:right/:$(git rev-parse topic:right)\n+\t3:tree:right/:$(git rev-parse base~1:right)\n+\t3:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543943","messageId":"89726faf7ec457d6f709ac435e9b4bb73d6c6fda.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 06/13] backfill: die on incompatible filter options","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:30Z","receivedAt":"2026-05-22T18:24:46Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'git backfill' command uses the path-walk API in a critical way: it\nuses the objects output from the command to find the batches of missing\nobjects that should be requested from the server. Unlike 'git\npack-objects', we cannot fall back to another mechanism.\n\nThe previous change added the path_walk_filter_compatible() method that\nwe can reuse here. Use it during argument validation in cmd_backfill().\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c  | 5 ++---\n t/t5620-backfill.sh | 8 ++++++++\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 7ffab2ea74..b80f9ebe69 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -96,9 +96,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \tif (revs->explicit_diff_merges)\n \t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n \t\t    \"--diff-merges\");\n-\tif (revs->filter.choice)\n-\t\tdie(_(\"'%s' cannot be used with 'git backfill'\"),\n-\t\t    \"--filter\");\n+\tif (!path_walk_filter_compatible(&revs->filter))\n+\t\tdie(_(\"cannot backfill with these filter options\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex 94f35ce190..ede89f8c33 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -15,6 +15,14 @@ test_expect_success 'backfill rejects unexpected arguments' '\n \ttest_grep \"unrecognized argument: --unexpected-arg\" err\n '\n \n+test_expect_success 'backfill rejects incompatible filter options' '\n+\ttest_must_fail git backfill --objects --filter=tree:1 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err &&\n+\n+\ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n+\ttest_grep \"cannot backfill with these filter options\" err\n+'\n+\n # We create objects in the 'src' repo.\n test_expect_success 'setup repo for object creation' '\n \techo \"{print \\$1}\" >print_1.awk &&\n-- \ngitgitgadget\n\n"},{"id":"543944","messageId":"3884d4737feb2c81d734aea979317e4cd5d64490.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 07/13] path-walk: support blob size limit filter","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:31Z","receivedAt":"2026-05-22T18:24:47Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nExtend the path-walk API to handle the 'blob:limit=<size>' object\nfilter natively. This filter omits blobs whose size is equal to or\ngreater than the given limit, matching the semantics used by the\nlist-objects-filter machinery.\n\nWhen revs->filter.choice is LOFC_BLOB_LIMIT, the prepare_filters()\nmethod stores the limit value in info->blob_limit and clears the filter\nfrom revs. If the limit is zero, this degenerates to blob:none (all\nblobs excluded), so info->blobs is set to 0 instead.\n\nDuring walk_path(), blob batches are filtered before being delivered to\nthe callback: each blob's size is checked via odb_read_object_info(),\nand only blobs strictly smaller than the limit are included. Blobs whose\nsize cannot be determined (e.g. missing in a partial clone) are\nconservatively included, matching the existing filter behavior. Empty\nbatches after filtering are skipped entirely.\n\nThe check for inclusion in the path batch looks a little strange at\nfirst glance. We use odb_read_object_info() to read the object's size.\nBased on all of the assumptions to this point, this _should_ return\nOBJ_BLOB. Since we are focused on the size filter, we use a\nshort-circuited OR (||) to skip the size check if that method returns a\ndifferent object type.\n\nNotice that this inspection of object sizes requires the content to be\npresent in the repository. The odb_read_object_info() call will download\na missing blob on-demand. This means that the use of the path-walk API\nwithin 'git backfill' would not operate nicely with this filter type.\nThe intention of that command is to download missing blobs in batches.\nDownloading objects one-by-one would go against the point. Update the\nvalidation in 'git backfill' to add its own compatibility check on top\nof path_walk_filter_compatible().\n\nAdd tests for blob:limit=0 (equivalent to blob:none) and blob:limit=3\n(which exercises partial filtering within a batch where some blobs are\nkept and others are excluded).\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n builtin/backfill.c                  |  2 +\n path-walk.c                         | 41 +++++++++++++--\n path-walk.h                         |  7 +++\n t/t5620-backfill.sh                 |  2 +-\n t/t6601-path-walk.sh                | 82 +++++++++++++++++++++++++++++\n 6 files changed, 130 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 2994faf988..85ae48b699 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,7 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n-supports the `--filter=<spec>` form `blob:none`.\n+supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex b80f9ebe69..5254a42711 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -98,6 +98,8 @@ static void reject_unsupported_rev_list_options(struct rev_info *revs)\n \t\t    \"--diff-merges\");\n \tif (!path_walk_filter_compatible(&revs->filter))\n \t\tdie(_(\"cannot backfill with these filter options\"));\n+\tif (revs->filter.blob_limit_value)\n+\t\tdie(_(\"cannot backfill with blob size limits\"));\n }\n \n static int do_backfill(struct backfill_context *ctx)\ndiff --git a/path-walk.c b/path-walk.c\nindex bd81508163..04b924d4de 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n@@ -327,13 +328,35 @@ static int walk_path(struct path_walk_context *ctx,\n \t/*\n \t * Evaluate function pointer on this data, if requested.\n \t * Ignore object type filters for tagged objects (path starts\n-\t * with `/`).\n+\t * with `/`), first for blobs and then other types.\n \t */\n-\tif ((list->type == OBJ_TREE && (ctx->info->trees || path_is_for_direct_objects(path))) ||\n-\t    (list->type == OBJ_BLOB && (ctx->info->blobs || path_is_for_direct_objects(path))) ||\n-\t    (list->type == OBJ_TAG && ctx->info->tags))\n+\tif (list->type == OBJ_BLOB &&\n+\t    ctx->info->blob_limit &&\n+\t    !path_is_for_direct_objects(path)) {\n+\t\tstruct oid_array filtered = OID_ARRAY_INIT;\n+\n+\t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n+\t\t\tunsigned long size;\n+\n+\t\t\tif (odb_read_object_info(ctx->repo->objects,\n+\t\t\t\t\t\t &list->oids.oid[i],\n+\t\t\t\t\t\t &size) != OBJ_BLOB ||\n+\t\t\t\tsize < ctx->info->blob_limit)\n+\t\t\t\toid_array_append(&filtered,\n+\t\t\t\t\t\t &list->oids.oid[i]);\n+\t\t}\n+\n+\t\tif (filtered.nr)\n+\t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n+\t\t\t\t\t\t ctx->info->path_fn_data);\n+\t\toid_array_clear(&filtered);\n+\t} else if (path_is_for_direct_objects(path) ||\n+\t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n+\t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n+\t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n \t\tret = ctx->info->path_fn(path, &list->oids, list->type,\n \t\t\t\t\tctx->info->path_fn_data);\n+\t}\n \n \t/* Expand data for children. */\n \tif (list->type == OBJ_TREE) {\n@@ -510,6 +533,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_BLOB_LIMIT:\n+\t\tif (info) {\n+\t\t\tif (!options->blob_limit_value)\n+\t\t\t\tinfo->blobs = 0;\n+\t\t\telse\n+\t\t\t\tinfo->blob_limit = options->blob_limit_value;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/path-walk.h b/path-walk.h\nindex a1736ecb2b..60ceb65433 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -47,6 +47,13 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If non-zero, specifies a maximum blob size. Blobs with a\n+\t * size equal to or greater than this limit will not be\n+\t * emitted unless included in 'pending'.\n+\t */\n+\tunsigned long blob_limit;\n+\n \t/**\n \t * When 'prune_all_uninteresting' is set and a path has all objects\n \t * marked as UNINTERESTING, then the path-walk will not visit those\ndiff --git a/t/t5620-backfill.sh b/t/t5620-backfill.sh\nindex ede89f8c33..d2ea68e065 100755\n--- a/t/t5620-backfill.sh\n+++ b/t/t5620-backfill.sh\n@@ -20,7 +20,7 @@ test_expect_success 'backfill rejects incompatible filter options' '\n \ttest_grep \"cannot backfill with these filter options\" err &&\n \n \ttest_must_fail git backfill --objects --filter=blob:limit=10m 2>err &&\n-\ttest_grep \"cannot backfill with these filter options\" err\n+\ttest_grep \"cannot backfill with blob size limits\" err\n '\n \n # We create objects in the 'src' repo.\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex b0ee31ee2d..45f366d738 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -477,4 +477,86 @@ test_expect_success 'topic only, blob:none filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, blob:limit=0 filter' '\n+\ttest-tool path-walk --filter=blob:limit=0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:tree:a/:$(git rev-parse base:a)\n+\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t6:tree:left/:$(git rev-parse base:left)\n+\t6:tree:left/:$(git rev-parse base~2:left)\n+\t7:tree:right/:$(git rev-parse topic:right)\n+\t7:tree:right/:$(git rev-parse base~1:right)\n+\t7:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, blob:limit=3 filter' '\n+\ttest-tool path-walk --filter=blob:limit=3 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:tree:right/:$(git rev-parse topic:right)\n+\t9:tree:right/:$(git rev-parse base~1:right)\n+\t9:tree:right/:$(git rev-parse base~2:right)\n+\t10:blob:right/c:$(git rev-parse base~2:right/c)\n+\t11:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543945","messageId":"31b4ef0fa10acc284ce31cd030dc788173471ee1.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 08/13] path-walk: add pl_sparse_trees to control tree pruning","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:32Z","receivedAt":"2026-05-22T18:24:48Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe path-walk API prunes trees and blobs when a sparse-checkout pattern\nlist is provided, which is the correct behavior for 'git backfill\n--sparse' since it only needs to fill in objects at paths within the\nsparse cone.\n\nHowever, a future change will use the path-walk API with a sparse:<oid>\nfilter that restricts only blobs while retaining all reachable trees.\nTo support both behaviors, add a 'pl_sparse_trees' flag to\npath_walk_info. When set (as in 'git backfill --sparse' and the\n--stdin-pl test helper mode), the sparse patterns prune both trees and\nblobs. When unset, only blobs are filtered and all trees are walked and\nreported.\n\nAdditionally, move the SEEN flag assignment in add_tree_entries() to\nafter the sparse pattern and pathspec checks. Previously, SEEN was set\nimmediately upon discovering an object, before checking whether its path\nmatched the sparse patterns. When the same object ID appeared at\nmultiple paths (e.g. sibling directories with identical contents), the\nfirst path to be visited would mark the object as SEEN. If that path was\noutside the sparse cone, the object would be skipped there but also\nnever discovered at its in-cone path.\n\nBy deferring the SEEN flag until after the checks pass, objects that are\nskipped due to sparse filtering remain discoverable at other paths where\nthey may be in scope.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/backfill.c        |  1 +\n path-walk.c               |  5 +++--\n path-walk.h               |  6 ++++++\n t/helper/test-path-walk.c |  6 +++++-\n t/t6601-path-walk.sh      | 37 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 52 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 5254a42711..e71e0f4742 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -109,6 +109,7 @@ static int do_backfill(struct backfill_context *ctx)\n \n \tif (ctx->sparse) {\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = 1;\n \t\tif (get_sparse_checkout_patterns(info.pl)) {\n \t\t\tpath_walk_info_clear(&info);\n \t\t\treturn error(_(\"problem loading sparse-checkout\"));\ndiff --git a/path-walk.c b/path-walk.c\nindex 04b924d4de..225857bbc8 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -183,7 +183,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t/* Skip this object if already seen. */\n \t\tif (o->flags & SEEN)\n \t\t\tcontinue;\n-\t\to->flags |= SEEN;\n \n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n@@ -204,7 +203,8 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\t\t\t\t  ctx->repo->index);\n \n \t\t\tif (ctx->info->pl->use_cone_patterns &&\n-\t\t\t    match == NOT_MATCHED)\n+\t\t\t    match == NOT_MATCHED &&\n+\t\t\t    (type == OBJ_BLOB || ctx->info->pl_sparse_trees))\n \t\t\t\tcontinue;\n \t\t\telse if (!ctx->info->pl->use_cone_patterns &&\n \t\t\t\t type == OBJ_BLOB &&\n@@ -239,6 +239,7 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\t\tcontinue;\n \t\t}\n \n+\t\to->flags |= SEEN;\n \t\tadd_path_to_list(ctx, path.buf, type, &entry.oid,\n \t\t\t\t !(o->flags & UNINTERESTING));\n \ndiff --git a/path-walk.h b/path-walk.h\nindex 60ceb65433..7e57ae5f65 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -76,8 +76,14 @@ struct path_walk_info {\n \t * of the cone. If not in cone mode, then all tree paths will be\n \t * explored but the path_fn will only be called when the path matches\n \t * the sparse-checkout patterns.\n+\t *\n+\t * When 'pl_sparse_trees' is zero, the sparse patterns only restrict\n+\t * blobs and all trees are included in the walk output. This matches\n+\t * the behavior of the sparse:oid object filter. When nonzero, trees\n+\t * are also pruned by the sparse patterns (as used by backfill).\n \t */\n \tstruct pattern_list *pl;\n+\tint pl_sparse_trees;\n };\n \n #define PATH_WALK_INFO_INIT {   \\\ndiff --git a/t/helper/test-path-walk.c b/t/helper/test-path-walk.c\nindex 88f86ae0dc..3f2b50a9aa 100644\n--- a/t/helper/test-path-walk.c\n+++ b/t/helper/test-path-walk.c\n@@ -68,7 +68,7 @@ static int emit_block(const char *path, struct oid_array *oids,\n \n int cmd__path_walk(int argc, const char **argv)\n {\n-\tint res, stdin_pl = 0;\n+\tint res, stdin_pl = 0, pl_sparse_trees = -1;\n \tstruct rev_info revs = REV_INFO_INIT;\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tstruct path_walk_test_data data = { 0 };\n@@ -89,6 +89,8 @@ int cmd__path_walk(int argc, const char **argv)\n \t\t\t N_(\"toggle aggressive edge walk\")),\n \t\tOPT_BOOL(0, \"stdin-pl\", &stdin_pl,\n \t\t\t N_(\"read a pattern list over stdin\")),\n+\t\tOPT_BOOL(0, \"pl-sparse-trees\", &pl_sparse_trees,\n+\t\t\t N_(\"toggle pruning of trees by sparse patterns\")),\n \t\tOPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),\n \t\tOPT_END(),\n \t};\n@@ -116,6 +118,8 @@ int cmd__path_walk(int argc, const char **argv)\n \tif (stdin_pl) {\n \t\tstruct strbuf in = STRBUF_INIT;\n \t\tCALLOC_ARRAY(info.pl, 1);\n+\t\tinfo.pl_sparse_trees = (pl_sparse_trees >= 0) ?\n+\t\t\tpl_sparse_trees : 1;\n \n \t\tinfo.pl->use_cone_patterns = 1;\n \ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 45f366d738..02ad83dfb0 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -206,6 +206,43 @@ test_expect_success 'base & topic, sparse' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'base & topic, sparse, no tree pruning' '\n+\tcat >patterns <<-EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\n+\ttest-tool path-walk --stdin-pl --no-pl-sparse-trees \\\n+\t\t-- base topic <patterns >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:a/:$(git rev-parse base:a)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:blob:left/b:$(git rev-parse base:left/b)\n+\t6:tree:right/:$(git rev-parse topic:right)\n+\t6:tree:right/:$(git rev-parse base~1:right)\n+\t6:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:3\n+\tcommits:4\n+\ttags:0\n+\ttrees:10\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'topic only' '\n \ttest-tool path-walk -- topic >out &&\n \n-- \ngitgitgadget\n\n"},{"id":"543946","messageId":"7d8f0aa036b7e1f1844e66a1158c3c3f1dbb0e8c.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 09/13] pack-objects: support sparse:oid filter with path-walk","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:33Z","receivedAt":"2026-05-22T18:24:50Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe --filter=sparse:<oid> option to 'git pack-objects' allows focusing\nan object set to a sparse-checkout definition. This reduces the set of\nmatching blobs while retaining all reachable trees. No server currently\nsupports fetching with this filter because it is expensive to compute\nand reachability bitmaps do not help without a significant effort to\nextend the bitmap feature to store bitmaps for each supported sparse-\ncheckout definition.\n\nWithout focusing on serving fetches and clones with these filters, there\nare still benefits that could be realized by making this faster. With\nthe sparse index, it's more realistic now than ever to be able to\noperate a local clone that was bootstrapped by a packfile created with\na sparse filter, because the missing trees are not needed to move a\nsparse-checkout from one commit to another or to view the history of any\npath in scope. Such clones could perhaps be bootstrapped by partial\nbundles.\n\nPreviously, constructing these sparse packs has been incredibly\ncomputationally inefficient. The revision walk that explores which\nobjects are in scope spends a lot of time checking each object to see if\nit matches the sparse-checkout patterns, causing quadratic behavior\n(number of objects times number of sparse-checkout patterns). This\nimproves somewhat when using cone-mode sparse-checkout patterns that can\nuse hashtables and prefix matches to determine containment. However, the\ncheck per object is still too expensive for most cases.\n\nThis is where the path-walk feature comes in. We can proceed as normal\nby placing objects in bins by path and _then_ check a group of objects\nall at once. Since sparse:<oid> only restricts blobs, the path-walk must\ninclude all reachable trees while using the cone-mode patterns to skip\nblobs at paths outside the sparse scope. This establishes a baseline for\na potential future \"treesparse:<oid>\" filter that would also restrict\ntrees, but introducing such a new filter is deferred to a later change.\n\nThe implementation here is focused around loading the sparse-checkout\npatterns from the provided object ID and checking that the patterns are\nindeed cone-mode patterns. We can then load the correct pattern list\ninto the path walk context and use the logic that already exists from\nbff45557675 (backfill: add --sparse option, 2025-02-03), though that\nfeature loads sparse-checkout patterns from the worktree's local\nsettings and also restricts tree objects. We use a combination of errors\nand warnings to signal problems during this load. The difference is that\nerrors are likely fatal for the non-path-walk version while the warnings\nare probably just implementation details for the path-walk version and\nthe 'git pack-objects' command can fall back to the revision walk\nversion.\n\nNow that the SEEN flag is deferred until after pattern checks (from the\nprevious commit), handle the case where a tree with a shared OID appears\nat both an out-of-cone and in-cone path. When trees are not being pruned\n(pl_sparse_trees == 0), the path-walk re-walks the tree at the in-cone\npath so that in-cone blobs within it are discovered. The new tests in\nt5317 and t6601 demonstrate this behavior and would fail without these\nchanges.\n\nThe performance test p5315 shows the impact of this change when using\nsparse filters:\n\nTest                                              HEAD~1     HEAD\n----------------------------------------------------------------------\n5315.10: repack (sparse:oid)                      77.98    77.47  -0.7%\n5315.11: repack size (sparse:oid)                187.5M   187.4M  -0.0%\n5315.12: repack (sparse:oid, --path-walk)         77.91    31.41 -59.7%\n5315.13: repack size (sparse:oid, --path-walk)   187.5M   161.1M -14.1%\n\nThese performance tests were run on the Git repository. The --path-walk\nfeature shows meaningful space savings (14% smaller for sparse packs)\nand dramatic time savings (60% faster) by leveraging the path-walk's\nability to skip blobs outside the sparse scope.\n\nCo-authored-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Taylor Blaue <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-backfill.adoc        |   4 +\n Documentation/git-pack-objects.adoc    |   3 +-\n builtin/pack-objects.c                 |  16 ++-\n path-walk.c                            |  81 ++++++++++++++-\n t/t5317-pack-objects-filter-objects.sh | 125 +++++++++++++++++++++++\n t/t6601-path-walk.sh                   | 131 +++++++++++++++++++++++++\n 6 files changed, 350 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc\nindex c0a3b80615..82d6a1969d 100644\n--- a/Documentation/git-backfill.adoc\n+++ b/Documentation/git-backfill.adoc\n@@ -80,6 +80,10 @@ OPTIONS\n +\n You may also use commit-limiting options understood by\n linkgit:git-rev-list[1] such as `--first-parent`, `--since`, or pathspecs.\n++\n+Most `--filter=<spec>` options don't work with the purpose of\n+`git backfill`, but the `sparse:<oid>` filter is integrated to provide a\n+focused set of paths to download, distinct from the `--sparse` option.\n \n SEE ALSO\n --------\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 85ae48b699..e38853391b 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,7 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n-supports the `--filter=<spec>` forms `blob:none` and `blob:limit=<n>`.\n+supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n+`sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex bc9fb5b457..b783dc62bc 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4754,7 +4754,7 @@ static int add_objects_by_path(const char *path,\n \treturn 0;\n }\n \n-static void get_object_list_path_walk(struct rev_info *revs)\n+static int get_object_list_path_walk(struct rev_info *revs)\n {\n \tstruct path_walk_info info = PATH_WALK_INFO_INIT;\n \tunsigned int processed = 0;\n@@ -4777,8 +4777,9 @@ static void get_object_list_path_walk(struct rev_info *revs)\n \tresult = walk_objects_by_path(&info);\n \ttrace2_region_leave(\"pack-objects\", \"path-walk\", revs->repo);\n \n-\tif (result)\n-\t\tdie(_(\"failed to pack objects via path-walk\"));\n+\tpath_walk_info_clear(&info);\n+\n+\treturn result;\n }\n \n static void get_object_list(struct rev_info *revs, struct strvec *argv)\n@@ -4841,8 +4842,13 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)\n \t\tfn_show_object = show_object;\n \n \tif (path_walk) {\n-\t\tget_object_list_path_walk(revs);\n-\t} else {\n+\t\tif (get_object_list_path_walk(revs)) {\n+\t\t\twarning(_(\"failed to pack objects via path-walk\"));\n+\t\t\tpath_walk = 0;\n+\t\t}\n+\t}\n+\n+\tif (!path_walk) {\n \t\tif (prepare_revision_walk(revs))\n \t\t\tdie(_(\"revision walk setup failed\"));\n \t\tmark_edges_uninteresting(revs, show_edge, sparse);\ndiff --git a/path-walk.c b/path-walk.c\nindex 225857bbc8..ce38dcf1e9 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -10,6 +10,7 @@\n #include \"hex.h\"\n #include \"list-objects.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"object-name.h\"\n #include \"odb.h\"\n #include \"object.h\"\n #include \"oid-array.h\"\n@@ -180,10 +181,6 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\t\treturn -1;\n \t\t}\n \n-\t\t/* Skip this object if already seen. */\n-\t\tif (o->flags & SEEN)\n-\t\t\tcontinue;\n-\n \t\tstrbuf_setlen(&path, base_len);\n \t\tstrbuf_add(&path, entry.path, entry.pathlen);\n \n@@ -194,6 +191,40 @@ static int add_tree_entries(struct path_walk_context *ctx,\n \t\tif (type == OBJ_TREE)\n \t\t\tstrbuf_addch(&path, '/');\n \n+\t\tif (o->flags & SEEN) {\n+\t\t\t/*\n+\t\t\t * A tree with a shared OID may appear at multiple\n+\t\t\t * paths. Even though we already added this tree to\n+\t\t\t * the output at some other path, we still need to\n+\t\t\t * walk into it at this in-cone path to discover\n+\t\t\t * blobs that were not found at the earlier\n+\t\t\t * out-of-cone path.\n+\t\t\t *\n+\t\t\t * Only do this for paths not yet in our map, to\n+\t\t\t * avoid duplicate entries when the same tree OID\n+\t\t\t * appears at the same path across multiple commits.\n+\t\t\t */\n+\t\t\tif (type == OBJ_TREE && ctx->info->pl &&\n+\t\t\t    ctx->info->pl->use_cone_patterns &&\n+\t\t\t    !ctx->info->pl_sparse_trees &&\n+\t\t\t    !strmap_contains(&ctx->paths_to_lists, path.buf)) {\n+\t\t\t\tint dtype;\n+\t\t\t\tenum pattern_match_result m;\n+\t\t\t\tm = path_matches_pattern_list(path.buf, path.len,\n+\t\t\t\t\t\t\t      path.buf + base_len,\n+\t\t\t\t\t\t\t      &dtype,\n+\t\t\t\t\t\t\t      ctx->info->pl,\n+\t\t\t\t\t\t\t      ctx->repo->index);\n+\t\t\t\tif (m != NOT_MATCHED) {\n+\t\t\t\t\tadd_path_to_list(ctx, path.buf, type,\n+\t\t\t\t\t\t\t &entry.oid,\n+\t\t\t\t\t\t\t !(o->flags & UNINTERESTING));\n+\t\t\t\t\tpush_to_stack(ctx, path.buf);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (ctx->info->pl) {\n \t\t\tint dtype;\n \t\t\tenum pattern_match_result match;\n@@ -544,6 +575,48 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_SPARSE_OID:\n+\t\tif (info) {\n+\t\t\tstruct object_id sparse_oid;\n+\t\t\tstruct repository *repo = info->revs->repo;\n+\n+\t\t\tif (info->pl) {\n+\t\t\t\twarning(_(\"sparse filter cannot be combined with existing sparse patterns\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (repo_get_oid_with_flags(repo,\n+\t\t\t\t\t\t    options->sparse_oid_name,\n+\t\t\t\t\t\t    &sparse_oid,\n+\t\t\t\t\t\t    GET_OID_BLOB)) {\n+\t\t\t\terror(_(\"unable to access sparse blob in '%s'\"),\n+\t\t\t\t      options->sparse_oid_name);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tCALLOC_ARRAY(info->pl, 1);\n+\t\t\tinfo->pl->use_cone_patterns = 1;\n+\n+\t\t\tif (add_patterns_from_blob_to_list(&sparse_oid, \"\", 0,\n+\t\t\t\t\t\t\t   info->pl) < 0) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\terror(_(\"unable to parse sparse filter data in '%s'\"),\n+\t\t\t\t      oid_to_hex(&sparse_oid));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tif (!info->pl->use_cone_patterns) {\n+\t\t\t\tclear_pattern_list(info->pl);\n+\t\t\t\tFREE_AND_NULL(info->pl);\n+\t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tdefault:\n \t\terror(_(\"object filter '%s' not supported by the path-walk API\"),\n \t\t      list_objects_filter_spec(options));\ndiff --git a/t/t5317-pack-objects-filter-objects.sh b/t/t5317-pack-objects-filter-objects.sh\nindex 501d715b9a..dddb79ba62 100755\n--- a/t/t5317-pack-objects-filter-objects.sh\n+++ b/t/t5317-pack-objects-filter-objects.sh\n@@ -478,4 +478,129 @@ test_expect_success 'verify pack-objects w/ --missing=allow-any' '\n \tEOF\n '\n \n+# Test that --path-walk produces the same object set as standard traversal\n+# when using sparse:oid filters with cone-mode patterns.\n+#\n+# The sparse:oid filter restricts only blobs, not trees. Both standard\n+# and path-walk should produce identical sets of blobs, commits, and trees.\n+\n+test_expect_success 'setup pw_sparse for path-walk comparison' '\n+\tgit init pw_sparse &&\n+\tmkdir -p pw_sparse/inc/sub pw_sparse/exc/sub &&\n+\n+\tfor n in 1 2\n+\tdo\n+\t\techo \"inc $n\" >pw_sparse/inc/file$n &&\n+\t\techo \"inc sub $n\" >pw_sparse/inc/sub/file$n &&\n+\t\techo \"exc $n\" >pw_sparse/exc/file$n &&\n+\t\techo \"exc sub $n\" >pw_sparse/exc/sub/file$n &&\n+\t\techo \"root $n\" >pw_sparse/root$n || return 1\n+\tdone &&\n+\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"first\" &&\n+\n+\techo \"inc 1 modified\" >pw_sparse/inc/file1 &&\n+\techo \"exc 1 modified\" >pw_sparse/exc/file1 &&\n+\techo \"root 1 modified\" >pw_sparse/root1 &&\n+\tgit -C pw_sparse add . &&\n+\tgit -C pw_sparse commit -m \"second\" &&\n+\n+\t# Cone-mode sparse pattern: include root + inc/\n+\tprintf \"/*\\n!/*/\\n/inc/\\n\" |\n+\tgit -C pw_sparse hash-object -w --stdin >sparse_oid\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk produces same blobs' '\n+\toid=$(cat sparse_oid) &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >standard.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../standard.pack &&\n+\tgit -C pw_sparse verify-pack -v ../standard.pack >standard_verify &&\n+\n+\tgit -C pw_sparse pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >pathwalk.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_sparse index-pack ../pathwalk.pack &&\n+\tgit -C pw_sparse verify-pack -v ../pathwalk.pack >pathwalk_verify &&\n+\n+\t# Blobs must match exactly\n+\tgrep -E \"^[0-9a-f]{40} blob\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_blobs &&\n+\ttest_cmp standard_blobs pathwalk_blobs &&\n+\n+\t# Commits must match exactly\n+\tgrep -E \"^[0-9a-f]{40} commit\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_commits &&\n+\tgrep -E \"^[0-9a-f]{40} commit\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_commits &&\n+\ttest_cmp standard_commits pathwalk_commits\n+'\n+\n+test_expect_success 'sparse:oid with --path-walk includes all trees' '\n+\t# The sparse:oid filter restricts only blobs, not trees.\n+\t# Both standard and path-walk should include the same trees.\n+\tgrep -E \"^[0-9a-f]{40} tree\" standard_verify |\n+\tawk \"{print \\$1}\" | sort >standard_trees &&\n+\tgrep -E \"^[0-9a-f]{40} tree\" pathwalk_verify |\n+\tawk \"{print \\$1}\" | sort >pathwalk_trees &&\n+\n+\ttest_cmp standard_trees pathwalk_trees\n+'\n+\n+# Test the edge case where the same tree/blob OID appears at both an\n+# in-cone and out-of-cone path. When sibling directories have identical\n+# contents, they share a tree OID. The path-walk defers marking objects\n+# SEEN until after checking sparse patterns, so an object at an out-of-cone\n+# path can still be discovered at an in-cone path.\n+\n+test_expect_success 'setup pw_shared for shared OID across cone boundary' '\n+\tgit init pw_shared &&\n+\tmkdir pw_shared/aaa pw_shared/zzz &&\n+\techo \"shared content\" >pw_shared/aaa/file &&\n+\techo \"shared content\" >pw_shared/zzz/file &&\n+\techo \"root file\" >pw_shared/rootfile &&\n+\tgit -C pw_shared add . &&\n+\tgit -C pw_shared commit -m \"aaa and zzz share tree OID\" &&\n+\n+\t# Verify they share a tree OID\n+\taaa_tree=$(git -C pw_shared rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git -C pw_shared rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tprintf \"/*\\n!/*/\\n/zzz/\\n\" |\n+\tgit -C pw_shared hash-object -w --stdin >shared_sparse_oid\n+'\n+\n+test_expect_success 'shared tree OID: --path-walk blobs match standard' '\n+\toid=$(cat shared_sparse_oid) &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--filter=sparse:oid=$oid >shared_std.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_std.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_std.pack >shared_std_verify &&\n+\n+\tgit -C pw_shared pack-objects --revs --stdout \\\n+\t\t--path-walk --filter=sparse:oid=$oid >shared_pw.pack <<-EOF &&\n+\tHEAD\n+\tEOF\n+\tgit -C pw_shared index-pack ../shared_pw.pack &&\n+\tgit -C pw_shared verify-pack -v ../shared_pw.pack >shared_pw_verify &&\n+\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_std_verify |\n+\tawk \"{print \\$1}\" | sort >shared_std_blobs &&\n+\tgrep -E \"^[0-9a-f]{40} blob\" shared_pw_verify |\n+\tawk \"{print \\$1}\" | sort >shared_pw_blobs &&\n+\ttest_cmp shared_std_blobs shared_pw_blobs\n+'\n+\n test_done\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 02ad83dfb0..ac294867a5 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -596,4 +596,135 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'setup sparse filter blob' '\n+\t# Cone-mode patterns: include root, exclude all dirs, include left/\n+\tcat >patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/left/\n+\tEOF\n+\tsparse_oid=$(git hash-object -w -t blob patterns)\n+'\n+\n+test_expect_success 'all, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree::$(git rev-parse topic^{tree})\n+\t3:tree::$(git rev-parse base^{tree})\n+\t3:tree::$(git rev-parse base~1^{tree})\n+\t3:tree::$(git rev-parse base~2^{tree})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n+\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n+\t4:blob:a:$(git rev-parse base~2:a)\n+\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t9:blob:left/b:$(git rev-parse base:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:6\n+\tcommits:4\n+\ttags:7\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, sparse:oid filter' '\n+\ttest-tool path-walk --filter=sparse:oid=$sparse_oid -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tree::$(git rev-parse topic^{tree})\n+\t1:tree::$(git rev-parse base~1^{tree})\n+\t1:tree::$(git rev-parse base~2^{tree})\n+\t2:blob:a:$(git rev-parse base~2:a)\n+\t3:tree:left/:$(git rev-parse base~2:left)\n+\t4:blob:left/b:$(git rev-parse base~2:left/b)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:2\n+\tcommits:3\n+\ttags:0\n+\ttrees:7\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+# Demonstrate the SEEN flag ordering issue: when the same tree/blob OID\n+# appears at two sibling paths where one is in-cone and the other is\n+# out-of-cone, the path-walk must still discover blobs at the in-cone\n+# path even when the shared tree OID was first encountered out-of-cone.\n+# Since sparse:oid includes all trees, the out-of-cone tree (aaa/) is\n+# walked first, and its blob is skipped. The path-walk then re-walks\n+# the same tree OID at the in-cone path (zzz/) to find the blob there.\n+\n+test_expect_success 'setup shared tree OID across cone boundary' '\n+\tgit checkout --orphan shared-tree &&\n+\tgit rm -rf . &&\n+\tmkdir aaa zzz &&\n+\techo \"shared content\" >aaa/file &&\n+\techo \"shared content\" >zzz/file &&\n+\techo \"root file\" >rootfile &&\n+\tgit add aaa zzz rootfile &&\n+\tgit commit -m \"aaa and zzz have same tree OID\" &&\n+\n+\t# Verify they really share a tree OID\n+\taaa_tree=$(git rev-parse HEAD:aaa) &&\n+\tzzz_tree=$(git rev-parse HEAD:zzz) &&\n+\ttest \"$aaa_tree\" = \"$zzz_tree\" &&\n+\n+\t# Cone pattern: include root + zzz/ (not aaa/)\n+\tcat >shared-patterns <<-\\EOF &&\n+\t/*\n+\t!/*/\n+\t/zzz/\n+\tEOF\n+\tshared_sparse_oid=$(git hash-object -w -t blob shared-patterns)\n+'\n+\n+test_expect_success 'sparse:oid with shared tree OID across cone boundary' '\n+\ttest-tool path-walk \\\n+\t\t--filter=sparse:oid=$shared_sparse_oid \\\n+\t\t-- shared-tree >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse shared-tree)\n+\t1:tree::$(git rev-parse shared-tree^{tree})\n+\t2:blob:rootfile:$(git rev-parse shared-tree:rootfile)\n+\t3:tree:aaa/:$(git rev-parse shared-tree:aaa)\n+\t4:tree:zzz/:$(git rev-parse shared-tree:zzz)\n+\t5:blob:zzz/file:$(git rev-parse shared-tree:zzz/file)\n+\tblobs:2\n+\tcommits:1\n+\ttags:0\n+\ttrees:3\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"543947","messageId":"a68676d0de4e941602dbed8840df783b7392ea6a.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 10/13] t6601: tag otherwise-unreachable trees","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:34Z","receivedAt":"2026-05-22T18:24:51Z","isPatch":true,"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe tests in t6601-path-walk.sh demonstrate the behavior of the\npath-walk API under different conditions. One thing that I noticed while\nupdating the behavior of directly-requested objects is that we don't\nactually emit tagged trees. This was previously not noticed due to those\ntagged trees actually being reachable from commits that we are including\nin the path-walk.\n\nUpdate the test setup to have tree-tag and tree-tag2 point to trees that\nare otherwise unreachable.\n\nIt is worth noting that this does not meaningfully change any of the\nother test cases, demontrating the bug.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t6601-path-walk.sh | 24 +++++++++++-------------\n 1 file changed, 11 insertions(+), 13 deletions(-)\n\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex ac294867a5..92c524d145 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -7,17 +7,15 @@ test_description='direct path-walk API tests'\n test_expect_success 'setup test repository' '\n \tgit checkout -b base &&\n \n-\t# Make some objects that will only be reachable\n-\t# via non-commit tags.\n-\tmkdir child &&\n-\techo file >child/file &&\n-\tgit add child &&\n-\tgit commit -m \"will abandon\" &&\n-\tgit tag -a -m \"tree\" tree-tag HEAD^{tree} &&\n-\techo file2 >file2 &&\n-\tgit add file2 &&\n-\tgit commit --amend -m \"will abandon\" &&\n-\tgit tag tree-tag2 HEAD^{tree} &&\n+\t# Create tree objects that are only reachable via tags,\n+\t# not from any commit in the history.\n+\tchild_blob_oid=$(echo \"child blob content\" | git hash-object -t blob -w --stdin) &&\n+\tchild_tree_oid=$(printf \"100644 blob %s\\tfile\\n\" \"$child_blob_oid\" | git mktree) &&\n+\ttree_tag_oid=$(printf \"040000 tree %s\\tchild\\n\" \"$child_tree_oid\" | git mktree) &&\n+\tgit tag -a -m \"tree\" tree-tag \"$tree_tag_oid\" &&\n+\tfile2_blob_oid=$(echo \"tagged tree file2\" | git hash-object -t blob -w --stdin) &&\n+\ttree_tag2_oid=$(printf \"040000 tree %s\\tchild\\n100644 blob %s\\tfile2\\n\" \"$child_tree_oid\" \"$file2_blob_oid\" | git mktree) &&\n+\tgit tag tree-tag2 \"$tree_tag2_oid\" &&\n \n \techo blob >file &&\n \tblob_oid=$(git hash-object -t blob -w --stdin <file) &&\n@@ -26,7 +24,7 @@ test_expect_success 'setup test repository' '\n \tblob2_oid=$(git hash-object -t blob -w --stdin <file2) &&\n \tgit tag blob-tag2 \"$blob2_oid\" &&\n \n-\trm -fr child file file2 &&\n+\trm -fr file file2 &&\n \n \tmkdir left &&\n \tmkdir right &&\n@@ -34,7 +32,7 @@ test_expect_success 'setup test repository' '\n \techo b >left/b &&\n \techo c >right/c &&\n \tgit add . &&\n-\tgit commit --amend -m \"first\" &&\n+\tgit commit -m \"first\" &&\n \tgit tag -m \"first\" first HEAD &&\n \n \techo d >right/d &&\n-- \ngitgitgadget\n\n"},{"id":"543948","messageId":"b0db73c6cc2ed70675e35d7cfdd1a484ab337486.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 11/13] path-walk: support `tree:0` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:35Z","receivedAt":"2026-05-22T18:24:52Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `tree:0` object filter omits all trees and blobs from the result,\nkeeping only commits and tags. Consequently, this filter type should\nhas a fairly straightforward integration with path-walk, as the decision\nto include an object depends only on its type and does not depend on any\npath-sensitive state.\n\nMapping it onto `path_walk_info` is direct: set `info->trees = 0` and\n`info->blobs = 0` in `prepare_filters()` when the `LOFC_TREE_DEPTH`\nchoice is requested with depth zero. The existing code already plumbs\nthose flags through the rest of the walk:\n\n - 'walk_objects_by_path()' sets `revs->blob_objects = info->blobs` and\n   `revs->tree_objects = info->trees` before `prepare_revision_walk()`,\n   so the revision walk doesn't try to enumerate trees or blobs itself.\n\n - The commit-walk loop short-circuits the root-tree fetch with\n   \"if (!info->trees && !info->blobs) continue;\", so we never even\n   look up the root tree, let alone descend into it.\n\n - `setup_pending_objects()` skips pending trees and blobs based on\n   the same flags.\n\nThis means the path-walk doesn't allocate or expand any tree structures\nat all under `tree:0`, which matches the intended behavior of the\nfilter.\n\nHowever, this requires first fixing some issues with how the path-walk\nAPI handles directly-requested trees _and_ trees requested through\nlightweight tags. These changes create substantial updates to\nt6601-path-walk.sh, which the previous change highlighted as a problem\nby tagging otherwise-unreachable trees and having them not appear in the\noutput.\n\nNon-zero tree-depth filters are not supported. Those depend on the depth\nat which a tree is visited, which is a path-walk concept the filter\nmachinery doesn't currently share with the path-walk API. Reject them in\n`prepare_filters()` with a helpful error and let pack-objects fall back\nto the regular traversal, the same way it already does for unsupported\nfilters.\n\nAdd coverage in t6601 for both `--all` and a single-branch case to\nconfirm that no trees or blobs are emitted, and a separate test that\n`tree:1` is rejected with the expected error message. Place the new\ntests before \"setup sparse filter blob\" so they run on the original set\nof refs, before the orphan branch that the sparse-tree tests create.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |   4 +-\n path-walk.c                         |  53 +++++++--\n t/t6601-path-walk.sh                | 165 ++++++++++++++++++----------\n 3 files changed, 152 insertions(+), 70 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex e38853391b..c86219be91 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -404,8 +404,8 @@ will be automatically changed to version `1`.\n +\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n-supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`, and\n-`sparse:<oid>`.\n+supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n+`tree:0`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex ce38dcf1e9..cb67b8ce86 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -390,11 +390,18 @@ static int walk_path(struct path_walk_context *ctx,\n \t\t\t\t\tctx->info->path_fn_data);\n \t}\n \n-\t/* Expand data for children. */\n-\tif (list->type == OBJ_TREE) {\n+\t/*\n+\t * Expand tree children, except when the set is directly requested\n+\t * _and_ we are otherwise filtering out trees.\n+\t */\n+\tif (list->type == OBJ_TREE &&\n+\t    (!path_is_for_direct_objects(path) || ctx->info->trees)) {\n+\t\t/* Use root path if expanding from tagged/direct trees. */\n+\t\tconst char *expand_path = !strcmp(path, \"/tagged-trees\")\n+\t\t\t\t\t  ? root_path : path;\n \t\tfor (size_t i = 0; i < list->oids.nr; i++) {\n \t\t\tret |= add_tree_entries(ctx,\n-\t\t\t\t\t    path,\n+\t\t\t\t\t    expand_path,\n \t\t\t\t\t    &list->oids.oid[i]);\n \t\t}\n \t}\n@@ -442,12 +449,12 @@ static int setup_pending_objects(struct path_walk_info *info,\n {\n \tstruct type_and_oid_list *tags = NULL;\n \tstruct type_and_oid_list *tagged_blobs = NULL;\n-\tstruct type_and_oid_list *root_tree_list = NULL;\n+\tstruct type_and_oid_list *tagged_trees = NULL;\n \n \tif (info->tags)\n \t\tCALLOC_ARRAY(tags, 1);\n \tCALLOC_ARRAY(tagged_blobs, 1);\n-\troot_tree_list = strmap_get(&ctx->paths_to_lists, root_path);\n+\tCALLOC_ARRAY(tagged_trees, 1);\n \n \t/*\n \t * Pending objects include:\n@@ -491,14 +498,15 @@ static int setup_pending_objects(struct path_walk_info *info,\n \n \t\tswitch (obj->type) {\n \t\tcase OBJ_TREE:\n-\t\t\tif (pending->path) {\n-\t\t\t\tchar *path = *pending->path ? xstrfmt(\"%s/\", pending->path)\n-\t\t\t\t\t\t\t    : xstrdup(\"\");\n+\t\t\tif (pending->path && *pending->path) {\n+\t\t\t\tchar *path = xstrfmt(\"%s/\", pending->path);\n \t\t\t\tadd_path_to_list(ctx, path, OBJ_TREE, &obj->oid, 1);\n \t\t\t\tfree(path);\n+\t\t\t} else if (!pending->path || !info->trees) {\n+\t\t\t\toid_array_append(&tagged_trees->oids, &obj->oid);\n \t\t\t} else {\n-\t\t\t\t/* assume a root tree, such as a lightweight tag. */\n-\t\t\t\toid_array_append(&root_tree_list->oids, &obj->oid);\n+\t\t\t\tadd_path_to_list(ctx, root_path, OBJ_TREE,\n+\t\t\t\t\t\t &obj->oid, 1);\n \t\t\t}\n \t\t\tbreak;\n \n@@ -535,6 +543,18 @@ static int setup_pending_objects(struct path_walk_info *info,\n \t\t\tfree(tagged_blobs);\n \t\t}\n \t}\n+\tif (tagged_trees) {\n+\t\tif (tagged_trees->oids.nr) {\n+\t\t\tconst char *tagged_tree_path = \"/tagged-trees\";\n+\t\t\ttagged_trees->type = OBJ_TREE;\n+\t\t\ttagged_trees->maybe_interesting = 1;\n+\t\t\tstrmap_put(&ctx->paths_to_lists, tagged_tree_path, tagged_trees);\n+\t\t\tpush_to_stack(ctx, tagged_tree_path);\n+\t\t} else {\n+\t\t\toid_array_clear(&tagged_trees->oids);\n+\t\t\tfree(tagged_trees);\n+\t\t}\n+\t}\n \tif (tags) {\n \t\tif (tags->oids.nr) {\n \t\t\tconst char *tag_path = \"/tags\";\n@@ -575,6 +595,19 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_TREE_DEPTH:\n+\t\tif (options->tree_exclude_depth) {\n+\t\t\terror(_(\"tree:%lu filter not supported by the path-walk API\"),\n+\t\t\t      options->tree_exclude_depth);\n+\t\t\treturn 0;\n+\t\t}\n+\t\tif (info) {\n+\t\t\tinfo->trees = 0;\n+\t\t\tinfo->blobs = 0;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 92c524d145..566db7c7e3 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -77,23 +77,23 @@ test_expect_success 'all' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n \t4:blob:a:$(git rev-parse base~2:a)\n-\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n-\t6:tree:a/:$(git rev-parse base:a)\n-\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t8:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n-\t9:tree:left/:$(git rev-parse base:left)\n-\t9:tree:left/:$(git rev-parse base~2:left)\n-\t10:blob:left/b:$(git rev-parse base~2:left/b)\n-\t10:blob:left/b:$(git rev-parse base:left/b)\n-\t11:tree:right/:$(git rev-parse topic:right)\n-\t11:tree:right/:$(git rev-parse base~1:right)\n-\t11:tree:right/:$(git rev-parse base~2:right)\n-\t12:blob:right/c:$(git rev-parse base~2:right/c)\n-\t12:blob:right/c:$(git rev-parse topic:right/c)\n-\t13:blob:right/d:$(git rev-parse base~1:right/d)\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t7:tree:a/:$(git rev-parse base:a)\n+\t8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t9:blob:child/file:$(git rev-parse refs/tags/tree-tag:child/file)\n+\t10:tree:left/:$(git rev-parse base:left)\n+\t10:tree:left/:$(git rev-parse base~2:left)\n+\t11:blob:left/b:$(git rev-parse base~2:left/b)\n+\t11:blob:left/b:$(git rev-parse base:left/b)\n+\t12:tree:right/:$(git rev-parse topic:right)\n+\t12:tree:right/:$(git rev-parse base~1:right)\n+\t12:tree:right/:$(git rev-parse base~2:right)\n+\t13:blob:right/c:$(git rev-parse base~2:right/c)\n+\t13:blob:right/c:$(git rev-parse topic:right/c)\n+\t14:blob:right/d:$(git rev-parse base~1:right/d)\n \tblobs:10\n \tcommits:4\n \ttags:7\n@@ -471,15 +471,15 @@ test_expect_success 'all, blob:none filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n-\t4:tree:a/:$(git rev-parse base:a)\n-\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t6:tree:left/:$(git rev-parse base:left)\n-\t6:tree:left/:$(git rev-parse base~2:left)\n-\t7:tree:right/:$(git rev-parse topic:right)\n-\t7:tree:right/:$(git rev-parse base~1:right)\n-\t7:tree:right/:$(git rev-parse base~2:right)\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:tree:right/:$(git rev-parse topic:right)\n+\t8:tree:right/:$(git rev-parse base~1:right)\n+\t8:tree:right/:$(git rev-parse base~2:right)\n \tblobs:2\n \tcommits:4\n \ttags:7\n@@ -533,15 +533,15 @@ test_expect_success 'all, blob:limit=0 filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n-\t4:tree:a/:$(git rev-parse base:a)\n-\t5:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t6:tree:left/:$(git rev-parse base:left)\n-\t6:tree:left/:$(git rev-parse base~2:left)\n-\t7:tree:right/:$(git rev-parse topic:right)\n-\t7:tree:right/:$(git rev-parse base~1:right)\n-\t7:tree:right/:$(git rev-parse base~2:right)\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t4:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t5:tree:a/:$(git rev-parse base:a)\n+\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t7:tree:left/:$(git rev-parse base:left)\n+\t7:tree:left/:$(git rev-parse base~2:left)\n+\t8:tree:right/:$(git rev-parse topic:right)\n+\t8:tree:right/:$(git rev-parse base~1:right)\n+\t8:tree:right/:$(git rev-parse base~2:right)\n \tblobs:2\n \tcommits:4\n \ttags:7\n@@ -572,19 +572,19 @@ test_expect_success 'all, blob:limit=3 filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n \t4:blob:a:$(git rev-parse base~2:a)\n-\t5:tree:a/:$(git rev-parse base:a)\n-\t6:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t7:tree:left/:$(git rev-parse base:left)\n-\t7:tree:left/:$(git rev-parse base~2:left)\n-\t8:blob:left/b:$(git rev-parse base~2:left/b)\n-\t9:tree:right/:$(git rev-parse topic:right)\n-\t9:tree:right/:$(git rev-parse base~1:right)\n-\t9:tree:right/:$(git rev-parse base~2:right)\n-\t10:blob:right/c:$(git rev-parse base~2:right/c)\n-\t11:blob:right/d:$(git rev-parse base~1:right/d)\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t6:tree:a/:$(git rev-parse base:a)\n+\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t8:tree:left/:$(git rev-parse base:left)\n+\t8:tree:left/:$(git rev-parse base~2:left)\n+\t9:blob:left/b:$(git rev-parse base~2:left/b)\n+\t10:tree:right/:$(git rev-parse topic:right)\n+\t10:tree:right/:$(git rev-parse base~1:right)\n+\t10:tree:right/:$(git rev-parse base~2:right)\n+\t11:blob:right/c:$(git rev-parse base~2:right/c)\n+\t12:blob:right/d:$(git rev-parse base~1:right/d)\n \tblobs:6\n \tcommits:4\n \ttags:7\n@@ -594,6 +594,55 @@ test_expect_success 'all, blob:limit=3 filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:2\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'topic only, tree:0 filter' '\n+\ttest-tool path-walk --filter=tree:0 -- topic >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:3\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'tree:1 filter is rejected' '\n+\ttest_must_fail test-tool path-walk --filter=tree:1 -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n@@ -625,19 +674,19 @@ test_expect_success 'all, sparse:oid filter' '\n \t3:tree::$(git rev-parse base^{tree})\n \t3:tree::$(git rev-parse base~1^{tree})\n \t3:tree::$(git rev-parse base~2^{tree})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag^{})\n-\t3:tree::$(git rev-parse refs/tags/tree-tag2^{})\n \t4:blob:a:$(git rev-parse base~2:a)\n-\t5:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n-\t6:tree:a/:$(git rev-parse base:a)\n-\t7:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n-\t8:tree:left/:$(git rev-parse base:left)\n-\t8:tree:left/:$(git rev-parse base~2:left)\n-\t9:blob:left/b:$(git rev-parse base~2:left/b)\n-\t9:blob:left/b:$(git rev-parse base:left/b)\n-\t10:tree:right/:$(git rev-parse topic:right)\n-\t10:tree:right/:$(git rev-parse base~1:right)\n-\t10:tree:right/:$(git rev-parse base~2:right)\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t5:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t6:blob:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)\n+\t7:tree:a/:$(git rev-parse base:a)\n+\t8:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t9:tree:left/:$(git rev-parse base:left)\n+\t9:tree:left/:$(git rev-parse base~2:left)\n+\t10:blob:left/b:$(git rev-parse base~2:left/b)\n+\t10:blob:left/b:$(git rev-parse base:left/b)\n+\t11:tree:right/:$(git rev-parse topic:right)\n+\t11:tree:right/:$(git rev-parse base~1:right)\n+\t11:tree:right/:$(git rev-parse base~2:right)\n \tblobs:6\n \tcommits:4\n \ttags:7\n-- \ngitgitgadget\n\n"},{"id":"543949","messageId":"6845988f50ed9452994007937f04c322225c4819.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 12/13] path-walk: support `object:type` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:36Z","receivedAt":"2026-05-22T18:24:53Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `object:type` filter accepts only objects of a single type; it is\nthe second member of the object-info-only filter family that bitmap\ntraversal already supports.\n\nLike `blob:none` and `tree:0`, it can be evaluated with nothing more\nthan the object's type, which is exactly the granularity path-walk's\nexisting info->{commits,trees,blobs,tags} flags already control.\n\nMap `LOFC_OBJECT_TYPE` in `prepare_filters()` by AND-ing each flag\nagainst the filtered type. A single `object:type=X` filter\napplied to the default info (all flags = 1) leaves `info->X = 1` and\nall the others 0, which is what we want.\n\nUsing an AND rather than straight assignment prepares us for a\nsubsequent change to implement combined object filters.\n\nThe path-walk machinery is mostly already wired for the per-type\ndistinction:\n\n - `walk_path()` calls `path_fn` for a batch only when the corresponding\n   `info->X` flag is set, so unwanted types are silently not reported.\n\n - `add_tree_entries()` skips tree entries of type `OBJ_BLOB` when\n   `info->blobs` is unset, so we don't even allocate paths for them.\n\n - The commit-walk loop short-circuits the root-tree fetch when\n   `!info->trees && !info->blobs`, so commit-only filters don't descend\n   into trees at all.\n\nBut there are a couple of side effects of the \"trees off, blobs on\" case\nthat need fixing:\n\n 1. 'setup_pending_objects()' previously skipped pending trees as soon\n    as `info->trees` was zero. For 'object:type=blob' the call site\n    needs those pending trees: a lightweight tag pointing to a tree, or\n    an annotated tag whose peeled target is a tree, can both reach\n    blobs that are otherwise unreachable from any commit's root tree.\n    Loosen the gate to \"if (!info->trees && !info->blobs) continue\" and\n    similarly retrieve the root_tree_list whenever either trees or\n    blobs are wanted.\n\n 2. The revision machinery's `handle_commit()` drops pending trees when\n    `revs->tree_objects` is zero (see the 'OBJ_TREE' handler in\n    revision.c), so by the time path-walk sees the pending list\n    after `prepare_revision_walk()` the tree-bearing pendings would\n    already be gone. Fix this by setting\n\n        revs->tree_objects = info->trees || info->blobs\n\n    so pending trees survive `prepare_revision_walk()` whenever we\n    need to walk into them. Path-walk still resets tree_objects to\n    zero immediately after `prepare_revision_walk()` returns, so the\n    rev-walk itself never enumerates trees redundantly with\n    path-walk's own descent.\n\nAdd coverage in t6601 for each of the four `object:type` values. The\n'object:type=blob' test in particular asserts that file2 and child/file\n(both reachable only through tag-pointed trees) show up in the output,\nexercising the pending-tree fix.\n\nUpdate Documentation/git-pack-objects.adoc to add object:type to\nthe list of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  2 +-\n path-walk.c                         | 13 ++++-\n path-walk.h                         |  6 +++\n t/t6601-path-walk.sh                | 84 +++++++++++++++++++++++++++++\n 4 files changed, 103 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex c86219be91..f2852ebd31 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,7 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n-`tree:0`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex cb67b8ce86..418972e753 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -382,7 +382,7 @@ static int walk_path(struct path_walk_context *ctx,\n \t\t\tret = ctx->info->path_fn(path, &filtered, list->type,\n \t\t\t\t\t\t ctx->info->path_fn_data);\n \t\toid_array_clear(&filtered);\n-\t} else if (path_is_for_direct_objects(path) ||\n+\t} else if ((!ctx->info->strict_types && path_is_for_direct_objects(path)) ||\n \t\t   (list->type == OBJ_TREE && ctx->info->trees) ||\n \t\t   (list->type == OBJ_BLOB && ctx->info->blobs) ||\n \t\t   (list->type == OBJ_TAG && ctx->info->tags)) {\n@@ -608,6 +608,17 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t}\n \t\treturn 1;\n \n+\tcase LOFC_OBJECT_TYPE:\n+\t\tif (info) {\n+\t\t\tinfo->commits &= options->object_type == OBJ_COMMIT;\n+\t\t\tinfo->tags &= options->object_type == OBJ_TAG;\n+\t\t\tinfo->trees &= options->object_type == OBJ_TREE;\n+\t\t\tinfo->blobs &= options->object_type == OBJ_BLOB;\n+\t\t\tinfo->strict_types = 1;\n+\t\t\tlist_objects_filter_release(options);\n+\t\t}\n+\t\treturn 1;\n+\n \tcase LOFC_SPARSE_OID:\n \t\tif (info) {\n \t\t\tstruct object_id sparse_oid;\ndiff --git a/path-walk.h b/path-walk.h\nindex 7e57ae5f65..a2652b2d46 100644\n--- a/path-walk.h\n+++ b/path-walk.h\n@@ -47,6 +47,12 @@ struct path_walk_info {\n \tint blobs;\n \tint tags;\n \n+\t/**\n+\t * If 'strict_types' is 0, then direct object requests will no longer\n+\t * override the object type restrictions.\n+\t */\n+\tint strict_types;\n+\n \t/**\n \t * If non-zero, specifies a maximum blob size. Blobs with a\n \t * size equal to or greater than this limit will not be\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 566db7c7e3..0fd8e61c76 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -643,6 +643,90 @@ test_expect_success 'tree:1 filter is rejected' '\n \ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n '\n \n+test_expect_success 'all, object:type=commit filter' '\n+\ttest-tool path-walk --filter=object:type=commit -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\tblobs:0\n+\tcommits:4\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tag filter' '\n+\ttest-tool path-walk --filter=object:type=tag -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tag:/tags:$(git rev-parse refs/tags/first)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t0:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t0:tag:/tags:$(git rev-parse refs/tags/third)\n+\t0:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t0:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t0:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\tblobs:0\n+\tcommits:0\n+\ttags:7\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=tree filter' '\n+\ttest-tool path-walk --filter=object:type=tree -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:tree::$(git rev-parse topic^{tree})\n+\t0:tree::$(git rev-parse base^{tree})\n+\t0:tree::$(git rev-parse base~1^{tree})\n+\t0:tree::$(git rev-parse base~2^{tree})\n+\t1:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{})\n+\t1:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2^{})\n+\t2:tree:a/:$(git rev-parse base:a)\n+\t3:tree:child/:$(git rev-parse refs/tags/tree-tag:child)\n+\t4:tree:left/:$(git rev-parse base:left)\n+\t4:tree:left/:$(git rev-parse base~2:left)\n+\t5:tree:right/:$(git rev-parse topic:right)\n+\t5:tree:right/:$(git rev-parse base~1:right)\n+\t5:tree:right/:$(git rev-parse base~2:right)\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:13\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, object:type=blob filter' '\n+\ttest-tool path-walk --filter=object:type=blob -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:left/b:$(git rev-parse base:left/b)\n+\t2:blob:left/b:$(git rev-parse base~2:left/b)\n+\t3:blob:right/c:$(git rev-parse base~2:right/c)\n+\t3:blob:right/c:$(git rev-parse topic:right/c)\n+\t4:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:8\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n\n"},{"id":"543950","messageId":"d33d89925119fc9449e29c8031e6962894e8d5e1.1779474277.git.gitgitgadget@gmail.com","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"[PATCH v5 13/13] path-walk: support `combine` filter","fromName":"Taylor Blau via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-22T18:24:37Z","receivedAt":"2026-05-22T18:24:54Z","isPatch":true,"body":"From: Taylor Blau <me@ttaylorr.com>\n\nThe `combine` filter takes the intersection of its children, that is:\nobjects are shown only when all child filters would admit the object.\n\nThe preceding patches added support for many individual filter types.\nEnable users to compose these filters by implementing support for the\n`combine` filter type.\n\nMapping intersection onto path_walk_info works because every supported\nchild filter is a monotonic restriction:\n\n - `blob:none`, `tree:0` unconditionally clear `info->blobs` and (for\n   `tree:0`) `info->trees`; clearing an already-cleared flag is a\n   no-op.\n\n - `object:type=X` is now expressed as an AND of each type flag with the\n   filtered type, so applying multiple such filters only refines the\n   existing set rather than overwrites it.\n\n - `blob:limit=N` has to compose too: the intersection of \"size < L1\"\n   and \"size < L2\" is \"size < min(L1, L2)\".\n\n   Update the `LOFC_BLOB_LIMIT` handler to take the running minimum when\n   `info->blob_limit` is already set, so a combined filter with, e.g.,\n   both \"blob:limit=10\" and \"blob:limit=5\" produces a limit of 5\n   regardless of ordering.\n\n - `sparse:oid` is left unchanged. A `combine` filter that includes a\n   `sparse:oid` is allowed at most once, since the existing handler\n   refuses to overwrite `info->pl`. Two `sparse:oid` filters in a single\n   `combine` would be unusual and are rejected with a warning, matching\n   the standalone `sparse:oid` behavior.\n\nImplementation-wise, the existing `prepare_filters()` called\n`list_objects_filter_release()` inside each case branch. That works fine for\ntop-level filters, but `combine` filters need to recurse over its child\nfilters without releasing each one in turn (since the parent's release\niterates the sub array). Split `prepare_filters()` into a recursive helper\nthat performs only the mutation, plus a thin wrapper that calls the helper\nand then releases the top-level filter once.\n\nThe `LOFC_COMBINE` case in the helper just walks `sub_nr` and recurses;\nchild filters are released by the wrapper's single\n`list_objects_filter_release()` call on the parent (which itself recursively\nreleases each sub-filter, the same way it always has).\n\nIf any sub-filter is unsupported (e.g. \"tree:1\", \"sparse:<path>\", or a\nnot-yet-supported choice), the recursion bubbles a failure up and the\nexisting pack-objects/backfill fallback paths kick in.\n\nAdd coverage in t6601:\n\n  - \"combine:blob:none+tree:0\" collapses to \"tree:0\"\n\n  - \"combine:object:type=blob+blob:limit=3\" yields only the blobs\n    smaller than three bytes\n\n  - \"combine:object:type=blob+object:type=tree\" intersects to empty\n\n  - \"combine:tree:1+blob:none\" reports the \"tree:1\" error.\n\nUpdate Documentation/git-pack-objects.adoc to add combine to the\nlist of supported --filter forms.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-pack-objects.adoc |  3 +-\n path-walk.c                         | 25 ++++++++--\n t/t6601-path-walk.sh                | 71 +++++++++++++++++++++++++++++\n 3 files changed, 93 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex f2852ebd31..8a27aa19fd 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -405,7 +405,8 @@ will be automatically changed to version `1`.\n Incompatible with `--delta-islands`. The `--use-bitmap-index` option is\n ignored in the presence of `--path-walk`. The `--path-walk` option\n supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,\n-`tree:0`, `object:type=<type>`, and `sparse:<oid>`.\n+`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter\n+types can be combined with the `combine:<spec>+<spec>` form.\n \n \n DELTA ISLANDS\ndiff --git a/path-walk.c b/path-walk.c\nindex 418972e753..94ff90bd15 100644\n--- a/path-walk.c\n+++ b/path-walk.c\n@@ -571,8 +571,8 @@ static int setup_pending_objects(struct path_walk_info *info,\n \treturn 0;\n }\n \n-static int prepare_filters(struct path_walk_info *info,\n-\t\t\t   struct list_objects_filter_options *options)\n+static int prepare_filters_one(struct path_walk_info *info,\n+\t\t\t       struct list_objects_filter_options *options)\n {\n \tswitch (options->choice) {\n \tcase LOFC_DISABLED:\n@@ -589,7 +589,8 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tif (!options->blob_limit_value)\n \t\t\t\tinfo->blobs = 0;\n-\t\t\telse\n+\t\t\telse if (!info->blob_limit ||\n+\t\t\t\t info->blob_limit > options->blob_limit_value)\n \t\t\t\tinfo->blob_limit = options->blob_limit_value;\n \t\t\tlist_objects_filter_release(options);\n \t\t}\n@@ -604,7 +605,6 @@ static int prepare_filters(struct path_walk_info *info,\n \t\tif (info) {\n \t\t\tinfo->trees = 0;\n \t\t\tinfo->blobs = 0;\n-\t\t\tlist_objects_filter_release(options);\n \t\t}\n \t\treturn 1;\n \n@@ -656,8 +656,13 @@ static int prepare_filters(struct path_walk_info *info,\n \t\t\t\twarning(_(\"sparse filter is not cone-mode compatible\"));\n \t\t\t\treturn 0;\n \t\t\t}\n+\t\t}\n+\t\treturn 1;\n \n-\t\t\tlist_objects_filter_release(options);\n+\tcase LOFC_COMBINE:\n+\t\tfor (size_t i = 0; i < options->sub_nr; i++) {\n+\t\t\tif (!prepare_filters_one(info, &options->sub[i]))\n+\t\t\t\treturn 0;\n \t\t}\n \t\treturn 1;\n \n@@ -668,6 +673,16 @@ static int prepare_filters(struct path_walk_info *info,\n \t}\n }\n \n+static int prepare_filters(struct path_walk_info *info,\n+\t\t\t   struct list_objects_filter_options *options)\n+{\n+\tif (!prepare_filters_one(info, options))\n+\t\treturn 0;\n+\tif (info)\n+\t\tlist_objects_filter_release(options);\n+\treturn 1;\n+}\n+\n int path_walk_filter_compatible(struct list_objects_filter_options *options)\n {\n \treturn prepare_filters(NULL, options);\ndiff --git a/t/t6601-path-walk.sh b/t/t6601-path-walk.sh\nindex 0fd8e61c76..e9fcd85e75 100755\n--- a/t/t6601-path-walk.sh\n+++ b/t/t6601-path-walk.sh\n@@ -727,6 +727,77 @@ test_expect_success 'all, object:type=blob filter' '\n \ttest_cmp_sorted expect out\n '\n \n+test_expect_success 'all, combine:blob:none+tree:0 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:blob:none+tree:0 -- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:commit::$(git rev-parse topic)\n+\t0:commit::$(git rev-parse base)\n+\t0:commit::$(git rev-parse base~1)\n+\t0:commit::$(git rev-parse base~2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/first)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.1)\n+\t1:tag:/tags:$(git rev-parse refs/tags/second.2)\n+\t1:tag:/tags:$(git rev-parse refs/tags/third)\n+\t1:tag:/tags:$(git rev-parse refs/tags/fourth)\n+\t1:tag:/tags:$(git rev-parse refs/tags/tree-tag)\n+\t1:tag:/tags:$(git rev-parse refs/tags/blob-tag)\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})\n+\t3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)\n+\tblobs:2\n+\tcommits:4\n+\ttags:7\n+\ttrees:2\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine:object:type=blob+blob:limit=3 filter' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+blob:limit=3 \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})\n+\t0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})\n+\t1:blob:a:$(git rev-parse base~2:a)\n+\t2:blob:left/b:$(git rev-parse base~2:left/b)\n+\t3:blob:right/c:$(git rev-parse base~2:right/c)\n+\t4:blob:right/d:$(git rev-parse base~1:right/d)\n+\tblobs:6\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'all, combine of disjoint object:types is empty' '\n+\ttest-tool path-walk \\\n+\t\t--filter=combine:object:type=blob+object:type=tree \\\n+\t\t-- --all >out &&\n+\n+\tcat >expect <<-EOF &&\n+\tblobs:0\n+\tcommits:0\n+\ttags:0\n+\ttrees:0\n+\tEOF\n+\n+\ttest_cmp_sorted expect out\n+'\n+\n+test_expect_success 'combine: rejects unsupported subfilters' '\n+\ttest_must_fail test-tool path-walk \\\n+\t\t--filter=combine:tree:1+blob:none -- --all 2>err &&\n+\ttest_grep \"tree:1 filter not supported by the path-walk API\" err\n+'\n+\n test_expect_success 'setup sparse filter blob' '\n \t# Cone-mode patterns: include root, exclude all dirs, include left/\n \tcat >patterns <<-\\EOF &&\n-- \ngitgitgadget\n"},{"id":"543955","messageId":"ahDbS+CtwsGx62Q3@nand.local","threadId":"65575","inReplyTo":"pull.2101.v5.git.1779474277.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-05-22T22:40:11Z","receivedAt":"2026-05-22T22:40:14Z","isPatch":true,"body":"On Fri, May 22, 2026 at 06:24:24PM +0000, Derrick Stolee via GitGitGadget wrote:\n> Range-diff vs v4:\n>\n>   1:  0840110116 =  1:  0840110116 t5620: make test work with path-walk var\n>   2:  d7c87545f3 =  2:  d7c87545f3 pack-objects: pass --objects with --path-walk\n>   3:  fb8a0f9c43 !  3:  697ef716d2 t/perf: add pack-objects filter and path-walk benchmark\n>      @@ t/perf/p5315-pack-objects-filter.sh (new)\n>       +\t\tawk \"{print \\$4;}\" >top-dirs &&\n>       +\ttop_nr=$(wc -l <top-dirs) &&\n>       +\n>      -+\t>depth2-dirs &&\n>       +\twhile read tdir\n>       +\tdo\n>      -+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n>      -+\tdone <top-dirs >depth2-dirs.raw &&\n>      -+\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n>      ++\t\tgit ls-tree -d --format=\"$tdir/%(path)\" \"HEAD:$tdir\" || return 1\n>      ++\tdone <top-dirs >depth2-dirs &&\n>       +\n>       +\td2_nr=$(wc -l <depth2-dirs) &&\n>       +\n>   4:  e77c8a6bbc =  4:  91845bcef0 path-walk: always emit directly-requested objects\n>   5:  f4904f81e0 =  5:  fdb9361198 path-walk: support blobless filter\n>   6:  f37467e46f =  6:  89726faf7e backfill: die on incompatible filter options\n>   7:  133c1b156c =  7:  3884d4737f path-walk: support blob size limit filter\n>   8:  0f517be8e3 =  8:  31b4ef0fa1 path-walk: add pl_sparse_trees to control tree pruning\n>   9:  b4dc09ab69 =  9:  7d8f0aa036 pack-objects: support sparse:oid filter with path-walk\n>  10:  0b1eed0790 = 10:  a68676d0de t6601: tag otherwise-unreachable trees\n>  11:  b23244c4c2 = 11:  b0db73c6cc path-walk: support `tree:0` filter\n>  12:  7e1e503361 = 12:  6845988f50 path-walk: support `object:type` filter\n>  13:  a615b1a707 = 13:  d33d899251 path-walk: support `combine` filter\n\nThe range-diff looks good to me. Thanks!\n\nThanks,\nTaylor\n"},{"id":"544017","messageId":"xmqqldd8gypa.fsf@gitster.g","threadId":"65575","inReplyTo":"ahDbS+CtwsGx62Q3@nand.local","subject":"Re: [PATCH v5 00/13] pack-objects: integrate --path-walk and some --filter options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-24T23:44:17Z","receivedAt":"2026-05-24T23:44:20Z","isPatch":true,"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Fri, May 22, 2026 at 06:24:24PM +0000, Derrick Stolee via GitGitGadget wrote:\n>> Range-diff vs v4:\n>>\n>>   1:  0840110116 =  1:  0840110116 t5620: make test work with path-walk var\n>>   2:  d7c87545f3 =  2:  d7c87545f3 pack-objects: pass --objects with --path-walk\n>>   3:  fb8a0f9c43 !  3:  697ef716d2 t/perf: add pack-objects filter and path-walk benchmark\n>>      @@ t/perf/p5315-pack-objects-filter.sh (new)\n>>       +\t\tawk \"{print \\$4;}\" >top-dirs &&\n>>       +\ttop_nr=$(wc -l <top-dirs) &&\n>>       +\n>>      -+\t>depth2-dirs &&\n>>       +\twhile read tdir\n>>       +\tdo\n>>      -+\t\tgit ls-tree -d --name-only \"HEAD:$tdir\" 2>/dev/null || return 1\n>>      -+\tdone <top-dirs >depth2-dirs.raw &&\n>>      -+\tsed \"s|^|$tdir/|\" <depth2-dirs.raw >depth2-dirs &&\n>>      ++\t\tgit ls-tree -d --format=\"$tdir/%(path)\" \"HEAD:$tdir\" || return 1\n>>      ++\tdone <top-dirs >depth2-dirs &&\n>>       +\n>>       +\td2_nr=$(wc -l <depth2-dirs) &&\n>>       +\n>>   4:  e77c8a6bbc =  4:  91845bcef0 path-walk: always emit directly-requested objects\n>>   5:  f4904f81e0 =  5:  fdb9361198 path-walk: support blobless filter\n>>   6:  f37467e46f =  6:  89726faf7e backfill: die on incompatible filter options\n>>   7:  133c1b156c =  7:  3884d4737f path-walk: support blob size limit filter\n>>   8:  0f517be8e3 =  8:  31b4ef0fa1 path-walk: add pl_sparse_trees to control tree pruning\n>>   9:  b4dc09ab69 =  9:  7d8f0aa036 pack-objects: support sparse:oid filter with path-walk\n>>  10:  0b1eed0790 = 10:  a68676d0de t6601: tag otherwise-unreachable trees\n>>  11:  b23244c4c2 = 11:  b0db73c6cc path-walk: support `tree:0` filter\n>>  12:  7e1e503361 = 12:  6845988f50 path-walk: support `object:type` filter\n>>  13:  a615b1a707 = 13:  d33d899251 path-walk: support `combine` filter\n>\n> The range-diff looks good to me. Thanks!\n\nGood.  And as you in <ahDbS+CtwsGx62Q3@nand.local> were already\nhappy with everything else in the previous iteration of this series,\nnot just the changes the range-diff output shows, but also the\nnon-changes from the previous iteration, look good to you ;-).\n\nLet's mark the topic for 'next'.  Thanks, both.\n"}]}