Subject: [PATCH 7/9] list-objects-filter-options: implement auto filter resolution

From: Christian Couder <[email-masked]>
Date: Tue, 23 Dec 2025 12:11:11 +0100

In a following commit, we will need to aggregate filters from multiple
accepted promisor remotes into a single filter.

For that purpose, let's add a `list_objects_filter_combine()` helper
function that takes a list of filter specifications and combines them
into a single string. If multiple filters are provided, it constructs a
"combine:..." filter, ensuring that sub-filters are properly
URL-encoded using the existing `allow_unencoded` logic.

In a following commit, we will add a `--filter=auto` option that will
enable a client to use the filters suggested by the server for the
promisor remotes the client accepted.

To simplify the filter processing related to this new feature, let's
also add a small `list_objects_filter_resolve_auto()` function.

Signed-off-by: Christian Couder <[email-masked]>
---
 list-objects-filter-options.c                | 35 ++++++++++++++++++++
 list-objects-filter-options.h                | 19 +++++++++++
 t/unit-tests/u-list-objects-filter-options.c | 33 ++++++++++++++++++
 3 files changed, 87 insertions(+)

diff --git a/list-objects-filter-options.c b/list-objects-filter-options.c
index f13ae5caeb..4a9c1991c1 100644
--- a/list-objects-filter-options.c
+++ b/list-objects-filter-options.c
@@ -230,6 +230,41 @@ static void filter_spec_append_urlencode(
 		     filter->filter_spec.buf + orig_len);
 }
 
+char *list_objects_filter_combine(const struct string_list *specs)
+{
+	struct strbuf buf = STRBUF_INIT;
+
+	if (!specs->nr)
+		return NULL;
+
+	if (specs->nr == 1)
+		return xstrdup(specs->items[0].string);
+
+	strbuf_addstr(&buf, "combine:");
+
+	for (size_t i = 0; i < specs->nr; i++) {
+		const char *spec = specs->items[i].string;
+		if (i > 0)
+			strbuf_addch(&buf, '+');
+
+		strbuf_addstr_urlencode(&buf, spec, allow_unencoded);
+	}
+
+	return strbuf_detach(&buf, NULL);
+}
+
+void list_objects_filter_resolve_auto(struct list_objects_filter_options *filter_options,
+	char *new_filter, struct strbuf *errbuf)
+{
+	if (filter_options->choice != LOFC_AUTO)
+		return;
+
+	list_objects_filter_release(filter_options);
+
+	if (new_filter)
+		gently_parse_list_objects_filter(filter_options, new_filter, errbuf);
+}
+
 /*
  * Changes filter_options into an equivalent LOFC_COMBINE filter options
  * instance. Does not do anything if filter_options is already LOFC_COMBINE.
diff --git a/list-objects-filter-options.h b/list-objects-filter-options.h
index 77d7bbc846..832d615c17 100644
--- a/list-objects-filter-options.h
+++ b/list-objects-filter-options.h
@@ -6,6 +6,7 @@
 #include "strbuf.h"
 
 struct option;
+struct string_list;
 
 /*
  * The list of defined filters for list-objects.
@@ -168,4 +169,22 @@ void list_objects_filter_copy(
 	struct list_objects_filter_options *dest,
 	const struct list_objects_filter_options *src);
 
+/*
+ * Combine the filter specs in 'specs' into a combined filter string
+ * like "combine:<spec1>+<spec2>", where <spec1>, <spec2>, etc are
+ * properly urlencoded. If 'specs' contains no element, NULL is
+ * returned. If 'specs' contains a single element, a copy of that
+ * element is returned.
+ */
+char *list_objects_filter_combine(const struct string_list *specs);
+
+/*
+ * Check if 'filter_options' are an 'auto' filter, and if that's the
+ * case populate it with the filter specified by 'new_filter'.
+ */
+void list_objects_filter_resolve_auto(
+	struct list_objects_filter_options *filter_options,
+	char *new_filter,
+	struct strbuf *errbuf);
+
 #endif /* LIST_OBJECTS_FILTER_OPTIONS_H */
diff --git a/t/unit-tests/u-list-objects-filter-options.c b/t/unit-tests/u-list-objects-filter-options.c
index f7d73701b5..84a012af3c 100644
--- a/t/unit-tests/u-list-objects-filter-options.c
+++ b/t/unit-tests/u-list-objects-filter-options.c
@@ -1,6 +1,7 @@
 #include "unit-test.h"
 #include "list-objects-filter-options.h"
 #include "strbuf.h"
+#include "string-list.h"
 
 /* Helper to test gently_parse_list_objects_filter() */
 static void check_gentle_parse(const char *filter_spec,
@@ -51,3 +52,35 @@ void test_list_objects_filter_options__combine_auto_fails(void)
 	check_gentle_parse("combine:blob:none+auto", 0, 1, 0);
 	check_gentle_parse("combine:auto+auto", 0, 1, 0);
 }
+
+/* Helper to test list_objects_filter_combine() */
+static void check_combine(const char **specs, size_t nr, const char *expected)
+{
+	struct string_list spec_list = STRING_LIST_INIT_NODUP;
+	char *actual;
+
+	for (size_t i = 0; i < nr; i++)
+		string_list_append(&spec_list, specs[i]);
+
+	actual = list_objects_filter_combine(&spec_list);
+
+	cl_assert_equal_s(actual, expected);
+
+	free(actual);
+	string_list_clear(&spec_list, 0);
+}
+
+void test_list_objects_filter_options__combine_helper(void)
+{
+	const char *empty[] = { NULL };
+	const char *one[] = { "blob:none" };
+	const char *two[] = { "blob:none", "tree:0" };
+	const char *complex[] = { "blob:limit=1k", "object:type=tag" };
+	const char *needs_encoding[] = { "blob:none", "combine:tree:0+blob:limit=1k" };
+
+	check_combine(empty, 0, NULL);
+	check_combine(one, 1, "blob:none");
+	check_combine(two, 2, "combine:blob:none+tree:0");
+	check_combine(complex, 2, "combine:blob:limit=1k+object:type=tag");
+	check_combine(needs_encoding, 2, "combine:blob:none+combine:tree:0%2bblob:limit=1k");
+}
-- 
2.52.0.319.gfcaffa7898