git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v4 6/8] fetch: add --negotiation-include option for negotiation

From
Matthew John Cheetham <mjcheetham@outlook.com>
Date
May 18, 2026, 17:12 UTC
Message-ID
<VI0PR03MB1163443EECE427785282AB153C0032@VI0PR03MB11634.eurprd03.prod.outlook.com>
In-Reply-To
<b4cd458fe0b7625736348655c3aa704affc541c9.1778762495.git.gitgitgadget@gmail.com>
On 2026-05-14 13:41, Derrick Stolee via GitGitGadget wrote:
Show 28 quoted lines
> 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.
brances/branches from v3 fixed - thanks!
Show 8 quoted lines
> 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.
Now references the new have_sent() API - good!
Show 41 quoted lines
> 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`.
> +

Good - the syntax placeholder matches --negotiate-restrict, and we mention object hashes being valid input.

Show 57 quoted lines
>   `--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,

Great! Now we have a unified implementation that takes a string_list and ouputs an oid_array, used for both restrict and include lists. We pre-resolve so that fetch-pack gets the oid_array so the ref resolution happens in the same layer for both.

Just one small issue I see - the function emits a warning for "--negotiate-restrict" when we don't match a ref, even if this is handling a --negotiate-include value. Perhaps we should pass the option name in as a parameter?

   add_negotiation_tips(&negotiation_include,
                        "--negotiation-include",
                        &transport->smart_opt->negotiation_include_tips);
Show 34 quoted lines
> @@ -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;
>   }
Having the shared helper makes this much nicer. Good!
Show 23 quoted lines
> @@ -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,
LGTM
Show 31 quoted lines
> 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);
> +	}
> +}
> +

Nice - this is much simpler since ref resolution has been hoisted up and we only deal with pre-resolved OIDs.

Show 76 quoted lines
>   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)

Here we're using the new have_sent() API and replaces the manual COMMON bit flipping.

Show 54 quoted lines
> @@ -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;
>   }
The v2 wiring looks correct.
Show 88 quoted lines
> @@ -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
> +'
> +

Good! This checks for missing refs (not just invalid ones with all zeros).

Show 145 quoted lines
>   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

Overall this version of the patch is much better IMO! Just that one comment about the incorrect option name in the warning message.

Thanks, Matthew

