Skip to contentSkip to conversation

fetch: add --must-have and remote.*.mustHave

PATCH Superseded Derrick Stolee via GitGitGadget 5 participants 86 messages Apr 8 – May 20 Documentation Page 1 of 2

lore
Share & tools
lore
#0 lore raw

cover fetch: add --must-have and remote.*.mustHave

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

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/4 t5516: fix test order flakiness

From: Derrick Stolee <[email protected]>

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.

---
 t/t5516-fetch-push.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
t/t5516-fetch-push.sh +1 -1
index 29e2f17608..ac8447f21e 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#2 lore raw

PATCH 2/4 fetch: add --must-have option for negotiation

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/fetch-options.adoc +18 -0
index 81a9d7f9bb..852e30191e 100644
@@ -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,
builtin/fetch.c +10 -1
index 4795b2a13c..5d29cc6b1a 100644
@@ -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),
fetch-pack.c +86 -6

199 lines not on this page · raw message

fetch-pack.h +9 -1
index 9d3470366f..2e97ca5ea2 100644
@@ -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
t/t5510-fetch.sh +73 -0

79 lines not on this page · raw message

transport.c +3 -1
index 107f4fa5dc..90923a640a 100644
@@ -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;
transport.h +6 -0
index 892f19454a..7f8d779e9b 100644
@@ -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.
Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 3/4 remote: add mustHave config as default for --must-have

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(+)
Documentation/config/remote.adoc +23 -0
index 91e46f66f5..9df8be27eb 100644
@@ -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.
Documentation/fetch-options.adoc +4 -0
index 852e30191e..fa3969d68d 100644
@@ -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
builtin/fetch.c +3 -0
index 5d29cc6b1a..fa491c106f 100644
@@ -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;
 }
remote.c +6 -0
index 7ca2a6501b..e07ec08fb3 100644
@@ -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"))
remote.h +1 -0
index fc052945ee..e125313f45 100644
@@ -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;
t/t5510-fetch.sh +48 -0

54 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#4 lore raw

PATCH 4/4 send-pack: pass --must-have for push negotiation

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 send-pack.c           | 12 +++++++++++-
 send-pack.h           |  1 +
 t/t5516-fetch-push.sh | 15 +++++++++++++++
 transport.c           |  1 +
 4 files changed, 28 insertions(+), 1 deletion(-)
send-pack.c +11 -1
index 67d6987b1c..baa52680bb 100644
@@ -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);
 	}
 
send-pack.h +1 -0
index c5ded2d200..194a1898e5 100644
@@ -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,
t/t5516-fetch-push.sh +15 -0
index ac8447f21e..9272609eac 100755
@@ -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 &&
 
transport.c +1 -0
index 90923a640a..e65f896ff3 100644
@@ -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;
Signature · 1 line
gitgitgadget
#5 lore raw
In reply to Derrick Stolee via GitGitGadget
Quoted 13 lines · Derrick Stolee via GitGitGadget

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:

Quoted 6 lines

* 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?

#6 lore raw
Quoted 22 lines · Junio C Hamano
Quoted 13 lines · Derrick Stolee via GitGitGadget

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:

Quoted 6 lines

* 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?

Quoted 9 lines

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

#7 lore raw

v2 cover fetch: rework negotiation tip options

In reply to Derrick Stolee via GitGitGadget

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 <[email protected]>
      
       ## 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 <[email protected]>
      
     @@ 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 <[email protected]>

## 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.

## 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 <[email protected]>
      
       ## 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 <[email protected]>
      
     @@ 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;
Signature · 1 line
gitgitgadget
#8 lore raw

PATCH v2 1/7 t5516: fix test order flakiness

From: Derrick Stolee <[email protected]>

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.

---
 t/t5516-fetch-push.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
t/t5516-fetch-push.sh +1 -1
index 29e2f17608..ac8447f21e 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#9 lore raw

PATCH v2 2/7 fetch: add --negotiation-restrict option

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/fetch-options.adoc +4 -0
index 81a9d7f9bb..c07b85499f 100644
@@ -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.
 +
builtin/fetch.c +7 -4
index 4795b2a13c..3bcb0c9686 100644
@@ -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)
builtin/pull.c +3 -0
index 7e67fdce97..821cc6699a 100644
@@ -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,
t/t5510-fetch.sh +25 -0
index 5dcb4b51a4..dc3ce56d84 100755
@@ -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 &&
 	(
t/t5702-protocol-v2.sh +2 -2
index f826ac46a5..9f6cf4142d 100755
@@ -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 \
Signature · 1 line
gitgitgadget
#14 lore raw

PATCH v2 3/7 transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
builtin/fetch.c +3 -3
index 3bcb0c9686..4c3c5f2faa 100644
@@ -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");
fetch-pack.c +9 -9
index 6ecd468ef7..baf239adf9 100644
@@ -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);
 
fetch-pack.h +2 -2
index 9d3470366f..6c70c942c2 100644
@@ -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[],
transport-helper.c +1 -1
index 4d95d84f9e..0e5b3b7202 100644
@@ -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)
transport.c +5 -5
index 107f4fa5dc..a3051f6733 100644
@@ -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);
transport.h +2 -2
index 892f19454a..cdeb33c16f 100644
@@ -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
Signature · 1 line
gitgitgadget
#16 lore raw

PATCH v2 4/7 remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/remote.adoc +16 -0
index 91e46f66f5..5e8ac6cfdd 100644
@@ -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.
builtin/fetch.c +22 -2
index 4c3c5f2faa..57b2b667ff 100644
@@ -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 {
remote.c +6 -0
index 7ca2a6501b..07cdf6434d 100644
@@ -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"))
remote.h +1 -0
index fc052945ee..e6ec37c393 100644
@@ -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;
t/t5510-fetch.sh +22 -0
index dc3ce56d84..0d87494794 100755
@@ -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 &&
 	(
Signature · 1 line
gitgitgadget
#18 lore raw

PATCH v2 5/7 fetch: add --negotiation-require option for negotiation

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/fetch-options.adoc +19 -0
index c07b85499f..85ffc5b32b 100644
@@ -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,
builtin/fetch.c +10 -0
index 57b2b667ff..b60652e6b1 100644
@@ -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),
builtin/pull.c +3 -0
index 821cc6699a..973186ecdc 100644
@@ -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,
fetch-pack.c +90 -6

203 lines not on this page · raw message

fetch-pack.h +9 -1
index 6c70c942c2..1daea8c542 100644
@@ -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
t/t5510-fetch.sh +66 -0

72 lines not on this page · raw message

transport.c +3 -1
index a3051f6733..d1b0e9eda0 100644
@@ -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;
transport.h +6 -0
index cdeb33c16f..8737f23008 100644
@@ -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.
Signature · 1 line
gitgitgadget
#23 lore raw

PATCH v2 6/7 remote: add negotiationRequire config as default for --negotiation-require

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(+)
Documentation/config/remote.adoc +24 -0
index 5e8ac6cfdd..9dbe820275 100644
@@ -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.
Documentation/fetch-options.adoc +4 -0
index 85ffc5b32b..16c6e8cee9 100644
@@ -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
builtin/fetch.c +10 -0
index b60652e6b1..a398115fb5 100644
@@ -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;
 }
remote.c +6 -0
index 07cdf6434d..53deed7565 100644
@@ -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"))
remote.h +1 -0
index e6ec37c393..d986257c78 100644
@@ -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;
t/t5510-fetch.sh +46 -0

52 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#24 lore raw

PATCH v2 7/7 send-pack: pass negotiation config in push

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 send-pack.c           | 34 ++++++++++++++++++++++++++++------
 send-pack.h           |  2 ++
 t/t5516-fetch-push.sh | 30 ++++++++++++++++++++++++++++++
 transport.c           |  2 ++
 4 files changed, 62 insertions(+), 6 deletions(-)
send-pack.c +28 -6
index 67d6987b1c..1bf17a73a9 100644
@@ -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);
 	}
 
send-pack.h +2 -0
index c5ded2d200..112f31121a 100644
@@ -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,
t/t5516-fetch-push.sh +30 -0
index ac8447f21e..03b797cef5 100755
@@ -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 &&
 
transport.c +2 -0
index d1b0e9eda0..9903eb1a53 100644
@@ -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;
Signature · 1 line
gitgitgadget
#17 lore raw

Re: [PATCH v2 4/7] remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget
Quoted 36 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/remote.adoc +5 -0
index 91e46f66f5..5e8ac6cfdd 100644
@@ -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

Documentation/config/remote.adoc priority configuration file (e.g. `.git/config` in a repository) to clear · line 124

Quoted 10 lines
Documentation/config/remote.adoc +10 -0
+	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-<.

#19 lore raw

Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation

In reply to Derrick Stolee via GitGitGadget
Quoted 8 lines · Derrick Stolee via GitGitGadget

+--negotiation-require=<revision>::

Documentation/fetch-options.adoc +7 -0
+	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.
++

Documentation/fetch-options.adoc See also the `fetch.negotiationAlgorithm` and `push.negotiate` · line 93

Quoted 10 lines
Documentation/fetch-options.adoc +10 -0
+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.

builtin/fetch.c static struct transport *gsecondary; · line 102

Quoted 9 lines
builtin/fetch.c +1 -0
index 57b2b667ff..b60652e6b1 100644
@@ -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]?

builtin/fetch.c static struct transport *gsecondary;

Quoted 5 lines
builtin/fetch.c +5 -0
+	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?

builtin/fetch.c

Quoted 2 lines
builtin/fetch.c +2 -0
+			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).

