{"thread":{"id":"65110","subject":"[PATCH] fetch, clone: add fetch.blobSizeLimit config","startedAt":"2026-03-01T16:45:02Z","lastAt":"2026-05-11T07:30:25Z","messageCount":30,"participants":["Alan Braithwaite via GitGitGadget","Patrick Steinhardt","Jeff King","Junio C Hamano","Alan Braithwaite","brian m. carlson"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"537450","messageId":"pull.2058.git.1772383499900.gitgitgadget@gmail.com","threadId":"65110","inReplyTo":null,"subject":"[PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Alan Braithwaite via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-01T16:44:59Z","receivedAt":"2026-03-01T16:45:02Z","isPatch":true,"body":"From: Alan Braithwaite <alan@braithwaite.dev>\n\nExternal tools like git-lfs and git-fat use the filter clean/smudge\nmechanism to manage large binary objects, but this requires pointer\nfiles, a separate storage backend, and careful coordination. Git's\npartial clone infrastructure provides a more native approach: large\nblobs can be excluded at the protocol level during fetch and lazily\nretrieved on demand. However, enabling this requires passing\n`--filter=blob:limit=<size>` on every clone, which is not\ndiscoverable and cannot be set as a global default.\n\nAdd a new `fetch.blobSizeLimit` configuration option that enables\nsize-based partial clone behavior globally. When set, both `git\nclone` and `git fetch` automatically apply a `blob:limit=<size>`\nfilter. Blobs larger than the threshold that are not needed for the\ncurrent worktree are excluded from the transfer and lazily fetched\non demand when needed (e.g., during checkout, diff, or merge).\n\nThis makes it easy to work with repositories that have accumulated\nlarge binary files in their history, without downloading all of\nthem upfront.\n\nThe precedence order is:\n  1. Explicit `--filter=` on the command line (highest)\n  2. Existing `remote.<name>.partialclonefilter`\n  3. `fetch.blobSizeLimit` (new, lowest)\n\nOnce a clone or fetch applies this setting, the remote is registered\nas a promisor remote with the corresponding filter spec, so\nsubsequent fetches inherit it automatically. If the server does not\nsupport object filtering, the setting is silently ignored.\n\nSigned-off-by: Alan Braithwaite <alan@braithwaite.dev>\n---\n    fetch, clone: add fetch.blobSizeLimit config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2058\n\n Documentation/config/fetch.adoc | 19 +++++++++++\n builtin/clone.c                 | 13 +++++++\n builtin/fetch.c                 | 45 +++++++++++++++++++------\n t/t5616-partial-clone.sh        | 60 +++++++++++++++++++++++++++++++++\n 4 files changed, 127 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc\nindex cd40db0cad..4165354dd9 100644\n--- a/Documentation/config/fetch.adoc\n+++ b/Documentation/config/fetch.adoc\n@@ -103,6 +103,25 @@ config setting.\n \tfile helps performance of many Git commands, including `git merge-base`,\n \t`git push -f`, and `git log --graph`. Defaults to `false`.\n \n+`fetch.blobSizeLimit`::\n+\tWhen set to a size value (e.g., `1m`, `100k`, `1g`), both\n+\tlinkgit:git-clone[1] and linkgit:git-fetch[1] will automatically\n+\tuse `--filter=blob:limit=<value>` to enable partial clone\n+\tbehavior. Blobs larger than this threshold are excluded from the\n+\tinitial transfer and lazily fetched on demand when needed (e.g.,\n+\tduring checkout).\n++\n+This provides a convenient way to enable size-based partial clones\n+globally without passing `--filter` on every command. Once a clone or\n+fetch applies this setting, the remote is registered as a promisor\n+remote with the corresponding filter, so subsequent fetches inherit\n+the filter automatically.\n++\n+An explicit `--filter` option on the command line takes precedence over\n+this config. An existing `remote.<name>.partialclonefilter` also takes\n+precedence. If the server does not support object filtering, the\n+setting is silently ignored.\n+\n `fetch.bundleURI`::\n \tThis value stores a URI for downloading Git object data from a bundle\n \tURI before performing an incremental fetch from the origin Git server.\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 45d8fa0eed..1e3261b623 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -78,6 +78,7 @@ static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n static int max_jobs = -1;\n static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n static int config_filter_submodules = -1;    /* unspecified */\n+static char *config_blob_size_limit;\n static int option_remote_submodules;\n \n static int recurse_submodules_cb(const struct option *opt,\n@@ -753,6 +754,10 @@ static int git_clone_config(const char *k, const char *v,\n \t\tconfig_reject_shallow = git_config_bool(k, v);\n \tif (!strcmp(k, \"clone.filtersubmodules\"))\n \t\tconfig_filter_submodules = git_config_bool(k, v);\n+\tif (!strcmp(k, \"fetch.blobsizelimit\")) {\n+\t\tfree(config_blob_size_limit);\n+\t\tgit_config_string(&config_blob_size_limit, k, v);\n+\t}\n \n \treturn git_default_config(k, v, ctx, cb);\n }\n@@ -1010,6 +1015,13 @@ int cmd_clone(int argc,\n \targc = parse_options(argc, argv, prefix, builtin_clone_options,\n \t\t\t     builtin_clone_usage, 0);\n \n+\tif (!filter_options.choice && config_blob_size_limit) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tstrbuf_addf(&buf, \"blob:limit=%s\", config_blob_size_limit);\n+\t\tparse_list_objects_filter(&filter_options, buf.buf);\n+\t\tstrbuf_release(&buf);\n+\t}\n+\n \tif (argc > 2)\n \t\tusage_msg_opt(_(\"Too many arguments.\"),\n \t\t\tbuiltin_clone_usage, builtin_clone_options);\n@@ -1634,6 +1646,7 @@ int cmd_clone(int argc,\n \t\t       ref_storage_format);\n \n \tlist_objects_filter_release(&filter_options);\n+\tfree(config_blob_size_limit);\n \n \tstring_list_clear(&option_not, 0);\n \tstring_list_clear(&option_config, 0);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 573c295241..ff898cb6f4 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -109,6 +109,7 @@ struct fetch_config {\n \tint recurse_submodules;\n \tint parallel;\n \tint submodule_fetch_jobs;\n+\tchar *blob_size_limit;\n };\n \n static int git_fetch_config(const char *k, const char *v,\n@@ -160,6 +161,9 @@ static int git_fetch_config(const char *k, const char *v,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(k, \"fetch.blobsizelimit\"))\n+\t\treturn git_config_string(&fetch_config->blob_size_limit, k, v);\n+\n \tif (!strcmp(k, \"fetch.output\")) {\n \t\tif (!v)\n \t\t\treturn config_error_nonbool(k);\n@@ -2342,7 +2346,8 @@ static int fetch_multiple(struct string_list *list, int max_children,\n  * or inherit the default filter-spec from the config.\n  */\n static inline void fetch_one_setup_partial(struct remote *remote,\n-\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n+\t\t\t\t\t   struct list_objects_filter_options *filter_options,\n+\t\t\t\t\t   const struct fetch_config *config)\n {\n \t/*\n \t * Explicit --no-filter argument overrides everything, regardless\n@@ -2352,10 +2357,12 @@ static inline void fetch_one_setup_partial(struct remote *remote,\n \t\treturn;\n \n \t/*\n-\t * If no prior partial clone/fetch and the current fetch DID NOT\n-\t * request a partial-fetch, do a normal fetch.\n+\t * If no prior partial clone/fetch, the current fetch did not\n+\t * request a partial-fetch, and no global blob size limit is\n+\t * configured, do a normal fetch.\n \t */\n-\tif (!repo_has_promisor_remote(the_repository) && !filter_options->choice)\n+\tif (!repo_has_promisor_remote(the_repository) &&\n+\t    !filter_options->choice && !config->blob_size_limit)\n \t\treturn;\n \n \t/*\n@@ -2372,11 +2379,27 @@ static inline void fetch_one_setup_partial(struct remote *remote,\n \t/*\n \t * Do a partial-fetch from the promisor remote using either the\n \t * explicitly given filter-spec or inherit the filter-spec from\n-\t * the config.\n+\t * the per-remote config.\n+\t */\n+\tif (repo_has_promisor_remote(the_repository)) {\n+\t\tpartial_clone_get_default_filter_spec(filter_options,\n+\t\t\t\t\t\t      remote->name);\n+\t\tif (filter_options->choice)\n+\t\t\treturn;\n+\t}\n+\n+\t/*\n+\t * Fall back to the global fetch.blobSizeLimit config. This\n+\t * enables partial clone behavior without requiring --filter\n+\t * on the command line or a pre-existing promisor remote.\n \t */\n-\tif (!filter_options->choice)\n-\t\tpartial_clone_get_default_filter_spec(filter_options, remote->name);\n-\treturn;\n+\tif (!filter_options->choice && config->blob_size_limit) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tstrbuf_addf(&buf, \"blob:limit=%s\", config->blob_size_limit);\n+\t\tparse_list_objects_filter(filter_options, buf.buf);\n+\t\tstrbuf_release(&buf);\n+\t\tpartial_clone_register(remote->name, filter_options);\n+\t}\n }\n \n static int fetch_one(struct remote *remote, int argc, const char **argv,\n@@ -2762,9 +2785,10 @@ int cmd_fetch(int argc,\n \t\toidset_clear(&acked_commits);\n \t\ttrace2_region_leave(\"fetch\", \"negotiate-only\", the_repository);\n \t} else if (remote) {\n-\t\tif (filter_options.choice || repo_has_promisor_remote(the_repository)) {\n+\t\tif (filter_options.choice || repo_has_promisor_remote(the_repository) ||\n+\t\t    config.blob_size_limit) {\n \t\t\ttrace2_region_enter(\"fetch\", \"setup-partial\", the_repository);\n-\t\t\tfetch_one_setup_partial(remote, &filter_options);\n+\t\t\tfetch_one_setup_partial(remote, &filter_options, &config);\n \t\t\ttrace2_region_leave(\"fetch\", \"setup-partial\", the_repository);\n \t\t}\n \t\ttrace2_region_enter(\"fetch\", \"fetch-one\", the_repository);\n@@ -2876,5 +2900,6 @@ int cmd_fetch(int argc,\n  cleanup:\n \tstring_list_clear(&list, 0);\n \tlist_objects_filter_release(&filter_options);\n+\tfree(config.blob_size_limit);\n \treturn result;\n }\ndiff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\nindex 1e354e057f..44b41f315f 100755\n--- a/t/t5616-partial-clone.sh\n+++ b/t/t5616-partial-clone.sh\n@@ -722,6 +722,66 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n \tgit -C partial gc --prune=now\n '\n \n+# Test fetch.blobSizeLimit config\n+\n+test_expect_success 'setup for fetch.blobSizeLimit tests' '\n+\tgit init blob-limit-src &&\n+\techo \"small\" >blob-limit-src/small.txt &&\n+\tdd if=/dev/zero of=blob-limit-src/large.bin bs=1024 count=100 2>/dev/null &&\n+\tgit -C blob-limit-src add . &&\n+\tgit -C blob-limit-src commit -m \"initial\" &&\n+\n+\tgit clone --bare \"file://$(pwd)/blob-limit-src\" blob-limit-srv.bare &&\n+\tgit -C blob-limit-srv.bare config --local uploadpack.allowfilter 1 &&\n+\tgit -C blob-limit-srv.bare config --local uploadpack.allowanysha1inwant 1\n+'\n+\n+test_expect_success 'clone with fetch.blobSizeLimit config applies filter' '\n+\tgit -c fetch.blobSizeLimit=1k clone \\\n+\t\t\"file://$(pwd)/blob-limit-srv.bare\" blob-limit-clone &&\n+\n+\ttest \"$(git -C blob-limit-clone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C blob-limit-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n+'\n+\n+test_expect_success 'clone with --filter overrides fetch.blobSizeLimit' '\n+\tgit -c fetch.blobSizeLimit=1k clone --filter=blob:none \\\n+\t\t\"file://$(pwd)/blob-limit-srv.bare\" blob-limit-override &&\n+\n+\ttest \"$(git -C blob-limit-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'fetch with fetch.blobSizeLimit registers promisor remote' '\n+\tgit clone --no-checkout \"file://$(pwd)/blob-limit-srv.bare\" blob-limit-fetch &&\n+\n+\t# Sanity: not yet a partial clone\n+\ttest_must_fail git -C blob-limit-fetch config --local remote.origin.promisor &&\n+\n+\t# Add a new commit to the server\n+\techo \"new-small\" >blob-limit-src/new-small.txt &&\n+\tdd if=/dev/zero of=blob-limit-src/new-large.bin bs=1024 count=100 2>/dev/null &&\n+\tgit -C blob-limit-src add . &&\n+\tgit -C blob-limit-src commit -m \"second\" &&\n+\tgit -C blob-limit-src push \"file://$(pwd)/blob-limit-srv.bare\" main &&\n+\n+\t# Fetch with the config set\n+\tgit -C blob-limit-fetch -c fetch.blobSizeLimit=1k fetch origin &&\n+\n+\ttest \"$(git -C blob-limit-fetch config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C blob-limit-fetch config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n+'\n+\n+test_expect_success 'fetch.blobSizeLimit does not override existing partialclonefilter' '\n+\tgit clone --filter=blob:none \\\n+\t\t\"file://$(pwd)/blob-limit-srv.bare\" blob-limit-existing &&\n+\n+\ttest \"$(git -C blob-limit-existing config --local remote.origin.partialclonefilter)\" = \"blob:none\" &&\n+\n+\t# Fetch with a different blobSizeLimit; existing filter should win\n+\tgit -C blob-limit-existing -c fetch.blobSizeLimit=1k fetch origin &&\n+\n+\ttest \"$(git -C blob-limit-existing config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\n-- \ngitgitgadget\n"},{"id":"537503","messageId":"aaV6PLJCrpb2mQnq@pks.im","threadId":"65110","inReplyTo":"pull.2058.git.1772383499900.gitgitgadget@gmail.com","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T11:53:32Z","receivedAt":"2026-03-02T11:53:39Z","isPatch":true,"body":"On Sun, Mar 01, 2026 at 04:44:59PM +0000, Alan Braithwaite via GitGitGadget wrote:\n> From: Alan Braithwaite <alan@braithwaite.dev>\n> \n> External tools like git-lfs and git-fat use the filter clean/smudge\n> mechanism to manage large binary objects, but this requires pointer\n> files, a separate storage backend, and careful coordination. Git's\n> partial clone infrastructure provides a more native approach: large\n> blobs can be excluded at the protocol level during fetch and lazily\n> retrieved on demand. However, enabling this requires passing\n> `--filter=blob:limit=<size>` on every clone, which is not\n> discoverable and cannot be set as a global default.\n\nI'm not sure that we should make blob size limiting the default. The\nproblem with specifying a limit is that this is comparatively expensive\nto compute on the server side: we have to look up each blob so that we\ncan determine its size. Unfortunately, such requests cannot (currently)\nbe optimized via for example bitmaps, or any other cache that we have.\n\nSo if we want to make any filter the default, I'd propose that we should\nrather think about filters that are computationally less expensive, like\nfor example `--filter=blob:none`. This can be computed efficiently via\nbitmaps.\n\nThe downside is of course that in this case we have to do way more\nbackfill fetches compared to the case where we only leave out a couple\nof blobs. But unless we figure out a way to serve the size limit filter\nin a more efficient way I'm not sure about proper alternatives.\n\nAnother question to consider: is it really sensible to set this setting\nglobally? It is very much dependent on the forge that you're connecting\nto, as forges may not even allow object filters at all, or only a subset\nof them.\n\nThanks!\n\nPatrick\n"},{"id":"537576","messageId":"20260302182838.GI28275@coredump.intra.peff.net","threadId":"65110","inReplyTo":"aaV6PLJCrpb2mQnq@pks.im","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-02T18:28:38Z","receivedAt":"2026-03-02T18:28:39Z","isPatch":true,"body":"On Mon, Mar 02, 2026 at 12:53:32PM +0100, Patrick Steinhardt wrote:\n\n> On Sun, Mar 01, 2026 at 04:44:59PM +0000, Alan Braithwaite via GitGitGadget wrote:\n> > From: Alan Braithwaite <alan@braithwaite.dev>\n> > \n> > External tools like git-lfs and git-fat use the filter clean/smudge\n> > mechanism to manage large binary objects, but this requires pointer\n> > files, a separate storage backend, and careful coordination. Git's\n> > partial clone infrastructure provides a more native approach: large\n> > blobs can be excluded at the protocol level during fetch and lazily\n> > retrieved on demand. However, enabling this requires passing\n> > `--filter=blob:limit=<size>` on every clone, which is not\n> > discoverable and cannot be set as a global default.\n> \n> I'm not sure that we should make blob size limiting the default. The\n> problem with specifying a limit is that this is comparatively expensive\n> to compute on the server side: we have to look up each blob so that we\n> can determine its size. Unfortunately, such requests cannot (currently)\n> be optimized via for example bitmaps, or any other cache that we have.\n\nWe actually can do blob:limit filters with bitmaps. See 84243da129\n(pack-bitmap: implement BLOB_LIMIT filtering, 2020-02-14). It's more\nexpensive than blob:none, but not much. Once we have the list of blobs\nwe can get their sizes directly from the packfile. It's stuff like\npath-limiting that is truly expensive, because it requires a traversal.\n\nAll that said, I'd be wary of turning on partial clones like this by\ndefault. I feel like there are still a lot of performance gotchas\nlurking (and possibly some correctness ones, too).\n\n-Peff\n"},{"id":"537586","messageId":"xmqq342i12ky.fsf@gitster.g","threadId":"65110","inReplyTo":"aaV6PLJCrpb2mQnq@pks.im","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-02T18:57:49Z","receivedAt":"2026-03-02T18:57:52Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> I'm not sure that we should make blob size limiting the default. The\n> problem with specifying a limit is that this is comparatively expensive\n> to compute on the server side: we have to look up each blob so that we\n> can determine its size. Unfortunately, such requests cannot (currently)\n> be optimized via for example bitmaps, or any other cache that we have.\n> ...\n> Another question to consider: is it really sensible to set this setting\n> globally? It is very much dependent on the forge that you're connecting\n> to, as forges may not even allow object filters at all, or only a subset\n> of them.\n\nBoth are good questions, but to affect \"clone\" you'd need either\n\"git -c that.variable=setting clone\" or have it in ~/.gitconfig no?\n\nAs to this extra variable, it can already be done with existing\nremote.*.partialCloneFilter, it seems, so I do not know why we want\nto add it.\n\n\n"},{"id":"537605","messageId":"a3e064fe-9f0d-448f-b034-4a95dcd3fe97@app.fastmail.com","threadId":"65110","inReplyTo":"xmqq342i12ky.fsf@gitster.g","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Alan Braithwaite","fromEmail":"alan@braithwaite.dev","sentAt":"2026-03-02T21:36:40Z","receivedAt":"2026-03-02T21:38:14Z","isPatch":true,"body":"Patrick, Peff, Junio — thanks for taking the time to look at\nthis.\n\nPatrick wrote:\n> I'm not sure that we should make blob size limiting the\n> default.\n\nTo clarify — this is a user-opt-in config, not a default. You\nwould only get partial clone behavior if you explicitly set\nfetch.blobSizeLimit in your gitconfig.\n\nPeff wrote:\n> We actually can do blob:limit filters with bitmaps. See\n> 84243da129 (pack-bitmap: implement BLOB_LIMIT filtering,\n> 2020-02-14).\n\nGood to know. I'm not positive, but my understanding is that\nthis patch only touches client code, and the server sees an\nidentical request to what `git clone --filter=blob:limit=1m`\nalready sends today. If that's correct, anyone can already\nimpose that cost — this patch just makes it easier to opt in.\n\n> All that said, I'd be wary of turning on partial clones like\n> this by default.\n\nThat's fair. I'm not attached to getting this merged — it was\nmore exploratory to start a discussion.\n\nJunio wrote:\n> As to this extra variable, it can already be done with\n> existing remote.*.partialCloneFilter, it seems, so I do not\n> know why we want to add it.\n\nI may not understand the config as well as you do, but my\nreading is that remote.*.partialCloneFilter requires a specific\nremote name and only takes effect on subsequent fetches from an\nalready-registered promisor remote — not the initial clone. You\nwould also need remote.origin.promisor=true set globally, which\nseems odd. If I'm understanding correctly, there is currently\nno way to say \"all new clones should use a blob size filter\"\nvia config alone. But please correct me if I'm wrong.\n\nSeparately — is my understanding correct that partial clone\nwith blob:limit works today without server-side changes,\nassuming uploadpack.allowFilter is enabled? If so, I'm happy\nto maintain this as a local client patch for my own workflow.\n\nThanks again,\nAlan\n\n\nOn Mon, Mar 2, 2026, at 10:57, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> I'm not sure that we should make blob size limiting the default. The\n>> problem with specifying a limit is that this is comparatively expensive\n>> to compute on the server side: we have to look up each blob so that we\n>> can determine its size. Unfortunately, such requests cannot (currently)\n>> be optimized via for example bitmaps, or any other cache that we have.\n>> ...\n>> Another question to consider: is it really sensible to set this setting\n>> globally? It is very much dependent on the forge that you're connecting\n>> to, as forges may not even allow object filters at all, or only a subset\n>> of them.\n>\n> Both are good questions, but to affect \"clone\" you'd need either\n> \"git -c that.variable=setting clone\" or have it in ~/.gitconfig no?\n>\n> As to this extra variable, it can already be done with existing\n> remote.*.partialCloneFilter, it seems, so I do not know why we want\n> to add it.\n"},{"id":"537647","messageId":"aaaACBJVAZPypVtn@pks.im","threadId":"65110","inReplyTo":"a3e064fe-9f0d-448f-b034-4a95dcd3fe97@app.fastmail.com","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-03T06:30:32Z","receivedAt":"2026-03-03T06:30:40Z","isPatch":true,"body":"On Mon, Mar 02, 2026 at 01:36:40PM -0800, Alan Braithwaite wrote:\n> Peff wrote:\n> > We actually can do blob:limit filters with bitmaps. See\n> > 84243da129 (pack-bitmap: implement BLOB_LIMIT filtering,\n> > 2020-02-14).\n> \n> Good to know. I'm not positive, but my understanding is that\n> this patch only touches client code, and the server sees an\n> identical request to what `git clone --filter=blob:limit=1m`\n> already sends today. If that's correct, anyone can already\n> impose that cost — this patch just makes it easier to opt in.\n\nAh, right, that's something I forgot. I've seen too many performance\nissues recently with blob:limit fetches, so I jumped the gun.\n\n> Junio wrote:\n> > As to this extra variable, it can already be done with\n> > existing remote.*.partialCloneFilter, it seems, so I do not\n> > know why we want to add it.\n> \n> I may not understand the config as well as you do, but my\n> reading is that remote.*.partialCloneFilter requires a specific\n> remote name and only takes effect on subsequent fetches from an\n> already-registered promisor remote — not the initial clone. You\n> would also need remote.origin.promisor=true set globally, which\n> seems odd. If I'm understanding correctly, there is currently\n> no way to say \"all new clones should use a blob size filter\"\n> via config alone. But please correct me if I'm wrong.\n\nNo, you're right about this one, and I think this is a sensible thing to\nwant. But what I'd like to see is a bit more nuance, I guess:\n\n  - It should be possible to specify the configuration per URL. If you\n    know that git.example.com knows object filters you may want to turn\n    them on for that domain specifically. So the mechanism would work\n    similar to \"url.<base>.insteadOf\" or \"http.<url>.*\" settings.\n\n  - The infrastructure shouldn't cast any specific filter into stone.\n    Instead, it should be possible to specify a default filter.\n\nI'd assume that these settings should only impact the initial clone to\nuse a default filter in case the cloned URL matches the configured URL.\nFor existing repositories it shouldn't have any impact, as we should\ncontinue to respect the \".git/config\" there when it comes to promisors\nand filters.\n\n> Separately — is my understanding correct that partial clone\n> with blob:limit works today without server-side changes,\n> assuming uploadpack.allowFilter is enabled? If so, I'm happy\n> to maintain this as a local client patch for my own workflow.\n\nYes, blob:limit filters are supported by many forges nowadays.\n\nPatrick\n"},{"id":"537667","messageId":"d4e2aa7e-6c6e-43a5-96ad-848d9447d194@app.fastmail.com","threadId":"65110","inReplyTo":"aaaACBJVAZPypVtn@pks.im","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Alan Braithwaite","fromEmail":"alan@braithwaite.dev","sentAt":"2026-03-03T14:00:29Z","receivedAt":"2026-03-03T14:00:52Z","isPatch":true,"body":"Patrick wrote:\n> No, you're right about this one, and I think this is a\n> sensible thing to want. But what I'd like to see is a bit\n> more nuance, I guess:\n>\n>   - It should be possible to specify the configuration per\n>     URL. If you know that git.example.com knows object\n>     filters you may want to turn them on for that domain\n>     specifically. So the mechanism would work similar to\n>     \"url.<base>.insteadOf\" or \"http.<url>.*\" settings.\n>\n>   - The infrastructure shouldn't cast any specific filter\n>     into stone. Instead, it should be possible to specify a\n>     default filter.\n\nThanks, this is great feedback. I took a look at the existing\nURL-based config patterns and I think the http.<url>.* model\nis the right one to follow, since it already uses the\nurlmatch_config_entry() infrastructure with proper URL\nnormalization, host globs, and longest-match specificity.\n\nHere's what I'm thinking for a v2. I'd like to get feedback\non the design before implementing:\n\nThe config would use a new section that supports both a global\ndefault and per-URL overrides, following the same pattern as\nhttp.sslVerify vs http.<url>.sslVerify:\n\n    # Global default — applies to all clones/fetches\n    [fetch]\n        partialCloneFilter = blob:limit=1m\n\n    # Per-URL override — more specific match wins\n    [fetch \"https://github.com/\"]\n        partialCloneFilter = blob:limit=5m\n\n    [fetch \"https://internal.corp.com/\"]\n        partialCloneFilter = blob:none\n\nDesign points:\n\n  - Accepts any filter spec, not just blob:limit. This\n    addresses your point about not casting a specific filter\n    into stone.\n\n  - Uses fetch.<url>.partialCloneFilter, following the\n    http.<url>.* precedent. The urlmatch.c infrastructure\n    handles URL normalization, host globs (*.example.com),\n    default port stripping, and path-based specificity\n    ordering — so no new matching logic would be needed.\n\n  - A bare fetch.partialCloneFilter (no URL) acts as the\n    global default, the same way http.sslVerify is the\n    global default that http.<url>.sslVerify can override.\n\n  - Only applies to initial clone and to fetches where no\n    existing remote.<name>.partialCloneFilter is set. Existing\n    repos continue using their per-remote config.\n\n  - Explicit --filter on the command line still takes\n    precedence over everything.\n\n  - If the server does not support object filtering, the\n    setting is silently ignored (existing behavior).\n\nI chose fetch.* rather than clone.* so that both git-clone\nand git-fetch can use the same config. In practice this\nmainly matters for the initial clone, since once the promisor\nremote is registered, subsequent fetches inherit the filter\nfrom remote.<name>.partialCloneFilter anyway.\n\nDoes this direction make sense? Happy to hear if there are\nconcerns before I start on a v2.\n\nThanks,\n- Alan\n"},{"id":"537670","messageId":"20260303143400.GA820518@coredump.intra.peff.net","threadId":"65110","inReplyTo":"a3e064fe-9f0d-448f-b034-4a95dcd3fe97@app.fastmail.com","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-03T14:34:00Z","receivedAt":"2026-03-03T14:34:02Z","isPatch":true,"body":"On Mon, Mar 02, 2026 at 01:36:40PM -0800, Alan Braithwaite wrote:\n\n> Peff wrote:\n> > We actually can do blob:limit filters with bitmaps. See\n> > 84243da129 (pack-bitmap: implement BLOB_LIMIT filtering,\n> > 2020-02-14).\n> \n> Good to know. I'm not positive, but my understanding is that\n> this patch only touches client code, and the server sees an\n> identical request to what `git clone --filter=blob:limit=1m`\n> already sends today. If that's correct, anyone can already\n> impose that cost — this patch just makes it easier to opt in.\n\nYes, that's correct. The server protects itself by refusing to support\ncertain filters that are too expensive. Usually by setting\nuploadpackfilter.allow to \"false\", followed by enabling\nuploadpackfilter.*.allow for particular filters.\n\nWhen we added those, we left the defaults as-is (allowing everything).\nThat's OK for casual use amongst your own repositories, but terrible for\na hosting site. I don't know if it would be worth revisiting the\ndefaults.\n\nBut anyway, all orthogonal to the topic in this thread.\n\n-Peff\n"},{"id":"537683","messageId":"aab5iICOAMrH2aQZ@pks.im","threadId":"65110","inReplyTo":"d4e2aa7e-6c6e-43a5-96ad-848d9447d194@app.fastmail.com","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-03T15:08:56Z","receivedAt":"2026-03-03T15:09:03Z","isPatch":true,"body":"On Tue, Mar 03, 2026 at 06:00:29AM -0800, Alan Braithwaite wrote:\n> Patrick wrote:\n> > No, you're right about this one, and I think this is a\n> > sensible thing to want. But what I'd like to see is a bit\n> > more nuance, I guess:\n> >\n> >   - It should be possible to specify the configuration per\n> >     URL. If you know that git.example.com knows object\n> >     filters you may want to turn them on for that domain\n> >     specifically. So the mechanism would work similar to\n> >     \"url.<base>.insteadOf\" or \"http.<url>.*\" settings.\n> >\n> >   - The infrastructure shouldn't cast any specific filter\n> >     into stone. Instead, it should be possible to specify a\n> >     default filter.\n> \n> Thanks, this is great feedback. I took a look at the existing\n> URL-based config patterns and I think the http.<url>.* model\n> is the right one to follow, since it already uses the\n> urlmatch_config_entry() infrastructure with proper URL\n> normalization, host globs, and longest-match specificity.\n> \n> Here's what I'm thinking for a v2. I'd like to get feedback\n> on the design before implementing:\n> \n> The config would use a new section that supports both a global\n> default and per-URL overrides, following the same pattern as\n> http.sslVerify vs http.<url>.sslVerify:\n> \n>     # Global default — applies to all clones/fetches\n>     [fetch]\n>         partialCloneFilter = blob:limit=1m\n> \n>     # Per-URL override — more specific match wins\n>     [fetch \"https://github.com/\"]\n>         partialCloneFilter = blob:limit=5m\n> \n>     [fetch \"https://internal.corp.com/\"]\n>         partialCloneFilter = blob:none\n> \n> Design points:\n> \n>   - Accepts any filter spec, not just blob:limit. This\n>     addresses your point about not casting a specific filter\n>     into stone.\n> \n>   - Uses fetch.<url>.partialCloneFilter, following the\n>     http.<url>.* precedent. The urlmatch.c infrastructure\n>     handles URL normalization, host globs (*.example.com),\n>     default port stripping, and path-based specificity\n>     ordering — so no new matching logic would be needed.\n> \n>   - A bare fetch.partialCloneFilter (no URL) acts as the\n>     global default, the same way http.sslVerify is the\n>     global default that http.<url>.sslVerify can override.\n> \n>   - Only applies to initial clone and to fetches where no\n>     existing remote.<name>.partialCloneFilter is set. Existing\n>     repos continue using their per-remote config.\n> \n>   - Explicit --filter on the command line still takes\n>     precedence over everything.\n> \n>   - If the server does not support object filtering, the\n>     setting is silently ignored (existing behavior).\n> \n> I chose fetch.* rather than clone.* so that both git-clone\n> and git-fetch can use the same config. In practice this\n> mainly matters for the initial clone, since once the promisor\n> remote is registered, subsequent fetches inherit the filter\n> from remote.<name>.partialCloneFilter anyway.\n\nI think using something like \"clone.<url>.defaultObjectFilter\" would be\na more sensible design. The idea is that we'd only honor this filter on\nthe initial clone to basically be equivalent to `git clone --filter=`. I\ndon't think any subsequent fetches should be impacted at all, as turning\na full clone into a partial clone would need more consideration.\n\nPatrick\n"},{"id":"537696","messageId":"xmqqseagetd1.fsf@gitster.g","threadId":"65110","inReplyTo":"aaaACBJVAZPypVtn@pks.im","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-03T17:05:30Z","receivedAt":"2026-03-03T17:05:33Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> No, you're right about this one, and I think this is a sensible thing to\n> want. But what I'd like to see is a bit more nuance, I guess:\n>\n>   - It should be possible to specify the configuration per URL. If you\n>     know that git.example.com knows object filters you may want to turn\n>     them on for that domain specifically. So the mechanism would work\n>     similar to \"url.<base>.insteadOf\" or \"http.<url>.*\" settings.\n>\n>   - The infrastructure shouldn't cast any specific filter into stone.\n>     Instead, it should be possible to specify a default filter.\n>\n> I'd assume that these settings should only impact the initial clone to\n> use a default filter in case the cloned URL matches the configured URL.\n> For existing repositories it shouldn't have any impact, as we should\n> continue to respect the \".git/config\" there when it comes to promisors\n> and filters.\n\nAhh, thanks for pointing out the flaw in my thinking that forgets\nthat \"remote.<name>.partialCloneFilter\" would not work in the\ninitial state where there is no <name> associated with the remote\nrepository you are trying to contact.  I agree that something like\n\"remote.<url>.particialCloneFilter\" is a more proper way forward.\n\n"},{"id":"537706","messageId":"xmqqtsuwdccu.fsf@gitster.g","threadId":"65110","inReplyTo":"aab5iICOAMrH2aQZ@pks.im","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-03T17:58:09Z","receivedAt":"2026-03-03T17:58:11Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> I think using something like \"clone.<url>.defaultObjectFilter\" would be\n> a more sensible design. The idea is that we'd only honor this filter on\n> the initial clone to basically be equivalent to `git clone --filter=`. I\n> don't think any subsequent fetches should be impacted at all, as turning\n> a full clone into a partial clone would need more consideration.\n\nYup, I like this one.  Should <url> be giving a repository fully, or\nbe some pattern that groups similar repositories together?  You\nwould not be wanting to clone exactly the same repository so many\ntimes for a configuration variable to matter in general.\n"},{"id":"537746","messageId":"aae-CboMNIPRmKts@pks.im","threadId":"65110","inReplyTo":"xmqqtsuwdccu.fsf@gitster.g","subject":"Re: [PATCH] fetch, clone: add fetch.blobSizeLimit config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-04T05:07:21Z","receivedAt":"2026-03-04T05:07:29Z","isPatch":true,"body":"On Tue, Mar 03, 2026 at 09:58:09AM -0800, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > I think using something like \"clone.<url>.defaultObjectFilter\" would be\n> > a more sensible design. The idea is that we'd only honor this filter on\n> > the initial clone to basically be equivalent to `git clone --filter=`. I\n> > don't think any subsequent fetches should be impacted at all, as turning\n> > a full clone into a partial clone would need more consideration.\n> \n> Yup, I like this one.  Should <url> be giving a repository fully, or\n> be some pattern that groups similar repositories together?  You\n> would not be wanting to clone exactly the same repository so many\n> times for a configuration variable to matter in general.\n\nI'd propose that it should work the same as our \"http.<url>.*\" config:\n\n  - You can enable partial clones for a complete domain, like for\n    example \"github.com\" or \"gitlab.com\".\n\n  - You can specify a namespace, like \"gitlab.com/example\", so that all\n    projects in there would be using the filter.\n\n  - You can specify a project, like \"gitlab.com/example/project.git\".\n\nI'd say that this should be sufficient for most usecases.\n\nPatrick\n"},{"id":"537870","messageId":"pull.2058.v2.git.1772672251281.gitgitgadget@gmail.com","threadId":"65110","inReplyTo":"pull.2058.git.1772383499900.gitgitgadget@gmail.com","subject":"[PATCH v2] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-05T00:57:31Z","receivedAt":"2026-03-05T00:57:33Z","isPatch":true,"body":"From: Alan Braithwaite <alan@braithwaite.dev>\n\nAdd a new configuration option that lets users specify a default\npartial clone filter per URL pattern.  When cloning a repository\nwhose URL matches a configured pattern, git-clone automatically\napplies the filter, equivalent to passing --filter on the command\nline.\n\n    [clone \"https://github.com/\"]\n        defaultObjectFilter = blob:limit=5m\n\n    [clone \"https://internal.corp.com/large-project/\"]\n        defaultObjectFilter = blob:none\n\nURL matching uses the existing urlmatch_config_entry() infrastructure,\nfollowing the same rules as http.<url>.* — you can match a domain,\na namespace path, or a specific project, and the most specific match\nwins.\n\nThe config only affects the initial clone.  Once the clone completes,\nthe filter is recorded in remote.<name>.partialCloneFilter, so\nsubsequent fetches inherit it automatically.  An explicit --filter\nflag on the command line takes precedence.\n\nOnly the URL-qualified form (clone.<url>.defaultObjectFilter) is\nhonored; a bare clone.defaultObjectFilter without a URL subsection\nis ignored.\n\nSigned-off-by: Alan Braithwaite <alan@braithwaite.dev>\n---\n    fetch, clone: add fetch.blobSizeLimit config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2058\n\nRange-diff vs v1:\n\n 1:  818b64e2e2 ! 1:  4a73edd2e8 fetch, clone: add fetch.blobSizeLimit config\n     @@ Metadata\n      Author: Alan Braithwaite <alan@braithwaite.dev>\n      \n       ## Commit message ##\n     -    fetch, clone: add fetch.blobSizeLimit config\n     +    clone: add clone.<url>.defaultObjectFilter config\n      \n     -    External tools like git-lfs and git-fat use the filter clean/smudge\n     -    mechanism to manage large binary objects, but this requires pointer\n     -    files, a separate storage backend, and careful coordination. Git's\n     -    partial clone infrastructure provides a more native approach: large\n     -    blobs can be excluded at the protocol level during fetch and lazily\n     -    retrieved on demand. However, enabling this requires passing\n     -    `--filter=blob:limit=<size>` on every clone, which is not\n     -    discoverable and cannot be set as a global default.\n     +    Add a new configuration option that lets users specify a default\n     +    partial clone filter per URL pattern.  When cloning a repository\n     +    whose URL matches a configured pattern, git-clone automatically\n     +    applies the filter, equivalent to passing --filter on the command\n     +    line.\n      \n     -    Add a new `fetch.blobSizeLimit` configuration option that enables\n     -    size-based partial clone behavior globally. When set, both `git\n     -    clone` and `git fetch` automatically apply a `blob:limit=<size>`\n     -    filter. Blobs larger than the threshold that are not needed for the\n     -    current worktree are excluded from the transfer and lazily fetched\n     -    on demand when needed (e.g., during checkout, diff, or merge).\n     +        [clone \"https://github.com/\"]\n     +            defaultObjectFilter = blob:limit=5m\n      \n     -    This makes it easy to work with repositories that have accumulated\n     -    large binary files in their history, without downloading all of\n     -    them upfront.\n     +        [clone \"https://internal.corp.com/large-project/\"]\n     +            defaultObjectFilter = blob:none\n      \n     -    The precedence order is:\n     -      1. Explicit `--filter=` on the command line (highest)\n     -      2. Existing `remote.<name>.partialclonefilter`\n     -      3. `fetch.blobSizeLimit` (new, lowest)\n     +    URL matching uses the existing urlmatch_config_entry() infrastructure,\n     +    following the same rules as http.<url>.* — you can match a domain,\n     +    a namespace path, or a specific project, and the most specific match\n     +    wins.\n      \n     -    Once a clone or fetch applies this setting, the remote is registered\n     -    as a promisor remote with the corresponding filter spec, so\n     -    subsequent fetches inherit it automatically. If the server does not\n     -    support object filtering, the setting is silently ignored.\n     +    The config only affects the initial clone.  Once the clone completes,\n     +    the filter is recorded in remote.<name>.partialCloneFilter, so\n     +    subsequent fetches inherit it automatically.  An explicit --filter\n     +    flag on the command line takes precedence.\n     +\n     +    Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n     +    honored; a bare clone.defaultObjectFilter without a URL subsection\n     +    is ignored.\n      \n          Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n      \n     - ## Documentation/config/fetch.adoc ##\n     -@@ Documentation/config/fetch.adoc: config setting.\n     - \tfile helps performance of many Git commands, including `git merge-base`,\n     - \t`git push -f`, and `git log --graph`. Defaults to `false`.\n     - \n     -+`fetch.blobSizeLimit`::\n     -+\tWhen set to a size value (e.g., `1m`, `100k`, `1g`), both\n     -+\tlinkgit:git-clone[1] and linkgit:git-fetch[1] will automatically\n     -+\tuse `--filter=blob:limit=<value>` to enable partial clone\n     -+\tbehavior. Blobs larger than this threshold are excluded from the\n     -+\tinitial transfer and lazily fetched on demand when needed (e.g.,\n     -+\tduring checkout).\n     + ## Documentation/config/clone.adoc ##\n     +@@ Documentation/config/clone.adoc: endif::[]\n     + \tIf a partial clone filter is provided (see `--filter` in\n     + \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n     + \tthe filter to submodules.\n     ++\n     ++`clone.<url>.defaultObjectFilter`::\n     ++\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n     ++\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n     ++\tuse `--filter=<value>` when the clone URL matches `<url>`.\n     ++\tObjects matching the filter are excluded from the initial\n     ++\ttransfer and lazily fetched on demand (e.g., during checkout).\n     ++\tSubsequent fetches inherit the filter via the per-remote config\n     ++\tthat is written during the clone.\n      ++\n     -+This provides a convenient way to enable size-based partial clones\n     -+globally without passing `--filter` on every command. Once a clone or\n     -+fetch applies this setting, the remote is registered as a promisor\n     -+remote with the corresponding filter, so subsequent fetches inherit\n     -+the filter automatically.\n     ++The URL matching follows the same rules as `http.<url>.*` (see\n     ++linkgit:git-config[1]).  The most specific URL match wins.  You can\n     ++match a complete domain, a namespace, or a specific project:\n      ++\n     -+An explicit `--filter` option on the command line takes precedence over\n     -+this config. An existing `remote.<name>.partialclonefilter` also takes\n     -+precedence. If the server does not support object filtering, the\n     -+setting is silently ignored.\n     ++----\n     ++[clone \"https://github.com/\"]\n     ++    defaultObjectFilter = blob:limit=5m\n      +\n     - `fetch.bundleURI`::\n     - \tThis value stores a URI for downloading Git object data from a bundle\n     - \tURI before performing an incremental fetch from the origin Git server.\n     ++[clone \"https://internal.corp.com/large-project/\"]\n     ++    defaultObjectFilter = blob:none\n     ++----\n     +++\n     ++An explicit `--filter` option on the command line takes precedence\n     ++over this config.  Only affects the initial clone; it has no effect\n     ++on later fetches into an existing repository.  If the server does\n     ++not support object filtering, the setting is silently ignored.\n      \n       ## builtin/clone.c ##\n     -@@ builtin/clone.c: static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;\n     - static int max_jobs = -1;\n     - static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;\n     - static int config_filter_submodules = -1;    /* unspecified */\n     -+static char *config_blob_size_limit;\n     - static int option_remote_submodules;\n     - \n     - static int recurse_submodules_cb(const struct option *opt,\n     +@@\n     + #include \"path.h\"\n     + #include \"pkt-line.h\"\n     + #include \"list-objects-filter-options.h\"\n     ++#include \"urlmatch.h\"\n     + #include \"hook.h\"\n     + #include \"bundle.h\"\n     + #include \"bundle-uri.h\"\n      @@ builtin/clone.c: static int git_clone_config(const char *k, const char *v,\n     - \t\tconfig_reject_shallow = git_config_bool(k, v);\n     - \tif (!strcmp(k, \"clone.filtersubmodules\"))\n     - \t\tconfig_filter_submodules = git_config_bool(k, v);\n     -+\tif (!strcmp(k, \"fetch.blobsizelimit\")) {\n     -+\t\tfree(config_blob_size_limit);\n     -+\t\tgit_config_string(&config_blob_size_limit, k, v);\n     -+\t}\n     - \n       \treturn git_default_config(k, v, ctx, cb);\n       }\n     -@@ builtin/clone.c: int cmd_clone(int argc,\n     - \targc = parse_options(argc, argv, prefix, builtin_clone_options,\n     - \t\t\t     builtin_clone_usage, 0);\n       \n     -+\tif (!filter_options.choice && config_blob_size_limit) {\n     -+\t\tstruct strbuf buf = STRBUF_INIT;\n     -+\t\tstrbuf_addf(&buf, \"blob:limit=%s\", config_blob_size_limit);\n     -+\t\tparse_list_objects_filter(&filter_options, buf.buf);\n     -+\t\tstrbuf_release(&buf);\n     ++struct clone_filter_data {\n     ++\tchar *default_object_filter;\n     ++};\n     ++\n     ++static int clone_filter_collect(const char *var, const char *value,\n     ++\t\t\t\tconst struct config_context *ctx UNUSED,\n     ++\t\t\t\tvoid *cb)\n     ++{\n     ++\tstruct clone_filter_data *data = cb;\n     ++\n     ++\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n     ++\t\tfree(data->default_object_filter);\n     ++\t\tdata->default_object_filter = xstrdup(value);\n      +\t}\n     ++\treturn 0;\n     ++}\n     ++\n     ++/*\n     ++ * Look up clone.<url>.defaultObjectFilter using the urlmatch\n     ++ * infrastructure.  Only URL-qualified forms are supported; a bare\n     ++ * clone.defaultObjectFilter (without a URL) is ignored.\n     ++ */\n     ++static char *get_default_object_filter(const char *url)\n     ++{\n     ++\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n     ++\tstruct clone_filter_data data = { 0 };\n     ++\tstruct string_list_item *item;\n     ++\tchar *normalized_url;\n     ++\n     ++\tconfig.section = \"clone\";\n     ++\tconfig.key = \"defaultobjectfilter\";\n     ++\tconfig.collect_fn = clone_filter_collect;\n     ++\tconfig.cascade_fn = git_clone_config;\n     ++\tconfig.cb = &data;\n     ++\n     ++\tnormalized_url = url_normalize(url, &config.url);\n     ++\n     ++\trepo_config(the_repository, urlmatch_config_entry, &config);\n     ++\tfree(normalized_url);\n      +\n     - \tif (argc > 2)\n     - \t\tusage_msg_opt(_(\"Too many arguments.\"),\n     - \t\t\tbuiltin_clone_usage, builtin_clone_options);\n     -@@ builtin/clone.c: int cmd_clone(int argc,\n     - \t\t       ref_storage_format);\n     - \n     - \tlist_objects_filter_release(&filter_options);\n     -+\tfree(config_blob_size_limit);\n     - \n     - \tstring_list_clear(&option_not, 0);\n     - \tstring_list_clear(&option_config, 0);\n     -\n     - ## builtin/fetch.c ##\n     -@@ builtin/fetch.c: struct fetch_config {\n     - \tint recurse_submodules;\n     - \tint parallel;\n     - \tint submodule_fetch_jobs;\n     -+\tchar *blob_size_limit;\n     - };\n     - \n     - static int git_fetch_config(const char *k, const char *v,\n     -@@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,\n     - \t\treturn 0;\n     - \t}\n     - \n     -+\tif (!strcmp(k, \"fetch.blobsizelimit\"))\n     -+\t\treturn git_config_string(&fetch_config->blob_size_limit, k, v);\n     -+\n     - \tif (!strcmp(k, \"fetch.output\")) {\n     - \t\tif (!v)\n     - \t\t\treturn config_error_nonbool(k);\n     -@@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children,\n     -  * or inherit the default filter-spec from the config.\n     -  */\n     - static inline void fetch_one_setup_partial(struct remote *remote,\n     --\t\t\t\t\t   struct list_objects_filter_options *filter_options)\n     -+\t\t\t\t\t   struct list_objects_filter_options *filter_options,\n     -+\t\t\t\t\t   const struct fetch_config *config)\n     - {\n     - \t/*\n     - \t * Explicit --no-filter argument overrides everything, regardless\n     -@@ builtin/fetch.c: static inline void fetch_one_setup_partial(struct remote *remote,\n     - \t\treturn;\n     - \n     - \t/*\n     --\t * If no prior partial clone/fetch and the current fetch DID NOT\n     --\t * request a partial-fetch, do a normal fetch.\n     -+\t * If no prior partial clone/fetch, the current fetch did not\n     -+\t * request a partial-fetch, and no global blob size limit is\n     -+\t * configured, do a normal fetch.\n     - \t */\n     --\tif (!repo_has_promisor_remote(the_repository) && !filter_options->choice)\n     -+\tif (!repo_has_promisor_remote(the_repository) &&\n     -+\t    !filter_options->choice && !config->blob_size_limit)\n     - \t\treturn;\n     - \n     - \t/*\n     -@@ builtin/fetch.c: static inline void fetch_one_setup_partial(struct remote *remote,\n     - \t/*\n     - \t * Do a partial-fetch from the promisor remote using either the\n     - \t * explicitly given filter-spec or inherit the filter-spec from\n     --\t * the config.\n     -+\t * the per-remote config.\n     ++\t/*\n     ++\t * Reject the bare form clone.defaultObjectFilter (no URL\n     ++\t * subsection).  urlmatch stores the best match in vars with\n     ++\t * hostmatch_len == 0 for non-URL-qualified entries; discard\n     ++\t * the result if that is what we got.\n      +\t */\n     -+\tif (repo_has_promisor_remote(the_repository)) {\n     -+\t\tpartial_clone_get_default_filter_spec(filter_options,\n     -+\t\t\t\t\t\t      remote->name);\n     -+\t\tif (filter_options->choice)\n     -+\t\t\treturn;\n     ++\titem = string_list_lookup(&config.vars, \"defaultobjectfilter\");\n     ++\tif (item) {\n     ++\t\tconst struct urlmatch_item *m = item->util;\n     ++\t\tif (!m->hostmatch_len && !m->pathmatch_len) {\n     ++\t\t\tFREE_AND_NULL(data.default_object_filter);\n     ++\t\t}\n      +\t}\n      +\n     -+\t/*\n     -+\t * Fall back to the global fetch.blobSizeLimit config. This\n     -+\t * enables partial clone behavior without requiring --filter\n     -+\t * on the command line or a pre-existing promisor remote.\n     - \t */\n     --\tif (!filter_options->choice)\n     --\t\tpartial_clone_get_default_filter_spec(filter_options, remote->name);\n     --\treturn;\n     -+\tif (!filter_options->choice && config->blob_size_limit) {\n     -+\t\tstruct strbuf buf = STRBUF_INIT;\n     -+\t\tstrbuf_addf(&buf, \"blob:limit=%s\", config->blob_size_limit);\n     -+\t\tparse_list_objects_filter(filter_options, buf.buf);\n     -+\t\tstrbuf_release(&buf);\n     -+\t\tpartial_clone_register(remote->name, filter_options);\n     -+\t}\n     - }\n     ++\turlmatch_config_release(&config);\n     ++\n     ++\treturn data.default_object_filter;\n     ++}\n     ++\n     + static int write_one_config(const char *key, const char *value,\n     + \t\t\t    const struct config_context *ctx,\n     + \t\t\t    void *data)\n     +@@ builtin/clone.c: int cmd_clone(int argc,\n     + \t} else\n     + \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n       \n     - static int fetch_one(struct remote *remote, int argc, const char **argv,\n     -@@ builtin/fetch.c: int cmd_fetch(int argc,\n     - \t\toidset_clear(&acked_commits);\n     - \t\ttrace2_region_leave(\"fetch\", \"negotiate-only\", the_repository);\n     - \t} else if (remote) {\n     --\t\tif (filter_options.choice || repo_has_promisor_remote(the_repository)) {\n     -+\t\tif (filter_options.choice || repo_has_promisor_remote(the_repository) ||\n     -+\t\t    config.blob_size_limit) {\n     - \t\t\ttrace2_region_enter(\"fetch\", \"setup-partial\", the_repository);\n     --\t\t\tfetch_one_setup_partial(remote, &filter_options);\n     -+\t\t\tfetch_one_setup_partial(remote, &filter_options, &config);\n     - \t\t\ttrace2_region_leave(\"fetch\", \"setup-partial\", the_repository);\n     - \t\t}\n     - \t\ttrace2_region_enter(\"fetch\", \"fetch-one\", the_repository);\n     -@@ builtin/fetch.c: int cmd_fetch(int argc,\n     -  cleanup:\n     - \tstring_list_clear(&list, 0);\n     - \tlist_objects_filter_release(&filter_options);\n     -+\tfree(config.blob_size_limit);\n     - \treturn result;\n     - }\n     ++\tif (!filter_options.choice) {\n     ++\t\tchar *config_filter = get_default_object_filter(repo);\n     ++\t\tif (config_filter) {\n     ++\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n     ++\t\t\tfree(config_filter);\n     ++\t\t}\n     ++\t}\n     ++\n     + \t/* no need to be strict, transport_set_option() will validate it again */\n     + \tif (option_depth && atoi(option_depth) < 1)\n     + \t\tdie(_(\"depth %s is not a positive number\"), option_depth);\n      \n       ## t/t5616-partial-clone.sh ##\n      @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non-promisor commits, gc work\n       \tgit -C partial gc --prune=now\n       '\n       \n     -+# Test fetch.blobSizeLimit config\n     ++# Test clone.<url>.defaultObjectFilter config\n     ++\n     ++test_expect_success 'setup for clone.defaultObjectFilter tests' '\n     ++\tgit init default-filter-src &&\n     ++\techo \"small\" >default-filter-src/small.txt &&\n     ++\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n     ++\tgit -C default-filter-src add . &&\n     ++\tgit -C default-filter-src commit -m \"initial\" &&\n     ++\n     ++\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n     ++\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n     ++\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n     ++'\n      +\n     -+test_expect_success 'setup for fetch.blobSizeLimit tests' '\n     -+\tgit init blob-limit-src &&\n     -+\techo \"small\" >blob-limit-src/small.txt &&\n     -+\tdd if=/dev/zero of=blob-limit-src/large.bin bs=1024 count=100 2>/dev/null &&\n     -+\tgit -C blob-limit-src add . &&\n     -+\tgit -C blob-limit-src commit -m \"initial\" &&\n     ++test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n     ++\t\t\"$SERVER_URL\" default-filter-clone &&\n      +\n     -+\tgit clone --bare \"file://$(pwd)/blob-limit-src\" blob-limit-srv.bare &&\n     -+\tgit -C blob-limit-srv.bare config --local uploadpack.allowfilter 1 &&\n     -+\tgit -C blob-limit-srv.bare config --local uploadpack.allowanysha1inwant 1\n     ++\ttest \"$(git -C default-filter-clone config --local remote.origin.promisor)\" = \"true\" &&\n     ++\ttest \"$(git -C default-filter-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n      +'\n      +\n     -+test_expect_success 'clone with fetch.blobSizeLimit config applies filter' '\n     -+\tgit -c fetch.blobSizeLimit=1k clone \\\n     -+\t\t\"file://$(pwd)/blob-limit-srv.bare\" blob-limit-clone &&\n     ++test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n     ++\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n      +\n     -+\ttest \"$(git -C blob-limit-clone config --local remote.origin.promisor)\" = \"true\" &&\n     -+\ttest \"$(git -C blob-limit-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n     ++\ttest \"$(git -C default-filter-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n      +'\n      +\n     -+test_expect_success 'clone with --filter overrides fetch.blobSizeLimit' '\n     -+\tgit -c fetch.blobSizeLimit=1k clone --filter=blob:none \\\n     -+\t\t\"file://$(pwd)/blob-limit-srv.bare\" blob-limit-override &&\n     ++test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n     ++\t\t\"$SERVER_URL\" default-filter-blobnone &&\n      +\n     -+\ttest \"$(git -C blob-limit-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n     ++\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n      +'\n      +\n     -+test_expect_success 'fetch with fetch.blobSizeLimit registers promisor remote' '\n     -+\tgit clone --no-checkout \"file://$(pwd)/blob-limit-srv.bare\" blob-limit-fetch &&\n     ++test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n     ++\t\t\"$SERVER_URL\" default-filter-tree0 &&\n      +\n     -+\t# Sanity: not yet a partial clone\n     -+\ttest_must_fail git -C blob-limit-fetch config --local remote.origin.promisor &&\n     -+\n     -+\t# Add a new commit to the server\n     -+\techo \"new-small\" >blob-limit-src/new-small.txt &&\n     -+\tdd if=/dev/zero of=blob-limit-src/new-large.bin bs=1024 count=100 2>/dev/null &&\n     -+\tgit -C blob-limit-src add . &&\n     -+\tgit -C blob-limit-src commit -m \"second\" &&\n     -+\tgit -C blob-limit-src push \"file://$(pwd)/blob-limit-srv.bare\" main &&\n     ++\ttest \"$(git -C default-filter-tree0 config --local remote.origin.promisor)\" = \"true\" &&\n     ++\ttest \"$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)\" = \"tree:0\"\n     ++'\n      +\n     -+\t# Fetch with the config set\n     -+\tgit -C blob-limit-fetch -c fetch.blobSizeLimit=1k fetch origin &&\n     ++test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit \\\n     ++\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n     ++\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n     ++\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n      +\n     -+\ttest \"$(git -C blob-limit-fetch config --local remote.origin.promisor)\" = \"true\" &&\n     -+\ttest \"$(git -C blob-limit-fetch config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n     ++\ttest \"$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n      +'\n      +\n     -+test_expect_success 'fetch.blobSizeLimit does not override existing partialclonefilter' '\n     -+\tgit clone --filter=blob:none \\\n     -+\t\t\"file://$(pwd)/blob-limit-srv.bare\" blob-limit-existing &&\n     ++test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n     ++\tgit \\\n     ++\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n     ++\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n      +\n     -+\ttest \"$(git -C blob-limit-existing config --local remote.origin.partialclonefilter)\" = \"blob:none\" &&\n     ++\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n     ++'\n      +\n     -+\t# Fetch with a different blobSizeLimit; existing filter should win\n     -+\tgit -C blob-limit-existing -c fetch.blobSizeLimit=1k fetch origin &&\n     ++test_expect_success 'bare clone.defaultObjectFilter without URL is ignored' '\n     ++\tgit -c clone.defaultObjectFilter=blob:none \\\n     ++\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n      +\n     -+\ttest \"$(git -C blob-limit-existing config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\ttest_must_fail git -C default-filter-bare-key config --local remote.origin.promisor\n      +'\n       \n       . \"$TEST_DIRECTORY\"/lib-httpd.sh\n\n\n Documentation/config/clone.adoc | 26 ++++++++++++\n builtin/clone.c                 | 68 ++++++++++++++++++++++++++++++\n t/t5616-partial-clone.sh        | 73 +++++++++++++++++++++++++++++++++\n 3 files changed, 167 insertions(+)\n\ndiff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\nindex 0a10efd174..5805ab51c2 100644\n--- a/Documentation/config/clone.adoc\n+++ b/Documentation/config/clone.adoc\n@@ -21,3 +21,29 @@ endif::[]\n \tIf a partial clone filter is provided (see `--filter` in\n \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n \tthe filter to submodules.\n+\n+`clone.<url>.defaultObjectFilter`::\n+\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n+\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n+\tuse `--filter=<value>` when the clone URL matches `<url>`.\n+\tObjects matching the filter are excluded from the initial\n+\ttransfer and lazily fetched on demand (e.g., during checkout).\n+\tSubsequent fetches inherit the filter via the per-remote config\n+\tthat is written during the clone.\n++\n+The URL matching follows the same rules as `http.<url>.*` (see\n+linkgit:git-config[1]).  The most specific URL match wins.  You can\n+match a complete domain, a namespace, or a specific project:\n++\n+----\n+[clone \"https://github.com/\"]\n+    defaultObjectFilter = blob:limit=5m\n+\n+[clone \"https://internal.corp.com/large-project/\"]\n+    defaultObjectFilter = blob:none\n+----\n++\n+An explicit `--filter` option on the command line takes precedence\n+over this config.  Only affects the initial clone; it has no effect\n+on later fetches into an existing repository.  If the server does\n+not support object filtering, the setting is silently ignored.\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 45d8fa0eed..5e20b5343d 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -44,6 +44,7 @@\n #include \"path.h\"\n #include \"pkt-line.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"urlmatch.h\"\n #include \"hook.h\"\n #include \"bundle.h\"\n #include \"bundle-uri.h\"\n@@ -757,6 +758,65 @@ static int git_clone_config(const char *k, const char *v,\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n+struct clone_filter_data {\n+\tchar *default_object_filter;\n+};\n+\n+static int clone_filter_collect(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx UNUSED,\n+\t\t\t\tvoid *cb)\n+{\n+\tstruct clone_filter_data *data = cb;\n+\n+\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n+\t\tfree(data->default_object_filter);\n+\t\tdata->default_object_filter = xstrdup(value);\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Look up clone.<url>.defaultObjectFilter using the urlmatch\n+ * infrastructure.  Only URL-qualified forms are supported; a bare\n+ * clone.defaultObjectFilter (without a URL) is ignored.\n+ */\n+static char *get_default_object_filter(const char *url)\n+{\n+\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n+\tstruct clone_filter_data data = { 0 };\n+\tstruct string_list_item *item;\n+\tchar *normalized_url;\n+\n+\tconfig.section = \"clone\";\n+\tconfig.key = \"defaultobjectfilter\";\n+\tconfig.collect_fn = clone_filter_collect;\n+\tconfig.cascade_fn = git_clone_config;\n+\tconfig.cb = &data;\n+\n+\tnormalized_url = url_normalize(url, &config.url);\n+\n+\trepo_config(the_repository, urlmatch_config_entry, &config);\n+\tfree(normalized_url);\n+\n+\t/*\n+\t * Reject the bare form clone.defaultObjectFilter (no URL\n+\t * subsection).  urlmatch stores the best match in vars with\n+\t * hostmatch_len == 0 for non-URL-qualified entries; discard\n+\t * the result if that is what we got.\n+\t */\n+\titem = string_list_lookup(&config.vars, \"defaultobjectfilter\");\n+\tif (item) {\n+\t\tconst struct urlmatch_item *m = item->util;\n+\t\tif (!m->hostmatch_len && !m->pathmatch_len) {\n+\t\t\tFREE_AND_NULL(data.default_object_filter);\n+\t\t}\n+\t}\n+\n+\turlmatch_config_release(&config);\n+\n+\treturn data.default_object_filter;\n+}\n+\n static int write_one_config(const char *key, const char *value,\n \t\t\t    const struct config_context *ctx,\n \t\t\t    void *data)\n@@ -1057,6 +1117,14 @@ int cmd_clone(int argc,\n \t} else\n \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n \n+\tif (!filter_options.choice) {\n+\t\tchar *config_filter = get_default_object_filter(repo);\n+\t\tif (config_filter) {\n+\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n+\t\t\tfree(config_filter);\n+\t\t}\n+\t}\n+\n \t/* no need to be strict, transport_set_option() will validate it again */\n \tif (option_depth && atoi(option_depth) < 1)\n \t\tdie(_(\"depth %s is not a positive number\"), option_depth);\ndiff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\nindex 1e354e057f..33010f3b7d 100755\n--- a/t/t5616-partial-clone.sh\n+++ b/t/t5616-partial-clone.sh\n@@ -722,6 +722,79 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n \tgit -C partial gc --prune=now\n '\n \n+# Test clone.<url>.defaultObjectFilter config\n+\n+test_expect_success 'setup for clone.defaultObjectFilter tests' '\n+\tgit init default-filter-src &&\n+\techo \"small\" >default-filter-src/small.txt &&\n+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n+\tgit -C default-filter-src add . &&\n+\tgit -C default-filter-src commit -m \"initial\" &&\n+\n+\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-clone &&\n+\n+\ttest \"$(git -C default-filter-clone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n+'\n+\n+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n+\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n+\n+\ttest \"$(git -C default-filter-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-blobnone &&\n+\n+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-tree0 &&\n+\n+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)\" = \"tree:0\"\n+'\n+\n+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n+\n+\ttest \"$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n+\tgit \\\n+\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n+\n+\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n+'\n+\n+test_expect_success 'bare clone.defaultObjectFilter without URL is ignored' '\n+\tgit -c clone.defaultObjectFilter=blob:none \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n+\n+\ttest_must_fail git -C default-filter-bare-key config --local remote.origin.promisor\n+'\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\n-- \ngitgitgadget\n"},{"id":"537988","messageId":"xmqqcy1i3xt4.fsf@gitster.g","threadId":"65110","inReplyTo":"pull.2058.v2.git.1772672251281.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] clone: add clone.<url>.defaultObjectFilter config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-05T19:01:43Z","receivedAt":"2026-03-05T19:01:46Z","isPatch":true,"body":"\"Alan Braithwaite via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Alan Braithwaite <alan@braithwaite.dev>\n>\n> Add a new configuration option that lets users specify a default\n> partial clone filter per URL pattern.  When cloning a repository\n> whose URL matches a configured pattern, git-clone automatically\n> applies the filter, equivalent to passing --filter on the command\n> line.\n>\n>     [clone \"https://github.com/\"]\n>         defaultObjectFilter = blob:limit=5m\n>\n>     [clone \"https://internal.corp.com/large-project/\"]\n>         defaultObjectFilter = blob:none\n>\n> URL matching uses the existing urlmatch_config_entry() infrastructure,\n> following the same rules as http.<url>.* — you can match a domain,\n> a namespace path, or a specific project, and the most specific match\n> wins.\n>\n> The config only affects the initial clone.  Once the clone completes,\n> the filter is recorded in remote.<name>.partialCloneFilter, so\n> subsequent fetches inherit it automatically.  An explicit --filter\n> flag on the command line takes precedence.\n\nThe motivation behind the change is clearly described.  Reusing the\nexisting urlmatch_config_entry() infrastructure is very appropriate\nas it makes the feature intuitive for those familiar with\nhttp.<url>.* settings.\n\n> Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n> honored; a bare clone.defaultObjectFilter without a URL subsection\n> is ignored.\n\nThis is unlike how http.<url>.<var> configuration variables work,\nand while I can see that server operators may not want to see users\nset clone.defaultObjectFilter and affect traffic with _all_ sites, I\nam afraid that this design choice may appear a bit counter-intuitive\nto end users.\n\n\n> Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n\n>  Documentation/config/clone.adoc | 26 ++++++++++++\n>  builtin/clone.c                 | 68 ++++++++++++++++++++++++++++++\n>  t/t5616-partial-clone.sh        | 73 +++++++++++++++++++++++++++++++++\n>  3 files changed, 167 insertions(+)\n>\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 45d8fa0eed..5e20b5343d 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -44,6 +44,7 @@\n>  #include \"path.h\"\n>  #include \"pkt-line.h\"\n>  #include \"list-objects-filter-options.h\"\n> +#include \"urlmatch.h\"\n>  #include \"hook.h\"\n>  #include \"bundle.h\"\n>  #include \"bundle-uri.h\"\n> @@ -757,6 +758,65 @@ static int git_clone_config(const char *k, const char *v,\n>  \treturn git_default_config(k, v, ctx, cb);\n>  }\n>  \n> +struct clone_filter_data {\n> +\tchar *default_object_filter;\n> +};\n> +\n> +static int clone_filter_collect(const char *var, const char *value,\n> +\t\t\t\tconst struct config_context *ctx UNUSED,\n> +\t\t\t\tvoid *cb)\n> +{\n> +\tstruct clone_filter_data *data = cb;\n> +\n> +\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n> +\t\tfree(data->default_object_filter);\n> +\t\tdata->default_object_filter = xstrdup(value);\n> +\t}\n> +\treturn 0;\n> +}\n\nThis will segfault with a \"value-less truth\", i.e.,\n\n\t[clone \"<URL>\"]\n\t\tdefaultObjectFilter\n\nso there should be \n\n\t\tif (!value)\n\t\t\treturn config_error_nonbool(var);\n\nin it.\n\nI cannot convince myself that a new structure only to hold a single\n\"char *\" member is not over-engineering.  Wouldn't it work equally\nwell (unless you have an immediate plan to add more members to the\nstruct, that is):\n\n\tchar **filter_spec_p = cb;\n\n\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n\t\tif (!value)\n\t\t\tretgurn config_error_nonbool(var);\n\t\tfree(*filter_spec_p);\n\t\t*filter_spec_p = xstrdup(value);\n\t}\n\treturn 0;\n\n> +/*\n> + * Look up clone.<url>.defaultObjectFilter using the urlmatch\n> + * infrastructure.  Only URL-qualified forms are supported; a bare\n> + * clone.defaultObjectFilter (without a URL) is ignored.\n> + */\n> +static char *get_default_object_filter(const char *url)\n> +{\n> +\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n> +\tstruct clone_filter_data data = { 0 };\n> +\tstruct string_list_item *item;\n> +\tchar *normalized_url;\n> +\n> +\tconfig.section = \"clone\";\n> +\tconfig.key = \"defaultobjectfilter\";\n> +\tconfig.collect_fn = clone_filter_collect;\n> +\tconfig.cascade_fn = git_clone_config;\n> +\tconfig.cb = &data;\n> +\n> +\tnormalized_url = url_normalize(url, &config.url);\n> +\n> +\trepo_config(the_repository, urlmatch_config_entry, &config);\n> +\tfree(normalized_url);\n\nThis forces a second full scan of the configuration space.  But it\ncannot be avoided, because the existing repo_config() call has to\nhappen early before we call parse_options() to give us the\nconfigured default to overwrite with the command line, and we would\nnot know what our URL is before we called parse_options().\n\nHowever, I thihk you want to leave the .cascade_fn NULL; you do not\nwant urlmatch_config_entry() to call git_clone_config() AGAIN on the\nconfiguration variables, as the first call to repo_config() before\nwe call parse_options() should have already handled them, no?\n\nThanks.\n"},{"id":"538016","messageId":"f0521097-1ec6-4e47-88d5-8c5be47ded3b@app.fastmail.com","threadId":"65110","inReplyTo":"xmqqcy1i3xt4.fsf@gitster.g","subject":"Re: [PATCH v2] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite","fromEmail":"alan@braithwaite.dev","sentAt":"2026-03-05T23:11:41Z","receivedAt":"2026-03-05T23:12:03Z","isPatch":true,"body":"Junio C Hamano wrote:\n> This is unlike how http.<url>.<var> configuration variables work,\n> and while I can see that server operators may not want to see users\n> set clone.defaultObjectFilter and affect traffic with _all_ sites, I\n> am afraid that this design choice may appear a bit counter-intuitive\n> to end users.\n\nFunny enough, I actually prefer that but gathered from the previous\ncommentary that it wasn't desired.  I'd be more than content to add it.\n\nJunio C Hamano wrote:\n> I cannot convince myself that a new structure only to hold a single\n> \"char *\" member is not over-engineering.  Wouldn't it work equally\n> well (unless you have an immediate plan to add more members to the\n> struct, that is):\n\nYou're right, it's been a while I've written C.  Thanks for catching\nthat.  I think my mind was going somewhere else with it, but YAGNI.\n\nJunio C Hamano wrote:\n> However, I think you want to leave the .cascade_fn NULL; you do not\n> want urlmatch_config_entry() to call git_clone_config() AGAIN on the\n> configuration variables, as the first call to repo_config() before\n> we call parse_options() should have already handled them, no?\n\nGood catch. I'll fix it. Will set cascade_fn to NULL so the second\npass only looks at clone.<url>.defaultObjectFilter entries.\n\nThanks for the review and for your patience as I shake the gopher\nout of me and figure out how to do real programming again.\n\nThanks,\n- Alan\n\nOn Thu, Mar 5, 2026, at 11:01, Junio C Hamano wrote:\n> \"Alan Braithwaite via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Alan Braithwaite <alan@braithwaite.dev>\n>>\n>> Add a new configuration option that lets users specify a default\n>> partial clone filter per URL pattern.  When cloning a repository\n>> whose URL matches a configured pattern, git-clone automatically\n>> applies the filter, equivalent to passing --filter on the command\n>> line.\n>>\n>>     [clone \"https://github.com/\"]\n>>         defaultObjectFilter = blob:limit=5m\n>>\n>>     [clone \"https://internal.corp.com/large-project/\"]\n>>         defaultObjectFilter = blob:none\n>>\n>> URL matching uses the existing urlmatch_config_entry() infrastructure,\n>> following the same rules as http.<url>.* — you can match a domain,\n>> a namespace path, or a specific project, and the most specific match\n>> wins.\n>>\n>> The config only affects the initial clone.  Once the clone completes,\n>> the filter is recorded in remote.<name>.partialCloneFilter, so\n>> subsequent fetches inherit it automatically.  An explicit --filter\n>> flag on the command line takes precedence.\n>\n> The motivation behind the change is clearly described.  Reusing the\n> existing urlmatch_config_entry() infrastructure is very appropriate\n> as it makes the feature intuitive for those familiar with\n> http.<url>.* settings.\n>\n>> Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n>> honored; a bare clone.defaultObjectFilter without a URL subsection\n>> is ignored.\n>\n> This is unlike how http.<url>.<var> configuration variables work,\n> and while I can see that server operators may not want to see users\n> set clone.defaultObjectFilter and affect traffic with _all_ sites, I\n> am afraid that this design choice may appear a bit counter-intuitive\n> to end users.\n>\n>\n>> Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n>\n>>  Documentation/config/clone.adoc | 26 ++++++++++++\n>>  builtin/clone.c                 | 68 ++++++++++++++++++++++++++++++\n>>  t/t5616-partial-clone.sh        | 73 +++++++++++++++++++++++++++++++++\n>>  3 files changed, 167 insertions(+)\n>>\n>> diff --git a/builtin/clone.c b/builtin/clone.c\n>> index 45d8fa0eed..5e20b5343d 100644\n>> --- a/builtin/clone.c\n>> +++ b/builtin/clone.c\n>> @@ -44,6 +44,7 @@\n>>  #include \"path.h\"\n>>  #include \"pkt-line.h\"\n>>  #include \"list-objects-filter-options.h\"\n>> +#include \"urlmatch.h\"\n>>  #include \"hook.h\"\n>>  #include \"bundle.h\"\n>>  #include \"bundle-uri.h\"\n>> @@ -757,6 +758,65 @@ static int git_clone_config(const char *k, const char *v,\n>>  \treturn git_default_config(k, v, ctx, cb);\n>>  }\n>>  \n>> +struct clone_filter_data {\n>> +\tchar *default_object_filter;\n>> +};\n>> +\n>> +static int clone_filter_collect(const char *var, const char *value,\n>> +\t\t\t\tconst struct config_context *ctx UNUSED,\n>> +\t\t\t\tvoid *cb)\n>> +{\n>> +\tstruct clone_filter_data *data = cb;\n>> +\n>> +\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n>> +\t\tfree(data->default_object_filter);\n>> +\t\tdata->default_object_filter = xstrdup(value);\n>> +\t}\n>> +\treturn 0;\n>> +}\n>\n> This will segfault with a \"value-less truth\", i.e.,\n>\n> \t[clone \"<URL>\"]\n> \t\tdefaultObjectFilter\n>\n> so there should be \n>\n> \t\tif (!value)\n> \t\t\treturn config_error_nonbool(var);\n>\n> in it.\n>\n> I cannot convince myself that a new structure only to hold a single\n> \"char *\" member is not over-engineering.  Wouldn't it work equally\n> well (unless you have an immediate plan to add more members to the\n> struct, that is):\n>\n> \tchar **filter_spec_p = cb;\n>\n> \tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n> \t\tif (!value)\n> \t\t\tretgurn config_error_nonbool(var);\n> \t\tfree(*filter_spec_p);\n> \t\t*filter_spec_p = xstrdup(value);\n> \t}\n> \treturn 0;\n>\n>> +/*\n>> + * Look up clone.<url>.defaultObjectFilter using the urlmatch\n>> + * infrastructure.  Only URL-qualified forms are supported; a bare\n>> + * clone.defaultObjectFilter (without a URL) is ignored.\n>> + */\n>> +static char *get_default_object_filter(const char *url)\n>> +{\n>> +\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n>> +\tstruct clone_filter_data data = { 0 };\n>> +\tstruct string_list_item *item;\n>> +\tchar *normalized_url;\n>> +\n>> +\tconfig.section = \"clone\";\n>> +\tconfig.key = \"defaultobjectfilter\";\n>> +\tconfig.collect_fn = clone_filter_collect;\n>> +\tconfig.cascade_fn = git_clone_config;\n>> +\tconfig.cb = &data;\n>> +\n>> +\tnormalized_url = url_normalize(url, &config.url);\n>> +\n>> +\trepo_config(the_repository, urlmatch_config_entry, &config);\n>> +\tfree(normalized_url);\n>\n> This forces a second full scan of the configuration space.  But it\n> cannot be avoided, because the existing repo_config() call has to\n> happen early before we call parse_options() to give us the\n> configured default to overwrite with the command line, and we would\n> not know what our URL is before we called parse_options().\n>\n> However, I thihk you want to leave the .cascade_fn NULL; you do not\n> want urlmatch_config_entry() to call git_clone_config() AGAIN on the\n> configuration variables, as the first call to repo_config() before\n> we call parse_options() should have already handled them, no?\n>\n> Thanks.\n"},{"id":"538064","messageId":"pull.2058.v3.git.1772780113400.gitgitgadget@gmail.com","threadId":"65110","inReplyTo":"pull.2058.v2.git.1772672251281.gitgitgadget@gmail.com","subject":"[PATCH v3] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-06T06:55:13Z","receivedAt":"2026-03-06T06:55:18Z","isPatch":true,"body":"From: Alan Braithwaite <alan@braithwaite.dev>\n\nAdd a new configuration option that lets users specify a default\npartial clone filter per URL pattern.  When cloning a repository\nwhose URL matches a configured pattern, git-clone automatically\napplies the filter, equivalent to passing --filter on the command\nline.\n\n    [clone \"https://github.com/\"]\n        defaultObjectFilter = blob:limit=5m\n\n    [clone \"https://internal.corp.com/large-project/\"]\n        defaultObjectFilter = blob:none\n\nURL matching uses the existing urlmatch_config_entry() infrastructure,\nfollowing the same rules as http.<url>.* — you can match a domain,\na namespace path, or a specific project, and the most specific match\nwins.\n\nThe config only affects the initial clone.  Once the clone completes,\nthe filter is recorded in remote.<name>.partialCloneFilter, so\nsubsequent fetches inherit it automatically.  An explicit --filter\nflag on the command line takes precedence.\n\nOnly the URL-qualified form (clone.<url>.defaultObjectFilter) is\nhonored; a bare clone.defaultObjectFilter without a URL subsection\nis ignored.\n\nSigned-off-by: Alan Braithwaite <alan@braithwaite.dev>\n---\n    fetch, clone: add fetch.blobSizeLimit config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2058\n\nRange-diff vs v2:\n\n 1:  4a73edd2e8 ! 1:  5408412f2a clone: add clone.<url>.defaultObjectFilter config\n     @@ Documentation/config/clone.adoc: endif::[]\n       \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n       \tthe filter to submodules.\n      +\n     ++`clone.defaultObjectFilter`::\n      +`clone.<url>.defaultObjectFilter`::\n      +\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n      +\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n     -+\tuse `--filter=<value>` when the clone URL matches `<url>`.\n     ++\tuse `--filter=<value>` to enable partial clone behavior.\n      +\tObjects matching the filter are excluded from the initial\n      +\ttransfer and lazily fetched on demand (e.g., during checkout).\n      +\tSubsequent fetches inherit the filter via the per-remote config\n      +\tthat is written during the clone.\n      ++\n     -+The URL matching follows the same rules as `http.<url>.*` (see\n     -+linkgit:git-config[1]).  The most specific URL match wins.  You can\n     -+match a complete domain, a namespace, or a specific project:\n     ++The bare `clone.defaultObjectFilter` applies to all clones.  The\n     ++URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n     ++setting to clones whose URL matches `<url>`, following the same\n     ++rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n     ++specific URL match wins.  You can match a domain, a namespace, or a\n     ++specific project:\n      ++\n      +----\n     ++[clone]\n     ++    defaultObjectFilter = blob:limit=1m\n     ++\n      +[clone \"https://github.com/\"]\n      +    defaultObjectFilter = blob:limit=5m\n      +\n     @@ builtin/clone.c: static int git_clone_config(const char *k, const char *v,\n       \treturn git_default_config(k, v, ctx, cb);\n       }\n       \n     -+struct clone_filter_data {\n     -+\tchar *default_object_filter;\n     -+};\n     -+\n      +static int clone_filter_collect(const char *var, const char *value,\n      +\t\t\t\tconst struct config_context *ctx UNUSED,\n      +\t\t\t\tvoid *cb)\n      +{\n     -+\tstruct clone_filter_data *data = cb;\n     ++\tchar **filter_spec_p = cb;\n      +\n      +\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n     -+\t\tfree(data->default_object_filter);\n     -+\t\tdata->default_object_filter = xstrdup(value);\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n     ++\t\tfree(*filter_spec_p);\n     ++\t\t*filter_spec_p = xstrdup(value);\n      +\t}\n      +\treturn 0;\n      +}\n      +\n      +/*\n     -+ * Look up clone.<url>.defaultObjectFilter using the urlmatch\n     -+ * infrastructure.  Only URL-qualified forms are supported; a bare\n     -+ * clone.defaultObjectFilter (without a URL) is ignored.\n     ++ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n     ++ * using the urlmatch infrastructure.  A URL-qualified entry that matches\n     ++ * the clone URL takes precedence over the bare form, following the same\n     ++ * rules as http.<url>.* configuration variables.\n      + */\n      +static char *get_default_object_filter(const char *url)\n      +{\n      +\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n     -+\tstruct clone_filter_data data = { 0 };\n     -+\tstruct string_list_item *item;\n     ++\tchar *filter_spec = NULL;\n      +\tchar *normalized_url;\n      +\n      +\tconfig.section = \"clone\";\n      +\tconfig.key = \"defaultobjectfilter\";\n      +\tconfig.collect_fn = clone_filter_collect;\n     -+\tconfig.cascade_fn = git_clone_config;\n     -+\tconfig.cb = &data;\n     ++\tconfig.cb = &filter_spec;\n      +\n      +\tnormalized_url = url_normalize(url, &config.url);\n      +\n      +\trepo_config(the_repository, urlmatch_config_entry, &config);\n      +\tfree(normalized_url);\n     -+\n     -+\t/*\n     -+\t * Reject the bare form clone.defaultObjectFilter (no URL\n     -+\t * subsection).  urlmatch stores the best match in vars with\n     -+\t * hostmatch_len == 0 for non-URL-qualified entries; discard\n     -+\t * the result if that is what we got.\n     -+\t */\n     -+\titem = string_list_lookup(&config.vars, \"defaultobjectfilter\");\n     -+\tif (item) {\n     -+\t\tconst struct urlmatch_item *m = item->util;\n     -+\t\tif (!m->hostmatch_len && !m->pathmatch_len) {\n     -+\t\t\tFREE_AND_NULL(data.default_object_filter);\n     -+\t\t}\n     -+\t}\n     -+\n      +\turlmatch_config_release(&config);\n      +\n     -+\treturn data.default_object_filter;\n     ++\treturn filter_spec;\n      +}\n      +\n       static int write_one_config(const char *key, const char *value,\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n      +'\n      +\n     -+test_expect_success 'bare clone.defaultObjectFilter without URL is ignored' '\n     ++test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '\n      +\tgit -c clone.defaultObjectFilter=blob:none \\\n      +\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n      +\n     -+\ttest_must_fail git -C default-filter-bare-key config --local remote.origin.promisor\n     ++\ttest \"$(git -C default-filter-bare-key config --local remote.origin.promisor)\" = \"true\" &&\n     ++\ttest \"$(git -C default-filter-bare-key config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++'\n     ++\n     ++test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit \\\n     ++\t\t-c clone.defaultObjectFilter=blob:limit=1k \\\n     ++\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n     ++\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n     ++\n     ++\ttest \"$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n      +'\n       \n       . \"$TEST_DIRECTORY\"/lib-httpd.sh\n\n\n Documentation/config/clone.adoc | 33 +++++++++++++\n builtin/clone.c                 | 50 ++++++++++++++++++++\n t/t5616-partial-clone.sh        | 84 +++++++++++++++++++++++++++++++++\n 3 files changed, 167 insertions(+)\n\ndiff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\nindex 0a10efd174..7ef6321be2 100644\n--- a/Documentation/config/clone.adoc\n+++ b/Documentation/config/clone.adoc\n@@ -21,3 +21,36 @@ endif::[]\n \tIf a partial clone filter is provided (see `--filter` in\n \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n \tthe filter to submodules.\n+\n+`clone.defaultObjectFilter`::\n+`clone.<url>.defaultObjectFilter`::\n+\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n+\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n+\tuse `--filter=<value>` to enable partial clone behavior.\n+\tObjects matching the filter are excluded from the initial\n+\ttransfer and lazily fetched on demand (e.g., during checkout).\n+\tSubsequent fetches inherit the filter via the per-remote config\n+\tthat is written during the clone.\n++\n+The bare `clone.defaultObjectFilter` applies to all clones.  The\n+URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n+setting to clones whose URL matches `<url>`, following the same\n+rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n+specific URL match wins.  You can match a domain, a namespace, or a\n+specific project:\n++\n+----\n+[clone]\n+    defaultObjectFilter = blob:limit=1m\n+\n+[clone \"https://github.com/\"]\n+    defaultObjectFilter = blob:limit=5m\n+\n+[clone \"https://internal.corp.com/large-project/\"]\n+    defaultObjectFilter = blob:none\n+----\n++\n+An explicit `--filter` option on the command line takes precedence\n+over this config.  Only affects the initial clone; it has no effect\n+on later fetches into an existing repository.  If the server does\n+not support object filtering, the setting is silently ignored.\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 45d8fa0eed..b549191707 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -44,6 +44,7 @@\n #include \"path.h\"\n #include \"pkt-line.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"urlmatch.h\"\n #include \"hook.h\"\n #include \"bundle.h\"\n #include \"bundle-uri.h\"\n@@ -757,6 +758,47 @@ static int git_clone_config(const char *k, const char *v,\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n+static int clone_filter_collect(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx UNUSED,\n+\t\t\t\tvoid *cb)\n+{\n+\tchar **filter_spec_p = cb;\n+\n+\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tfree(*filter_spec_p);\n+\t\t*filter_spec_p = xstrdup(value);\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n+ * using the urlmatch infrastructure.  A URL-qualified entry that matches\n+ * the clone URL takes precedence over the bare form, following the same\n+ * rules as http.<url>.* configuration variables.\n+ */\n+static char *get_default_object_filter(const char *url)\n+{\n+\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n+\tchar *filter_spec = NULL;\n+\tchar *normalized_url;\n+\n+\tconfig.section = \"clone\";\n+\tconfig.key = \"defaultobjectfilter\";\n+\tconfig.collect_fn = clone_filter_collect;\n+\tconfig.cb = &filter_spec;\n+\n+\tnormalized_url = url_normalize(url, &config.url);\n+\n+\trepo_config(the_repository, urlmatch_config_entry, &config);\n+\tfree(normalized_url);\n+\turlmatch_config_release(&config);\n+\n+\treturn filter_spec;\n+}\n+\n static int write_one_config(const char *key, const char *value,\n \t\t\t    const struct config_context *ctx,\n \t\t\t    void *data)\n@@ -1057,6 +1099,14 @@ int cmd_clone(int argc,\n \t} else\n \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n \n+\tif (!filter_options.choice) {\n+\t\tchar *config_filter = get_default_object_filter(repo);\n+\t\tif (config_filter) {\n+\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n+\t\t\tfree(config_filter);\n+\t\t}\n+\t}\n+\n \t/* no need to be strict, transport_set_option() will validate it again */\n \tif (option_depth && atoi(option_depth) < 1)\n \t\tdie(_(\"depth %s is not a positive number\"), option_depth);\ndiff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\nindex 1e354e057f..a4bfdb329e 100755\n--- a/t/t5616-partial-clone.sh\n+++ b/t/t5616-partial-clone.sh\n@@ -722,6 +722,90 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n \tgit -C partial gc --prune=now\n '\n \n+# Test clone.<url>.defaultObjectFilter config\n+\n+test_expect_success 'setup for clone.defaultObjectFilter tests' '\n+\tgit init default-filter-src &&\n+\techo \"small\" >default-filter-src/small.txt &&\n+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n+\tgit -C default-filter-src add . &&\n+\tgit -C default-filter-src commit -m \"initial\" &&\n+\n+\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-clone &&\n+\n+\ttest \"$(git -C default-filter-clone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n+'\n+\n+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n+\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n+\n+\ttest \"$(git -C default-filter-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-blobnone &&\n+\n+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-tree0 &&\n+\n+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)\" = \"tree:0\"\n+'\n+\n+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n+\n+\ttest \"$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n+\tgit \\\n+\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n+\n+\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n+'\n+\n+test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '\n+\tgit -c clone.defaultObjectFilter=blob:none \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n+\n+\ttest \"$(git -C default-filter-bare-key config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-bare-key config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c clone.defaultObjectFilter=blob:limit=1k \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n+\n+\ttest \"$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\n-- \ngitgitgadget\n"},{"id":"538066","messageId":"aaqu44_sDJYcftWd@fruit.crustytoothpaste.net","threadId":"65110","inReplyTo":"pull.2058.v3.git.1772780113400.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] clone: add clone.<url>.defaultObjectFilter config","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-06T10:39:31Z","receivedAt":"2026-03-06T10:39:39Z","isPatch":true,"body":"On 2026-03-06 at 06:55:13, Alan Braithwaite via GitGitGadget wrote:\n> From: Alan Braithwaite <alan@braithwaite.dev>\n> \n> Add a new configuration option that lets users specify a default\n> partial clone filter per URL pattern.  When cloning a repository\n> whose URL matches a configured pattern, git-clone automatically\n> applies the filter, equivalent to passing --filter on the command\n> line.\n> \n>     [clone \"https://github.com/\"]\n>         defaultObjectFilter = blob:limit=5m\n> \n>     [clone \"https://internal.corp.com/large-project/\"]\n>         defaultObjectFilter = blob:none\n> \n> URL matching uses the existing urlmatch_config_entry() infrastructure,\n> following the same rules as http.<url>.* — you can match a domain,\n> a namespace path, or a specific project, and the most specific match\n> wins.\n> \n> The config only affects the initial clone.  Once the clone completes,\n> the filter is recorded in remote.<name>.partialCloneFilter, so\n> subsequent fetches inherit it automatically.  An explicit --filter\n> flag on the command line takes precedence.\n> \n> Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n> honored; a bare clone.defaultObjectFilter without a URL subsection\n> is ignored.\n\nWe've historically not implemented default filtering for clones because\nit makes it hard to reason about the behaviour of the clone command.\nFor instance, if I have a script that clones a repository, it almost\ncertainly expects a full clone unless it requested something else.\n\nFor instance, I run `foo setup` which clones my repository and then I\nsuspend my laptop.  I go the airport and get on an airplane which lacks\nWi-Fi.  I then run `foo blargle`, which operates on the repository, but\nthat fails because it was a partial clone and I'm offline.  I didn't\nrealize this wouldn't work because I didn't know that the foo command\nrequired a full clone since it's just a script I got from my distro.\n\nWe've traditionally placed this kind of customizable configuration into\n`scalar` instead, which is designed to be configurable and set options\nfor large repositories that would want to control clone and fetch\noptions.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"538103","messageId":"xmqq1phw21op.fsf@gitster.g","threadId":"65110","inReplyTo":"aaqu44_sDJYcftWd@fruit.crustytoothpaste.net","subject":"Re: [PATCH v3] clone: add clone.<url>.defaultObjectFilter config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-06T19:33:10Z","receivedAt":"2026-03-06T19:33:13Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> We've historically not implemented default filtering for clones because\n> it makes it hard to reason about the behaviour of the clone command.\n> For instance, if I have a script that clones a repository, it almost\n> certainly expects a full clone unless it requested something else.\n> ...\n> We've traditionally placed this kind of customizable configuration into\n> `scalar` instead, which is designed to be configurable and set options\n> for large repositories that would want to control clone and fetch\n> options.\n\nHmph, my knee-jerk reaction to the early part of your message was\n\"oh, but isn't clone a Porcelain (admittedly without corresponding\nplumbing) whose defaults and end-user experiences are meant to be\nupdated from time to time to help users?\" but I didn't realize that\nwe have another class, which is \"scalar\", these days that we can add\nthese settings to.  I do not have objections to add something to\n\"scalar\", but I personally feel that the configuration for clone is\nsuch a bad thing to have.\n\nDo we have a way to defeat the configured filter to say \"no\nfiltering, we want everything\" from the command line?  If not, that\nneeds to be addressed, if we were to add this configuration.\n\nThanks.\n\n"},{"id":"538117","messageId":"pull.2058.v4.git.1772833649843.gitgitgadget@gmail.com","threadId":"65110","inReplyTo":"pull.2058.v3.git.1772780113400.gitgitgadget@gmail.com","subject":"[PATCH v4] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-06T21:47:29Z","receivedAt":"2026-03-06T21:47:32Z","isPatch":true,"body":"From: Alan Braithwaite <alan@braithwaite.dev>\n\nAdd a new configuration option that lets users specify a default\npartial clone filter per URL pattern.  When cloning a repository\nwhose URL matches a configured pattern, git-clone automatically\napplies the filter, equivalent to passing --filter on the command\nline.\n\n    [clone \"https://github.com/\"]\n        defaultObjectFilter = blob:limit=5m\n\n    [clone \"https://internal.corp.com/large-project/\"]\n        defaultObjectFilter = blob:none\n\nURL matching uses the existing urlmatch_config_entry() infrastructure,\nfollowing the same rules as http.<url>.* — you can match a domain,\na namespace path, or a specific project, and the most specific match\nwins.\n\nThe config only affects the initial clone.  Once the clone completes,\nthe filter is recorded in remote.<name>.partialCloneFilter, so\nsubsequent fetches inherit it automatically.  An explicit --filter\nflag on the command line takes precedence.\n\nOnly the URL-qualified form (clone.<url>.defaultObjectFilter) is\nhonored; a bare clone.defaultObjectFilter without a URL subsection\nis ignored.\n\nSigned-off-by: Alan Braithwaite <alan@braithwaite.dev>\n---\n    fetch, clone: add fetch.blobSizeLimit config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2058\n\nRange-diff vs v3:\n\n 1:  5408412f2a ! 1:  4bf3e1ec63 clone: add clone.<url>.defaultObjectFilter config\n     @@ Documentation/config/clone.adoc: endif::[]\n      +----\n      ++\n      +An explicit `--filter` option on the command line takes precedence\n     -+over this config.  Only affects the initial clone; it has no effect\n     -+on later fetches into an existing repository.  If the server does\n     -+not support object filtering, the setting is silently ignored.\n     ++over this config, and `--no-filter` defeats it entirely to force a\n     ++full clone.  Only affects the initial clone; it has no effect on\n     ++later fetches into an existing repository.  If the server does not\n     ++support object filtering, the setting is silently ignored.\n      \n       ## builtin/clone.c ##\n      @@\n     @@ builtin/clone.c: int cmd_clone(int argc,\n       \t} else\n       \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n       \n     -+\tif (!filter_options.choice) {\n     ++\tif (!filter_options.choice && !filter_options.no_filter) {\n      +\t\tchar *config_filter = get_default_object_filter(repo);\n      +\t\tif (config_filter) {\n      +\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n      +\n      +\ttest \"$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++'\n     ++\n     ++test_expect_success '--no-filter defeats clone.defaultObjectFilter' '\n     ++\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n     ++\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n     ++\t\tclone --no-filter \"$SERVER_URL\" default-filter-no-filter &&\n     ++\n     ++\ttest_must_fail git -C default-filter-no-filter config --local remote.origin.promisor\n      +'\n       \n       . \"$TEST_DIRECTORY\"/lib-httpd.sh\n\n\n Documentation/config/clone.adoc | 34 ++++++++++++\n builtin/clone.c                 | 50 ++++++++++++++++++\n t/t5616-partial-clone.sh        | 92 +++++++++++++++++++++++++++++++++\n 3 files changed, 176 insertions(+)\n\ndiff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\nindex 0a10efd174..1d6c0957a0 100644\n--- a/Documentation/config/clone.adoc\n+++ b/Documentation/config/clone.adoc\n@@ -21,3 +21,37 @@ endif::[]\n \tIf a partial clone filter is provided (see `--filter` in\n \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n \tthe filter to submodules.\n+\n+`clone.defaultObjectFilter`::\n+`clone.<url>.defaultObjectFilter`::\n+\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n+\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n+\tuse `--filter=<value>` to enable partial clone behavior.\n+\tObjects matching the filter are excluded from the initial\n+\ttransfer and lazily fetched on demand (e.g., during checkout).\n+\tSubsequent fetches inherit the filter via the per-remote config\n+\tthat is written during the clone.\n++\n+The bare `clone.defaultObjectFilter` applies to all clones.  The\n+URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n+setting to clones whose URL matches `<url>`, following the same\n+rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n+specific URL match wins.  You can match a domain, a namespace, or a\n+specific project:\n++\n+----\n+[clone]\n+    defaultObjectFilter = blob:limit=1m\n+\n+[clone \"https://github.com/\"]\n+    defaultObjectFilter = blob:limit=5m\n+\n+[clone \"https://internal.corp.com/large-project/\"]\n+    defaultObjectFilter = blob:none\n+----\n++\n+An explicit `--filter` option on the command line takes precedence\n+over this config, and `--no-filter` defeats it entirely to force a\n+full clone.  Only affects the initial clone; it has no effect on\n+later fetches into an existing repository.  If the server does not\n+support object filtering, the setting is silently ignored.\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 45d8fa0eed..1207655815 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -44,6 +44,7 @@\n #include \"path.h\"\n #include \"pkt-line.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"urlmatch.h\"\n #include \"hook.h\"\n #include \"bundle.h\"\n #include \"bundle-uri.h\"\n@@ -757,6 +758,47 @@ static int git_clone_config(const char *k, const char *v,\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n+static int clone_filter_collect(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx UNUSED,\n+\t\t\t\tvoid *cb)\n+{\n+\tchar **filter_spec_p = cb;\n+\n+\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tfree(*filter_spec_p);\n+\t\t*filter_spec_p = xstrdup(value);\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n+ * using the urlmatch infrastructure.  A URL-qualified entry that matches\n+ * the clone URL takes precedence over the bare form, following the same\n+ * rules as http.<url>.* configuration variables.\n+ */\n+static char *get_default_object_filter(const char *url)\n+{\n+\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n+\tchar *filter_spec = NULL;\n+\tchar *normalized_url;\n+\n+\tconfig.section = \"clone\";\n+\tconfig.key = \"defaultobjectfilter\";\n+\tconfig.collect_fn = clone_filter_collect;\n+\tconfig.cb = &filter_spec;\n+\n+\tnormalized_url = url_normalize(url, &config.url);\n+\n+\trepo_config(the_repository, urlmatch_config_entry, &config);\n+\tfree(normalized_url);\n+\turlmatch_config_release(&config);\n+\n+\treturn filter_spec;\n+}\n+\n static int write_one_config(const char *key, const char *value,\n \t\t\t    const struct config_context *ctx,\n \t\t\t    void *data)\n@@ -1057,6 +1099,14 @@ int cmd_clone(int argc,\n \t} else\n \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n \n+\tif (!filter_options.choice && !filter_options.no_filter) {\n+\t\tchar *config_filter = get_default_object_filter(repo);\n+\t\tif (config_filter) {\n+\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n+\t\t\tfree(config_filter);\n+\t\t}\n+\t}\n+\n \t/* no need to be strict, transport_set_option() will validate it again */\n \tif (option_depth && atoi(option_depth) < 1)\n \t\tdie(_(\"depth %s is not a positive number\"), option_depth);\ndiff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\nindex 1e354e057f..e85d2a8ce8 100755\n--- a/t/t5616-partial-clone.sh\n+++ b/t/t5616-partial-clone.sh\n@@ -722,6 +722,98 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n \tgit -C partial gc --prune=now\n '\n \n+# Test clone.<url>.defaultObjectFilter config\n+\n+test_expect_success 'setup for clone.defaultObjectFilter tests' '\n+\tgit init default-filter-src &&\n+\techo \"small\" >default-filter-src/small.txt &&\n+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n+\tgit -C default-filter-src add . &&\n+\tgit -C default-filter-src commit -m \"initial\" &&\n+\n+\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-clone &&\n+\n+\ttest \"$(git -C default-filter-clone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n+'\n+\n+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n+\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n+\n+\ttest \"$(git -C default-filter-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-blobnone &&\n+\n+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-tree0 &&\n+\n+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)\" = \"tree:0\"\n+'\n+\n+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n+\n+\ttest \"$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n+\tgit \\\n+\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n+\n+\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n+'\n+\n+test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '\n+\tgit -c clone.defaultObjectFilter=blob:none \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n+\n+\ttest \"$(git -C default-filter-bare-key config --local remote.origin.promisor)\" = \"true\" &&\n+\ttest \"$(git -C default-filter-bare-key config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c clone.defaultObjectFilter=blob:limit=1k \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n+\n+\ttest \"$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n+'\n+\n+test_expect_success '--no-filter defeats clone.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone --no-filter \"$SERVER_URL\" default-filter-no-filter &&\n+\n+\ttest_must_fail git -C default-filter-no-filter config --local remote.origin.promisor\n+'\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\n-- \ngitgitgadget\n"},{"id":"538119","messageId":"18655b73-0050-4255-aa7a-c0bcb854fc6b@app.fastmail.com","threadId":"65110","inReplyTo":"xmqq1phw21op.fsf@gitster.g","subject":"Re: [PATCH v3] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite","fromEmail":"alan@braithwaite.dev","sentAt":"2026-03-06T21:50:20Z","receivedAt":"2026-03-06T21:50:43Z","isPatch":true,"body":"> Do we have a way to defeat the configured filter to say \"no\n> filtering, we want everything\" from the command line?  If not, that\n> needs to be addressed, if we were to add this configuration.\n\nGreat point, added a check for the no-filter flag and made that\noverride any defaultObjectFilter setting for the clone.\n\nThanks,\n- Alan\n\nOn Fri, Mar 6, 2026, at 11:33, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n>\n>> We've historically not implemented default filtering for clones because\n>> it makes it hard to reason about the behaviour of the clone command.\n>> For instance, if I have a script that clones a repository, it almost\n>> certainly expects a full clone unless it requested something else.\n>> ...\n>> We've traditionally placed this kind of customizable configuration into\n>> `scalar` instead, which is designed to be configurable and set options\n>> for large repositories that would want to control clone and fetch\n>> options.\n>\n> Hmph, my knee-jerk reaction to the early part of your message was\n> \"oh, but isn't clone a Porcelain (admittedly without corresponding\n> plumbing) whose defaults and end-user experiences are meant to be\n> updated from time to time to help users?\" but I didn't realize that\n> we have another class, which is \"scalar\", these days that we can add\n> these settings to.  I do not have objections to add something to\n> \"scalar\", but I personally feel that the configuration for clone is\n> such a bad thing to have.\n>\n> Do we have a way to defeat the configured filter to say \"no\n> filtering, we want everything\" from the command line?  If not, that\n> needs to be addressed, if we were to add this configuration.\n>\n> Thanks.\n"},{"id":"538123","messageId":"xmqqfr6cy53q.fsf@gitster.g","threadId":"65110","inReplyTo":"pull.2058.v4.git.1772833649843.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] clone: add clone.<url>.defaultObjectFilter config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-06T22:18:17Z","receivedAt":"2026-03-06T22:18:19Z","isPatch":true,"body":"\"Alan Braithwaite via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Alan Braithwaite <alan@braithwaite.dev>\n>\n> Add a new configuration option that lets users specify a default\n> partial clone filter per URL pattern.  When cloning a repository\n> whose URL matches a configured pattern, git-clone automatically\n> applies the filter, equivalent to passing --filter on the command\n> line.\n>\n>     [clone \"https://github.com/\"]\n>         defaultObjectFilter = blob:limit=5m\n>\n>     [clone \"https://internal.corp.com/large-project/\"]\n>         defaultObjectFilter = blob:none\n>\n> URL matching uses the existing urlmatch_config_entry() infrastructure,\n> following the same rules as http.<url>.* — you can match a domain,\n> a namespace path, or a specific project, and the most specific match\n> wins.\n>\n> The config only affects the initial clone.  Once the clone completes,\n> the filter is recorded in remote.<name>.partialCloneFilter, so\n> subsequent fetches inherit it automatically.  An explicit --filter\n> flag on the command line takes precedence.\n>\n> Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n> honored; a bare clone.defaultObjectFilter without a URL subsection\n> is ignored.\n\nIs this still valid?  It is inconsistent with the updated\ndocumentation where both clone.defaultObjectFilter and\nclone.<url>.defaultObjectFilter are listed.\n\nThese iterations of patches may require a bit more careful\nproofreading before getting sent to the mailing list for others to\ncomment on, I suspect?\n\n> Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n> ---\n> ...\n> +`clone.defaultObjectFilter`::\n> +`clone.<url>.defaultObjectFilter`::\n> +\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n> +\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n> +\tuse `--filter=<value>` to enable partial clone behavior.\n> +\tObjects matching the filter are excluded from the initial\n> +\ttransfer and lazily fetched on demand (e.g., during checkout).\n> +\tSubsequent fetches inherit the filter via the per-remote config\n> +\tthat is written during the clone.\n> ++\n> +The bare `clone.defaultObjectFilter` applies to all clones.  The\n> +URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n> +setting to clones whose URL matches `<url>`, following the same\n> +rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n> +specific URL match wins.  You can match a domain, a namespace, or a\n> +specific project:\n\n\nIn the test script we see a handful of lines like these\n\n> +\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n> +\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n\nadded.  They may have been written to mimick an existing line in a\ntest elsewhere, but see efforts by others like\n\n    https://lore.kernel.org/git/20260305225128.54283-1-francescopaparatto@gmail.com/\n\nThanks.\n"},{"id":"538151","messageId":"01215e9c-c110-4860-a285-7f09bc6596e5@app.fastmail.com","threadId":"65110","inReplyTo":"xmqqfr6cy53q.fsf@gitster.g","subject":"Re: [PATCH v4] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite","fromEmail":"alan@braithwaite.dev","sentAt":"2026-03-07T01:04:36Z","receivedAt":"2026-03-07T01:04:57Z","isPatch":true,"body":"Thanks for the careful review, Junio.  You're right on both\ncounts.  The stale commit message and the test style were\nsloppy oversights that I should have caught before resubmitting.\n\nI'll be more disciplined about reviewing the full diff\n(including the commit message) against the actual behavior for\nfuture patches.  Thanks for helping out on my first patch.\n\nThe updated incoming patch addresses both issues: the commit\nmessage now accurately describes the bare and URL-qualified\nforms, and all tests use the test_cmp pattern.  I'll be\nsubmitting what I think should be the final version shortly,\nbut I'm happy to continue iterating if anything else looks\nconcerning.\n\nThanks,\n- Alan\n\nOn Fri, Mar 6, 2026, at 14:18, Junio C Hamano wrote:\n> \"Alan Braithwaite via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: Alan Braithwaite <alan@braithwaite.dev>\n>>\n>> Add a new configuration option that lets users specify a default\n>> partial clone filter per URL pattern.  When cloning a repository\n>> whose URL matches a configured pattern, git-clone automatically\n>> applies the filter, equivalent to passing --filter on the command\n>> line.\n>>\n>>     [clone \"https://github.com/\"]\n>>         defaultObjectFilter = blob:limit=5m\n>>\n>>     [clone \"https://internal.corp.com/large-project/\"]\n>>         defaultObjectFilter = blob:none\n>>\n>> URL matching uses the existing urlmatch_config_entry() infrastructure,\n>> following the same rules as http.<url>.* — you can match a domain,\n>> a namespace path, or a specific project, and the most specific match\n>> wins.\n>>\n>> The config only affects the initial clone.  Once the clone completes,\n>> the filter is recorded in remote.<name>.partialCloneFilter, so\n>> subsequent fetches inherit it automatically.  An explicit --filter\n>> flag on the command line takes precedence.\n>>\n>> Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n>> honored; a bare clone.defaultObjectFilter without a URL subsection\n>> is ignored.\n>\n> Is this still valid?  It is inconsistent with the updated\n> documentation where both clone.defaultObjectFilter and\n> clone.<url>.defaultObjectFilter are listed.\n>\n> These iterations of patches may require a bit more careful\n> proofreading before getting sent to the mailing list for others to\n> comment on, I suspect?\n>\n>> Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n>> ---\n>> ...\n>> +`clone.defaultObjectFilter`::\n>> +`clone.<url>.defaultObjectFilter`::\n>> +\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n>> +\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n>> +\tuse `--filter=<value>` to enable partial clone behavior.\n>> +\tObjects matching the filter are excluded from the initial\n>> +\ttransfer and lazily fetched on demand (e.g., during checkout).\n>> +\tSubsequent fetches inherit the filter via the per-remote config\n>> +\tthat is written during the clone.\n>> ++\n>> +The bare `clone.defaultObjectFilter` applies to all clones.  The\n>> +URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n>> +setting to clones whose URL matches `<url>`, following the same\n>> +rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n>> +specific URL match wins.  You can match a domain, a namespace, or a\n>> +specific project:\n>\n>\n> In the test script we see a handful of lines like these\n>\n>> +\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n>> +\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n>\n> added.  They may have been written to mimick an existing line in a\n> test elsewhere, but see efforts by others like\n>\n>     \n> https://lore.kernel.org/git/20260305225128.54283-1-francescopaparatto@gmail.com/\n>\n> Thanks.\n"},{"id":"538155","messageId":"pull.2058.v5.git.1772847236966.gitgitgadget@gmail.com","threadId":"65110","inReplyTo":"pull.2058.v4.git.1772833649843.gitgitgadget@gmail.com","subject":"[PATCH v5] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-07T01:33:56Z","receivedAt":"2026-03-07T01:33:59Z","isPatch":true,"body":"From: Alan Braithwaite <alan@braithwaite.dev>\n\nAdd a new configuration option that lets users specify a default\npartial clone filter, optionally scoped by URL pattern.  When\ncloning a repository whose URL matches a configured pattern,\ngit-clone automatically applies the filter, equivalent to passing\n--filter on the command line.\n\n    [clone]\n        defaultObjectFilter = blob:limit=1m\n\n    [clone \"https://github.com/\"]\n        defaultObjectFilter = blob:limit=5m\n\n    [clone \"https://internal.corp.com/large-project/\"]\n        defaultObjectFilter = blob:none\n\nThe bare clone.defaultObjectFilter applies to all clones.  The\nURL-qualified form clone.<url>.defaultObjectFilter restricts the\nsetting to matching URLs.  URL matching uses the existing\nurlmatch_config_entry() infrastructure, following the same rules as\nhttp.<url>.* — a domain, namespace, or specific project can be\nmatched, and the most specific match wins.\n\nThe config only affects the initial clone.  Once the clone completes,\nthe filter is recorded in remote.<name>.partialCloneFilter, so\nsubsequent fetches inherit it automatically.  An explicit --filter\non the command line takes precedence, and --no-filter defeats the\nconfigured default entirely.\n\nSigned-off-by: Alan Braithwaite <alan@braithwaite.dev>\n---\n    fetch, clone: add fetch.blobSizeLimit config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v5\nPull-Request: https://github.com/gitgitgadget/git/pull/2058\n\nRange-diff vs v4:\n\n 1:  4bf3e1ec63 ! 1:  fa1ea69bdb clone: add clone.<url>.defaultObjectFilter config\n     @@ Commit message\n          clone: add clone.<url>.defaultObjectFilter config\n      \n          Add a new configuration option that lets users specify a default\n     -    partial clone filter per URL pattern.  When cloning a repository\n     -    whose URL matches a configured pattern, git-clone automatically\n     -    applies the filter, equivalent to passing --filter on the command\n     -    line.\n     +    partial clone filter, optionally scoped by URL pattern.  When\n     +    cloning a repository whose URL matches a configured pattern,\n     +    git-clone automatically applies the filter, equivalent to passing\n     +    --filter on the command line.\n     +\n     +        [clone]\n     +            defaultObjectFilter = blob:limit=1m\n      \n              [clone \"https://github.com/\"]\n                  defaultObjectFilter = blob:limit=5m\n     @@ Commit message\n              [clone \"https://internal.corp.com/large-project/\"]\n                  defaultObjectFilter = blob:none\n      \n     -    URL matching uses the existing urlmatch_config_entry() infrastructure,\n     -    following the same rules as http.<url>.* — you can match a domain,\n     -    a namespace path, or a specific project, and the most specific match\n     -    wins.\n     +    The bare clone.defaultObjectFilter applies to all clones.  The\n     +    URL-qualified form clone.<url>.defaultObjectFilter restricts the\n     +    setting to matching URLs.  URL matching uses the existing\n     +    urlmatch_config_entry() infrastructure, following the same rules as\n     +    http.<url>.* — a domain, namespace, or specific project can be\n     +    matched, and the most specific match wins.\n      \n          The config only affects the initial clone.  Once the clone completes,\n          the filter is recorded in remote.<name>.partialCloneFilter, so\n          subsequent fetches inherit it automatically.  An explicit --filter\n     -    flag on the command line takes precedence.\n     -\n     -    Only the URL-qualified form (clone.<url>.defaultObjectFilter) is\n     -    honored; a bare clone.defaultObjectFilter without a URL subsection\n     -    is ignored.\n     +    on the command line takes precedence, and --no-filter defeats the\n     +    configured default entirely.\n      \n          Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n      \n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n      +\t\t\"$SERVER_URL\" default-filter-clone &&\n      +\n     -+\ttest \"$(git -C default-filter-clone config --local remote.origin.promisor)\" = \"true\" &&\n     -+\ttest \"$(git -C default-filter-clone config --local remote.origin.partialclonefilter)\" = \"blob:limit=1024\"\n     ++\techo true >expect &&\n     ++\tgit -C default-filter-clone config --local remote.origin.promisor >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\n     ++\techo \"blob:limit=1024\" >expect &&\n     ++\tgit -C default-filter-clone config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n      +\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n      +\n     -+\ttest \"$(git -C default-filter-override config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\techo \"blob:none\" >expect &&\n     ++\tgit -C default-filter-override config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n      +\t\t\"$SERVER_URL\" default-filter-blobnone &&\n      +\n     -+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.promisor)\" = \"true\" &&\n     -+\ttest \"$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\techo true >expect &&\n     ++\tgit -C default-filter-blobnone config --local remote.origin.promisor >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\n     ++\techo \"blob:none\" >expect &&\n     ++\tgit -C default-filter-blobnone config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n      +\t\t\"$SERVER_URL\" default-filter-tree0 &&\n      +\n     -+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.promisor)\" = \"true\" &&\n     -+\ttest \"$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)\" = \"tree:0\"\n     ++\techo true >expect &&\n     ++\tgit -C default-filter-tree0 config --local remote.origin.promisor >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\n     ++\techo \"tree:0\" >expect &&\n     ++\tgit -C default-filter-tree0 config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n      +\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n      +\n     -+\ttest \"$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\techo \"blob:none\" >expect &&\n     ++\tgit -C default-filter-url-specific config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\tgit -c clone.defaultObjectFilter=blob:none \\\n      +\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n      +\n     -+\ttest \"$(git -C default-filter-bare-key config --local remote.origin.promisor)\" = \"true\" &&\n     -+\ttest \"$(git -C default-filter-bare-key config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\techo true >expect &&\n     ++\tgit -C default-filter-bare-key config --local remote.origin.promisor >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\n     ++\techo \"blob:none\" >expect &&\n     ++\tgit -C default-filter-bare-key config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n      +\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n      +\n     -+\ttest \"$(git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter)\" = \"blob:none\"\n     ++\techo \"blob:none\" >expect &&\n     ++\tgit -C default-filter-url-over-bare config --local remote.origin.partialclonefilter >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success '--no-filter defeats clone.defaultObjectFilter' '\n\n\n Documentation/config/clone.adoc |  34 +++++++++\n builtin/clone.c                 |  50 ++++++++++++++\n t/t5616-partial-clone.sh        | 118 ++++++++++++++++++++++++++++++++\n 3 files changed, 202 insertions(+)\n\ndiff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\nindex 0a10efd174..1d6c0957a0 100644\n--- a/Documentation/config/clone.adoc\n+++ b/Documentation/config/clone.adoc\n@@ -21,3 +21,37 @@ endif::[]\n \tIf a partial clone filter is provided (see `--filter` in\n \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n \tthe filter to submodules.\n+\n+`clone.defaultObjectFilter`::\n+`clone.<url>.defaultObjectFilter`::\n+\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n+\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n+\tuse `--filter=<value>` to enable partial clone behavior.\n+\tObjects matching the filter are excluded from the initial\n+\ttransfer and lazily fetched on demand (e.g., during checkout).\n+\tSubsequent fetches inherit the filter via the per-remote config\n+\tthat is written during the clone.\n++\n+The bare `clone.defaultObjectFilter` applies to all clones.  The\n+URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n+setting to clones whose URL matches `<url>`, following the same\n+rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n+specific URL match wins.  You can match a domain, a namespace, or a\n+specific project:\n++\n+----\n+[clone]\n+    defaultObjectFilter = blob:limit=1m\n+\n+[clone \"https://github.com/\"]\n+    defaultObjectFilter = blob:limit=5m\n+\n+[clone \"https://internal.corp.com/large-project/\"]\n+    defaultObjectFilter = blob:none\n+----\n++\n+An explicit `--filter` option on the command line takes precedence\n+over this config, and `--no-filter` defeats it entirely to force a\n+full clone.  Only affects the initial clone; it has no effect on\n+later fetches into an existing repository.  If the server does not\n+support object filtering, the setting is silently ignored.\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 45d8fa0eed..1207655815 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -44,6 +44,7 @@\n #include \"path.h\"\n #include \"pkt-line.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"urlmatch.h\"\n #include \"hook.h\"\n #include \"bundle.h\"\n #include \"bundle-uri.h\"\n@@ -757,6 +758,47 @@ static int git_clone_config(const char *k, const char *v,\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n+static int clone_filter_collect(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx UNUSED,\n+\t\t\t\tvoid *cb)\n+{\n+\tchar **filter_spec_p = cb;\n+\n+\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tfree(*filter_spec_p);\n+\t\t*filter_spec_p = xstrdup(value);\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n+ * using the urlmatch infrastructure.  A URL-qualified entry that matches\n+ * the clone URL takes precedence over the bare form, following the same\n+ * rules as http.<url>.* configuration variables.\n+ */\n+static char *get_default_object_filter(const char *url)\n+{\n+\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n+\tchar *filter_spec = NULL;\n+\tchar *normalized_url;\n+\n+\tconfig.section = \"clone\";\n+\tconfig.key = \"defaultobjectfilter\";\n+\tconfig.collect_fn = clone_filter_collect;\n+\tconfig.cb = &filter_spec;\n+\n+\tnormalized_url = url_normalize(url, &config.url);\n+\n+\trepo_config(the_repository, urlmatch_config_entry, &config);\n+\tfree(normalized_url);\n+\turlmatch_config_release(&config);\n+\n+\treturn filter_spec;\n+}\n+\n static int write_one_config(const char *key, const char *value,\n \t\t\t    const struct config_context *ctx,\n \t\t\t    void *data)\n@@ -1057,6 +1099,14 @@ int cmd_clone(int argc,\n \t} else\n \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n \n+\tif (!filter_options.choice && !filter_options.no_filter) {\n+\t\tchar *config_filter = get_default_object_filter(repo);\n+\t\tif (config_filter) {\n+\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n+\t\t\tfree(config_filter);\n+\t\t}\n+\t}\n+\n \t/* no need to be strict, transport_set_option() will validate it again */\n \tif (option_depth && atoi(option_depth) < 1)\n \t\tdie(_(\"depth %s is not a positive number\"), option_depth);\ndiff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\nindex 1e354e057f..1254901f3e 100755\n--- a/t/t5616-partial-clone.sh\n+++ b/t/t5616-partial-clone.sh\n@@ -722,6 +722,124 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n \tgit -C partial gc --prune=now\n '\n \n+# Test clone.<url>.defaultObjectFilter config\n+\n+test_expect_success 'setup for clone.defaultObjectFilter tests' '\n+\tgit init default-filter-src &&\n+\techo \"small\" >default-filter-src/small.txt &&\n+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n+\tgit -C default-filter-src add . &&\n+\tgit -C default-filter-src commit -m \"initial\" &&\n+\n+\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-clone &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-clone config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"blob:limit=1024\" >expect &&\n+\tgit -C default-filter-clone config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n+\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-override config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-blobnone &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-blobnone config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-blobnone config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-tree0 &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-tree0 config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"tree:0\" >expect &&\n+\tgit -C default-filter-tree0 config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-url-specific config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n+\tgit \\\n+\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n+\n+\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n+'\n+\n+test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '\n+\tgit -c clone.defaultObjectFilter=blob:none \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-bare-key config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-bare-key config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c clone.defaultObjectFilter=blob:limit=1k \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-url-over-bare config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-filter defeats clone.defaultObjectFilter' '\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone --no-filter \"$SERVER_URL\" default-filter-no-filter &&\n+\n+\ttest_must_fail git -C default-filter-no-filter config --local remote.origin.promisor\n+'\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\n-- \ngitgitgadget\n"},{"id":"538562","messageId":"abEdTQrRtAveH1rB@pks.im","threadId":"65110","inReplyTo":"pull.2058.v5.git.1772847236966.gitgitgadget@gmail.com","subject":"Re: [PATCH v5] clone: add clone.<url>.defaultObjectFilter config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T07:44:13Z","receivedAt":"2026-03-11T07:44:21Z","isPatch":true,"body":"On Sat, Mar 07, 2026 at 01:33:56AM +0000, Alan Braithwaite via GitGitGadget wrote:\n> diff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\n> index 0a10efd174..1d6c0957a0 100644\n> --- a/Documentation/config/clone.adoc\n> +++ b/Documentation/config/clone.adoc\n> @@ -21,3 +21,37 @@ endif::[]\n>  \tIf a partial clone filter is provided (see `--filter` in\n>  \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n>  \tthe filter to submodules.\n> +\n> +`clone.defaultObjectFilter`::\n> +`clone.<url>.defaultObjectFilter`::\n> +\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n> +\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n> +\tuse `--filter=<value>` to enable partial clone behavior.\n> +\tObjects matching the filter are excluded from the initial\n> +\ttransfer and lazily fetched on demand (e.g., during checkout).\n> +\tSubsequent fetches inherit the filter via the per-remote config\n> +\tthat is written during the clone.\n> ++\n> +The bare `clone.defaultObjectFilter` applies to all clones.  The\n> +URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n> +setting to clones whose URL matches `<url>`, following the same\n> +rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n> +specific URL match wins.  You can match a domain, a namespace, or a\n> +specific project:\n> ++\n> +----\n> +[clone]\n> +    defaultObjectFilter = blob:limit=1m\n> +\n> +[clone \"https://github.com/\"]\n> +    defaultObjectFilter = blob:limit=5m\n> +\n> +[clone \"https://internal.corp.com/large-project/\"]\n> +    defaultObjectFilter = blob:none\n> +----\n> ++\n> +An explicit `--filter` option on the command line takes precedence\n> +over this config, and `--no-filter` defeats it entirely to force a\n> +full clone.  Only affects the initial clone; it has no effect on\n> +later fetches into an existing repository.  If the server does not\n> +support object filtering, the setting is silently ignored.\n\nThis all reads good to me.\n\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 45d8fa0eed..1207655815 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -757,6 +758,47 @@ static int git_clone_config(const char *k, const char *v,\n>  \treturn git_default_config(k, v, ctx, cb);\n>  }\n>  \n> +static int clone_filter_collect(const char *var, const char *value,\n> +\t\t\t\tconst struct config_context *ctx UNUSED,\n> +\t\t\t\tvoid *cb)\n> +{\n> +\tchar **filter_spec_p = cb;\n> +\n> +\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n> +\t\tif (!value)\n> +\t\t\treturn config_error_nonbool(var);\n> +\t\tfree(*filter_spec_p);\n> +\t\t*filter_spec_p = xstrdup(value);\n> +\t}\n> +\treturn 0;\n> +}\n> +\n> +/*\n> + * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n> + * using the urlmatch infrastructure.  A URL-qualified entry that matches\n> + * the clone URL takes precedence over the bare form, following the same\n> + * rules as http.<url>.* configuration variables.\n> + */\n> +static char *get_default_object_filter(const char *url)\n> +{\n> +\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n> +\tchar *filter_spec = NULL;\n> +\tchar *normalized_url;\n> +\n> +\tconfig.section = \"clone\";\n> +\tconfig.key = \"defaultobjectfilter\";\n> +\tconfig.collect_fn = clone_filter_collect;\n> +\tconfig.cb = &filter_spec;\n> +\n> +\tnormalized_url = url_normalize(url, &config.url);\n\n`url_normalize()` will return a `NULL` pointer in case it cannot parse\nthe URL. We need to be prepared for this, otherwise we might segfault.\nI guess the best route is to simply ignore the URL in that case --\notherwise, we would always error out in case the remote has a weird URL\nconfigured.\n\n> diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\n> index 1e354e057f..1254901f3e 100755\n> --- a/t/t5616-partial-clone.sh\n> +++ b/t/t5616-partial-clone.sh\n> @@ -722,6 +722,124 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n>  \tgit -C partial gc --prune=now\n>  '\n>  \n> +# Test clone.<url>.defaultObjectFilter config\n> +\n> +test_expect_success 'setup for clone.defaultObjectFilter tests' '\n> +\tgit init default-filter-src &&\n> +\techo \"small\" >default-filter-src/small.txt &&\n> +\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n> +\tgit -C default-filter-src add . &&\n> +\tgit -C default-filter-src commit -m \"initial\" &&\n> +\n> +\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n> +\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n> +\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n> +'\n> +\n> +test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n> +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n> +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n> +\t\t\"$SERVER_URL\" default-filter-clone &&\n\nDo we want to \"test_when_finished rm -rf default-filter-clone\" here and\nfor all the subsequent tests?\n\nPatrick\n"},{"id":"539004","messageId":"9b67801b-ce07-42b6-b2c6-2e7f0e5fd5f7@app.fastmail.com","threadId":"65110","inReplyTo":"abEdTQrRtAveH1rB@pks.im","subject":"Re: [PATCH v5] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite","fromEmail":"alan@braithwaite.dev","sentAt":"2026-03-15T01:33:38Z","receivedAt":"2026-03-15T01:34:01Z","isPatch":true,"body":"Thanks for the review, Patrick.\n\n> `url_normalize()` will return a `NULL` pointer in case\n> it cannot parse the URL.  We need to be prepared for\n> this, otherwise we might segfault.\n\nGood catch.  The updated patch guards on the return value\nand skips the urlmatch lookup entirely when the URL cannot\nbe normalized.  Today `match_urls()` happens to handle\nthis safely (it returns 0 when `url->url` is NULL), but an\nexplicit NULL check guards against future regressions in\nthat code path.\n\n> Do we want to \"test_when_finished rm -rf\n> default-filter-clone\" here and for all the subsequent\n> tests?\n\nDone -- added `test_when_finished` cleanup to each test.\n\nPatch incoming. :)\n\nThanks,\n- Alan\n\nOn Wed, Mar 11, 2026, at 00:44, Patrick Steinhardt wrote:\n> On Sat, Mar 07, 2026 at 01:33:56AM +0000, Alan Braithwaite via \n> GitGitGadget wrote:\n>> diff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\n>> index 0a10efd174..1d6c0957a0 100644\n>> --- a/Documentation/config/clone.adoc\n>> +++ b/Documentation/config/clone.adoc\n>> @@ -21,3 +21,37 @@ endif::[]\n>>  \tIf a partial clone filter is provided (see `--filter` in\n>>  \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n>>  \tthe filter to submodules.\n>> +\n>> +`clone.defaultObjectFilter`::\n>> +`clone.<url>.defaultObjectFilter`::\n>> +\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n>> +\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n>> +\tuse `--filter=<value>` to enable partial clone behavior.\n>> +\tObjects matching the filter are excluded from the initial\n>> +\ttransfer and lazily fetched on demand (e.g., during checkout).\n>> +\tSubsequent fetches inherit the filter via the per-remote config\n>> +\tthat is written during the clone.\n>> ++\n>> +The bare `clone.defaultObjectFilter` applies to all clones.  The\n>> +URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n>> +setting to clones whose URL matches `<url>`, following the same\n>> +rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n>> +specific URL match wins.  You can match a domain, a namespace, or a\n>> +specific project:\n>> ++\n>> +----\n>> +[clone]\n>> +    defaultObjectFilter = blob:limit=1m\n>> +\n>> +[clone \"https://github.com/\"]\n>> +    defaultObjectFilter = blob:limit=5m\n>> +\n>> +[clone \"https://internal.corp.com/large-project/\"]\n>> +    defaultObjectFilter = blob:none\n>> +----\n>> ++\n>> +An explicit `--filter` option on the command line takes precedence\n>> +over this config, and `--no-filter` defeats it entirely to force a\n>> +full clone.  Only affects the initial clone; it has no effect on\n>> +later fetches into an existing repository.  If the server does not\n>> +support object filtering, the setting is silently ignored.\n>\n> This all reads good to me.\n>\n>> diff --git a/builtin/clone.c b/builtin/clone.c\n>> index 45d8fa0eed..1207655815 100644\n>> --- a/builtin/clone.c\n>> +++ b/builtin/clone.c\n>> @@ -757,6 +758,47 @@ static int git_clone_config(const char *k, const char *v,\n>>  \treturn git_default_config(k, v, ctx, cb);\n>>  }\n>>  \n>> +static int clone_filter_collect(const char *var, const char *value,\n>> +\t\t\t\tconst struct config_context *ctx UNUSED,\n>> +\t\t\t\tvoid *cb)\n>> +{\n>> +\tchar **filter_spec_p = cb;\n>> +\n>> +\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n>> +\t\tif (!value)\n>> +\t\t\treturn config_error_nonbool(var);\n>> +\t\tfree(*filter_spec_p);\n>> +\t\t*filter_spec_p = xstrdup(value);\n>> +\t}\n>> +\treturn 0;\n>> +}\n>> +\n>> +/*\n>> + * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n>> + * using the urlmatch infrastructure.  A URL-qualified entry that matches\n>> + * the clone URL takes precedence over the bare form, following the same\n>> + * rules as http.<url>.* configuration variables.\n>> + */\n>> +static char *get_default_object_filter(const char *url)\n>> +{\n>> +\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n>> +\tchar *filter_spec = NULL;\n>> +\tchar *normalized_url;\n>> +\n>> +\tconfig.section = \"clone\";\n>> +\tconfig.key = \"defaultobjectfilter\";\n>> +\tconfig.collect_fn = clone_filter_collect;\n>> +\tconfig.cb = &filter_spec;\n>> +\n>> +\tnormalized_url = url_normalize(url, &config.url);\n>\n> `url_normalize()` will return a `NULL` pointer in case it cannot parse\n> the URL. We need to be prepared for this, otherwise we might segfault.\n> I guess the best route is to simply ignore the URL in that case --\n> otherwise, we would always error out in case the remote has a weird URL\n> configured.\n>\n>> diff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\n>> index 1e354e057f..1254901f3e 100755\n>> --- a/t/t5616-partial-clone.sh\n>> +++ b/t/t5616-partial-clone.sh\n>> @@ -722,6 +722,124 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n>>  \tgit -C partial gc --prune=now\n>>  '\n>>  \n>> +# Test clone.<url>.defaultObjectFilter config\n>> +\n>> +test_expect_success 'setup for clone.defaultObjectFilter tests' '\n>> +\tgit init default-filter-src &&\n>> +\techo \"small\" >default-filter-src/small.txt &&\n>> +\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n>> +\tgit -C default-filter-src add . &&\n>> +\tgit -C default-filter-src commit -m \"initial\" &&\n>> +\n>> +\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n>> +\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n>> +\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n>> +'\n>> +\n>> +test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n>> +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n>> +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n>> +\t\t\"$SERVER_URL\" default-filter-clone &&\n>\n> Do we want to \"test_when_finished rm -rf default-filter-clone\" here and\n> for all the subsequent tests?\n>\n> Patrick\n"},{"id":"539012","messageId":"pull.2058.v6.git.1773553022381.gitgitgadget@gmail.com","threadId":"65110","inReplyTo":"pull.2058.v5.git.1772847236966.gitgitgadget@gmail.com","subject":"[PATCH v6] clone: add clone.<url>.defaultObjectFilter config","fromName":"Alan Braithwaite via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-15T05:37:02Z","receivedAt":"2026-03-15T05:37:04Z","isPatch":true,"body":"From: Alan Braithwaite <alan@braithwaite.dev>\n\nAdd a new configuration option that lets users specify a default\npartial clone filter, optionally scoped by URL pattern.  When\ncloning a repository whose URL matches a configured pattern,\ngit-clone automatically applies the filter, equivalent to passing\n--filter on the command line.\n\n    [clone]\n        defaultObjectFilter = blob:limit=1m\n\n    [clone \"https://github.com/\"]\n        defaultObjectFilter = blob:limit=5m\n\n    [clone \"https://internal.corp.com/large-project/\"]\n        defaultObjectFilter = blob:none\n\nThe bare clone.defaultObjectFilter applies to all clones.  The\nURL-qualified form clone.<url>.defaultObjectFilter restricts the\nsetting to matching URLs.  URL matching uses the existing\nurlmatch_config_entry() infrastructure, following the same rules as\nhttp.<url>.* — a domain, namespace, or specific project can be\nmatched, and the most specific match wins.\n\nThe config only affects the initial clone.  Once the clone completes,\nthe filter is recorded in remote.<name>.partialCloneFilter, so\nsubsequent fetches inherit it automatically.  An explicit --filter\non the command line takes precedence, and --no-filter defeats the\nconfigured default entirely.\n\nSigned-off-by: Alan Braithwaite <alan@braithwaite.dev>\n---\n    fetch, clone: add fetch.blobSizeLimit config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v6\nPull-Request: https://github.com/gitgitgadget/git/pull/2058\n\nRange-diff vs v5:\n\n 1:  fa1ea69bdb ! 1:  480453b2e7 clone: add clone.<url>.defaultObjectFilter config\n     @@ builtin/clone.c: static int git_clone_config(const char *k, const char *v,\n      +\tconfig.cb = &filter_spec;\n      +\n      +\tnormalized_url = url_normalize(url, &config.url);\n     ++\tif (!normalized_url) {\n     ++\t\turlmatch_config_release(&config);\n     ++\t\treturn NULL;\n     ++\t}\n      +\n      +\trepo_config(the_repository, urlmatch_config_entry, &config);\n      +\tfree(normalized_url);\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +test_expect_success 'setup for clone.defaultObjectFilter tests' '\n      +\tgit init default-filter-src &&\n      +\techo \"small\" >default-filter-src/small.txt &&\n     -+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n      +\tgit -C default-filter-src add . &&\n      +\tgit -C default-filter-src commit -m \"initial\" &&\n      +\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n     ++\ttest_when_finished \"rm -r default-filter-clone\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n      +\t\t\"$SERVER_URL\" default-filter-clone &&\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n     ++\ttest_when_finished \"rm -r default-filter-override\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n      +\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n     ++\ttest_when_finished \"rm -r default-filter-blobnone\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n      +\t\t\"$SERVER_URL\" default-filter-blobnone &&\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n     ++\ttest_when_finished \"rm -r default-filter-tree0\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n      +\t\t\"$SERVER_URL\" default-filter-tree0 &&\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n     ++\ttest_when_finished \"rm -r default-filter-url-specific\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit \\\n      +\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n     ++\ttest_when_finished \"rm -r default-filter-url-nomatch\" &&\n      +\tgit \\\n      +\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n      +\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '\n     ++\ttest_when_finished \"rm -r default-filter-bare-key\" &&\n      +\tgit -c clone.defaultObjectFilter=blob:none \\\n      +\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n      +\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n     ++\ttest_when_finished \"rm -r default-filter-url-over-bare\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit \\\n      +\t\t-c clone.defaultObjectFilter=blob:limit=1k \\\n     @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n      +'\n      +\n      +test_expect_success '--no-filter defeats clone.defaultObjectFilter' '\n     ++\ttest_when_finished \"rm -r default-filter-no-filter\" &&\n      +\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n      +\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n      +\t\tclone --no-filter \"$SERVER_URL\" default-filter-no-filter &&\n\n\n Documentation/config/clone.adoc |  34 +++++++++\n builtin/clone.c                 |  54 ++++++++++++++\n t/t5616-partial-clone.sh        | 126 ++++++++++++++++++++++++++++++++\n 3 files changed, 214 insertions(+)\n\ndiff --git a/Documentation/config/clone.adoc b/Documentation/config/clone.adoc\nindex 0a10efd174..1d6c0957a0 100644\n--- a/Documentation/config/clone.adoc\n+++ b/Documentation/config/clone.adoc\n@@ -21,3 +21,37 @@ endif::[]\n \tIf a partial clone filter is provided (see `--filter` in\n \tlinkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply\n \tthe filter to submodules.\n+\n+`clone.defaultObjectFilter`::\n+`clone.<url>.defaultObjectFilter`::\n+\tWhen set to a filter spec string (e.g., `blob:limit=1m`,\n+\t`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically\n+\tuse `--filter=<value>` to enable partial clone behavior.\n+\tObjects matching the filter are excluded from the initial\n+\ttransfer and lazily fetched on demand (e.g., during checkout).\n+\tSubsequent fetches inherit the filter via the per-remote config\n+\tthat is written during the clone.\n++\n+The bare `clone.defaultObjectFilter` applies to all clones.  The\n+URL-qualified form `clone.<url>.defaultObjectFilter` restricts the\n+setting to clones whose URL matches `<url>`, following the same\n+rules as `http.<url>.*` (see linkgit:git-config[1]).  The most\n+specific URL match wins.  You can match a domain, a namespace, or a\n+specific project:\n++\n+----\n+[clone]\n+    defaultObjectFilter = blob:limit=1m\n+\n+[clone \"https://github.com/\"]\n+    defaultObjectFilter = blob:limit=5m\n+\n+[clone \"https://internal.corp.com/large-project/\"]\n+    defaultObjectFilter = blob:none\n+----\n++\n+An explicit `--filter` option on the command line takes precedence\n+over this config, and `--no-filter` defeats it entirely to force a\n+full clone.  Only affects the initial clone; it has no effect on\n+later fetches into an existing repository.  If the server does not\n+support object filtering, the setting is silently ignored.\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 45d8fa0eed..18316a7da9 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -44,6 +44,7 @@\n #include \"path.h\"\n #include \"pkt-line.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"urlmatch.h\"\n #include \"hook.h\"\n #include \"bundle.h\"\n #include \"bundle-uri.h\"\n@@ -757,6 +758,51 @@ static int git_clone_config(const char *k, const char *v,\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n+static int clone_filter_collect(const char *var, const char *value,\n+\t\t\t\tconst struct config_context *ctx UNUSED,\n+\t\t\t\tvoid *cb)\n+{\n+\tchar **filter_spec_p = cb;\n+\n+\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tfree(*filter_spec_p);\n+\t\t*filter_spec_p = xstrdup(value);\n+\t}\n+\treturn 0;\n+}\n+\n+/*\n+ * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n+ * using the urlmatch infrastructure.  A URL-qualified entry that matches\n+ * the clone URL takes precedence over the bare form, following the same\n+ * rules as http.<url>.* configuration variables.\n+ */\n+static char *get_default_object_filter(const char *url)\n+{\n+\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n+\tchar *filter_spec = NULL;\n+\tchar *normalized_url;\n+\n+\tconfig.section = \"clone\";\n+\tconfig.key = \"defaultobjectfilter\";\n+\tconfig.collect_fn = clone_filter_collect;\n+\tconfig.cb = &filter_spec;\n+\n+\tnormalized_url = url_normalize(url, &config.url);\n+\tif (!normalized_url) {\n+\t\turlmatch_config_release(&config);\n+\t\treturn NULL;\n+\t}\n+\n+\trepo_config(the_repository, urlmatch_config_entry, &config);\n+\tfree(normalized_url);\n+\turlmatch_config_release(&config);\n+\n+\treturn filter_spec;\n+}\n+\n static int write_one_config(const char *key, const char *value,\n \t\t\t    const struct config_context *ctx,\n \t\t\t    void *data)\n@@ -1057,6 +1103,14 @@ int cmd_clone(int argc,\n \t} else\n \t\tdie(_(\"repository '%s' does not exist\"), repo_name);\n \n+\tif (!filter_options.choice && !filter_options.no_filter) {\n+\t\tchar *config_filter = get_default_object_filter(repo);\n+\t\tif (config_filter) {\n+\t\t\tparse_list_objects_filter(&filter_options, config_filter);\n+\t\t\tfree(config_filter);\n+\t\t}\n+\t}\n+\n \t/* no need to be strict, transport_set_option() will validate it again */\n \tif (option_depth && atoi(option_depth) < 1)\n \t\tdie(_(\"depth %s is not a positive number\"), option_depth);\ndiff --git a/t/t5616-partial-clone.sh b/t/t5616-partial-clone.sh\nindex 1e354e057f..e8cf5e353a 100755\n--- a/t/t5616-partial-clone.sh\n+++ b/t/t5616-partial-clone.sh\n@@ -722,6 +722,132 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work\n \tgit -C partial gc --prune=now\n '\n \n+# Test clone.<url>.defaultObjectFilter config\n+\n+test_expect_success 'setup for clone.defaultObjectFilter tests' '\n+\tgit init default-filter-src &&\n+\techo \"small\" >default-filter-src/small.txt &&\n+\tgit -C default-filter-src add . &&\n+\tgit -C default-filter-src commit -m \"initial\" &&\n+\n+\tgit clone --bare \"file://$(pwd)/default-filter-src\" default-filter-srv.bare &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&\n+\tgit -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '\n+\ttest_when_finished \"rm -r default-filter-clone\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-clone &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-clone config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"blob:limit=1024\" >expect &&\n+\tgit -C default-filter-clone config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '\n+\ttest_when_finished \"rm -r default-filter-override\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k\" \\\n+\t\tclone --filter=blob:none \"$SERVER_URL\" default-filter-override &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-override config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '\n+\ttest_when_finished \"rm -r default-filter-blobnone\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-blobnone &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-blobnone config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-blobnone config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '\n+\ttest_when_finished \"rm -r default-filter-tree0\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=tree:0\" clone \\\n+\t\t\"$SERVER_URL\" default-filter-tree0 &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-tree0 config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"tree:0\" >expect &&\n+\tgit -C default-filter-tree0 config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '\n+\ttest_when_finished \"rm -r default-filter-url-specific\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c \"clone.file://.defaultObjectFilter=blob:limit=1k\" \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-specific &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-url-specific config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '\n+\ttest_when_finished \"rm -r default-filter-url-nomatch\" &&\n+\tgit \\\n+\t\t-c \"clone.https://other.example.com/.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-url-nomatch &&\n+\n+\ttest_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor\n+'\n+\n+test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '\n+\ttest_when_finished \"rm -r default-filter-bare-key\" &&\n+\tgit -c clone.defaultObjectFilter=blob:none \\\n+\t\tclone \"file://$(pwd)/default-filter-srv.bare\" default-filter-bare-key &&\n+\n+\techo true >expect &&\n+\tgit -C default-filter-bare-key config --local remote.origin.promisor >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-bare-key config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '\n+\ttest_when_finished \"rm -r default-filter-url-over-bare\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit \\\n+\t\t-c clone.defaultObjectFilter=blob:limit=1k \\\n+\t\t-c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone \"$SERVER_URL\" default-filter-url-over-bare &&\n+\n+\techo \"blob:none\" >expect &&\n+\tgit -C default-filter-url-over-bare config --local remote.origin.partialclonefilter >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--no-filter defeats clone.defaultObjectFilter' '\n+\ttest_when_finished \"rm -r default-filter-no-filter\" &&\n+\tSERVER_URL=\"file://$(pwd)/default-filter-srv.bare\" &&\n+\tgit -c \"clone.$SERVER_URL.defaultObjectFilter=blob:none\" \\\n+\t\tclone --no-filter \"$SERVER_URL\" default-filter-no-filter &&\n+\n+\ttest_must_fail git -C default-filter-no-filter config --local remote.origin.promisor\n+'\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\n-- \ngitgitgadget\n"},{"id":"539051","messageId":"xmqqldfsrd7c.fsf@gitster.g","threadId":"65110","inReplyTo":"pull.2058.v6.git.1773553022381.gitgitgadget@gmail.com","subject":"Re: [PATCH v6] clone: add clone.<url>.defaultObjectFilter config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-15T21:32:23Z","receivedAt":"2026-03-15T21:32:25Z","isPatch":true,"body":"\"Alan Braithwaite via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Alan Braithwaite <alan@braithwaite.dev>\n>\n> Add a new configuration option that lets users specify a default\n> partial clone filter, optionally scoped by URL pattern.  When\n> cloning a repository whose URL matches a configured pattern,\n> git-clone automatically applies the filter, equivalent to passing\n> --filter on the command line.\n>\n>     [clone]\n>         defaultObjectFilter = blob:limit=1m\n>\n>     [clone \"https://github.com/\"]\n>         defaultObjectFilter = blob:limit=5m\n>\n>     [clone \"https://internal.corp.com/large-project/\"]\n>         defaultObjectFilter = blob:none\n>\n> The bare clone.defaultObjectFilter applies to all clones.  The\n> URL-qualified form clone.<url>.defaultObjectFilter restricts the\n> setting to matching URLs.  URL matching uses the existing\n> urlmatch_config_entry() infrastructure, following the same rules as\n> http.<url>.* — a domain, namespace, or specific project can be\n> matched, and the most specific match wins.\n>\n> The config only affects the initial clone.  Once the clone completes,\n> the filter is recorded in remote.<name>.partialCloneFilter, so\n> subsequent fetches inherit it automatically.  An explicit --filter\n> on the command line takes precedence, and --no-filter defeats the\n> configured default entirely.\n>\n> Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>\n> ---\n>     fetch, clone: add fetch.blobSizeLimit config\n>\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2058%2Fabraithwaite%2Falan%2Ffetch-blob-size-limit-v6\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2058/abraithwaite/alan/fetch-blob-size-limit-v6\n> Pull-Request: https://github.com/gitgitgadget/git/pull/2058\n\nI as a bistander reviewer would have appreciated some mention of\nwhere some changes relative to the previous iteration came from.\n\nE.g., check for !normalized_url case is from a realization that\nurl_normaize() can return NULL.  Use of test_when_finished all of\nthe place is to clean cruft after each test did its thing.\n\nWhat I am most unsure about is what the removal of \"large.bin\" in a\ntest is about.  What was it trying to achieve by having the file\nthat weighs 100kB, and what was the reason the file got removed (is\nit because whatever the presence of the file was trying to verify in\nthe previous iteration is already checked by other means and if so\nwhat is it?  Or is it something else?).\n\nMechanically generated range-diff alone does not answer questions\nlike the above.\n\nOther than the \"dd\" thing, everything is looking good.\n\nWill replace.  Thanks.\n"},{"id":"539074","messageId":"abe1l8ONmFIhzaxi@pks.im","threadId":"65110","inReplyTo":"pull.2058.v6.git.1773553022381.gitgitgadget@gmail.com","subject":"Re: [PATCH v6] clone: add clone.<url>.defaultObjectFilter config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-16T07:47:35Z","receivedAt":"2026-03-16T07:47:41Z","isPatch":true,"body":"On Sun, Mar 15, 2026 at 05:37:02AM +0000, Alan Braithwaite via GitGitGadget wrote:\n>  1:  fa1ea69bdb ! 1:  480453b2e7 clone: add clone.<url>.defaultObjectFilter config\n>      @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n>       +test_expect_success 'setup for clone.defaultObjectFilter tests' '\n>       +\tgit init default-filter-src &&\n>       +\techo \"small\" >default-filter-src/small.txt &&\n>      -+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n>       +\tgit -C default-filter-src add . &&\n>       +\tgit -C default-filter-src commit -m \"initial\" &&\n>       +\n\nAs Junio already pointed out, this change here is a bit puzzling. Not\nthat I think it's a problem, but one wonders why this existed in the\nfirst place if it seemed to not be necessary.\n\n> diff --git a/builtin/clone.c b/builtin/clone.c\n> index 45d8fa0eed..18316a7da9 100644\n> --- a/builtin/clone.c\n> +++ b/builtin/clone.c\n> @@ -757,6 +758,51 @@ static int git_clone_config(const char *k, const char *v,\n>  \treturn git_default_config(k, v, ctx, cb);\n>  }\n>  \n> +static int clone_filter_collect(const char *var, const char *value,\n> +\t\t\t\tconst struct config_context *ctx UNUSED,\n> +\t\t\t\tvoid *cb)\n> +{\n> +\tchar **filter_spec_p = cb;\n> +\n> +\tif (!strcmp(var, \"clone.defaultobjectfilter\")) {\n> +\t\tif (!value)\n> +\t\t\treturn config_error_nonbool(var);\n> +\t\tfree(*filter_spec_p);\n> +\t\t*filter_spec_p = xstrdup(value);\n> +\t}\n> +\treturn 0;\n> +}\n> +\n> +/*\n> + * Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter\n> + * using the urlmatch infrastructure.  A URL-qualified entry that matches\n> + * the clone URL takes precedence over the bare form, following the same\n> + * rules as http.<url>.* configuration variables.\n> + */\n> +static char *get_default_object_filter(const char *url)\n> +{\n> +\tstruct urlmatch_config config = URLMATCH_CONFIG_INIT;\n> +\tchar *filter_spec = NULL;\n> +\tchar *normalized_url;\n> +\n> +\tconfig.section = \"clone\";\n> +\tconfig.key = \"defaultobjectfilter\";\n> +\tconfig.collect_fn = clone_filter_collect;\n> +\tconfig.cb = &filter_spec;\n> +\n> +\tnormalized_url = url_normalize(url, &config.url);\n> +\tif (!normalized_url) {\n> +\t\turlmatch_config_release(&config);\n> +\t\treturn NULL;\n> +\t}\n\nWe haven't allocated anything, right? So in theory, we should be able to\nreturn early without calling `urlmatch_config_release()`. This could be\nstressed further by moving the error path earlier, so that it's the\nfirst thing we do in the function.\n\nPatrick\n"},{"id":"543000","messageId":"xmqq8q9qvffs.fsf@gitster.g","threadId":"65110","inReplyTo":"abe1l8ONmFIhzaxi@pks.im","subject":"Re: [PATCH v6] clone: add clone.<url>.defaultObjectFilter config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-11T02:38:15Z","receivedAt":"2026-05-11T02:38:18Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Sun, Mar 15, 2026 at 05:37:02AM +0000, Alan Braithwaite via GitGitGadget wrote:\n>>  1:  fa1ea69bdb ! 1:  480453b2e7 clone: add clone.<url>.defaultObjectFilter config\n>>      @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n>>       +test_expect_success 'setup for clone.defaultObjectFilter tests' '\n>>       +\tgit init default-filter-src &&\n>>       +\techo \"small\" >default-filter-src/small.txt &&\n>>      -+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n>>       +\tgit -C default-filter-src add . &&\n>>       +\tgit -C default-filter-src commit -m \"initial\" &&\n>>       +\n>\n> As Junio already pointed out, this change here is a bit puzzling. Not\n> that I think it's a problem, but one wonders why this existed in the\n> first place if it seemed to not be necessary.\n>> ...\n>> +\tnormalized_url = url_normalize(url, &config.url);\n>> +\tif (!normalized_url) {\n>> +\t\turlmatch_config_release(&config);\n>> +\t\treturn NULL;\n>> +\t}\n>\n> We haven't allocated anything, right? So in theory, we should be able to\n> return early without calling `urlmatch_config_release()`. This could be\n> stressed further by moving the error path earlier, so that it's the\n> first thing we do in the function.\n\n\nWe haven't heard any response to these points raised in the message\nI am responding to.  Should I still keep the patch in my tree,\nhoping that a responses may come some day?  I am tempted to discard\nthe topic as it has been quite a while since we last looked at it.\n\nThanks.\n"},{"id":"543025","messageId":"agGFi9G7HxatnyFs@pks.im","threadId":"65110","inReplyTo":"xmqq8q9qvffs.fsf@gitster.g","subject":"Re: [PATCH v6] clone: add clone.<url>.defaultObjectFilter config","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-11T07:30:19Z","receivedAt":"2026-05-11T07:30:25Z","isPatch":true,"body":"On Mon, May 11, 2026 at 11:38:15AM +0900, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > On Sun, Mar 15, 2026 at 05:37:02AM +0000, Alan Braithwaite via GitGitGadget wrote:\n> >>  1:  fa1ea69bdb ! 1:  480453b2e7 clone: add clone.<url>.defaultObjectFilter config\n> >>      @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non\n> >>       +test_expect_success 'setup for clone.defaultObjectFilter tests' '\n> >>       +\tgit init default-filter-src &&\n> >>       +\techo \"small\" >default-filter-src/small.txt &&\n> >>      -+\tdd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&\n> >>       +\tgit -C default-filter-src add . &&\n> >>       +\tgit -C default-filter-src commit -m \"initial\" &&\n> >>       +\n> >\n> > As Junio already pointed out, this change here is a bit puzzling. Not\n> > that I think it's a problem, but one wonders why this existed in the\n> > first place if it seemed to not be necessary.\n> >> ...\n> >> +\tnormalized_url = url_normalize(url, &config.url);\n> >> +\tif (!normalized_url) {\n> >> +\t\turlmatch_config_release(&config);\n> >> +\t\treturn NULL;\n> >> +\t}\n> >\n> > We haven't allocated anything, right? So in theory, we should be able to\n> > return early without calling `urlmatch_config_release()`. This could be\n> > stressed further by moving the error path earlier, so that it's the\n> > first thing we do in the function.\n> \n> \n> We haven't heard any response to these points raised in the message\n> I am responding to.  Should I still keep the patch in my tree,\n> hoping that a responses may come some day?  I am tempted to discard\n> the topic as it has been quite a while since we last looked at it.\n\nIt's been a while indeed. I'd say we can discard it for now, as we can\neasily add it back in at a later point in time once the next version is\nposted.\n\nPatrick\n"}]}