* [PATCH 0/4] fetch: add --must-have and remote.*.mustHave
@ 2026-04-08 14:36 Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 1/4] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
` (5 more replies)
0 siblings, 6 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-08 14:36 UTC (permalink / raw)
To: git; +Cc: gitster, jonathantanmy, chooglen, ps, Derrick Stolee
Fetch negotiation aims to find enough information from haves and wants such
that the server can be reasonably confident that it will send all necessary
objects and not too many "extra" objects that the client already has.
However, this can break down if there are too many references, since Git
truncates the list of haves based on a few factors (a 256 count limit or the
server sending an ACK at the right time).
We already have the --negotiation-tip feature to focus the set of references
that are used in negotiation, but I feel like this is designed backwards.
I'd rather that we have a way to say "this is an important set of refs, but
feel free to add more refs if needed" than "only use these refs for
negotiation".
Here's an example that demonstrates the problem. In an internal monorepo,
developers work off of the 'main' branch so there are thousands of user
branches that each add a few commits different from the 'main' branch.
However, there is also a long-lived 'release' branch. This branch has a
first-parent history that is parallel to 'main' and each of those commits is
a merge whose second parent is a commit from 'main' that had a successful CI
run. There are additional changes in the 'release' branch merge commits that
add some changelog data, so there is a nontrivial set of novel blob content
in that branch and not just a different set of commits.
The problem we had was that our georeplication system was regularly fetching
from the origin and trying to get all data from all reachable branches. When
the 'release' branch updated, the client would run out of haves before
advertising its copy of the 'release' branch, but it would still list the
new 'release' tip as a want. The server would then think that the client had
never fetched that branch before and would send all of the changelog data
from the whole history of the repo. (This led to a lot of downstream
problems; we mitigated by setting a refspec that stopped fetching the
'release' branch, but this is not ideal.)
What I'd like is a mechanism to say "always advertise the client's version
of 'main' and 'release' but also opportunistically include some user
branches".
Based on my understanding, the '--negotiation-tip' option is close but not
quite what I want. I could have the client only advertise 'release' and
'main' and never advertise any user branches. But then we'd download all
content from each user branch every time it updates. Perhaps this would
happen even with opportunistic inclusion of more haves, but I'd like to
explore this area more.
There's also an issue that the '--negotiation-tip' feature doesn't seem to
have a config key that enables it without CLI arguments. This is something
that we could consider independently.
This patch series adds a new '--must-have' argument in the same places that
'--negotiation-tip' exists. This adds a set of references that will always
be included in the negotiation as haves, but then the rest of the
negotiation can proceed as normal. It also adds a new
'remote.<name>.mustHave' config option to enable this behavior without CLI
arguments.
The series is organized into four changes:
1. The first change updates a test that started failing after I added some
tests before it. This fix should be valuable on its own, but I need it
for my tests to pass.
2. The second change adds the --must-have option for fetch and tests it
relative to --negotiation-tip.
3. The third change adds the config option to enable --must-have by
default. Any use in the CLI completely overrides the config option.
4. The fourth change adds capabilities for the config to update negotiation
during git push.
During development, I had briefly considered only using config values, but
that required some strange changes to care about the remote name in the
transport layer. This was most different in the 'git push' integration. When
I discovered the '--negotiation-tip' feature during the process, that gave
me a clear pattern to follow with the addition of a config on top.
I've CC'd some folks who have been working on or near the
'--negotiation-tip' feature for feedback as to how these things fit together
in the bigger picture.
Big picture questions to think about:
* Is this a valuable addition to the fetch negotiation?
* Is the interaction between --must-have and --negotiation-tip correct?
* Is the "must have" name sensical to users? I expect that this only
matters to experts, but I'm open to better names that could be more
self-documenting.
* Should we add a similar config key for --negotiation-tip?
Thanks, -Stolee
Derrick Stolee (4):
t5516: fix test order flakiness
fetch: add --must-have option for negotiation
remote: add mustHave config as default for --must-have
send-pack: pass --must-have for push negotiation
Documentation/config/remote.adoc | 23 ++++++
Documentation/fetch-options.adoc | 22 ++++++
builtin/fetch.c | 14 +++-
fetch-pack.c | 92 +++++++++++++++++++++--
fetch-pack.h | 10 ++-
remote.c | 6 ++
remote.h | 1 +
send-pack.c | 12 ++-
send-pack.h | 1 +
t/t5510-fetch.sh | 121 +++++++++++++++++++++++++++++++
t/t5516-fetch-push.sh | 17 ++++-
transport.c | 5 +-
transport.h | 6 ++
13 files changed, 319 insertions(+), 11 deletions(-)
base-commit: 6e8d538aab8fe4dd07ba9fb87b5c7edcfa5706ad
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2085%2Fderrickstolee%2Fmust-have-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2085/derrickstolee/must-have-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2085
--
gitgitgadget
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH 1/4] t5516: fix test order flakiness
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
@ 2026-04-08 14:36 ` Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 2/4] fetch: add --must-have option for negotiation Derrick Stolee via GitGitGadget
` (4 subsequent siblings)
5 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-08 14:36 UTC (permalink / raw)
To: git; +Cc: gitster, jonathantanmy, chooglen, ps, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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. Use 'sort -k 3' to match
the actual number of columns in the output.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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 &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH 2/4] fetch: add --must-have option for negotiation
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 1/4] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
@ 2026-04-08 14:36 ` Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 3/4] remote: add mustHave config as default for --must-have Derrick Stolee via GitGitGadget
` (3 subsequent siblings)
5 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-08 14:36 UTC (permalink / raw)
To: git; +Cc: gitster, jonathantanmy, chooglen, ps, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
Add a --must-have option to git fetch that specifies ref patterns whose
tips should always be sent as "have" commits during negotiation,
regardless of what the negotiation algorithm selects.
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-tip.
This is useful when certain references are important for negotiation
efficiency but might be skipped by the negotiation algorithm or excluded
by --negotiation-tip. Unlike --negotiation-tip which restricts the have
set, --must-have is additive: the negotiation algorithm still runs and
advertises its own selected commits, but the refs matching --must-have
are sent unconditionally on top of those.
If --negotiation-tip is used, the have set is first restricted by that
option and then increased to include the tips specified by --must-have.
Due to the comparision with --negotiation-tip, a previously untranslated
warning around --negotiation-tip is converted into a translatable string
with a swap for which option that is relevant.
Getting this functionality to work requires moving these options through
the transport API layer.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/fetch-options.adoc | 18 +++++++
builtin/fetch.c | 11 +++-
fetch-pack.c | 92 +++++++++++++++++++++++++++++---
fetch-pack.h | 10 +++-
t/t5510-fetch.sh | 73 +++++++++++++++++++++++++
transport.c | 4 +-
transport.h | 6 +++
7 files changed, 205 insertions(+), 9 deletions(-)
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index 81a9d7f9bb..852e30191e 100644
--- a/Documentation/fetch-options.adoc
+++ b/Documentation/fetch-options.adoc
@@ -69,6 +69,24 @@ See also the `fetch.negotiationAlgorithm` and `push.negotiate`
configuration variables documented in linkgit:git-config[1], and the
`--negotiate-only` option below.
+`--must-have=<revision>`::
+ 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-tip` 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-tip`.
++
+If `--negotiation-tip` is used, the have set is first restricted by that
+option and then increased to include the tips specified by `--must-have`.
+
`--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 4795b2a13c..5d29cc6b1a 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 must_have = STRING_LIST_INIT_NODUP;
struct fetch_config {
enum display_format display_format;
@@ -1599,7 +1600,13 @@ 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-tip");
+ }
+ if (must_have.nr) {
+ if (transport->smart_options)
+ transport->smart_options->must_have = &must_have;
+ else
+ warning(_("ignoring %s because the protocol does not support it"), "--must-have");
}
return transport;
}
@@ -2567,6 +2574,8 @@ 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, "must-have", &must_have, 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/fetch-pack.c b/fetch-pack.c
index 6ecd468ef7..afd4f31225 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,40 @@ 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;
+ oidset_insert(set, ref->oid);
+ return 0;
+}
+
+static void resolve_must_have(const struct string_list *must_have,
+ struct oidset *result)
+{
+ struct string_list_item *item;
+
+ if (!must_have || !must_have->nr)
+ return;
+
+ for_each_string_list_item(item, must_have) {
+ 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 +382,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 must_have_oids = OIDSET_INIT;
if (args->stateless_rpc && multi_ack == 1)
die(_("the option '%s' requires '%s'"), "--stateless-rpc", "multi_ack_detailed");
@@ -474,7 +510,24 @@ 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 --must-have */
+ resolve_must_have(args->must_have, &must_have_oids);
+ if (oidset_size(&must_have_oids)) {
+ struct oidset_iter iter;
+ oidset_iter_init(&must_have_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 --must-have */
+ if (oidset_contains(&must_have_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 +637,7 @@ done:
flushes++;
}
strbuf_release(&req_buf);
+ oidset_clear(&must_have_oids);
if (!got_ready || !no_done)
consume_shallow_list(args, &reader);
@@ -1305,12 +1359,25 @@ 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 *must_have_oids)
{
int haves_added = 0;
const struct object_id *oid;
+ /* Send unconditional haves from --must-have */
+ if (must_have_oids) {
+ struct oidset_iter iter;
+ oidset_iter_init(must_have_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 (must_have_oids && oidset_contains(must_have_oids, oid))
+ continue;
packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid));
if (++haves_added >= *haves_to_send)
break;
@@ -1358,7 +1425,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 *must_have_oids)
{
int haves_added;
int done_sent = 0;
@@ -1413,7 +1481,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,
+ must_have_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 +1726,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 must_have_oids = OIDSET_INIT;
struct packet_reader reader;
int in_vain = 0, negotiation_started = 0;
int negotiation_round = 0;
@@ -1708,6 +1778,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
reader.me = "fetch-pack";
}
+ resolve_must_have(args->must_have, &must_have_oids);
+
while (state != FETCH_DONE) {
switch (state) {
case FETCH_CHECK_LOCAL:
@@ -1747,7 +1819,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,
+ &must_have_oids)) {
trace2_region_leave_printf("negotiation_v2", "round",
the_repository, "%d",
negotiation_round);
@@ -1883,6 +1956,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
negotiator->release(negotiator);
oidset_clear(&common);
+ oidset_clear(&must_have_oids);
return ref;
}
@@ -2181,12 +2255,14 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips,
const struct string_list *server_options,
int stateless_rpc,
int fd[],
- struct oidset *acked_commits)
+ struct oidset *acked_commits,
+ const struct string_list *must_have)
{
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 must_have_oids = OIDSET_INIT;
int haves_to_send = INITIAL_FLUSH;
int in_vain = 0;
int seen_ack = 0;
@@ -2205,6 +2281,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips,
add_to_object_array,
&nt_object_array);
+ resolve_must_have(must_have, &must_have_oids);
+
trace2_region_enter("fetch-pack", "negotiate_using_fetch", the_repository);
while (!last_iteration) {
int haves_added;
@@ -2221,7 +2299,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,
+ &must_have_oids);
in_vain += haves_added;
if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN))
last_iteration = 1;
@@ -2273,6 +2352,7 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips,
clear_common_flag(acked_commits);
object_array_clear(&nt_object_array);
+ oidset_clear(&must_have_oids);
negotiator.release(&negotiator);
strbuf_release(&req_buf);
}
diff --git a/fetch-pack.h b/fetch-pack.h
index 9d3470366f..2e97ca5ea2 100644
--- a/fetch-pack.h
+++ b/fetch-pack.h
@@ -23,6 +23,13 @@ struct fetch_pack_args {
*/
const struct oid_array *negotiation_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 *must_have;
+
unsigned deepen_relative:1;
unsigned quiet:1;
unsigned keep_pack:1;
@@ -93,7 +100,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips,
const struct string_list *server_options,
int stateless_rpc,
int fd[],
- struct oidset *acked_commits);
+ struct oidset *acked_commits,
+ const struct string_list *must_have);
/*
* 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 5dcb4b51a4..c34f3805c1 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1728,6 +1728,79 @@ test_expect_success REFFILES "HEAD is updated even with conflicts" '
)
'
+test_expect_success '--must-have includes configured refs as haves' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
+ # With --negotiation-tip restricting tips, only alpha_1 is
+ # normally sent. --must-have should also include beta_1.
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
+ --negotiation-tip=alpha_1 \
+ --must-have=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 '--must-have 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-tip=alpha_1 \
+ --must-have="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 '--must-have is additive with negotiation' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
+ # Without --negotiation-tip, all local refs are used as tips.
+ # --must-have should add its refs unconditionally on top.
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
+ --must-have=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 '--must-have ignores non-existent refs silently' '
+ setup_negotiation_tip server server 0 &&
+
+ git -C client fetch --quiet \
+ --negotiation-tip=alpha_1 \
+ --must-have=refs/tags/nonexistent \
+ origin alpha_s beta_s 2>err &&
+ test_must_be_empty err
+'
+
+test_expect_success '--must-have avoids duplicates with negotiator' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
+ # Configure a ref that will also be a negotiation tip.
+ # fetch should still complete successfully.
+ ALPHA_1=$(git -C client rev-parse alpha_1) &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
+ --negotiation-tip=alpha_1 \
+ --must-have=refs/tags/alpha_1 \
+ origin alpha_s beta_s &&
+
+ # alpha_1 should appear as a have
+ test_grep "fetch> have $ALPHA_1" trace >matches &&
+ test_line_count = 1 matches
+'
+
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd
diff --git a/transport.c b/transport.c
index 107f4fa5dc..90923a640a 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_tips = data->options.negotiation_tips;
+ args.must_have = data->options.must_have;
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.must_have);
ret = 0;
}
goto cleanup;
diff --git a/transport.h b/transport.h
index 892f19454a..7f8d779e9b 100644
--- a/transport.h
+++ b/transport.h
@@ -48,6 +48,12 @@ struct git_transport_options {
*/
struct oid_array *negotiation_tips;
+ /*
+ * If non-empty, ref patterns whose tips should always be sent
+ * as "have" lines during negotiation.
+ */
+ const struct string_list *must_have;
+
/*
* If allocated, whenever transport_fetch_refs() is called, add known
* common commits to this oidset instead of fetching any packfiles.
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH 3/4] remote: add mustHave config as default for --must-have
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 1/4] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 2/4] fetch: add --must-have option for negotiation Derrick Stolee via GitGitGadget
@ 2026-04-08 14:36 ` Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 4/4] send-pack: pass --must-have for push negotiation Derrick Stolee via GitGitGadget
` (2 subsequent siblings)
5 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-08 14:36 UTC (permalink / raw)
To: git; +Cc: gitster, jonathantanmy, chooglen, ps, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
Add a new multi-valued config option remote.<name>.mustHave that
specifies ref patterns whose tips should always be sent as "have"
commits during fetch negotiation with that remote.
Parse the option in handle_config() following the same pattern as
remote.<name>.serverOption. Store the values in a string_list on struct
remote so they are available per-remote.
In builtin/fetch.c, when no --must-have options are given on the command
line, use the remote.<name>.mustHave config values as the default. If
the user explicitly provides --must-have on the CLI, the config is not
used, giving CLI precedence.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/config/remote.adoc | 23 +++++++++++++++
Documentation/fetch-options.adoc | 4 +++
builtin/fetch.c | 3 ++
remote.c | 6 ++++
remote.h | 1 +
t/t5510-fetch.sh | 48 ++++++++++++++++++++++++++++++++
6 files changed, 85 insertions(+)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index 91e46f66f5..9df8be27eb 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc
@@ -107,6 +107,29 @@ 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.<name>.mustHave::
+ 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-tip`.
++
+These config values are used as defaults for the `--must-have` command-line
+option. If `--must-have` 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.<name>.mustHave` are sent unconditionally on
+top of those heuristically selected commits. This option is also used
+during push negotiation when `push.negotiate` is enabled.
+
remote.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index 852e30191e..fa3969d68d 100644
--- a/Documentation/fetch-options.adoc
+++ b/Documentation/fetch-options.adoc
@@ -86,6 +86,10 @@ is the same as for `--negotiation-tip`.
+
If `--negotiation-tip` is used, the have set is first restricted by that
option and then increased to include the tips specified by `--must-have`.
++
+If this option is not specified on the command line, then any
+`remote.<name>.mustHave` 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 5d29cc6b1a..fa491c106f 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1607,6 +1607,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
transport->smart_options->must_have = &must_have;
else
warning(_("ignoring %s because the protocol does not support it"), "--must-have");
+ } else if (remote->must_have.nr) {
+ if (transport->smart_options)
+ transport->smart_options->must_have = &remote->must_have;
}
return transport;
}
diff --git a/remote.c b/remote.c
index 7ca2a6501b..e07ec08fb3 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->must_have);
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->must_have, 0);
}
static void add_merge(struct branch *branch, const char *name)
@@ -562,6 +564,10 @@ 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, "musthave")) {
+ if (!value)
+ return config_error_nonbool(key);
+ string_list_append(&remote->must_have, value);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
if (!strcmp(value, "never"))
diff --git a/remote.h b/remote.h
index fc052945ee..e125313f45 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 must_have;
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 c34f3805c1..09e7b613a5 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1801,6 +1801,54 @@ test_expect_success '--must-have avoids duplicates with negotiator' '
test_line_count = 1 matches
'
+test_expect_success 'remote.<name>.mustHave used as default for --must-have' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
+ # No --must-have on CLI; config should be used as default.
+ git -C client config --add remote.origin.mustHave refs/tags/beta_1 &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
+ --negotiation-tip=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.<name>.mustHave works with glob patterns' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
+ git -C client config --add remote.origin.mustHave "refs/tags/beta_*" &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
+ --negotiation-tip=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 --must-have overrides remote.<name>.mustHave' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
+ # Config says beta_2, CLI says beta_1; only CLI should be used.
+ git -C client config --add remote.origin.mustHave refs/tags/beta_2 &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
+ --negotiation-tip=alpha_1 \
+ --must-have=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_DIRECTORY"/lib-httpd.sh
start_httpd
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH 4/4] send-pack: pass --must-have for push negotiation
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
` (2 preceding siblings ...)
2026-04-08 14:36 ` [PATCH 3/4] remote: add mustHave config as default for --must-have Derrick Stolee via GitGitGadget
@ 2026-04-08 14:36 ` Derrick Stolee via GitGitGadget
2026-04-08 18:59 ` [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Junio C Hamano
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
5 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-08 14:36 UTC (permalink / raw)
To: git; +Cc: gitster, jonathantanmy, chooglen, ps, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
When push.negotiate is enabled, send-pack spawns a 'git fetch
--negotiate-only' subprocess to discover common commits. Previously
this subprocess had no way to include must-have refs in the
negotiation.
Add a must_have field to send_pack_args, set it from the transport
layer where the remote struct is available, and pass explicit
--must-have arguments to the negotiation subprocess. This approach
directly passes the resolved config values rather than relying on the
subprocess to read remote config, which is more robust when the URL
alone is used as the remote identifier.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
send-pack.c | 12 +++++++++++-
send-pack.h | 1 +
t/t5516-fetch-push.sh | 15 +++++++++++++++
transport.c | 1 +
4 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/send-pack.c b/send-pack.c
index 67d6987b1c..baa52680bb 100644
--- a/send-pack.c
+++ b/send-pack.c
@@ -433,6 +433,7 @@ 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 *must_have,
const struct ref *remote_refs,
struct oid_array *commons)
{
@@ -452,6 +453,14 @@ static void get_commons_through_negotiation(struct repository *r,
nr_negotiation_tip++;
}
}
+
+ if (must_have) {
+ struct string_list_item *item;
+ for_each_string_list_item(item, must_have)
+ strvec_pushf(&child.args, "--must-have=%s",
+ item->string);
+ }
+
strvec_push(&child.args, url);
if (!nr_negotiation_tip) {
@@ -528,7 +537,8 @@ 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->must_have,
+ remote_refs, &commons);
trace2_region_leave("send_pack", "push_negotiate", r);
}
diff --git a/send-pack.h b/send-pack.h
index c5ded2d200..194a1898e5 100644
--- a/send-pack.h
+++ b/send-pack.h
@@ -18,6 +18,7 @@ struct repository;
struct send_pack_args {
const char *url;
+ const struct string_list *must_have;
unsigned verbose:1,
quiet:1,
porcelain:1,
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index ac8447f21e..9272609eac 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -254,6 +254,21 @@ test_expect_success 'push with negotiation does not attempt to fetch submodules'
! grep "Fetching submodule" err
'
+test_expect_success 'push with negotiation and remote.<name>.mustHave' '
+ test_when_finished rm -rf musthave &&
+ mk_empty musthave &&
+ git push musthave $the_first_commit:refs/remotes/origin/first_commit &&
+ test_commit -C musthave unrelated_commit &&
+ git -C musthave 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.musthave.mustHave=refs/heads/main \
+ push musthave 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 90923a640a..e65f896ff3 100644
--- a/transport.c
+++ b/transport.c
@@ -921,6 +921,7 @@ 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.must_have = &transport->remote->must_have;
if (flags & TRANSPORT_PUSH_CERT_ALWAYS)
args.push_cert = SEND_PACK_PUSH_CERT_ALWAYS;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* Re: [PATCH 0/4] fetch: add --must-have and remote.*.mustHave
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
` (3 preceding siblings ...)
2026-04-08 14:36 ` [PATCH 4/4] send-pack: pass --must-have for push negotiation Derrick Stolee via GitGitGadget
@ 2026-04-08 18:59 ` Junio C Hamano
2026-04-09 12:53 ` Derrick Stolee
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
5 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2026-04-08 18:59 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget
Cc: git, jonathantanmy, chooglen, ps, Derrick Stolee
"Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Based on my understanding, the '--negotiation-tip' option is close but not
> quite what I want. I could have the client only advertise 'release' and
> 'main' and never advertise any user branches. But then we'd download all
> content from each user branch every time it updates. Perhaps this would
> happen even with opportunistic inclusion of more haves, but I'd like to
> explore this area more.
>
> There's also an issue that the '--negotiation-tip' feature doesn't seem to
> have a config key that enables it without CLI arguments. This is something
> that we could consider independently.
> ...
> Big picture questions to think about:
>
> * Is this a valuable addition to the fetch negotiation?
> * Is the interaction between --must-have and --negotiation-tip correct?
> * Is the "must have" name sensical to users? I expect that this only
> matters to experts, but I'm open to better names that could be more
> self-documenting.
> * Should we add a similar config key for --negotiation-tip?
Just like you, I hate the name "must have", but stepping back a bit,
would it work if we add a single boolean option that says "use the
negotiation tips as the primary source of 'have's you'd send, but
unlike the way how the original negotiation-tip feature worked
without this bit enabled, which did not send anything other than the
ones reachable by negotiation tips, do advertise opportunistically
other tips", essentially turning the existing negotiation-tips
feature into your must-have feature? You could even call the option
"--negotiate-better(=(yes|no))" or something, perhaps?
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH 0/4] fetch: add --must-have and remote.*.mustHave
2026-04-08 18:59 ` [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Junio C Hamano
@ 2026-04-09 12:53 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-04-09 12:53 UTC (permalink / raw)
To: Junio C Hamano, Derrick Stolee via GitGitGadget
Cc: git, jonathantanmy, chooglen, ps
On 4/8/2026 2:59 PM, Junio C Hamano wrote:
> "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> Based on my understanding, the '--negotiation-tip' option is close but not
>> quite what I want. I could have the client only advertise 'release' and
>> 'main' and never advertise any user branches. But then we'd download all
>> content from each user branch every time it updates. Perhaps this would
>> happen even with opportunistic inclusion of more haves, but I'd like to
>> explore this area more.
>>
>> There's also an issue that the '--negotiation-tip' feature doesn't seem to
>> have a config key that enables it without CLI arguments. This is something
>> that we could consider independently.
>> ...
>> Big picture questions to think about:
>>
>> * Is this a valuable addition to the fetch negotiation?
>> * Is the interaction between --must-have and --negotiation-tip correct?
>> * Is the "must have" name sensical to users? I expect that this only
>> matters to experts, but I'm open to better names that could be more
>> self-documenting.
>> * Should we add a similar config key for --negotiation-tip?
>
> Just like you, I hate the name "must have", but stepping back a bit,
> would it work if we add a single boolean option that says "use the
> negotiation tips as the primary source of 'have's you'd send, but
> unlike the way how the original negotiation-tip feature worked
> without this bit enabled, which did not send anything other than the
> ones reachable by negotiation tips, do advertise opportunistically
> other tips", essentially turning the existing negotiation-tips
> feature into your must-have feature? You could even call the option
> "--negotiate-better(=(yes|no))" or something, perhaps?
I like this line of thought. You essentially want to use the existing
scaffolding of the --negotiate-tip option but change it from being a
_maximum set_ to being a _minimum set_.
## Considering --negotiation-tip-mode=<mode>
With that in mind, we could have an option like --negotiation-tip-mode
that takes one of a few options. Here are some word choices that I
immediately thought about:
* maximum|minimum: Are these sets a maximum set to choose from or a
minimum set to include?
* restrict|include: Are we restricting the haves to this set, or are
we including these tips by default?
* v1|v2: Use numerical versions to indicate the mode without commentary
so it could be extended in the future to v3 or more.
None of these jump out as a clear winner in my head. I'm interested in
more exploration of this space before rerolling.
## To mix modes, or not to mix modes?
One downside of this approach is that it disables the ability to use
both modes, at least in its most obvious implementation. What if someone
wants to force a minimum set of wants but also wants to focus the set
of additional wants to a specific ref space?
Theoretically, we could implement the option to toggle with multiple
options, using
--negotiation-tip-mode=minimum --negotiation-tip=refs/remotes/origin/main \
--negotiation-tip-mode=maximum --negotiation-tip=refs/remotes/origin/*
and as we process the --negotiation-tip options we'd put the input data
into different lists. Would this complexity be worth it compared to making
a new set of options?
This also becomes more complicated how to describe the interaction of
these options and any config options that enable them by default. When
exactly does the config get ignored in favor of CLI options?
## Considering --negotiation-(required|restricted)
We could alternatively create two new types of options that are clearly
related:
* --negotiation-restricted works exactly like --negotiation-tips and
would be a synonym (with the old one being "deprecated" in favor of
the newer one).
* --negotiation-required works like the --must-have in this series.
---
Thanks for considering these options with me. There is a lot of room
for creativity here. This series isn't even my first attempt at this
functionality because there are so many possible ways to accomplish
this goal.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 0/7] fetch: rework negotiation tip options
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
` (4 preceding siblings ...)
2026-04-08 18:59 ` [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Junio C Hamano
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
` (7 more replies)
5 siblings, 8 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee
Fetch negotiation aims to find enough information from haves and wants such
that the server can be reasonably confident that it will send all necessary
objects and not too many "extra" objects that the client already has.
However, this can break down if there are too many references, since Git
truncates the list of haves based on a few factors (a 256 count limit or the
server sending an ACK at the right time).
We already have the --negotiation-tip feature to focus the set of references
that are used in negotiation, but I feel like this is designed backwards.
I'd rather that we have a way to say "this is an important set of refs, but
feel free to add more refs if needed" than "only use these refs for
negotiation".
Here's an example that demonstrates the problem. In an internal monorepo,
developers work off of the 'main' branch so there are thousands of user
branches that each add a few commits different from the 'main' branch.
However, there is also a long-lived 'release' branch. This branch has a
first-parent history that is parallel to 'main' and each of those commits is
a merge whose second parent is a commit from 'main' that had a successful CI
run. There are additional changes in the 'release' branch merge commits that
add some changelog data, so there is a nontrivial set of novel blob content
in that branch and not just a different set of commits.
The problem we had was that our georeplication system was regularly fetching
from the origin and trying to get all data from all reachable branches. When
the 'release' branch updated, the client would run out of haves before
advertising its copy of the 'release' branch, but it would still list the
new 'release' tip as a want. The server would then think that the client had
never fetched that branch before and would send all of the changelog data
from the whole history of the repo. (This led to a lot of downstream
problems; we mitigated by setting a refspec that stopped fetching the
'release' branch, but this is not ideal.)
What I'd like is a mechanism to say "always advertise the client's version
of 'main' and 'release' but also opportunistically include some user
branches".
Based on my understanding, the '--negotiation-tip' option is close but not
quite what I want. I could have the client only advertise 'release' and
'main' and never advertise any user branches. But then we'd download all
content from each user branch every time it updates. Perhaps this would
happen even with opportunistic inclusion of more haves, but I'd like to
explore this area more.
There's also an issue that the '--negotiation-tip' feature doesn't seem to
have a config key that enables it without CLI arguments. This is something
that we could consider independently.
This patch series adds a new '--negotiation-require' option that does what I
want: it makes sure that these references are used for 'have's during
negotiation. In order to help clarify the difference between this and
'--negotiation-tip', I first create a synonym called
'--negotiation-restrict'.
Both of these options get 'remote.*.negotiation(Require|Restrict)' config
options that enable their behavior by default.
During development, I had briefly considered only using config values, but
that required some strange changes to care about the remote name in the
transport layer. This was most different in the 'git push' integration. When
I discovered the '--negotiation-tip' feature during the process, that gave
me a clear pattern to follow with the addition of a config on top.
Updates in v2
=============
This version is a near-complete rewrite based on feedback around the names
of the previous option and config. The --negotiation-restrict option is new
and the ability to set it via config is also new.
I did try to be more careful around translatable error messages, too.
Thanks, -Stolee
Derrick Stolee (7):
t5516: fix test order flakiness
fetch: add --negotiation-restrict option
transport: rename negotiation_tips
remote: add remote.*.negotiationRestrict config
fetch: add --negotiation-require option for negotiation
remote: add negotiationRequire config as default for
--negotiation-require
send-pack: pass negotiation config in push
Documentation/config/remote.adoc | 40 ++++++++
Documentation/fetch-options.adoc | 27 ++++++
builtin/fetch.c | 59 ++++++++++--
builtin/pull.c | 6 ++
fetch-pack.c | 114 +++++++++++++++++++---
fetch-pack.h | 14 ++-
remote.c | 12 +++
remote.h | 2 +
send-pack.c | 34 +++++--
send-pack.h | 2 +
t/t5510-fetch.sh | 159 +++++++++++++++++++++++++++++++
t/t5516-fetch-push.sh | 32 ++++++-
t/t5702-protocol-v2.sh | 4 +-
transport-helper.c | 2 +-
transport.c | 16 ++--
transport.h | 10 +-
16 files changed, 489 insertions(+), 44 deletions(-)
base-commit: 6e8d538aab8fe4dd07ba9fb87b5c7edcfa5706ad
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2085%2Fderrickstolee%2Fmust-have-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2085/derrickstolee/must-have-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2085
Range-diff vs v1:
1: 540a5682b4 = 1: 466c56abe0 t5516: fix test order flakiness
-: ---------- > 2: 9a25b0fade fetch: add --negotiation-restrict option
-: ---------- > 3: 0f89665aee transport: rename negotiation_tips
-: ---------- > 4: a731f4fc87 remote: add remote.*.negotiationRestrict config
2: 8b39eb9e6c ! 5: 49c80cef2e fetch: add --must-have option for negotiation
@@ Metadata
Author: Derrick Stolee <stolee@gmail.com>
## Commit message ##
- fetch: add --must-have option for negotiation
+ fetch: add --negotiation-require option for negotiation
- Add a --must-have option to git fetch that specifies ref patterns whose
- tips should always be sent as "have" commits during negotiation,
- regardless of what the negotiation algorithm selects.
+ Add a new --negotiation-require 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.
- 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-tip.
+ 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 is useful when certain references are important for negotiation
- efficiency but might be skipped by the negotiation algorithm or excluded
- by --negotiation-tip. Unlike --negotiation-tip which restricts the have
- set, --must-have is additive: the negotiation algorithm still runs and
- advertises its own selected commits, but the refs matching --must-have
- are sent unconditionally on top of those.
+ 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 brances. In this way, the
+ 'require' 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.
- If --negotiation-tip is used, the have set is first restricted by that
- option and then increased to include the tips specified by --must-have.
+ The argument may be an exact ref name or a glob pattern. Non-existent
+ refs are silently ignored.
- Due to the comparision with --negotiation-tip, a previously untranslated
- warning around --negotiation-tip is converted into a translatable string
- with a swap for which option that is relevant.
-
- Getting this functionality to work requires moving these options through
- the transport API layer.
+ Also add --negotiation-require to 'git pull' passthrough options.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
@@ Documentation/fetch-options.adoc: See also the `fetch.negotiationAlgorithm` and
configuration variables documented in linkgit:git-config[1], and the
`--negotiate-only` option below.
-+`--must-have=<revision>`::
++`--negotiation-require=<revision>`::
+ 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-tip` restricts the set of tips or when the
-+ negotiation algorithm would otherwise skip them.
++ 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-tip`.
++is the same as for `--negotiation-restrict`.
++
-+If `--negotiation-tip` is used, the have set is first restricted by that
-+option and then increased to include the tips specified by `--must-have`.
++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`.
+
`--negotiate-only`::
Do not fetch anything from the server, and instead print the
@@ builtin/fetch.c: 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 must_have = STRING_LIST_INIT_NODUP;
++static struct string_list negotiation_require = STRING_LIST_INIT_NODUP;
struct fetch_config {
enum display_format display_format;
@@ builtin/fetch.c: 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-tip");
-+ }
-+ if (must_have.nr) {
+ strbuf_release(&config_name);
+ }
+ }
++ if (negotiation_require.nr) {
+ if (transport->smart_options)
-+ transport->smart_options->must_have = &must_have;
++ transport->smart_options->negotiation_require = &negotiation_require;
+ else
-+ warning(_("ignoring %s because the protocol does not support it"), "--must-have");
- }
++ warning(_("ignoring %s because the protocol does not support it"),
++ "--negotiation-require");
++ }
return transport;
}
+
@@ builtin/fetch.c: 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, "must-have", &must_have, N_("revision"),
+ 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),
+ ## builtin/pull.c ##
+@@ builtin/pull.c: int cmd_pull(int argc,
+ 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,
+
## fetch-pack.c ##
@@
#include "oidset.h"
@@ fetch-pack.c: 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;
-+ oidset_insert(set, ref->oid);
++ if (odb_has_object(the_repository->objects, ref->oid, 0))
++ oidset_insert(set, ref->oid);
+ return 0;
+}
+
-+static void resolve_must_have(const struct string_list *must_have,
-+ struct oidset *result)
++static void resolve_negotiation_require(const struct string_list *negotiation_require,
++ struct oidset *result)
+{
+ struct string_list_item *item;
+
-+ if (!must_have || !must_have->nr)
++ if (!negotiation_require || !negotiation_require->nr)
+ return;
+
-+ for_each_string_list_item(item, must_have) {
++ 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))
@@ fetch-pack.c: 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 must_have_oids = OIDSET_INIT;
++ 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");
@@ fetch-pack.c: static int find_common(struct fetch_negotiator *negotiator,
flushes = 0;
retval = -1;
+
-+ /* Send unconditional haves from --must-have */
-+ resolve_must_have(args->must_have, &must_have_oids);
-+ if (oidset_size(&must_have_oids)) {
++ /* 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(&must_have_oids, &iter);
++ oidset_iter_init(&negotiation_require_oids, &iter);
+
+ while ((oid = oidset_iter_next(&iter))) {
+ packet_buf_write(&req_buf, "have %s\n",
@@ fetch-pack.c: static int find_common(struct fetch_negotiator *negotiator,
+ }
+
while ((oid = negotiator->next(negotiator))) {
-+ /* avoid duplicate oids from --must-have */
-+ if (oidset_contains(&must_have_oids, oid))
++ /* 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));
@@ fetch-pack.c: done:
flushes++;
}
strbuf_release(&req_buf);
-+ oidset_clear(&must_have_oids);
++ oidset_clear(&negotiation_require_oids);
if (!got_ready || !no_done)
consume_shallow_list(args, &reader);
@@ fetch-pack.c: static void add_common(struct strbuf *req_buf, struct oidset *comm
struct strbuf *req_buf,
- int *haves_to_send)
+ int *haves_to_send,
-+ struct oidset *must_have_oids)
++ struct oidset *negotiation_require_oids)
{
int haves_added = 0;
const struct object_id *oid;
-+ /* Send unconditional haves from --must-have */
-+ if (must_have_oids) {
++ /* Send unconditional haves from --negotiation-require */
++ if (negotiation_require_oids) {
+ struct oidset_iter iter;
-+ oidset_iter_init(must_have_oids, &iter);
++ oidset_iter_init(negotiation_require_oids, &iter);
+
+ while ((oid = oidset_iter_next(&iter)))
+ packet_buf_write(req_buf, "have %s\n",
@@ fetch-pack.c: static void add_common(struct strbuf *req_buf, struct oidset *comm
+ }
+
while ((oid = negotiator->next(negotiator))) {
-+ if (must_have_oids && oidset_contains(must_have_oids, oid))
++ 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)
@@ fetch-pack.c: static int send_fetch_request(struct fetch_negotiator *negotiator,
int *haves_to_send, int *in_vain,
- int sideband_all, int seen_ack)
+ int sideband_all, int seen_ack,
-+ struct oidset *must_have_oids)
++ struct oidset *negotiation_require_oids)
{
int haves_added;
int done_sent = 0;
@@ fetch-pack.c: static int send_fetch_request(struct fetch_negotiator *negotiator,
- haves_added = add_haves(negotiator, &req_buf, haves_to_send);
+ haves_added = add_haves(negotiator, &req_buf, haves_to_send,
-+ must_have_oids);
++ 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);
@@ fetch-pack.c: 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 must_have_oids = OIDSET_INIT;
++ struct oidset negotiation_require_oids = OIDSET_INIT;
struct packet_reader reader;
int in_vain = 0, negotiation_started = 0;
int negotiation_round = 0;
@@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
- reader.me = "fetch-pack";
- }
+ state = FETCH_SEND_REQUEST;
-+ resolve_must_have(args->must_have, &must_have_oids);
-+
- while (state != FETCH_DONE) {
- switch (state) {
- case FETCH_CHECK_LOCAL:
+ 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;
@@ fetch-pack.c: 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,
-+ &must_have_oids)) {
++ &negotiation_require_oids)) {
trace2_region_leave_printf("negotiation_v2", "round",
the_repository, "%d",
negotiation_round);
@@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
negotiator->release(negotiator);
oidset_clear(&common);
-+ oidset_clear(&must_have_oids);
++ oidset_clear(&negotiation_require_oids);
return ref;
}
-@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_tips,
+@@ fetch-pack.c: 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 *must_have)
++ 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 must_have_oids = OIDSET_INIT;
++ struct oidset negotiation_require_oids = OIDSET_INIT;
int haves_to_send = INITIAL_FLUSH;
int in_vain = 0;
int seen_ack = 0;
-@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_tips,
- add_to_object_array,
- &nt_object_array);
+@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips,
+ fetch_negotiator_init(the_repository, &negotiator);
+ mark_tips(&negotiator, negotiation_restrict_tips);
-+ resolve_must_have(must_have, &must_have_oids);
++ resolve_negotiation_require(negotiation_require,
++ &negotiation_require_oids);
+
- trace2_region_enter("fetch-pack", "negotiate_using_fetch", the_repository);
- while (!last_iteration) {
- int haves_added;
-@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_tips,
+ packet_reader_init(&reader, fd[0], NULL, 0,
+ PACKET_READ_CHOMP_NEWLINE |
+ PACKET_READ_DIE_ON_ERR_PACKET);
+@@ fetch-pack.c: 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,
-+ &must_have_oids);
++ &negotiation_require_oids);
in_vain += haves_added;
if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN))
last_iteration = 1;
-@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_tips,
+@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips,
clear_common_flag(acked_commits);
object_array_clear(&nt_object_array);
-+ oidset_clear(&must_have_oids);
++ oidset_clear(&negotiation_require_oids);
negotiator.release(&negotiator);
strbuf_release(&req_buf);
}
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_tip
## fetch-pack.h ##
@@ fetch-pack.h: struct fetch_pack_args {
*/
- 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 *must_have;
++ const struct string_list *negotiation_require;
+
unsigned deepen_relative:1;
unsigned quiet:1;
unsigned keep_pack:1;
-@@ fetch-pack.h: void negotiate_using_fetch(const struct oid_array *negotiation_tips,
+@@ fetch-pack.h: 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 *must_have);
++ const struct string_list *negotiation_require);
/*
* Print an appropriate error message for each sought ref that wasn't
## t/t5510-fetch.sh ##
-@@ t/t5510-fetch.sh: test_expect_success REFFILES "HEAD is updated even with conflicts" '
- )
+@@ t/t5510-fetch.sh: test_expect_success 'CLI --negotiation-restrict overrides remote config' '
+ test_grep ! "fetch> have $BETA_1" trace
'
-+test_expect_success '--must-have includes configured refs as haves' '
++test_expect_success '--negotiation-require includes configured refs as haves' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ # With --negotiation-tip restricting tips, only alpha_1 is
-+ # normally sent. --must-have should also include beta_1.
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
-+ --negotiation-tip=alpha_1 \
-+ --must-have=refs/tags/beta_1 \
++ --negotiation-restrict=alpha_1 \
++ --negotiation-require=refs/tags/beta_1 \
+ origin alpha_s beta_s &&
+
+ ALPHA_1=$(git -C client rev-parse alpha_1) &&
@@ t/t5510-fetch.sh: test_expect_success REFFILES "HEAD is updated even with confli
+ test_grep "fetch> have $BETA_1" trace
+'
+
-+test_expect_success '--must-have works with glob patterns' '
++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-tip=alpha_1 \
-+ --must-have="refs/tags/beta_*" \
++ --negotiation-restrict=alpha_1 \
++ --negotiation-require="refs/tags/beta_*" \
+ origin alpha_s beta_s &&
+
+ BETA_1=$(git -C client rev-parse beta_1) &&
@@ t/t5510-fetch.sh: test_expect_success REFFILES "HEAD is updated even with confli
+ test_grep "fetch> have $BETA_2" trace
+'
+
-+test_expect_success '--must-have is additive with negotiation' '
++test_expect_success '--negotiation-require is additive with negotiation' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ # Without --negotiation-tip, all local refs are used as tips.
-+ # --must-have should add its refs unconditionally on top.
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
-+ --must-have=refs/tags/beta_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
+'
+
-+test_expect_success '--must-have ignores non-existent refs silently' '
++test_expect_success '--negotiation-require ignores non-existent refs silently' '
+ setup_negotiation_tip server server 0 &&
+
+ git -C client fetch --quiet \
-+ --negotiation-tip=alpha_1 \
-+ --must-have=refs/tags/nonexistent \
++ --negotiation-restrict=alpha_1 \
++ --negotiation-require=refs/tags/nonexistent \
+ origin alpha_s beta_s 2>err &&
+ test_must_be_empty err
+'
+
-+test_expect_success '--must-have avoids duplicates with negotiator' '
++test_expect_success '--negotiation-require avoids duplicates with negotiator' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ # Configure a ref that will also be a negotiation tip.
-+ # fetch should still complete successfully.
+ ALPHA_1=$(git -C client rev-parse alpha_1) &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
-+ --negotiation-tip=alpha_1 \
-+ --must-have=refs/tags/alpha_1 \
++ --negotiation-restrict=alpha_1 \
++ --negotiation-require=refs/tags/alpha_1 \
+ origin alpha_s beta_s &&
+
-+ # alpha_1 should appear as a have
+ test_grep "fetch> have $ALPHA_1" trace >matches &&
+ test_line_count = 1 matches
+'
+
- . "$TEST_DIRECTORY"/lib-httpd.sh
- start_httpd
-
+ test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
+ git init df-conflict &&
+ (
## transport.c ##
@@ transport.c: static int fetch_refs_via_pack(struct transport *transport,
args.stateless_rpc = transport->stateless_rpc;
args.server_options = transport->server_options;
- args.negotiation_tips = data->options.negotiation_tips;
-+ args.must_have = data->options.must_have;
+ 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) {
@@ transport.c: static int fetch_refs_via_pack(struct transport *transport,
data->fd,
- data->options.acked_commits);
+ data->options.acked_commits,
-+ data->options.must_have);
++ data->options.negotiation_require);
ret = 0;
}
goto cleanup;
@@ transport.c: static int fetch_refs_via_pack(struct transport *transport,
## transport.h ##
@@ transport.h: struct git_transport_options {
*/
- 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 *must_have;
++ const struct string_list *negotiation_require;
+
/*
* If allocated, whenever transport_fetch_refs() is called, add known
3: fbc98b0cbb ! 6: 081f904c07 remote: add mustHave config as default for --must-have
@@ Metadata
Author: Derrick Stolee <stolee@gmail.com>
## Commit message ##
- remote: add mustHave config as default for --must-have
+ remote: add negotiationRequire config as default for --negotiation-require
- Add a new multi-valued config option remote.<name>.mustHave that
- specifies ref patterns whose tips should always be sent as "have"
- commits during fetch negotiation with that remote.
+ Add a new 'remote.<name>.negotiationRequire' multi-valued config option
+ that provides default values for --negotiation-require when no
+ --negotiation-require arguments are specified over the command line.
+ This is a mirror of how 'remote.<name>.negotiationRestrict' specifies
+ defaults for the --negotiation-restrict arguments.
- Parse the option in handle_config() following the same pattern as
- remote.<name>.serverOption. Store the values in a string_list on struct
- remote so they are available per-remote.
+ 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_require()
+ codepath as the CLI options.
- In builtin/fetch.c, when no --must-have options are given on the command
- line, use the remote.<name>.mustHave config values as the default. If
- the user explicitly provides --must-have on the CLI, the config is not
- used, giving CLI precedence.
+ 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/remote.adoc ##
-@@ Documentation/config/remote.adoc: 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`).
+@@ Documentation/config/remote.adoc: 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.<name>.mustHave::
++remote.<name>.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.
@@ Documentation/config/remote.adoc: priority configuration file (e.g. `.git/config
++
+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-tip`.
++as for `--negotiation-restrict`.
++
-+These config values are used as defaults for the `--must-have` command-line
-+option. If `--must-have` is specified on the command line, then the config
-+values are not used.
++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.<name>.mustHave` are sent unconditionally on
-+top of those heuristically selected commits. This option is also used
-+during push negotiation when `push.negotiate` is enabled.
++but the refs matching `remote.<name>.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.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.
## Documentation/fetch-options.adoc ##
-@@ Documentation/fetch-options.adoc: is the same as for `--negotiation-tip`.
- +
- If `--negotiation-tip` is used, the have set is first restricted by that
- option and then increased to include the tips specified by `--must-have`.
+@@ Documentation/fetch-options.adoc: 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.<name>.mustHave` config values for the current remote are used
-+instead.
++`remote.<name>.negotiationRequire` config values for the current remote
++are used instead.
`--negotiate-only`::
Do not fetch anything from the server, and instead print the
## builtin/fetch.c ##
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remote, int deepen,
- transport->smart_options->must_have = &must_have;
else
- warning(_("ignoring %s because the protocol does not support it"), "--must-have");
-+ } else if (remote->must_have.nr) {
-+ if (transport->smart_options)
-+ transport->smart_options->must_have = &remote->must_have;
+ 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;
}
## remote.c ##
@@ remote.c: 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->must_have);
+ 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);
@@ remote.c: 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->must_have, 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)
@@ remote.c: 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, "musthave")) {
+ 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->must_have, value);
++ string_list_append(&remote->negotiation_require, value);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
if (!strcmp(value, "never"))
## remote.h ##
@@ remote.h: struct remote {
- char *http_proxy_authmethod;
struct string_list server_options;
-+ struct string_list must_have;
+ struct string_list negotiation_restrict;
++ struct string_list negotiation_require;
enum follow_remote_head_settings follow_remote_head;
const char *no_warn_branch;
## t/t5510-fetch.sh ##
-@@ t/t5510-fetch.sh: test_expect_success '--must-have avoids duplicates with negotiator' '
+@@ t/t5510-fetch.sh: test_expect_success '--negotiation-require avoids duplicates with negotiator' '
test_line_count = 1 matches
'
-+test_expect_success 'remote.<name>.mustHave used as default for --must-have' '
++test_expect_success 'remote.<name>.negotiationRequire used as default for --negotiation-require' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ # No --must-have on CLI; config should be used as default.
-+ git -C client config --add remote.origin.mustHave refs/tags/beta_1 &&
++ git -C client config --add remote.origin.negotiationRequire refs/tags/beta_1 &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
-+ --negotiation-tip=alpha_1 \
++ --negotiation-restrict=alpha_1 \
+ origin alpha_s beta_s &&
+
+ ALPHA_1=$(git -C client rev-parse alpha_1) &&
@@ t/t5510-fetch.sh: test_expect_success '--must-have avoids duplicates with negoti
+ test_grep "fetch> have $BETA_1" trace
+'
+
-+test_expect_success 'remote.<name>.mustHave works with glob patterns' '
++test_expect_success 'remote.<name>.negotiationRequire works with glob patterns' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ git -C client config --add remote.origin.mustHave "refs/tags/beta_*" &&
++ git -C client config --add remote.origin.negotiationRequire "refs/tags/beta_*" &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
-+ --negotiation-tip=alpha_1 \
++ --negotiation-restrict=alpha_1 \
+ origin alpha_s beta_s &&
+
+ BETA_1=$(git -C client rev-parse beta_1) &&
@@ t/t5510-fetch.sh: test_expect_success '--must-have avoids duplicates with negoti
+ test_grep "fetch> have $BETA_2" trace
+'
+
-+test_expect_success 'CLI --must-have overrides remote.<name>.mustHave' '
++test_expect_success 'CLI --negotiation-require overrides remote.<name>.negotiationRequire' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ # Config says beta_2, CLI says beta_1; only CLI should be used.
-+ git -C client config --add remote.origin.mustHave refs/tags/beta_2 &&
++ git -C client config --add remote.origin.negotiationRequire refs/tags/beta_2 &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
-+ --negotiation-tip=alpha_1 \
-+ --must-have=refs/tags/beta_1 \
++ --negotiation-restrict=alpha_1 \
++ --negotiation-require=refs/tags/beta_1 \
+ origin alpha_s beta_s &&
+
+ BETA_1=$(git -C client rev-parse beta_1) &&
@@ t/t5510-fetch.sh: test_expect_success '--must-have avoids duplicates with negoti
+ test_grep ! "fetch> have $BETA_2" trace
+'
+
- . "$TEST_DIRECTORY"/lib-httpd.sh
- start_httpd
-
+ test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
+ git init df-conflict &&
+ (
4: 6c227f18ab ! 7: 7cccf59beb send-pack: pass --must-have for push negotiation
@@ Metadata
Author: Derrick Stolee <stolee@gmail.com>
## Commit message ##
- send-pack: pass --must-have for push negotiation
+ send-pack: pass negotiation config in push
- When push.negotiate is enabled, send-pack spawns a 'git fetch
- --negotiate-only' subprocess to discover common commits. Previously
- this subprocess had no way to include must-have refs in the
- negotiation.
+ When push.negotiate is enabled, 'git push' spawns a child 'git fetch
+ --negotiate-only' process to find common commits. Pass
+ --negotiation-require and --negotiation-restrict options from the
+ 'remote.<name>.negotiationRequire' and
+ 'remote.<name>.negotiationRestrict' config keys to this child process.
- Add a must_have field to send_pack_args, set it from the transport
- layer where the remote struct is available, and pass explicit
- --must-have arguments to the negotiation subprocess. This approach
- directly passes the resolved config values rather than relying on the
- subprocess to read remote config, which is more robust when the URL
- alone is used as the remote identifier.
+ 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 negotiationRequire is configured, the specified ref patterns
+ are passed as --negotiation-require to ensure their tips are always
+ sent as 'have' lines during push negotiation.
+
+ This change also updates the use of --negotiation-tip into
+ --negotiation-restrict now that the new synonym exists.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
@@ send-pack.c: 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 *must_have,
++ const struct string_list *negotiation_require,
++ const struct string_list *negotiation_restrict,
const struct ref *remote_refs,
struct oid_array *commons)
{
@@ send-pack.c: static void get_commons_through_negotiation(struct repository *r,
- nr_negotiation_tip++;
+ 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 (must_have) {
++ if (negotiation_require && negotiation_require->nr) {
+ struct string_list_item *item;
-+ for_each_string_list_item(item, must_have)
-+ strvec_pushf(&child.args, "--must-have=%s",
++ for_each_string_list_item(item, negotiation_require)
++ strvec_pushf(&child.args, "--negotiation-require=%s",
+ item->string);
+ }
+
@@ send-pack.c: int send_pack(struct repository *r,
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->must_have,
++ get_commons_through_negotiation(r, args->url,
++ args->negotiation_require,
++ args->negotiation_restrict,
+ remote_refs, &commons);
trace2_region_leave("send_pack", "push_negotiate", r);
}
@@ send-pack.h: struct repository;
struct send_pack_args {
const char *url;
-+ const struct string_list *must_have;
++ const struct string_list *negotiation_require;
++ const struct string_list *negotiation_restrict;
unsigned verbose:1,
quiet:1,
porcelain:1,
@@ t/t5516-fetch-push.sh: test_expect_success 'push with negotiation does not attem
! grep "Fetching submodule" err
'
-+test_expect_success 'push with negotiation and remote.<name>.mustHave' '
-+ test_when_finished rm -rf musthave &&
-+ mk_empty musthave &&
-+ git push musthave $the_first_commit:refs/remotes/origin/first_commit &&
-+ test_commit -C musthave unrelated_commit &&
-+ git -C musthave config receive.hideRefs refs/remotes/origin/first_commit &&
++test_expect_success 'push with negotiation and remote.<name>.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.<name>.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.musthave.mustHave=refs/heads/main \
-+ push musthave refs/heads/main:refs/remotes/origin/main &&
++ -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
+'
@@ transport.c: static int git_transport_push(struct transport *transport, struct r
args.atomic = !!(flags & TRANSPORT_PUSH_ATOMIC);
args.push_options = transport->push_options;
args.url = transport->url;
-+ args.must_have = &transport->remote->must_have;
++ 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;
--
gitgitgadget
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v2 1/7] t5516: fix test order flakiness
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
` (6 subsequent siblings)
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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. Use 'sort -k 3' to match
the actual number of columns in the output.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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 &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v2 2/7] fetch: add --negotiation-restrict option
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-15 21:57 ` Junio C Hamano
2026-04-15 15:14 ` [PATCH v2 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
` (5 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/fetch-options.adoc | 4 ++++
builtin/fetch.c | 11 +++++++----
builtin/pull.c | 3 +++
t/t5510-fetch.sh | 25 +++++++++++++++++++++++++
t/t5702-protocol-v2.sh | 4 ++--
5 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index 81a9d7f9bb..c07b85499f 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=(<commit>|<glob>)`::
`--negotiation-tip=(<commit>|<glob>)`::
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.
+
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 4795b2a13c..3bcb0c9686 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;
}
@@ -2567,6 +2568,8 @@ 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_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 +2660,7 @@ int cmd_fetch(int argc,
}
if (negotiate_only && !negotiation_tip.nr)
- die(_("--negotiate-only needs one or more --negotiation-tip=*"));
+ die(_("--negotiate-only needs one or more --negotiation-restrict=*"));
if (deepen_relative) {
if (deepen_relative < 0)
diff --git a/builtin/pull.c b/builtin/pull.c
index 7e67fdce97..821cc6699a 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -999,6 +999,9 @@ 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_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/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 \
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v2 3/7] transport: rename negotiation_tips
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-20 8:11 ` Patrick Steinhardt
2026-04-15 15:14 ` [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
` (4 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
The previous change added the --negotiation-restrict synonym for the
--negotiation-tips 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
builtin/fetch.c | 6 +++---
fetch-pack.c | 18 +++++++++---------
fetch-pack.h | 4 ++--
transport-helper.c | 2 +-
transport.c | 10 +++++-----
transport.h | 4 ++--
6 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 3bcb0c9686..4c3c5f2faa 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1534,7 +1534,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;
@@ -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,
@@ -1597,7 +1597,7 @@ 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 %s because the protocol does not support it"),
"--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 4d95d84f9e..0e5b3b7202 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 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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (2 preceding siblings ...)
2026-04-15 15:14 ` [PATCH v2 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-15 19:16 ` Junio C Hamano
2026-04-15 15:14 ` [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation Derrick Stolee via GitGitGadget
` (3 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
In a previous change, the --negotiation-restrict command-line option of
'git fetch' was added as a synonym of --negotiation-tips. 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/config/remote.adoc | 16 ++++++++++++++++
builtin/fetch.c | 24 ++++++++++++++++++++++--
remote.c | 6 ++++++
remote.h | 1 +
t/t5510-fetch.sh | 22 ++++++++++++++++++++++
5 files changed, 67 insertions(+), 2 deletions(-)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index 91e46f66f5..5e8ac6cfdd 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc
@@ -107,6 +107,22 @@ 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.<name>.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.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 4c3c5f2faa..57b2b667ff 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_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);
+ }
}
return transport;
}
@@ -2659,8 +2672,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-restrict=*"));
+ if (negotiate_only && !negotiation_tip.nr) {
+ /*
+ * Defer this check: remote.<name>.negotiationRestrict may
+ * provide defaults in prepare_transport().
+ */
+ }
if (deepen_relative) {
if (deepen_relative < 0)
@@ -2749,6 +2766,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/remote.c b/remote.c
index 7ca2a6501b..07cdf6434d 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,10 @@ 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, "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..0d87494794 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1485,6 +1485,28 @@ test_expect_success '--negotiation-restrict and --negotiation-tip can be mixed'
check_negotiation_tip
'
+test_expect_success 'remote.<name>.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 SYMLINKS 'clone does not get confused by a D/F conflict' '
git init df-conflict &&
(
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (3 preceding siblings ...)
2026-04-15 15:14 ` [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-15 19:50 ` Junio C Hamano
2026-04-20 8:11 ` Patrick Steinhardt
2026-04-15 15:14 ` [PATCH v2 6/7] remote: add negotiationRequire config as default for --negotiation-require Derrick Stolee via GitGitGadget
` (2 subsequent siblings)
7 siblings, 2 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
Add a new --negotiation-require 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 brances. In this way, the
'require' 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.
Also add --negotiation-require to 'git pull' passthrough options.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/fetch-options.adoc | 19 +++++++
builtin/fetch.c | 10 ++++
builtin/pull.c | 3 +
fetch-pack.c | 96 ++++++++++++++++++++++++++++++--
fetch-pack.h | 10 +++-
t/t5510-fetch.sh | 66 ++++++++++++++++++++++
transport.c | 4 +-
transport.h | 6 ++
8 files changed, 206 insertions(+), 8 deletions(-)
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index c07b85499f..85ffc5b32b 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-require=<revision>`::
+ 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`.
+
`--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 57b2b667ff..b60652e6b1 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;
@@ -1615,6 +1616,13 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
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");
+ }
return transport;
}
@@ -2583,6 +2591,8 @@ int cmd_fetch(int argc,
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),
diff --git a/builtin/pull.c b/builtin/pull.c
index 821cc6699a..973186ecdc 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -1002,6 +1002,9 @@ int cmd_pull(int argc,
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 baf239adf9..a0029253f1 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,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");
@@ -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 @@ done:
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;
@@ -1729,6 +1802,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
state = FETCH_SEND_REQUEST;
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;
}
@@ -2181,12 +2258,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 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;
@@ -2197,6 +2276,9 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips,
fetch_negotiator_init(the_repository, &negotiator);
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);
@@ -2221,7 +2303,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_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_restrict_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 6c70c942c2..1daea8c542 100644
--- a/fetch-pack.h
+++ b/fetch-pack.h
@@ -23,6 +23,13 @@ struct fetch_pack_args {
*/
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;
unsigned keep_pack:1;
@@ -93,7 +100,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 string_list *negotiation_require);
/*
* 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 0d87494794..ec30b81c71 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1507,6 +1507,72 @@ test_expect_success 'CLI --negotiation-restrict overrides remote config' '
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 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..d1b0e9eda0 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_require = data->options.negotiation_require;
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_require);
ret = 0;
}
goto cleanup;
diff --git a/transport.h b/transport.h
index cdeb33c16f..8737f23008 100644
--- a/transport.h
+++ b/transport.h
@@ -48,6 +48,12 @@ struct git_transport_options {
*/
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
* common commits to this oidset instead of fetching any packfiles.
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v2 6/7] remote: add negotiationRequire config as default for --negotiation-require
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (4 preceding siblings ...)
2026-04-15 15:14 ` [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
Add a new 'remote.<name>.negotiationRequire' multi-valued config option
that provides default values for --negotiation-require when no
--negotiation-require arguments are specified over the command line.
This is a mirror of how 'remote.<name>.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_require()
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/config/remote.adoc | 24 +++++++++++++++++
Documentation/fetch-options.adoc | 4 +++
builtin/fetch.c | 10 +++++++
remote.c | 6 +++++
remote.h | 1 +
t/t5510-fetch.sh | 46 ++++++++++++++++++++++++++++++++
6 files changed, 91 insertions(+)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index 5e8ac6cfdd..9dbe820275 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc
@@ -123,6 +123,30 @@ 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.<name>.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.<name>.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.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index 85ffc5b32b..16c6e8cee9 100644
--- a/Documentation/fetch-options.adoc
+++ b/Documentation/fetch-options.adoc
@@ -91,6 +91,10 @@ 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.<name>.negotiationRequire` 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 b60652e6b1..a398115fb5 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1622,6 +1622,16 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
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;
}
diff --git a/remote.c b/remote.c
index 07cdf6434d..53deed7565 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_require);
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_require, 0);
}
static void add_merge(struct branch *branch, const char *name)
@@ -568,6 +570,10 @@ static int handle_config(const char *key, const char *value,
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 e6ec37c393..d986257c78 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_require;
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 ec30b81c71..0246ac6bc5 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1573,6 +1573,52 @@ test_expect_success '--negotiation-require avoids duplicates with negotiator' '
test_line_count = 1 matches
'
+test_expect_success 'remote.<name>.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.<name>.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.<name>.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 &&
(
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v2 7/7] send-pack: pass negotiation config in push
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (5 preceding siblings ...)
2026-04-15 15:14 ` [PATCH v2 6/7] remote: add negotiationRequire config as default for --negotiation-require Derrick Stolee via GitGitGadget
@ 2026-04-15 15:14 ` Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-15 15:14 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
When push.negotiate is enabled, 'git push' spawns a child 'git fetch
--negotiate-only' process to find common commits. Pass
--negotiation-require and --negotiation-restrict options from the
'remote.<name>.negotiationRequire' and
'remote.<name>.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 negotiationRequire is configured, the specified ref patterns
are passed as --negotiation-require to ensure their tips are always
sent as 'have' lines during push negotiation.
This change also updates the use of --negotiation-tip into
--negotiation-restrict now that the new synonym exists.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
send-pack.c | 34 ++++++++++++++++++++++++++++------
send-pack.h | 2 ++
t/t5516-fetch-push.sh | 30 ++++++++++++++++++++++++++++++
transport.c | 2 ++
4 files changed, 62 insertions(+), 6 deletions(-)
diff --git a/send-pack.c b/send-pack.c
index 67d6987b1c..1bf17a73a9 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 c5ded2d200..112f31121a 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/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index ac8447f21e..03b797cef5 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.<name>.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.<name>.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 d1b0e9eda0..9903eb1a53 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_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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* Re: [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config
2026-04-15 15:14 ` [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
@ 2026-04-15 19:16 ` Junio C Hamano
0 siblings, 0 replies; 54+ messages in thread
From: Junio C Hamano @ 2026-04-15 19:16 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget; +Cc: git, ps, Derrick Stolee
"Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
> From: Derrick Stolee <stolee@gmail.com>
>
> In a previous change, the --negotiation-restrict command-line option of
> 'git fetch' was added as a synonym of --negotiation-tips. 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.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> Documentation/config/remote.adoc | 16 ++++++++++++++++
> builtin/fetch.c | 24 ++++++++++++++++++++++--
> remote.c | 6 ++++++
> remote.h | 1 +
> t/t5510-fetch.sh | 22 ++++++++++++++++++++++
> 5 files changed, 67 insertions(+), 2 deletions(-)
>
> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
> index 91e46f66f5..5e8ac6cfdd 100644
> --- a/Documentation/config/remote.adoc
> +++ b/Documentation/config/remote.adoc
> @@ -107,6 +107,22 @@ 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.<name>.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.
This is a tangent, but I wonder what happens when this is set in
/etc/gitconfig or ~/.gitconfig by mistake. I personally do not
think of any good reason to set it in either of these two places,
so it might be fine to declare that we read this only from local
configuration file or "git -c var=val" command line, but alternative
that is easier to implement would be to allow for a variable
definition syntax that allows you to say "forget everything you read
so far, clear this multi-valued variable", e.g.
== in /etc/gitconfig ==
[remote "origin"]
negotiationRestrict = refs/pull/*
== in .git/config ==
[remote "origin"]
# clear them
negotiationRestrict =
negotiationRestrict = refs/heads/*
negotiationRestrict = refs/tags/*
or something like that, perhaps?
It is a shame that our configuration framework do not allow
specifying their meanings and semantics to variables like
parse-options do (where OPT_STRING_LIST naturally allows
--no-negotiation-restrict to act as a way to clear the deck).
Because there is no official way to programatically declare that
remote.<name>.negotiationRestrict is a multi-valued variable whose
values are stored in a string-list, the config callback needs to
be coded to implement the behaviour for each variable X-<.
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation
2026-04-15 15:14 ` [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation Derrick Stolee via GitGitGadget
@ 2026-04-15 19:50 ` Junio C Hamano
2026-04-21 18:06 ` Derrick Stolee
2026-04-20 8:11 ` Patrick Steinhardt
1 sibling, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2026-04-15 19:50 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget; +Cc: git, ps, Derrick Stolee
"Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
> +`--negotiation-require=<revision>`::
> + 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`.
Very readable. Nice.
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index 57b2b667ff..b60652e6b1 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;
I thought _tip was renamed to _restrict in an earlier step, but that
was only in the transport in [3/7]. Perhaps we want to rename the
file-scope static variable negotiation_tip to negotiation_restrict
in an earlier step, like in [2/7]?
> + 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;
The configuration (or command line) says --nego-require=refs/heads/main
but this old repository only has refs/heads/master; we do not want
to error out in such a case.
Is it true, though? nego-{require,restrict} feels quite tied to
each project and unless the configuration or command line options
are applied blindly regardless of the project, such an error should
not happen. Perhaps the user who gives a command line option
"--nego-require=refs/heads/naster" may want to be reminded of a
possible typo?
> + if (!odb_has_object(the_repository->objects, &oid, 0))
> + continue;
This is a bit curious. When does the first condition holds but not
the second? A lazy clone whose ref-tip contains a missing commit
promised by somebody else?
In the presense of "promised objects are allowed to be missing"
rule, silently skipping a missing object here is certainly
conservative, but this is not an object that is buried deep in a
tree hierarchy, but the top-level commit or tag that is directly
pointed at by a ref, isn't it? I am a bit uneasy that we ignore
such potential repository corruption (i.e., a missing object may not
be something a promisor remote promised but simply missing).
> @@ -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));
> + }
> + }
OK. I think it makes sense to send these early. We have already
dealt with the usual "tips" by calling mark_tips() way earlier, but
that hasn't produced any "have" yet, and these will go before the
ones from traversal. We do not traverse from these "require" and
that may be why these are not called "_tips"?
And sending these early means the other side has much less chance to
say "we've heard enough, stop!", so in a sense they are of much
higher priority "have"s (I wonder what happens when they do want to
say "stop!" while we are giving a lot of "have" from this loop,
though).
> while ((oid = negotiator->next(negotiator))) {
> + /* avoid duplicate oids from --negotiation-require */
> + if (oidset_contains(&negotiation_require_oids, oid))
> + continue;
If objects rechable from "require" are traversed like others, then
this "avoid duplicate" would become unnecessary, right?
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option
2026-04-15 15:14 ` [PATCH v2 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
@ 2026-04-15 21:57 ` Junio C Hamano
2026-04-19 23:00 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2026-04-15 21:57 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget; +Cc: git, ps, Derrick Stolee
"Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
> - 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);
> - warning("ignoring --negotiation-tip because the protocol does not support it");
> + warning(_("ignoring %s because the protocol does not support it"),
> + "--negotiation-restrict");
These are nice touches to make sure translators cannot possibly
botch these option names that must be given verbatim.
> }
> return transport;
> }
> @@ -2567,6 +2568,8 @@ 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")),
Is OPT_ALIAS() suitable for this?
> @@ -2657,7 +2660,7 @@ int cmd_fetch(int argc,
> }
>
> if (negotiate_only && !negotiation_tip.nr)
> - die(_("--negotiate-only needs one or more --negotiation-tip=*"));
> + die(_("--negotiate-only needs one or more --negotiation-restrict=*"));
OK. Shouldn't this also do the "%s" thing?
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option
2026-04-15 21:57 ` Junio C Hamano
@ 2026-04-19 23:00 ` Derrick Stolee
2026-04-20 10:32 ` Junio C Hamano
0 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee @ 2026-04-19 23:00 UTC (permalink / raw)
To: Junio C Hamano, Derrick Stolee via GitGitGadget; +Cc: git, ps
On 4/15/26 5:57 PM, Junio C Hamano wrote:
> "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> - 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);
>> - warning("ignoring --negotiation-tip because the protocol does not support it");
>> + warning(_("ignoring %s because the protocol does not support it"),
>> + "--negotiation-restrict");
>
> These are nice touches to make sure translators cannot possibly
> botch these option names that must be given verbatim.
>> @@ -2657,7 +2660,7 @@ int cmd_fetch(int argc,
>> }
>>
>> if (negotiate_only && !negotiation_tip.nr)
>> - die(_("--negotiate-only needs one or more --negotiation-tip=*"));
>> + die(_("--negotiate-only needs one or more --negotiation-restrict=*"));
>
> OK. Shouldn't this also do the "%s" thing?
I think I had focused on adding "%s" to strings that were not
previously translated, but adjusting the string under translation
is enough to require retranslation. I should make it easier to
translate, too.
>> }
>> return transport;
>> }
>> @@ -2567,6 +2568,8 @@ 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")),
>
> Is OPT_ALIAS() suitable for this?
I was not aware of this. Thanks for the pointer!
I do plan to make "negotiation-tip" an alias for "negotiation-restrict"
based on the new preference for *-restrict as the "real" option now. Is
that the right way to do this?
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 3/7] transport: rename negotiation_tips
2026-04-15 15:14 ` [PATCH v2 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
@ 2026-04-20 8:11 ` Patrick Steinhardt
0 siblings, 0 replies; 54+ messages in thread
From: Patrick Steinhardt @ 2026-04-20 8:11 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget; +Cc: git, gitster, Derrick Stolee
On Wed, Apr 15, 2026 at 03:14:22PM +0000, Derrick Stolee via GitGitGadget wrote:
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index 3bcb0c9686..4c3c5f2faa 100644
> --- a/builtin/fetch.c
> +++ b/builtin/fetch.c
Don't we want to also rename the local `negotiation_tip` variable in
`cmd_fetch()`?
Patrick
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation
2026-04-15 15:14 ` [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation Derrick Stolee via GitGitGadget
2026-04-15 19:50 ` Junio C Hamano
@ 2026-04-20 8:11 ` Patrick Steinhardt
2026-04-20 11:41 ` Derrick Stolee
1 sibling, 1 reply; 54+ messages in thread
From: Patrick Steinhardt @ 2026-04-20 8:11 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget; +Cc: git, gitster, Derrick Stolee
On Wed, Apr 15, 2026 at 03:14:24PM +0000, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> Add a new --negotiation-require 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.
When reading "--negotiation-require" my mind immediately shifts towards
a mode where we require the remote to have a specific reference, and if
not we'll abort. That's of course not what you're proposing here, but I
would think that I may not be the only person making that connection.
Would an alternative like "--negotiation-include" or
"--negotiation-expand" be better?
> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
> index c07b85499f..85ffc5b32b 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-require=<revision>`::
> + 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`.
This interaction makes sense. You can basically say "send only local
branches, but please _also_ send that one particular ref over there".
> diff --git a/fetch-pack.c b/fetch-pack.c
> index baf239adf9..a0029253f1 100644
> --- a/fetch-pack.c
> +++ b/fetch-pack.c
> @@ -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));
> + }
> + }
Okay, so here we now unconditionally send our requested object IDs.
One thing I was wondering is whether we need to flush eventually. It can
happen that the user specifies millions of refs, either intentionally or
by accident. But I guess the answer might be "no", as the intent of the
feature is that we indeed want to send all of those to the remote side,
and the remote is being asked to consider all of those OIDs.
Patrick
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option
2026-04-19 23:00 ` Derrick Stolee
@ 2026-04-20 10:32 ` Junio C Hamano
2026-04-20 11:35 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Junio C Hamano @ 2026-04-20 10:32 UTC (permalink / raw)
To: Derrick Stolee; +Cc: Derrick Stolee via GitGitGadget, git, ps
Derrick Stolee <stolee@gmail.com> writes:
>>> 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")),
>>
>> Is OPT_ALIAS() suitable for this?
>
> I was not aware of this. Thanks for the pointer!
>
> I do plan to make "negotiation-tip" an alias for "negotiation-restrict"
> based on the new preference for *-restrict as the "real" option now. Is
> that the right way to do this?
Let's see.
$ git grep OPT_ALIAS builtin/clone.c
builtin/clone.c: OPT_ALIAS(0, "recursive", "recurse-submodules"),
$ git clone -h
usage: git clone [<options>] [--] <repo> [<dir>]
-v, --[no-]verbose be more verbose
-q, --[no-]quiet be more quiet
...
--[no-]recurse-submodules[=<pathspec>]
initialize submodules in the clone
--[no-]recursive[=<pathspec>]
alias of --recurse-submodules
...
I think we gave the operation the name "recursive", with a common
short sightedness that anything we are adding "recursive" for is the
only kind of recursiveness, and then prepared for a future where
things other than submodules can also be sources of recursiveness by
making "recurse-submodules" the official name, while still allowing
historical name as the synonym.
In this case, if "-restrict" will become the official name, it
should be listed first, and then the historical name should be made
its alias.
So
OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_tip, ...),
OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"),
would be the right combination in the correct order, I think.
Mention the official thing first, and then tell that another thing
is an alias to what the readers have already seen after that (e.g.,
c28b036f (clone: reorder --recursive/--recurse-submodules,
2020-03-16)).
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option
2026-04-20 10:32 ` Junio C Hamano
@ 2026-04-20 11:35 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-04-20 11:35 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Derrick Stolee via GitGitGadget, git, ps
On 4/20/2026 6:32 AM, Junio C Hamano wrote:
> Derrick Stolee <stolee@gmail.com> writes:
>
>>>> 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")),
>>>
>>> Is OPT_ALIAS() suitable for this?
>>
>> I was not aware of this. Thanks for the pointer!
>>
>> I do plan to make "negotiation-tip" an alias for "negotiation-restrict"
>> based on the new preference for *-restrict as the "real" option now. Is
>> that the right way to do this?
>
> Let's see.
...
> So
>
> OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_tip, ...),
> OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"),
>
> would be the right combination in the correct order, I think.
> Mention the official thing first, and then tell that another thing
> is an alias to what the readers have already seen after that (e.g.,
> c28b036f (clone: reorder --recursive/--recurse-submodules,
> 2020-03-16)).
Thanks! This is indeed what I have in my local copy in preparation
for v3. It helps to have early confirmation about this.
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation
2026-04-20 8:11 ` Patrick Steinhardt
@ 2026-04-20 11:41 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-04-20 11:41 UTC (permalink / raw)
To: Patrick Steinhardt, Derrick Stolee via GitGitGadget; +Cc: git, gitster
On 4/20/2026 4:11 AM, Patrick Steinhardt wrote:
> On Wed, Apr 15, 2026 at 03:14:24PM +0000, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>
>>
>> Add a new --negotiation-require 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.
>
> When reading "--negotiation-require" my mind immediately shifts towards
> a mode where we require the remote to have a specific reference, and if
> not we'll abort. That's of course not what you're proposing here, but I
> would think that I may not be the only person making that connection.
>
> Would an alternative like "--negotiation-include" or
> "--negotiation-expand" be better?
"include" does sound good to me. I'm open to it. I'll let this idea
stew and try prepping my local branch in this direction.
>> + /* 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));
>> + }
>> + }
>
> Okay, so here we now unconditionally send our requested object IDs.
>
> One thing I was wondering is whether we need to flush eventually. It can
> happen that the user specifies millions of refs, either intentionally or
> by accident. But I guess the answer might be "no", as the intent of the
> feature is that we indeed want to send all of those to the remote side,
> and the remote is being asked to consider all of those OIDs.
The idea is indeed to send all of the requested OIDs, but this does
present an interesting behavior where the Git client can allow the
user to misconfigure themselves to send larger-than-normal negotiation
requests. Previously, the client would protect the negotiation with a
maximum set of haves.
Is there any concern about this becoming a vector for increased load
on servers?
Would it be good to have some kind of advice message when the config
matches a set of haves that we think is too large? That would maybe
be a way to help users get out of a self-made problem.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation
2026-04-15 19:50 ` Junio C Hamano
@ 2026-04-21 18:06 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-04-21 18:06 UTC (permalink / raw)
To: Junio C Hamano, Derrick Stolee via GitGitGadget; +Cc: git, ps
On 4/15/26 3:50 PM, Junio C Hamano wrote:
> "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:
...
>> +static struct string_list negotiation_require = STRING_LIST_INIT_NODUP;
>
> I thought _tip was renamed to _restrict in an earlier step, but that
> was only in the transport in [3/7]. Perhaps we want to rename the
> file-scope static variable negotiation_tip to negotiation_restrict
> in an earlier step, like in [2/7]?
This one was missed but will be fixed in v3.
>> + 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;
>
> The configuration (or command line) says --nego-require=refs/heads/main
> but this old repository only has refs/heads/master; we do not want
> to error out in such a case.
>
> Is it true, though? nego-{require,restrict} feels quite tied to
> each project and unless the configuration or command line options
> are applied blindly regardless of the project, such an error should
> not happen. Perhaps the user who gives a command line option
> "--nego-require=refs/heads/naster" may want to be reminded of a
> possible typo?
You're right here. We shouldn't die() on a bad ref passed this way.
This should be a best-effort attempt to include a "have" and continue
normally if it isn't found.
>> + if (!odb_has_object(the_repository->objects, &oid, 0))
>> + continue;
>
> This is a bit curious. When does the first condition holds but not
> the second? A lazy clone whose ref-tip contains a missing commit
> promised by somebody else?
Good point. This would occur if a ref exists but points to a missing
object, which should mean the repo is corrupt and we can't trust that
the fetch will succeed (or should).
> In the presense of "promised objects are allowed to be missing"
> rule, silently skipping a missing object here is certainly
> conservative, but this is not an object that is buried deep in a
> tree hierarchy, but the top-level commit or tag that is directly
> pointed at by a ref, isn't it? I am a bit uneasy that we ignore
> such potential repository corruption (i.e., a missing object may not
> be something a promisor remote promised but simply missing).
I'll update this to be an error.
>> @@ -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));
>> + }
>> + }
>
> OK. I think it makes sense to send these early. We have already
> dealt with the usual "tips" by calling mark_tips() way earlier, but
> that hasn't produced any "have" yet, and these will go before the
> ones from traversal. We do not traverse from these "require" and
> that may be why these are not called "_tips"?
It is correct that these required haves are not plugged into the
walk.
> And sending these early means the other side has much less chance to
> say "we've heard enough, stop!", so in a sense they are of much
> higher priority "have"s (I wonder what happens when they do want to
> say "stop!" while we are giving a lot of "have" from this loop,
> though).
I believe that we don't give the server an opportunity to say "stop"
until we've completed a "round" (see the 'if (flush_at <= ++count)'
case).
With this in mind, I should be incrementing 'count' while sending
the required haves.
>> while ((oid = negotiator->next(negotiator))) {
>> + /* avoid duplicate oids from --negotiation-require */
>> + if (oidset_contains(&negotiation_require_oids, oid))
>> + continue;
>
> If objects rechable from "require" are traversed like others, then
> this "avoid duplicate" would become unnecessary, right?
Yes, if we add the required things to the traversal, then we wouldn't
worry about duplicates. We'd also need to do things in a different
way:
1. The negotiator has a next() method that could do a number of
things, but is responsible for walking history and ignoring IDs
that are reachable from already-emitted haves.
2. This _could_ help us avoid sending required ID if we have
emitted a have that could reach that ID.
3. However, this requires waiting until we flush a round of haves
before determining if we should send the required IDs based on
the walk to this point.
Perhaps the better way to incorporate things together would be to
mark the required IDs as COMMON as we emit them, which would signal
to the negotiation walks that they should not re-emit it as a have
(but since we don't add SEEN, they still walk through the commit to
its later ancestors).
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v3 0/7] fetch: rework negotiation tip options
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (6 preceding siblings ...)
2026-04-15 15:14 ` [PATCH v2 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
` (7 more replies)
7 siblings, 8 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee
Fetch negotiation aims to find enough information from haves and wants such
that the server can be reasonably confident that it will send all necessary
objects and not too many "extra" objects that the client already has.
However, this can break down if there are too many references, since Git
truncates the list of haves based on a few factors (a 256 count limit or the
server sending an ACK at the right time).
We already have the --negotiation-tip feature to focus the set of references
that are used in negotiation, but I feel like this is designed backwards.
I'd rather that we have a way to say "this is an important set of refs, but
feel free to add more refs if needed" than "only use these refs for
negotiation".
Here's an example that demonstrates the problem. In an internal monorepo,
developers work off of the 'main' branch so there are thousands of user
branches that each add a few commits different from the 'main' branch.
However, there is also a long-lived 'release' branch. This branch has a
first-parent history that is parallel to 'main' and each of those commits is
a merge whose second parent is a commit from 'main' that had a successful CI
run. There are additional changes in the 'release' branch merge commits that
add some changelog data, so there is a nontrivial set of novel blob content
in that branch and not just a different set of commits.
The problem we had was that our georeplication system was regularly fetching
from the origin and trying to get all data from all reachable branches. When
the 'release' branch updated, the client would run out of haves before
advertising its copy of the 'release' branch, but it would still list the
new 'release' tip as a want. The server would then think that the client had
never fetched that branch before and would send all of the changelog data
from the whole history of the repo. (This led to a lot of downstream
problems; we mitigated by setting a refspec that stopped fetching the
'release' branch, but this is not ideal.)
What I'd like is a mechanism to say "always advertise the client's version
of 'main' and 'release' but also opportunistically include some user
branches".
Based on my understanding, the '--negotiation-tip' option is close but not
quite what I want. I could have the client only advertise 'release' and
'main' and never advertise any user branches. But then we'd download all
content from each user branch every time it updates. Perhaps this would
happen even with opportunistic inclusion of more haves, but I'd like to
explore this area more.
There's also an issue that the '--negotiation-tip' feature doesn't seem to
have a config key that enables it without CLI arguments. This is something
that we could consider independently.
This patch series adds a new '--negotiation-include' option that does what I
want: it makes sure that these references are included as 'have's during
negotiation. In order to help clarify the difference between this and
'--negotiation-tip', I first create a synonym called
'--negotiation-restrict'.
Both of these options get 'remote.*.negotiation(Include|Restrict)' config
options that enable their behavior by default.
During development, I had briefly considered only using config values, but
that required some strange changes to care about the remote name in the
transport layer. This was most different in the 'git push' integration. When
I discovered the '--negotiation-tip' feature during the process, that gave
me a clear pattern to follow with the addition of a config on top.
Updates in v2
=============
This version is a near-complete rewrite based on feedback around the names
of the previous option and config. The --negotiation-restrict option is new
and the ability to set it via config is also new.
I did try to be more careful around translatable error messages, too.
Updates in v3
=============
* --negotiation-tip is now an alias of --negotiation-restrict.
* More translatable strings use %s to isolate non-translatable options from
translatable words.
* The string_list named negotiation_tip is now renamed to
negotiation_restrict.
* The config options now allow an empty value to reset the list.
* The --negotiation-require option is now called --negotiation-include.
* Similarly, the config option is renamed and all code references.
* The included haves now mark their commits as COMMON so commits that they
can reach are not included in the negotiation walk if they are reached
from the restricted commits.
* The ref iterators are more careful about failing on bad references (ref
exists but object doesn't) and ignoring missing references (perhaps
config is erroneous?).
* When sending tips during push negotiation, use the --negotiation-restrict
option instead of -tip.
Thanks, -Stolee
Derrick Stolee (7):
t5516: fix test order flakiness
fetch: add --negotiation-restrict option
transport: rename negotiation_tips
remote: add remote.*.negotiationRestrict config
fetch: add --negotiation-include option for negotiation
remote: add remote.*.negotiationInclude config
send-pack: pass negotiation config in push
Documentation/config/remote.adoc | 46 +++++++++
Documentation/fetch-options.adoc | 27 +++++
builtin/fetch.c | 70 ++++++++++---
builtin/pull.c | 6 ++
fetch-pack.c | 130 +++++++++++++++++++++---
fetch-pack.h | 14 ++-
remote.c | 16 +++
remote.h | 2 +
send-pack.c | 39 ++++++--
send-pack.h | 2 +
t/t5510-fetch.sh | 166 +++++++++++++++++++++++++++++++
t/t5516-fetch-push.sh | 32 +++++-
t/t5702-protocol-v2.sh | 4 +-
transport-helper.c | 2 +-
transport.c | 16 +--
transport.h | 10 +-
16 files changed, 529 insertions(+), 53 deletions(-)
base-commit: 6e8d538aab8fe4dd07ba9fb87b5c7edcfa5706ad
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2085%2Fderrickstolee%2Fmust-have-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2085/derrickstolee/must-have-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2085
Range-diff vs v2:
1: 466c56abe0 = 1: 466c56abe0 t5516: fix test order flakiness
2: 9a25b0fade ! 2: fe875399a8 fetch: add --negotiation-restrict option
@@ Commit message
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.
+ 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
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot
return transport;
}
@@ builtin/fetch.c: 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"),
- N_("report that we have only objects reachable from this object")),
+- 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")),
+ 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),
@@ builtin/fetch.c: int cmd_fetch(int argc,
if (negotiate_only && !negotiation_tip.nr)
- die(_("--negotiate-only needs one or more --negotiation-tip=*"));
-+ die(_("--negotiate-only needs one or more --negotiation-restrict=*"));
++ die(_("%s needs one or more %s"), "--negotiate-only",
++ "--negotiation-restrict=*");
if (deepen_relative) {
if (deepen_relative < 0)
3: 0f89665aee ! 3: 4332cbf266 transport: rename negotiation_tips
@@ Commit message
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.
+
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## builtin/fetch.c ##
+@@ builtin/fetch.c: 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;
@@ builtin/fetch.c: static int add_oid(const struct reference *ref, void *cb_data)
return 0;
}
@@ builtin/fetch.c: static int add_oid(const struct reference *ref, void *cb_data)
{
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,
+ };
@@ builtin/fetch.c: 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);
@@ builtin/fetch.c: static void add_negotiation_tips(struct git_transport_options *
static struct transport *prepare_transport(struct remote *remote, int deepen,
@@ builtin/fetch.c: 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_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");
+@@ builtin/fetch.c: 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,
+@@ builtin/fetch.c: 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=*");
+
## fetch-pack.c ##
@@ fetch-pack.c: static int next_flush(int stateless_rpc, int count)
4: a731f4fc87 ! 4: d2f48b78b5 remote: add remote.*.negotiationRestrict config
@@ Commit message
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.
+
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/remote.adoc ##
@@ Documentation/config/remote.adoc: priority configuration file (e.g. `.git/config
+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.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot
+ } 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);
++ string_list_append(&negotiation_restrict, item->string);
+ if (transport->smart_options)
+ add_negotiation_restrict_tips(transport->smart_options);
+ else {
@@ builtin/fetch.c: 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-restrict=*"));
-+ if (negotiate_only && !negotiation_tip.nr) {
-+ /*
-+ * Defer this check: remote.<name>.negotiationRestrict may
-+ * provide defaults in prepare_transport().
-+ */
-+ }
-
+- 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"));
@@ builtin/fetch.c: 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=*"));
++ die(_("%s needs one or more %s"), "--negotiate-only",
++ "--negotiation-restrict=*");
if (gtransport->smart_options) {
gtransport->smart_options->acked_commits = &acked_commits;
} else {
@@ remote.c: static int handle_config(const char *key, const char *value,
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);
++ /* reset list on empty value. */
++ if (!value || !*value)
++ string_list_clear(&remote->negotiation_restrict, 0);
++ else
++ string_list_append(&remote->negotiation_restrict, value);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
if (!strcmp(value, "never"))
@@ t/t5510-fetch.sh: test_expect_success '--negotiation-restrict and --negotiation-
+test_expect_success 'remote.<name>.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 \
5: 49c80cef2e ! 5: ae81ef36a1 fetch: add --negotiation-require option for negotiation
@@ Metadata
Author: Derrick Stolee <stolee@gmail.com>
## Commit message ##
- fetch: add --negotiation-require option for negotiation
+ fetch: add --negotiation-include option for negotiation
- Add a new --negotiation-require option to 'git fetch', which ensures
+ 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.
@@ Commit message
--negotiation-restrict to focus the negotiation to 'dev' and 'release'
would avoid those problematic downloads, but would still not allow
advertising potentially-relevant user brances. In this way, the
- 'require' version solves the problem I mention while allowing
+ '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.
+ refs are silently ignored. This behavior is also updated in the ref matching
+ logic for the related --negotiation-restrict option to match.
- Also add --negotiation-require to 'git pull' passthrough options.
+ The implementation outputs the requested objects as haves before the
+ negotiation algorithm kicks in and performs a priority-queue walk from the
+ tip commits. In order to avoid duplicates, we mark the requested objects as
+ COMMON so they (and their descendants) are not output by the negotiator. The
+ negotiator still outputs at least one have before a round is flushed, when
+ the server could ACK to stop the negotiation.
+
+ Also add --negotiation-include to 'git pull' passthrough options.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
@@ Documentation/fetch-options.adoc: See also the `fetch.negotiationAlgorithm` and
configuration variables documented in linkgit:git-config[1], and the
`--negotiate-only` option below.
-+`--negotiation-require=<revision>`::
++`--negotiation-include=<revision>`::
+ 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
@@ Documentation/fetch-options.adoc: See also the `fetch.negotiationAlgorithm` and
++
+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`.
++`--negotiation-include`.
+
`--negotiate-only`::
Do not fetch anything from the server, and instead print the
@@ builtin/fetch.c
@@ builtin/fetch.c: 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;
+ 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;
+@@ builtin/fetch.c: static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
+ 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;
+ }
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remote, int deepen,
strbuf_release(&config_name);
}
}
-+ if (negotiation_require.nr) {
++ if (negotiation_include.nr) {
+ if (transport->smart_options)
-+ transport->smart_options->negotiation_require = &negotiation_require;
++ transport->smart_options->negotiation_include = &negotiation_include;
+ else
+ warning(_("ignoring %s because the protocol does not support it"),
-+ "--negotiation-require");
++ "--negotiation-include");
+ }
return transport;
}
@@ builtin/fetch.c: 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_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"),
+ 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")),
@@ builtin/pull.c: int cmd_pull(int argc,
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"),
++ 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,
@@ fetch-pack.c: 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);
++ if (!odb_has_object(the_repository->objects, ref->oid, 0))
++ die(_("the object %s does not exist"), oid_to_hex(ref->oid));
++ oidset_insert(set, ref->oid);
+ return 0;
+}
+
-+static void resolve_negotiation_require(const struct string_list *negotiation_require,
++static void resolve_negotiation_include(const struct string_list *negotiation_include,
+ struct oidset *result)
+{
+ struct string_list_item *item;
+
-+ if (!negotiation_require || !negotiation_require->nr)
++ if (!negotiation_include || !negotiation_include->nr)
+ return;
+
-+ for_each_string_list_item(item, negotiation_require) {
++ for_each_string_list_item(item, negotiation_include) {
+ if (!has_glob_specials(item->string)) {
+ struct object_id oid;
++
++ /* Ignore missing reference. */
+ if (repo_get_oid(the_repository, item->string, &oid))
+ continue;
++
++ /* Fail on missing object pointed by ref. */
+ if (!odb_has_object(the_repository->objects, &oid, 0))
-+ continue;
++ die(_("the object %s does not exist"),
++ item->string);
++
+ oidset_insert(result, &oid);
+ } else {
+ struct refs_for_each_ref_options opts = {
@@ fetch-pack.c: 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;
++ 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");
@@ fetch-pack.c: static int find_common(struct fetch_negotiator *negotiator,
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)) {
++ /* Send unconditional haves from --negotiation-include */
++ resolve_negotiation_include(args->negotiation_include,
++ &negotiation_include_oids);
++ if (oidset_size(&negotiation_include_oids)) {
+ struct oidset_iter iter;
-+ oidset_iter_init(&negotiation_require_oids, &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++;
++
++ /*
++ * If this is a commit, then mark as COMMON to
++ * avoid the negotiator also outputting it as
++ * a have.
++ */
++ commit = lookup_commit(the_repository, oid);
++ if (commit &&
++ !repo_parse_commit(the_repository, commit))
++ commit->object.flags |= COMMON;
+ }
+ }
+
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++;
@@ fetch-pack.c: done:
flushes++;
}
strbuf_release(&req_buf);
-+ oidset_clear(&negotiation_require_oids);
++ oidset_clear(&negotiation_include_oids);
if (!got_ready || !no_done)
consume_shallow_list(args, &reader);
@@ fetch-pack.c: static void add_common(struct strbuf *req_buf, struct oidset *comm
struct strbuf *req_buf,
- int *haves_to_send)
+ int *haves_to_send,
-+ struct oidset *negotiation_require_oids)
++ struct oidset *negotiation_include_oids)
{
int haves_added = 0;
const struct object_id *oid;
-+ /* Send unconditional haves from --negotiation-require */
-+ if (negotiation_require_oids) {
++ /* Send unconditional haves from --negotiation-include */
++ if (negotiation_include_oids) {
+ struct oidset_iter iter;
-+ oidset_iter_init(negotiation_require_oids, &iter);
++ oidset_iter_init(negotiation_include_oids, &iter);
+
+ while ((oid = oidset_iter_next(&iter)))
+ packet_buf_write(req_buf, "have %s\n",
@@ fetch-pack.c: static void add_common(struct strbuf *req_buf, struct oidset *comm
+ }
+
while ((oid = negotiator->next(negotiator))) {
-+ if (negotiation_require_oids &&
-+ oidset_contains(negotiation_require_oids, oid))
++ if (negotiation_include_oids &&
++ oidset_contains(negotiation_include_oids, oid))
+ continue;
packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid));
if (++haves_added >= *haves_to_send)
@@ fetch-pack.c: static int send_fetch_request(struct fetch_negotiator *negotiator,
int *haves_to_send, int *in_vain,
- int sideband_all, int seen_ack)
+ int sideband_all, int seen_ack,
-+ struct oidset *negotiation_require_oids)
++ struct oidset *negotiation_include_oids)
{
int haves_added;
int done_sent = 0;
@@ fetch-pack.c: static int send_fetch_request(struct fetch_negotiator *negotiator,
- haves_added = add_haves(negotiator, &req_buf, haves_to_send);
+ haves_added = add_haves(negotiator, &req_buf, haves_to_send,
-+ negotiation_require_oids);
++ 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);
@@ fetch-pack.c: 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 oidset negotiation_include_oids = OIDSET_INIT;
struct packet_reader reader;
int in_vain = 0, negotiation_started = 0;
int negotiation_round = 0;
@@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
state = FETCH_SEND_REQUEST;
mark_tips(negotiator, args->negotiation_restrict_tips);
-+ resolve_negotiation_require(args->negotiation_require,
-+ &negotiation_require_oids);
++ resolve_negotiation_include(args->negotiation_include,
++ &negotiation_include_oids);
for_each_cached_alternate(negotiator,
insert_one_alternate_object);
break;
@@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
reader.use_sideband,
- seen_ack)) {
+ seen_ack,
-+ &negotiation_require_oids)) {
++ &negotiation_include_oids)) {
trace2_region_leave_printf("negotiation_v2", "round",
the_repository, "%d",
negotiation_round);
@@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
negotiator->release(negotiator);
oidset_clear(&common);
-+ oidset_clear(&negotiation_require_oids);
++ oidset_clear(&negotiation_include_oids);
return ref;
}
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
int fd[],
- struct oidset *acked_commits)
+ struct oidset *acked_commits,
-+ const struct string_list *negotiation_require)
++ const struct string_list *negotiation_include)
{
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;
++ struct oidset negotiation_include_oids = OIDSET_INIT;
int haves_to_send = INITIAL_FLUSH;
int in_vain = 0;
int seen_ack = 0;
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
fetch_negotiator_init(the_repository, &negotiator);
mark_tips(&negotiator, negotiation_restrict_tips);
-+ resolve_negotiation_require(negotiation_require,
-+ &negotiation_require_oids);
++ resolve_negotiation_include(negotiation_include,
++ &negotiation_include_oids);
+
packet_reader_init(&reader, fd[0], NULL, 0,
PACKET_READ_CHOMP_NEWLINE |
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
- haves_added = add_haves(&negotiator, &req_buf, &haves_to_send);
+ haves_added = add_haves(&negotiator, &req_buf, &haves_to_send,
-+ &negotiation_require_oids);
++ &negotiation_include_oids);
in_vain += haves_added;
if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN))
last_iteration = 1;
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
clear_common_flag(acked_commits);
object_array_clear(&nt_object_array);
-+ oidset_clear(&negotiation_require_oids);
++ oidset_clear(&negotiation_include_oids);
negotiator.release(&negotiator);
strbuf_release(&req_buf);
}
@@ fetch-pack.h: struct fetch_pack_args {
+ * as "have" lines during negotiation, regardless of what the
+ * negotiation algorithm selects.
+ */
-+ const struct string_list *negotiation_require;
++ const struct string_list *negotiation_include;
+
unsigned deepen_relative:1;
unsigned quiet:1;
@@ fetch-pack.h: void negotiate_using_fetch(const struct oid_array *negotiation_res
int fd[],
- struct oidset *acked_commits);
+ struct oidset *acked_commits,
-+ const struct string_list *negotiation_require);
++ const struct string_list *negotiation_include);
/*
* Print an appropriate error message for each sought ref that wasn't
@@ t/t5510-fetch.sh: test_expect_success 'CLI --negotiation-restrict overrides remo
test_grep ! "fetch> have $BETA_1" trace
'
-+test_expect_success '--negotiation-require includes configured refs as haves' '
++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-require=refs/tags/beta_1 \
++ --negotiation-include=refs/tags/beta_1 \
+ origin alpha_s beta_s &&
+
+ ALPHA_1=$(git -C client rev-parse alpha_1) &&
@@ t/t5510-fetch.sh: test_expect_success 'CLI --negotiation-restrict overrides remo
+ test_grep "fetch> have $BETA_1" trace
+'
+
-+test_expect_success '--negotiation-require works with glob patterns' '
++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-require="refs/tags/beta_*" \
++ --negotiation-include="refs/tags/beta_*" \
+ origin alpha_s beta_s &&
+
+ BETA_1=$(git -C client rev-parse beta_1) &&
@@ t/t5510-fetch.sh: test_expect_success 'CLI --negotiation-restrict overrides remo
+ test_grep "fetch> have $BETA_2" trace
+'
+
-+test_expect_success '--negotiation-require is additive with negotiation' '
++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-require=refs/tags/beta_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
+'
+
-+test_expect_success '--negotiation-require ignores non-existent refs silently' '
++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-require=refs/tags/nonexistent \
++ --negotiation-include=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_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-require=refs/tags/alpha_1 \
++ --negotiation-include=refs/tags/alpha_1 \
+ origin alpha_s beta_s &&
+
+ test_grep "fetch> have $ALPHA_1" trace >matches &&
@@ transport.c: 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_require = data->options.negotiation_require;
++ args.negotiation_include = data->options.negotiation_include;
args.reject_shallow_remote = transport->smart_options->reject_shallow;
if (!data->finished_handshake) {
@@ transport.c: static int fetch_refs_via_pack(struct transport *transport,
data->fd,
- data->options.acked_commits);
+ data->options.acked_commits,
-+ data->options.negotiation_require);
++ data->options.negotiation_include);
ret = 0;
}
goto cleanup;
@@ transport.h: struct git_transport_options {
+ * If non-empty, ref patterns whose tips should always be sent
+ * as "have" lines during negotiation.
+ */
-+ const struct string_list *negotiation_require;
++ const struct string_list *negotiation_include;
+
/*
* If allocated, whenever transport_fetch_refs() is called, add known
6: 081f904c07 ! 6: a2d15fa12a remote: add negotiationRequire config as default for --negotiation-require
@@ Metadata
Author: Derrick Stolee <stolee@gmail.com>
## Commit message ##
- remote: add negotiationRequire config as default for --negotiation-require
+ remote: add remote.*.negotiationInclude config
- Add a new 'remote.<name>.negotiationRequire' multi-valued config option
- that provides default values for --negotiation-require when no
- --negotiation-require arguments are specified over the command line.
- This is a mirror of how 'remote.<name>.negotiationRestrict' specifies
- defaults for the --negotiation-restrict arguments.
+ Add a new 'remote.<name>.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.<name>.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_require()
- codepath as the CLI options.
+ 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.
+ 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/remote.adoc ##
-@@ Documentation/config/remote.adoc: command-line option. If `--negotiation-restrict` (or its synonym
- `--negotiation-tip`) is specified on the command line, then the config
- values are not used.
+@@ Documentation/config/remote.adoc: values are not used.
+ Blank values signal to ignore all previous values, allowing a reset of
+ the list from broader config scenarios.
-+remote.<name>.negotiationRequire::
++remote.<name>.negotiationInclude::
+ 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.
@@ Documentation/config/remote.adoc: command-line option. If `--negotiation-restri
+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
++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.<name>.negotiationRequire` are sent
++but the refs matching `remote.<name>.negotiationInclude` are sent
+unconditionally on top of those heuristically selected commits. This
+option is also used during push negotiation when `push.negotiate` is
+enabled.
+++
++Blank values signal to ignore all previous values, allowing a reset of
++the list from broader config scenarios.
+
remote.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
@@ Documentation/fetch-options.adoc
@@ Documentation/fetch-options.adoc: 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`.
+ `--negotiation-include`.
++
+If this option is not specified on the command line, then any
-+`remote.<name>.negotiationRequire` config values for the current remote
++`remote.<name>.negotiationInclude` config values for the current remote
+are used instead.
`--negotiate-only`::
@@ builtin/fetch.c
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remote, int deepen,
else
warning(_("ignoring %s because the protocol does not support it"),
- "--negotiation-require");
-+ } else if (remote->negotiation_require.nr) {
+ "--negotiation-include");
++ } else if (remote->negotiation_include.nr) {
+ if (transport->smart_options) {
-+ transport->smart_options->negotiation_require = &remote->negotiation_require;
++ transport->smart_options->negotiation_include = &remote->negotiation_include;
+ } else {
+ struct strbuf config_name = STRBUF_INIT;
-+ strbuf_addf(&config_name, "remote.%s.negotiationRequire", remote->name);
++ 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);
@@ remote.c: 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_require);
++ string_list_init_dup(&ret->negotiation_include);
ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1,
remote_state->remotes_alloc);
@@ remote.c: 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_require, 0);
++ string_list_clear(&remote->negotiation_include, 0);
}
static void add_merge(struct branch *branch, const char *name)
@@ remote.c: static int handle_config(const char *key, const char *value,
- 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);
+ string_list_clear(&remote->negotiation_restrict, 0);
+ else
+ string_list_append(&remote->negotiation_restrict, value);
++ } else if (!strcmp(subkey, "negotiationinclude")) {
++ /* reset list on empty value. */
++ if (!value || !*value)
++ string_list_clear(&remote->negotiation_include, 0);
++ else
++ string_list_append(&remote->negotiation_include, value);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
if (!strcmp(value, "never"))
@@ remote.h: struct remote {
struct string_list server_options;
struct string_list negotiation_restrict;
-+ struct string_list negotiation_require;
++ struct string_list negotiation_include;
enum follow_remote_head_settings follow_remote_head;
const char *no_warn_branch;
## t/t5510-fetch.sh ##
-@@ t/t5510-fetch.sh: test_expect_success '--negotiation-require avoids duplicates with negotiator' '
+@@ t/t5510-fetch.sh: test_expect_success '--negotiation-include avoids duplicates with negotiator' '
test_line_count = 1 matches
'
-+test_expect_success 'remote.<name>.negotiationRequire used as default for --negotiation-require' '
++test_expect_success 'remote.<name>.negotiationInclude used as default for --negotiation-include' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
+
-+ git -C client config --add remote.origin.negotiationRequire refs/tags/beta_1 &&
++ # 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=alpha_1 \
+ origin alpha_s beta_s &&
@@ t/t5510-fetch.sh: test_expect_success '--negotiation-require avoids duplicates w
+ test_grep "fetch> have $BETA_1" trace
+'
+
-+test_expect_success 'remote.<name>.negotiationRequire works with glob patterns' '
++test_expect_success 'remote.<name>.negotiationInclude 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 -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 &&
@@ t/t5510-fetch.sh: test_expect_success '--negotiation-require avoids duplicates w
+ test_grep "fetch> have $BETA_2" trace
+'
+
-+test_expect_success 'CLI --negotiation-require overrides remote.<name>.negotiationRequire' '
++test_expect_success 'CLI --negotiation-include overrides remote.<name>.negotiationInclude' '
+ 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 -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-require=refs/tags/beta_1 \
++ --negotiation-include=refs/tags/beta_1 \
+ origin alpha_s beta_s &&
+
+ BETA_1=$(git -C client rev-parse beta_1) &&
7: 7cccf59beb ! 7: e6c79f0661 send-pack: pass negotiation config in push
@@ Commit message
When push.negotiate is enabled, 'git push' spawns a child 'git fetch
--negotiate-only' process to find common commits. Pass
- --negotiation-require and --negotiation-restrict options from the
- 'remote.<name>.negotiationRequire' and
+ --negotiation-include and --negotiation-restrict options from the
+ 'remote.<name>.negotiationInclude' and
'remote.<name>.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 negotiationRequire is configured, the specified ref patterns
- are passed as --negotiation-require to ensure their tips are always
+ 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.
This change also updates the use of --negotiation-tip into
@@ send-pack.c: 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_include,
+ const struct string_list *negotiation_restrict,
const struct ref *remote_refs,
struct oid_array *commons)
{
-@@ send-pack.c: static void get_commons_through_negotiation(struct repository *r,
+ 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);
@@ send-pack.c: static void get_commons_through_negotiation(struct repository *r,
+ for_each_string_list_item(item, negotiation_restrict)
+ strvec_pushf(&child.args, "--negotiation-restrict=%s",
+ item->string);
-+ nr_negotiation_tip = negotiation_restrict->nr;
++ 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-tip=%s",
++ strvec_pushf(&child.args, "--negotiation-restrict=%s",
+ oid_to_hex(&ref->new_oid));
-+ nr_negotiation_tip++;
++ nr_negotiation++;
+ }
}
}
+
-+ if (negotiation_require && negotiation_require->nr) {
++ if (negotiation_include && negotiation_include->nr) {
+ struct string_list_item *item;
-+ for_each_string_list_item(item, negotiation_require)
-+ strvec_pushf(&child.args, "--negotiation-require=%s",
++ 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_tip) {
++ if (!nr_negotiation) {
+ child_process_clear(&child);
+ return;
+ }
@@ send-pack.c: 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_include,
+ args->negotiation_restrict,
+ remote_refs, &commons);
trace2_region_leave("send_pack", "push_negotiate", r);
@@ send-pack.h: struct repository;
struct send_pack_args {
const char *url;
-+ const struct string_list *negotiation_require;
++ const struct string_list *negotiation_include;
+ const struct string_list *negotiation_restrict;
unsigned verbose:1,
quiet:1,
@@ t/t5516-fetch-push.sh: test_expect_success 'push with negotiation does not attem
! grep "Fetching submodule" err
'
-+test_expect_success 'push with negotiation and remote.<name>.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_expect_success 'push with negotiation and remote.<name>.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_require.negotiationRequire=refs/heads/main \
-+ push negotiation_require refs/heads/main:refs/remotes/origin/main &&
++ -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
+'
@@ transport.c: static int git_transport_push(struct transport *transport, struct r
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_include = &transport->remote->negotiation_include;
+ args.negotiation_restrict = &transport->remote->negotiation_restrict;
if (flags & TRANSPORT_PUSH_CERT_ALWAYS)
--
gitgitgadget
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v3 1/7] t5516: fix test order flakiness
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 10:50 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
` (6 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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. Use 'sort -k 3' to match
the actual number of columns in the output.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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 &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v3 2/7] fetch: add --negotiation-restrict option
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 11:11 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
` (5 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/fetch-options.adoc | 4 ++++
builtin/fetch.c | 13 ++++++++-----
builtin/pull.c | 3 +++
t/t5510-fetch.sh | 25 +++++++++++++++++++++++++
t/t5702-protocol-v2.sh | 4 ++--
5 files changed, 42 insertions(+), 7 deletions(-)
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index 81a9d7f9bb..c07b85499f 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=(<commit>|<glob>)`::
`--negotiation-tip=(<commit>|<glob>)`::
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.
+
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..821cc6699a 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -999,6 +999,9 @@ 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_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/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 \
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v3 3/7] transport: rename negotiation_tips
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 11:30 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
` (4 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
The previous change added the --negotiation-restrict synonym for the
--negotiation-tips 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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 4d95d84f9e..0e5b3b7202 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 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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (2 preceding siblings ...)
2026-04-22 15:25 ` [PATCH v3 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 12:29 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
` (3 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
In a previous change, the --negotiation-restrict command-line option of
'git fetch' was added as a synonym of --negotiation-tips. 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/config/remote.adoc | 19 +++++++++++++++++++
builtin/fetch.c | 21 +++++++++++++++++----
remote.c | 8 ++++++++
remote.h | 1 +
t/t5510-fetch.sh | 26 ++++++++++++++++++++++++++
5 files changed, 71 insertions(+), 4 deletions(-)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index 91e46f66f5..f1d889d03e 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc
@@ -107,6 +107,25 @@ 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.<name>.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.
++
+Blank values signal to ignore all previous values, allowing a reset of
+the list from broader config scenarios.
+
remote.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2ba0051d52..a1960e3e0c 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,6 +2758,10 @@ 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(_("%s needs one or more %s"), "--negotiate-only",
+ "--negotiation-restrict=*");
if (gtransport->smart_options) {
gtransport->smart_options->acked_commits = &acked_commits;
} else {
diff --git a/remote.c b/remote.c
index 7ca2a6501b..166a56408a 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,12 @@ 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")) {
+ /* reset list on empty value. */
+ if (!value || !*value)
+ string_list_clear(&remote->negotiation_restrict, 0);
+ else
+ string_list_append(&remote->negotiation_restrict, value);
} 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.<name>.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 &&
(
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (3 preceding siblings ...)
2026-04-22 15:25 ` [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 14:38 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 6/7] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
` (2 subsequent siblings)
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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 brances. 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
negotiation algorithm kicks in and performs a priority-queue walk from the
tip commits. In order to avoid duplicates, we mark the requested objects as
COMMON so they (and their descendants) are not output by the negotiator. The
negotiator still outputs at least one have before a round is flushed, when
the server could ACK to stop the negotiation.
Also add --negotiation-include to 'git pull' passthrough options.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/fetch-options.adoc | 19 ++++++
builtin/fetch.c | 16 ++++-
builtin/pull.c | 3 +
fetch-pack.c | 112 +++++++++++++++++++++++++++++--
fetch-pack.h | 10 ++-
t/t5510-fetch.sh | 66 ++++++++++++++++++
transport.c | 4 +-
transport.h | 6 ++
8 files changed, 227 insertions(+), 9 deletions(-)
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index c07b85499f..decc7f6abd 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=<revision>`::
+ 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-include`.
+
`--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 a1960e3e0c..ef50e2fbe9 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;
@@ -1547,10 +1548,14 @@ static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
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;
}
@@ -1615,6 +1620,13 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
strbuf_release(&config_name);
}
}
+ if (negotiation_include.nr) {
+ if (transport->smart_options)
+ transport->smart_options->negotiation_include = &negotiation_include;
+ else
+ warning(_("ignoring %s because the protocol does not support it"),
+ "--negotiation-include");
+ }
return transport;
}
@@ -2582,6 +2594,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 821cc6699a..86c85b60ef 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -1002,6 +1002,9 @@ int cmd_pull(int argc,
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-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..8b080b0080 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,48 @@ 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))
+ die(_("the object %s does not exist"), oid_to_hex(ref->oid));
+ oidset_insert(set, ref->oid);
+ return 0;
+}
+
+static void resolve_negotiation_include(const struct string_list *negotiation_include,
+ struct oidset *result)
+{
+ struct string_list_item *item;
+
+ if (!negotiation_include || !negotiation_include->nr)
+ return;
+
+ for_each_string_list_item(item, negotiation_include) {
+ if (!has_glob_specials(item->string)) {
+ struct object_id oid;
+
+ /* Ignore missing reference. */
+ if (repo_get_oid(the_repository, item->string, &oid))
+ continue;
+
+ /* Fail on missing object pointed by ref. */
+ if (!odb_has_object(the_repository->objects, &oid, 0))
+ die(_("the object %s does not exist"),
+ item->string);
+
+ 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 +390,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 +518,33 @@ 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 */
+ resolve_negotiation_include(args->negotiation_include,
+ &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++;
+
+ /*
+ * If this is a commit, then mark as COMMON to
+ * avoid the negotiator also outputting it as
+ * a have.
+ */
+ commit = lookup_commit(the_repository, oid);
+ if (commit &&
+ !repo_parse_commit(the_repository, commit))
+ commit->object.flags |= COMMON;
+ }
+ }
+
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 +655,7 @@ done:
flushes++;
}
strbuf_release(&req_buf);
+ oidset_clear(&negotiation_include_oids);
if (!got_ready || !no_done)
consume_shallow_list(args, &reader);
@@ -1305,12 +1377,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_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)))
+ packet_buf_write(req_buf, "have %s\n",
+ oid_to_hex(oid));
+ }
+
while ((oid = negotiator->next(negotiator))) {
+ if (negotiation_include_oids &&
+ oidset_contains(negotiation_include_oids, oid))
+ continue;
packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid));
if (++haves_added >= *haves_to_send)
break;
@@ -1358,7 +1444,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 +1500,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 +1745,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 +1818,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
state = FETCH_SEND_REQUEST;
mark_tips(negotiator, args->negotiation_restrict_tips);
+ resolve_negotiation_include(args->negotiation_include,
+ &negotiation_include_oids);
for_each_cached_alternate(negotiator,
insert_one_alternate_object);
break;
@@ -1747,7 +1838,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 +1975,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 +2274,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 string_list *negotiation_include)
{
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 +2292,9 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips,
fetch_negotiator_init(the_repository, &negotiator);
mark_tips(&negotiator, negotiation_restrict_tips);
+ resolve_negotiation_include(negotiation_include,
+ &negotiation_include_oids);
+
packet_reader_init(&reader, fd[0], NULL, 0,
PACKET_READ_CHOMP_NEWLINE |
PACKET_READ_DIE_ON_ERR_PACKET);
@@ -2221,7 +2319,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 +2372,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..32ae94d0b4 100644
--- a/fetch-pack.h
+++ b/fetch-pack.h
@@ -23,6 +23,13 @@ struct fetch_pack_args {
*/
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_include;
+
unsigned deepen_relative:1;
unsigned quiet:1;
unsigned keep_pack:1;
@@ -93,7 +100,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 string_list *negotiation_include);
/*
* 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..4316f8d4ea 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1511,6 +1511,72 @@ 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 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..8a2d8adffc 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 = data->options.negotiation_include;
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);
ret = 0;
}
goto cleanup;
diff --git a/transport.h b/transport.h
index cdeb33c16f..6092775a27 100644
--- a/transport.h
+++ b/transport.h
@@ -48,6 +48,12 @@ struct git_transport_options {
*/
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_include;
+
/*
* If allocated, whenever transport_fetch_refs() is called, add known
* common commits to this oidset instead of fetching any packfiles.
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v3 6/7] remote: add remote.*.negotiationInclude config
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (4 preceding siblings ...)
2026-04-22 15:25 ` [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 14:54 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
Add a new 'remote.<name>.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.<name>.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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/config/remote.adoc | 27 ++++++++++++++++++
Documentation/fetch-options.adoc | 4 +++
builtin/fetch.c | 10 +++++++
remote.c | 8 ++++++
remote.h | 1 +
t/t5510-fetch.sh | 49 ++++++++++++++++++++++++++++++++
6 files changed, 99 insertions(+)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index f1d889d03e..44de6d3c1f 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc
@@ -126,6 +126,33 @@ values are not used.
Blank values signal to ignore all previous values, allowing a reset of
the list from broader config scenarios.
+remote.<name>.negotiationInclude::
+ 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-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.<name>.negotiationInclude` are sent
+unconditionally on top of those heuristically selected commits. This
+option is also used during push negotiation when `push.negotiate` is
+enabled.
++
+Blank values signal to ignore all previous values, allowing a reset of
+the list from broader config scenarios.
+
remote.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.
diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
index decc7f6abd..c475932602 100644
--- a/Documentation/fetch-options.adoc
+++ b/Documentation/fetch-options.adoc
@@ -91,6 +91,10 @@ 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.<name>.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 ef50e2fbe9..827438cf98 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1626,6 +1626,16 @@ 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) {
+ transport->smart_options->negotiation_include = &remote->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 166a56408a..15f3f12184 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)
@@ -570,6 +572,12 @@ static int handle_config(const char *key, const char *value,
string_list_clear(&remote->negotiation_restrict, 0);
else
string_list_append(&remote->negotiation_restrict, value);
+ } else if (!strcmp(subkey, "negotiationinclude")) {
+ /* reset list on empty value. */
+ if (!value || !*value)
+ string_list_clear(&remote->negotiation_include, 0);
+ else
+ string_list_append(&remote->negotiation_include, value);
} 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 4316f8d4ea..db73ed5379 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1577,6 +1577,55 @@ test_expect_success '--negotiation-include avoids duplicates with negotiator' '
test_line_count = 1 matches
'
+test_expect_success 'remote.<name>.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=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.<name>.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.<name>.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 SYMLINKS 'clone does not get confused by a D/F conflict' '
git init df-conflict &&
(
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v3 7/7] send-pack: pass negotiation config in push
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (5 preceding siblings ...)
2026-04-22 15:25 ` [PATCH v3 6/7] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
@ 2026-04-22 15:25 ` Derrick Stolee via GitGitGadget
2026-05-12 15:14 ` Matthew John Cheetham
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
7 siblings, 1 reply; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-04-22 15:25 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Derrick Stolee, Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.<name>.negotiationInclude' and
'remote.<name>.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.
This change also updates the use of --negotiation-tip into
--negotiation-restrict now that the new synonym exists.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
send-pack.c | 39 +++++++++++++++++++++++++++++++--------
send-pack.h | 2 ++
t/t5516-fetch-push.sh | 30 ++++++++++++++++++++++++++++++
transport.c | 2 ++
4 files changed, 65 insertions(+), 8 deletions(-)
diff --git a/send-pack.c b/send-pack.c
index 67d6987b1c..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)) {
- 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 = 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.<name>.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.<name>.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 8a2d8adffc..60b73feb34 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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* Re: [PATCH v3 1/7] t5516: fix test order flakiness
2026-04-22 15:25 ` [PATCH v3 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
@ 2026-05-12 10:50 ` Matthew John Cheetham
0 siblings, 0 replies; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 10:50 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> 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. Use 'sort -k 3' to match
> the actual number of columns in the output.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> 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 &&
Makes sense. Looks like 3f763ddf28 ("fetch: set remote/HEAD if it does
not exist") originally changed it from -k3 to -k4 by mistake.
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 2/7] fetch: add --negotiation-restrict option
2026-04-22 15:25 ` [PATCH v3 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
@ 2026-05-12 11:11 ` Matthew John Cheetham
2026-05-12 14:23 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 11:11 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> 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.
This motivation reads well. Preparing the new naming convention before
introducing the new, complementary option, is the right order to do this
in, IMO.
> 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.
Nice extra win!
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> Documentation/fetch-options.adoc | 4 ++++
> builtin/fetch.c | 13 ++++++++-----
> builtin/pull.c | 3 +++
> t/t5510-fetch.sh | 25 +++++++++++++++++++++++++
> t/t5702-protocol-v2.sh | 4 ++--
> 5 files changed, 42 insertions(+), 7 deletions(-)
>
> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
> index 81a9d7f9bb..c07b85499f 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=(<commit>|<glob>)`::
> `--negotiation-tip=(<commit>|<glob>)`::
> 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.
> +
By my eyes it looks like two other references to the old name remain and
could also be updated for consistency (since --negotiation-restrict is
now the preferred name):
1. Documentation/fetch-options.adoc, under `--negotiate-only`:
"ancestors of the provided `--negotiation-tip=` arguments"
2. Documentation/config/fetch.adoc:
"See also the `--negotiate-only` and `--negotiation-tip` options"
Of course the old name will still work, so this is more a nit-pick :-)
> 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;
> }
Keeping the option name out of the translation string prevents
accidental translation of a fixed symbol - good.
> @@ -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;
> }
Same as above - good.
> @@ -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),
Good. Makes the --negotiate-restrict name the primary and the 'tip' the
alias, matching the docs' preference.
Keeping the variable named `negotiate_tip` helps reduce churn in this
patch (and has no outwardly visible impact anyway). I see a future patch
renames the variable - nice choice for reviewability.
> @@ -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)
Much love for i18n!
> diff --git a/builtin/pull.c b/builtin/pull.c
> index 7e67fdce97..821cc6699a 100644
> --- a/builtin/pull.c
> +++ b/builtin/pull.c
> @@ -999,6 +999,9 @@ 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_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,
It's a shame we don't have a nice way to combine the `OPT_ALIAS` and
`OPT_PASSTHRU_ARGV` functionality, but it's only a small duplication
cost of the repeated definition.
> 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 \
Looks like this test is the only place asserting the '--negotiate-tip'
string literal in the tree - good, no others to update.
Except the two doc cross-references above (nits) this looks good to me.
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 3/7] transport: rename negotiation_tips
2026-04-22 15:25 ` [PATCH v3 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
@ 2026-05-12 11:30 ` Matthew John Cheetham
2026-05-12 14:33 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 11:30 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> The previous change added the --negotiation-restrict synonym for the
> --negotiation-tips 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'.
Nitpick: s/tips/tip/ .. no trailing s for either the option name, nor
the (old) variable name. The function names do use the plural however.
> 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.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> 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;
Good - now mirrors the new, preferred, option name.
> 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,
> };
All callers and references are renamed to match consistency. Good.
> @@ -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[],
LGTM up to here.
> diff --git a/transport-helper.c b/transport-helper.c
> index 4d95d84f9e..0e5b3b7202 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)
Oh! Looks like a place was missed when renaming the preferred option
name in strings. It probably makes sense to do this rename in this patch
(rather than in patch 1) since we're already updating the struct field
name here anyway, but up to you.
Also do we also want to make it translatable like the others?
> 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
Just a missing string rename, and a nitpick typo in the commit message,
but otherwise this patch looks functionally correct.
Aside: I just noticed another '--negotiation-tip' instance in the
`get_commons_through_negotiation` function in send-pack.c. It still uses
the 'tip' option name when forming the shell cmdline.
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config
2026-04-22 15:25 ` [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
@ 2026-05-12 12:29 ` Matthew John Cheetham
2026-05-12 14:52 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 12:29 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee<stolee@gmail.com>
>
> In a previous change, the --negotiation-restrict command-line option of
> 'git fetch' was added as a synonym of --negotiation-tips. Both of these
> options restrict the set of 'haves' the client can send as part of
> negotiation.
s/tips/tip/ as per the previous patch comments. Not important either
way.
> 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.
>
> Signed-off-by: Derrick Stolee<stolee@gmail.com>
> ---
> Documentation/config/remote.adoc | 19 +++++++++++++++++++
> builtin/fetch.c | 21 +++++++++++++++++----
> remote.c | 8 ++++++++
> remote.h | 1 +
> t/t5510-fetch.sh | 26 ++++++++++++++++++++++++++
> 5 files changed, 71 insertions(+), 4 deletions(-)
>
> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
> index 91e46f66f5..f1d889d03e 100644
> --- a/Documentation/config/remote.adoc
> +++ b/Documentation/config/remote.adoc
> @@ -107,6 +107,25 @@ 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.<name>.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.
> ++
> +Blank values signal to ignore all previous values, allowing a reset of
> +the list from broader config scenarios.
> +
> remote.<name>.followRemoteHEAD::
> How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
> when fetching using the configured refspecs of a remote.
You say "during `git fetch` and `git push`", but does `push` actually
honour the new config?
When the `push.negotiate` config is on then
`get_commons_through_negotiation()` from send-pack.c shells out to
`git fetch --negotiate-only` with one `--negotiation-tip=<oid>` arg per
ref being pushed, then the URL. This means the CLI restrict list is
always non-empty in the subprocess so in `prepare_transport()` (in the
below hunk) the `if (negotiation_restrict.nr)` arm is always taken and
the new `else if (remote->negotiation_restrict.nr)` arm is never taken.
BUT.. reading ahead I see that patch 7 actually wires up negotiation
config for push - so my commentary here will be moot! Do we want to drop
the "and `git push`" part from this until patch 7, when it is wired up
appropriately?
One other suggestion: perhaps we should clarify that `push.negotiate`
needs to be set for `remote.<name>.negotiationRestrict` to be honoured
during pushes?
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index 2ba0051d52..a1960e3e0c 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;
> }
See above - this new arm is not reachable on the push.negotiate=true
path until patch 7 wires send-pack up.
> @@ -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,6 +2758,10 @@ 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(_("%s needs one or more %s"), "--negotiate-only",
> + "--negotiation-restrict=*");
> if (gtransport->smart_options) {
> gtransport->smart_options->acked_commits = &acked_commits;
> } else {
This new condition fires whenever `gtransport->smart_options` is NULL,
i.e. the transport doesn't support smart options. Before this case was
handled three lines after this hunk by:
} else {
warning(_("protocol does not support --negotiate-only, exiting"));
result = 1;
trace2_region_leave("fetch", "negotiate-only", the_repository);
goto cleanup;
}
What happens now if a user runs --negotiate-only against a non-smart
transport is they see an odd message:
fatal: --negotiate-only needs one or more --negotiation-restrict=*
..but they may have specified --negotiation-restrict options.
Do we instead want &&?
if (gtransport->smart_options &&
!gtransport->smart_options->negotiation_restrict_tips)
die(_("%s needs one or more %s"), "--negotiate-only",
"--negotiation-restrict=*");
> diff --git a/remote.c b/remote.c
> index 7ca2a6501b..166a56408a 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,12 @@ 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")) {
> + /* reset list on empty value. */
> + if (!value || !*value)
> + string_list_clear(&remote->negotiation_restrict, 0);
> + else
> + string_list_append(&remote->negotiation_restrict, value);
> } else if (!strcmp(subkey, "followremotehead")) {
> const char *no_warn_branch;
> if (!strcmp(value, "never"))
Here we use the 'empty value means reset the list' pattern, but I notice
that the `parse_transport_option()` function already supports this reset
pattern (and used by serveroption above), with a small difference:
if (!value)
return config_error_nonbool(var);
if (!*value)
string_list_clear(transport_options, 0);
So NULL is an error, but empty string is 'reset'. Is it worth being
consistent with other options that use `parse_transport_options`?
> 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.<name>.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 &&
> (
> -- gitgitgadget
>
General shape of this patch is good. The main thing that tripped me up
when reading this patch is the doc claim about push, which only becomes
true after patch 7 lands.
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 2/7] fetch: add --negotiation-restrict option
2026-05-12 11:11 ` Matthew John Cheetham
@ 2026-05-12 14:23 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-05-12 14:23 UTC (permalink / raw)
To: Matthew John Cheetham, Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps
On 5/12/26 7:11 AM, Matthew John Cheetham wrote:
> On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>
>> --- 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=(<commit>|<glob>)`::
>> `--negotiation-tip=(<commit>|<glob>)`::
>> 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.
>> +
>
> By my eyes it looks like two other references to the old name remain and
> could also be updated for consistency (since --negotiation-restrict is
> now the preferred name):
>
> 1. Documentation/fetch-options.adoc, under `--negotiate-only`:
> "ancestors of the provided `--negotiation-tip=` arguments"
>
> 2. Documentation/config/fetch.adoc:
> "See also the `--negotiate-only` and `--negotiation-tip` options"
>
> Of course the old name will still work, so this is more a nit-pick :-)
Thanks for catching these! I will make the correct updates in the
next version.
>> diff --git a/builtin/pull.c b/builtin/pull.c
>> index 7e67fdce97..821cc6699a 100644
>> --- a/builtin/pull.c
>> +++ b/builtin/pull.c
>> @@ -999,6 +999,9 @@ 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_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,
>
> It's a shame we don't have a nice way to combine the `OPT_ALIAS` and
> `OPT_PASSTHRU_ARGV` functionality, but it's only a small duplication
> cost of the repeated definition.
Actually, I just missed that I should use OPT_ALIAS in 'pull' as well as
how it's used in 'fetch'. Will fix.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 3/7] transport: rename negotiation_tips
2026-05-12 11:30 ` Matthew John Cheetham
@ 2026-05-12 14:33 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-05-12 14:33 UTC (permalink / raw)
To: Matthew John Cheetham, Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps
On 5/12/26 7:30 AM, Matthew John Cheetham wrote:
> On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>
>>
>> The previous change added the --negotiation-restrict synonym for the
>> --negotiation-tips 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'.
>
> Nitpick: s/tips/tip/ .. no trailing s for either the option name, nor
> the (old) variable name. The function names do use the plural however.
Thanks for the close eye!
>> diff --git a/transport-helper.c b/transport-helper.c
>> index 4d95d84f9e..0e5b3b7202 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)
>
> Oh! Looks like a place was missed when renaming the preferred option name in
> strings. It probably makes sense to do this rename in this patch
> (rather than in patch 1) since we're already updating the struct field
> name here anyway, but up to you.
>
> Also do we also want to make it translatable like the others?
I will update this as part of the previous patch that handles all the strings,
including making it translatable and dropping the capital "I" at the start.
Good find.
> Aside: I just noticed another '--negotiation-tip' instance in the
> `get_commons_through_negotiation` function in send-pack.c. It still uses
> the 'tip' option name when forming the shell cmdline.
Thanks! I've done a more careful search and confirmed that you found
everything I had missed.
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation
2026-04-22 15:25 ` [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
@ 2026-05-12 14:38 ` Matthew John Cheetham
2026-05-12 16:54 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 14:38 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> 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 brances. 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.
Nice explanation and motivation for the need of such as feature.
One small typo: s/brances/branches/
> 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.
Calling out the intent for the behaviour change (non-existent refs are
silently ignored). This is an important point.
> The implementation outputs the requested objects as haves before the
> negotiation algorithm kicks in and performs a priority-queue walk from the
> tip commits. In order to avoid duplicates, we mark the requested objects as
> COMMON so they (and their descendants) are not output by the negotiator. The
> negotiator still outputs at least one have before a round is flushed, when
> the server could ACK to stop the negotiation.
>
> Also add --negotiation-include to 'git pull' passthrough options.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> Documentation/fetch-options.adoc | 19 ++++++
> builtin/fetch.c | 16 ++++-
> builtin/pull.c | 3 +
> fetch-pack.c | 112 +++++++++++++++++++++++++++++--
> fetch-pack.h | 10 ++-
> t/t5510-fetch.sh | 66 ++++++++++++++++++
> transport.c | 4 +-
> transport.h | 6 ++
> 8 files changed, 227 insertions(+), 9 deletions(-)
>
> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc
> index c07b85499f..decc7f6abd 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=<revision>`::
> + 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-include`.
> +
The placeholder `<revision>` and the description in the body of "ref
name or glob" slightly disagree with each other. The
`--negotiation-restrict` docs use `(<commit>|<glob>)` in the syntax
definition and
"a glob on ref names, a ref, or .. SHA-1 of a commit".
`resolve_negotiation_include()` calls `repo_get_oid()` for non-globs
so bare OIDs and abbreviated SHAs work too. Perhaps consider aligning
the syntaxes, and mention that OIDs work too.
> `--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 a1960e3e0c..ef50e2fbe9 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;
> @@ -1547,10 +1548,14 @@ static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
> 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;
> }
This is the change in behaviour - unresolvable revs were a fatal error
and are now silently ignored.
Note that t5510 '--negotiation-tip rejects missing OIDs' still passes
because it uses an all-zero OID, which parses as a valid hex string,
and dies on the second check "object does not exist". Using something
like `--negotiation-tip=notreal` that previously would error will now
silently be ignored.
Is it worth another test? (invalid object vs not exists)?
> @@ -1615,6 +1620,13 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
> strbuf_release(&config_name);
> }
> }
> + if (negotiation_include.nr) {
> + if (transport->smart_options)
> + transport->smart_options->negotiation_include = &negotiation_include;
> + else
> + warning(_("ignoring %s because the protocol does not support it"),
> + "--negotiation-include");
> + }
> return transport;
> }
There is a difference between the existing `--negotiation-restrict`
option and the new `--negotiation-include` option. Patch 3's commit
message says:
"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."
The new option passes the raw `string_list` to the transport layer and
lets it resolve it instead. If the transport layer now learns how to
resolve refs to oids, why not for tips/restrict?
Would it be easier for future readers for these complementary options
to resolve their inputs at the same layer? Or at least call out why:
"would prefer raw tips but for back-compat we resolve in the built-in"
for example.
> @@ -2582,6 +2594,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 821cc6699a..86c85b60ef 100644
> --- a/builtin/pull.c
> +++ b/builtin/pull.c
> @@ -1002,6 +1002,9 @@ int cmd_pull(int argc,
> 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-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..8b080b0080 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,48 @@ 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))
> + die(_("the object %s does not exist"), oid_to_hex(ref->oid));
> + oidset_insert(set, ref->oid);
> + return 0;
> +}
> +
> +static void resolve_negotiation_include(const struct string_list *negotiation_include,
> + struct oidset *result)
> +{
> + struct string_list_item *item;
> +
> + if (!negotiation_include || !negotiation_include->nr)
> + return;
> +
> + for_each_string_list_item(item, negotiation_include) {
> + if (!has_glob_specials(item->string)) {
> + struct object_id oid;
> +
> + /* Ignore missing reference. */
> + if (repo_get_oid(the_repository, item->string, &oid))
> + continue;
> +
> + /* Fail on missing object pointed by ref. */
> + if (!odb_has_object(the_repository->objects, &oid, 0))
> + die(_("the object %s does not exist"),
> + item->string);
> +
> + 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);
> + }
> + }
> +}
> +
`resolve_negotiation_include()` is basically doing the same as
`add_negotiation_restrict_tips()` except outputting to an `oidset`
vs `oid_array`. This is a result of the difference in ref resolution
layer between `--negotiation-restrict/tip` and `-include`.
> static int find_common(struct fetch_negotiator *negotiator,
> struct fetch_pack_args *args,
> int fd[2], struct object_id *result_oid,
> @@ -347,6 +390,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 +518,33 @@ 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 */
> + resolve_negotiation_include(args->negotiation_include,
> + &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++;
> +
> + /*
> + * If this is a commit, then mark as COMMON to
> + * avoid the negotiator also outputting it as
> + * a have.
> + */
> + commit = lookup_commit(the_repository, oid);
> + if (commit &&
> + !repo_parse_commit(the_repository, commit))
> + commit->object.flags |= COMMON;
> + }
> + }
> +
I want to make sure I understand the COMMON pre-marking before
commenting further on this patch. My understanding is there are actually
two different COMMON bits in the tree, one defined in fetch-pack.c
(bit 6) and one in negotiator/default.c (bit 2):
- fetch-pack.c's COMMON (bit 6) is set after a server ACK confirms an
OID is common with us and is read to decide when we've established
enough common ground to terminate negotiation. This is not consulted
in find_common().
- negotiator/default.c's COMMON (bit 2) is a book-keeping flag used by
`get_rev()` to decide if we skip emitting a commit as a 'have'.
Since we're in fetch-pack.c here, the `commit->object.flags |= COMMON`
line is setting bit 6. The `get_rev()` call in negotiator/default.c
never checks bit 6, only bit 2. As far as I can tell, this mark won't
suppress the negotiator from emitting another 'have' line in the
protocol v0/v1 paths in `find_common()`.
The v2 path doesn't touch the flags.. `add_haves` dedups via
`oidset_contains()`:
while ((oid = negotiator->next(negotiator))) {
if (negotiation_include_oids &&
oidset_contains(negotiation_include_oids, oid))
continue;
packet_buf_write(req_buf, "have %s\n", ...);
}
This works, and is what the new 'avoids duplicates with negotiator' test
runs against, on protocol v2. If we run on protocol v0/v1, and if my
assessment is correct, then we'd see a duplicate I think?
Sorry if I've not understood correctly or am missing something, which is
entirely possible :-)
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config
2026-05-12 12:29 ` Matthew John Cheetham
@ 2026-05-12 14:52 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-05-12 14:52 UTC (permalink / raw)
To: Matthew John Cheetham, Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps
On 5/12/26 8:29 AM, Matthew John Cheetham wrote:
> On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
>
>> From: Derrick Stolee<stolee@gmail.com>
>>
>> In a previous change, the --negotiation-restrict command-line option of
>> 'git fetch' was added as a synonym of --negotiation-tips. Both of these
>> options restrict the set of 'haves' the client can send as part of
>> negotiation.
>
> s/tips/tip/ as per the previous patch comments. Not important either
> way.
Thanks.
>> +remote.<name>.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.
>> ++
>> +Blank values signal to ignore all previous values, allowing a reset of
>> +the list from broader config scenarios.
>> +
>> remote.<name>.followRemoteHEAD::
>> How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
>> when fetching using the configured refspecs of a remote.
>
>
> You say "during `git fetch` and `git push`", but does `push` actually
> honour the new config?
>
> When the `push.negotiate` config is on then
> `get_commons_through_negotiation()` from send-pack.c shells out to
> `git fetch --negotiate-only` with one `--negotiation-tip=<oid>` arg per
> ref being pushed, then the URL. This means the CLI restrict list is
> always non-empty in the subprocess so in `prepare_transport()` (in the
> below hunk) the `if (negotiation_restrict.nr)` arm is always taken and the new
> `else if (remote->negotiation_restrict.nr)` arm is never taken.
>
> BUT.. reading ahead I see that patch 7 actually wires up negotiation
> config for push - so my commentary here will be moot! Do we want to drop
> the "and `git push`" part from this until patch 7, when it is wired up
> appropriately?
You're right that this documentation is premature about 'git push'.
> One other suggestion: perhaps we should clarify that `push.negotiate`
> needs to be set for `remote.<name>.negotiationRestrict` to be honoured
> during pushes?
Yes. I'll rewrite this to focus on 'git fetch'. Then in patch 7 I can
add a new detail about how to make this behavior be respected in 'git push'.
>> if (deepen_relative) {
>> if (deepen_relative < 0)
>> die(_("negative depth in --deepen is not supported"));
>> @@ -2749,6 +2758,10 @@ 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(_("%s needs one or more %s"), "--negotiate-only",
>> + "--negotiation-restrict=*");
>> if (gtransport->smart_options) {
>> gtransport->smart_options->acked_commits = &acked_commits;
>> } else {
>
>
> This new condition fires whenever `gtransport->smart_options` is NULL,
> i.e. the transport doesn't support smart options. Before this case was
> handled three lines after this hunk by:
>
> } else {
> warning(_("protocol does not support --negotiate-only, exiting"));
> result = 1;
> trace2_region_leave("fetch", "negotiate-only", the_repository);
> goto cleanup;
> }
>
> What happens now if a user runs --negotiate-only against a non-smart
> transport is they see an odd message:
>
> fatal: --negotiate-only needs one or more --negotiation-restrict=*
>
> ..but they may have specified --negotiation-restrict options.
>
> Do we instead want &&?
>
> if (gtransport->smart_options &&
> !gtransport->smart_options->negotiation_restrict_tips)
> die(_("%s needs one or more %s"), "--negotiate-only",
> "--negotiation-restrict=*");
You are right that we want to say "we have smart options but haven't
specified restrict arguments" so we can leave the later if/else to
handle the null smart_options case. But actually, I think that it
would be better to reorganize the conditions altogether:
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;
This is easier to reason about:
* If we don't have smart options, then skip out of the negotiation logic.
* If we don't have restrict tips, then die().
* Do the negotiation logic only if the previous two conditions didn't hold.
>> @@ -562,6 +564,12 @@ 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")) {
>> + /* reset list on empty value. */
>> + if (!value || !*value)
>> + string_list_clear(&remote->negotiation_restrict, 0);
>> + else
>> + string_list_append(&remote->negotiation_restrict, value);
>> } else if (!strcmp(subkey, "followremotehead")) {
>> const char *no_warn_branch;
>> if (!strcmp(value, "never"))
>
>
> Here we use the 'empty value means reset the list' pattern, but I notice
> that the `parse_transport_option()` function already supports this reset
> pattern (and used by serveroption above), with a small difference:
>
> if (!value)
> return config_error_nonbool(var);
> if (!*value)
> string_list_clear(transport_options, 0);
>
> So NULL is an error, but empty string is 'reset'. Is it worth being
> consistent with other options that use `parse_transport_options`?
Thanks for catching this! Let's be consistent. NULL is likely
impossible in this case, but let's be consistent. It also needs
to return.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 6/7] remote: add remote.*.negotiationInclude config
2026-04-22 15:25 ` [PATCH v3 6/7] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
@ 2026-05-12 14:54 ` Matthew John Cheetham
2026-05-12 17:55 ` Derrick Stolee
0 siblings, 1 reply; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 14:54 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> Add a new 'remote.<name>.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.<name>.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.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> Documentation/config/remote.adoc | 27 ++++++++++++++++++
> Documentation/fetch-options.adoc | 4 +++
> builtin/fetch.c | 10 +++++++
> remote.c | 8 ++++++
> remote.h | 1 +
> t/t5510-fetch.sh | 49 ++++++++++++++++++++++++++++++++
> 6 files changed, 99 insertions(+)
This patch is a mirror of patch 4 that added the remote config for
negotiateRestrict. Some of the same comments apply here too:
- reusing `parse_transport_option()` vs inline resetting the list
- values could be commit SHAs as well as refs/globs
> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
> index f1d889d03e..44de6d3c1f 100644
> --- a/Documentation/config/remote.adoc
> +++ b/Documentation/config/remote.adoc
> @@ -126,6 +126,33 @@ values are not used.
> Blank values signal to ignore all previous values, allowing a reset of
> the list from broader config scenarios.
>
> +remote.<name>.negotiationInclude::
> + 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`.
Should this say "..same as for `--negotiation-include`"?
This way each `remote.<name>.negotiationX` doc cross-references the
corresponding `--negotiation-X` command line option.
> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
> index 4316f8d4ea..db73ed5379 100755
> --- a/t/t5510-fetch.sh
> +++ b/t/t5510-fetch.sh
> @@ -1577,6 +1577,55 @@ test_expect_success '--negotiation-include avoids duplicates with negotiator' '
> test_line_count = 1 matches
> '
>
> +test_expect_success 'remote.<name>.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=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
> +'
This test sets up the include list as [alpha_1, "", beta_1] which after
the reset should become [beta_1], but the assertions in the test only
check that alpha_1 (sent via the --negotiation-restrict option) and
beta_1 (sent via the include) appear. If the reset of the list didn't
work then the test still passes because alpha_1 is sent via the CLI
option.
> +test_expect_success 'remote.<name>.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.<name>.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 SYMLINKS 'clone does not get confused by a D/F conflict' '
> git init df-conflict &&
> (
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 7/7] send-pack: pass negotiation config in push
2026-04-22 15:25 ` [PATCH v3 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
@ 2026-05-12 15:14 ` Matthew John Cheetham
0 siblings, 0 replies; 54+ messages in thread
From: Matthew John Cheetham @ 2026-05-12 15:14 UTC (permalink / raw)
To: Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps, Derrick Stolee
On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
>
> 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.<name>.negotiationInclude' and
> 'remote.<name>.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.
>
> This change also updates the use of --negotiation-tip into
> --negotiation-restrict now that the new synonym exists.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
> send-pack.c | 39 +++++++++++++++++++++++++++++++--------
> send-pack.h | 2 ++
> t/t5516-fetch-push.sh | 30 ++++++++++++++++++++++++++++++
> transport.c | 2 ++
> 4 files changed, 65 insertions(+), 8 deletions(-)
This patch wires up the negotiation behaviour with push, added in the
previous patches.
> diff --git a/send-pack.c b/send-pack.c
> index 67d6987b1c..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)) {
> - 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 = 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;
> }
Reads cleanly, and also updates the calls to fetch to use the new
preferred option name `restrict` vs the older `tip`. Nice!
> @@ -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.<name>.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.<name>.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 8a2d8adffc..60b73feb34 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;
I've got no other comments on this patch :-)
Thanks,
Matthew
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation
2026-05-12 14:38 ` Matthew John Cheetham
@ 2026-05-12 16:54 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-05-12 16:54 UTC (permalink / raw)
To: Matthew John Cheetham, Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps
On 5/12/26 10:38 AM, Matthew John Cheetham wrote:
> On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>
>>
>> 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 brances. 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.
>
> Nice explanation and motivation for the need of such as feature.
>
> One small typo: s/brances/branches/
Thanks.
>> --- 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=<revision>`::
>> + 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-include`.
>> +
>
> The placeholder `<revision>` and the description in the body of "ref
> name or glob" slightly disagree with each other. The `--negotiation-restrict`
> docs use `(<commit>|<glob>)` in the syntax definition and
> "a glob on ref names, a ref, or .. SHA-1 of a commit".
Good eye.
> `resolve_negotiation_include()` calls `repo_get_oid()` for non-globs
> so bare OIDs and abbreviated SHAs work too. Perhaps consider aligning the
> syntaxes, and mention that OIDs work too.
Will do.
>> @@ -1547,10 +1548,14 @@ static void add_negotiation_restrict_tips(struct
>> git_transport_options *smart_op
>> 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;
>> }
>
> This is the change in behaviour - unresolvable revs were a fatal error
> and are now silently ignored.
>
> Note that t5510 '--negotiation-tip rejects missing OIDs' still passes
> because it uses an all-zero OID, which parses as a valid hex string,
> and dies on the second check "object does not exist". Using something
> like `--negotiation-tip=notreal` that previously would error will now
> silently be ignored.
>
> Is it worth another test? (invalid object vs not exists)?
Yes, let's add a test to guarantee this behavior works.
>> @@ -1615,6 +1620,13 @@ static struct transport *prepare_transport(struct
>> remote *remote, int deepen,
>> strbuf_release(&config_name);
>> }
>> }
>> + if (negotiation_include.nr) {
>> + if (transport->smart_options)
>> + transport->smart_options->negotiation_include =
>> &negotiation_include;
>> + else
>> + warning(_("ignoring %s because the protocol does not support it"),
>> + "--negotiation-include");
>> + }
>> return transport;
>> }
>
> There is a difference between the existing `--negotiation-restrict`
> option and the new `--negotiation-include` option. Patch 3's commit
> message says:
>
> "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."
>
> The new option passes the raw `string_list` to the transport layer and
> lets it resolve it instead. If the transport layer now learns how to
> resolve refs to oids, why not for tips/restrict?
>
> Would it be easier for future readers for these complementary options
> to resolve their inputs at the same layer? Or at least call out why:
> "would prefer raw tips but for back-compat we resolve in the built-in"
> for example.
This is a really key observation. It's a bit of work to unravel, but I
think it's better for unifying these things. Look forward to a better
organization in the next version.
>> +static void resolve_negotiation_include(const struct string_list
>> *negotiation_include,
>> + struct oidset *result)
>> +{
>> + struct string_list_item *item;
>> +
>> + if (!negotiation_include || !negotiation_include->nr)
>> + return;
>> +
>> + for_each_string_list_item(item, negotiation_include) {
>> + if (!has_glob_specials(item->string)) {
>> + struct object_id oid;
>> +
>> + /* Ignore missing reference. */
>> + if (repo_get_oid(the_repository, item->string, &oid))
>> + continue;
>> +
>> + /* Fail on missing object pointed by ref. */
>> + if (!odb_has_object(the_repository->objects, &oid, 0))
>> + die(_("the object %s does not exist"),
>> + item->string);
>> +
>> + 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);
>> + }
>> + }
>> +}
>> +
>
> `resolve_negotiation_include()` is basically doing the same as
> `add_negotiation_restrict_tips()` except outputting to an `oidset`
> vs `oid_array`. This is a result of the difference in ref resolution
> layer between `--negotiation-restrict/tip` and `-include`.
Yes, this code will be replaced with a unified approach in the
next version.
>> static int find_common(struct fetch_negotiator *negotiator,
>> struct fetch_pack_args *args,
>> int fd[2], struct object_id *result_oid,
>> @@ -347,6 +390,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 +518,33 @@ 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 */
>> + resolve_negotiation_include(args->negotiation_include,
>> + &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++;
>> +
>> + /*
>> + * If this is a commit, then mark as COMMON to
>> + * avoid the negotiator also outputting it as
>> + * a have.
>> + */
>> + commit = lookup_commit(the_repository, oid);
>> + if (commit &&
>> + !repo_parse_commit(the_repository, commit))
>> + commit->object.flags |= COMMON;
>> + }
>> + }
>> +
>
> I want to make sure I understand the COMMON pre-marking before
> commenting further on this patch. My understanding is there are actually
> two different COMMON bits in the tree, one defined in fetch-pack.c
> (bit 6) and one in negotiator/default.c (bit 2):
>
> - fetch-pack.c's COMMON (bit 6) is set after a server ACK confirms an
> OID is common with us and is read to decide when we've established
> enough common ground to terminate negotiation. This is not consulted
> in find_common().
>
> - negotiator/default.c's COMMON (bit 2) is a book-keeping flag used by
> `get_rev()` to decide if we skip emitting a commit as a 'have'.
>
> Since we're in fetch-pack.c here, the `commit->object.flags |= COMMON`
> line is setting bit 6. The `get_rev()` call in negotiator/default.c
> never checks bit 6, only bit 2. As far as I can tell, this mark won't
> suppress the negotiator from emitting another 'have' line in the
> protocol v0/v1 paths in `find_common()`.
>
> The v2 path doesn't touch the flags.. `add_haves` dedups via `oidset_contains()`:
>
> while ((oid = negotiator->next(negotiator))) {
> if (negotiation_include_oids &&
> oidset_contains(negotiation_include_oids, oid))
> continue;
> packet_buf_write(req_buf, "have %s\n", ...);
> }
>
> This works, and is what the new 'avoids duplicates with negotiator' test
> runs against, on protocol v2. If we run on protocol v0/v1, and if my
> assessment is correct, then we'd see a duplicate I think?
>
> Sorry if I've not understood correctly or am missing something, which is
> entirely possible :-)
This is a great catch! It shows that I'm breaking some abstractions here,
and thus it's easy to make such a mistake. It's worse that I don't catch
this problem in the tests that I am adding. I'll add a test that
demonstrates the difference.
But beyond that, I think the biggest issue is that the consumer of an
abstract 'negotiator' is assuming something about its implementation. This
means that I should update the negotiator struct to have a function
pointer dedicated to "I chose to send this 'have'" and then the negotiator
can control how to prevent sending more 'have's reachable from those tips.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* Re: [PATCH v3 6/7] remote: add remote.*.negotiationInclude config
2026-05-12 14:54 ` Matthew John Cheetham
@ 2026-05-12 17:55 ` Derrick Stolee
0 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee @ 2026-05-12 17:55 UTC (permalink / raw)
To: Matthew John Cheetham, Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps
On 5/12/26 10:54 AM, Matthew John Cheetham wrote:
> On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:
> This patch is a mirror of patch 4 that added the remote config for
> negotiateRestrict. Some of the same comments apply here too:
>
> - reusing `parse_transport_option()` vs inline resetting the list
>
> - values could be commit SHAs as well as refs/globs
Will do. Thanks.
>> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
>> index f1d889d03e..44de6d3c1f 100644
>> --- a/Documentation/config/remote.adoc
>> +++ b/Documentation/config/remote.adoc
>> @@ -126,6 +126,33 @@ values are not used.
>> Blank values signal to ignore all previous values, allowing a reset of
>> the list from broader config scenarios.
>> +remote.<name>.negotiationInclude::
>> + 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`.
>
> Should this say "..same as for `--negotiation-include`"?
>
> This way each `remote.<name>.negotiationX` doc cross-references the
> corresponding `--negotiation-X` command line option.
Good find. The rest of the description uses *-include.
>> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
>> index 4316f8d4ea..db73ed5379 100755
>> --- a/t/t5510-fetch.sh
>> +++ b/t/t5510-fetch.sh
>> @@ -1577,6 +1577,55 @@ test_expect_success '--negotiation-include avoids
>> duplicates with negotiator' '
>> test_line_count = 1 matches
>> '
>> +test_expect_success 'remote.<name>.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=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
>> +'
>
> This test sets up the include list as [alpha_1, "", beta_1] which after
> the reset should become [beta_1], but the assertions in the test only
> check that alpha_1 (sent via the --negotiation-restrict option) and
> beta_1 (sent via the include) appear. If the reset of the list didn't
> work then the test still passes because alpha_1 is sent via the CLI
> option.
Good point. the negotiation-restrict option is making this less clear.
If I point the restrict option at alpha_2, then I think it exercises
things correctly.
Thanks,
-Stolee
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v4 0/8] fetch: rework negotiation tip options
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (6 preceding siblings ...)
2026-04-22 15:25 ` [PATCH v3 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 1/8] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
` (7 more replies)
7 siblings, 8 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee
Fetch negotiation aims to find enough information from haves and wants such
that the server can be reasonably confident that it will send all necessary
objects and not too many "extra" objects that the client already has.
However, this can break down if there are too many references, since Git
truncates the list of haves based on a few factors (a 256 count limit or the
server sending an ACK at the right time).
We already have the --negotiation-tip feature to focus the set of references
that are used in negotiation, but I feel like this is designed backwards.
I'd rather that we have a way to say "this is an important set of refs, but
feel free to add more refs if needed" than "only use these refs for
negotiation".
Here's an example that demonstrates the problem. In an internal monorepo,
developers work off of the 'main' branch so there are thousands of user
branches that each add a few commits different from the 'main' branch.
However, there is also a long-lived 'release' branch. This branch has a
first-parent history that is parallel to 'main' and each of those commits is
a merge whose second parent is a commit from 'main' that had a successful CI
run. There are additional changes in the 'release' branch merge commits that
add some changelog data, so there is a nontrivial set of novel blob content
in that branch and not just a different set of commits.
The problem we had was that our georeplication system was regularly fetching
from the origin and trying to get all data from all reachable branches. When
the 'release' branch updated, the client would run out of haves before
advertising its copy of the 'release' branch, but it would still list the
new 'release' tip as a want. The server would then think that the client had
never fetched that branch before and would send all of the changelog data
from the whole history of the repo. (This led to a lot of downstream
problems; we mitigated by setting a refspec that stopped fetching the
'release' branch, but this is not ideal.)
What I'd like is a mechanism to say "always advertise the client's version
of 'main' and 'release' but also opportunistically include some user
branches".
Based on my understanding, the '--negotiation-tip' option is close but not
quite what I want. I could have the client only advertise 'release' and
'main' and never advertise any user branches. But then we'd download all
content from each user branch every time it updates. Perhaps this would
happen even with opportunistic inclusion of more haves, but I'd like to
explore this area more.
There's also an issue that the '--negotiation-tip' feature doesn't seem to
have a config key that enables it without CLI arguments. This is something
that we could consider independently.
This patch series adds a new '--negotiation-include' option that does what I
want: it makes sure that these references are included as 'have's during
negotiation. In order to help clarify the difference between this and
'--negotiation-tip', I first create a synonym called
'--negotiation-restrict'.
Both of these options get 'remote.*.negotiation(Include|Restrict)' config
options that enable their behavior by default.
During development, I had briefly considered only using config values, but
that required some strange changes to care about the remote name in the
transport layer. This was most different in the 'git push' integration. When
I discovered the '--negotiation-tip' feature during the process, that gave
me a clear pattern to follow with the addition of a config on top.
Updates in v2
=============
This version is a near-complete rewrite based on feedback around the names
of the previous option and config. The --negotiation-restrict option is new
and the ability to set it via config is also new.
I did try to be more careful around translatable error messages, too.
Updates in v3
=============
* --negotiation-tip is now an alias of --negotiation-restrict.
* More translatable strings use %s to isolate non-translatable options from
translatable words.
* The string_list named negotiation_tip is now renamed to
negotiation_restrict.
* The config options now allow an empty value to reset the list.
* The --negotiation-require option is now called --negotiation-include.
* Similarly, the config option is renamed and all code references.
* The included haves now mark their commits as COMMON so commits that they
can reach are not included in the negotiation walk if they are reached
from the restricted commits.
* The ref iterators are more careful about failing on bad references (ref
exists but object doesn't) and ignoring missing references (perhaps
config is erroneous?).
* When sending tips during push negotiation, use the --negotiation-restrict
option instead of -tip.
Updates in v4
=============
Thanks, Matthew, for the detailed review! There are some big changes in this
version.
* Expanded commit message to cite the commit that introduced the bug
(3f763ddf28).
* Renamed --negotiation-tip to --negotiation-restrict throughout docs/code
(including send-pack.c, transport-helper.c, builtin/pull.c). Added
OPT_ALIAS in git-pull.
* Switched config parsing to use parse_transport_option() helper. Removed
git push from docs (not implemented yet). Restructured --negotiate-only
validation flow.
* NEW Patch 5: Added have_sent() interface to negotiators, so included
haves can be de-duplicated properly by the negotiation algorithm.
* Replaced COMMON flag hack with negotiator->have_sent() calls. Moved
ref-pattern resolution into builtin/fetch.c (add_negotiation_tips()) so
fetch-pack receives pre-resolved oid_array instead of string_list. Added
test for --negotiation-tip ignoring missing refs. Added
duplicate-avoidance test for v0. Accepts commit hashes in addition to ref
names/globs.
* Use parse_transport_option() for config. Updated docs to mention commit
hashes. Removed git push from config docs. Fixed test to use correct
restrict/include combinations.
* In the last patch, add doc notes that remote config values also apply
during git push with push.negotiate, now that they are integrated by that
change.
Thanks, -Stolee
Derrick Stolee (8):
t5516: fix test order flakiness
fetch: add --negotiation-restrict option
transport: rename negotiation_tips
remote: add remote.*.negotiationRestrict config
negotiator: add have_sent() interface
fetch: add --negotiation-include option for negotiation
remote: add remote.*.negotiationInclude config
send-pack: pass negotiation config in push
Documentation/config/fetch.adoc | 2 +-
Documentation/config/remote.adoc | 51 +++++++++
Documentation/fetch-options.adoc | 29 ++++-
builtin/fetch.c | 82 ++++++++++---
builtin/pull.c | 6 +-
fetch-negotiator.h | 9 ++
fetch-pack.c | 99 +++++++++++++---
fetch-pack.h | 10 +-
negotiator/default.c | 8 ++
negotiator/noop.c | 7 ++
negotiator/skipping.c | 8 ++
remote.c | 10 ++
remote.h | 2 +
send-pack.c | 39 +++++--
send-pack.h | 2 +
t/t5510-fetch.sh | 191 +++++++++++++++++++++++++++++++
t/t5516-fetch-push.sh | 32 +++++-
t/t5702-protocol-v2.sh | 4 +-
transport-helper.c | 5 +-
transport.c | 20 +++-
transport.h | 7 +-
21 files changed, 561 insertions(+), 62 deletions(-)
base-commit: 6e8d538aab8fe4dd07ba9fb87b5c7edcfa5706ad
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2085%2Fderrickstolee%2Fmust-have-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2085/derrickstolee/must-have-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/2085
Range-diff vs v3:
1: 466c56abe0 ! 1: 7409a479d6 t5516: fix test order flakiness
@@ Commit message
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. Use 'sort -k 3' to match
- the actual number of columns in the output.
+ 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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
2: fe875399a8 ! 2: 7836a2d6a5 fetch: add --negotiation-restrict option
@@ Commit message
Signed-off-by: Derrick Stolee <stolee@gmail.com>
+ ## Documentation/config/fetch.adoc ##
+@@
+ 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`::
+
## Documentation/fetch-options.adoc ##
@@ Documentation/fetch-options.adoc: the current repository has the same history as the source repository.
`.git/shallow`. This option updates `.git/shallow` and accepts such
@@ Documentation/fetch-options.adoc: the current repository has the same history as
This option may be specified more than once; if so, Git will report
commits reachable from any of the given commits.
+
+@@ Documentation/fetch-options.adoc: 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)`.
## builtin/fetch.c ##
@@ builtin/fetch.c: static void add_negotiation_tips(struct git_transport_options *smart_options)
@@ builtin/fetch.c: int cmd_fetch(int argc,
## builtin/pull.c ##
@@ builtin/pull.c: int cmd_pull(int argc,
- OPT_PASSTHRU_ARGV(0, "negotiation-tip", &opt_fetch, N_("revision"),
+ 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_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,
+ ## send-pack.c ##
+@@ send-pack.c: 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++;
+ }
+
## t/t5510-fetch.sh ##
@@ t/t5510-fetch.sh: EOF
test_cmp fatal-expect fatal-actual
@@ t/t5702-protocol-v2.sh: setup_negotiate_only () {
EOF
test_must_fail git -c protocol.version=2 -C client fetch \
+
+ ## transport-helper.c ##
+@@ transport-helper.c: 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);
3: 4332cbf266 ! 3: 401bdaff7c transport: rename negotiation_tips
@@ Commit message
transport: rename negotiation_tips
The previous change added the --negotiation-restrict synonym for the
- --negotiation-tips 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'.
+ --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.
+ 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.
@@ transport-helper.c: static int fetch_refs(struct transport *transport,
- if (data->transport_options.negotiation_tips)
+ if (data->transport_options.negotiation_restrict_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)
## transport.c ##
@@ transport.c: static int fetch_refs_via_pack(struct transport *transport,
4: d2f48b78b5 ! 4: a14c568a1f remote: add remote.*.negotiationRestrict config
@@ Metadata
## Commit message ##
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-tips. Both of these
- options restrict the set of 'haves' the client can send as part of
- negotiation.
+ 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.
+ '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.
@@ Documentation/config/remote.adoc: priority configuration file (e.g. `.git/config
`$HOME/.gitconfig`).
+remote.<name>.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.
++ 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
@@ builtin/fetch.c: 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)
+- 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=*");
- if (gtransport->smart_options) {
- gtransport->smart_options->acked_commits = &acked_commits;
- } else {
++
++ gtransport->smart_options->acked_commits = &acked_commits;
++
+ if (server_options.nr)
+ gtransport->server_options = &server_options;
+ result = transport_fetch_refs(gtransport, NULL);
## remote.c ##
@@ remote.c: static struct remote *make_remote(struct remote_state *remote_state,
@@ remote.c: static int handle_config(const char *key, const char *value,
return parse_transport_option(key, value,
&remote->server_options);
+ } else if (!strcmp(subkey, "negotiationrestrict")) {
-+ /* reset list on empty value. */
-+ if (!value || !*value)
-+ string_list_clear(&remote->negotiation_restrict, 0);
-+ else
-+ string_list_append(&remote->negotiation_restrict, value);
++ return parse_transport_option(key, value,
++ &remote->negotiation_restrict);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
if (!strcmp(value, "never"))
-: ---------- > 5: 94b79784fe negotiator: add have_sent() interface
5: ae81ef36a1 ! 6: b4cd458fe0 fetch: add --negotiation-include option for negotiation
@@ Commit message
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 brances. In this way, the
+ 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.
@@ Commit message
logic for the related --negotiation-restrict option to match.
The implementation outputs the requested objects as haves before the
- negotiation algorithm kicks in and performs a priority-queue walk from the
- tip commits. In order to avoid duplicates, we mark the requested objects as
- COMMON so they (and their descendants) are not output by the negotiator. The
- negotiator still outputs at least one have before a round is flushed, when
- the server could ACK to stop the negotiation.
+ 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.
@@ Documentation/fetch-options.adoc: See also the `fetch.negotiationAlgorithm` and
configuration variables documented in linkgit:git-config[1], and the
`--negotiate-only` option below.
-+`--negotiation-include=<revision>`::
-+ Ensure that the given ref tip is always sent as a "have" line
-+ during fetch negotiation, regardless of what the negotiation
++`--negotiation-include=(<commit>|<glob>)`::
++ 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 ref is sent
++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`) or a
-+glob pattern (e.g. `refs/heads/release/{asterisk}`). The pattern syntax
-+is the same as for `--negotiation-restrict`.
++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
@@ Documentation/fetch-options.adoc: See also the `fetch.negotiationAlgorithm` and
+
`--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,
## builtin/fetch.c ##
@@ builtin/fetch.c: static struct transport *gsecondary;
@@ builtin/fetch.c: static struct transport *gsecondary;
struct fetch_config {
enum display_format display_format;
-@@ builtin/fetch.c: static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
+@@ builtin/fetch.c: 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)
+ {
+ 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;
@@ builtin/fetch.c: static void add_negotiation_restrict_tips(struct git_transport_
oid_array_append(oids, &oid);
continue;
}
+@@ builtin/fetch.c: static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
+ warning(_("ignoring %s=%s because it does not match any refs"),
+ "--negotiation-restrict", s);
+ }
+- smart_options->negotiation_restrict_tips = oids;
++ *output_list = oids;
+ }
+
+ static struct transport *prepare_transport(struct remote *remote, int deepen,
+@@ builtin/fetch.c: 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);
+ else
+ warning(_("ignoring %s because the protocol does not support it"),
+ "--negotiation-restrict");
+@@ builtin/fetch.c: 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);
+ else {
+ struct strbuf config_name = STRBUF_INIT;
+ strbuf_addf(&config_name, "remote.%s.negotiationRestrict", remote->name);
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remote, int deepen,
strbuf_release(&config_name);
}
}
+ if (negotiation_include.nr) {
+ if (transport->smart_options)
-+ transport->smart_options->negotiation_include = &negotiation_include;
++ add_negotiation_tips(&negotiation_include,
++ &transport->smart_options->negotiation_include_tips);
+ else
+ warning(_("ignoring %s because the protocol does not support it"),
+ "--negotiation-include");
@@ builtin/fetch.c: int cmd_fetch(int argc,
## builtin/pull.c ##
@@ builtin/pull.c: int cmd_pull(int argc,
- 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_PASSTHRU_ARGV(0, "negotiation-include", &opt_fetch, N_("revision"),
+ N_("ensure this ref is always sent as a negotiation have"),
+ 0),
@@ fetch-pack.c: static void send_filter(struct fetch_pack_args *args,
}
}
-+static int add_oid_to_oidset(const struct reference *ref, void *cb_data)
++static void add_oids_to_set(const struct oid_array *array,
++ struct oidset *set)
+{
-+ struct oidset *set = cb_data;
-+ if (!odb_has_object(the_repository->objects, ref->oid, 0))
-+ die(_("the object %s does not exist"), oid_to_hex(ref->oid));
-+ oidset_insert(set, ref->oid);
-+ return 0;
-+}
-+
-+static void resolve_negotiation_include(const struct string_list *negotiation_include,
-+ struct oidset *result)
-+{
-+ struct string_list_item *item;
-+
-+ if (!negotiation_include || !negotiation_include->nr)
++ if (!array)
+ return;
+
-+ for_each_string_list_item(item, negotiation_include) {
-+ if (!has_glob_specials(item->string)) {
-+ struct object_id oid;
++ 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));
+
-+ /* Ignore missing reference. */
-+ if (repo_get_oid(the_repository, item->string, &oid))
-+ continue;
-+
-+ /* Fail on missing object pointed by ref. */
-+ if (!odb_has_object(the_repository->objects, &oid, 0))
-+ die(_("the object %s does not exist"),
-+ item->string);
-+
-+ 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);
-+ }
++ oidset_insert(set, oid);
+ }
+}
+
@@ fetch-pack.c: static int find_common(struct fetch_negotiator *negotiator,
retval = -1;
+
+ /* Send unconditional haves from --negotiation-include */
-+ resolve_negotiation_include(args->negotiation_include,
-+ &negotiation_include_oids);
++ 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);
@@ fetch-pack.c: static int find_common(struct fetch_negotiator *negotiator,
+ print_verbose(args, "have %s", oid_to_hex(oid));
+ count++;
+
-+ /*
-+ * If this is a commit, then mark as COMMON to
-+ * avoid the negotiator also outputting it as
-+ * a have.
-+ */
+ commit = lookup_commit(the_repository, oid);
-+ if (commit &&
-+ !repo_parse_commit(the_repository, commit))
-+ commit->object.flags |= COMMON;
++ if (commit)
++ negotiator->have_sent(negotiator, commit);
+ }
+ }
+
@@ fetch-pack.c: static void add_common(struct strbuf *req_buf, struct oidset *comm
+ struct oidset_iter iter;
+ oidset_iter_init(negotiation_include_oids, &iter);
+
-+ while ((oid = oidset_iter_next(&iter)))
-+ packet_buf_write(req_buf, "have %s\n",
-+ oid_to_hex(oid));
++ 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))) {
-+ if (negotiation_include_oids &&
-+ oidset_contains(negotiation_include_oids, oid))
-+ continue;
packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid));
if (++haves_added >= *haves_to_send)
- break;
@@ fetch-pack.c: static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out,
struct fetch_pack_args *args,
const struct ref *wants, struct oidset *common,
@@ fetch-pack.c: static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
state = FETCH_SEND_REQUEST;
mark_tips(negotiator, args->negotiation_restrict_tips);
-+ resolve_negotiation_include(args->negotiation_include,
-+ &negotiation_include_oids);
++ add_oids_to_set(args->negotiation_include_tips,
++ &negotiation_include_oids);
for_each_cached_alternate(negotiator,
insert_one_alternate_object);
break;
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
int fd[],
- struct oidset *acked_commits)
+ struct oidset *acked_commits,
-+ const struct string_list *negotiation_include)
++ const struct oid_array *negotiation_include_tips)
{
struct fetch_negotiator negotiator;
struct packet_reader reader;
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
fetch_negotiator_init(the_repository, &negotiator);
mark_tips(&negotiator, negotiation_restrict_tips);
-+ resolve_negotiation_include(negotiation_include,
-+ &negotiation_include_oids);
++ add_oids_to_set(negotiation_include_tips,
++ &negotiation_include_oids);
+
packet_reader_init(&reader, fd[0], NULL, 0,
PACKET_READ_CHOMP_NEWLINE |
@@ fetch-pack.c: void negotiate_using_fetch(const struct oid_array *negotiation_res
## fetch-pack.h ##
@@ fetch-pack.h: 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;
-+ /*
-+ * 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_include;
-+
unsigned deepen_relative:1;
unsigned quiet:1;
- unsigned keep_pack:1;
@@ fetch-pack.h: 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_include);
++ const struct oid_array *negotiation_include_tips);
/*
* Print an appropriate error message for each sought ref that wasn't
## t/t5510-fetch.sh ##
+@@ t/t5510-fetch.sh: 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 \
@@ t/t5510-fetch.sh: test_expect_success 'CLI --negotiation-restrict overrides remote config' '
test_grep ! "fetch> have $BETA_1" trace
'
@@ t/t5510-fetch.sh: test_expect_success 'CLI --negotiation-restrict overrides remo
+ 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 &&
@@ transport.c: 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 = data->options.negotiation_include;
++ args.negotiation_include_tips = data->options.negotiation_include_tips;
args.reject_shallow_remote = transport->smart_options->reject_shallow;
if (!data->finished_handshake) {
@@ transport.c: static int fetch_refs_via_pack(struct transport *transport,
data->fd,
- data->options.acked_commits);
+ data->options.acked_commits,
-+ data->options.negotiation_include);
++ data->options.negotiation_include_tips);
ret = 0;
}
goto cleanup;
+@@ transport.c: 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);
## transport.h ##
@@ transport.h: 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 non-empty, ref patterns whose tips should always be sent
-+ * as "have" lines during negotiation.
-+ */
-+ const struct string_list *negotiation_include;
-+
/*
* If allocated, whenever transport_fetch_refs() is called, add known
- * common commits to this oidset instead of fetching any packfiles.
6: a2d15fa12a ! 7: 7bd70a970b remote: add remote.*.negotiationInclude config
@@ Documentation/config/remote.adoc: values are not used.
the list from broader config scenarios.
+remote.<name>.negotiationInclude::
-+ 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.
++ 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`) or a
-+glob pattern (e.g. `refs/heads/release/*`). The pattern syntax is the same
-+as for `--negotiation-restrict`.
++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
@@ Documentation/config/remote.adoc: values are not used.
when fetching using the configured refspecs of a remote.
## Documentation/fetch-options.adoc ##
-@@ Documentation/fetch-options.adoc: is the same as for `--negotiation-restrict`.
+@@ Documentation/fetch-options.adoc: 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`.
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot
"--negotiation-include");
+ } else if (remote->negotiation_include.nr) {
+ if (transport->smart_options) {
-+ transport->smart_options->negotiation_include = &remote->negotiation_include;
++ add_negotiation_tips(&remote->negotiation_include,
++ &transport->smart_options->negotiation_include_tips);
+ } else {
+ struct strbuf config_name = STRBUF_INIT;
+ strbuf_addf(&config_name, "remote.%s.negotiationInclude", remote->name);
@@ remote.c: static void remote_clear(struct remote *remote)
static void add_merge(struct branch *branch, const char *name)
@@ remote.c: static int handle_config(const char *key, const char *value,
- string_list_clear(&remote->negotiation_restrict, 0);
- else
- string_list_append(&remote->negotiation_restrict, value);
+ } else if (!strcmp(subkey, "negotiationrestrict")) {
+ return parse_transport_option(key, value,
+ &remote->negotiation_restrict);
+ } else if (!strcmp(subkey, "negotiationinclude")) {
-+ /* reset list on empty value. */
-+ if (!value || !*value)
-+ string_list_clear(&remote->negotiation_include, 0);
-+ else
-+ string_list_append(&remote->negotiation_include, value);
++ return parse_transport_option(key, value,
++ &remote->negotiation_include);
} else if (!strcmp(subkey, "followremotehead")) {
const char *no_warn_branch;
if (!strcmp(value, "never"))
@@ t/t5510-fetch.sh: test_expect_success '--negotiation-include avoids duplicates w
+ 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=alpha_1 \
++ --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 &&
++ test_grep ! "fetch> have $ALPHA_1" trace &&
+ BETA_1=$(git -C client rev-parse beta_1) &&
+ test_grep "fetch> have $BETA_1" trace
+'
@@ t/t5510-fetch.sh: test_expect_success '--negotiation-include avoids duplicates w
+ 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 &&
- (
+ test_expect_success '--negotiation-include avoids duplicates with v0' '
+ test_when_finished rm -f trace &&
+ setup_negotiation_tip server server 0 &&
7: e6c79f0661 ! 8: 5b968245eb send-pack: pass negotiation config in push
@@ Commit message
Signed-off-by: Derrick Stolee <stolee@gmail.com>
+ ## Documentation/config/remote.adoc ##
+@@ Documentation/config/remote.adoc: 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.
+
+@@ Documentation/config/remote.adoc: unconditionally on top of those heuristically selected commits. This
+ option is also used during push negotiation when `push.negotiate` is
+ enabled.
+ +
++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.
+
+
## send-pack.c ##
@@ send-pack.c: static void reject_invalid_nonce(const char *nonce, int len)
@@ send-pack.c: static void reject_invalid_nonce(const char *nonce, int len)
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",
+ strvec_pushf(&child.args, "--negotiation-restrict=%s",
+- oid_to_hex(&ref->new_oid));
+- nr_negotiation_tip++;
+ item->string);
+ nr_negotiation = negotiation_restrict->nr;
+ } else {
--
gitgitgadget
^ permalink raw reply [flat|nested] 54+ messages in thread
* [PATCH v4 1/8] t5516: fix test order flakiness
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 2/8] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
` (6 subsequent siblings)
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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 &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 2/8] fetch: add --negotiation-restrict option
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 1/8] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 3/8] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
` (5 subsequent siblings)
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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=(<commit>|<glob>)`::
`--negotiation-tip=(<commit>|<glob>)`::
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);
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 3/8] transport: rename negotiation_tips
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 1/8] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 2/8] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 4/8] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
` (4 subsequent siblings)
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 4/8] remote: add remote.*.negotiationRestrict config
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (2 preceding siblings ...)
2026-05-14 12:41 ` [PATCH v4 3/8] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 5/8] negotiator: add have_sent() interface Derrick Stolee via GitGitGadget
` (3 subsequent siblings)
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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.<name>.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.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/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.<name>.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 &&
(
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 5/8] negotiator: add have_sent() interface
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (3 preceding siblings ...)
2026-05-14 12:41 ` [PATCH v4 4/8] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 6/8] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
` (2 subsequent siblings)
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 6/8] fetch: add --negotiation-include option for negotiation
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (4 preceding siblings ...)
2026-05-14 12:41 ` [PATCH v4 5/8] negotiator: add have_sent() interface Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 7/8] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 8/8] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/fetch-options.adoc | 19 +++++++
builtin/fetch.c | 32 ++++++++---
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, 227 insertions(+), 18 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=(<commit>|<glob>)`::
+ 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..6b456b3689 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,28 @@ 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)
{
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;
}
@@ -1561,7 +1567,7 @@ static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
warning(_("ignoring %s=%s because it does not match any refs"),
"--negotiation-restrict", s);
}
- smart_options->negotiation_restrict_tips = oids;
+ *output_list = oids;
}
static struct transport *prepare_transport(struct remote *remote, int deepen,
@@ -1597,7 +1603,8 @@ 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);
else
warning(_("ignoring %s because the protocol does not support it"),
"--negotiation-restrict");
@@ -1606,7 +1613,8 @@ 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);
else {
struct strbuf config_name = STRBUF_INIT;
strbuf_addf(&config_name, "remote.%s.negotiationRestrict", remote->name);
@@ -1615,6 +1623,14 @@ 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);
+ else
+ warning(_("ignoring %s because the protocol does not support it"),
+ "--negotiation-include");
+ }
return transport;
}
@@ -2582,6 +2598,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
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 7/8] remote: add remote.*.negotiationInclude config
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (5 preceding siblings ...)
2026-05-14 12:41 ` [PATCH v4 6/8] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 8/8] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
Add a new 'remote.<name>.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.<name>.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.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
Documentation/config/remote.adoc | 27 ++++++++++++++++++
Documentation/fetch-options.adoc | 4 +++
builtin/fetch.c | 11 +++++++
remote.c | 5 ++++
remote.h | 1 +
t/t5510-fetch.sh | 49 ++++++++++++++++++++++++++++++++
6 files changed, 97 insertions(+)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index 4dcf81fbce..9ae20e4379 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc
@@ -125,6 +125,33 @@ values are not used.
Blank values signal to ignore all previous values, allowing a reset of
the list from broader config scenarios.
+remote.<name>.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.<name>.negotiationInclude` are sent
+unconditionally on top of those heuristically selected commits. This
+option is also used during push negotiation when `push.negotiate` is
+enabled.
++
+Blank values signal to ignore all previous values, allowing a reset of
+the list from broader config scenarios.
+
remote.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/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.<name>.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 6b456b3689..2308cab377 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1630,6 +1630,17 @@ 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);
+ } 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.<name>.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.<name>.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.<name>.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 &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
* [PATCH v4 8/8] send-pack: pass negotiation config in push
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
` (6 preceding siblings ...)
2026-05-14 12:41 ` [PATCH v4 7/8] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
@ 2026-05-14 12:41 ` Derrick Stolee via GitGitGadget
7 siblings, 0 replies; 54+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2026-05-14 12:41 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <stolee@gmail.com>
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.<name>.negotiationInclude' and
'remote.<name>.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.
This change also updates the use of --negotiation-tip into
--negotiation-restrict now that the new synonym exists.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
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 9ae20e4379..460b4e7952 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.
@@ -149,6 +152,9 @@ unconditionally on top of those heuristically selected commits. This
option is also used during push negotiation when `push.negotiate` is
enabled.
+
+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.<name>.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.<name>.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;
--
gitgitgadget
^ permalink raw reply related [flat|nested] 54+ messages in thread
end of thread, other threads:[~2026-05-14 12:41 UTC | newest]
Thread overview: 54+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-08 14:36 [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 1/4] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 2/4] fetch: add --must-have option for negotiation Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 3/4] remote: add mustHave config as default for --must-have Derrick Stolee via GitGitGadget
2026-04-08 14:36 ` [PATCH 4/4] send-pack: pass --must-have for push negotiation Derrick Stolee via GitGitGadget
2026-04-08 18:59 ` [PATCH 0/4] fetch: add --must-have and remote.*.mustHave Junio C Hamano
2026-04-09 12:53 ` Derrick Stolee
2026-04-15 15:14 ` [PATCH v2 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
2026-04-15 21:57 ` Junio C Hamano
2026-04-19 23:00 ` Derrick Stolee
2026-04-20 10:32 ` Junio C Hamano
2026-04-20 11:35 ` Derrick Stolee
2026-04-15 15:14 ` [PATCH v2 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
2026-04-20 8:11 ` Patrick Steinhardt
2026-04-15 15:14 ` [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
2026-04-15 19:16 ` Junio C Hamano
2026-04-15 15:14 ` [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation Derrick Stolee via GitGitGadget
2026-04-15 19:50 ` Junio C Hamano
2026-04-21 18:06 ` Derrick Stolee
2026-04-20 8:11 ` Patrick Steinhardt
2026-04-20 11:41 ` Derrick Stolee
2026-04-15 15:14 ` [PATCH v2 6/7] remote: add negotiationRequire config as default for --negotiation-require Derrick Stolee via GitGitGadget
2026-04-15 15:14 ` [PATCH v2 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 0/7] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-04-22 15:25 ` [PATCH v3 1/7] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-05-12 10:50 ` Matthew John Cheetham
2026-04-22 15:25 ` [PATCH v3 2/7] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
2026-05-12 11:11 ` Matthew John Cheetham
2026-05-12 14:23 ` Derrick Stolee
2026-04-22 15:25 ` [PATCH v3 3/7] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
2026-05-12 11:30 ` Matthew John Cheetham
2026-05-12 14:33 ` Derrick Stolee
2026-04-22 15:25 ` [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
2026-05-12 12:29 ` Matthew John Cheetham
2026-05-12 14:52 ` Derrick Stolee
2026-04-22 15:25 ` [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
2026-05-12 14:38 ` Matthew John Cheetham
2026-05-12 16:54 ` Derrick Stolee
2026-04-22 15:25 ` [PATCH v3 6/7] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
2026-05-12 14:54 ` Matthew John Cheetham
2026-05-12 17:55 ` Derrick Stolee
2026-04-22 15:25 ` [PATCH v3 7/7] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
2026-05-12 15:14 ` Matthew John Cheetham
2026-05-14 12:41 ` [PATCH v4 0/8] fetch: rework negotiation tip options Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 1/8] t5516: fix test order flakiness Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 2/8] fetch: add --negotiation-restrict option Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 3/8] transport: rename negotiation_tips Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 4/8] remote: add remote.*.negotiationRestrict config Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 5/8] negotiator: add have_sent() interface Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 6/8] fetch: add --negotiation-include option for negotiation Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 7/8] remote: add remote.*.negotiationInclude config Derrick Stolee via GitGitGadget
2026-05-14 12:41 ` [PATCH v4 8/8] send-pack: pass negotiation config in push Derrick Stolee via GitGitGadget
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox