From 4e5b2a37956f40acd016f078b1e9a883e97a9f27 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:48 +0000 Subject: [PATCH 1/8] t5516: fix test order flakiness The 'fetch follows tags by default' test sorts using 'sort -k 4', but for-each-ref output only has 3 columns. This relies on sort treating records with fewer fields as having an empty fourth field, which may produce unstable results depending on locale. This appears to be an accident added in 3f763ddf28 (fetch: set remote/HEAD if it does not exist, 2024-11-22). Use 'sort -k 3' to match the actual number of columns in the output. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- t/t5516-fetch-push.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index 29e2f17608..ac8447f21e 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -1349,7 +1349,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 && From 1a445fc60b84df95253d740f3a112343ab5ed8d2 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:49 +0000 Subject: [PATCH 2/8] fetch: add --negotiation-restrict option The --negotiation-tip option to 'git fetch' and 'git pull' allows users to specify that they want to focus negotiation on a small set of references. This is a _restriction_ on the negotiation set, helping to focus the negotiation when the ref count is high. However, it doesn't allow for the ability to opportunistically select references beyond that list. This subtle detail that this is a 'maximum set' and not a 'minimum set' is not immediately clear from the option name. This makes it more complicated to add a new option that provides the complementary behavior of a minimum set. For now, create a new synonym option, --negotiation-restrict, that behaves identically to --negotiation-tip. Update the documentation to make it clear that this new name is the preferred option, but we keep the old name for compatibility. Mark --negotiation-tip as an alias of the new, preferred option. Update a few warning messages with the new option, but also make them translatable with the option name inserted by formatting. At least one of these messages will be reused later for a new option. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/config/fetch.adoc | 2 +- Documentation/fetch-options.adoc | 6 +++++- builtin/fetch.c | 13 ++++++++----- builtin/pull.c | 3 ++- send-pack.c | 2 +- t/t5510-fetch.sh | 25 +++++++++++++++++++++++++ t/t5702-protocol-v2.sh | 4 ++-- transport-helper.c | 3 ++- 8 files changed, 46 insertions(+), 12 deletions(-) diff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc index cd40db0cad..04ac90912d 100644 --- a/Documentation/config/fetch.adoc +++ b/Documentation/config/fetch.adoc @@ -76,7 +76,7 @@ default is `skipping`. Unknown values will cause `git fetch` to error out. + -See also the `--negotiate-only` and `--negotiation-tip` options to +See also the `--negotiate-only` and `--negotiation-restrict` options to linkgit:git-fetch[1]. `fetch.showForcedUpdates`:: diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 81a9d7f9bb..d39cecb446 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. + @@ -71,7 +75,7 @@ configuration variables documented in linkgit:git-config[1], and the `--negotiate-only`:: Do not fetch anything from the server, and instead print the - ancestors of the provided `--negotiation-tip=` arguments, + ancestors of the provided `--negotiation-restrict=` arguments, which we have in common with the server. + This is incompatible with `--recurse-submodules=(yes|on-demand)`. diff --git a/builtin/fetch.c b/builtin/fetch.c index 4795b2a13c..fc950fe35b 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1558,8 +1558,8 @@ 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; } @@ -1599,7 +1599,8 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, if (transport->smart_options) add_negotiation_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"); } return transport; } @@ -2565,8 +2566,9 @@ int cmd_fetch(int argc, N_("specify fetch refmap"), PARSE_OPT_NONEG, parse_refmap_arg), OPT_STRING_LIST('o', "server-option", &server_options, N_("server-specific"), N_("option to transmit")), OPT_IPVERSION(&family), - OPT_STRING_LIST(0, "negotiation-tip", &negotiation_tip, N_("revision"), + OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_tip, N_("revision"), N_("report that we have only objects reachable from this object")), + OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"), 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), @@ -2657,7 +2659,8 @@ int cmd_fetch(int argc, } if (negotiate_only && !negotiation_tip.nr) - die(_("--negotiate-only needs one or more --negotiation-tip=*")); + die(_("%s needs one or more %s"), "--negotiate-only", + "--negotiation-restrict=*"); if (deepen_relative) { if (deepen_relative < 0) diff --git a/builtin/pull.c b/builtin/pull.c index 7e67fdce97..cc6ce485fc 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -996,9 +996,10 @@ int cmd_pull(int argc, OPT_PASSTHRU('6', "ipv6", &opt_ipv6, NULL, N_("use IPv6 addresses only"), PARSE_OPT_NOARG), - OPT_PASSTHRU_ARGV(0, "negotiation-tip", &opt_fetch, N_("revision"), + OPT_PASSTHRU_ARGV(0, "negotiation-restrict", &opt_fetch, N_("revision"), N_("report that we have only objects reachable from this object"), 0), + OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"), 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/send-pack.c b/send-pack.c index 67d6987b1c..3d5d36ba3b 100644 --- a/send-pack.c +++ b/send-pack.c @@ -447,7 +447,7 @@ static void get_commons_through_negotiation(struct repository *r, 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", + strvec_pushf(&child.args, "--negotiation-restrict=%s", oid_to_hex(&ref->new_oid)); nr_negotiation_tip++; } diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 5dcb4b51a4..dc3ce56d84 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1460,6 +1460,31 @@ 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 SYMLINKS 'clone does not get confused by a D/F conflict' ' git init df-conflict && ( diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index f826ac46a5..9f6cf4142d 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 4d95d84f9e..dd78d40668 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -755,7 +755,8 @@ static int fetch_refs(struct transport *transport, } if (data->transport_options.negotiation_tips) - warning("Ignoring --negotiation-tip because the protocol does not support it."); + warning(_("ignoring %s because the protocol does not support it."), + "--negotiation-restrict"); if (data->fetch) return fetch_with_fetch(transport, nr_heads, to_fetch); From 4aef7dbb063cfd0923baae5a431913256edad667 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:50 +0000 Subject: [PATCH 3/8] transport: rename negotiation_tips The previous change added the --negotiation-restrict synonym for the --negotiation-tip option for 'git fetch'. In anticipation of adding a new option that behaves similarly but with distinct changes to its behavior, rename the internal representation of this data from 'negotiation_tips' to 'negotiation_restrict_tips'. The 'tips' part is kept because this is an oid_array in the transport layer. This requires the builtin to handle parsing refs into collections of oids so the transport layer can handle this cleaner form of the data. Also update the string_list used to store the inputs from command-line options. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- builtin/fetch.c | 18 +++++++++--------- fetch-pack.c | 18 +++++++++--------- fetch-pack.h | 4 ++-- transport-helper.c | 2 +- transport.c | 10 +++++----- transport.h | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index fc950fe35b..2ba0051d52 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -98,7 +98,7 @@ static struct transport *gtransport; 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_restrict = STRING_LIST_INIT_NODUP; struct fetch_config { enum display_format display_format; @@ -1534,13 +1534,13 @@ 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; - for (i = 0; i < negotiation_tip.nr; i++) { - const char *s = negotiation_tip.items[i].string; + for (i = 0; i < negotiation_restrict.nr; i++) { + const char *s = negotiation_restrict.items[i].string; struct refs_for_each_ref_options opts = { .pattern = s, }; @@ -1561,7 +1561,7 @@ static void add_negotiation_tips(struct git_transport_options *smart_options) 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, @@ -1595,9 +1595,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, set_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec); set_option(transport, TRANS_OPT_FROM_PROMISOR, "1"); } - if (negotiation_tip.nr) { + if (negotiation_restrict.nr) { if (transport->smart_options) - add_negotiation_tips(transport->smart_options); + add_negotiation_restrict_tips(transport->smart_options); else warning(_("ignoring %s because the protocol does not support it"), "--negotiation-restrict"); @@ -2566,7 +2566,7 @@ int cmd_fetch(int argc, N_("specify fetch refmap"), PARSE_OPT_NONEG, parse_refmap_arg), OPT_STRING_LIST('o', "server-option", &server_options, N_("server-specific"), N_("option to transmit")), OPT_IPVERSION(&family), - OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_tip, N_("revision"), + OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_restrict, N_("revision"), N_("report that we have only objects reachable from this object")), OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"), OPT_BOOL(0, "negotiate-only", &negotiate_only, @@ -2658,7 +2658,7 @@ int cmd_fetch(int argc, config.display_format = DISPLAY_FORMAT_PORCELAIN; } - if (negotiate_only && !negotiation_tip.nr) + if (negotiate_only && !negotiation_restrict.nr) die(_("%s needs one or more %s"), "--negotiate-only", "--negotiation-restrict=*"); diff --git a/fetch-pack.c b/fetch-pack.c index 6ecd468ef7..baf239adf9 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -291,21 +291,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; } @@ -355,7 +355,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; @@ -1728,7 +1728,7 @@ 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); for_each_cached_alternate(negotiator, insert_one_alternate_object); break; @@ -2177,7 +2177,7 @@ 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[], @@ -2195,13 +2195,13 @@ 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); 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); diff --git a/fetch-pack.h b/fetch-pack.h index 9d3470366f..6c70c942c2 100644 --- a/fetch-pack.h +++ b/fetch-pack.h @@ -21,7 +21,7 @@ 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; unsigned deepen_relative:1; unsigned quiet:1; @@ -89,7 +89,7 @@ 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[], diff --git a/transport-helper.c b/transport-helper.c index dd78d40668..f4388da766 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 %s because the protocol does not support it."), "--negotiation-restrict"); diff --git a/transport.c b/transport.c index 107f4fa5dc..a3051f6733 100644 --- a/transport.c +++ b/transport.c @@ -463,7 +463,7 @@ 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.reject_shallow_remote = transport->smart_options->reject_shallow; if (!data->finished_handshake) { @@ -491,7 +491,7 @@ 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, @@ -979,9 +979,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 892f19454a..cdeb33c16f 100644 --- a/transport.h +++ b/transport.h @@ -40,13 +40,13 @@ 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 allocated, whenever transport_fetch_refs() is called, add known From 8bb252f86c30a3066ec64f99f94719c01a53743a Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:51 +0000 Subject: [PATCH 4/8] remote: add remote.*.negotiationRestrict config In a previous change, the --negotiation-restrict command-line option of 'git fetch' was added as a synonym of --negotiation-tip. Both of these options restrict the set of 'haves' the client can send as part of negotiation. This was previously not available via a configuration option. Add a new 'remote..negotiationRestrict' multi-valued config option that updates 'git fetch ' to use these restrictions by default. If the user provides even one --negotiation-restrict argument, then the config is ignored. An empty value resets the value list to allow ignoring earlier config values, such as those that might be set in system or global config. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/config/remote.adoc | 18 ++++++++++++++++++ builtin/fetch.c | 28 +++++++++++++++++++++------- remote.c | 5 +++++ remote.h | 1 + t/t5510-fetch.sh | 26 ++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 91e46f66f5..4dcf81fbce 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -107,6 +107,24 @@ 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`, 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. ++ +Blank values signal to ignore all previous values, allowing a reset of +the list from broader config scenarios. + 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/builtin/fetch.c b/builtin/fetch.c index 2ba0051d52..a957739f37 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1601,6 +1601,19 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, else 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_restrict, 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); + } } return transport; } @@ -2658,10 +2671,6 @@ int cmd_fetch(int argc, config.display_format = DISPLAY_FORMAT_PORCELAIN; } - if (negotiate_only && !negotiation_restrict.nr) - die(_("%s needs one or more %s"), "--negotiate-only", - "--negotiation-restrict=*"); - if (deepen_relative) { if (deepen_relative < 0) die(_("negative depth in --deepen is not supported")); @@ -2749,14 +2758,19 @@ 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->acked_commits = &acked_commits; - } else { + + if (!gtransport->smart_options) { warning(_("protocol does not support --negotiate-only, exiting")); result = 1; trace2_region_leave("fetch", "negotiate-only", the_repository); goto cleanup; } + if (!gtransport->smart_options->negotiation_restrict_tips) + die(_("%s needs one or more %s"), "--negotiate-only", + "--negotiation-restrict=*"); + + gtransport->smart_options->acked_commits = &acked_commits; + if (server_options.nr) gtransport->server_options = &server_options; result = transport_fetch_refs(gtransport, NULL); diff --git a/remote.c b/remote.c index 7ca2a6501b..620086e16e 100644 --- a/remote.c +++ b/remote.c @@ -152,6 +152,7 @@ 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); ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1, remote_state->remotes_alloc); @@ -179,6 +180,7 @@ 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); } static void add_merge(struct branch *branch, const char *name) @@ -562,6 +564,9 @@ 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")) { + return parse_transport_option(key, value, + &remote->negotiation_restrict); } else if (!strcmp(subkey, "followremotehead")) { const char *no_warn_branch; if (!strcmp(value, "never")) diff --git a/remote.h b/remote.h index fc052945ee..e6ec37c393 100644 --- a/remote.h +++ b/remote.h @@ -117,6 +117,7 @@ struct remote { char *http_proxy_authmethod; struct string_list server_options; + struct string_list negotiation_restrict; enum follow_remote_head_settings follow_remote_head; const char *no_warn_branch; diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index dc3ce56d84..eff3ce8e2d 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1485,6 +1485,32 @@ test_expect_success '--negotiation-restrict and --negotiation-tip can be mixed' check_negotiation_tip ' +test_expect_success 'remote..negotiationRestrict used as default' ' + setup_negotiation_tip server server 0 && + + # test the reset of the list on an empty value + git -C client config --add remote.origin.negotiationRestrict alpha_2 && + git -C client config --add remote.origin.negotiationRestrict "" && + 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 SYMLINKS 'clone does not get confused by a D/F conflict' ' git init df-conflict && ( From 22b2f3d2a319af32e9f3add0b3cc7732cbf4733b Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:52 +0000 Subject: [PATCH 5/8] negotiator: add have_sent() interface In a future change, we will introduce a capability to choose specific commit OIDs as 'have's in fetch negotiation, with the ability to have the negotiator choose more 'have's to increase coverage beyond that required core set. The negotiator works to avoid emitting 'have's that can reach each other, but that logic is hidden beneath the negotiator's iterator function pointer ('next'). We need a way to communicate to the negotiator that we have picked a 'have' so it could incorporate that into its logic. Add a have_sent() method to the fetch_negotiator interface. This is the signal that allows the negotiator to track the commit as already shown and can perform the proper bookkeeping to avoid emitting those objects or anything they can reach. For our non-trivial negotiators, it is sufficient to mark these commits as common, so the implementation is quite simple. This logic will be exercised in the next change. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- fetch-negotiator.h | 9 +++++++++ negotiator/default.c | 8 ++++++++ negotiator/noop.c | 7 +++++++ negotiator/skipping.c | 8 ++++++++ 4 files changed, 32 insertions(+) diff --git a/fetch-negotiator.h b/fetch-negotiator.h index e348905a1f..6ca422a064 100644 --- a/fetch-negotiator.h +++ b/fetch-negotiator.h @@ -47,6 +47,15 @@ struct fetch_negotiator { */ int (*ack)(struct fetch_negotiator *, struct commit *); + /* + * Inform the negotiator that this commit has already been sent as + * a "have" line outside of the negotiator's control. The negotiator + * should avoid outputting it from next() and may use it to optimize + * further negotiation (e.g., by treating it and its ancestors as + * common). + */ + void (*have_sent)(struct fetch_negotiator *, struct commit *); + void (*release)(struct fetch_negotiator *); /* internal use */ diff --git a/negotiator/default.c b/negotiator/default.c index 116dedcf83..05ab616f39 100644 --- a/negotiator/default.c +++ b/negotiator/default.c @@ -175,6 +175,13 @@ static int ack(struct fetch_negotiator *n, struct commit *c) return known_to_be_common; } +static void have_sent(struct fetch_negotiator *n, struct commit *c) +{ + if (repo_parse_commit(the_repository, c)) + return; + mark_common(n->data, c, 0, 0); +} + static void release(struct fetch_negotiator *n) { clear_prio_queue(&((struct negotiation_state *)n->data)->rev_list); @@ -188,6 +195,7 @@ void default_negotiator_init(struct fetch_negotiator *negotiator) negotiator->add_tip = add_tip; negotiator->next = next; negotiator->ack = ack; + negotiator->have_sent = have_sent; negotiator->release = release; negotiator->data = CALLOC_ARRAY(ns, 1); ns->rev_list.compare = compare_commits_by_commit_date; diff --git a/negotiator/noop.c b/negotiator/noop.c index 65e3c20008..edf1b456f3 100644 --- a/negotiator/noop.c +++ b/negotiator/noop.c @@ -29,6 +29,12 @@ static int ack(struct fetch_negotiator *n UNUSED, struct commit *c UNUSED) return 0; } +static void have_sent(struct fetch_negotiator *n UNUSED, + struct commit *c UNUSED) +{ + /* nothing to do */ +} + static void release(struct fetch_negotiator *n UNUSED) { /* nothing to release */ @@ -40,6 +46,7 @@ void noop_negotiator_init(struct fetch_negotiator *negotiator) negotiator->add_tip = add_tip; negotiator->next = next; negotiator->ack = ack; + negotiator->have_sent = have_sent; negotiator->release = release; negotiator->data = NULL; } diff --git a/negotiator/skipping.c b/negotiator/skipping.c index 0a272130fb..69472c58e1 100644 --- a/negotiator/skipping.c +++ b/negotiator/skipping.c @@ -243,6 +243,13 @@ static int ack(struct fetch_negotiator *n, struct commit *c) return known_to_be_common; } +static void have_sent(struct fetch_negotiator *n, struct commit *c) +{ + if (repo_parse_commit(the_repository, c)) + return; + mark_common(n->data, c); +} + static void release(struct fetch_negotiator *n) { struct data *data = n->data; @@ -259,6 +266,7 @@ void skipping_negotiator_init(struct fetch_negotiator *negotiator) negotiator->add_tip = add_tip; negotiator->next = next; negotiator->ack = ack; + negotiator->have_sent = have_sent; negotiator->release = release; negotiator->data = CALLOC_ARRAY(data, 1); data->rev_list.compare = compare; From e2164742c9ceb60ac9ddd2114f49304fd73df1f3 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:53 +0000 Subject: [PATCH 6/8] fetch: add --negotiation-include option for negotiation Add a new --negotiation-include option to 'git fetch', which ensures that certain ref tips are always sent as 'have' lines during fetch negotiation, regardless of what the negotiation algorithm selects. This is useful when the repository has a large number of references, so the normal negotiation algorithm truncates the list. This is especially important in repositories with long parallel commit histories. For example, a repo could have a 'dev' branch for development and a 'release' branch for released versions. If the 'dev' branch isn't selected for negotiation, then it's not a big deal because there are many in-progress development branches with a shared history. However, if 'release' is not selected for negotiation, then the server may think that this is the first time the client has asked for that reference, causing a full download of its parallel commit history (and any extra data that may be unique to that branch). This is based on a real example where certain fetches would grow to 60+ GB when a release branch updated. This option is a complement to --negotiation-restrict, which reduces the negotiation ref set to a specific list. In the earlier example, using --negotiation-restrict to focus the negotiation to 'dev' and 'release' would avoid those problematic downloads, but would still not allow advertising potentially-relevant user branches. In this way, the 'include' version solves the problem I mention while allowing negotiation to pick other references opportunistically. The two options can also be combined to allow the best of both worlds. The argument may be an exact ref name or a glob pattern. Non-existent refs are silently ignored. This behavior is also updated in the ref matching logic for the related --negotiation-restrict option to match. The implementation outputs the requested objects as haves before the negotiator performs its own algorithm to choose the next haves. Use the new have_sent() interface to signal these have commits were sent before engaging with the negotiator's next() iterator. Also add --negotiation-include to 'git pull' passthrough options. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/fetch-options.adoc | 19 +++++++ builtin/fetch.c | 38 ++++++++++--- builtin/pull.c | 3 ++ fetch-pack.c | 81 +++++++++++++++++++++++++--- fetch-pack.h | 6 ++- t/t5510-fetch.sh | 91 ++++++++++++++++++++++++++++++++ transport.c | 8 ++- transport.h | 5 +- 8 files changed, 232 insertions(+), 19 deletions(-) diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index d39cecb446..7b897a7202 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -73,6 +73,25 @@ See also the `fetch.negotiationAlgorithm` and `push.negotiate` configuration variables documented in linkgit:git-config[1], and the `--negotiate-only` option below. +`--negotiation-include=(|)`:: + Ensure that the commits at the given tips are always sent as "have" + lines 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 commit is sent +unconditionally. ++ +The argument may be an exact ref name (e.g. `refs/heads/release`), an +object hash, 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-include`. + `--negotiate-only`:: Do not fetch anything from the server, and instead print the ancestors of the provided `--negotiation-restrict=` arguments, diff --git a/builtin/fetch.c b/builtin/fetch.c index a957739f37..ba56e9022b 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_restrict = STRING_LIST_INIT_NODUP; +static struct string_list negotiation_include = STRING_LIST_INIT_NODUP; struct fetch_config { enum display_format display_format; @@ -1534,23 +1535,29 @@ static int add_oid(const struct reference *ref, void *cb_data) return 0; } -static void add_negotiation_restrict_tips(struct git_transport_options *smart_options) +static void add_negotiation_tips(struct string_list *input_list, + struct oid_array **output_list, + const char *argname) { struct oid_array *oids = xcalloc(1, sizeof(*oids)); int i; - for (i = 0; i < negotiation_restrict.nr; i++) { - const char *s = negotiation_restrict.items[i].string; + for (i = 0; i < input_list->nr; i++) { + const char *s = input_list->items[i].string; struct refs_for_each_ref_options opts = { .pattern = s, }; int old_nr; if (!has_glob_specials(s)) { struct object_id oid; + + /* Ignore missing reference. */ if (repo_get_oid(the_repository, s, &oid)) - die(_("%s is not a valid object"), s); + continue; + /* Fail on missing object pointed by ref. */ if (!odb_has_object(the_repository->objects, &oid, 0)) die(_("the object %s does not exist"), s); + oid_array_append(oids, &oid); continue; } @@ -1559,9 +1566,9 @@ static void add_negotiation_restrict_tips(struct git_transport_options *smart_op add_oid, oids, &opts); if (old_nr == oids->nr) warning(_("ignoring %s=%s because it does not match any refs"), - "--negotiation-restrict", s); + argname, s); } - smart_options->negotiation_restrict_tips = oids; + *output_list = oids; } static struct transport *prepare_transport(struct remote *remote, int deepen, @@ -1597,7 +1604,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, } if (negotiation_restrict.nr) { if (transport->smart_options) - add_negotiation_restrict_tips(transport->smart_options); + add_negotiation_tips(&negotiation_restrict, + &transport->smart_options->negotiation_restrict_tips, + "--negotiation-restrict"); else warning(_("ignoring %s because the protocol does not support it"), "--negotiation-restrict"); @@ -1606,7 +1615,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, for_each_string_list_item(item, &remote->negotiation_restrict) string_list_append(&negotiation_restrict, item->string); if (transport->smart_options) - add_negotiation_restrict_tips(transport->smart_options); + add_negotiation_tips(&negotiation_restrict, + &transport->smart_options->negotiation_restrict_tips, + "--negotiation-restrict"); else { struct strbuf config_name = STRBUF_INIT; strbuf_addf(&config_name, "remote.%s.negotiationRestrict", remote->name); @@ -1615,6 +1626,15 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, strbuf_release(&config_name); } } + if (negotiation_include.nr) { + if (transport->smart_options) + add_negotiation_tips(&negotiation_include, + &transport->smart_options->negotiation_include_tips, + "--negotiation-include"); + else + warning(_("ignoring %s because the protocol does not support it"), + "--negotiation-include"); + } return transport; } @@ -2582,6 +2602,8 @@ int cmd_fetch(int argc, OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_restrict, N_("revision"), N_("report that we have only objects reachable from this object")), OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"), + OPT_STRING_LIST(0, "negotiation-include", &negotiation_include, 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), diff --git a/builtin/pull.c b/builtin/pull.c index cc6ce485fc..d49b09114a 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -1000,6 +1000,9 @@ int cmd_pull(int argc, N_("report that we have only objects reachable from this object"), 0), OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"), + OPT_PASSTHRU_ARGV(0, "negotiation-include", &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 baf239adf9..96071434b8 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" @@ -332,6 +333,21 @@ static void send_filter(struct fetch_pack_args *args, } } +static void add_oids_to_set(const struct oid_array *array, + struct oidset *set) +{ + if (!array) + return; + + for (size_t i = 0; i < array->nr; i++) { + struct object_id *oid = &array->oid[i]; + if (!odb_has_object(the_repository->objects, oid, 0)) + die(_("the object %s does not exist"), oid_to_hex(oid)); + + oidset_insert(set, oid); + } +} + static int find_common(struct fetch_negotiator *negotiator, struct fetch_pack_args *args, int fd[2], struct object_id *result_oid, @@ -347,6 +363,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_include_oids = OIDSET_INIT; if (args->stateless_rpc && multi_ack == 1) die(_("the option '%s' requires '%s'"), "--stateless-rpc", "multi_ack_detailed"); @@ -474,6 +491,27 @@ 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-include */ + add_oids_to_set(args->negotiation_include_tips, + &negotiation_include_oids); + if (oidset_size(&negotiation_include_oids)) { + struct oidset_iter iter; + oidset_iter_init(&negotiation_include_oids, &iter); + + while ((oid = oidset_iter_next(&iter))) { + struct commit *commit; + packet_buf_write(&req_buf, "have %s\n", + oid_to_hex(oid)); + print_verbose(args, "have %s", oid_to_hex(oid)); + count++; + + commit = lookup_commit(the_repository, oid); + if (commit) + negotiator->have_sent(negotiator, commit); + } + } + while ((oid = negotiator->next(negotiator))) { packet_buf_write(&req_buf, "have %s\n", oid_to_hex(oid)); print_verbose(args, "have %s", oid_to_hex(oid)); @@ -584,6 +622,7 @@ done: flushes++; } strbuf_release(&req_buf); + oidset_clear(&negotiation_include_oids); if (!got_ready || !no_done) consume_shallow_list(args, &reader); @@ -1305,11 +1344,27 @@ 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_include_oids) { int haves_added = 0; const struct object_id *oid; + /* Send unconditional haves from --negotiation-include */ + if (negotiation_include_oids) { + struct oidset_iter iter; + oidset_iter_init(negotiation_include_oids, &iter); + + while ((oid = oidset_iter_next(&iter))) { + struct commit *commit = lookup_commit(the_repository, oid); + if (commit) { + packet_buf_write(req_buf, "have %s\n", + oid_to_hex(oid)); + negotiator->have_sent(negotiator, commit); + } + } + } + while ((oid = negotiator->next(negotiator))) { packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid)); if (++haves_added >= *haves_to_send) @@ -1358,7 +1413,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_include_oids) { int haves_added; int done_sent = 0; @@ -1413,7 +1469,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_include_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 +1714,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_include_oids = OIDSET_INIT; struct packet_reader reader; int in_vain = 0, negotiation_started = 0; int negotiation_round = 0; @@ -1729,6 +1787,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, state = FETCH_SEND_REQUEST; mark_tips(negotiator, args->negotiation_restrict_tips); + add_oids_to_set(args->negotiation_include_tips, + &negotiation_include_oids); for_each_cached_alternate(negotiator, insert_one_alternate_object); break; @@ -1747,7 +1807,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_include_oids)) { trace2_region_leave_printf("negotiation_v2", "round", the_repository, "%d", negotiation_round); @@ -1883,6 +1944,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, negotiator->release(negotiator); oidset_clear(&common); + oidset_clear(&negotiation_include_oids); return ref; } @@ -2181,12 +2243,14 @@ 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 oid_array *negotiation_include_tips) { 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_include_oids = OIDSET_INIT; int haves_to_send = INITIAL_FLUSH; int in_vain = 0; int seen_ack = 0; @@ -2197,6 +2261,9 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, fetch_negotiator_init(the_repository, &negotiator); mark_tips(&negotiator, negotiation_restrict_tips); + add_oids_to_set(negotiation_include_tips, + &negotiation_include_oids); + packet_reader_init(&reader, fd[0], NULL, 0, PACKET_READ_CHOMP_NEWLINE | PACKET_READ_DIE_ON_ERR_PACKET); @@ -2221,7 +2288,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_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_include_oids); in_vain += haves_added; if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN)) last_iteration = 1; @@ -2273,6 +2341,7 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, clear_common_flag(acked_commits); object_array_clear(&nt_object_array); + oidset_clear(&negotiation_include_oids); negotiator.release(&negotiator); strbuf_release(&req_buf); } diff --git a/fetch-pack.h b/fetch-pack.h index 6c70c942c2..6d0dec7f41 100644 --- a/fetch-pack.h +++ b/fetch-pack.h @@ -19,9 +19,10 @@ struct fetch_pack_args { /* * If not NULL, during packfile negotiation, fetch-pack will send "have" - * lines only with these tips and their ancestors. + * lines for all _include_ tips and then a subset of the _restrict_ tips. */ const struct oid_array *negotiation_restrict_tips; + const struct oid_array *negotiation_include_tips; unsigned deepen_relative:1; unsigned quiet:1; @@ -93,7 +94,8 @@ 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 oid_array *negotiation_include_tips); /* * Print an appropriate error message for each sought ref that wasn't diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index eff3ce8e2d..bc2e2af959 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1460,6 +1460,16 @@ EOF test_cmp fatal-expect fatal-actual ' +test_expect_success '--negotiation-tip ignores missing refs and invalid hashes' ' + setup_negotiation_tip server server 0 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-tip=alpha_1 --negotiation-tip=beta_1 \ + --negotiation-tip=no-such-ref \ + --negotiation-tip=invalid-hash \ + origin alpha_s beta_s && + check_negotiation_tip +' + test_expect_success '--negotiation-restrict limits "have" lines sent' ' setup_negotiation_tip server server 0 && GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ @@ -1511,6 +1521,87 @@ test_expect_success 'CLI --negotiation-restrict overrides remote config' ' test_grep ! "fetch> have $BETA_1" trace ' +test_expect_success '--negotiation-include 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-include=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-include 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-include="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-include 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-include=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-include ignores non-existent refs silently' ' + setup_negotiation_tip server server 0 && + + git -C client fetch --quiet \ + --negotiation-restrict=alpha_1 \ + --negotiation-include=refs/tags/nonexistent \ + origin alpha_s beta_s 2>err && + test_must_be_empty err +' + +test_expect_success '--negotiation-include 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-include=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 '--negotiation-include avoids duplicates with v0' ' + 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 \ + -c protocol.version=0 fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-include=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 SYMLINKS 'clone does not get confused by a D/F conflict' ' git init df-conflict && ( diff --git a/transport.c b/transport.c index a3051f6733..fa54928966 100644 --- a/transport.c +++ b/transport.c @@ -464,6 +464,7 @@ static int fetch_refs_via_pack(struct transport *transport, args.stateless_rpc = transport->stateless_rpc; args.server_options = transport->server_options; args.negotiation_restrict_tips = data->options.negotiation_restrict_tips; + args.negotiation_include_tips = data->options.negotiation_include_tips; args.reject_shallow_remote = transport->smart_options->reject_shallow; if (!data->finished_handshake) { @@ -495,7 +496,8 @@ static int fetch_refs_via_pack(struct transport *transport, transport->server_options, transport->stateless_rpc, data->fd, - data->options.acked_commits); + data->options.acked_commits, + data->options.negotiation_include_tips); ret = 0; } goto cleanup; @@ -983,6 +985,10 @@ static int disconnect_git(struct transport *transport) oid_array_clear(data->options.negotiation_restrict_tips); free(data->options.negotiation_restrict_tips); } + if (data->options.negotiation_include_tips) { + oid_array_clear(data->options.negotiation_include_tips); + free(data->options.negotiation_include_tips); + } list_objects_filter_release(&data->options.filter_options); oid_array_clear(&data->extra_have); oid_array_clear(&data->shallow); diff --git a/transport.h b/transport.h index cdeb33c16f..97d905ecc0 100644 --- a/transport.h +++ b/transport.h @@ -40,13 +40,14 @@ struct git_transport_options { /* * This is only used during fetch. See the documentation of - * negotiation_restrict_tips in struct fetch_pack_args. + * these member names in struct fetch_pack_args. * - * This field is only supported by transports that support connect or + * These fields are only supported by transports that support connect or * stateless_connect. Set this field directly instead of using * transport_set_option(). */ struct oid_array *negotiation_restrict_tips; + struct oid_array *negotiation_include_tips; /* * If allocated, whenever transport_fetch_refs() is called, add known From 6f37fecfed7633d47b2c0e16fde0a8ca89e45beb Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:54 +0000 Subject: [PATCH 7/8] remote: add remote.*.negotiationInclude config Add a new 'remote..negotiationInclude' multi-valued config option that provides default values for --negotiation-include when no --negotiation-include arguments are specified over the command line. This is a mirror of how 'remote..negotiationRestrict' specifies defaults for the --negotiation-restrict arguments. Each value is either an exact ref name or a glob pattern whose tips should always be sent as 'have' lines during negotiation. The config values are resolved through the same resolve_negotiation_include() codepath as the CLI options. This option is additive with the normal negotiation process: the negotiation algorithm still runs and advertises its own selected commits, but the refs matching the config are sent unconditionally on top of those heuristically selected commits. Similar to the negotiationRestrict config, an empty value resets the value list to allow ignoring earlier config values, such as those that might be set in system or global config. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/config/remote.adoc | 25 ++++++++++++++++ Documentation/fetch-options.adoc | 4 +++ builtin/fetch.c | 12 ++++++++ remote.c | 5 ++++ remote.h | 1 + t/t5510-fetch.sh | 49 ++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+) diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 4dcf81fbce..1951df154e 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -125,6 +125,31 @@ values are not used. Blank values signal to ignore all previous values, allowing a reset of the list from broader config scenarios. +remote..negotiationInclude:: + When negotiating with this remote during `git fetch`, 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 references, commit hashes, + or ref pattern globs 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`), a +commit hash, or a glob pattern (e.g. `refs/heads/release/*`). The +pattern syntax is the same as for `--negotiation-include`. ++ +These config values are used as defaults for the `--negotiation-include` +command-line option. If `--negotiation-include` 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..negotiationInclude` are sent +unconditionally on top of those heuristically selected commits. ++ +Blank values signal to ignore all previous values, allowing a reset of +the list from broader config scenarios. + 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 7b897a7202..8074004377 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -91,6 +91,10 @@ 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-include`. ++ +If this option is not specified on the command line, then any +`remote..negotiationInclude` config values for the current remote +are used instead. `--negotiate-only`:: Do not fetch anything from the server, and instead print the diff --git a/builtin/fetch.c b/builtin/fetch.c index ba56e9022b..1af6500c1d 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1634,6 +1634,18 @@ static struct transport *prepare_transport(struct remote *remote, int deepen, else warning(_("ignoring %s because the protocol does not support it"), "--negotiation-include"); + } else if (remote->negotiation_include.nr) { + if (transport->smart_options) { + add_negotiation_tips(&remote->negotiation_include, + &transport->smart_options->negotiation_include_tips, + "--negotiation-include"); + } else { + struct strbuf config_name = STRBUF_INIT; + strbuf_addf(&config_name, "remote.%s.negotiationInclude", remote->name); + warning(_("ignoring %s because the protocol does not support it"), + config_name.buf); + strbuf_release(&config_name); + } } return transport; } diff --git a/remote.c b/remote.c index 620086e16e..6fb5758820 100644 --- a/remote.c +++ b/remote.c @@ -153,6 +153,7 @@ static struct remote *make_remote(struct remote_state *remote_state, 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_include); ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1, remote_state->remotes_alloc); @@ -181,6 +182,7 @@ static void remote_clear(struct remote *remote) 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_include, 0); } static void add_merge(struct branch *branch, const char *name) @@ -567,6 +569,9 @@ static int handle_config(const char *key, const char *value, } else if (!strcmp(subkey, "negotiationrestrict")) { return parse_transport_option(key, value, &remote->negotiation_restrict); + } else if (!strcmp(subkey, "negotiationinclude")) { + return parse_transport_option(key, value, + &remote->negotiation_include); } else if (!strcmp(subkey, "followremotehead")) { const char *no_warn_branch; if (!strcmp(value, "never")) diff --git a/remote.h b/remote.h index e6ec37c393..d8809b6991 100644 --- a/remote.h +++ b/remote.h @@ -118,6 +118,7 @@ struct remote { struct string_list server_options; struct string_list negotiation_restrict; + struct string_list negotiation_include; enum follow_remote_head_settings follow_remote_head; const char *no_warn_branch; diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index bc2e2af959..33f61ac12a 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1587,6 +1587,55 @@ test_expect_success '--negotiation-include avoids duplicates with negotiator' ' test_line_count = 1 matches ' +test_expect_success 'remote..negotiationInclude used as default for --negotiation-include' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + # test the reset of the list on an empty value + git -C client config --add remote.origin.negotiationInclude refs/tags/alpha_1 && + git -C client config --add remote.origin.negotiationInclude "" && + git -C client config --add remote.origin.negotiationInclude refs/tags/beta_1 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=beta_2 \ + 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..negotiationInclude works with glob patterns' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + git -C client config --add remote.origin.negotiationInclude "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-include overrides remote..negotiationInclude' ' + test_when_finished rm -f trace && + setup_negotiation_tip server server 0 && + + git -C client config --add remote.origin.negotiationInclude refs/tags/beta_2 && + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \ + --negotiation-restrict=alpha_1 \ + --negotiation-include=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 '--negotiation-include avoids duplicates with v0' ' test_when_finished rm -f trace && setup_negotiation_tip server server 0 && From a6d92c48e4426b88a427a75ed2c20d1daa5dc7f7 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 19 May 2026 16:24:55 +0000 Subject: [PATCH 8/8] send-pack: pass negotiation config in push When push.negotiate is enabled, 'git push' spawns a child 'git fetch --negotiate-only' process to find common commits. Pass --negotiation-include and --negotiation-restrict options from the 'remote..negotiationInclude' and 'remote..negotiationRestrict' config keys to this child process. When negotiationRestrict is configured, it replaces the default behavior of using all remote refs as negotiation tips. This allows the user to control which local refs are used for push negotiation. When negotiationInclude is configured, the specified ref patterns are passed as --negotiation-include to ensure their tips are always sent as 'have' lines during push negotiation. Reviewed-by: Matthew John Cheetham Signed-off-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/config/remote.adoc | 6 ++++++ send-pack.c | 37 ++++++++++++++++++++++++++------ send-pack.h | 2 ++ t/t5516-fetch-push.sh | 30 ++++++++++++++++++++++++++ transport.c | 2 ++ 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 1951df154e..eb9c8a3c48 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -122,6 +122,9 @@ command-line option. If `--negotiation-restrict` (or its synonym `--negotiation-tip`) is specified on the command line, then the config values are not used. + +These values also influence negotiation during `git push` if +`push.negotiate` is enabled. ++ Blank values signal to ignore all previous values, allowing a reset of the list from broader config scenarios. @@ -147,6 +150,9 @@ negotiation algorithm still runs and advertises its own selected commits, but the refs matching `remote..negotiationInclude` are sent unconditionally on top of those heuristically selected commits. + +These values also influence negotiation during `git push` if +`push.negotiate` is enabled. ++ Blank values signal to ignore all previous values, allowing a reset of the list from broader config scenarios. diff --git a/send-pack.c b/send-pack.c index 3d5d36ba3b..d18e030ce8 100644 --- a/send-pack.c +++ b/send-pack.c @@ -433,28 +433,48 @@ 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_include, + const struct string_list *negotiation_restrict, const struct ref *remote_refs, struct oid_array *commons) { struct child_process child = CHILD_PROCESS_INIT; const struct ref *ref; int len = r->hash_algo->hexsz + 1; /* hash + NL */ - int nr_negotiation_tip = 0; + int nr_negotiation = 0; child.git_cmd = 1; 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)) { + + 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", - oid_to_hex(&ref->new_oid)); - nr_negotiation_tip++; + item->string); + nr_negotiation = negotiation_restrict->nr; + } else { + for (ref = remote_refs; ref; ref = ref->next) { + if (!is_null_oid(&ref->new_oid)) { + strvec_pushf(&child.args, "--negotiation-restrict=%s", + oid_to_hex(&ref->new_oid)); + nr_negotiation++; + } } } + + if (negotiation_include && negotiation_include->nr) { + struct string_list_item *item; + for_each_string_list_item(item, negotiation_include) + strvec_pushf(&child.args, "--negotiation-include=%s", + item->string); + nr_negotiation += negotiation_include->nr; + } + strvec_push(&child.args, url); - if (!nr_negotiation_tip) { + if (!nr_negotiation) { child_process_clear(&child); return; } @@ -528,7 +548,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_include, + 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 c5ded2d200..13850c98bb 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_include; + const struct string_list *negotiation_restrict; unsigned verbose:1, quiet:1, porcelain:1, diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index ac8447f21e..177cbc6c75 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..negotiationInclude' ' + test_when_finished rm -rf negotiation_include && + mk_empty negotiation_include && + git push negotiation_include $the_first_commit:refs/remotes/origin/first_commit && + test_commit -C negotiation_include unrelated_commit && + git -C negotiation_include 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_include.negotiationInclude=refs/heads/main \ + push negotiation_include 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 && diff --git a/transport.c b/transport.c index fa54928966..a2d8958cb8 100644 --- a/transport.c +++ b/transport.c @@ -921,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_include = &transport->remote->negotiation_include; + args.negotiation_restrict = &transport->remote->negotiation_restrict; if (flags & TRANSPORT_PUSH_CERT_ALWAYS) args.push_cert = SEND_PACK_PUSH_CERT_ALWAYS;