Skip to content

Commit 480453b

Browse files
committed
clone: add clone.<url>.defaultObjectFilter config
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>
1 parent 7b2bccb commit 480453b

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed

Documentation/config/clone.adoc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,37 @@ endif::[]
2121
If a partial clone filter is provided (see `--filter` in
2222
linkgit:git-rev-list[1]) and `--recurse-submodules` is used, also apply
2323
the filter to submodules.
24+
25+
`clone.defaultObjectFilter`::
26+
`clone.<url>.defaultObjectFilter`::
27+
When set to a filter spec string (e.g., `blob:limit=1m`,
28+
`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically
29+
use `--filter=<value>` to enable partial clone behavior.
30+
Objects matching the filter are excluded from the initial
31+
transfer and lazily fetched on demand (e.g., during checkout).
32+
Subsequent fetches inherit the filter via the per-remote config
33+
that is written during the clone.
34+
+
35+
The bare `clone.defaultObjectFilter` applies to all clones. The
36+
URL-qualified form `clone.<url>.defaultObjectFilter` restricts the
37+
setting to clones whose URL matches `<url>`, following the same
38+
rules as `http.<url>.*` (see linkgit:git-config[1]). The most
39+
specific URL match wins. You can match a domain, a namespace, or a
40+
specific project:
41+
+
42+
----
43+
[clone]
44+
defaultObjectFilter = blob:limit=1m
45+
46+
[clone "https://github.com/"]
47+
defaultObjectFilter = blob:limit=5m
48+
49+
[clone "https://internal.corp.com/large-project/"]
50+
defaultObjectFilter = blob:none
51+
----
52+
+
53+
An explicit `--filter` option on the command line takes precedence
54+
over this config, and `--no-filter` defeats it entirely to force a
55+
full clone. Only affects the initial clone; it has no effect on
56+
later fetches into an existing repository. If the server does not
57+
support object filtering, the setting is silently ignored.

builtin/clone.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "path.h"
4545
#include "pkt-line.h"
4646
#include "list-objects-filter-options.h"
47+
#include "urlmatch.h"
4748
#include "hook.h"
4849
#include "bundle.h"
4950
#include "bundle-uri.h"
@@ -757,6 +758,51 @@ static int git_clone_config(const char *k, const char *v,
757758
return git_default_config(k, v, ctx, cb);
758759
}
759760

761+
static int clone_filter_collect(const char *var, const char *value,
762+
const struct config_context *ctx UNUSED,
763+
void *cb)
764+
{
765+
char **filter_spec_p = cb;
766+
767+
if (!strcmp(var, "clone.defaultobjectfilter")) {
768+
if (!value)
769+
return config_error_nonbool(var);
770+
free(*filter_spec_p);
771+
*filter_spec_p = xstrdup(value);
772+
}
773+
return 0;
774+
}
775+
776+
/*
777+
* Look up clone.defaultObjectFilter or clone.<url>.defaultObjectFilter
778+
* using the urlmatch infrastructure. A URL-qualified entry that matches
779+
* the clone URL takes precedence over the bare form, following the same
780+
* rules as http.<url>.* configuration variables.
781+
*/
782+
static char *get_default_object_filter(const char *url)
783+
{
784+
struct urlmatch_config config = URLMATCH_CONFIG_INIT;
785+
char *filter_spec = NULL;
786+
char *normalized_url;
787+
788+
config.section = "clone";
789+
config.key = "defaultobjectfilter";
790+
config.collect_fn = clone_filter_collect;
791+
config.cb = &filter_spec;
792+
793+
normalized_url = url_normalize(url, &config.url);
794+
if (!normalized_url) {
795+
urlmatch_config_release(&config);
796+
return NULL;
797+
}
798+
799+
repo_config(the_repository, urlmatch_config_entry, &config);
800+
free(normalized_url);
801+
urlmatch_config_release(&config);
802+
803+
return filter_spec;
804+
}
805+
760806
static int write_one_config(const char *key, const char *value,
761807
const struct config_context *ctx,
762808
void *data)
@@ -1057,6 +1103,14 @@ int cmd_clone(int argc,
10571103
} else
10581104
die(_("repository '%s' does not exist"), repo_name);
10591105

1106+
if (!filter_options.choice && !filter_options.no_filter) {
1107+
char *config_filter = get_default_object_filter(repo);
1108+
if (config_filter) {
1109+
parse_list_objects_filter(&filter_options, config_filter);
1110+
free(config_filter);
1111+
}
1112+
}
1113+
10601114
/* no need to be strict, transport_set_option() will validate it again */
10611115
if (option_depth && atoi(option_depth) < 1)
10621116
die(_("depth %s is not a positive number"), option_depth);

t/t5616-partial-clone.sh

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,132 @@ test_expect_success 'after fetching descendants of non-promisor commits, gc work
722722
git -C partial gc --prune=now
723723
'
724724

725+
# Test clone.<url>.defaultObjectFilter config
726+
727+
test_expect_success 'setup for clone.defaultObjectFilter tests' '
728+
git init default-filter-src &&
729+
echo "small" >default-filter-src/small.txt &&
730+
git -C default-filter-src add . &&
731+
git -C default-filter-src commit -m "initial" &&
732+
733+
git clone --bare "file://$(pwd)/default-filter-src" default-filter-srv.bare &&
734+
git -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&
735+
git -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1
736+
'
737+
738+
test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '
739+
test_when_finished "rm -r default-filter-clone" &&
740+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
741+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" clone \
742+
"$SERVER_URL" default-filter-clone &&
743+
744+
echo true >expect &&
745+
git -C default-filter-clone config --local remote.origin.promisor >actual &&
746+
test_cmp expect actual &&
747+
748+
echo "blob:limit=1024" >expect &&
749+
git -C default-filter-clone config --local remote.origin.partialclonefilter >actual &&
750+
test_cmp expect actual
751+
'
752+
753+
test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '
754+
test_when_finished "rm -r default-filter-override" &&
755+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
756+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" \
757+
clone --filter=blob:none "$SERVER_URL" default-filter-override &&
758+
759+
echo "blob:none" >expect &&
760+
git -C default-filter-override config --local remote.origin.partialclonefilter >actual &&
761+
test_cmp expect actual
762+
'
763+
764+
test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '
765+
test_when_finished "rm -r default-filter-blobnone" &&
766+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
767+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" clone \
768+
"$SERVER_URL" default-filter-blobnone &&
769+
770+
echo true >expect &&
771+
git -C default-filter-blobnone config --local remote.origin.promisor >actual &&
772+
test_cmp expect actual &&
773+
774+
echo "blob:none" >expect &&
775+
git -C default-filter-blobnone config --local remote.origin.partialclonefilter >actual &&
776+
test_cmp expect actual
777+
'
778+
779+
test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '
780+
test_when_finished "rm -r default-filter-tree0" &&
781+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
782+
git -c "clone.$SERVER_URL.defaultObjectFilter=tree:0" clone \
783+
"$SERVER_URL" default-filter-tree0 &&
784+
785+
echo true >expect &&
786+
git -C default-filter-tree0 config --local remote.origin.promisor >actual &&
787+
test_cmp expect actual &&
788+
789+
echo "tree:0" >expect &&
790+
git -C default-filter-tree0 config --local remote.origin.partialclonefilter >actual &&
791+
test_cmp expect actual
792+
'
793+
794+
test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '
795+
test_when_finished "rm -r default-filter-url-specific" &&
796+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
797+
git \
798+
-c "clone.file://.defaultObjectFilter=blob:limit=1k" \
799+
-c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
800+
clone "$SERVER_URL" default-filter-url-specific &&
801+
802+
echo "blob:none" >expect &&
803+
git -C default-filter-url-specific config --local remote.origin.partialclonefilter >actual &&
804+
test_cmp expect actual
805+
'
806+
807+
test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '
808+
test_when_finished "rm -r default-filter-url-nomatch" &&
809+
git \
810+
-c "clone.https://other.example.com/.defaultObjectFilter=blob:none" \
811+
clone "file://$(pwd)/default-filter-srv.bare" default-filter-url-nomatch &&
812+
813+
test_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor
814+
'
815+
816+
test_expect_success 'bare clone.defaultObjectFilter applies to all clones' '
817+
test_when_finished "rm -r default-filter-bare-key" &&
818+
git -c clone.defaultObjectFilter=blob:none \
819+
clone "file://$(pwd)/default-filter-srv.bare" default-filter-bare-key &&
820+
821+
echo true >expect &&
822+
git -C default-filter-bare-key config --local remote.origin.promisor >actual &&
823+
test_cmp expect actual &&
824+
825+
echo "blob:none" >expect &&
826+
git -C default-filter-bare-key config --local remote.origin.partialclonefilter >actual &&
827+
test_cmp expect actual
828+
'
829+
830+
test_expect_success 'URL-specific clone.defaultObjectFilter overrides bare form' '
831+
test_when_finished "rm -r default-filter-url-over-bare" &&
832+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
833+
git \
834+
-c clone.defaultObjectFilter=blob:limit=1k \
835+
-c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
836+
clone "$SERVER_URL" default-filter-url-over-bare &&
837+
838+
echo "blob:none" >expect &&
839+
git -C default-filter-url-over-bare config --local remote.origin.partialclonefilter >actual &&
840+
test_cmp expect actual
841+
'
842+
843+
test_expect_success '--no-filter defeats clone.defaultObjectFilter' '
844+
test_when_finished "rm -r default-filter-no-filter" &&
845+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
846+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
847+
clone --no-filter "$SERVER_URL" default-filter-no-filter &&
848+
849+
test_must_fail git -C default-filter-no-filter config --local remote.origin.promisor
850+
'
725851

726852
. "$TEST_DIRECTORY"/lib-httpd.sh
727853
start_httpd

0 commit comments

Comments
 (0)