Merge branch 'ds/fetch-negotiation-options'

The negotiation tip options in "git fetch" have been reworked to
allow requiring certain refs to be sent as "have" lines, and to
restrict negotiation to a specific set of refs.

* ds/fetch-negotiation-options:
  send-pack: pass negotiation config in push
  remote: add remote.*.negotiationInclude config
  fetch: add --negotiation-include option for negotiation
  negotiator: add have_sent() interface
  remote: add remote.*.negotiationRestrict config
  transport: rename negotiation_tips
  fetch: add --negotiation-restrict option
  t5516: fix test order flakiness
This commit is contained in:
Junio C Hamano
2026-05-27 14:15:45 +09:00
21 changed files with 564 additions and 62 deletions

View File

@@ -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`::

View File

@@ -107,6 +107,55 @@ 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.
+
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.
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.
+
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.
remote.<name>.followRemoteHEAD::
How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
when fetching using the configured refspecs of a remote.

View File

@@ -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.
+
@@ -69,9 +73,32 @@ 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`.
+
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
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)`.

View File

@@ -98,7 +98,8 @@ 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;
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_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_tip.nr; i++) {
const char *s = negotiation_tip.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;
}
@@ -1558,10 +1565,10 @@ 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"),
argname, s);
}
smart_options->negotiation_tips = oids;
*output_list = oids;
}
static struct transport *prepare_transport(struct remote *remote, int deepen,
@@ -1595,11 +1602,50 @@ 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_tips(&negotiation_restrict,
&transport->smart_options->negotiation_restrict_tips,
"--negotiation-restrict");
else
warning("ignoring --negotiation-tip because the protocol does not support it");
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_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);
warning(_("ignoring %s because the protocol does not support it"),
config_name.buf);
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");
} 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;
}
@@ -2565,8 +2611,11 @@ 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_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),
@@ -2656,9 +2705,6 @@ 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-tip=*"));
if (deepen_relative) {
if (deepen_relative < 0)
die(_("negative depth in --deepen is not supported"));
@@ -2746,14 +2792,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);

View File

@@ -996,9 +996,13 @@ 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_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,

View File

@@ -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 */

View File

@@ -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"
@@ -290,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;
}
@@ -331,6 +332,21 @@ static void send_filter(struct fetch_pack_args *args,
}
}
static void add_oids_to_set(const struct oid_array *array,
struct oidset *set)
{
if (!array)
return;
for (size_t i = 0; i < array->nr; i++) {
struct object_id *oid = &array->oid[i];
if (!odb_has_object(the_repository->objects, oid, 0))
die(_("the object %s does not exist"), oid_to_hex(oid));
oidset_insert(set, oid);
}
}
static int find_common(struct fetch_negotiator *negotiator,
struct fetch_pack_args *args,
int fd[2], struct object_id *result_oid,
@@ -346,6 +362,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");
@@ -354,7 +371,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;
@@ -473,6 +490,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));
@@ -583,6 +621,7 @@ done:
flushes++;
}
strbuf_release(&req_buf);
oidset_clear(&negotiation_include_oids);
if (!got_ready || !no_done)
consume_shallow_list(args, &reader);
@@ -1304,11 +1343,27 @@ static void add_common(struct strbuf *req_buf, struct oidset *common)
static int add_haves(struct fetch_negotiator *negotiator,
struct strbuf *req_buf,
int *haves_to_send)
int *haves_to_send,
struct oidset *negotiation_include_oids)
{
int haves_added = 0;
const struct object_id *oid;
/* Send unconditional haves from --negotiation-include */
if (negotiation_include_oids) {
struct oidset_iter iter;
oidset_iter_init(negotiation_include_oids, &iter);
while ((oid = oidset_iter_next(&iter))) {
struct commit *commit = lookup_commit(the_repository, oid);
if (commit) {
packet_buf_write(req_buf, "have %s\n",
oid_to_hex(oid));
negotiator->have_sent(negotiator, commit);
}
}
}
while ((oid = negotiator->next(negotiator))) {
packet_buf_write(req_buf, "have %s\n", oid_to_hex(oid));
if (++haves_added >= *haves_to_send)
@@ -1357,7 +1412,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;
@@ -1412,7 +1468,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;
@@ -1730,7 +1788,9 @@ 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);
add_oids_to_set(args->negotiation_include_tips,
&negotiation_include_oids);
for_each_cached_alternate(negotiator,
insert_one_alternate_object);
break;
@@ -1749,7 +1809,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);
@@ -1886,6 +1947,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
fsck_options_clear(&fsck_options);
oidset_clear(&common);
oidset_clear(&negotiation_include_oids);
return ref;
}
@@ -2180,16 +2242,18 @@ 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[],
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;
@@ -2198,13 +2262,16 @@ 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);
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);
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);
@@ -2224,7 +2291,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips,
packet_buf_write(&req_buf, "wait-for-done");
haves_added = add_haves(&negotiator, &req_buf, &haves_to_send);
haves_added = add_haves(&negotiator, &req_buf, &haves_to_send,
&negotiation_include_oids);
in_vain += haves_added;
if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN))
last_iteration = 1;
@@ -2276,6 +2344,7 @@ void negotiate_using_fetch(const struct oid_array *negotiation_tips,
clear_common_flag(acked_commits);
object_array_clear(&nt_object_array);
oidset_clear(&negotiation_include_oids);
negotiator.release(&negotiator);
strbuf_release(&req_buf);
}

View File

@@ -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_tips;
const struct oid_array *negotiation_restrict_tips;
const struct oid_array *negotiation_include_tips;
unsigned deepen_relative:1;
unsigned quiet:1;
@@ -89,11 +90,12 @@ 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[],
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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -153,6 +153,8 @@ 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);
string_list_init_dup(&ret->negotiation_include);
ALLOC_GROW(remote_state->remotes, remote_state->remotes_nr + 1,
remote_state->remotes_alloc);
@@ -180,6 +182,8 @@ 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);
string_list_clear(&remote->negotiation_include, 0);
}
static void add_merge(struct branch *branch, const char *name)
@@ -563,6 +567,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")) {
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"))

View File

@@ -117,6 +117,8 @@ struct remote {
char *http_proxy_authmethod;
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;

View File

@@ -434,28 +434,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;
}
@@ -529,7 +549,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);
}

View File

@@ -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,

View File

@@ -1465,6 +1465,197 @@ 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 \
--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 '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 '--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 'remote.<name>.negotiationInclude used as default for --negotiation-include' '
test_when_finished rm -f trace &&
setup_negotiation_tip server server 0 &&
# test the reset of the list on an empty value
git -C client config --add remote.origin.negotiationInclude refs/tags/alpha_1 &&
git -C client config --add remote.origin.negotiationInclude "" &&
git -C client config --add remote.origin.negotiationInclude refs/tags/beta_1 &&
GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
--negotiation-restrict=beta_2 \
origin alpha_s beta_s &&
ALPHA_1=$(git -C client rev-parse alpha_1) &&
test_grep ! "fetch> have $ALPHA_1" trace &&
BETA_1=$(git -C client rev-parse beta_1) &&
test_grep "fetch> have $BETA_1" trace
'
test_expect_success 'remote.<name>.negotiationInclude works with glob patterns' '
test_when_finished rm -f trace &&
setup_negotiation_tip server server 0 &&
git -C client config --add remote.origin.negotiationInclude "refs/tags/beta_*" &&
GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
--negotiation-restrict=alpha_1 \
origin alpha_s beta_s &&
BETA_1=$(git -C client rev-parse beta_1) &&
test_grep "fetch> have $BETA_1" trace &&
BETA_2=$(git -C client rev-parse beta_2) &&
test_grep "fetch> have $BETA_2" trace
'
test_expect_success 'CLI --negotiation-include overrides remote.<name>.negotiationInclude' '
test_when_finished rm -f trace &&
setup_negotiation_tip server server 0 &&
git -C client config --add remote.origin.negotiationInclude refs/tags/beta_2 &&
GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
--negotiation-restrict=alpha_1 \
--negotiation-include=refs/tags/beta_1 \
origin alpha_s beta_s &&
BETA_1=$(git -C client rev-parse beta_1) &&
test_grep "fetch> have $BETA_1" trace &&
BETA_2=$(git -C client rev-parse beta_2) &&
test_grep ! "fetch> have $BETA_2" trace
'
test_expect_success '--negotiation-include avoids duplicates with v0' '
test_when_finished rm -f trace &&
setup_negotiation_tip server server 0 &&
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 &&
(

View File

@@ -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 &&
@@ -1349,7 +1379,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 &&

View File

@@ -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 \

View File

@@ -754,8 +754,9 @@ static int fetch_refs(struct transport *transport,
set_helper_option(transport, "filter", spec);
}
if (data->transport_options.negotiation_tips)
warning("Ignoring --negotiation-tip because the protocol does not support it.");
if (data->transport_options.negotiation_restrict_tips)
warning(_("ignoring %s because the protocol does not support it."),
"--negotiation-restrict");
if (data->fetch)
return fetch_with_fetch(transport, nr_heads, to_fetch);

View File

@@ -464,7 +464,8 @@ 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.negotiation_include_tips = data->options.negotiation_include_tips;
args.reject_shallow_remote = transport->smart_options->reject_shallow;
if (!data->finished_handshake) {
@@ -492,11 +493,12 @@ 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,
data->options.acked_commits);
data->options.acked_commits,
data->options.negotiation_include_tips);
ret = 0;
}
goto cleanup;
@@ -920,6 +922,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;
@@ -980,9 +984,13 @@ 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);
}
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);

View File

@@ -40,13 +40,14 @@ struct git_transport_options {
/*
* This is only used during fetch. See the documentation of
* negotiation_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_tips;
struct oid_array *negotiation_restrict_tips;
struct oid_array *negotiation_include_tips;
/*
* If allocated, whenever transport_fetch_refs() is called, add known