Previous: Derrick Stolee via GitGitGadgetNext: Derrick Stolee via GitGitGadget
Message 58 of 86 in “fetch: add --must-have and remote.*.mustHave”
  1. 0/4 fetch: add --must-have and remote.*.mustHaveDerrick Stolee via GitGitGadget, Apr 8, 2026
  2. 1/4 t5516: fix test order flakinessDerrick Stolee via GitGitGadget, Apr 8, 2026
  3. 2/4 fetch: add --must-have option for negotiationDerrick Stolee via GitGitGadget, Apr 8, 2026
  4. 3/4 remote: add mustHave config as default for --must-haveDerrick Stolee via GitGitGadget, Apr 8, 2026
  5. 4/4 send-pack: pass --must-have for push negotiationDerrick Stolee via GitGitGadget, Apr 8, 2026
  6. Junio C HamanoApr 8, 2026
  7. Derrick StoleeApr 9, 2026
  8. 0/7 fetch: rework negotiation tip optionsDerrick Stolee via GitGitGadget, Apr 15, 2026
  9. 1/7 t5516: fix test order flakinessDerrick Stolee via GitGitGadget, Apr 15, 2026
  10. 2/7 fetch: add --negotiation-restrict optionDerrick Stolee via GitGitGadget, Apr 15, 2026
  11. Junio C HamanoApr 15, 2026
  12. Derrick StoleeApr 19, 2026
  13. Junio C HamanoApr 20, 2026
  14. Derrick StoleeApr 20, 2026
  15. 3/7 transport: rename negotiation_tipsDerrick Stolee via GitGitGadget, Apr 15, 2026
  16. Patrick SteinhardtApr 20, 2026
  17. 4/7 remote: add remote.*.negotiationRestrict configDerrick Stolee via GitGitGadget, Apr 15, 2026
  18. Junio C HamanoApr 15, 2026
  19. 5/7 fetch: add --negotiation-require option for negotiationDerrick Stolee via GitGitGadget, Apr 15, 2026
  20. Junio C HamanoApr 15, 2026
  21. Derrick StoleeApr 21, 2026
  22. Patrick SteinhardtApr 20, 2026
  23. Derrick StoleeApr 20, 2026
  24. 6/7 remote: add negotiationRequire config as default for --negotiation-requireDerrick Stolee via GitGitGadget, Apr 15, 2026
  25. 7/7 send-pack: pass negotiation config in pushDerrick Stolee via GitGitGadget, Apr 15, 2026
  26. 0/7 fetch: rework negotiation tip optionsDerrick Stolee via GitGitGadget, Apr 22, 2026
  27. 1/7 t5516: fix test order flakinessDerrick Stolee via GitGitGadget, Apr 22, 2026
  28. Matthew John CheethamMay 12, 2026
  29. 2/7 fetch: add --negotiation-restrict optionDerrick Stolee via GitGitGadget, Apr 22, 2026
  30. Matthew John CheethamMay 12, 2026
  31. Derrick StoleeMay 12, 2026
  32. 3/7 transport: rename negotiation_tipsDerrick Stolee via GitGitGadget, Apr 22, 2026
  33. Matthew John CheethamMay 12, 2026
  34. Derrick StoleeMay 12, 2026
  35. 4/7 remote: add remote.*.negotiationRestrict configDerrick Stolee via GitGitGadget, Apr 22, 2026
  36. Matthew John CheethamMay 12, 2026
  37. Derrick StoleeMay 12, 2026
  38. 5/7 fetch: add --negotiation-include option for negotiationDerrick Stolee via GitGitGadget, Apr 22, 2026
  39. Matthew John CheethamMay 12, 2026
  40. Derrick StoleeMay 12, 2026
  41. 6/7 remote: add remote.*.negotiationInclude configDerrick Stolee via GitGitGadget, Apr 22, 2026
  42. Matthew John CheethamMay 12, 2026
  43. Derrick StoleeMay 12, 2026
  44. 7/7 send-pack: pass negotiation config in pushDerrick Stolee via GitGitGadget, Apr 22, 2026
  45. Matthew John CheethamMay 12, 2026
  46. 0/8 fetch: rework negotiation tip optionsDerrick Stolee via GitGitGadget, May 14, 2026
  47. 1/8 t5516: fix test order flakinessDerrick Stolee via GitGitGadget, May 14, 2026
  48. Matthew John CheethamMay 18, 2026
  49. 2/8 fetch: add --negotiation-restrict optionDerrick Stolee via GitGitGadget, May 14, 2026
  50. Matthew John CheethamMay 18, 2026
  51. 3/8 transport: rename negotiation_tipsDerrick Stolee via GitGitGadget, May 14, 2026
  52. Matthew John CheethamMay 18, 2026
  53. 4/8 remote: add remote.*.negotiationRestrict configDerrick Stolee via GitGitGadget, May 14, 2026
  54. Matthew John CheethamMay 18, 2026
  55. 5/8 negotiator: add have_sent() interfaceDerrick Stolee via GitGitGadget, May 14, 2026
  56. Matthew John CheethamMay 18, 2026
  57. 6/8 fetch: add --negotiation-include option for negotiationDerrick Stolee via GitGitGadget, May 14, 2026
  58. Matthew John CheethamMay 18, 2026
  59. 7/8 remote: add remote.*.negotiationInclude configDerrick Stolee via GitGitGadget, May 14, 2026
  60. Matthew John CheethamMay 18, 2026
  61. 8/8 send-pack: pass negotiation config in pushDerrick Stolee via GitGitGadget, May 14, 2026
  62. Matthew John CheethamMay 18, 2026
  63. Matthew John CheethamMay 18, 2026
  64. Derrick StoleeMay 18, 2026
  65. 0/8 fetch: rework negotiation tip optionsDerrick Stolee via GitGitGadget, May 18, 2026
  66. 2/8 fetch: add --negotiation-restrict optionDerrick Stolee via GitGitGadget, May 18, 2026
  67. 3/8 transport: rename negotiation_tipsDerrick Stolee via GitGitGadget, May 18, 2026
  68. 4/8 remote: add remote.*.negotiationRestrict configDerrick Stolee via GitGitGadget, May 18, 2026
  69. 5/8 negotiator: add have_sent() interfaceDerrick Stolee via GitGitGadget, May 18, 2026
  70. 6/8 fetch: add --negotiation-include option for negotiationDerrick Stolee via GitGitGadget, May 18, 2026
  71. 7/8 remote: add remote.*.negotiationInclude configDerrick Stolee via GitGitGadget, May 18, 2026
  72. 8/8 send-pack: pass negotiation config in pushDerrick Stolee via GitGitGadget, May 18, 2026
  73. 1/8 t5516: fix test order flakinessDerrick Stolee via GitGitGadget, May 18, 2026
  74. Matthew John CheethamMay 19, 2026
  75. Derrick StoleeMay 19, 2026
  76. 0/8 fetch: rework negotiation tip optionsDerrick Stolee via GitGitGadget, May 19, 2026
  77. 1/8 t5516: fix test order flakinessDerrick Stolee via GitGitGadget, May 19, 2026
  78. 2/8 fetch: add --negotiation-restrict optionDerrick Stolee via GitGitGadget, May 19, 2026
  79. 3/8 transport: rename negotiation_tipsDerrick Stolee via GitGitGadget, May 19, 2026
  80. 4/8 remote: add remote.*.negotiationRestrict configDerrick Stolee via GitGitGadget, May 19, 2026
  81. 5/8 negotiator: add have_sent() interfaceDerrick Stolee via GitGitGadget, May 19, 2026
  82. 6/8 fetch: add --negotiation-include option for negotiationDerrick Stolee via GitGitGadget, May 19, 2026
  83. 7/8 remote: add remote.*.negotiationInclude configDerrick Stolee via GitGitGadget, May 19, 2026
  84. 8/8 send-pack: pass negotiation config in pushDerrick Stolee via GitGitGadget, May 19, 2026
  85. Matthew John CheethamMay 19, 2026
  86. Junio C HamanoMay 20, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.