list-objects: implement object type filter

While it already is possible to filter objects by some criteria in git-rev-list(1), it is not yet possible to filter out only a specific type of objects. This makes some filters less useful. The `blob:limit` filter for example filters blobs such that only those which are smaller than the given limit are returned. But it is unfit to ask only for these smallish blobs, given that git-rev-list(1) will continue to print tags, commits and trees. Now that we have the infrastructure in place to also filter tags and commits, we can improve this situation by implementing a new filter which selects objects based on their type. Above query can thus trivially be implemented with the following command: $ git rev-list --objects --filter=object:type=blob \ --filter=blob:limit=200 Furthermore, this filter allows to optimize for certain other cases: if for example only tags or commits have been selected, there is no need to walk down trees. The new filter is not yet supported in bitmaps. This is going to be implemented in a subsequent commit. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Apr 19, 2021 at 13:46 UTC b0c42a53c9d36ea69f4d2650001f05e98eb347cb
6 files changed +144 -3
Documentation/config/uploadpack.txt
+3 -3
@@ -66,9 +66,9 @@ uploadpackfilter.allow::
66 uploadpackfilter.<filter>.allow::
67 Explicitly allow or ban the object filter corresponding to
68 `<filter>`, where `<filter>` may be one of: `blob:none`,
69 - `blob:limit`, `tree`, `sparse:oid`, or `combine`. If using
70 - combined filters, both `combine` and all of the nested filter
71 - kinds must be allowed. Defaults to `uploadpackfilter.allow`.
69 + `blob:limit`, `object:type`, `tree`, `sparse:oid`, or `combine`.
70 + If using combined filters, both `combine` and all of the nested
71 + filter kinds must be allowed. Defaults to `uploadpackfilter.allow`.
72
73 uploadpackfilter.tree.maxDepth::
74 Only allow `--filter=tree:<n>` when `<n>` is no more than the value of
Documentation/rev-list-options.txt
+3
@@ -892,6 +892,9 @@ or units. n may be zero. The suffixes k, m, and g can be used to name
892 units in KiB, MiB, or GiB. For example, 'blob:limit=1k' is the same
893 as 'blob:limit=1024'.
894 +
895 +The form '--filter=object:type=(tag|commit|tree|blob)' omits all objects
896 +which are not of the requested type.
897 ++
898 The form '--filter=sparse:oid=<blob-ish>' uses a sparse-checkout
899 specification contained in the blob (or blob-expression) '<blob-ish>'
900 to omit blobs that would not be not required for a sparse checkout on
list-objects-filter-options.c
+15
@@ -29,6 +29,8 @@ const char *list_object_filter_config_name(enum list_objects_filter_choice c)
29 return "tree";
30 case LOFC_SPARSE_OID:
31 return "sparse:oid";
32 + case LOFC_OBJECT_TYPE:
33 + return "object:type";
34 case LOFC_COMBINE:
35 return "combine";
36 case LOFC__COUNT:
@@ -97,6 +99,19 @@ static int gently_parse_list_objects_filter(
99 }
100 return 1;
101
102 + } else if (skip_prefix(arg, "object:type=", &v0)) {
103 + int type = type_from_string_gently(v0, strlen(v0), 1);
104 + if (type < 0) {
105 + strbuf_addf(errbuf, _("'%s' for 'object:type=<type>' is"
106 + "not a valid object type"), v0);
107 + return 1;
108 + }
109 +
110 + filter_options->object_type = type;
111 + filter_options->choice = LOFC_OBJECT_TYPE;
112 +
113 + return 0;
114 +
115 } else if (skip_prefix(arg, "combine:", &v0)) {
116 return parse_combine_filter(filter_options, v0, errbuf);
117
list-objects-filter-options.h
+3
@@ -1,6 +1,7 @@
1 #ifndef LIST_OBJECTS_FILTER_OPTIONS_H
2 #define LIST_OBJECTS_FILTER_OPTIONS_H
3
4 +#include "cache.h"
5 #include "parse-options.h"
6 #include "string-list.h"
7
@@ -13,6 +14,7 @@ enum list_objects_filter_choice {
14 LOFC_BLOB_LIMIT,
15 LOFC_TREE_DEPTH,
16 LOFC_SPARSE_OID,
17 + LOFC_OBJECT_TYPE,
18 LOFC_COMBINE,
19 LOFC__COUNT /* must be last */
20 };
@@ -54,6 +56,7 @@ struct list_objects_filter_options {
56 char *sparse_oid_name;
57 unsigned long blob_limit_value;
58 unsigned long tree_exclude_depth;
59 + enum object_type object_type;
60
61 /* LOFC_COMBINE values */
62
list-objects-filter.c
+76
@@ -545,6 +545,81 @@ static void filter_sparse_oid__init(
545 filter->free_fn = filter_sparse_free;
546 }
547
548 +/*
549 + * A filter for list-objects to omit large blobs.
550 + * And to OPTIONALLY collect a list of the omitted OIDs.
551 + */
552 +struct filter_object_type_data {
553 + enum object_type object_type;
554 +};
555 +
556 +static enum list_objects_filter_result filter_object_type(
557 + struct repository *r,
558 + enum list_objects_filter_situation filter_situation,
559 + struct object *obj,
560 + const char *pathname,
561 + const char *filename,
562 + struct oidset *omits,
563 + void *filter_data_)
564 +{
565 + struct filter_object_type_data *filter_data = filter_data_;
566 +
567 + switch (filter_situation) {
568 + default:
569 + BUG("unknown filter_situation: %d", filter_situation);
570 +
571 + case LOFS_TAG:
572 + assert(obj->type == OBJ_TAG);
573 + if (filter_data->object_type == OBJ_TAG)
574 + return LOFR_MARK_SEEN | LOFR_DO_SHOW;
575 + return LOFR_MARK_SEEN;
576 +
577 + case LOFS_COMMIT:
578 + assert(obj->type == OBJ_COMMIT);
579 + if (filter_data->object_type == OBJ_COMMIT)
580 + return LOFR_MARK_SEEN | LOFR_DO_SHOW;
581 + return LOFR_MARK_SEEN;
582 +
583 + case LOFS_BEGIN_TREE:
584 + assert(obj->type == OBJ_TREE);
585 +
586 + /*
587 + * If we only want to show commits or tags, then there is no
588 + * need to walk down trees.
589 + */
590 + if (filter_data->object_type == OBJ_COMMIT ||
591 + filter_data->object_type == OBJ_TAG)
592 + return LOFR_SKIP_TREE;
593 +
594 + if (filter_data->object_type == OBJ_TREE)
595 + return LOFR_MARK_SEEN | LOFR_DO_SHOW;
596 +
597 + return LOFR_MARK_SEEN;
598 +
599 + case LOFS_BLOB:
600 + assert(obj->type == OBJ_BLOB);
601 +
602 + if (filter_data->object_type == OBJ_BLOB)
603 + return LOFR_MARK_SEEN | LOFR_DO_SHOW;
604 + return LOFR_MARK_SEEN;
605 +
606 + case LOFS_END_TREE:
607 + return LOFR_ZERO;
608 + }
609 +}
610 +
611 +static void filter_object_type__init(
612 + struct list_objects_filter_options *filter_options,
613 + struct filter *filter)
614 +{
615 + struct filter_object_type_data *d = xcalloc(1, sizeof(*d));
616 + d->object_type = filter_options->object_type;
617 +
618 + filter->filter_data = d;
619 + filter->filter_object_fn = filter_object_type;
620 + filter->free_fn = free;
621 +}
622 +
623 /* A filter which only shows objects shown by all sub-filters. */
624 struct combine_filter_data {
625 struct subfilter *sub;
@@ -691,6 +766,7 @@ static filter_init_fn s_filters[] = {
766 filter_blobs_limit__init,
767 filter_trees_depth__init,
768 filter_sparse_oid__init,
769 + filter_object_type__init,
770 filter_combine__init,
771 };
772
t/t6112-rev-list-filters-objects.sh
+44
@@ -159,6 +159,50 @@ test_expect_success 'verify blob:limit=1m' '
159 test_must_be_empty observed
160 '
161
162 +# Test object:type=<type> filter.
163 +
164 +test_expect_success 'setup object-type' '
165 + test_create_repo object-type &&
166 + test_commit --no-tag -C object-type message blob &&
167 + git -C object-type tag tag -m tag-message
168 +'
169 +
170 +test_expect_success 'verify object:type= fails with invalid type' '
171 + test_must_fail git -C object-type rev-list --objects --filter=object:type= HEAD &&
172 + test_must_fail git -C object-type rev-list --objects --filter=object:type=invalid HEAD
173 +'
174 +
175 +test_expect_success 'verify object:type=blob prints blob and commit' '
176 + git -C object-type rev-parse HEAD >expected &&
177 + printf "%s blob\n" $(git -C object-type rev-parse HEAD:blob) >>expected &&
178 + git -C object-type rev-list --objects --filter=object:type=blob HEAD >actual &&
179 + test_cmp expected actual
180 +'
181 +
182 +test_expect_success 'verify object:type=tree prints tree and commit' '
183 + (
184 + git -C object-type rev-parse HEAD &&
185 + printf "%s \n" $(git -C object-type rev-parse HEAD^{tree})
186 + ) >expected &&
187 + git -C object-type rev-list --objects --filter=object:type=tree HEAD >actual &&
188 + test_cmp expected actual
189 +'
190 +
191 +test_expect_success 'verify object:type=commit prints commit' '
192 + git -C object-type rev-parse HEAD >expected &&
193 + git -C object-type rev-list --objects --filter=object:type=commit HEAD >actual &&
194 + test_cmp expected actual
195 +'
196 +
197 +test_expect_success 'verify object:type=tag prints tag' '
198 + (
199 + git -C object-type rev-parse HEAD &&
200 + printf "%s tag\n" $(git -C object-type rev-parse tag)
201 + ) >expected &&
202 + git -C object-type rev-list --objects --filter=object:type=tag tag >actual &&
203 + test_cmp expected actual
204 +'
205 +
206 # Test sparse:path=<path> filter.
207 # !!!!
208 # NOTE: sparse:path filter support has been dropped for security reasons,