Subject: [PATCH v3] clone: add clone.<url>.defaultObjectFilter config

From: "Alan Braithwaite via GitGitGadget" <[email-masked]>
Date: Fri, 06 Mar 2026 06:55:13 +0000

From: Alan Braithwaite <[email-masked]>

Add a new configuration option that lets users specify a default
partial clone filter per URL pattern.  When cloning a repository
whose URL matches a configured pattern, git-clone automatically
applies the filter, equivalent to passing --filter on the command
line.

    [clone "https://github.com/"]
        defaultObjectFilter = blob:limit=5m

    [clone "https://internal.corp.com/large-project/"]
        defaultObjectFilter = blob:none

URL matching uses the existing urlmatch_config_entry() infrastructure,
following the same rules as http.<url>.* — you can match a domain,
a namespace path, or a specific project, and the most specific match
wins.

The config only affects the initial clone.  Once the clone completes,
the filter is recorded in remote.<name>.partialCloneFilter, so
subsequent fetches inherit it automatically.  An explicit --filter
flag on the command line takes precedence.

Only the URL-qualified form (clone.<url>.defaultObjectFilter) is
honored; a bare clone.defaultObjectFilter without a URL subsection
is ignored.

Signed-off-by: Alan Braithwaite <[email-masked]>
---
    fetch, clone: add fetch.blobSizeLimit config

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2058

