]> git.ipfire.org Git - thirdparty/git.git/commitdiff
clone: add clone.<url>.defaultObjectFilter config
authorAlan Braithwaite <alan@braithwaite.dev>
Sun, 15 Mar 2026 05:37:02 +0000 (05:37 +0000)
committerJunio C Hamano <gitster@pobox.com>
Sun, 15 Mar 2026 21:24:19 +0000 (14:24 -0700)
Add a new configuration option that lets users specify a default
partial clone filter, optionally scoped by URL pattern.  When
cloning a repository whose URL matches a configured pattern,
git-clone automatically applies the filter, equivalent to passing
--filter on the command line.

    [clone]
        defaultObjectFilter = blob:limit=1m

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

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

The bare clone.defaultObjectFilter applies to all clones.  The
URL-qualified form clone.<url>.defaultObjectFilter restricts the
setting to matching URLs.  URL matching uses the existing
urlmatch_config_entry() infrastructure, following the same rules as
http.<url>.* — a domain, namespace, or specific project can be
matched, and the most specific match wins.

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

Signed-off-by: Alan Braithwaite <alan@braithwaite.dev>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/config/clone.adoc
builtin/clone.c
t/t5616-partial-clone.sh

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