]> git.ipfire.org Git - thirdparty/git.git/commitdiff
path-walk: support `combine` filter
authorTaylor Blau <me@ttaylorr.com>
Fri, 22 May 2026 18:24:37 +0000 (18:24 +0000)
committerJunio C Hamano <gitster@pobox.com>
Sun, 24 May 2026 09:41:07 +0000 (18:41 +0900)
The `combine` filter takes the intersection of its children, that is:
objects are shown only when all child filters would admit the object.

The preceding patches added support for many individual filter types.
Enable users to compose these filters by implementing support for the
`combine` filter type.

Mapping intersection onto path_walk_info works because every supported
child filter is a monotonic restriction:

 - `blob:none`, `tree:0` unconditionally clear `info->blobs` and (for
   `tree:0`) `info->trees`; clearing an already-cleared flag is a
   no-op.

 - `object:type=X` is now expressed as an AND of each type flag with the
   filtered type, so applying multiple such filters only refines the
   existing set rather than overwrites it.

 - `blob:limit=N` has to compose too: the intersection of "size < L1"
   and "size < L2" is "size < min(L1, L2)".

   Update the `LOFC_BLOB_LIMIT` handler to take the running minimum when
   `info->blob_limit` is already set, so a combined filter with, e.g.,
   both "blob:limit=10" and "blob:limit=5" produces a limit of 5
   regardless of ordering.

 - `sparse:oid` is left unchanged. A `combine` filter that includes a
   `sparse:oid` is allowed at most once, since the existing handler
   refuses to overwrite `info->pl`. Two `sparse:oid` filters in a single
   `combine` would be unusual and are rejected with a warning, matching
   the standalone `sparse:oid` behavior.

Implementation-wise, the existing `prepare_filters()` called
`list_objects_filter_release()` inside each case branch. That works fine for
top-level filters, but `combine` filters need to recurse over its child
filters without releasing each one in turn (since the parent's release
iterates the sub array). Split `prepare_filters()` into a recursive helper
that performs only the mutation, plus a thin wrapper that calls the helper
and then releases the top-level filter once.

The `LOFC_COMBINE` case in the helper just walks `sub_nr` and recurses;
child filters are released by the wrapper's single
`list_objects_filter_release()` call on the parent (which itself recursively
releases each sub-filter, the same way it always has).

If any sub-filter is unsupported (e.g. "tree:1", "sparse:<path>", or a
not-yet-supported choice), the recursion bubbles a failure up and the
existing pack-objects/backfill fallback paths kick in.

Add coverage in t6601:

  - "combine:blob:none+tree:0" collapses to "tree:0"

  - "combine:object:type=blob+blob:limit=3" yields only the blobs
    smaller than three bytes

  - "combine:object:type=blob+object:type=tree" intersects to empty

  - "combine:tree:1+blob:none" reports the "tree:1" error.

Update Documentation/git-pack-objects.adoc to add combine to the
list of supported --filter forms.

Signed-off-by: Taylor Blau <me@ttaylorr.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-pack-objects.adoc
path-walk.c
t/t6601-path-walk.sh

index f2852ebd3172dc7dca128f64e9f9aa9dad2bcd63..8a27aa19fd3f1f74b7e51a2fec8ec6b4d8716e0f 100644 (file)
@@ -405,7 +405,8 @@ will be automatically changed to version `1`.
 Incompatible with `--delta-islands`. The `--use-bitmap-index` option is
 ignored in the presence of `--path-walk`. The `--path-walk` option
 supports the `--filter=<spec>` forms `blob:none`, `blob:limit=<n>`,
-`tree:0`, `object:type=<type>`, and `sparse:<oid>`.
+`tree:0`, `object:type=<type>`, and `sparse:<oid>`. These supported filter
+types can be combined with the `combine:<spec>+<spec>` form.
 
 
 DELTA ISLANDS
index 418972e753840d81258377fe5d72bc2b16b75fc2..94ff90bd1566b629b4660f5c3e5cdacf2a7ab24e 100644 (file)
@@ -571,8 +571,8 @@ static int setup_pending_objects(struct path_walk_info *info,
        return 0;
 }
 