Range-diff vs v2:

 1:  4a73edd2e8 ! 1:  5408412f2a clone: add clone.<url>.defaultObjectFilter config
     @@ Documentation/config/clone.adoc: endif::[]
       	linkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply
       	the filter to submodules.
      +
     ++`clone.defaultObjectFilter`::
      +`clone.<url>.defaultObjectFilter`::
      +	When set to a filter spec string (e.g., `blob:limit=1m`,
      +	`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically
     -+	use `--filter=<value>` when the clone URL matches `<url>`.
     ++	use `--filter=<value>` to enable partial clone behavior.
      +	Objects matching the filter are excluded from the initial
      +	transfer and lazily fetched on demand (e.g., during checkout).
      +	Subsequent fetches inherit the filter via the per-remote config
      +	that is written during the clone.
      ++
     -+The URL matching follows the same rules as `http.<url>.*` (see
     -+linkgit:git-config[1]).  The most specific URL match wins.  You can
     -+match a complete domain, a namespace, or a specific project:
     ++The bare `clone.defaultObjectFilter` applies to all clones.  The
     ++URL-qualified form `clone.<url>.defaultObjectFilter` restricts the
     ++setting to clones whose URL matches `<url>`, following the same
     ++rules as `http.<url>.*` (see linkgit:git-config[1]).  The most
     ++specific URL match wins.  You can match a domain, a namespace, or a
     ++specific project:
      ++
      +----
     ++[clone]
     ++    defaultObjectFilter = blob:limit=1m
     ++
      +[clone "https://github.com/"]
      +    defaultObjectFilter = blob:limit=5m
      +
     @@ builtin/clone.c: static int git_clone_config(const char *k, const char *v,
       	return git_default_config(k, v, ctx, cb);
       }
       
     -+struct clone_filter_data {
     -+	char *default_object_filter;
     -+};
     -+
      +static int clone_filter_collect(const char *var, const char *value,
      +				const struct config_context *ctx UNUSED,
      +				void *cb)
      +{
     -+	struct clone_filter_data *data = cb;
     ++	char **filter_spec_p = cb;
      +
      +	if (!strcmp(var, "clone.defaultobjectfilter")) {
     -+		free(data->default_object_filter);
     -+		data->default_object_filter = xstrdup(value);
     ++		if (!value)
     ++			return config_error_nonbool(var);
     ++		free(*filter_spec_p);
     ++		*filter_spec_p = xstrdup(value);
      +	}
      +	return 0;
      +}
      +
      +/*
     -+ * Look up clone.<url>.defaultObjectFilter using the urlmatch
     -+ * infrastructure.  Only URL-qualified forms are supported; a bare
     -+ * clone.defaultObjectFilter (without a URL) is ignored.
     ++ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter
     ++ * using the urlmatch infrastructure.  A URL-qualified entry that matches
     ++ * the clone URL takes precedence over the bare form, following the same
     ++ * rules as http.<url>.* configuration variables.
      + */
      +static char *get_default_object_filter(const char *url)
      +{
      +	struct urlmatch_config config = URLMATCH_CONFIG_INIT;
     -+	struct clone_filter_data data = { 0 };
     -+	struct string_list_item *item;
     ++	char *filter_spec = NULL;
      +	char *normalized_url;
      +
      +	config.section = "clone";
      +	config.key = "defaultobjectfilter";
      +	config.collect_fn = clone_filter_collect;
     -+	config.cascade_fn = git_clone_config;
     -+	config.cb = &data;
     ++	config.cb = &filter_spec;
      +
      +	normalized_url = url_normalize(url, &config.url);
      +
      +	repo_config(the_repository, urlmatch_config_entry, &config);
      +	free(normalized_url);
     -+
     -+	/*
     -+	 * Reject the bare form clone.defaultObjectFilter (no URL
     -+	 * subsection).  urlmatch stores the best match in vars with
     -+	 * hostmatch_len == 0 for non-URL-qualified entries; discard
     -+	 * the result if that is what we got.
     -+	 */
     -+	item = string_list_lookup(&config.vars, "defaultobjectfilter");
     -+	if (item) {
     -+		const struct urlmatch_item *m = item->util;
     -+		if (!m->hostmatch_len && !m->pathmatch_len) {
     -+			FREE_AND_NULL(data.default_object_filter);
     -+		}
     -+	}
     -+
      +	urlmatch_config_release(&config);
      +
     -+	return data.default_object_filter;
     ++	return filter_spec;
      +}
      +
       static int write_one_config(const char *key, const char *value,
     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
      +	test_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor
      +'
      +
     -+test_expect_success 'bare clone.defaultObjectFilter without URL is ignored' '
     ++test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '
      +	git -c clone.defaultObjectFilter=blob:none \
      +		clone "file://$(pwd)/default-filter-srv.bare" default-filter-bare-key &&
      +
     -+	test_must_fail git -C default-filter-bare-key config --local remote.origin.promisor
     ++	test "$(git -C default-filter-bare-key config --local remote.origin.promisor)" = "true" &&
     ++	test "$(git -C default-filter-bare-key config --local remote.origin.partialclonefilter)" = "blob:none"
     ++'
     ++
     ++test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '
     ++	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
     ++	git \
     ++		-c clone.defaultObjectFilter=blob:limit=1k \
     ++		-c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
     ++		clone "$SERVER_URL" default-filter-url-over-bare &&
     ++
     ++	test "$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)" = "blob:none"
      +'
       
       . "$TEST_DIRECTORY"/lib-httpd.sh

 Documentation/config/clone.adoc | 33 +++++++++++++
 builtin/clone.c                 | 50 ++++++++++++++++++++
 t/t5616-partial-clone.sh        | 84 +++++++++++++++++++++++++++++++++
 3 files changed, 167 insertions(+)

diff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc
index 0a10efd174..7ef6321be2 100644
--- a/Documentation/config/clone.adoc
+++ b/Documentation/config/clone.adoc
@@ -21,3 +21,36 @@ endif::[]
 	If a partial clone filter is provided (see `--filter` in
 	linkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply
 	the filter to submodules.
+
+`clone.defaultObjectFilter`::
+`clone.<url>.defaultObjectFilter`::
+	When set to a filter spec string (e.g., `blob:limit=1m`,
+	`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically
+	use `--filter=<value>` to enable partial clone behavior.
+	Objects matching the filter are excluded from the initial
+	transfer and lazily fetched on demand (e.g., during checkout).
+	Subsequent fetches inherit the filter via the per-remote config
+	that is written during the clone.
++
+The bare `clone.defaultObjectFilter` applies to all clones.  The
+URL-qualified form `clone.<url>.defaultObjectFilter` restricts the
+setting to clones whose URL matches `<url>`, following the same
+rules as `http.<url>.*` (see linkgit:git-config[1]).  The most
+specific URL match wins.  You can match a domain, a namespace, or a
+specific project:
++
+----
+[clone]
+    defaultObjectFilter = blob:limit=1m
+
+[clone "https://github.com/"]
+    defaultObjectFilter = blob:limit=5m
+
+[clone "https://internal.corp.com/large-project/"]
+    defaultObjectFilter = blob:none
+----
++
+An explicit `--filter` option on the command line takes precedence
+over this config.  Only affects the initial clone; it has no effect
+on later fetches into an existing repository.  If the server does
+not support object filtering, the setting is silently ignored.
diff --git a/builtin/clone.c b/builtin/clone.c
index 45d8fa0eed..b549191707 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -44,6 +44,7 @@
 #include "path.h"
 #include "pkt-line.h"
 #include "list-objects-filter-options.h"
+#include "urlmatch.h"
 #include "hook.h"
 #include "bundle.h"
 #include "bundle-uri.h"
@@ -757,6 +758,47 @@ static int git_clone_config(const char *k, const char *v,
 	return git_default_config(k, v, ctx, cb);
 }
 
+static int clone_filter_collect(const char *var, const char *value,
+				const struct config_context *ctx UNUSED,
+				void *cb)
+{
+	char **filter_spec_p = cb;
+
+	if (!strcmp(var, "clone.defaultobjectfilter")) {
+		if (!value)
+			return config_error_nonbool(var);
+		free(*filter_spec_p);
+		*filter_spec_p = xstrdup(value);
+	}
+	return 0;
+}
+
+/*
+ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter
+ * using the urlmatch infrastructure.  A URL-qualified entry that matches
+ * the clone URL takes precedence over the bare form, following the same
+ * rules as http.<url>.* configuration variables.
+ */
+static char *get_default_object_filter(const char *url)
+{
+	struct urlmatch_config config = URLMATCH_CONFIG_INIT;
+	char *filter_spec = NULL;
+	char *normalized_url;
+
+	config.section = "clone";
+	config.key = "defaultobjectfilter";
+	config.collect_fn = clone_filter_collect;
+	config.cb = &filter_spec;
+
+	normalized_url = url_normalize(url, &config.url);
+
+	repo_config(the_repository, urlmatch_config_entry, &config);
+	free(normalized_url);
+	urlmatch_config_release(&config);
+
+	return filter_spec;
+}
+
 static int write_one_config(const char *key, const char *value,
 			    const struct config_context *ctx,
 			    void *data)
@@ -1057,6 +1099,14 @@ int cmd_clone(int argc,
 	} else
 		die(_("repository '%s' does not exist"), repo_name);
 
+	if (!filter_options.choice) {
+		char *config_filter = get_default_object_filter(repo);
+		if (config_filter) {
+			parse_list_objects_filter(&filter_options, config_filter);
+			free(config_filter);
+		}
+	}
+
 	/* no need to be strict, transport_set_option() will validate it again */
 	if (option_depth && atoi(option_depth) < 1)
 		die(_("depth %s is not a positive number"), option_depth);
diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh
index 1e354e057f..a4bfdb329e 100755
--- a/t/t5616-partial-clone.sh
+++ b/t/t5616-partial-clone.sh
@@ -722,6 +722,90 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work
 	git -C partial gc --prune=now
 '
 
+# Test clone.<url>.defaultObjectFilter config
+
+test_expect_success 'setup for clone.defaultObjectFilter tests' '
+	git init default-filter-src &&
+	echo "small" >default-filter-src/small.txt &&
+	dd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&
+	git -C default-filter-src add . &&
+	git -C default-filter-src commit -m "initial" &&
+
+	git clone --bare "file://$(pwd)/default-filter-src" default-filter-srv.bare &&
+	git -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&
+	git -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1
+'
+
+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '
+	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+	git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" clone \
+		"$SERVER_URL" default-filter-clone &&
+
+	test "$(git -C default-filter-clone config --local remote.origin.promisor)" = "true" &&
+	test "$(git -C default-filter-clone config --local remote.origin.partialclonefilter)" = "blob:limit=1024"
+'
+
+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '
+	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+	git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" \
+		clone --filter=blob:none "$SERVER_URL" default-filter-override &&
+
+	test "$(git -C default-filter-override config --local remote.origin.partialclonefilter)" = "blob:none"
+'
+
+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '
+	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+	git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" clone \
+		"$SERVER_URL" default-filter-blobnone &&
+
+	test "$(git -C default-filter-blobnone config --local remote.origin.promisor)" = "true" &&
+	test "$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)" = "blob:none"
+'
+
+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '
+	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+	git -c "clone.$SERVER_URL.defaultObjectFilter=tree:0" clone \
+		"$SERVER_URL" default-filter-tree0 &&
+
+	test "$(git -C default-filter-tree0 config --local remote.origin.promisor)" = "true" &&
+	test "$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)" = "tree:0"
+'
+
+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '
+	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+	git \
+		-c "clone.file://.defaultObjectFilter=blob:limit=1k" \
+		-c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
+		clone "$SERVER_URL" default-filter-url-specific &&
+
+	test "$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)" = "blob:none"
+'
+
+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '
+	git \
+		-c "clone.https://other.example.com/.defaultObjectFilter=blob:none" \
+		clone "file://$(pwd)/default-filter-srv.bare" default-filter-url-nomatch &&
+
+	test_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor
+'
+
+test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '
+	git -c clone.defaultObjectFilter=blob:none \
+		clone "file://$(pwd)/default-filter-srv.bare" default-filter-bare-key &&
+
+	test "$(git -C default-filter-bare-key config --local remote.origin.promisor)" = "true" &&
+	test "$(git -C default-filter-bare-key config --local remote.origin.partialclonefilter)" = "blob:none"
+'
+
+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '
+	SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+	git \
+		-c clone.defaultObjectFilter=blob:limit=1k \
+		-c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
+		clone "$SERVER_URL" default-filter-url-over-bare &&
+
+	test "$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)" = "blob:none"
+'
 
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd

base-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5
-- 
gitgitgadget