Skip to content

Commit 5d76a10

Browse files
abraithwaitegitster
authored andcommitted
clone: add clone.<url>.defaultObjectFilter config
Add a new configuration option that lets users specify a default partial clone filter per URL pattern. When cloning a repository whose URL matches a configured pattern, git-clone automatically applies the filter, equivalent to passing --filter on the command line. [clone "https://github.com/"] defaultObjectFilter = blob:limit=5m [clone "https://internal.corp.com/large-project/"] defaultObjectFilter = blob:none URL matching uses the existing urlmatch_config_entry() infrastructure, following the same rules as http.<url>.* — you can match a domain, a namespace path, or a specific project, and the most specific match wins. The config only affects the initial clone. Once the clone completes, the filter is recorded in remote.<name>.partialCloneFilter, so subsequent fetches inherit it automatically. An explicit --filter flag on the command line takes precedence. Only the URL-qualified form (clone.<url>.defaultObjectFilter) is honored; a bare clone.defaultObjectFilter without a URL subsection is ignored. Signed-off-by: Alan Braithwaite <alan@braithwaite.dev> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 795c338 commit 5d76a10

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

Documentation/config/clone.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,29 @@ 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.<url>.defaultObjectFilter`::
26+
When set to a filter spec string (e.g., `blob:limit=1m`,
27+
`blob:none`, `tree:0`), linkgit:git-clone[1] will automatically
28+
use `--filter=<value>` when the clone URL matches `<url>`.
29+
Objects matching the filter are excluded from the initial
30+
transfer and lazily fetched on demand (e.g., during checkout).
31+
Subsequent fetches inherit the filter via the per-remote config
32+
that is written during the clone.
33+
+
34+
The URL matching follows the same rules as `http.<url>.*` (see
35+
linkgit:git-config[1]). The most specific URL match wins. You can
36+
match a complete domain, a namespace, or a specific project:
37+
+
38+
----
39+
[clone "https://github.com/"]
40+
defaultObjectFilter = blob:limit=5m
41+
42+
[clone "https://internal.corp.com/large-project/"]
43+
defaultObjectFilter = blob:none
44+
----
45+
+
46+
An explicit `--filter` option on the command line takes precedence
47+
over this config. Only affects the initial clone; it has no effect
48+
on later fetches into an existing repository. If the server does
49+
not support object filtering, the setting is silently ignored.

builtin/clone.c

Lines changed: 68 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"
@@ -759,6 +760,65 @@ static int git_clone_config(const char *k, const char *v,
759760
return git_default_config(k, v, ctx, cb);
760761
}
761762

763+
struct clone_filter_data {
764+
char *default_object_filter;
765+
};
766+
767+
static int clone_filter_collect(const char *var, const char *value,
768+
const struct config_context *ctx UNUSED,
769+
void *cb)
770+
{
771+
struct clone_filter_data *data = cb;
772+
773+
if (!strcmp(var, "clone.defaultobjectfilter")) {
774+
free(data->default_object_filter);
775+
data->default_object_filter = xstrdup(value);
776+
}
777+
return 0;
778+
}
779+
780+
/*
781+
* Look up clone.<url>.defaultObjectFilter using the urlmatch
782+
* infrastructure. Only URL-qualified forms are supported; a bare
783+
* clone.defaultObjectFilter (without a URL) is ignored.
784+
*/
785+
static char *get_default_object_filter(const char *url)
786+
{
787+
struct urlmatch_config config = URLMATCH_CONFIG_INIT;
788+
struct clone_filter_data data = { 0 };
789+
struct string_list_item *item;
790+
char *normalized_url;
791+
792+
config.section = "clone";
793+
config.key = "defaultobjectfilter";
794+
config.collect_fn = clone_filter_collect;
795+
config.cascade_fn = git_clone_config;
796+
config.cb = &data;
797+
798+
normalized_url = url_normalize(url, &config.url);
799+
800+
repo_config(the_repository, urlmatch_config_entry, &config);
801+
free(normalized_url);
802+
803+
/*
804+
* Reject the bare form clone.defaultObjectFilter (no URL
805+
* subsection). urlmatch stores the best match in vars with
806+
* hostmatch_len == 0 for non-URL-qualified entries; discard
807+
* the result if that is what we got.
808+
*/
809+
item = string_list_lookup(&config.vars, "defaultobjectfilter");
810+
if (item) {
811+
const struct urlmatch_item *m = item->util;
812+
if (!m->hostmatch_len && !m->pathmatch_len) {
813+
FREE_AND_NULL(data.default_object_filter);
814+
}
815+
}
816+
817+
urlmatch_config_release(&config);
818+
819+
return data.default_object_filter;
820+
}
821+
762822
static int write_one_config(const char *key, const char *value,
763823
const struct config_context *ctx,
764824
void *data)
@@ -1059,6 +1119,14 @@ int cmd_clone(int argc,
10591119
} else
10601120
die(_("repository '%s' does not exist"), repo_name);
10611121

1122+
if (!filter_options.choice) {
1123+
char *config_filter = get_default_object_filter(repo);
1124+
if (config_filter) {
1125+
parse_list_objects_filter(&filter_options, config_filter);
1126+
free(config_filter);
1127+
}
1128+
}
1129+
10621130
/* no need to be strict, transport_set_option() will validate it again */
10631131
if (option_depth && atoi(option_depth) < 1)
10641132
die(_("depth %s is not a positive number"), option_depth);

t/t5616-partial-clone.sh

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

726+
# Test clone.<url>.defaultObjectFilter config
727+
728+
test_expect_success 'setup for clone.defaultObjectFilter tests' '
729+
git init default-filter-src &&
730+
echo "small" >default-filter-src/small.txt &&
731+
dd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&
732+
git -C default-filter-src add . &&
733+
git -C default-filter-src commit -m "initial" &&
734+
735+
git clone --bare "file://$(pwd)/default-filter-src" default-filter-srv.bare &&
736+
git -C default-filter-srv.bare config --local uploadpack.allowfilter 1 &&
737+
git -C default-filter-srv.bare config --local uploadpack.allowanysha1inwant 1
738+
'
739+
740+
test_expect_success 'clone with clone.<url>.defaultObjectFilter applies filter' '
741+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
742+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" clone \
743+
"$SERVER_URL" default-filter-clone &&
744+
745+
test "$(git -C default-filter-clone config --local remote.origin.promisor)" = "true" &&
746+
test "$(git -C default-filter-clone config --local remote.origin.partialclonefilter)" = "blob:limit=1024"
747+
'
748+
749+
test_expect_success 'clone with --filter overrides clone.<url>.defaultObjectFilter' '
750+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
751+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:limit=1k" \
752+
clone --filter=blob:none "$SERVER_URL" default-filter-override &&
753+
754+
test "$(git -C default-filter-override config --local remote.origin.partialclonefilter)" = "blob:none"
755+
'
756+
757+
test_expect_success 'clone with clone.<url>.defaultObjectFilter=blob:none works' '
758+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
759+
git -c "clone.$SERVER_URL.defaultObjectFilter=blob:none" clone \
760+
"$SERVER_URL" default-filter-blobnone &&
761+
762+
test "$(git -C default-filter-blobnone config --local remote.origin.promisor)" = "true" &&
763+
test "$(git -C default-filter-blobnone config --local remote.origin.partialclonefilter)" = "blob:none"
764+
'
765+
766+
test_expect_success 'clone.<url>.defaultObjectFilter with tree:0 works' '
767+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
768+
git -c "clone.$SERVER_URL.defaultObjectFilter=tree:0" clone \
769+
"$SERVER_URL" default-filter-tree0 &&
770+
771+
test "$(git -C default-filter-tree0 config --local remote.origin.promisor)" = "true" &&
772+
test "$(git -C default-filter-tree0 config --local remote.origin.partialclonefilter)" = "tree:0"
773+
'
774+
775+
test_expect_success 'most specific URL match wins for clone.defaultObjectFilter' '
776+
SERVER_URL="file://$(pwd)/default-filter-srv.bare" &&
777+
git \
778+
-c "clone.file://.defaultObjectFilter=blob:limit=1k" \
779+
-c "clone.$SERVER_URL.defaultObjectFilter=blob:none" \
780+
clone "$SERVER_URL" default-filter-url-specific &&
781+
782+
test "$(git -C default-filter-url-specific config --local remote.origin.partialclonefilter)" = "blob:none"
783+
'
784+
785+
test_expect_success 'non-matching URL does not apply clone.defaultObjectFilter' '
786+
git \
787+
-c "clone.https://other.example.com/.defaultObjectFilter=blob:none" \
788+
clone "file://$(pwd)/default-filter-srv.bare" default-filter-url-nomatch &&
789+
790+
test_must_fail git -C default-filter-url-nomatch config --local remote.origin.promisor
791+
'
792+
793+
test_expect_success 'bare clone.defaultObjectFilter without URL is ignored' '
794+
git -c clone.defaultObjectFilter=blob:none \
795+
clone "file://$(pwd)/default-filter-srv.bare" default-filter-bare-key &&
796+
797+
test_must_fail git -C default-filter-bare-key config --local remote.origin.promisor
798+
'
726799

727800
. "$TEST_DIRECTORY"/lib-httpd.sh
728801
start_httpd

0 commit comments

Comments
 (0)