diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 91e46f66f5dd1c..9dbe820275dd19 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -107,6 +107,46 @@ priority configuration file (e.g. `.git/config` in a repository) to clear the values inherited from a lower priority configuration files (e.g. `$HOME/.gitconfig`). +remote..negotiationRestrict:: + When negotiating with this remote during `git fetch` and `git push`, + restrict the commits advertised as "have" lines to only those + reachable from refs matching the given patterns. This multi-valued + config option behaves like `--negotiation-restrict` on the command + line. ++ +Each value is either an exact ref name (e.g. `refs/heads/release`) or a +glob pattern (e.g. `refs/heads/release/*`). The pattern syntax is the +same as for `--negotiation-restrict`. ++ +These config values are used as defaults for the `--negotiation-restrict` +command-line option. If `--negotiation-restrict` (or its synonym +`--negotiation-tip`) is specified on the command line, then the config +values are not used. + +remote..negotiationRequire:: + When negotiating with this remote during `git fetch` and `git push`, + the client advertises a list of commits that exist locally. In + repos with many references, this list of "haves" can be truncated. + Depending on data shape, dropping certain references may be + expensive. This multi-valued config option specifies ref patterns + whose tips should always be sent as "have" commits during fetch + negotiation with this remote. ++ +Each value is either an exact ref name (e.g. `refs/heads/release`) or a +glob pattern (e.g. `refs/heads/release/*`). The pattern syntax is the same +as for `--negotiation-restrict`. ++ +These config values are used as defaults for the `--negotiation-require` +command-line option. If `--negotiation-require` is specified on the +command line, then the config values are not used. ++ +This option is additive with the normal negotiation process: the +negotiation algorithm still runs and advertises its own selected commits, +but the refs matching `remote..negotiationRequire` are sent +unconditionally on top of those heuristically selected commits. This +option is also used during push negotiation when `push.negotiate` is +enabled. + remote..followRemoteHEAD:: How linkgit:git-fetch[1] should handle updates to `remotes//HEAD` when fetching using the configured refspecs of a remote. diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 81a9d7f9bbc11d..16c6e8cee91531 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -49,6 +49,7 @@ the current repository has the same history as the source repository. `.git/shallow`. This option updates `.git/shallow` and accepts such refs. +`--negotiation-restrict=(|)`:: `--negotiation-tip=(|)`:: By default, Git will report, to the server, commits reachable from all local refs to find common commits in an attempt to @@ -58,6 +59,9 @@ the current repository has the same history as the source repository. local ref is likely to have commits in common with the upstream ref being fetched. + +`--negotiation-restrict` is the preferred name for this option; +`--negotiation-tip` is accepted as a synonym. ++ This option may be specified more than once; if so, Git will report commits reachable from any of the given commits. + @@ -69,6 +73,29 @@ See also the `fetch.negotiationAlgorithm` and `push.negotiate` configuration variables documented in linkgit:git-config[1], and the `--negotiate-only` option below. +`--negotiation-require=`:: + Ensure that the given ref tip is always sent as a "have" line + during fetch negotiation, regardless of what the negotiation + algorithm selects. This is useful to guarantee that common + history reachable from specific refs is always considered, even + when `--negotiation-restrict` restricts the set of tips or when + the negotiation algorithm would otherwise skip them. ++ +This option may be specified more than once; if so, each ref is sent +unconditionally. ++ +The argument may be an exact ref name (e.g. `refs/heads/release`) or a +glob pattern (e.g. `refs/heads/release/{asterisk}`). The pattern syntax +is the same as for `--negotiation-restrict`. ++ +If `--negotiation-restrict` is used, the have set is first restricted by +that option and then increased to include the tips specified by +`--negotiation-require`. ++ +If this option is not specified on the command line, then any +`remote..negotiationRequire` config values for the current remote +are used instead. + `--negotiate-only`:: Do not fetch anything from the server, and instead print the ancestors of the provided `--negotiation-tip=` arguments, diff --git a/builtin/fetch.c b/builtin/fetch.c index 4795b2a13c30e3..a398115fb5e0f6 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -99,6 +99,7 @@ static struct transport *gsecondary; static struct refspec refmap = REFSPEC_INIT_FETCH; 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_require = STRING_LIST_INIT_NODUP; struct fetch_config { enum display_format display_format; @@ -1534,7 +1535,7 @@ static int add_oid(const struct reference *ref, void *cb_data) return 0; } -static void add_negotiation_tips(struct git_transport_options *smart_options) +static void add_negotiation_restrict_tips(struct git_transport_options *smart_options) { struct oid_array *oids = xcalloc(1, sizeof(*oids)); int i; @@ -1558,10 +1559,10 @@ static void add_negotiation_tips(struct git_transport_options *smart_options) refs_for_each_ref_ext(get_main_ref_store(the_repository), add_oid, oids, &opts); if (old_nr == oids->nr) - warning("ignoring --negotiation-tip=%s because it does not match any refs", - s); + warning(_("ignoring %s=%s because it does not match any refs"), + "--negotiation-restrict", s); } - smart_options->negotiation_tips = oids; + smart_options->negotiation_restrict_tips = oids; } static struct transport *prepare_transport(struct remote *remote, int deepen, @@ -1597,9 +1598,40 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, } if (negotiation_tip.nr) { if (transport->smart_options) - add_negotiation_tips(transport->smart_options); + add_negotiation_restrict_tips(transport->smart_options); else - warning("ignoring --negotiation-tip because the protocol does not support it"); + warning(_("ignoring %s because the protocol does not support it"), + "--negotiation-restrict"); + } else if (remote->negotiation_restrict.nr) { + struct string_list_item *item; + for_each_string_list_item(item, &remote->negotiation_restrict) + string_list_append(&negotiation_tip, item->string); + if (transport->smart_options) + add_negotiation_restrict_tips(transport->smart_options); + else { + struct strbuf config_name = STRBUF_INIT; + strbuf_addf(&config_name, "remote.%s.negotiationRestrict", remote->name); + warning(_("ignoring %s because the protocol does not support it"), + config_name.buf); + strbuf_release(&config_name); + } + } + if (negotiation_require.nr) { + if (transport->smart_options) + transport->smart_options->negotiation_require = &negotiation_require; + else + warning(_("ignoring %s because the protocol does not support it"), + "--negotiation-require"); + } else if (remote->negotiation_require.nr) { + if (transport->smart_options) { + transport->smart_options->negotiation_require = &remote->negotiation_require; + } else { + struct strbuf config_name = STRBUF_INIT; + strbuf_addf(&config_name, "remote.%s.negotiationRequire", remote->name); + warning(_("ignoring %s because the protocol does not support it"), + config_name.buf); + strbuf_release(&config_name); + } } return transport; } @@ -2567,6 +2599,10 @@ int cmd_fetch(int argc, OPT_IPVERSION(&family), OPT_STRING_LIST(0, "negotiation-tip", &negotiation_tip, N_("revision"), N_("report that we have only objects reachable from this object")), + OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_tip, N_("revision"), + N_("report that we have only objects reachable from this object")), + OPT_STRING_LIST(0, "negotiation-require", &negotiation_require, N_("revision"), + N_("ensure this ref is always sent as a negotiation have")), OPT_BOOL(0, "negotiate-only", &negotiate_only, N_("do not fetch a packfile; instead, print ancestors of negotiation tips")), OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), @@ -2656,8 +2692,12 @@ int cmd_fetch(int argc, config.display_format = DISPLAY_FORMAT_PORCELAIN; } - if (negotiate_only && !negotiation_tip.nr) - die(_("--negotiate-only needs one or more --negotiation-tip=*")); + if (negotiate_only && !negotiation_tip.nr) { + /* + * Defer this check: remote..negotiationRestrict may + * provide defaults in prepare_transport(). + */ + } if (deepen_relative) { if (deepen_relative < 0) @@ -2746,6 +2786,9 @@ int cmd_fetch(int argc, if (!remote) die(_("must supply remote when using --negotiate-only")); gtransport = prepare_transport(remote, 1, &filter_options); + if (!gtransport->smart_options || + !gtransport->smart_options->negotiation_restrict_tips) + die(_("--negotiate-only needs one or more --negotiation-restrict=*")); if (gtransport->smart_options) { gtransport->smart_options->acked_commits = &acked_commits; } else { diff --git a/builtin/pull.c b/builtin/pull.c index 7e67fdce97fd1d..973186ecdcb307 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -999,6 +999,12 @@ int cmd_pull(int argc, OPT_PASSTHRU_ARGV(0, "negotiation-tip", &opt_fetch, N_("revision"), N_("report that we have only objects reachable from this object"), 0), + OPT_PASSTHRU_ARGV(0, "negotiation-restrict", &opt_fetch, N_("revision"), + N_("report that we have only objects reachable from this object"), + 0), + OPT_PASSTHRU_ARGV(0, "negotiation-require", &opt_fetch, N_("revision"), + N_("ensure this ref is always sent as a negotiation have"), + 0), OPT_BOOL(0, "show-forced-updates", &opt_show_forced_updates, N_("check for forced-updates on all updated branches")), OPT_PASSTHRU(0, "set-upstream", &set_upstream, NULL, diff --git a/fetch-pack.c b/fetch-pack.c index 6ecd468ef766a8..a0029253f18993 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -25,6 +25,7 @@ #include "oidset.h" #include "packfile.h" #include "odb.h" +#include "object-name.h" #include "path.h" #include "connected.h" #include "fetch-negotiator.h" @@ -291,21 +292,21 @@ static int next_flush(int stateless_rpc, int count) } static void mark_tips(struct fetch_negotiator *negotiator, - const struct oid_array *negotiation_tips) + const struct oid_array *negotiation_restrict_tips) { struct refs_for_each_ref_options opts = { .flags = REFS_FOR_EACH_INCLUDE_BROKEN, }; int i; - if (!negotiation_tips) { + if (!negotiation_restrict_tips) { refs_for_each_ref_ext(get_main_ref_store(the_repository), rev_list_insert_ref_oid, negotiator, &opts); return; } - for (i = 0; i < negotiation_tips->nr; i++) - rev_list_insert_ref(negotiator, &negotiation_tips->oid[i]); + for (i = 0; i < negotiation_restrict_tips->nr; i++) + rev_list_insert_ref(negotiator, &negotiation_restrict_tips->oid[i]); return; } @@ -332,6 +333,41 @@ static void send_filter(struct fetch_pack_args *args, } } +static int add_oid_to_oidset(const struct reference *ref, void *cb_data) +{ + struct oidset *set = cb_data; + if (odb_has_object(the_repository->objects, ref->oid, 0)) + oidset_insert(set, ref->oid); + return 0; +} + +static void resolve_negotiation_require(const struct string_list *negotiation_require, + struct oidset *result) +{ + struct string_list_item *item; + + if (!negotiation_require || !negotiation_require->nr) + return; + + for_each_string_list_item(item, negotiation_require) { + if (!has_glob_specials(item->string)) { + struct object_id oid; + if (repo_get_oid(the_repository, item->string, &oid)) + continue; + if (!odb_has_object(the_repository->objects, &oid, 0)) + continue; + oidset_insert(result, &oid); + } else { + struct refs_for_each_ref_options opts = { + .pattern = item->string, + }; + refs_for_each_ref_ext( + get_main_ref_store(the_repository), + add_oid_to_oidset, result, &opts); + } + } +} + static int find_common(struct fetch_negotiator *negotiator, struct fetch_pack_args *args, int fd[2], struct object_id *result_oid, @@ -347,6 +383,7 @@ static int find_common(struct fetch_negotiator *negotiator, struct strbuf req_buf = STRBUF_INIT; size_t state_len = 0; struct packet_reader reader; + struct oidset negotiation_require_oids = OIDSET_INIT; if (args->stateless_rpc && multi_ack == 1) die(_("the option '%s' requires '%s'"), "--stateless-rpc", "multi_ack_detailed"); @@ -355,7 +392,7 @@ static int find_common(struct fetch_negotiator *negotiator, PACKET_READ_CHOMP_NEWLINE | PACKET_READ_DIE_ON_ERR_PACKET); - mark_tips(negotiator, args->negotiation_tips); + mark_tips(negotiator, args->negotiation_restrict_tips); for_each_cached_alternate(negotiator, insert_one_alternate_object); fetching = 0; @@ -474,7 +511,25 @@ static int find_common(struct fetch_negotiator *negotiator, trace2_region_enter("fetch-pack", "negotiation_v0_v1", the_repository); flushes = 0; retval = -1; + + /* Send unconditional haves from --negotiation-require */ + resolve_negotiation_require(args->negotiation_require, + &negotiation_require_oids); + if (oidset_size(&negotiation_require_oids)) { + struct oidset_iter iter; + oidset_iter_init(&negotiation_require_oids, &iter); + + while ((oid = oidset_iter_next(&iter))) { + packet_buf_write(&req_buf, "have %s\n", + oid_to_hex(oid)); + print_verbose(args, "have %s", oid_to_hex(oid)); + } + } + while ((oid = negotiator->next(negotiator))) { + /* avoid duplicate oids from --negotiation-require */ + if (oidset_contains(&negotiation_require_oids, oid)) + continue; packet_buf_write(&req_buf, "have %s\n", oid_to_hex(oid)); print_verbose(args, "have %s", oid_to_hex(oid)); in_vain++; @@ -584,6 +639,7 @@ static int find_common(struct fetch_negotiator *negotiator, flushes++; } strbuf_release(&req_buf); + oidset_clear(&negotiation_require_oids); if (!got_ready || !no_done) consume_shallow_list(args, &reader); @@ -1305,12 +1361,26 @@ static void add_common(struct strbuf *req_buf, struct oidset *common) static int add_haves(struct fetch_negotiator *negotiator, struct strbuf *req_buf, - int *haves_to_send) + int *haves_to_send, + struct oidset *negotiation_require_oids) { int haves_added = 0; const struct object_id *oid; + /* Send unconditional haves from --negotiation-require */ + if (negotiation_require_oids) { + struct oidset_iter iter; + oidset_iter_init(negotiation_require_oids, &iter); + + while ((oid = oidset_iter_next(&iter))) + packet_buf_write(req_buf, "have %s\n", + oid_to_hex(oid)); + } + while ((oid = negotiator->next(negotiator))) { + if (negotiation_require_oids && + oidset_contains(negotiation_require_oids, oid)) + continue; packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid)); if (++haves_added >= *haves_to_send) break; @@ -1358,7 +1428,8 @@ static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out, struct fetch_pack_args *args, const struct ref *wants, struct oidset *common, int *haves_to_send, int *in_vain, - int sideband_all, int seen_ack) + int sideband_all, int seen_ack, + struct oidset *negotiation_require_oids) { int haves_added; int done_sent = 0; @@ -1413,7 +1484,8 @@ static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out, /* Add all of the common commits we've found in previous rounds */ add_common(&req_buf, common); - haves_added = add_haves(negotiator, &req_buf, haves_to_send); + haves_added = add_haves(negotiator, &req_buf, haves_to_send, + negotiation_require_oids); *in_vain += haves_added; trace2_data_intmax("negotiation_v2", the_repository, "haves_added", haves_added); trace2_data_intmax("negotiation_v2", the_repository, "in_vain", *in_vain); @@ -1657,6 +1729,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, struct ref *ref = copy_ref_list(orig_ref); enum fetch_state state = FETCH_CHECK_LOCAL; struct oidset common = OIDSET_INIT; + struct oidset negotiation_require_oids = OIDSET_INIT; struct packet_reader reader; int in_vain = 0, negotiation_started = 0; int negotiation_round = 0; @@ -1728,7 +1801,9 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, else state = FETCH_SEND_REQUEST; - mark_tips(negotiator, args->negotiation_tips); + mark_tips(negotiator, args->negotiation_restrict_tips); + resolve_negotiation_require(args->negotiation_require, + &negotiation_require_oids); for_each_cached_alternate(negotiator, insert_one_alternate_object); break; @@ -1747,7 +1822,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, &common, &haves_to_send, &in_vain, reader.use_sideband, - seen_ack)) { + seen_ack, + &negotiation_require_oids)) { trace2_region_leave_printf("negotiation_v2", "round", the_repository, "%d", negotiation_round); @@ -1883,6 +1959,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, negotiator->release(negotiator); oidset_clear(&common); + oidset_clear(&negotiation_require_oids); return ref; } @@ -2177,16 +2254,18 @@ static void clear_common_flag(struct oidset *s) } } -void negotiate_using_fetch(const struct oid_array *negotiation_tips, +void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, const struct string_list *server_options, int stateless_rpc, int fd[], - struct oidset *acked_commits) + struct oidset *acked_commits, + const struct string_list *negotiation_require) { struct fetch_negotiator negotiator; struct packet_reader reader; struct object_array nt_object_array = OBJECT_ARRAY_INIT; struct strbuf req_buf = STRBUF_INIT; + struct oidset negotiation_require_oids = OIDSET_INIT; int haves_to_send = INITIAL_FLUSH; int in_vain = 0; int seen_ack = 0; @@ -2195,13 +2274,16 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips, timestamp_t min_generation = GENERATION_NUMBER_INFINITY; fetch_negotiator_init(the_repository, &negotiator); - mark_tips(&negotiator, negotiation_tips); + mark_tips(&negotiator, negotiation_restrict_tips); + + resolve_negotiation_require(negotiation_require, + &negotiation_require_oids); packet_reader_init(&reader, fd[0], NULL, 0, PACKET_READ_CHOMP_NEWLINE | PACKET_READ_DIE_ON_ERR_PACKET); - oid_array_for_each((struct oid_array *) negotiation_tips, + oid_array_for_each((struct oid_array *) negotiation_restrict_tips, add_to_object_array, &nt_object_array); @@ -2221,7 +2303,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips, packet_buf_write(&req_buf, "wait-for-done"); - haves_added = add_haves(&negotiator, &req_buf, &haves_to_send); + haves_added = add_haves(&negotiator, &req_buf, &haves_to_send, + &negotiation_require_oids); in_vain += haves_added; if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN)) last_iteration = 1; @@ -2273,6 +2356,7 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips, clear_common_flag(acked_commits); object_array_clear(&nt_object_array); + oidset_clear(&negotiation_require_oids); negotiator.release(&negotiator); strbuf_release(&req_buf); } diff --git a/fetch-pack.h b/fetch-pack.h index 9d3470366f85ec..1daea8c542dc9e 100644 --- a/fetch-pack.h +++ b/fetch-pack.h @@ -21,7 +21,14 @@ struct fetch_pack_args { * If not NULL, during packfile negotiation, fetch-pack will send "have" * lines only with these tips and their ancestors. */ - const struct oid_array *negotiation_tips; + const struct oid_array *negotiation_restrict_tips; + + /* + * If non-empty, ref patterns whose tips should always be sent + * as "have" lines during negotiation, regardless of what the + * negotiation algorithm selects. + */ + const struct string_list *negotiation_require; unsigned deepen_relative:1; unsigned quiet:1; @@ -89,11 +96,12 @@ struct ref *fetch_pack(struct fetch_pack_args *args, * In the capability advertisement that has happened prior to invoking this * function, the "wait-for-done" capability must be present. */ -void negotiate_using_fetch(const struct oid_array *negotiation_tips, +void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, const struct string_list *server_options, int stateless_rpc, int fd[], - struct oidset *acked_commits); + struct oidset *acked_commits, + const struct string_list *negotiation_require); /* * Print an appropriate error message for each sought ref that wasn't diff --git a/remote.c b/remote.c index 7ca2a6501b4920..53deed7565b32f 100644 --- a/remote.c +++ b/remote.c @@ -152,6 +152,8 @@ static struct remote *make_remote(struct remote_state *remote_state, refspec_init_push(&ret->push); refspec_init_fetch(&ret->fetch); string_list_init_dup(&ret->server_options); + string_list_init_dup(&ret->negotiation_restrict); + string_list_init_dup(&ret->negotiation_require); ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1, remote_state->remotes_alloc); @@ -179,6 +181,8 @@ static void remote_clear(struct remote *remote) FREE_AND_NULL(remote->http_proxy); FREE_AND_NULL(remote->http_proxy_authmethod); string_list_clear(&remote->server_options, 0); + string_list_clear(&remote->negotiation_restrict, 0); + string_list_clear(&remote->negotiation_require, 0); } static void add_merge(struct branch *branch, const char *name) @@ -562,6 +566,14 @@ static int handle_config(const char *key, const char *value, } else if (!strcmp(subkey, "serveroption")) { return parse_transport_option(key, value, &remote->server_options); + } else if (!strcmp(subkey, "negotiationrestrict")) { + if (!value) + return config_error_nonbool(key); + string_list_append(&remote->negotiation_restrict, value); + } else if (!strcmp(subkey, "negotiationrequire")) { + if (!value) + return config_error_nonbool(key); + string_list_append(&remote->negotiation_require, value); } else if (!strcmp(subkey, "followremotehead")) { const char *no_warn_branch; if (!strcmp(value, "never")) diff --git a/remote.h b/remote.h index fc052945ee451d..d986257c78c666 100644 --- a/remote.h +++ b/remote.h @@ -117,6 +117,8 @@ struct remote { char *http_proxy_authmethod; struct string_list server_options; + struct string_list negotiation_restrict; + struct string_list negotiation_require; enum follow_remote_head_settings follow_remote_head; const char *no_warn_branch; diff --git a/send-pack.c b/send-pack.c index 67d6987b1ccd7e..1bf17a73a99eb4 100644 --- a/send-pack.c +++ b/send-pack.c @@ -433,6 +433,8 @@ static void reject_invalid_nonce(const char *nonce, int len) static void get_commons_through_negotiation(struct repository *r, const char *url, + const struct string_list *negotiation_require, + const struct string_list *negotiation_restrict, const struct ref *remote_refs, struct oid_array *commons) { @@ -445,13 +447,30 @@ static void get_commons_through_negotiation(struct repository *r, child.no_stdin = 1; child.out = -1; strvec_pushl(&child.args, "fetch", "--negotiate-only", NULL); - for (ref = remote_refs; ref; ref = ref->next) { - if (!is_null_oid(&ref->new_oid)) { - strvec_pushf(&child.args, "--negotiation-tip=%s", - oid_to_hex(&ref->new_oid)); - nr_negotiation_tip++; + + if (negotiation_restrict && negotiation_restrict->nr) { + struct string_list_item *item; + for_each_string_list_item(item, negotiation_restrict) + strvec_pushf(&child.args, "--negotiation-restrict=%s", + item->string); + nr_negotiation_tip = negotiation_restrict->nr; + } else { + for (ref = remote_refs; ref; ref = ref->next) { + if (!is_null_oid(&ref->new_oid)) { + strvec_pushf(&child.args, "--negotiation-tip=%s", + oid_to_hex(&ref->new_oid)); + nr_negotiation_tip++; + } } } + + if (negotiation_require && negotiation_require->nr) { + struct string_list_item *item; + for_each_string_list_item(item, negotiation_require) + strvec_pushf(&child.args, "--negotiation-require=%s", + item->string); + } + strvec_push(&child.args, url); if (!nr_negotiation_tip) { @@ -528,7 +547,10 @@ int send_pack(struct repository *r, repo_config_get_bool(r, "push.negotiate", &push_negotiate); if (push_negotiate) { trace2_region_enter("send_pack", "push_negotiate", r); - get_commons_through_negotiation(r, args->url, remote_refs, &commons); + get_commons_through_negotiation(r, args->url, + args->negotiation_require, + args->negotiation_restrict, + remote_refs, &commons); trace2_region_leave("send_pack", "push_negotiate", r); } diff --git a/send-pack.h b/send-pack.h index c5ded2d2006f13..112f31121a3835 100644 --- a/send-pack.h +++ b/send-pack.h @@ -18,6 +18,8 @@ struct repository; struct send_pack_args { const char *url; + const struct string_list *negotiation_require; + const struct string_list *negotiation_restrict; unsigned verbose:1, quiet:1, porcelain:1, diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 5dcb4b51a47d88..0246ac6bc5ecaa 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1460,6 +1460,165 @@ EOF test_cmp fatal-expect fatal-actual ' +test_expect_success '--negotiation-restrict limits "have" lines sent' ' + setup_negotiation_tip server server 0 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 --negotiation-restrict=beta_1 \ + origin alpha_s beta_s && + check_negotiation_tip +' + +test_expect_success '--negotiation-restrict understands globs' ' + setup_negotiation_tip server server 0 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=*_1 \ + origin alpha_s beta_s && + check_negotiation_tip +' + +test_expect_success '--negotiation-restrict and --negotiation-tip can be mixed' ' + setup_negotiation_tip server server 0 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-tip=beta_1 \ + origin alpha_s beta_s && + check_negotiation_tip +' + +test_expect_success 'remote..negotiationRestrict used as default' ' + setup_negotiation_tip server server 0 && + git -C client config --add remote.origin.negotiationRestrict alpha_1 && + git -C client config --add remote.origin.negotiationRestrict beta_1 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + origin alpha_s beta_s && + check_negotiation_tip +' + +test_expect_success 'CLI --negotiation-restrict overrides remote config' ' + setup_negotiation_tip server server 0 && + git -C client config --add remote.origin.negotiationRestrict alpha_1 && + git -C client config --add remote.origin.negotiationRestrict beta_1 && + ALPHA_1=$(git -C client rev-parse alpha_1) && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + origin alpha_s beta_s && + test_grep "fetch> have $ALPHA_1" trace && + BETA_1=$(git -C client rev-parse beta_1) && + test_grep ! "fetch> have $BETA_1" trace +' + +test_expect_success '--negotiation-require includes configured refs as haves' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-require=refs/tags/beta_1 \ + origin alpha_s beta_s && + + ALPHA_1=$(git -C client rev-parse alpha_1) && + test_grep "fetch> have $ALPHA_1" trace && + BETA_1=$(git -C client rev-parse beta_1) && + test_grep "fetch> have $BETA_1" trace +' + +test_expect_success '--negotiation-require works with glob patterns' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-require="refs/tags/beta_*" \ + origin alpha_s beta_s && + + BETA_1=$(git -C client rev-parse beta_1) && + test_grep "fetch> have $BETA_1" trace && + BETA_2=$(git -C client rev-parse beta_2) && + test_grep "fetch> have $BETA_2" trace +' + +test_expect_success '--negotiation-require is additive with negotiation' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-require=refs/tags/beta_1 \ + origin alpha_s beta_s && + + BETA_1=$(git -C client rev-parse beta_1) && + test_grep "fetch> have $BETA_1" trace +' + +test_expect_success '--negotiation-require ignores non-existent refs silently' ' + setup_negotiation_tip server server 0 && + + git -C client fetch --quiet \ + --negotiation-restrict=alpha_1 \ + --negotiation-require=refs/tags/nonexistent \ + origin alpha_s beta_s 2>err && + test_must_be_empty err +' + +test_expect_success '--negotiation-require avoids duplicates with negotiator' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + ALPHA_1=$(git -C client rev-parse alpha_1) && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-require=refs/tags/alpha_1 \ + origin alpha_s beta_s && + + test_grep "fetch> have $ALPHA_1" trace >matches && + test_line_count = 1 matches +' + +test_expect_success 'remote..negotiationRequire used as default for --negotiation-require' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + git -C client config --add remote.origin.negotiationRequire refs/tags/beta_1 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + origin alpha_s beta_s && + + ALPHA_1=$(git -C client rev-parse alpha_1) && + test_grep "fetch> have $ALPHA_1" trace && + BETA_1=$(git -C client rev-parse beta_1) && + test_grep "fetch> have $BETA_1" trace +' + +test_expect_success 'remote..negotiationRequire works with glob patterns' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + git -C client config --add remote.origin.negotiationRequire "refs/tags/beta_*" && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + origin alpha_s beta_s && + + BETA_1=$(git -C client rev-parse beta_1) && + test_grep "fetch> have $BETA_1" trace && + BETA_2=$(git -C client rev-parse beta_2) && + test_grep "fetch> have $BETA_2" trace +' + +test_expect_success 'CLI --negotiation-require overrides remote..negotiationRequire' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + git -C client config --add remote.origin.negotiationRequire refs/tags/beta_2 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-require=refs/tags/beta_1 \ + origin alpha_s beta_s && + + BETA_1=$(git -C client rev-parse beta_1) && + test_grep "fetch> have $BETA_1" trace && + BETA_2=$(git -C client rev-parse beta_2) && + test_grep ! "fetch> have $BETA_2" trace +' + test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' ' git init df-conflict && ( diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 29e2f176081561..03b797cef58b26 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -254,6 +254,36 @@ test_expect_success 'push with negotiation does not attempt to fetch submodules' ! grep "Fetching submodule" err ' +test_expect_success 'push with negotiation and remote..negotiationRequire' ' + test_when_finished rm -rf negotiation_require && + mk_empty negotiation_require && + git push negotiation_require $the_first_commit:refs/remotes/origin/first_commit && + test_commit -C negotiation_require unrelated_commit && + git -C negotiation_require config receive.hideRefs refs/remotes/origin/first_commit && + test_when_finished "rm event" && + GIT_TRACE2_EVENT="$(pwd)/event" \ + git -c protocol.version=2 -c push.negotiate=1 \ + -c remote.negotiation_require.negotiationRequire=refs/heads/main \ + push negotiation_require refs/heads/main:refs/remotes/origin/main && + test_grep \"key\":\"total_rounds\" event && + grep_wrote 2 event # 1 commit, 1 tree +' + +test_expect_success 'push with negotiation and remote..negotiationRestrict' ' + test_when_finished rm -rf negotiation_restrict && + mk_empty negotiation_restrict && + git push negotiation_restrict $the_first_commit:refs/remotes/origin/first_commit && + test_commit -C negotiation_restrict unrelated_commit && + git -C negotiation_restrict config receive.hideRefs refs/remotes/origin/first_commit && + test_when_finished "rm event" && + GIT_TRACE2_EVENT="$(pwd)/event" \ + git -c protocol.version=2 -c push.negotiate=1 \ + -c remote.negotiation_restrict.negotiationRestrict=refs/heads/main \ + push negotiation_restrict refs/heads/main:refs/remotes/origin/main && + test_grep \"key\":\"total_rounds\" event && + grep_wrote 2 event # 1 commit, 1 tree +' + test_expect_success 'push without wildcard' ' mk_empty testrepo && @@ -1349,7 +1379,7 @@ test_expect_success 'fetch follows tags by default' ' git for-each-ref >tmp1 && sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 | sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p" | - sort -k 4 >../expect + sort -k 3 >../expect ) && test_when_finished "rm -rf dst" && git init dst && diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index f826ac46a5be5a..9f6cf4142d5b83 100755 --- a/t/t5702-protocol-v2.sh +++ b/t/t5702-protocol-v2.sh @@ -869,14 +869,14 @@ setup_negotiate_only () { test_commit -C client three } -test_expect_success 'usage: --negotiate-only without --negotiation-tip' ' +test_expect_success 'usage: --negotiate-only without --negotiation-restrict' ' SERVER="server" && URI="file://$(pwd)/server" && setup_negotiate_only "$SERVER" "$URI" && cat >err.expect <<-\EOF && - fatal: --negotiate-only needs one or more --negotiation-tip=* + fatal: --negotiate-only needs one or more --negotiation-restrict=* EOF test_must_fail git -c protocol.version=2 -C client fetch \ diff --git a/transport-helper.c b/transport-helper.c index 4d95d84f9e4d05..0e5b3b7202cf20 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -754,7 +754,7 @@ static int fetch_refs(struct transport *transport, set_helper_option(transport, "filter", spec); } - if (data->transport_options.negotiation_tips) + if (data->transport_options.negotiation_restrict_tips) warning("Ignoring --negotiation-tip because the protocol does not support it."); if (data->fetch) diff --git a/transport.c b/transport.c index 107f4fa5dce96a..9903eb1a532dff 100644 --- a/transport.c +++ b/transport.c @@ -463,7 +463,8 @@ static int fetch_refs_via_pack(struct transport *transport, args.refetch = data->options.refetch; args.stateless_rpc = transport->stateless_rpc; args.server_options = transport->server_options; - args.negotiation_tips = data->options.negotiation_tips; + args.negotiation_restrict_tips = data->options.negotiation_restrict_tips; + args.negotiation_require = data->options.negotiation_require; args.reject_shallow_remote = transport->smart_options->reject_shallow; if (!data->finished_handshake) { @@ -491,11 +492,12 @@ static int fetch_refs_via_pack(struct transport *transport, warning(_("server does not support wait-for-done")); ret = -1; } else { - negotiate_using_fetch(data->options.negotiation_tips, + negotiate_using_fetch(data->options.negotiation_restrict_tips, transport->server_options, transport->stateless_rpc, data->fd, - data->options.acked_commits); + data->options.acked_commits, + data->options.negotiation_require); ret = 0; } goto cleanup; @@ -919,6 +921,8 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re args.atomic = !!(flags & TRANSPORT_PUSH_ATOMIC); args.push_options = transport->push_options; args.url = transport->url; + args.negotiation_require = &transport->remote->negotiation_require; + args.negotiation_restrict = &transport->remote->negotiation_restrict; if (flags & TRANSPORT_PUSH_CERT_ALWAYS) args.push_cert = SEND_PACK_PUSH_CERT_ALWAYS; @@ -979,9 +983,9 @@ static int disconnect_git(struct transport *transport) finish_connect(data->conn); } - if (data->options.negotiation_tips) { - oid_array_clear(data->options.negotiation_tips); - free(data->options.negotiation_tips); + if (data->options.negotiation_restrict_tips) { + oid_array_clear(data->options.negotiation_restrict_tips); + free(data->options.negotiation_restrict_tips); } list_objects_filter_release(&data->options.filter_options); oid_array_clear(&data->extra_have); diff --git a/transport.h b/transport.h index 892f19454a75d6..8737f23008ebc0 100644 --- a/transport.h +++ b/transport.h @@ -40,13 +40,19 @@ struct git_transport_options { /* * This is only used during fetch. See the documentation of - * negotiation_tips in struct fetch_pack_args. + * negotiation_restrict_tips in struct fetch_pack_args. * * This field is only supported by transports that support connect or * stateless_connect. Set this field directly instead of using * transport_set_option(). */ - struct oid_array *negotiation_tips; + struct oid_array *negotiation_restrict_tips; + + /* + * If non-empty, ref patterns whose tips should always be sent + * as "have" lines during negotiation. + */ + const struct string_list *negotiation_require; /* * If allocated, whenever transport_fetch_refs() is called, add known