Quoted 8 lines
builtin/fetch.c +4 -0
@@ -474,7 +511,25 @@ static int find_common(struct fetch_negotiator *negotiator,
 	trace2_region_enter("fetch-pack", "negotiation_v0_v1", the_repository);
 	flushes = 0;
 	retval = -1;
+
+	/* Send unconditional haves from --negotiation-require */
+	resolve_negotiation_require(args->negotiation_require,
+				    &negotiation_require_oids);

builtin/fetch.c static int find_common(struct fetch_negotiator *negotiator, · line 527

Quoted 10 lines
builtin/fetch.c +10 -0
+	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).

builtin/fetch.c static int find_common(struct fetch_negotiator *negotiator, · line 531

Quoted 4 lines
builtin/fetch.c +3 -0
 	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?

#10 lore raw

Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option

In reply to Derrick Stolee via GitGitGadget

builtin/fetch.c static void add_negotiation_tips(struct git_transport_options *smart_options)

Quoted 7 lines · Derrick Stolee via GitGitGadget
builtin/fetch.c +4 -3
-			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.

builtin/fetch.c int cmd_fetch(int argc, · line 2572

Quoted 9 lines
builtin/fetch.c +2 -0
 	}
 	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?

builtin/fetch.c int cmd_fetch(int argc, · line 2663

Quoted 6 lines
builtin/fetch.c +1 -1
@@ -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?

#11 lore raw

Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option

Quoted 13 lines · Junio C Hamano

builtin/fetch.c static void add_negotiation_tips(struct git_transport_options *smart_options)

Quoted 7 lines · Derrick Stolee via GitGitGadget
builtin/fetch.c +4 -3
-			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.

Quoted 8 lines

builtin/fetch.c int cmd_fetch(int argc, · line 2663

Quoted 6 lines
builtin/fetch.c +1 -1
@@ -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.

Quoted 11 lines

builtin/fetch.c int cmd_fetch(int argc, · line 2572

Quoted 9 lines
builtin/fetch.c +2 -0
  	}
  	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

#15 lore raw

Re: [PATCH v2 3/7] transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget

builtin/fetch.c

Quoted 4 lines · Derrick Stolee via GitGitGadget
builtin/fetch.c +0 -0
index 3bcb0c9686..4c3c5f2faa 100644

Don't we want to also rename the local negotiation_tip variable in cmd_fetch()?

Patrick

#21 lore raw

Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation

In reply to Derrick Stolee via GitGitGadget
Quoted 5 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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?

Quoted 16 lines
Documentation/fetch-options.adoc +8 -0
index c07b85499f..85ffc5b32b 100644
@@ -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.
++

Documentation/fetch-options.adoc See also the `fetch.negotiationAlgorithm` and `push.negotiate` · line 93

Quoted 10 lines
Documentation/fetch-options.adoc +10 -0
+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".

Quoted 12 lines
fetch-pack.c +4 -0
index baf239adf9..a0029253f1 100644
@@ -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);

fetch-pack.c static int find_common(struct fetch_negotiator *negotiator, · line 527

Quoted 10 lines
fetch-pack.c +10 -0
+	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

#12 lore raw

Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option

In reply to Derrick Stolee
Quoted 7 lines · Derrick Stolee
Quoted 6 lines

builtin/fetch.c int cmd_fetch(int argc, · line 2572

Quoted 4 lines

OPT_STRING_LIST(0, "negotiation-tip", &negotiation_tip, N_("revision"),
N_("report that we have only objects reachable from this object")),

builtin/fetch.c +2 -0
+		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?

Quoted 5 lines

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)).

#13 lore raw

Re: [PATCH v2 2/7] fetch: add --negotiation-restrict option

Quoted 16 lines · Junio C Hamano
Quoted 7 lines · Derrick Stolee
Quoted 6 lines

builtin/fetch.c int cmd_fetch(int argc, · line 2572

Quoted 4 lines

OPT_STRING_LIST(0, "negotiation-tip", &negotiation_tip, N_("revision"),
N_("report that we have only objects reachable from this object")),

builtin/fetch.c +2 -0
+		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?

Quoted 5 lines

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.

...

Quoted 10 lines

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

#22 lore raw

Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation

In reply to Patrick Steinhardt
Quoted 7 lines · Patrick Steinhardt
Quoted 5 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 7 lines

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.

Quoted 16 lines

fetch-pack.c static int find_common(struct fetch_negotiator *negotiator, · line 527

Quoted 13 lines
fetch-pack.c +13 -0
+	/* 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.

Quoted 5 lines

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

#20 lore raw

Re: [PATCH v2 5/7] fetch: add --negotiation-require option for negotiation

In reply to Junio C Hamano
Quoted 1 line · Junio C Hamano

"Derrick Stolee via GitGitGadget" <[email protected]> writes:

...

Quoted 6 lines
Quoted 1 line

+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.

Quoted 10 lines

fetch-pack.c static void send_filter(struct fetch_pack_args *args, · line 356

Quoted 5 lines
fetch-pack.c +5 -0
+	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.

Quoted 6 lines

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.

Quoted 6 lines

fetch-pack.c static void send_filter(struct fetch_pack_args *args, · line 358

Quoted 2 lines
fetch-pack.c +2 -0
+			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).

Quoted 7 lines

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.

Quoted 16 lines
Quoted 16 lines
fetch-pack.c +12 -0
@@ -474,7 +511,25 @@ static int find_common(struct fetch_negotiator *negotiator,
  	trace2_region_enter("fetch-pack", "negotiation_v0_v1", the_repository);
  	flushes = 0;
  	retval = -1;
+
+	/* Send unconditional haves from --negotiation-require */
+	resolve_negotiation_require(args->negotiation_require,
+				    &negotiation_require_oids);
+	if (oidset_size(&negotiation_require_oids)) {
+		struct oidset_iter iter;
+		oidset_iter_init(&negotiation_require_oids, &iter);
+
+		while ((oid = oidset_iter_next(&iter))) {
+			packet_buf_write(&req_buf, "have %s\n",
+					 oid_to_hex(oid));
+			print_verbose(args, "have %s", oid_to_hex(oid));
Quoted 8 lines

Review static int find_common(struct fetch_negotiator *negotiator, · line 527

Quoted 2 lines
(quoted diff) +2 -0
+		}
+	}

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.

Quoted 5 lines

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.

Quoted 7 lines

fetch-pack.c static int find_common(struct fetch_negotiator *negotiator, · line 532

Quoted 4 lines
fetch-pack.c +3 -0
  	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

#25 lore raw

v3 cover fetch: rework negotiation tip options

In reply to Derrick Stolee via GitGitGadget

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.
+

## 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 <[email protected]>
      
       ## 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 <[email protected]>
      
       ## 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 <[email protected]>
      
     @@ 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 <[email protected]>

## 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.

## 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)
Signature · 1 line
gitgitgadget
#26 lore raw

PATCH v3 1/7 t5516: fix test order flakiness

From: Derrick Stolee <[email protected]>

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.

---
 t/t5516-fetch-push.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
t/t5516-fetch-push.sh +1 -1
index 29e2f17608..ac8447f21e 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#28 lore raw

PATCH v3 2/7 fetch: add --negotiation-restrict option

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/fetch-options.adoc +4 -0
index 81a9d7f9bb..c07b85499f 100644
@@ -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.
 +
builtin/fetch.c +8 -5
index 4795b2a13c..fc950fe35b 100644
@@ -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)
builtin/pull.c +3 -0
index 7e67fdce97..821cc6699a 100644
@@ -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,
t/t5510-fetch.sh +25 -0
index 5dcb4b51a4..dc3ce56d84 100755
@@ -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 &&
 	(
t/t5702-protocol-v2.sh +2 -2
index f826ac46a5..9f6cf4142d 100755
@@ -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 \
Signature · 1 line
gitgitgadget
#31 lore raw

PATCH v3 3/7 transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
builtin/fetch.c +9 -9
index fc950fe35b..2ba0051d52 100644
@@ -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=*");
 
fetch-pack.c +9 -9
index 6ecd468ef7..baf239adf9 100644
@@ -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);
 
fetch-pack.h +2 -2
index 9d3470366f..6c70c942c2 100644
@@ -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[],
transport-helper.c +1 -1
index 4d95d84f9e..0e5b3b7202 100644
@@ -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)
transport.c +5 -5
index 107f4fa5dc..a3051f6733 100644
@@ -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);
transport.h +2 -2
index 892f19454a..cdeb33c16f 100644
@@ -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
Signature · 1 line
gitgitgadget
#34 lore raw

PATCH v3 4/7 remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/remote.adoc +19 -0
index 91e46f66f5..f1d889d03e 100644
@@ -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.
builtin/fetch.c +17 -4
index 2ba0051d52..a1960e3e0c 100644
@@ -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 {
remote.c +8 -0
index 7ca2a6501b..166a56408a 100644
@@ -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"))
remote.h +1 -0
index fc052945ee..e6ec37c393 100644
@@ -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;
t/t5510-fetch.sh +26 -0
index dc3ce56d84..eff3ce8e2d 100755
@@ -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 &&
 	(
Signature · 1 line
gitgitgadget
#37 lore raw

PATCH v3 5/7 fetch: add --negotiation-include option for negotiation

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/fetch-options.adoc +19 -0
index c07b85499f..decc7f6abd 100644
@@ -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,
builtin/fetch.c +15 -1
index a1960e3e0c..ef50e2fbe9 100644
@@ -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),
builtin/pull.c +3 -0
index 821cc6699a..86c85b60ef 100644
@@ -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,
fetch-pack.c +106 -6

218 lines not on this page · raw message

fetch-pack.h +9 -1
index 6c70c942c2..32ae94d0b4 100644
@@ -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
t/t5510-fetch.sh +66 -0

72 lines not on this page · raw message

transport.c +3 -1
index a3051f6733..8a2d8adffc 100644
@@ -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;
transport.h +6 -0
index cdeb33c16f..6092775a27 100644
@@ -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.
Signature · 1 line
gitgitgadget
#40 lore raw

PATCH v3 6/7 remote: add remote.*.negotiationInclude config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(+)
Documentation/config/remote.adoc +27 -0
index f1d889d03e..44de6d3c1f 100644
@@ -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.
Documentation/fetch-options.adoc +4 -0
index decc7f6abd..c475932602 100644
@@ -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
builtin/fetch.c +10 -0
index ef50e2fbe9..827438cf98 100644
@@ -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;
 }
remote.c +8 -0
index 166a56408a..15f3f12184 100644
@@ -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"))
remote.h +1 -0
index e6ec37c393..d8809b6991 100644
@@ -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;
t/t5510-fetch.sh +49 -0

55 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#43 lore raw

PATCH v3 7/7 send-pack: pass negotiation config in push

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 send-pack.c           | 39 +++++++++++++++++++++++++++++++--------
 send-pack.h           |  2 ++
 t/t5516-fetch-push.sh | 30 ++++++++++++++++++++++++++++++
 transport.c           |  2 ++
 4 files changed, 65 insertions(+), 8 deletions(-)
send-pack.c +31 -8
index 67d6987b1c..d18e030ce8 100644
@@ -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);
 	}
 
send-pack.h +2 -0
index c5ded2d200..13850c98bb 100644
@@ -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,
t/t5516-fetch-push.sh +30 -0
index ac8447f21e..177cbc6c75 100755
@@ -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 &&
 
transport.c +2 -0
index 8a2d8adffc..60b73feb34 100644
@@ -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;
Signature · 1 line
gitgitgadget
#27 lore raw

Re: [PATCH v3 1/7] t5516: fix test order flakiness

In reply to Derrick Stolee via GitGitGadget
Quoted 18 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
  t/t5516-fetch-push.sh | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
t/t5516-fetch-push.sh +0 -0
index 29e2f17608..ac8447f21e 100755

t/t5516-fetch-push.sh test_expect_success 'fetch follows tags by default' ' · line 1355

Quoted 8 lines
t/t5516-fetch-push.sh +1 -1
@@ -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

#29 lore raw

Re: [PATCH v3 2/7] fetch: add --negotiation-restrict option

In reply to Derrick Stolee via GitGitGadget
Quoted 14 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 5 lines

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.

Quoted 3 lines

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!

Quoted 22 lines
---
  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(-)
Documentation/fetch-options.adoc +1 -0
index 81a9d7f9bb..c07b85499f 100644
@@ -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

Documentation/fetch-options.adoc the current repository has the same history as the source repository. · line 67

Quoted 9 lines
Documentation/fetch-options.adoc +3 -0
@@ -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 :-)

Quoted 5 lines
builtin/fetch.c +0 -0
index 4795b2a13c..fc950fe35b 100644

builtin/fetch.c static void add_negotiation_tips(struct git_transport_options *smart_options) · line 1565

Quoted 10 lines
builtin/fetch.c +2 -2
@@ -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.

builtin/fetch.c static struct transport *prepare_transport(struct remote *remote, int deepen, · line 1606

Quoted 10 lines
builtin/fetch.c +2 -1
@@ -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.

builtin/fetch.c int cmd_fetch(int argc, · line 2574

Quoted 11 lines
builtin/fetch.c +2 -1
@@ -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.

builtin/fetch.c int cmd_fetch(int argc, · line 2666

Quoted 10 lines
builtin/fetch.c +2 -1
@@ -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!

Quoted 5 lines
builtin/pull.c +0 -0
index 7e67fdce97..821cc6699a 100644

builtin/pull.c int cmd_pull(int argc, · line 1007

Quoted 9 lines
builtin/pull.c +3 -0
@@ -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.

Quoted 47 lines
t/t5510-fetch.sh +25 -0
index 5dcb4b51a4..dc3ce56d84 100755
@@ -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 &&
  	(
t/t5702-protocol-v2.sh +1 -1
index f826ac46a5..9f6cf4142d 100755
@@ -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" &&

t/t5702-protocol-v2.sh setup_negotiate_only () { · line 882

Quoted 10 lines
t/t5702-protocol-v2.sh +1 -1
  	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

#32 lore raw

Re: [PATCH v3 3/7] transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget
Quoted 7 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 22 lines

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.

---
  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(-)
builtin/fetch.c +0 -0
index fc950fe35b..2ba0051d52 100644

builtin/fetch.c static struct transport *gtransport; · line 101

Quoted 5 lines
builtin/fetch.c +1 -1
@@ -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.

Quoted 9 lines
builtin/fetch.c +1 -1
  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)
  {

builtin/fetch.c static int add_oid(const struct reference *ref, void *cb_data) · line 1546

Quoted 10 lines
builtin/fetch.c +2 -2
  	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.

Quoted 126 lines
builtin/fetch.c +5 -5
@@ -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=*");
 
fetch-pack.c +9 -9
index 6ecd468ef7..baf239adf9 100644
@@ -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);
 
fetch-pack.h +1 -1
index 9d3470366f..6c70c942c2 100644
@@ -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;

fetch-pack.h struct ref *fetch_pack(struct fetch_pack_args *args, · line 95

Quoted 8 lines
fetch-pack.h +1 -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.

transport-helper.c static int fetch_refs(struct transport *transport, · line 760

Quoted 13 lines
transport-helper.c +1 -1
index 4d95d84f9e..0e5b3b7202 100644
@@ -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?

Quoted 45 lines
transport.c +5 -5
index 107f4fa5dc..a3051f6733 100644
@@ -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);
transport.h +1 -1
index 892f19454a..cdeb33c16f 100644
@@ -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.

transport.h struct git_transport_options { · line 52

Quoted 10 lines
transport.h +1 -1
  	 *
  	 * 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

#35 lore raw

Re: [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget
Quoted 6 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee<[email protected]>

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.

Quoted 39 lines

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.

---
  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(-)
Documentation/config/remote.adoc +12 -0
index 91e46f66f5..f1d889d03e 100644
@@ -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`

Documentation/config/remote.adoc priority configuration file (e.g. `.git/config` in a repository) to clear · line 131

Quoted 10 lines
Documentation/config/remote.adoc +7 -0
+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?

Quoted 14 lines
builtin/fetch.c +6 -0
index 2ba0051d52..a1960e3e0c 100644
@@ -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);

builtin/fetch.c static struct transport *prepare_transport(struct remote *remote, int deepen, · line 1619

Quoted 10 lines
builtin/fetch.c +7 -0
+		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.

Quoted 12 lines
builtin/fetch.c +0 -4
@@ -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"));

builtin/fetch.c int cmd_fetch(int argc, · line 2767

Quoted 10 lines
builtin/fetch.c +4 -0
@@ -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=*");
Quoted 23 lines
remote.c +2 -0
index 7ca2a6501b..166a56408a 100644
@@ -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.c static int handle_config(const char *key, const char *value, · line 575

Quoted 10 lines
remote.c +6 -0
  					      &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?

Quoted 41 lines
remote.h +1 -0
index fc052945ee..e6ec37c393 100644
@@ -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;
t/t5510-fetch.sh +20 -0
index dc3ce56d84..eff3ce8e2d 100755
@@ -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 \

t/t5510-fetch.sh test_expect_success '--negotiation-restrict and --negotiation-tip can be mixed'

Quoted 10 lines
t/t5510-fetch.sh +6 -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

#30 lore raw

Re: [PATCH v3 2/7] fetch: add --negotiation-restrict option

In reply to Matthew John Cheetham
Quoted 29 lines · Matthew John Cheetham
Quoted 15 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

Documentation/fetch-options.adoc +0 -0
@@ -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

Documentation/fetch-options.adoc +0 -0
@@ -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

Quoted 8 lines

      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):

Quoted 7 lines

 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.

Quoted 10 lines
Quoted 10 lines
builtin/pull.c +2 -0
index 7e67fdce97..821cc6699a 100644
@@ -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"),
Quoted 8 lines

builtin/pull.c int cmd_pull(int argc, · line 1007

Quoted 4 lines
builtin/pull.c +1 -0
+            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

#33 lore raw

Re: [PATCH v3 3/7] transport: rename negotiation_tips

In reply to Matthew John Cheetham
Quoted 11 lines · Matthew John Cheetham
Quoted 7 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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!

Quoted 16 lines

transport-helper.c static int fetch_refs(struct transport *transport, · line 758

Quoted 11 lines
transport-helper.c +1 -1
index 4d95d84f9e..0e5b3b7202 100644
@@ -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.

Quoted 1 line

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.

Quoted 3 lines

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

#38 lore raw

Re: [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation

In reply to Derrick Stolee via GitGitGadget
Quoted 20 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 8 lines

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/

Quoted 3 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.

Calling out the intent for the behaviour change (non-existent refs are silently ignored). This is an important point.

Quoted 38 lines

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.

---
  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(-)
Documentation/fetch-options.adoc +9 -0
index c07b85499f..decc7f6abd 100644
@@ -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

Documentation/fetch-options.adoc See also the `fetch.negotiationAlgorithm` and `push.negotiate` · line 94

Quoted 10 lines
Documentation/fetch-options.adoc +10 -0
+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.

Quoted 21 lines
Documentation/fetch-options.adoc +0 -0
  `--negotiate-only`::
  	Do not fetch anything from the server, and instead print the
  	ancestors of the provided `--negotiation-tip=` arguments,
builtin/fetch.c +3 -0
index a1960e3e0c..ef50e2fbe9 100644
@@ -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. */

builtin/fetch.c static void add_negotiation_restrict_tips(struct git_transport_options *smart_op · line 1561

Quoted 10 lines
builtin/fetch.c +3 -1
  			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)?

builtin/fetch.c static struct transport *prepare_transport(struct remote *remote, int deepen, · line 1631

Quoted 13 lines
builtin/fetch.c +7 -0
@@ -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.

Quoted 71 lines
builtin/fetch.c +2 -0
@@ -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),
builtin/pull.c +3 -0
index 821cc6699a..86c85b60ef 100644
@@ -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,
fetch-pack.c +33 -0
index baf239adf9..8b080b0080 100644
@@ -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 {

fetch-pack.c static void send_filter(struct fetch_pack_args *args, · line 377

Quoted 10 lines
fetch-pack.c +10 -0
+			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.

Quoted 32 lines
fetch-pack.c +18 -0
  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

fetch-pack.c static int find_common(struct fetch_negotiator *negotiator, · line 547

Quoted 10 lines
fetch-pack.c +10 -0
+			 * 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

#36 lore raw

Re: [PATCH v3 4/7] remote: add remote.*.negotiationRestrict config

In reply to Matthew John Cheetham
Quoted 11 lines · Matthew John Cheetham
Quoted 6 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee<[email protected]>

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.

Quoted 34 lines
Quoted 19 lines
Documentation/config/remote.adoc +19 -0
+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.
+
Quoted 3 lines

  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.

Quoted 4 lines

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'.

Quoted 3 lines

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'.

Quoted 32 lines
Quoted 4 lines

      if (deepen_relative) {
          if (deepen_relative < 0)
              die(_("negative depth in --deepen is not supported"));

builtin/fetch.c int cmd_fetch(int argc, · line 2767

Quoted 10 lines
builtin/fetch.c +4 -0
@@ -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=*

Quoted 8 lines

..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.

Quoted 19 lines

remote.c static int handle_config(const char *key, const char *value, · line 575

Quoted 13 lines
remote.c +6 -0
@@ -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:

Quoted 7 lines

  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

#41 lore raw

Re: [PATCH v3 6/7] remote: add remote.*.negotiationInclude config

In reply to Derrick Stolee via GitGitGadget
Quoted 23 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 8 lines
---
  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

Quoted 10 lines
Documentation/config/remote.adoc +2 -0
index f1d889d03e..44de6d3c1f 100644
@@ -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`,

Documentation/config/remote.adoc values are not used. · line 140

Quoted 10 lines
Documentation/config/remote.adoc +10 -0
+	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.

Quoted 15 lines
t/t5510-fetch.sh +7 -0
index 4316f8d4ea..db73ed5379 100755
@@ -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 "" &&

t/t5510-fetch.sh test_expect_success '--negotiation-include avoids duplicates with negotiator' ' · line 1596

Quoted 10 lines
t/t5510-fetch.sh +10 -0
+	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.

Quoted 24 lines
t/t5510-fetch.sh +24 -0
+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 &&

t/t5510-fetch.sh test_expect_success '--negotiation-include avoids duplicates with negotiator' ' · line 1631

Quoted 10 lines
t/t5510-fetch.sh +7 -0
+
+	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

#44 lore raw

Re: [PATCH v3 7/7] send-pack: pass negotiation config in push

In reply to Derrick Stolee via GitGitGadget
Quoted 19 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 7 lines
---
  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.

Quoted 50 lines
send-pack.c +23 -6
index 67d6987b1c..d18e030ce8 100644
@@ -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);

send-pack.c static void reject_invalid_nonce(const char *nonce, int len) · line 480

Quoted 10 lines
send-pack.c +4 -1
+		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!

Quoted 71 lines
send-pack.c +4 -1
@@ -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);
  	}
 
send-pack.h +2 -0
index c5ded2d200..13850c98bb 100644
@@ -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,
t/t5516-fetch-push.sh +30 -0
index ac8447f21e..177cbc6c75 100755
@@ -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 &&
 
transport.c +0 -0
index 8a2d8adffc..60b73feb34 100644

transport.c static int git_transport_push(struct transport *transport, struct ref *remote_re · line 928

Quoted 8 lines
transport.c +2 -0
@@ -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

#39 lore raw

Re: [PATCH v3 5/7] fetch: add --negotiation-include option for negotiation

In reply to Matthew John Cheetham
Quoted 30 lines · Matthew John Cheetham
Quoted 20 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 8 lines

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.

Quoted 3 lines

Nice explanation and motivation for the need of such as feature.

One small typo: s/brances/branches/

Thanks.

Quoted 20 lines
Quoted 20 lines
Documentation/fetch-options.adoc +0 -0
@@ -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. ++

Documentation/fetch-options.adoc +7 -0
+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`.
++
Quoted 8 lines

Documentation/fetch-options.adoc See also the `fetch.negotiationAlgorithm` and `push.negotiate` · line 94

Quoted 4 lines
Documentation/fetch-options.adoc +4 -0
+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.

Quoted 2 lines

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.

Quoted 21 lines
Quoted 9 lines
Documentation/fetch-options.adoc +0 -0
@@ -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);

Quoted 8 lines

+                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.

Quoted 7 lines

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.

Quoted 25 lines
Quoted 14 lines
Documentation/fetch-options.adoc +7 -0
@@ -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;
Quoted 1 line

  }

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."

Quoted 8 lines

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.

Quoted 31 lines
Quoted 31 lines

+static void resolve_negotiation_include(const struct string_list *negotiation_include,
+                    struct oidset *result)

Documentation/fetch-options.adoc +28 -0
+{
+    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);
+        }
Quoted 8 lines

Documentation/fetch-options.adoc

Quoted 3 lines

+    }

Documentation/fetch-options.adoc +2 -0
+}
+

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.

Quoted 70 lines
Quoted 31 lines

  static int find_common(struct fetch_negotiator *negotiator, struct fetch_pack_args *args,
                 int fd[2], struct object_id *result_oid,

Documentation/fetch-options.adoc +18 -0
@@ -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

Documentation/fetch-options.adoc static int find_common(struct fetch_negotiator *negotiator, · line 547

Quoted 10 lines
Documentation/fetch-options.adoc +10 -0
+             * 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", ...); }

Quoted 6 lines

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

#42 lore raw

Re: [PATCH v3 6/7] remote: add remote.*.negotiationInclude config

In reply to Matthew John Cheetham
Quoted 8 lines · Matthew John Cheetham

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.

Quoted 22 lines
Quoted 9 lines
Documentation/config/remote.adoc +0 -0
index f1d889d03e..44de6d3c1f 100644
@@ -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.

Documentation/config/remote.adoc +2 -0
+remote.<name>.negotiationInclude::
+    When negotiating with this remote during `git fetch` and `git push`,

Documentation/config/remote.adoc values are not used. · line 140

Quoted 10 lines
Documentation/config/remote.adoc +10 -0
+    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"?

Quoted 2 lines

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.

Quoted 27 lines
Quoted 27 lines
t/t5510-fetch.sh +0 -0
index 4316f8d4ea..db73ed5379 100755
@@ -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

Quoted 8 lines
Quoted 1 line

+'

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

#45 lore raw

v4 cover fetch: rework negotiation tip options

In reply to Derrick Stolee via GitGitGadget

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.

2: fe875399a8 ! 2: 7836a2d6a5 fetch: add --negotiation-restrict option
@@ Commit message

+ ## 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

+ ## 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 {

Signature · 1 line
gitgitgadget
#46 lore raw

PATCH v4 1/8 t5516: fix test order flakiness

From: Derrick Stolee <[email protected]>

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.

---
 t/t5516-fetch-push.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
t/t5516-fetch-push.sh +1 -1
index 29e2f17608..ac8447f21e 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#48 lore raw

PATCH v4 2/8 fetch: add --negotiation-restrict option

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/fetch.adoc +1 -1
index cd40db0cad..04ac90912d 100644
@@ -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`::
Documentation/fetch-options.adoc +5 -1
index 81a9d7f9bb..d39cecb446 100644
@@ -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)`.
builtin/fetch.c +8 -5
index 4795b2a13c..fc950fe35b 100644
@@ -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)
builtin/pull.c +2 -1
index 7e67fdce97..cc6ce485fc 100644
@@ -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,
send-pack.c +1 -1
index 67d6987b1c..3d5d36ba3b 100644
@@ -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++;
 		}
t/t5510-fetch.sh +25 -0
index 5dcb4b51a4..dc3ce56d84 100755
@@ -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 &&
 	(
t/t5702-protocol-v2.sh +2 -2
index f826ac46a5..9f6cf4142d 100755
@@ -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 \
transport-helper.c +2 -1
index 4d95d84f9e..dd78d40668 100644
@@ -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);
Signature · 1 line
gitgitgadget
#50 lore raw

PATCH v4 3/8 transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
builtin/fetch.c +9 -9
index fc950fe35b..2ba0051d52 100644
@@ -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=*");
 
fetch-pack.c +9 -9
index 6ecd468ef7..baf239adf9 100644
@@ -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);
 
fetch-pack.h +2 -2
index 9d3470366f..6c70c942c2 100644
@@ -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[],
transport-helper.c +1 -1
index dd78d40668..f4388da766 100644
@@ -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");
 
transport.c +5 -5
index 107f4fa5dc..a3051f6733 100644
@@ -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);
transport.h +2 -2
index 892f19454a..cdeb33c16f 100644
@@ -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
Signature · 1 line
gitgitgadget
#52 lore raw

PATCH v4 4/8 remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/remote.adoc +18 -0
index 91e46f66f5..4dcf81fbce 100644
@@ -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.
builtin/fetch.c +21 -7
index 2ba0051d52..a957739f37 100644
@@ -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);
remote.c +5 -0
index 7ca2a6501b..620086e16e 100644
@@ -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"))
remote.h +1 -0
index fc052945ee..e6ec37c393 100644
@@ -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;
t/t5510-fetch.sh +26 -0
index dc3ce56d84..eff3ce8e2d 100755
@@ -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 &&
 	(
Signature · 1 line
gitgitgadget
#54 lore raw

PATCH v4 5/8 negotiator: add have_sent() interface

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 fetch-negotiator.h    | 9 +++++++++
 negotiator/default.c  | 8 ++++++++
 negotiator/noop.c     | 7 +++++++
 negotiator/skipping.c | 8 ++++++++
 4 files changed, 32 insertions(+)
fetch-negotiator.h +9 -0
index e348905a1f..6ca422a064 100644
@@ -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 */
negotiator/default.c +8 -0
index 116dedcf83..05ab616f39 100644
@@ -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;
negotiator/noop.c +7 -0
index 65e3c20008..edf1b456f3 100644
@@ -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;
 }
negotiator/skipping.c +8 -0
index 0a272130fb..69472c58e1 100644
@@ -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;
Signature · 1 line
gitgitgadget
#56 lore raw

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

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/fetch-options.adoc +19 -0
index d39cecb446..7b897a7202 100644
@@ -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,
builtin/fetch.c +25 -7
index a957739f37..6b456b3689 100644
@@ -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),
builtin/pull.c +3 -0
index cc6ce485fc..d49b09114a 100644
@@ -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,
fetch-pack.c +75 -6

186 lines not on this page · raw message

fetch-pack.h +4 -2
index 6c70c942c2..6d0dec7f41 100644
@@ -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
t/t5510-fetch.sh +91 -0

103 lines not on this page · raw message

transport.c +7 -1
index a3051f6733..fa54928966 100644
@@ -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);
transport.h +3 -2
index cdeb33c16f..97d905ecc0 100644
@@ -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
Signature · 1 line
gitgitgadget
#58 lore raw

PATCH v4 7/8 remote: add remote.*.negotiationInclude config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(+)
Documentation/config/remote.adoc +27 -0
index 4dcf81fbce..9ae20e4379 100644
@@ -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.
Documentation/fetch-options.adoc +4 -0
index 7b897a7202..8074004377 100644
@@ -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
builtin/fetch.c +11 -0
index 6b456b3689..2308cab377 100644
@@ -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;
 }
remote.c +5 -0
index 620086e16e..6fb5758820 100644
@@ -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"))
remote.h +1 -0
index e6ec37c393..d8809b6991 100644
@@ -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;
t/t5510-fetch.sh +49 -0

55 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#60 lore raw

PATCH v4 8/8 send-pack: pass negotiation config in push

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/remote.adoc +6 -0
index 9ae20e4379..460b4e7952 100644
@@ -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.
 
send-pack.c +30 -7
index 3d5d36ba3b..d18e030ce8 100644
@@ -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);
 	}
 
send-pack.h +2 -0
index c5ded2d200..13850c98bb 100644
@@ -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,
t/t5516-fetch-push.sh +30 -0
index ac8447f21e..177cbc6c75 100755
@@ -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 &&
 
transport.c +2 -0
index fa54928966..a2d8958cb8 100644
@@ -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;
Signature · 1 line
gitgitgadget
#47 lore raw

Re: [PATCH v4 1/8] t5516: fix test order flakiness

In reply to Derrick Stolee via GitGitGadget
Quoted 9 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Expanding the message to back reference 3f763ddf28 is a nice change. Makes it easier for future people reading the history to follow back.

Quoted 10 lines
---
  t/t5516-fetch-push.sh | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
t/t5516-fetch-push.sh +0 -0
index 29e2f17608..ac8447f21e 100755

t/t5516-fetch-push.sh test_expect_success 'fetch follows tags by default' ' · line 1355

Quoted 8 lines
t/t5516-fetch-push.sh +1 -1
@@ -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 &&

Unchanged from v3 and still LGTM!

Thanks,
Matthew

#49 lore raw

Re: [PATCH v4 2/8] fetch: add --negotiation-restrict option

In reply to Derrick Stolee via GitGitGadget
Quoted 41 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
  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(-)
Documentation/config/fetch.adoc +0 -0
index cd40db0cad..04ac90912d 100644

Documentation/config/fetch.adoc line 80

Quoted 6 lines
Documentation/config/fetch.adoc +1 -1
@@ -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].

Good - this addressed my nit in v4 about mentioning the old name here.

Quoted 24 lines
Documentation/config/fetch.adoc +0 -0
  `fetch.showForcedUpdates`::
Documentation/fetch-options.adoc +4 -0
index 81a9d7f9bb..d39cecb446 100644
@@ -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.
  +

Documentation/fetch-options.adoc configuration variables documented in linkgit:git-config[1], and the · line 81

Quoted 8 lines
Documentation/fetch-options.adoc +1 -1
@@ -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)`.

Good! The --negotiate-only paragraph now uses --negotiate-restrict too.

Quoted 37 lines
builtin/fetch.c +6 -4
index 4795b2a13c..fc950fe35b 100644
@@ -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),

builtin/fetch.c int cmd_fetch(int argc, · line 2666

Quoted 9 lines
builtin/fetch.c +2 -1
@@ -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)

Unchanged from v3: OPT_ALIAS keeps back-compat and messages are i18n friendly. Nice.

Quoted 6 lines
builtin/pull.c +0 -0
index 7e67fdce97..cc6ce485fc 100644
@@ -996,9 +996,10 @@ int cmd_pull(int argc,
  		OPT_PASSTHRU('6',  "ipv6", &opt_ipv6, NULL,

builtin/pull.c int cmd_pull(int argc, · line 1005

Quoted 10 lines
builtin/pull.c +2 -1
  			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,

Oh nice! v3 had two OPT_PASSTHRU_ARGV (one for each name). Now we're using OPT_ALIAS here too for the old name which avoids duplicated description strings - yay!

I had to convince myself this works (does OPT_ALIAS forward to the passthru option?). Looking at parse-options.c, preprocess_options() substitutes the alias with a copy of the source option keeping only the alias's long_name. So really the child proc will get the old --negotiate-tip name but the child has an alias anyway.. works fine.

send-pack.c static void get_commons_through_negotiation(struct repository *r, · line 453

Quoted 13 lines
send-pack.c +1 -1
index 67d6987b1c..3d5d36ba3b 100644
@@ -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++;
  		}

Good. v3 was using the old name when shelling out (just a nit), but nicer to have the rename fully consistent.

Quoted 47 lines
t/t5510-fetch.sh +25 -0
index 5dcb4b51a4..dc3ce56d84 100755
@@ -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 &&
  	(
t/t5702-protocol-v2.sh +1 -1
index f826ac46a5..9f6cf4142d 100755
@@ -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" &&

t/t5702-protocol-v2.sh setup_negotiate_only () { · line 882

Quoted 10 lines
t/t5702-protocol-v2.sh +1 -1
  	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 \

Unchanged from v3 - still good!

transport-helper.c static int fetch_refs(struct transport *transport, · line 759

Quoted 11 lines
transport-helper.c +2 -1
index 4d95d84f9e..dd78d40668 100644
@@ -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");

Good - picks up a missed rename from v3.

transport-helper.c static int fetch_refs(struct transport *transport, · line 762

Quoted 2 lines
transport-helper.c +0 -0
  	if (data->fetch)
  		return fetch_with_fetch(transport, nr_heads, to_fetch);

Overall this patch LGTM! All my v3 issues were addressed.

Thanks,
Matthew

#51 lore raw

Re: [PATCH v4 3/8] transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget
Quoted 7 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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 typo (plural vs singular 'tips') is fixed - thanks!

Quoted 48 lines

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.

---
  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(-)
builtin/fetch.c +4 -4
index fc950fe35b..2ba0051d52 100644
@@ -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,
  		};

builtin/fetch.c static void add_negotiation_tips(struct git_transport_options *smart_options) · line 1565

Quoted 6 lines
builtin/fetch.c +1 -1
@@ -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;
  }

Same as in v3, this function will be refactored in a later patch, so just doing the rename now keeps things reviewable - thanks!

Quoted 23 lines
builtin/fetch.c +3 -3
  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,

builtin/fetch.c int cmd_fetch(int argc, · line 2663

Quoted 7 lines
builtin/fetch.c +1 -1
@@ -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=*");

Simple rename - looks good!

Quoted 153 lines
fetch-pack.c +9 -9
index 6ecd468ef7..baf239adf9 100644
@@ -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);
 
fetch-pack.h +2 -2
index 9d3470366f..6c70c942c2 100644
@@ -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[],
transport-helper.c +1 -1
index dd78d40668..f4388da766 100644
@@ -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");
 
transport.c +5 -5
index 107f4fa5dc..a3051f6733 100644
@@ -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);
transport.h +1 -1
index 892f19454a..cdeb33c16f 100644
@@ -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.

transport.h struct git_transport_options { · line 52

Quoted 10 lines
transport.h +1 -1
  	 *
  	 * 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

All just simple renames - looks good to me! This patch is good.

Thanks,
Matthew

#53 lore raw

Re: [PATCH v4 4/8] remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget
Quoted 44 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
  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(-)
Documentation/config/remote.adoc +11 -0
index 91e46f66f5..4dcf81fbce 100644
@@ -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`

Documentation/config/remote.adoc priority configuration file (e.g. `.git/config` in a repository) to clear · line 130

Quoted 10 lines
Documentation/config/remote.adoc +7 -0
+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.

Good - now we don't talk about 'git push' before we have wired that functionality up.

Quoted 14 lines
builtin/fetch.c +6 -0
index 2ba0051d52..a957739f37 100644
@@ -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);

builtin/fetch.c static struct transport *prepare_transport(struct remote *remote, int deepen, · line 1619

Quoted 10 lines
builtin/fetch.c +7 -0
+		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;
  }

Unchanged from v3 and still reads cleanly. Good.

Quoted 24 lines
builtin/fetch.c +2 -7
@@ -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;

builtin/fetch.c int cmd_fetch(int argc, · line 2776

Quoted 10 lines
builtin/fetch.c +6 -0
  		}
+		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);

This is nice! Addressed my concerns in v3 about the wrong message being shown, and makes it clearer with an up-front check for the protocol support.

Quoted 21 lines
remote.c +2 -0
index 7ca2a6501b..620086e16e 100644
@@ -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)

remote.c static int handle_config(const char *key, const char *value, · line 572

Quoted 9 lines
remote.c +3 -0
@@ -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"))

Good - reusing the parse_transport_option() as suggested makes this a smaller diff and consistent with 'serveroption' above.

Quoted 39 lines
remote.h +1 -0
index fc052945ee..e6ec37c393 100644
@@ -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;
t/t5510-fetch.sh +19 -0
index dc3ce56d84..eff3ce8e2d 100755
@@ -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 \

t/t5510-fetch.sh test_expect_success '--negotiation-restrict and --negotiation-tip can be mixed' · line 1516

Quoted 10 lines
t/t5510-fetch.sh +7 -0
+		--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 &&
  	(

Again, this patch looks good to me and addresses my previous comments.

Thanks,
Matthew

#55 lore raw

Re: [PATCH v4 5/8] negotiator: add have_sent() interface

In reply to Derrick Stolee via GitGitGadget
Quoted 15 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 5 lines

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.

This is a new patch in v4, and is in response to the COMMON (bit 2) vs COMMON (bit 6) issue of v3.

Having a defined have_sent() API on the negotiator, and letting the negotiator handle its own book-keeping is much nicer and safer.

Quoted 14 lines
---
  fetch-negotiator.h    | 9 +++++++++
  negotiator/default.c  | 8 ++++++++
  negotiator/noop.c     | 7 +++++++
  negotiator/skipping.c | 8 ++++++++
  4 files changed, 32 insertions(+)
fetch-negotiator.h +0 -0
index e348905a1f..6ca422a064 100644
@@ -47,6 +47,15 @@ struct fetch_negotiator {
  	 */
  	int (*ack)(struct fetch_negotiator *, struct commit *);

fetch-negotiator.h struct fetch_negotiator { · line 58

Quoted 10 lines
fetch-negotiator.h +9 -0
 
+	/*
+	 * 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 *);
+

Doc comment captures the contract well.

Quoted 22 lines
fetch-negotiator.h +0 -0
  	void (*release)(struct fetch_negotiator *);
 
  	/* internal use */
negotiator/default.c +7 -0
index 116dedcf83..05ab616f39 100644
@@ -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);

negotiator/default.c void default_negotiator_init(struct fetch_negotiator *negotiator) · line 201

Quoted 7 lines
negotiator/default.c +1 -0
@@ -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;

I traced this through default.c's mark_common() to confirm this does what we want.. sets the COMMON bit (bit 2) and propogates COMMON to the ancestors correctly. Good!

Quoted 18 lines
negotiator/noop.c +6 -0
index 65e3c20008..edf1b456f3 100644
@@ -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 */

negotiator/noop.c void noop_negotiator_init(struct fetch_negotiator *negotiator) · line 52

Quoted 7 lines
negotiator/noop.c +1 -0
@@ -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;
  }

Trivial implementation of the noop negotiator. Good.

Quoted 19 lines
negotiator/skipping.c +7 -0
index 0a272130fb..69472c58e1 100644
@@ -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;

negotiator/skipping.c void skipping_negotiator_init(struct fetch_negotiator *negotiator) · line 272

Quoted 7 lines
negotiator/skipping.c +1 -0
@@ -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;

I traced this too, to skipping.c's mark_common(), and it sets the COMMON bit correctly.

This patch is a welcome addition and LGTM!

Thanks,
Matthew

#57 lore raw

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

In reply to Derrick Stolee via GitGitGadget
Quoted 20 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 8 lines

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!

Quoted 8 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!

Quoted 31 lines

Also add --negotiation-include to 'git pull' passthrough options.

---
  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(-)
Documentation/fetch-options.adoc +9 -0
index d39cecb446..7b897a7202 100644
@@ -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

Documentation/fetch-options.adoc See also the `fetch.negotiationAlgorithm` and `push.negotiate` · line 94

Quoted 10 lines
Documentation/fetch-options.adoc +10 -0
+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.

Quoted 49 lines
Documentation/fetch-options.adoc +0 -0
  `--negotiate-only`::
  	Do not fetch anything from the server, and instead print the
  	ancestors of the provided `--negotiation-restrict=` arguments,
builtin/fetch.c +10 -4
index a957739f37..6b456b3689 100644
@@ -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;
  		}

builtin/fetch.c static void add_negotiation_restrict_tips(struct git_transport_options *smart_op · line 1573

Quoted 8 lines
builtin/fetch.c +1 -1
@@ -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);
Quoted 24 lines
builtin/fetch.c +4 -2
@@ -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);
  		}
  	}

builtin/fetch.c static struct transport *prepare_transport(struct remote *remote, int deepen, · line 1635

Quoted 10 lines
builtin/fetch.c +8 -0
+	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!

Quoted 14 lines
builtin/fetch.c +2 -0
@@ -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),
builtin/pull.c +0 -0
index cc6ce485fc..d49b09114a 100644

builtin/pull.c int cmd_pull(int argc, · line 1008

Quoted 9 lines
builtin/pull.c +3 -0
@@ -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

Quoted 21 lines
fetch-pack.c +6 -0
index baf239adf9..96071434b8 100644
@@ -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;

fetch-pack.c static void send_filter(struct fetch_pack_args *args, · line 350

Quoted 10 lines
fetch-pack.c +10 -0
+
+	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.

Quoted 66 lines
fetch-pack.c +33 -1
  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) {

fetch-pack.c static void add_common(struct strbuf *req_buf, struct oidset *common) · line 1370

Quoted 10 lines
fetch-pack.c +7 -0
+				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.

Quoted 48 lines
fetch-pack.c +9 -3
@@ -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);

fetch-pack.c static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args, · line 1949

Quoted 6 lines
fetch-pack.c +1 -0
@@ -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.

Quoted 78 lines
fetch-pack.c +9 -2
@@ -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);
  }
fetch-pack.h +4 -2
index 6c70c942c2..6d0dec7f41 100644
@@ -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
t/t5510-fetch.sh +0 -0
index eff3ce8e2d..bc2e2af959 100755
@@ -1460,6 +1460,16 @@ EOF
  	test_cmp fatal-expect fatal-actual
  '
 

t/t5510-fetch.sh EOF · line 1472

Quoted 10 lines
t/t5510-fetch.sh +10 -0
+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).

Quoted 135 lines
t/t5510-fetch.sh +81 -0
  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 &&
  	(
transport.c +7 -1
index a3051f6733..fa54928966 100644
@@ -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);
transport.h +1 -1
index cdeb33c16f..97d905ecc0 100644
@@ -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.
  	 *

transport.h struct git_transport_options { · line 53

Quoted 10 lines
transport.h +2 -1
-	 * 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

#59 lore raw

Re: [PATCH v4 7/8] remote: add remote.*.negotiationInclude config

In reply to Derrick Stolee via GitGitGadget
Quoted 42 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
  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(+)
Documentation/config/remote.adoc +2 -0
index 4dcf81fbce..9ae20e4379 100644
@@ -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

Documentation/config/remote.adoc values are not used. · line 139

Quoted 10 lines
Documentation/config/remote.adoc +10 -0
+	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`.

Thanks - references the correct cross-referenced option I raised in v3. Commit hashes are also explicitly mentioned - good.

Documentation/config/remote.adoc values are not used. · line 150

Quoted 10 lines
Documentation/config/remote.adoc +10 -0
+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.

One thing to mention: the "also used during push negotiation" sentence is added here, and then I see in the next patch (patch 8) we're adding another sentence "these values also influence negotiation during git push" to this same block.

Perhaps consider dropping the additional 'push' sentence here (patch 7) and let patch 8 add just its own sentence about push? Not too pressing.

Quoted 65 lines
Documentation/config/remote.adoc +4 -0
++
+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.
Documentation/fetch-options.adoc +4 -0
index 7b897a7202..8074004377 100644
@@ -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
builtin/fetch.c +11 -0
index 6b456b3689..2308cab377 100644
@@ -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;
  }
remote.c +2 -0
index 620086e16e..6fb5758820 100644
@@ -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)

remote.c static int handle_config(const char *key, const char *value, · line 577

Quoted 9 lines
remote.c +3 -0
@@ -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"))

Good - uses the parse_transport_options() like with the earlier patch.

Quoted 27 lines
remote.h +1 -0
index e6ec37c393..d8809b6991 100644
@@ -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;
t/t5510-fetch.sh +7 -0
index bc2e2af959..33f61ac12a 100755
@@ -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 "" &&

t/t5510-fetch.sh test_expect_success '--negotiation-include avoids duplicates with negotiator' ' · line 1606

Quoted 10 lines
t/t5510-fetch.sh +10 -0
+	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
+'

Great! Now this test will catch failures to the reset-list behaviour by using a different CLI option ref name than is in the config list.

Quoted 25 lines
t/t5510-fetch.sh +25 -0
+
+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 &&

t/t5510-fetch.sh test_expect_success '--negotiation-include avoids duplicates with negotiator' ' · line 1641

Quoted 10 lines
t/t5510-fetch.sh +7 -0
+
+	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 &&

Overall this patch addresses my comments from v3's review. Just the minor issue with forward reference to 'git push' behaviour in the docs.

Thanks,
Matthew

#61 lore raw

Re: [PATCH v4 8/8] send-pack: pass negotiation config in push

In reply to Derrick Stolee via GitGitGadget
Quoted 12 lines · Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

Quoted 6 lines

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.

Is this paragraph stale? Didn't we rename the option in patch 2?

Quoted 14 lines
---
  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(-)
Documentation/config/remote.adoc +0 -0
index 9ae20e4379..460b4e7952 100644

Documentation/config/remote.adoc command-line option. If `--negotiation-restrict` (or its synonym · line 130

Quoted 9 lines
Documentation/config/remote.adoc +3 -0
@@ -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.
 

Nice. We're now only mentioning git push behaviour once we've wired it up in this patch.

Quoted 140 lines
Documentation/config/remote.adoc +3 -0
@@ -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.
 
send-pack.c +30 -7
index 3d5d36ba3b..d18e030ce8 100644
@@ -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);
  	}
 
send-pack.h +2 -0
index c5ded2d200..13850c98bb 100644
@@ -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,
t/t5516-fetch-push.sh +30 -0
index ac8447f21e..177cbc6c75 100755
@@ -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 &&
 
transport.c +0 -0
index fa54928966..a2d8958cb8 100644

transport.c static int git_transport_push(struct transport *transport, struct ref *remote_re · line 928

Quoted 8 lines
transport.c +2 -0
@@ -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;

Again v4 of this patch LGTM.

Thanks,
Matthew

#62 lore raw

Re: [PATCH v4 0/8] fetch: rework negotiation tip options

In reply to Derrick Stolee via GitGitGadget
Quoted 20 lines · Derrick Stolee via GitGitGadget

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

Quoted 9 lines

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.

Thank you for going through the comments on v3 in detail. This is a nice improvement overall.

The main thing flagged (the COMMON bit confusion) is resolved by adding the new have_sent() API on the negotiator interface, which is much clearer and cleaner. The hoisting of the ref resolution to the same layer and reuse of add_negotiate_tips() is also done and appreciated!

I've left replies on each patch, with only a small number of easily addressed comments.

Thanks,
Matthew

#63 lore raw

Re: [PATCH v4 0/8] fetch: rework negotiation tip options

Quoted 35 lines · Matthew John Cheetham
Quoted 20 lines · Derrick Stolee via GitGitGadget

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

Quoted 9 lines

    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.

Thank you for going through the comments on v3 in detail. This is a nice improvement overall.

Quoted 7 lines

The main thing flagged (the COMMON bit confusion) is resolved by adding the new have_sent() API on the negotiator interface, which is much clearer and cleaner. The hoisting of the ref resolution to the same layer and reuse of add_negotiate_tips() is also done and appreciated!

I've left replies on each patch, with only a small number of easily addressed comments.

Thanks for your review, including a confirmation that I properly responded to your earlier review. Soon, I'll send a new version with the few minor edits included.

Thanks,
-Stolee

#64 lore raw

v5 cover fetch: rework negotiation tip options

In reply to Derrick Stolee via GitGitGadget

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 |  49 ++++++++
 Documentation/fetch-options.adoc |  29 ++++-
 builtin/fetch.c                  |  87 +++++++++++---
 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, 564 insertions(+), 62 deletions(-)

base-commit: 6e8d538aab8fe4dd07ba9fb87b5c7edcfa5706ad
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2085%2Fderrickstolee%2Fmust-have-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2085/derrickstolee/must-have-v5
Pull-Request: https://github.com/gitgitgadget/git/pull/2085

Range-diff vs v4:

1: 7409a479d6 ! 1: 538913a327 t5516: fix test order flakiness
@@ Commit message

Use 'sort -k 3' to match the actual number of columns in the output.

+ Reviewed-by: Matthew John Cheetham <[email protected]>

## t/t5516-fetch-push.sh ##
2: 7836a2d6a5 ! 2: 580aa58943 fetch: add --negotiation-restrict option
@@ Commit message
translatable with the option name inserted by formatting. At least one of these messages will be reused later for a new option.

+ Reviewed-by: Matthew John Cheetham <[email protected]>

## Documentation/config/fetch.adoc ##
3: 401bdaff7c ! 3: eee0543647 transport: rename negotiation_tips
@@ Commit message
Also update the string_list used to store the inputs from command-line options.

+ Reviewed-by: Matthew John Cheetham <[email protected]>

## builtin/fetch.c ##
4: a14c568a1f ! 4: 63c675e93e remote: add remote.*.negotiationRestrict config
@@ Commit message
An empty value resets the value list to allow ignoring earlier config values, such as those that might be set in system or global config.

+ Reviewed-by: Matthew John Cheetham <[email protected]>

## Documentation/config/remote.adoc ##
5: 94b79784fe ! 5: d423c56283 negotiator: add have_sent() interface
@@ Commit message
common, so the implementation is quite simple. This logic will be exercised in the next change.

+ Reviewed-by: Matthew John Cheetham <[email protected]>

## fetch-negotiator.h ##
6: b4cd458fe0 ! 6: e86c9791e2 fetch: add --negotiation-include option for negotiation

     @@ Commit message
      
          Also add --negotiation-include to 'git pull' passthrough options.
      
     +    Reviewed-by: Matthew John Cheetham <[email protected]>
          Signed-off-by: Derrick Stolee <[email protected]>
      
       ## Documentation/fetch-options.adoc ##
     @@ builtin/fetch.c: static int add_oid(const struct reference *ref, void *cb_data)
       
      -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 **output_list,
     ++				 const char *argname)
       {
       	struct oid_array *oids = xcalloc(1, sizeof(*oids));
       	int i;
     @@ builtin/fetch.c: static int add_oid(const struct reference *ref, void *cb_data)
       			continue;
       		}
      @@ builtin/fetch.c: static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
     + 				      add_oid, oids, &opts);
     + 		if (old_nr == oids->nr)
       			warning(_("ignoring %s=%s because it does not match any refs"),
     - 				"--negotiation-restrict", s);
     +-				"--negotiation-restrict", s);
     ++				argname, s);
       	}
      -	smart_options->negotiation_restrict_tips = oids;
      +	*output_list = oids;
     @@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot
       		if (transport->smart_options)
      -			add_negotiation_restrict_tips(transport->smart_options);
      +			add_negotiation_tips(&negotiation_restrict,
     -+					     &transport->smart_options->negotiation_restrict_tips);
     ++					     &transport->smart_options->negotiation_restrict_tips,
     ++					     "--negotiation-restrict");
       		else
       			warning(_("ignoring %s because the protocol does not support it"),
       				"--negotiation-restrict");
     @@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot
       		if (transport->smart_options)
      -			add_negotiation_restrict_tips(transport->smart_options);
      +			add_negotiation_tips(&negotiation_restrict,
     -+					     &transport->smart_options->negotiation_restrict_tips);
     ++					     &transport->smart_options->negotiation_restrict_tips,
     ++					     "--negotiation-restrict");
       		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 *remot
      +	if (negotiation_include.nr) {
      +		if (transport->smart_options)
      +			add_negotiation_tips(&negotiation_include,
     -+					     &transport->smart_options->negotiation_include_tips);
     ++					     &transport->smart_options->negotiation_include_tips,
     ++					     "--negotiation-include");
      +		else
      +			warning(_("ignoring %s because the protocol does not support it"),
      +				"--negotiation-include");

7: 7bd70a970b ! 7: e5714115b5 remote: add remote.*.negotiationInclude config
@@ Commit message
list to allow ignoring earlier config values, such as those that might be set in system or global config.

+ Reviewed-by: Matthew John Cheetham <[email protected]>

## Documentation/config/remote.adoc ##
@@ Documentation/config/remote.adoc: 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.
++unconditionally on top of those heuristically selected commits.
++
+Blank values signal to ignore all previous values, allowing a reset of +the list from broader config scenarios.
@@ builtin/fetch.c: static struct transport *prepare_transport(struct remote *remot
+ } else if (remote->negotiation_include.nr) {
+ if (transport->smart_options) {
+ add_negotiation_tips(&remote->negotiation_include,
-+ &transport->smart_options->negotiation_include_tips);
++ &transport->smart_options->negotiation_include_tips,
++ "--negotiation-include");
+ } else {
+ struct strbuf config_name = STRBUF_INIT;
+ strbuf_addf(&config_name, "remote.%s.negotiationInclude", remote->name);
8: 5b968245eb ! 8: ed0be32e2c send-pack: pass negotiation config in push
@@ Commit message
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.
-
+ Reviewed-by: Matthew John Cheetham <[email protected]>

## Documentation/config/remote.adoc ##
@@ Documentation/config/remote.adoc: command-line option. If `--negotiation-restri
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.
+@@ Documentation/config/remote.adoc: 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.
+
+These values also influence negotiation during git push if
+push.negotiate is enabled.

Signature · 1 line
gitgitgadget
#66 lore raw

PATCH v5 2/8 fetch: add --negotiation-restrict option

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/fetch.adoc +1 -1
index cd40db0cad..04ac90912d 100644
@@ -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`::
Documentation/fetch-options.adoc +5 -1
index 81a9d7f9bb..d39cecb446 100644
@@ -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)`.
builtin/fetch.c +8 -5
index 4795b2a13c..fc950fe35b 100644
@@ -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)
builtin/pull.c +2 -1
index 7e67fdce97..cc6ce485fc 100644
@@ -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,
send-pack.c +1 -1
index 67d6987b1c..3d5d36ba3b 100644
@@ -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++;
 		}
t/t5510-fetch.sh +25 -0
index 5dcb4b51a4..dc3ce56d84 100755
@@ -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 &&
 	(
t/t5702-protocol-v2.sh +2 -2
index f826ac46a5..9f6cf4142d 100755
@@ -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 \
transport-helper.c +2 -1
index 4d95d84f9e..dd78d40668 100644
@@ -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);
Signature · 1 line
gitgitgadget
#67 lore raw

PATCH v5 3/8 transport: rename negotiation_tips

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
builtin/fetch.c +9 -9
index fc950fe35b..2ba0051d52 100644
@@ -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=*");
 
fetch-pack.c +9 -9
index 6ecd468ef7..baf239adf9 100644
@@ -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);
 
fetch-pack.h +2 -2
index 9d3470366f..6c70c942c2 100644
@@ -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[],
transport-helper.c +1 -1
index dd78d40668..f4388da766 100644
@@ -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");
 
transport.c +5 -5
index 107f4fa5dc..a3051f6733 100644
@@ -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);
transport.h +2 -2
index 892f19454a..cdeb33c16f 100644
@@ -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
Signature · 1 line
gitgitgadget
#68 lore raw

PATCH v5 4/8 remote: add remote.*.negotiationRestrict config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 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(-)
Documentation/config/remote.adoc +18 -0
index 91e46f66f5..4dcf81fbce 100644
@@ -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.
builtin/fetch.c +21 -7
index 2ba0051d52..a957739f37 100644
@@ -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);
remote.c +5 -0
index 7ca2a6501b..620086e16e 100644
@@ -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"))
remote.h +1 -0
index fc052945ee..e6ec37c393 100644
@@ -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;
t/t5510-fetch.sh +26 -0
index dc3ce56d84..eff3ce8e2d 100755
@@ -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 &&
 	(
Signature · 1 line
gitgitgadget
#69 lore raw

PATCH v5 5/8 negotiator: add have_sent() interface

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 fetch-negotiator.h    | 9 +++++++++
 negotiator/default.c  | 8 ++++++++
 negotiator/noop.c     | 7 +++++++
 negotiator/skipping.c | 8 ++++++++
 4 files changed, 32 insertions(+)
fetch-negotiator.h +9 -0
index e348905a1f..6ca422a064 100644
@@ -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 */
negotiator/default.c +8 -0
index 116dedcf83..05ab616f39 100644
@@ -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;
negotiator/noop.c +7 -0
index 65e3c20008..edf1b456f3 100644
@@ -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;
 }
negotiator/skipping.c +8 -0
index 0a272130fb..69472c58e1 100644
@@ -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;
Signature · 1 line
gitgitgadget
#70 lore raw

PATCH v5 6/8 fetch: add --negotiation-include option for negotiation

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 Documentation/fetch-options.adoc | 19 +++++++
 builtin/fetch.c                  | 38 ++++++++++---
 builtin/pull.c                   |  3 ++
 fetch-pack.c                     | 81 +++++++++++++++++++++++++---
 fetch-pack.h                     |  6 ++-
 t/t5510-fetch.sh                 | 91 ++++++++++++++++++++++++++++++++
 transport.c                      |  8 ++-
 transport.h                      |  5 +-
 8 files changed, 232 insertions(+), 19 deletions(-)
Documentation/fetch-options.adoc +19 -0
index d39cecb446..7b897a7202 100644
@@ -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,
builtin/fetch.c +30 -8
index a957739f37..ba56e9022b 100644
@@ -99,6 +99,7 @@ static struct transport *gsecondary;
 static struct refspec refmap = REFSPEC_INIT_FETCH;
 static struct string_list server_options = STRING_LIST_INIT_DUP;
 static struct string_list negotiation_restrict = STRING_LIST_INIT_NODUP;
+static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;
 
 struct fetch_config {
 	enum display_format display_format;
@@ -1534,23 +1535,29 @@ static int add_oid(const struct reference *ref, void *cb_data)
 	return 0;
 }
 
-static void add_negotiation_restrict_tips(struct git_transport_options *smart_options)
+static void add_negotiation_tips(struct string_list *input_list,
+				 struct oid_array **output_list,
+				 const char *argname)
 {
 	struct oid_array *oids = xcalloc(1, sizeof(*oids));
 	int i;
 
-	for (i = 0; i < negotiation_restrict.nr; i++) {
-		const char *s = negotiation_restrict.items[i].string;
+	for (i = 0; i < input_list->nr; i++) {
+		const char *s = input_list->items[i].string;
 		struct refs_for_each_ref_options opts = {
 			.pattern = s,
 		};
 		int old_nr;
 		if (!has_glob_specials(s)) {
 			struct object_id oid;
+
+			/* Ignore missing reference. */
 			if (repo_get_oid(the_repository, s, &oid))
-				die(_("%s is not a valid object"), s);
+				continue;
+			/* Fail on missing object pointed by ref. */
 			if (!odb_has_object(the_repository->objects, &oid, 0))
 				die(_("the object %s does not exist"), s);
+
 			oid_array_append(oids, &oid);
 			continue;
 		}
@@ -1559,9 +1566,9 @@ static void add_negotiation_restrict_tips(struct git_transport_options *smart_op
 				      add_oid, oids, &opts);
 		if (old_nr == oids->nr)
 			warning(_("ignoring %s=%s because it does not match any refs"),
-				"--negotiation-restrict", s);
+				argname, s);
 	}
-	smart_options->negotiation_restrict_tips = oids;
+	*output_list = oids;
 }
 
 static struct transport *prepare_transport(struct remote *remote, int deepen,
@@ -1597,7 +1604,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
 	}
 	if (negotiation_restrict.nr) {
 		if (transport->smart_options)
-			add_negotiation_restrict_tips(transport->smart_options);
+			add_negotiation_tips(&negotiation_restrict,
+					     &transport->smart_options->negotiation_restrict_tips,
+					     "--negotiation-restrict");
 		else
 			warning(_("ignoring %s because the protocol does not support it"),
 				"--negotiation-restrict");
@@ -1606,7 +1615,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
 		for_each_string_list_item(item, &remote->negotiation_restrict)
 			string_list_append(&negotiation_restrict, item->string);
 		if (transport->smart_options)
-			add_negotiation_restrict_tips(transport->smart_options);
+			add_negotiation_tips(&negotiation_restrict,
+					     &transport->smart_options->negotiation_restrict_tips,
+					     "--negotiation-restrict");
 		else {
 			struct strbuf config_name = STRBUF_INIT;
 			strbuf_addf(&config_name, "remote.%s.negotiationRestrict", remote->name);
@@ -1615,6 +1626,15 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
 			strbuf_release(&config_name);
 		}
 	}
+	if (negotiation_include.nr) {
+		if (transport->smart_options)
+			add_negotiation_tips(&negotiation_include,
+					     &transport->smart_options->negotiation_include_tips,
+					     "--negotiation-include");
+		else
+			warning(_("ignoring %s because the protocol does not support it"),
+				"--negotiation-include");
+	}
 	return transport;
 }
 
@@ -2582,6 +2602,8 @@ int cmd_fetch(int argc,
 		OPT_STRING_LIST(0, "negotiation-restrict", &negotiation_restrict, N_("revision"),
 				N_("report that we have only objects reachable from this object")),
 		OPT_ALIAS(0, "negotiation-tip", "negotiation-restrict"),
+		OPT_STRING_LIST(0, "negotiation-include", &negotiation_include, N_("revision"),
+				N_("ensure this ref is always sent as a negotiation have")),
 		OPT_BOOL(0, "negotiate-only", &negotiate_only,
 			 N_("do not fetch a packfile; instead, print ancestors of negotiation tips")),
 		OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),
builtin/pull.c +3 -0
index cc6ce485fc..d49b09114a 100644
@@ -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,
fetch-pack.c +75 -6

186 lines not on this page · raw message

fetch-pack.h +4 -2
index 6c70c942c2..6d0dec7f41 100644
@@ -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
t/t5510-fetch.sh +91 -0

103 lines not on this page · raw message

transport.c +7 -1
index a3051f6733..fa54928966 100644
@@ -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);
transport.h +3 -2
index cdeb33c16f..97d905ecc0 100644
@@ -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
Signature · 1 line
gitgitgadget
#71 lore raw

PATCH v5 7/8 remote: add remote.*.negotiationInclude config

In reply to Derrick Stolee via GitGitGadget

From: Derrick Stolee <[email protected]>

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.

---
 Documentation/config/remote.adoc | 25 ++++++++++++++++
 Documentation/fetch-options.adoc |  4 +++
 builtin/fetch.c                  | 12 ++++++++
 remote.c                         |  5 ++++
 remote.h                         |  1 +
 t/t5510-fetch.sh                 | 49 ++++++++++++++++++++++++++++++++
 6 files changed, 96 insertions(+)
Documentation/config/remote.adoc +25 -0
index 4dcf81fbce..1951df154e 100644
@@ -125,6 +125,31 @@ values are not used.
 Blank values signal to ignore all previous values, allowing a reset of
 the list from broader config scenarios.
 
+remote.<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.
++
+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.
Documentation/fetch-options.adoc +4 -0
index 7b897a7202..8074004377 100644
@@ -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
builtin/fetch.c +12 -0
index ba56e9022b..1af6500c1d 100644
@@ -1634,6 +1634,18 @@ static struct transport *prepare_transport(struct remote *remote, int deepen,
 		else
 			warning(_("ignoring %s because the protocol does not support it"),
 				"--negotiation-include");
+	} else if (remote->negotiation_include.nr) {
+		if (transport->smart_options) {
+			add_negotiation_tips(&remote->negotiation_include,
+					     &transport->smart_options->negotiation_include_tips,
+					     "--negotiation-include");
+		} else {
+			struct strbuf config_name = STRBUF_INIT;
+			strbuf_addf(&config_name, "remote.%s.negotiationInclude", remote->name);
+			warning(_("ignoring %s because the protocol does not support it"),
+				config_name.buf);
+			strbuf_release(&config_name);
+		}
 	}
 	return transport;
 }
remote.c +5 -0
index 620086e16e..6fb5758820 100644
@@ -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"))
remote.h +1 -0
index e6ec37c393..d8809b6991 100644
@@ -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;
t/t5510-fetch.sh +49 -0

55 lines not on this page · raw message

Signature · 1 line
gitgitgadget