Merge branch 'cc/lop-filter-auto' into seen

"auto filter" logic for large-object promisor remote.

Comments?

* cc/lop-filter-auto:
  fetch-pack: wire up and enable auto filter logic
  promisor-remote: keep advertised filter in memory
  list-objects-filter-options: implement auto filter resolution
  list-objects-filter-options: support 'auto' mode for --filter
  doc: fetch: document `--filter=<filter-spec>` option
  fetch: make filter_options local to cmd_fetch()
  clone: make filter_options local to cmd_clone()
  promisor-remote: allow a client to store fields
  promisor-remote: refactor initialising field lists
This commit is contained in:
Junio C Hamano 2026-01-08 16:41:18 +09:00
commit 285db44532
17 changed files with 664 additions and 57 deletions

View File

@ -89,3 +89,36 @@ variable. The fields are checked only if the
`promisor.acceptFromServer` config variable is not set to "None". If `promisor.acceptFromServer` config variable is not set to "None". If
set to "None", this config variable has no effect. See set to "None", this config variable has no effect. See
linkgit:gitprotocol-v2[5]. linkgit:gitprotocol-v2[5].
promisor.storeFields::
A comma or space separated list of additional remote related
field names. If a client accepts an advertised remote, the
client will store the values associated with these field names
taken from the remote advertisement into its configuration,
and then reload its remote configuration. Currently,
"partialCloneFilter" and "token" are the only supported field
names.
+
For example if a server advertises "partialCloneFilter=blob:limit=20k"
for remote "foo", and that remote is accepted, then "blob:limit=20k"
will be stored for the "remote.foo.partialCloneFilter" configuration
variable.
+
If the new field value from an advertised remote is the same as the
existing field value for that remote on the client side, then no
change is made to the client configuration though.
+
When a new value is stored, a message is printed to standard error to
let users know about this.
+
Note that for security reasons, if the remote is not already
configured on the client side, nothing will be stored for that
remote. In any case, no new remote will be created and no URL will be
stored.
+
Before storing a partial clone filter, it's parsed to check it's
valid. If it's not, a warning is emitted and it's not stored.
+
Before storing a token, a check is performed to ensure it contains no
control character. If the check fails, a warning is emitted and it's
not stored.

View File

@ -88,6 +88,25 @@ linkgit:git-config[1].
This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes This is incompatible with `--recurse-submodules=(yes|on-demand)` and takes
precedence over the `fetch.output` config option. precedence over the `fetch.output` config option.
--filter=<filter-spec>::
Use the partial clone feature and request that the server sends
a subset of reachable objects according to a given object filter.
When using `--filter`, the supplied _<filter-spec>_ is used for
the partial fetch.
+
If `--filter=auto` is used, the filter specification is determined
automatically by combining the filter specifications advertised by
the server for the promisor remotes that the client accepts (see
linkgit:gitprotocol-v2[5] and the `promisor.acceptFromServer`
configuration option in linkgit:git-config[1]).
+
For details on all other available filter specifications, see the
`--filter=<filter-spec>` option in linkgit:git-rev-list[1].
+
For example, `--filter=blob:none` will filter out all blobs (file
contents) until needed by Git. Also, `--filter=blob:limit=<size>` will
filter out all blobs of size at least _<size>_.
ifndef::git-pull[] ifndef::git-pull[]
`--write-fetch-head`:: `--write-fetch-head`::
`--no-write-fetch-head`:: `--no-write-fetch-head`::

View File

@ -187,11 +187,26 @@ objects from the source repository into a pack in the cloned repository.
Use the partial clone feature and request that the server sends Use the partial clone feature and request that the server sends
a subset of reachable objects according to a given object filter. a subset of reachable objects according to a given object filter.
When using `--filter`, the supplied _<filter-spec>_ is used for When using `--filter`, the supplied _<filter-spec>_ is used for
the partial clone filter. For example, `--filter=blob:none` will the partial clone filter.
filter out all blobs (file contents) until needed by Git. Also, +
`--filter=blob:limit=<size>` will filter out all blobs of size If `--filter=auto` is used the filter specification is determined
at least _<size>_. For more details on filter specifications, see automatically through the 'promisor-remote' protocol (see
the `--filter` option in linkgit:git-rev-list[1]. linkgit:gitprotocol-v2[5]) by combining the filter specifications
advertised by the server for the promisor remotes that the client
accepts (see the `promisor.acceptFromServer` configuration option in
linkgit:git-config[1]). This allows the server to suggest the optimal
filter for the available promisor remotes.
+
As with other filter specifications, the "auto" value is persisted in
the configuration. This ensures that future fetches will continue to
adapt to the server's current recommendation.
+
For details on all other available filter specifications, see the
`--filter=<filter-spec>` option in linkgit:git-rev-list[1].
+
For example, `--filter=blob:none` will filter out all blobs (file
contents) until needed by Git. Also, `--filter=blob:limit=<size>` will
filter out all blobs of size at least _<size>_.
`--also-filter-submodules`:: `--also-filter-submodules`::
Also apply the partial clone filter to any submodules in the repository. Also apply the partial clone filter to any submodules in the repository.

View File

@ -812,10 +812,15 @@ MUST appear first in each pr-fields, in that order.
After these mandatory fields, the server MAY advertise the following After these mandatory fields, the server MAY advertise the following
optional fields in any order: optional fields in any order:
`partialCloneFilter`:: The filter specification used by the remote. `partialCloneFilter`:: The filter specification for the remote. It
corresponds to the "remote.<name>.partialCloneFilter" config setting.
Clients can use this to determine if the remote's filtering strategy Clients can use this to determine if the remote's filtering strategy
is compatible with their needs (e.g., checking if both use "blob:none"). is compatible with their needs (e.g., checking if both use
It corresponds to the "remote.<name>.partialCloneFilter" config setting. "blob:none"). Additionally they can use this through the
`--filter=auto` option in linkgit:git-clone[1]. With that option, the
filter specification of the clone will be automatically computed by
combining the filter specifications of the promisor remotes the client
accepts.
`token`:: An authentication token that clients can use when `token`:: An authentication token that clients can use when
connecting to the remote. It corresponds to the "remote.<name>.token" connecting to the remote. It corresponds to the "remote.<name>.token"
@ -826,9 +831,11 @@ are case-sensitive and MUST be transmitted exactly as specified
above. Clients MUST ignore fields they don't recognize to allow for above. Clients MUST ignore fields they don't recognize to allow for
future protocol extensions. future protocol extensions.
For now, the client can only use information transmitted through these The client can use information transmitted through these fields to
fields to decide if it accepts the advertised promisor remote. In the decide if it accepts the advertised promisor remote. Also, the client
future that information might be used for other purposes though. can be configured to store the values of these fields or use them
to automatically configure the repository (see "promisor.storeFields"
in linkgit:git-config[1] and `--filter=auto` in linkgit:git-clone[1]).
Field values MUST be urlencoded. Field values MUST be urlencoded.
@ -856,8 +863,9 @@ the server advertised, the client shouldn't advertise the
On the server side, the "promisor.advertise" and "promisor.sendFields" On the server side, the "promisor.advertise" and "promisor.sendFields"
configuration options can be used to control what it advertises. On configuration options can be used to control what it advertises. On
the client side, the "promisor.acceptFromServer" configuration option the client side, the "promisor.acceptFromServer" configuration option
can be used to control what it accepts. See the documentation of these can be used to control what it accepts, and the "promisor.storeFields"
configuration options for more information. option, to control what it stores. See the documentation of these
configuration options in linkgit:git-config[1] for more information.
Note that in the future it would be nice if the "promisor-remote" Note that in the future it would be nice if the "promisor-remote"
protocol capability could be used by the server, when responding to protocol capability could be used by the server, when responding to

View File

@ -1517,6 +1517,7 @@ CLAR_TEST_SUITES += u-dir
CLAR_TEST_SUITES += u-example-decorate CLAR_TEST_SUITES += u-example-decorate
CLAR_TEST_SUITES += u-hash CLAR_TEST_SUITES += u-hash
CLAR_TEST_SUITES += u-hashmap CLAR_TEST_SUITES += u-hashmap
CLAR_TEST_SUITES += u-list-objects-filter-options
CLAR_TEST_SUITES += u-mem-pool CLAR_TEST_SUITES += u-mem-pool
CLAR_TEST_SUITES += u-oid-array CLAR_TEST_SUITES += u-oid-array
CLAR_TEST_SUITES += u-oidmap CLAR_TEST_SUITES += u-oidmap

View File

@ -77,7 +77,6 @@ static struct string_list option_required_reference = STRING_LIST_INIT_NODUP;
static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP; static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP;
static int max_jobs = -1; static int max_jobs = -1;
static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP; static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP;
static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;
static int config_filter_submodules = -1; /* unspecified */ static int config_filter_submodules = -1; /* unspecified */
static int option_remote_submodules; static int option_remote_submodules;
@ -634,7 +633,9 @@ static int git_sparse_checkout_init(const char *repo)
return result; return result;
} }
static int checkout(int submodule_progress, int filter_submodules, static int checkout(int submodule_progress,
struct list_objects_filter_options *filter_options,
int filter_submodules,
enum ref_storage_format ref_storage_format) enum ref_storage_format ref_storage_format)
{ {
struct object_id oid; struct object_id oid;
@ -723,9 +724,9 @@ static int checkout(int submodule_progress, int filter_submodules,
strvec_pushf(&cmd.args, "--ref-format=%s", strvec_pushf(&cmd.args, "--ref-format=%s",
ref_storage_format_to_name(ref_storage_format)); ref_storage_format_to_name(ref_storage_format));
if (filter_submodules && filter_options.choice) if (filter_submodules && filter_options->choice)
strvec_pushf(&cmd.args, "--filter=%s", strvec_pushf(&cmd.args, "--filter=%s",
expand_list_objects_filter_spec(&filter_options)); expand_list_objects_filter_spec(filter_options));
if (option_single_branch >= 0) if (option_single_branch >= 0)
strvec_push(&cmd.args, option_single_branch ? strvec_push(&cmd.args, option_single_branch ?
@ -903,6 +904,7 @@ int cmd_clone(int argc,
enum transport_family family = TRANSPORT_FAMILY_ALL; enum transport_family family = TRANSPORT_FAMILY_ALL;
struct string_list option_config = STRING_LIST_INIT_DUP; struct string_list option_config = STRING_LIST_INIT_DUP;
int option_dissociate = 0; int option_dissociate = 0;
struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;
int option_filter_submodules = -1; /* unspecified */ int option_filter_submodules = -1; /* unspecified */
struct string_list server_options = STRING_LIST_INIT_NODUP; struct string_list server_options = STRING_LIST_INIT_NODUP;
const char *bundle_uri = NULL; const char *bundle_uri = NULL;
@ -999,6 +1001,8 @@ int cmd_clone(int argc,
NULL NULL
}; };
filter_options.allow_auto_filter = 1;
packet_trace_identity("clone"); packet_trace_identity("clone");
repo_config(the_repository, git_clone_config, NULL); repo_config(the_repository, git_clone_config, NULL);
@ -1625,9 +1629,13 @@ int cmd_clone(int argc,
return 1; return 1;
junk_mode = JUNK_LEAVE_REPO; junk_mode = JUNK_LEAVE_REPO;
err = checkout(submodule_progress, filter_submodules, err = checkout(submodule_progress,
&filter_options,
filter_submodules,
ref_storage_format); ref_storage_format);
list_objects_filter_release(&filter_options);
string_list_clear(&option_not, 0); string_list_clear(&option_not, 0);
string_list_clear(&option_config, 0); string_list_clear(&option_config, 0);
string_list_clear(&server_options, 0); string_list_clear(&server_options, 0);

View File

@ -97,7 +97,6 @@ static struct strbuf default_rla = STRBUF_INIT;
static struct transport *gtransport; static struct transport *gtransport;
static struct transport *gsecondary; static struct transport *gsecondary;
static struct refspec refmap = REFSPEC_INIT_FETCH; static struct refspec refmap = REFSPEC_INIT_FETCH;
static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;
static struct string_list server_options = STRING_LIST_INIT_DUP; static struct string_list server_options = STRING_LIST_INIT_DUP;
static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP; static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP;
@ -1449,7 +1448,8 @@ static void add_negotiation_tips(struct git_transport_options *smart_options)
smart_options->negotiation_tips = oids; smart_options->negotiation_tips = oids;
} }
static struct transport *prepare_transport(struct remote *remote, int deepen) static struct transport *prepare_transport(struct remote *remote, int deepen,
struct list_objects_filter_options *filter_options)
{ {
struct transport *transport; struct transport *transport;
@ -1473,9 +1473,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)
set_option(transport, TRANS_OPT_UPDATE_SHALLOW, "yes"); set_option(transport, TRANS_OPT_UPDATE_SHALLOW, "yes");
if (refetch) if (refetch)
set_option(transport, TRANS_OPT_REFETCH, "yes"); set_option(transport, TRANS_OPT_REFETCH, "yes");
if (filter_options.choice) { if (filter_options->choice) {
const char *spec = const char *spec =
expand_list_objects_filter_spec(&filter_options); expand_list_objects_filter_spec(filter_options);
set_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec); set_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec);
set_option(transport, TRANS_OPT_FROM_PROMISOR, "1"); set_option(transport, TRANS_OPT_FROM_PROMISOR, "1");
} }
@ -1493,7 +1493,8 @@ static int backfill_tags(struct display_state *display_state,
struct ref_transaction *transaction, struct ref_transaction *transaction,
struct ref *ref_map, struct ref *ref_map,
struct fetch_head *fetch_head, struct fetch_head *fetch_head,
const struct fetch_config *config) const struct fetch_config *config,
struct list_objects_filter_options *filter_options)
{ {
int retcode, cannot_reuse; int retcode, cannot_reuse;
@ -1507,7 +1508,7 @@ static int backfill_tags(struct display_state *display_state,
cannot_reuse = transport->cannot_reuse || cannot_reuse = transport->cannot_reuse ||
deepen_since || deepen_not.nr; deepen_since || deepen_not.nr;
if (cannot_reuse) { if (cannot_reuse) {
gsecondary = prepare_transport(transport->remote, 0); gsecondary = prepare_transport(transport->remote, 0, filter_options);
transport = gsecondary; transport = gsecondary;
} }
@ -1713,7 +1714,8 @@ out:
static int do_fetch(struct transport *transport, static int do_fetch(struct transport *transport,
struct refspec *rs, struct refspec *rs,
const struct fetch_config *config) const struct fetch_config *config,
struct list_objects_filter_options *filter_options)
{ {
struct ref_transaction *transaction = NULL; struct ref_transaction *transaction = NULL;
struct ref *ref_map = NULL; struct ref *ref_map = NULL;
@ -1873,7 +1875,7 @@ static int do_fetch(struct transport *transport,
* the transaction and don't commit anything. * the transaction and don't commit anything.
*/ */
if (backfill_tags(&display_state, transport, transaction, tags_ref_map, if (backfill_tags(&display_state, transport, transaction, tags_ref_map,
&fetch_head, config)) &fetch_head, config, filter_options))
retcode = 1; retcode = 1;
} }
@ -2198,20 +2200,21 @@ static int fetch_multiple(struct string_list *list, int max_children,
* Fetching from the promisor remote should use the given filter-spec * Fetching from the promisor remote should use the given filter-spec
* or inherit the default filter-spec from the config. * or inherit the default filter-spec from the config.
*/ */
static inline void fetch_one_setup_partial(struct remote *remote) static inline void fetch_one_setup_partial(struct remote *remote,
struct list_objects_filter_options *filter_options)
{ {
/* /*
* Explicit --no-filter argument overrides everything, regardless * Explicit --no-filter argument overrides everything, regardless
* of any prior partial clones and fetches. * of any prior partial clones and fetches.
*/ */
if (filter_options.no_filter) if (filter_options->no_filter)
return; return;
/* /*
* If no prior partial clone/fetch and the current fetch DID NOT * If no prior partial clone/fetch and the current fetch DID NOT
* request a partial-fetch, do a normal fetch. * request a partial-fetch, do a normal fetch.
*/ */
if (!repo_has_promisor_remote(the_repository) && !filter_options.choice) if (!repo_has_promisor_remote(the_repository) && !filter_options->choice)
return; return;
/* /*
@ -2220,8 +2223,8 @@ static inline void fetch_one_setup_partial(struct remote *remote)
* filter-spec as the default for subsequent fetches to this * filter-spec as the default for subsequent fetches to this
* remote if there is currently no default filter-spec. * remote if there is currently no default filter-spec.
*/ */
if (filter_options.choice) { if (filter_options->choice) {
partial_clone_register(remote->name, &filter_options); partial_clone_register(remote->name, filter_options);
return; return;
} }
@ -2230,14 +2233,15 @@ static inline void fetch_one_setup_partial(struct remote *remote)
* explicitly given filter-spec or inherit the filter-spec from * explicitly given filter-spec or inherit the filter-spec from
* the config. * the config.
*/ */
if (!filter_options.choice) if (!filter_options->choice)
partial_clone_get_default_filter_spec(&filter_options, remote->name); partial_clone_get_default_filter_spec(filter_options, remote->name);
return; return;
} }
static int fetch_one(struct remote *remote, int argc, const char **argv, static int fetch_one(struct remote *remote, int argc, const char **argv,
int prune_tags_ok, int use_stdin_refspecs, int prune_tags_ok, int use_stdin_refspecs,
const struct fetch_config *config) const struct fetch_config *config,
struct list_objects_filter_options *filter_options)
{ {
struct refspec rs = REFSPEC_INIT_FETCH; struct refspec rs = REFSPEC_INIT_FETCH;
int i; int i;
@ -2249,7 +2253,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
die(_("no remote repository specified; please specify either a URL or a\n" die(_("no remote repository specified; please specify either a URL or a\n"
"remote name from which new revisions should be fetched")); "remote name from which new revisions should be fetched"));
gtransport = prepare_transport(remote, 1); gtransport = prepare_transport(remote, 1, filter_options);
if (prune < 0) { if (prune < 0) {
/* no command line request */ /* no command line request */
@ -2304,7 +2308,7 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
sigchain_push_common(unlock_pack_on_signal); sigchain_push_common(unlock_pack_on_signal);
atexit(unlock_pack_atexit); atexit(unlock_pack_atexit);
sigchain_push(SIGPIPE, SIG_IGN); sigchain_push(SIGPIPE, SIG_IGN);
exit_code = do_fetch(gtransport, &rs, config); exit_code = do_fetch(gtransport, &rs, config, filter_options);
sigchain_pop(SIGPIPE); sigchain_pop(SIGPIPE);
refspec_clear(&rs); refspec_clear(&rs);
transport_disconnect(gtransport); transport_disconnect(gtransport);
@ -2329,6 +2333,7 @@ int cmd_fetch(int argc,
const char *submodule_prefix = ""; const char *submodule_prefix = "";
const char *bundle_uri; const char *bundle_uri;
struct string_list list = STRING_LIST_INIT_DUP; struct string_list list = STRING_LIST_INIT_DUP;
struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;
struct remote *remote = NULL; struct remote *remote = NULL;
int all = -1, multiple = 0; int all = -1, multiple = 0;
int result = 0; int result = 0;
@ -2434,6 +2439,8 @@ int cmd_fetch(int argc,
OPT_END() OPT_END()
}; };
filter_options.allow_auto_filter = 1;
packet_trace_identity("fetch"); packet_trace_identity("fetch");
/* Record the command line for the reflog */ /* Record the command line for the reflog */
@ -2594,7 +2601,7 @@ int cmd_fetch(int argc,
trace2_region_enter("fetch", "negotiate-only", the_repository); trace2_region_enter("fetch", "negotiate-only", the_repository);
if (!remote) if (!remote)
die(_("must supply remote when using --negotiate-only")); die(_("must supply remote when using --negotiate-only"));
gtransport = prepare_transport(remote, 1); gtransport = prepare_transport(remote, 1, &filter_options);
if (gtransport->smart_options) { if (gtransport->smart_options) {
gtransport->smart_options->acked_commits = &acked_commits; gtransport->smart_options->acked_commits = &acked_commits;
} else { } else {
@ -2616,12 +2623,12 @@ int cmd_fetch(int argc,
} else if (remote) { } else if (remote) {
if (filter_options.choice || repo_has_promisor_remote(the_repository)) { if (filter_options.choice || repo_has_promisor_remote(the_repository)) {
trace2_region_enter("fetch", "setup-partial", the_repository); trace2_region_enter("fetch", "setup-partial", the_repository);
fetch_one_setup_partial(remote); fetch_one_setup_partial(remote, &filter_options);
trace2_region_leave("fetch", "setup-partial", the_repository); trace2_region_leave("fetch", "setup-partial", the_repository);
} }
trace2_region_enter("fetch", "fetch-one", the_repository); trace2_region_enter("fetch", "fetch-one", the_repository);
result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs, result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,
&config); &config, &filter_options);
trace2_region_leave("fetch", "fetch-one", the_repository); trace2_region_leave("fetch", "fetch-one", the_repository);
} else { } else {
int max_children = max_jobs; int max_children = max_jobs;
@ -2727,5 +2734,6 @@ int cmd_fetch(int argc,
cleanup: cleanup:
string_list_clear(&list, 0); string_list_clear(&list, 0);
list_objects_filter_release(&filter_options);
return result; return result;
} }

View File

@ -35,6 +35,7 @@
#include "sigchain.h" #include "sigchain.h"
#include "mergesort.h" #include "mergesort.h"
#include "prio-queue.h" #include "prio-queue.h"
#include "promisor-remote.h"
static int transfer_unpack_limit = -1; static int transfer_unpack_limit = -1;
static int fetch_unpack_limit = -1; static int fetch_unpack_limit = -1;
@ -1661,6 +1662,25 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
struct string_list packfile_uris = STRING_LIST_INIT_DUP; struct string_list packfile_uris = STRING_LIST_INIT_DUP;
int i; int i;
struct strvec index_pack_args = STRVEC_INIT; struct strvec index_pack_args = STRVEC_INIT;
const char *promisor_remote_config;
if (server_feature_v2("promisor-remote", &promisor_remote_config)) {
char *remote_name = promisor_remote_reply(promisor_remote_config);
free(remote_name);
}
if (args->filter_options.choice == LOFC_AUTO) {
struct strbuf errbuf = STRBUF_INIT;
char *constructed_filter = promisor_remote_construct_filter(r);
list_objects_filter_resolve_auto(&args->filter_options,
constructed_filter, &errbuf);
if (errbuf.len > 0)
die(_("couldn't resolve 'auto' filter: %s"), errbuf.buf);
free(constructed_filter);
strbuf_release(&errbuf);
}
negotiator = &negotiator_alloc; negotiator = &negotiator_alloc;
if (args->refetch) if (args->refetch)

View File

@ -20,6 +20,8 @@ const char *list_object_filter_config_name(enum list_objects_filter_choice c)
case LOFC_DISABLED: case LOFC_DISABLED:
/* we have no name for "no filter at all" */ /* we have no name for "no filter at all" */
break; break;
case LOFC_AUTO:
return "auto";
case LOFC_BLOB_NONE: case LOFC_BLOB_NONE:
return "blob:none"; return "blob:none";
case LOFC_BLOB_LIMIT: case LOFC_BLOB_LIMIT:
@ -52,7 +54,17 @@ int gently_parse_list_objects_filter(
if (filter_options->choice) if (filter_options->choice)
BUG("filter_options already populated"); BUG("filter_options already populated");
if (!strcmp(arg, "blob:none")) { if (!strcmp(arg, "auto")) {
if (!filter_options->allow_auto_filter) {
strbuf_addstr(
errbuf,
_("'auto' filter not supported by this command"));
return 1;
}
filter_options->choice = LOFC_AUTO;
return 0;
} else if (!strcmp(arg, "blob:none")) {
filter_options->choice = LOFC_BLOB_NONE; filter_options->choice = LOFC_BLOB_NONE;
return 0; return 0;
@ -146,10 +158,20 @@ static int parse_combine_subfilter(
decoded = url_percent_decode(subspec->buf); decoded = url_percent_decode(subspec->buf);
result = has_reserved_character(subspec, errbuf) || result = has_reserved_character(subspec, errbuf);
gently_parse_list_objects_filter( if (result)
&filter_options->sub[new_index], decoded, errbuf); goto cleanup;
result = gently_parse_list_objects_filter(
&filter_options->sub[new_index], decoded, errbuf);
if (result)
goto cleanup;
result = (filter_options->sub[new_index].choice == LOFC_AUTO);
if (result)
strbuf_addstr(errbuf, _("an 'auto' filter cannot be combined"));
cleanup:
free(decoded); free(decoded);
return result; return result;
} }
@ -208,6 +230,41 @@ static void filter_spec_append_urlencode(
filter->filter_spec.buf + orig_len); filter->filter_spec.buf + orig_len);
} }
char *list_objects_filter_combine(const struct string_list *specs)
{
struct strbuf buf = STRBUF_INIT;
if (!specs->nr)
return NULL;
if (specs->nr == 1)
return xstrdup(specs->items[0].string);
strbuf_addstr(&buf, "combine:");
for (size_t i = 0; i < specs->nr; i++) {
const char *spec = specs->items[i].string;
if (i > 0)
strbuf_addch(&buf, '+');
strbuf_addstr_urlencode(&buf, spec, allow_unencoded);
}
return strbuf_detach(&buf, NULL);
}
void list_objects_filter_resolve_auto(struct list_objects_filter_options *filter_options,
char *new_filter, struct strbuf *errbuf)
{
if (filter_options->choice != LOFC_AUTO)
return;
list_objects_filter_release(filter_options);
if (new_filter)
gently_parse_list_objects_filter(filter_options, new_filter, errbuf);
}
/* /*
* Changes filter_options into an equivalent LOFC_COMBINE filter options * Changes filter_options into an equivalent LOFC_COMBINE filter options
* instance. Does not do anything if filter_options is already LOFC_COMBINE. * instance. Does not do anything if filter_options is already LOFC_COMBINE.
@ -263,6 +320,9 @@ void parse_list_objects_filter(
} else { } else {
struct list_objects_filter_options *sub; struct list_objects_filter_options *sub;
if (filter_options->choice == LOFC_AUTO)
die(_("an 'auto' filter is incompatible with any other filter"));
/* /*
* Make filter_options an LOFC_COMBINE spec so we can trivially * Make filter_options an LOFC_COMBINE spec so we can trivially
* add subspecs to it. * add subspecs to it.
@ -277,6 +337,9 @@ void parse_list_objects_filter(
if (gently_parse_list_objects_filter(sub, arg, &errbuf)) if (gently_parse_list_objects_filter(sub, arg, &errbuf))
die("%s", errbuf.buf); die("%s", errbuf.buf);
if (sub->choice == LOFC_AUTO)
die(_("an 'auto' filter is incompatible with any other filter"));
strbuf_addch(&filter_options->filter_spec, '+'); strbuf_addch(&filter_options->filter_spec, '+');
filter_spec_append_urlencode(filter_options, arg); filter_spec_append_urlencode(filter_options, arg);
} }
@ -317,6 +380,7 @@ void list_objects_filter_release(
struct list_objects_filter_options *filter_options) struct list_objects_filter_options *filter_options)
{ {
size_t sub; size_t sub;
unsigned int allow_auto_filter = filter_options->allow_auto_filter;
if (!filter_options) if (!filter_options)
return; return;
@ -326,6 +390,7 @@ void list_objects_filter_release(
list_objects_filter_release(&filter_options->sub[sub]); list_objects_filter_release(&filter_options->sub[sub]);
free(filter_options->sub); free(filter_options->sub);
list_objects_filter_init(filter_options); list_objects_filter_init(filter_options);
filter_options->allow_auto_filter = allow_auto_filter;
} }
void partial_clone_register( void partial_clone_register(

View File

@ -6,6 +6,7 @@
#include "strbuf.h" #include "strbuf.h"
struct option; struct option;
struct string_list;
/* /*
* The list of defined filters for list-objects. * The list of defined filters for list-objects.
@ -18,6 +19,7 @@ enum list_objects_filter_choice {
LOFC_SPARSE_OID, LOFC_SPARSE_OID,
LOFC_OBJECT_TYPE, LOFC_OBJECT_TYPE,
LOFC_COMBINE, LOFC_COMBINE,
LOFC_AUTO,
LOFC__COUNT /* must be last */ LOFC__COUNT /* must be last */
}; };
@ -50,6 +52,11 @@ struct list_objects_filter_options {
*/ */
unsigned int no_filter : 1; unsigned int no_filter : 1;
/*
* Is LOFC_AUTO a valid option?
*/
unsigned int allow_auto_filter : 1;
/* /*
* BEGIN choice-specific parsed values from within the filter-spec. Only * BEGIN choice-specific parsed values from within the filter-spec. Only
* some values will be defined for any given choice. * some values will be defined for any given choice.
@ -162,4 +169,22 @@ void list_objects_filter_copy(
struct list_objects_filter_options *dest, struct list_objects_filter_options *dest,
const struct list_objects_filter_options *src); const struct list_objects_filter_options *src);
/*
* Combine the filter specs in 'specs' into a combined filter string
* like "combine:<spec1>+<spec2>", where <spec1>, <spec2>, etc are
* properly urlencoded. If 'specs' contains no element, NULL is
* returned. If 'specs' contains a single element, a copy of that
* element is returned.
*/
char *list_objects_filter_combine(const struct string_list *specs);
/*
* Check if 'filter_options' are an 'auto' filter, and if that's the
* case populate it with the filter specified by 'new_filter'.
*/
void list_objects_filter_resolve_auto(
struct list_objects_filter_options *filter_options,
char *new_filter,
struct strbuf *errbuf);
#endif /* LIST_OBJECTS_FILTER_OPTIONS_H */ #endif /* LIST_OBJECTS_FILTER_OPTIONS_H */

View File

@ -745,6 +745,13 @@ static void filter_combine__init(
filter->finalize_omits_fn = filter_combine__finalize_omits; filter->finalize_omits_fn = filter_combine__finalize_omits;
} }
static void filter_auto__init(
struct list_objects_filter_options *filter_options UNUSED,
struct filter *filter UNUSED)
{
BUG("LOFC_AUTO should have been resolved before initializing the filter");
}
typedef void (*filter_init_fn)( typedef void (*filter_init_fn)(
struct list_objects_filter_options *filter_options, struct list_objects_filter_options *filter_options,
struct filter *filter); struct filter *filter);
@ -760,6 +767,7 @@ static filter_init_fn s_filters[] = {
filter_sparse_oid__init, filter_sparse_oid__init,
filter_object_type__init, filter_object_type__init,
filter_combine__init, filter_combine__init,
filter_auto__init,
}; };
struct filter *list_objects_filter__init( struct filter *list_objects_filter__init(

View File

@ -193,6 +193,7 @@ void promisor_remote_clear(struct promisor_remote_config *config)
while (config->promisors) { while (config->promisors) {
struct promisor_remote *r = config->promisors; struct promisor_remote *r = config->promisors;
free(r->partial_clone_filter); free(r->partial_clone_filter);
free(r->advertised_filter);
config->promisors = config->promisors->next; config->promisors = config->promisors->next;
free(r); free(r);
} }
@ -375,18 +376,24 @@ static char *fields_from_config(struct string_list *fields_list, const char *con
return fields; return fields;
} }
static struct string_list *initialize_fields_list(struct string_list *fields_list, int *initialized,
const char *config_key)
{
if (!*initialized) {
fields_list->cmp = strcasecmp;
fields_from_config(fields_list, config_key);
*initialized = 1;
}
return fields_list;
}
static struct string_list *fields_sent(void) static struct string_list *fields_sent(void)
{ {
static struct string_list fields_list = STRING_LIST_INIT_NODUP; static struct string_list fields_list = STRING_LIST_INIT_NODUP;
static int initialized; static int initialized;
if (!initialized) { return initialize_fields_list(&fields_list, &initialized, "promisor.sendFields");
fields_list.cmp = strcasecmp;
fields_from_config(&fields_list, "promisor.sendFields");
initialized = 1;
}
return &fields_list;
} }
static struct string_list *fields_checked(void) static struct string_list *fields_checked(void)
@ -394,13 +401,15 @@ static struct string_list *fields_checked(void)
static struct string_list fields_list = STRING_LIST_INIT_NODUP; static struct string_list fields_list = STRING_LIST_INIT_NODUP;
static int initialized; static int initialized;
if (!initialized) { return initialize_fields_list(&fields_list, &initialized, "promisor.checkFields");
fields_list.cmp = strcasecmp; }
fields_from_config(&fields_list, "promisor.checkFields");
initialized = 1;
}
return &fields_list; static struct string_list *fields_stored(void)
{
static struct string_list fields_list = STRING_LIST_INIT_NODUP;
static int initialized;
return initialize_fields_list(&fields_list, &initialized, "promisor.storeFields");
} }
/* /*
@ -692,6 +701,132 @@ static struct promisor_info *parse_one_advertised_remote(const char *remote_info
return info; return info;
} }
static bool store_one_field(struct repository *repo, const char *remote_name,
const char *field_name, const char *field_key,
const char *advertised, const char *current)
{
if (advertised && (!current || strcmp(current, advertised))) {
char *key = xstrfmt("remote.%s.%s", remote_name, field_key);
fprintf(stderr, _("Storing new %s from server for remote '%s'.\n"
" '%s' -> '%s'\n"),
field_name, remote_name,
current ? current : "",
advertised);
repo_config_set_worktree_gently(repo, key, advertised);
free(key);
return true;
}
return false;
}
/* Check that a filter is valid by parsing it */
static bool valid_filter(const char *filter, const char *remote_name)
{
struct list_objects_filter_options filter_opts = LIST_OBJECTS_FILTER_INIT;
struct strbuf err = STRBUF_INIT;
int res = gently_parse_list_objects_filter(&filter_opts, filter, &err);
if (res)
warning(_("invalid filter '%s' for remote '%s' "
"will not be stored: %s"),
filter, remote_name, err.buf);
list_objects_filter_release(&filter_opts);
strbuf_release(&err);
return !res;
}
/* Check that a token doesn't contain any control character */
static bool valid_token(const char *token, const char *remote_name)
{
const char *c = token;
for (; *c; c++)
if (iscntrl(*c)) {
warning(_("invalid token '%s' for remote '%s' "
"will not be stored"),
token, remote_name);
return false;
}
return true;
}
struct store_info {
struct repository *repo;
struct string_list config_info;
bool store_filter;
bool store_token;
};
static struct store_info *new_store_info(struct repository *repo)
{
struct string_list *fields_to_store = fields_stored();
struct store_info *s = xmalloc(sizeof(*s));
s->repo = repo;
string_list_init_nodup(&s->config_info);
promisor_config_info_list(repo, &s->config_info, fields_to_store);
string_list_sort(&s->config_info);
s->store_filter = !!string_list_lookup(fields_to_store, promisor_field_filter);
s->store_token = !!string_list_lookup(fields_to_store, promisor_field_token);
return s;
}
static void free_store_info(struct store_info *s)
{
if (s) {
promisor_info_list_clear(&s->config_info);
free(s);
}
}
static bool promisor_store_advertised_fields(struct promisor_info *advertised,
struct store_info *store_info)
{
struct promisor_info *p;
struct string_list_item *item;
const char *remote_name = advertised->name;
bool reload_config = false;
if (!(store_info->store_filter || store_info->store_token))
return false;
/*
* Get existing config info for the advertised promisor
* remote. This ensures the remote is already configured on
* the client side.
*/
item = string_list_lookup(&store_info->config_info, remote_name);
if (!item)
return false;
p = item->util;
if (store_info->store_filter && advertised->filter &&
valid_filter(advertised->filter, remote_name))
reload_config |= store_one_field(store_info->repo, remote_name,
"filter", promisor_field_filter,
advertised->filter, p->filter);
if (store_info->store_token && advertised->token &&
valid_token(advertised->token, remote_name))
reload_config |= store_one_field(store_info->repo, remote_name,
"token", promisor_field_token,
advertised->token, p->token);
return reload_config;
}
static void filter_promisor_remote(struct repository *repo, static void filter_promisor_remote(struct repository *repo,
struct strvec *accepted, struct strvec *accepted,
const char *info) const char *info)
@ -700,7 +835,10 @@ static void filter_promisor_remote(struct repository *repo,
enum accept_promisor accept = ACCEPT_NONE; enum accept_promisor accept = ACCEPT_NONE;
struct string_list config_info = STRING_LIST_INIT_NODUP; struct string_list config_info = STRING_LIST_INIT_NODUP;
struct string_list remote_info = STRING_LIST_INIT_DUP; struct string_list remote_info = STRING_LIST_INIT_DUP;
struct store_info *store_info = NULL;
struct string_list_item *item; struct string_list_item *item;
bool reload_config = false;
struct string_list captured_filters = STRING_LIST_INIT_DUP;
if (!repo_config_get_string_tmp(the_repository, "promisor.acceptfromserver", &accept_str)) { if (!repo_config_get_string_tmp(the_repository, "promisor.acceptfromserver", &accept_str)) {
if (!*accept_str || !strcasecmp("None", accept_str)) if (!*accept_str || !strcasecmp("None", accept_str))
@ -736,14 +874,50 @@ static void filter_promisor_remote(struct repository *repo,
string_list_sort(&config_info); string_list_sort(&config_info);
} }
if (should_accept_remote(accept, advertised, &config_info)) if (should_accept_remote(accept, advertised, &config_info)) {
if (!store_info)
store_info = new_store_info(repo);
if (promisor_store_advertised_fields(advertised, store_info))
reload_config = true;
strvec_push(accepted, advertised->name); strvec_push(accepted, advertised->name);
/* Capture advertised filters for accepted remotes */
if (advertised->filter) {
struct string_list_item *i;
i = string_list_append(&captured_filters, advertised->name);
i->util = xstrdup(advertised->filter);
}
}
promisor_info_free(advertised); promisor_info_free(advertised);
} }
promisor_info_list_clear(&config_info); promisor_info_list_clear(&config_info);
string_list_clear(&remote_info, 0); string_list_clear(&remote_info, 0);
free_store_info(store_info);
if (reload_config)
repo_promisor_remote_reinit(repo);
/* Apply captured filters to the stable repo state */
for_each_string_list_item(item, &captured_filters) {
struct promisor_remote *r = repo_promisor_remote_find(repo, item->string);
if (r) {
free(r->advertised_filter);
r->advertised_filter = item->util;
item->util = NULL;
}
}
string_list_clear(&captured_filters, 1);
/* Mark the remotes as accepted in the repository state */
for (size_t i = 0; i < accepted->nr; i++) {
struct promisor_remote *r = repo_promisor_remote_find(repo, accepted->v[i]);
if (r)
r->accepted = 1;
}
} }
char *promisor_remote_reply(const char *info) char *promisor_remote_reply(const char *info)
@ -789,3 +963,23 @@ void mark_promisor_remotes_as_accepted(struct repository *r, const char *remotes
string_list_clear(&accepted_remotes, 0); string_list_clear(&accepted_remotes, 0);
} }
char *promisor_remote_construct_filter(struct repository *repo)
{
struct string_list advertised_filters = STRING_LIST_INIT_NODUP;
struct promisor_remote *r;
char *result;
promisor_remote_init(repo);
for (r = repo->promisor_remote_config->promisors; r; r = r->next) {
if (r->accepted && r->advertised_filter)
string_list_append(&advertised_filters, r->advertised_filter);
}
result = list_objects_filter_combine(&advertised_filters);
string_list_clear(&advertised_filters, 0);
return result;
}

View File

@ -15,6 +15,7 @@ struct object_id;
struct promisor_remote { struct promisor_remote {
struct promisor_remote *next; struct promisor_remote *next;
char *partial_clone_filter; char *partial_clone_filter;
char *advertised_filter;
unsigned int accepted : 1; unsigned int accepted : 1;
const char name[FLEX_ARRAY]; const char name[FLEX_ARRAY];
}; };
@ -67,4 +68,9 @@ void mark_promisor_remotes_as_accepted(struct repository *repo, const char *remo
*/ */
int repo_has_accepted_promisor_remote(struct repository *r); int repo_has_accepted_promisor_remote(struct repository *r);
/*
* Use the filters from the accepted remotes to create a filter.
*/
char *promisor_remote_construct_filter(struct repository *repo);
#endif /* PROMISOR_REMOTE_H */ #endif /* PROMISOR_REMOTE_H */

View File

@ -4,6 +4,7 @@ clar_test_suites = [
'unit-tests/u-example-decorate.c', 'unit-tests/u-example-decorate.c',
'unit-tests/u-hash.c', 'unit-tests/u-hash.c',
'unit-tests/u-hashmap.c', 'unit-tests/u-hashmap.c',
'unit-tests/u-list-objects-filter-options.c',
'unit-tests/u-mem-pool.c', 'unit-tests/u-mem-pool.c',
'unit-tests/u-oid-array.c', 'unit-tests/u-oid-array.c',
'unit-tests/u-oidmap.c', 'unit-tests/u-oidmap.c',

View File

@ -360,6 +360,115 @@ test_expect_success "clone with promisor.checkFields" '
check_missing_objects server 1 "$oid" check_missing_objects server 1 "$oid"
' '
test_expect_success "clone with promisor.storeFields=partialCloneFilter" '
git -C server config promisor.advertise true &&
test_when_finished "rm -rf client" &&
git -C server remote add otherLop "https://invalid.invalid" &&
git -C server config remote.otherLop.token "fooBar" &&
git -C server config remote.otherLop.stuff "baz" &&
git -C server config remote.otherLop.partialCloneFilter "blob:limit=10k" &&
test_when_finished "git -C server remote remove otherLop" &&
git -C server config remote.lop.token "fooXXX" &&
git -C server config remote.lop.partialCloneFilter "blob:limit=8k" &&
test_config -C server promisor.sendFields "partialCloneFilter, token" &&
test_when_finished "rm trace" &&
# Clone from server to create a client
GIT_TRACE_PACKET="$(pwd)/trace" GIT_NO_LAZY_FETCH=0 git clone \
-c remote.lop.promisor=true \
-c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \
-c remote.lop.url="file://$(pwd)/lop" \
-c remote.lop.token="fooYYY" \
-c remote.lop.partialCloneFilter="blob:none" \
-c promisor.acceptfromserver=All \
-c promisor.storeFields=partialcloneFilter \
--no-local --filter="blob:limit=5k" server client 2>err &&
# Check that the filter from the server is stored
echo "blob:limit=8k" >expected &&
git -C client config remote.lop.partialCloneFilter >actual &&
test_cmp expected actual &&
# Check that user is notified when the filter is stored
test_grep "Storing new filter from server for remote '\''lop'\''" err &&
test_grep "'\''blob:none'\'' -> '\''blob:limit=8k'\''" err &&
# Check that the token from the server is NOT stored
echo "fooYYY" >expected &&
git -C client config remote.lop.token >actual &&
test_cmp expected actual &&
test_grep ! "Storing new token from server" err &&
# Check that the filter for an unknown remote is NOT stored
test_must_fail git -C client config remote.otherLop.partialCloneFilter >actual &&
# Check that the largest object is still missing on the server
check_missing_objects server 1 "$oid"
'
test_expect_success "clone and fetch with --filter=auto" '
git -C server config promisor.advertise true &&
test_when_finished "rm -rf client trace" &&
git -C server config remote.lop.partialCloneFilter "blob:limit=9500" &&
test_config -C server promisor.sendFields "partialCloneFilter" &&
GIT_TRACE_PACKET="$(pwd)/trace" GIT_NO_LAZY_FETCH=0 git clone \
-c remote.lop.promisor=true \
-c remote.lop.url="file://$(pwd)/lop" \
-c promisor.acceptfromserver=All \
--no-local --filter=auto server client 2>err &&
test_grep "filter blob:limit=9500" trace &&
test_grep ! "filter auto" trace &&
# Verify "auto" is persisted in config
echo auto >expected &&
git -C client config remote.origin.partialCloneFilter >actual &&
test_cmp expected actual &&
# Check that the largest object is still missing on the server
check_missing_objects server 1 "$oid" &&
# Now change the filter on the server
git -C server config remote.lop.partialCloneFilter "blob:limit=5678" &&
# Get a new commit on the server to ensure "git fetch" actually runs fetch-pack
test_commit -C template new-commit &&
git -C template push --all "$(pwd)/server" &&
# Perform a fetch WITH --filter=auto
rm -rf trace &&
GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch --filter=auto &&
# Verify that the new filter was used
test_grep "filter blob:limit=5678" trace &&
# Check that the largest object is still missing on the server
check_missing_objects server 1 "$oid" &&
# Change the filter on the server again
git -C server config remote.lop.partialCloneFilter "blob:limit=5432" &&
# Get yet a new commit on the server to ensure fetch-pack runs
test_commit -C template yet-a-new-commit &&
git -C template push --all "$(pwd)/server" &&
# Perform a fetch WITHOUT --filter=auto
# Relies on "auto" being persisted in the client config
rm -rf trace &&
GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch &&
# Verify that the new filter was used
test_grep "filter blob:limit=5432" trace &&
# Check that the largest object is still missing on the server
check_missing_objects server 1 "$oid"
'
test_expect_success "clone with promisor.advertise set to 'true' but don't delete the client" ' test_expect_success "clone with promisor.advertise set to 'true' but don't delete the client" '
git -C server config promisor.advertise true && git -C server config promisor.advertise true &&

View File

@ -0,0 +1,86 @@
#include "unit-test.h"
#include "list-objects-filter-options.h"
#include "strbuf.h"
#include "string-list.h"
/* Helper to test gently_parse_list_objects_filter() */
static void check_gentle_parse(const char *filter_spec,
int expect_success,
int allow_auto,
enum list_objects_filter_choice expected_choice)
{
struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT;
struct strbuf errbuf = STRBUF_INIT;
int ret;
filter_options.allow_auto_filter = allow_auto;
ret = gently_parse_list_objects_filter(&filter_options, filter_spec, &errbuf);
if (expect_success) {
cl_assert_equal_i(ret, 0);
cl_assert_equal_i(expected_choice, filter_options.choice);
cl_assert_equal_i(errbuf.len, 0);
} else {
cl_assert(ret != 0);
cl_assert(errbuf.len > 0);
}
strbuf_release(&errbuf);
list_objects_filter_release(&filter_options);
}
void test_list_objects_filter_options__regular_filters(void)
{
check_gentle_parse("blob:none", 1, 0, LOFC_BLOB_NONE);
check_gentle_parse("blob:none", 1, 1, LOFC_BLOB_NONE);
check_gentle_parse("blob:limit=5k", 1, 0, LOFC_BLOB_LIMIT);
check_gentle_parse("blob:limit=5k", 1, 1, LOFC_BLOB_LIMIT);
check_gentle_parse("combine:blob:none+tree:0", 1, 0, LOFC_COMBINE);
check_gentle_parse("combine:blob:none+tree:0", 1, 1, LOFC_COMBINE);
}
void test_list_objects_filter_options__auto_allowed(void)
{
check_gentle_parse("auto", 1, 1, LOFC_AUTO);
check_gentle_parse("auto", 0, 0, 0);
}
void test_list_objects_filter_options__combine_auto_fails(void)
{
check_gentle_parse("combine:auto+blob:none", 0, 1, 0);
check_gentle_parse("combine:blob:none+auto", 0, 1, 0);
check_gentle_parse("combine:auto+auto", 0, 1, 0);
}
/* Helper to test list_objects_filter_combine() */
static void check_combine(const char **specs, size_t nr, const char *expected)
{
struct string_list spec_list = STRING_LIST_INIT_NODUP;
char *actual;
for (size_t i = 0; i < nr; i++)
string_list_append(&spec_list, specs[i]);
actual = list_objects_filter_combine(&spec_list);
cl_assert_equal_s(actual, expected);
free(actual);
string_list_clear(&spec_list, 0);
}
void test_list_objects_filter_options__combine_helper(void)
{
const char *empty[] = { NULL };
const char *one[] = { "blob:none" };
const char *two[] = { "blob:none", "tree:0" };
const char *complex[] = { "blob:limit=1k", "object:type=tag" };
const char *needs_encoding[] = { "blob:none", "combine:tree:0+blob:limit=1k" };
check_combine(empty, 0, NULL);
check_combine(one, 1, "blob:none");
check_combine(two, 2, "combine:blob:none+tree:0");
check_combine(complex, 2, "combine:blob:limit=1k+object:type=tag");
check_combine(needs_encoding, 2, "combine:blob:none+combine:tree:0%2bblob:limit=1k");
}

View File

@ -1219,6 +1219,7 @@ struct transport *transport_get(struct remote *remote, const char *url)
*/ */
struct git_transport_data *data = xcalloc(1, sizeof(*data)); struct git_transport_data *data = xcalloc(1, sizeof(*data));
list_objects_filter_init(&data->options.filter_options); list_objects_filter_init(&data->options.filter_options);
data->options.filter_options.allow_auto_filter = 1;
ret->data = data; ret->data = data;
ret->vtable = &builtin_smart_vtable; ret->vtable = &builtin_smart_vtable;
ret->smart_options = &(data->options); ret->smart_options = &(data->options);