From: "Alan Braithwaite via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: ps@pks.im, christian.couder@gmail.com, jonathantanmy@google.com,
me@ttaylorr.com, gitster@pobox.com, Jeff King <peff@peff.net>,
"brian m. carlson" <sandals@crustytoothpaste.net>,
Alan Braithwaite <alan@braithwaite.dev>,
Alan Braithwaite <alan@braithwaite.dev>
Subject: [PATCH v6] clone: add clone.<url>.defaultObjectFilter config
Date: Sun, 15 Mar 2026 05:37:02 +0000 [thread overview]
Message-ID: <pull.2058.v6.git.1773553022381.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2058.v5.git.1772847236966.gitgitgadget@gmail.com>
From: Alan Braithwaite <alan@braithwaite.dev>
Add a new configuration option that lets users specify a default
partial clone filter, optionally scoped by 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]
defaultObjectFilter = blob:limit=1m
[clone "https://github.com/"]
defaultObjectFilter = blob:limit=5m
[clone "https://internal.corp.com/large-project/"]
defaultObjectFilter = blob:none
The bare clone.defaultObjectFilter applies to all clones. The
URL-qualified form clone.<url>.defaultObjectFilter restricts the
setting to matching URLs. URL matching uses the existing
urlmatch_config_entry() infrastructure, following the same rules as
http.<url>.* — a domain, namespace, or specific project can be
matched, 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
on the command line takes precedence, and --no-filter defeats the
configured default entirely.
Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>
---
fetch, clone: add fetch.blobSizeLimit config
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v6
Pull-Request: https://github.com/gitgitgadget/git/pull/2058
Range-diff vs v5:
1: fa1ea69bdb ! 1: 480453b2e7 clone: add clone.<url>.defaultObjectFilter config
@@ builtin/clone.c: static int git_clone_config(const char *k, const char *v,
+ config.cb = &filter_spec;
+
+ normalized_url = url_normalize(url, &config.url);
++ if (!normalized_url) {
++ urlmatch_config_release(&config);
++ return NULL;
++ }
+
+ repo_config(the_repository, urlmatch_config_entry, &config);
+ free(normalized_url);
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+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" &&
+
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '
++ test_when_finished "rm -r default-filter-clone" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" clone \
+ "$SERVER_URL" default-filter-clone &&
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '
++ test_when_finished "rm -r default-filter-override" &&
+ 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 &&
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '
++ test_when_finished "rm -r default-filter-blobnone" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" clone \
+ "$SERVER_URL" default-filter-blobnone &&
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '
++ test_when_finished "rm -r default-filter-tree0" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=tree:0" clone \
+ "$SERVER_URL" default-filter-tree0 &&
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '
++ test_when_finished "rm -r default-filter-url-specific" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git \
+ -c "clone.file://.defaultObjectFilter=blob:limit=1k" \
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '
++ test_when_finished "rm -r default-filter-url-nomatch" &&
+ git \
+ -c "clone.https://other.example.com/.defaultObjectFilter=blob:none" \
+ clone "file://$(pwd)/default-filter-srv.bare" default-filter-url-nomatch &&
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '
++ test_when_finished "rm -r default-filter-bare-key" &&
+ git -c clone.defaultObjectFilter=blob:none \
+ clone "file://$(pwd)/default-filter-srv.bare" default-filter-bare-key &&
+
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '
++ test_when_finished "rm -r default-filter-url-over-bare" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git \
+ -c clone.defaultObjectFilter=blob:limit=1k \
@@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
+'
+
+test_expect_success '--no-filter defeats clone.defaultObjectFilter' '
++ test_when_finished "rm -r default-filter-no-filter" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
+ clone --no-filter "$SERVER_URL" default-filter-no-filter &&
Documentation/config/clone.adoc | 34 +++++++++
builtin/clone.c | 54 ++++++++++++++
t/t5616-partial-clone.sh | 126 ++++++++++++++++++++++++++++++++
3 files changed, 214 insertions(+)
diff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc
index 0a10efd174..1d6c0957a0 100644
--- a/Documentation/config/clone.adoc
+++ b/Documentation/config/clone.adoc
@@ -21,3 +21,37 @@ 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, and `--no-filter` defeats it entirely to force a
+full clone. 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..18316a7da9 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,51 @@ 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);
+ if (!normalized_url) {
+ urlmatch_config_release(&config);
+ return NULL;
+ }
+
+ 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 +1103,14 @@ int cmd_clone(int argc,
} else
die(_("repository '%s' does not exist"), repo_name);
+ if (!filter_options.choice && !filter_options.no_filter) {
+ 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..e8cf5e353a 100755
--- a/t/t5616-partial-clone.sh
+++ b/t/t5616-partial-clone.sh
@@ -722,6 +722,132 @@ 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 &&
+ 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' '
+ test_when_finished "rm -r default-filter-clone" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" clone \
+ "$SERVER_URL" default-filter-clone &&
+
+ echo true >expect &&
+ git -C default-filter-clone config --local remote.origin.promisor >actual &&
+ test_cmp expect actual &&
+
+ echo "blob:limit=1024" >expect &&
+ git -C default-filter-clone config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '
+ test_when_finished "rm -r default-filter-override" &&
+ 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 &&
+
+ echo "blob:none" >expect &&
+ git -C default-filter-override config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '
+ test_when_finished "rm -r default-filter-blobnone" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" clone \
+ "$SERVER_URL" default-filter-blobnone &&
+
+ echo true >expect &&
+ git -C default-filter-blobnone config --local remote.origin.promisor >actual &&
+ test_cmp expect actual &&
+
+ echo "blob:none" >expect &&
+ git -C default-filter-blobnone config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '
+ test_when_finished "rm -r default-filter-tree0" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=tree:0" clone \
+ "$SERVER_URL" default-filter-tree0 &&
+
+ echo true >expect &&
+ git -C default-filter-tree0 config --local remote.origin.promisor >actual &&
+ test_cmp expect actual &&
+
+ echo "tree:0" >expect &&
+ git -C default-filter-tree0 config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '
+ test_when_finished "rm -r default-filter-url-specific" &&
+ 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 &&
+
+ echo "blob:none" >expect &&
+ git -C default-filter-url-specific config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '
+ test_when_finished "rm -r default-filter-url-nomatch" &&
+ 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' '
+ test_when_finished "rm -r default-filter-bare-key" &&
+ git -c clone.defaultObjectFilter=blob:none \
+ clone "file://$(pwd)/default-filter-srv.bare" default-filter-bare-key &&
+
+ echo true >expect &&
+ git -C default-filter-bare-key config --local remote.origin.promisor >actual &&
+ test_cmp expect actual &&
+
+ echo "blob:none" >expect &&
+ git -C default-filter-bare-key config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '
+ test_when_finished "rm -r default-filter-url-over-bare" &&
+ 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 &&
+
+ echo "blob:none" >expect &&
+ git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--no-filter defeats clone.defaultObjectFilter' '
+ test_when_finished "rm -r default-filter-no-filter" &&
+ SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
+ git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
+ clone --no-filter "$SERVER_URL" default-filter-no-filter &&
+
+ test_must_fail git -C default-filter-no-filter config --local remote.origin.promisor
+'
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd
base-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5
--
gitgitgadget
next prev parent reply other threads:[~2026-03-15 5:37 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-01 16:44 [PATCH] fetch, clone: add fetch.blobSizeLimit config Alan Braithwaite via GitGitGadget
2026-03-02 11:53 ` Patrick Steinhardt
2026-03-02 18:28 ` Jeff King
2026-03-02 18:57 ` Junio C Hamano
2026-03-02 21:36 ` Alan Braithwaite
2026-03-03 6:30 ` Patrick Steinhardt
2026-03-03 14:00 ` Alan Braithwaite
2026-03-03 15:08 ` Patrick Steinhardt
2026-03-03 17:58 ` Junio C Hamano
2026-03-04 5:07 ` Patrick Steinhardt
2026-03-03 17:05 ` Junio C Hamano
2026-03-03 14:34 ` Jeff King
2026-03-05 0:57 ` [PATCH v2] clone: add clone.<url>.defaultObjectFilter config Alan Braithwaite via GitGitGadget
2026-03-05 19:01 ` Junio C Hamano
2026-03-05 23:11 ` Alan Braithwaite
2026-03-06 6:55 ` [PATCH v3] " Alan Braithwaite via GitGitGadget
2026-03-06 10:39 ` brian m. carlson
2026-03-06 19:33 ` Junio C Hamano
2026-03-06 21:50 ` Alan Braithwaite
2026-03-06 21:47 ` [PATCH v4] " Alan Braithwaite via GitGitGadget
2026-03-06 22:18 ` Junio C Hamano
2026-03-07 1:04 ` Alan Braithwaite
2026-03-07 1:33 ` [PATCH v5] " Alan Braithwaite via GitGitGadget
2026-03-11 7:44 ` Patrick Steinhardt
2026-03-15 1:33 ` Alan Braithwaite
2026-03-15 5:37 ` Alan Braithwaite via GitGitGadget [this message]
2026-03-15 21:32 ` [PATCH v6] " Junio C Hamano
2026-03-16 7:47 ` Patrick Steinhardt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2058.v6.git.1773553022381.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=alan@braithwaite.dev \
--cc=christian.couder@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=jonathantanmy@google.com \
--cc=me@ttaylorr.com \
--cc=peff@peff.net \
--cc=ps@pks.im \
--cc=sandals@crustytoothpaste.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox