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.<name>.negotiationRestrict' multi-valued config option that updates 'git fetch <name>' 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 <mjcheetham@outlook.com> Signed-off-by: Derrick Stolee <stolee@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed May 19, 2026 at 16:24 UTC 8bb252f86c30a3066ec64f99f94719c01a53743a
5 files changed +71 -7
Documentation/config/remote.adoc
+18
@@ -107,6 +107,24 @@ priority configuration file (e.g. `.git/config` in a repository) to clear
107 the values inherited from a lower priority configuration files (e.g.
108 `$HOME/.gitconfig`).
109
110 +remote.<name>.negotiationRestrict::
111 + When negotiating with this remote during `git fetch`, restrict the
112 + commits advertised as "have" lines to only those reachable from refs
113 + matching the given patterns. This multi-valued config option behaves
114 + like `--negotiation-restrict` on the command line.
115 ++
116 +Each value is either an exact ref name (e.g. `refs/heads/release`) or a
117 +glob pattern (e.g. `refs/heads/release/*`). The pattern syntax is the
118 +same as for `--negotiation-restrict`.
119 ++
120 +These config values are used as defaults for the `--negotiation-restrict`
121 +command-line option. If `--negotiation-restrict` (or its synonym
122 +`--negotiation-tip`) is specified on the command line, then the config
123 +values are not used.
124 ++
125 +Blank values signal to ignore all previous values, allowing a reset of
126 +the list from broader config scenarios.
127 +
128 remote.<name>.followRemoteHEAD::
129 How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
130 when fetching using the configured refspecs of a remote.
builtin/fetch.c
+21 -7
@@ -1601,6 +1601,19 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
1601 else
1602 warning(_("ignoring %s because the protocol does not support it"),
1603 "--negotiation-restrict");
1604 + } else if (remote->negotiation_restrict.nr) {
1605 + struct string_list_item *item;
1606 + for_each_string_list_item(item, &remote->negotiation_restrict)
1607 + string_list_append(&negotiation_restrict, item->string);
1608 + if (transport->smart_options)
1609 + add_negotiation_restrict_tips(transport->smart_options);
1610 + else {
1611 + struct strbuf config_name = STRBUF_INIT;
1612 + strbuf_addf(&config_name, "remote.%s.negotiationRestrict", remote->name);
1613 + warning(_("ignoring %s because the protocol does not support it"),
1614 + config_name.buf);
1615 + strbuf_release(&config_name);
1616 + }
1617 }
1618 return transport;
1619 }
@@ -2658,10 +2671,6 @@ int cmd_fetch(int argc,
2671 config.display_format = DISPLAY_FORMAT_PORCELAIN;
2672 }
2673
2661 - if (negotiate_only && !negotiation_restrict.nr)
2662 - die(_("%s needs one or more %s"), "--negotiate-only",
2663 - "--negotiation-restrict=*");
2664 -
2674 if (deepen_relative) {
2675 if (deepen_relative < 0)
2676 die(_("negative depth in --deepen is not supported"));
@@ -2749,14 +2758,19 @@ int cmd_fetch(int argc,
2758 if (!remote)
2759 die(_("must supply remote when using --negotiate-only"));
2760 gtransport = prepare_transport(remote, 1, &filter_options);
2752 - if (gtransport->smart_options) {
2753 - gtransport->smart_options->acked_commits = &acked_commits;
2754 - } else {
2761 +
2762 + if (!gtransport->smart_options) {
2763 warning(_("protocol does not support --negotiate-only, exiting"));
2764 result = 1;
2765 trace2_region_leave("fetch", "negotiate-only", the_repository);
2766 goto cleanup;
2767 }
2768 + if (!gtransport->smart_options->negotiation_restrict_tips)
2769 + die(_("%s needs one or more %s"), "--negotiate-only",
2770 + "--negotiation-restrict=*");
2771 +
2772 + gtransport->smart_options->acked_commits = &acked_commits;
2773 +
2774 if (server_options.nr)
2775 gtransport->server_options = &server_options;
2776 result = transport_fetch_refs(gtransport, NULL);
remote.c
+5
@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,
152 refspec_init_push(&ret->push);
153 refspec_init_fetch(&ret->fetch);
154 string_list_init_dup(&ret->server_options);
155 + string_list_init_dup(&ret->negotiation_restrict);
156
157 ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1,
158 remote_state->remotes_alloc);
@@ -179,6 +180,7 @@ static void remote_clear(struct remote *remote)
180 FREE_AND_NULL(remote->http_proxy);
181 FREE_AND_NULL(remote->http_proxy_authmethod);
182 string_list_clear(&remote->server_options, 0);
183 + string_list_clear(&remote->negotiation_restrict, 0);
184 }
185
186 static void add_merge(struct branch *branch, const char *name)
@@ -562,6 +564,9 @@ static int handle_config(const char *key, const char *value,
564 } else if (!strcmp(subkey, "serveroption")) {
565 return parse_transport_option(key, value,
566 &remote->server_options);
567 + } else if (!strcmp(subkey, "negotiationrestrict")) {
568 + return parse_transport_option(key, value,
569 + &remote->negotiation_restrict);
570 } else if (!strcmp(subkey, "followremotehead")) {
571 const char *no_warn_branch;
572 if (!strcmp(value, "never"))
remote.h
+1
@@ -117,6 +117,7 @@ struct remote {
117 char *http_proxy_authmethod;
118
119 struct string_list server_options;
120 + struct string_list negotiation_restrict;
121
122 enum follow_remote_head_settings follow_remote_head;
123 const char *no_warn_branch;
t/t5510-fetch.sh
+26
@@ -1485,6 +1485,32 @@ test_expect_success '--negotiation-restrict and --negotiation-tip can be mixed'
1485 check_negotiation_tip
1486 '
1487
1488 +test_expect_success 'remote.<name>.negotiationRestrict used as default' '
1489 + setup_negotiation_tip server server 0 &&
1490 +
1491 + # test the reset of the list on an empty value
1492 + git -C client config --add remote.origin.negotiationRestrict alpha_2 &&
1493 + git -C client config --add remote.origin.negotiationRestrict "" &&
1494 + git -C client config --add remote.origin.negotiationRestrict alpha_1 &&
1495 + git -C client config --add remote.origin.negotiationRestrict beta_1 &&
1496 + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
1497 + origin alpha_s beta_s &&
1498 + check_negotiation_tip
1499 +'
1500 +
1501 +test_expect_success 'CLI --negotiation-restrict overrides remote config' '
1502 + setup_negotiation_tip server server 0 &&
1503 + git -C client config --add remote.origin.negotiationRestrict alpha_1 &&
1504 + git -C client config --add remote.origin.negotiationRestrict beta_1 &&
1505 + ALPHA_1=$(git -C client rev-parse alpha_1) &&
1506 + GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
1507 + --negotiation-restrict=alpha_1 \
1508 + origin alpha_s beta_s &&
1509 + test_grep "fetch> have $ALPHA_1" trace &&
1510 + BETA_1=$(git -C client rev-parse beta_1) &&
1511 + test_grep ! "fetch> have $BETA_1" trace
1512 +'
1513 +
1514 test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
1515 git init df-conflict &&
1516 (