-static int prepare_filters(struct path_walk_info *info,
-                          struct list_objects_filter_options *options)
+static int prepare_filters_one(struct path_walk_info *info,
+                              struct list_objects_filter_options *options)
 {
        switch (options->choice) {
        case LOFC_DISABLED:
@@ -589,7 +589,8 @@ static int prepare_filters(struct path_walk_info *info,
                if (info) {
                        if (!options->blob_limit_value)
                                info->blobs = 0;
-                       else
+                       else if (!info->blob_limit ||
+                                info->blob_limit > options->blob_limit_value)
                                info->blob_limit = options->blob_limit_value;
                        list_objects_filter_release(options);
                }
@@ -604,7 +605,6 @@ static int prepare_filters(struct path_walk_info *info,
                if (info) {
                        info->trees = 0;
                        info->blobs = 0;
-                       list_objects_filter_release(options);
                }
                return 1;
 
@@ -656,8 +656,13 @@ static int prepare_filters(struct path_walk_info *info,
                                warning(_("sparse filter is not cone-mode compatible"));
                                return 0;
                        }
+               }
+               return 1;
 
-                       list_objects_filter_release(options);
+       case LOFC_COMBINE:
+               for (size_t i = 0; i < options->sub_nr; i++) {
+                       if (!prepare_filters_one(info, &options->sub[i]))
+                               return 0;
                }
                return 1;
 
@@ -668,6 +673,16 @@ static int prepare_filters(struct path_walk_info *info,
        }
 }
 
+static int prepare_filters(struct path_walk_info *info,
+                          struct list_objects_filter_options *options)
+{
+       if (!prepare_filters_one(info, options))
+               return 0;
+       if (info)
+               list_objects_filter_release(options);
+       return 1;
+}
+
 int path_walk_filter_compatible(struct list_objects_filter_options *options)
 {
        return prepare_filters(NULL, options);
index 0fd8e61c76f458655dd0877b41e54fa5d025b5d3..e9fcd85e7520bf51a723ab40af3e0b3d7cac59a6 100755 (executable)
@@ -727,6 +727,77 @@ test_expect_success 'all, object:type=blob filter' '
        test_cmp_sorted expect out
 '
 
+test_expect_success 'all, combine:blob:none+tree:0 filter' '
+       test-tool path-walk \
+               --filter=combine:blob:none+tree:0 -- --all >out &&
+
+       cat >expect <<-EOF &&
+       0:commit::$(git rev-parse topic)
+       0:commit::$(git rev-parse base)
+       0:commit::$(git rev-parse base~1)
+       0:commit::$(git rev-parse base~2)
+       1:tag:/tags:$(git rev-parse refs/tags/first)
+       1:tag:/tags:$(git rev-parse refs/tags/second.1)
+       1:tag:/tags:$(git rev-parse refs/tags/second.2)
+       1:tag:/tags:$(git rev-parse refs/tags/third)
+       1:tag:/tags:$(git rev-parse refs/tags/fourth)
+       1:tag:/tags:$(git rev-parse refs/tags/tree-tag)
+       1:tag:/tags:$(git rev-parse refs/tags/blob-tag)
+       2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})
+       2:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})
+       3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag^{tree})
+       3:tree:/tagged-trees:$(git rev-parse refs/tags/tree-tag2)
+       blobs:2
+       commits:4
+       tags:7
+       trees:2
+       EOF
+
+       test_cmp_sorted expect out
+'
+
+test_expect_success 'all, combine:object:type=blob+blob:limit=3 filter' '
+       test-tool path-walk \
+               --filter=combine:object:type=blob+blob:limit=3 \
+               -- --all >out &&
+
+       cat >expect <<-EOF &&
+       0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})
+       0:blob:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})
+       1:blob:a:$(git rev-parse base~2:a)
+       2:blob:left/b:$(git rev-parse base~2:left/b)
+       3:blob:right/c:$(git rev-parse base~2:right/c)
+       4:blob:right/d:$(git rev-parse base~1:right/d)
+       blobs:6
+       commits:0
+       tags:0
+       trees:0
+       EOF
+
+       test_cmp_sorted expect out
+'
+
+test_expect_success 'all, combine of disjoint object:types is empty' '
+       test-tool path-walk \
+               --filter=combine:object:type=blob+object:type=tree \
+               -- --all >out &&
+
+       cat >expect <<-EOF &&
+       blobs:0
+       commits:0
+       tags:0
+       trees:0
+       EOF
+
+       test_cmp_sorted expect out
+'
+
+test_expect_success 'combine: rejects unsupported subfilters' '
+       test_must_fail test-tool path-walk \
+               --filter=combine:tree:1+blob:none -- --all 2>err &&
+       test_grep "tree:1 filter not supported by the path-walk API" err
+'
+
 test_expect_success 'setup sparse filter blob' '
        # Cone-mode patterns: include root, exclude all dirs, include left/
        cat >patterns <<-\EOF &&