Merge branch 'jt/push-negotiation'

"git push" learns to discover common ancestor with the receiving
end over protocol v2.

* jt/push-negotiation:
  send-pack: support push negotiation
  fetch: teach independent negotiation (no packfile)
  fetch-pack: refactor command and capability write
  fetch-pack: refactor add_haves()
  fetch-pack: refactor process_acks()
This commit is contained in:
Junio C Hamano 2021-05-16 21:05:22 +09:00
Родитель 97eea85a0a 477673d6f3
Коммит 644f4a2046
14 изменённых файлов: 466 добавлений и 111 удалений

Просмотреть файл

@ -120,3 +120,10 @@ push.useForceIfIncludes::
`--force-if-includes` as an option to linkgit:git-push[1] `--force-if-includes` as an option to linkgit:git-push[1]
in the command line. Adding `--no-force-if-includes` at the in the command line. Adding `--no-force-if-includes` at the
time of push overrides this configuration setting. time of push overrides this configuration setting.
push.negotiate::
If set to "true", attempt to reduce the size of the packfile
sent by rounds of negotiation in which the client and the
server attempt to find commits in common. If "false", Git will
rely solely on the server's ref advertisement to find commits
in common.

Просмотреть файл

@ -346,6 +346,14 @@ explained below.
client should download from all given URIs. Currently, the client should download from all given URIs. Currently, the
protocols supported are "http" and "https". protocols supported are "http" and "https".
If the 'wait-for-done' feature is advertised, the following argument
can be included in the client's request.
wait-for-done
Indicates to the server that it should never send "ready", but
should wait for the client to say "done" before sending the
packfile.
The response of `fetch` is broken into a number of sections separated by The response of `fetch` is broken into a number of sections separated by
delimiter packets (0001), with each section beginning with its section delimiter packets (0001), with each section beginning with its section
header. Most sections are sent only when the packfile is sent. header. Most sections are sent only when the packfile is sent.

Просмотреть файл

@ -83,6 +83,7 @@ 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_tip = STRING_LIST_INIT_NODUP;
static int fetch_write_commit_graph = -1; static int fetch_write_commit_graph = -1;
static int stdin_refspecs = 0; static int stdin_refspecs = 0;
static int negotiate_only;
static int git_fetch_config(const char *k, const char *v, void *cb) static int git_fetch_config(const char *k, const char *v, void *cb)
{ {
@ -205,6 +206,8 @@ static struct option builtin_fetch_options[] = {
TRANSPORT_FAMILY_IPV6), TRANSPORT_FAMILY_IPV6),
OPT_STRING_LIST(0, "negotiation-tip", &negotiation_tip, N_("revision"), OPT_STRING_LIST(0, "negotiation-tip", &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_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), OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options),
OPT_BOOL(0, "auto-maintenance", &enable_auto_gc, OPT_BOOL(0, "auto-maintenance", &enable_auto_gc,
N_("run 'maintenance --auto' after fetching")), N_("run 'maintenance --auto' after fetching")),
@ -2043,7 +2046,29 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
} }
} }
if (remote) { if (negotiate_only) {
struct oidset acked_commits = OIDSET_INIT;
struct oidset_iter iter;
const struct object_id *oid;
if (!remote)
die(_("must supply remote when using --negotiate-only"));
gtransport = prepare_transport(remote, 1);
if (gtransport->smart_options) {
gtransport->smart_options->acked_commits = &acked_commits;
} else {
warning(_("Protocol does not support --negotiate-only, exiting."));
return 1;
}
if (server_options.nr)
gtransport->server_options = &server_options;
result = transport_fetch_refs(gtransport, NULL);
oidset_iter_init(&acked_commits, &iter);
while ((oid = oidset_iter_next(&iter)))
printf("%s\n", oid_to_hex(oid));
oidset_clear(&acked_commits);
} else if (remote) {
if (filter_options.choice || has_promisor_remote()) if (filter_options.choice || has_promisor_remote())
fetch_one_setup_partial(remote); fetch_one_setup_partial(remote);
result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs); result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs);

Просмотреть файл

@ -23,6 +23,8 @@
#include "fetch-negotiator.h" #include "fetch-negotiator.h"
#include "fsck.h" #include "fsck.h"
#include "shallow.h" #include "shallow.h"
#include "commit-reach.h"
#include "commit-graph.h"
static int transfer_unpack_limit = -1; static int transfer_unpack_limit = -1;
static int fetch_unpack_limit = -1; static int fetch_unpack_limit = -1;
@ -45,6 +47,8 @@ static struct string_list uri_protocols = STRING_LIST_INIT_DUP;
/* Remember to update object flag allocation in object.h */ /* Remember to update object flag allocation in object.h */
#define COMPLETE (1U << 0) #define COMPLETE (1U << 0)
#define ALTERNATE (1U << 1) #define ALTERNATE (1U << 1)
#define COMMON (1U << 6)
#define REACH_SCRATCH (1U << 7)
/* /*
* After sending this many "have"s if we do not get any new ACK , we * After sending this many "have"s if we do not get any new ACK , we
@ -1195,11 +1199,9 @@ static void add_common(struct strbuf *req_buf, struct oidset *common)
} }
static int add_haves(struct fetch_negotiator *negotiator, static int add_haves(struct fetch_negotiator *negotiator,
int seen_ack,
struct strbuf *req_buf, struct strbuf *req_buf,
int *haves_to_send, int *in_vain) int *haves_to_send)
{ {
int ret = 0;
int haves_added = 0; int haves_added = 0;
const struct object_id *oid; const struct object_id *oid;
@ -1209,17 +1211,42 @@ static int add_haves(struct fetch_negotiator *negotiator,
break; break;
} }
*in_vain += haves_added;
if (!haves_added || (seen_ack && *in_vain >= MAX_IN_VAIN)) {
/* Send Done */
packet_buf_write(req_buf, "done\n");
ret = 1;
}
/* Increase haves to send on next round */ /* Increase haves to send on next round */
*haves_to_send = next_flush(1, *haves_to_send); *haves_to_send = next_flush(1, *haves_to_send);
return ret; return haves_added;
}
static void write_fetch_command_and_capabilities(struct strbuf *req_buf,
const struct string_list *server_options)
{
const char *hash_name;
if (server_supports_v2("fetch", 1))
packet_buf_write(req_buf, "command=fetch");
if (server_supports_v2("agent", 0))
packet_buf_write(req_buf, "agent=%s", git_user_agent_sanitized());
if (advertise_sid && server_supports_v2("session-id", 0))
packet_buf_write(req_buf, "session-id=%s", trace2_session_id());
if (server_options && server_options->nr &&
server_supports_v2("server-option", 1)) {
int i;
for (i = 0; i < server_options->nr; i++)
packet_buf_write(req_buf, "server-option=%s",
server_options->items[i].string);
}
if (server_feature_v2("object-format", &hash_name)) {
int hash_algo = hash_algo_by_name(hash_name);
if (hash_algo_by_ptr(the_hash_algo) != hash_algo)
die(_("mismatched algorithms: client %s; server %s"),
the_hash_algo->name, hash_name);
packet_buf_write(req_buf, "object-format=%s", the_hash_algo->name);
} else if (hash_algo_by_ptr(the_hash_algo) != GIT_HASH_SHA1) {
die(_("the server does not support algorithm '%s'"),
the_hash_algo->name);
}
packet_buf_delim(req_buf);
} }
static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out, static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out,
@ -1228,36 +1255,12 @@ static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out,
int *haves_to_send, int *in_vain, int *haves_to_send, int *in_vain,
int sideband_all, int seen_ack) int sideband_all, int seen_ack)
{ {
int ret = 0; int haves_added;
const char *hash_name; int done_sent = 0;
struct strbuf req_buf = STRBUF_INIT; struct strbuf req_buf = STRBUF_INIT;
if (server_supports_v2("fetch", 1)) write_fetch_command_and_capabilities(&req_buf, args->server_options);
packet_buf_write(&req_buf, "command=fetch");
if (server_supports_v2("agent", 0))
packet_buf_write(&req_buf, "agent=%s", git_user_agent_sanitized());
if (advertise_sid && server_supports_v2("session-id", 0))
packet_buf_write(&req_buf, "session-id=%s", trace2_session_id());
if (args->server_options && args->server_options->nr &&
server_supports_v2("server-option", 1)) {
int i;
for (i = 0; i < args->server_options->nr; i++)
packet_buf_write(&req_buf, "server-option=%s",
args->server_options->items[i].string);
}
if (server_feature_v2("object-format", &hash_name)) {
int hash_algo = hash_algo_by_name(hash_name);
if (hash_algo_by_ptr(the_hash_algo) != hash_algo)
die(_("mismatched algorithms: client %s; server %s"),
the_hash_algo->name, hash_name);
packet_buf_write(&req_buf, "object-format=%s", the_hash_algo->name);
} else if (hash_algo_by_ptr(the_hash_algo) != GIT_HASH_SHA1) {
die(_("the server does not support algorithm '%s'"),
the_hash_algo->name);
}
packet_buf_delim(&req_buf);
if (args->use_thin_pack) if (args->use_thin_pack)
packet_buf_write(&req_buf, "thin-pack"); packet_buf_write(&req_buf, "thin-pack");
if (args->no_progress) if (args->no_progress)
@ -1312,9 +1315,13 @@ 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 all of the common commits we've found in previous rounds */
add_common(&req_buf, common); add_common(&req_buf, common);
/* Add initial haves */ haves_added = add_haves(negotiator, &req_buf, haves_to_send);
ret = add_haves(negotiator, seen_ack, &req_buf, *in_vain += haves_added;
haves_to_send, in_vain); if (!haves_added || (seen_ack && *in_vain >= MAX_IN_VAIN)) {
/* Send Done */
packet_buf_write(&req_buf, "done\n");
done_sent = 1;
}
/* Send request */ /* Send request */
packet_buf_flush(&req_buf); packet_buf_flush(&req_buf);
@ -1322,7 +1329,7 @@ static int send_fetch_request(struct fetch_negotiator *negotiator, int fd_out,
die_errno(_("unable to write request to remote")); die_errno(_("unable to write request to remote"));
strbuf_release(&req_buf); strbuf_release(&req_buf);
return ret; return done_sent;
} }
/* /*
@ -1351,35 +1358,11 @@ static int process_section_header(struct packet_reader *reader,
return ret; return ret;
} }
enum common_found { static int process_ack(struct fetch_negotiator *negotiator,
/*
* No commit was found to be possessed by both the client and the
* server, and "ready" was not received.
*/
NO_COMMON_FOUND,
/*
* At least one commit was found to be possessed by both the client and
* the server, and "ready" was not received.
*/
COMMON_FOUND,
/*
* "ready" was received, indicating that the server is ready to send
* the packfile without any further negotiation.
*/
READY
};
static enum common_found process_acks(struct fetch_negotiator *negotiator,
struct packet_reader *reader, struct packet_reader *reader,
struct oidset *common) struct object_id *common_oid,
int *received_ready)
{ {
/* received */
int received_ready = 0;
int received_ack = 0;
process_section_header(reader, "acknowledgments", 0);
while (packet_reader_read(reader) == PACKET_READ_NORMAL) { while (packet_reader_read(reader) == PACKET_READ_NORMAL) {
const char *arg; const char *arg;
@ -1387,20 +1370,17 @@ static enum common_found process_acks(struct fetch_negotiator *negotiator,
continue; continue;
if (skip_prefix(reader->line, "ACK ", &arg)) { if (skip_prefix(reader->line, "ACK ", &arg)) {
struct object_id oid; if (!get_oid_hex(arg, common_oid)) {
received_ack = 1;
if (!get_oid_hex(arg, &oid)) {
struct commit *commit; struct commit *commit;
oidset_insert(common, &oid); commit = lookup_commit(the_repository, common_oid);
commit = lookup_commit(the_repository, &oid);
if (negotiator) if (negotiator)
negotiator->ack(negotiator, commit); negotiator->ack(negotiator, commit);
} }
continue; return 1;
} }
if (!strcmp(reader->line, "ready")) { if (!strcmp(reader->line, "ready")) {
received_ready = 1; *received_ready = 1;
continue; continue;
} }
@ -1418,13 +1398,12 @@ static enum common_found process_acks(struct fetch_negotiator *negotiator,
* sent. Therefore, a DELIM is expected if "ready" is sent, and a FLUSH * sent. Therefore, a DELIM is expected if "ready" is sent, and a FLUSH
* otherwise. * otherwise.
*/ */
if (received_ready && reader->status != PACKET_READ_DELIM) if (*received_ready && reader->status != PACKET_READ_DELIM)
die(_("expected packfile to be sent after 'ready'")); die(_("expected packfile to be sent after 'ready'"));
if (!received_ready && reader->status != PACKET_READ_FLUSH) if (!*received_ready && reader->status != PACKET_READ_FLUSH)
die(_("expected no other sections to be sent after no 'ready'")); die(_("expected no other sections to be sent after no 'ready'"));
return received_ready ? READY : return 0;
(received_ack ? COMMON_FOUND : NO_COMMON_FOUND);
} }
static void receive_shallow_info(struct fetch_pack_args *args, static void receive_shallow_info(struct fetch_pack_args *args,
@ -1548,10 +1527,10 @@ enum fetch_state {
FETCH_DONE, FETCH_DONE,
}; };
static void do_check_stateless_delimiter(const struct fetch_pack_args *args, static void do_check_stateless_delimiter(int stateless_rpc,
struct packet_reader *reader) struct packet_reader *reader)
{ {
check_stateless_delimiter(args->stateless_rpc, reader, check_stateless_delimiter(stateless_rpc, reader,
_("git fetch-pack: expected response end packet")); _("git fetch-pack: expected response end packet"));
} }
@ -1573,6 +1552,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
struct fetch_negotiator negotiator_alloc; struct fetch_negotiator negotiator_alloc;
struct fetch_negotiator *negotiator; struct fetch_negotiator *negotiator;
int seen_ack = 0; int seen_ack = 0;
struct object_id common_oid;
int received_ready = 0;
struct string_list packfile_uris = STRING_LIST_INIT_DUP; struct string_list packfile_uris = STRING_LIST_INIT_DUP;
int i; int i;
struct strvec index_pack_args = STRVEC_INIT; struct strvec index_pack_args = STRVEC_INIT;
@ -1631,22 +1612,22 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
break; break;
case FETCH_PROCESS_ACKS: case FETCH_PROCESS_ACKS:
/* Process ACKs/NAKs */ /* Process ACKs/NAKs */
switch (process_acks(negotiator, &reader, &common)) { process_section_header(&reader, "acknowledgments", 0);
case READY: while (process_ack(negotiator, &reader, &common_oid,
&received_ready)) {
in_vain = 0;
seen_ack = 1;
oidset_insert(&common, &common_oid);
}
if (received_ready) {
/* /*
* Don't check for response delimiter; get_pack() will * Don't check for response delimiter; get_pack() will
* read the rest of this response. * read the rest of this response.
*/ */
state = FETCH_GET_PACK; state = FETCH_GET_PACK;
break; } else {
case COMMON_FOUND: do_check_stateless_delimiter(args->stateless_rpc, &reader);
in_vain = 0;
seen_ack = 1;
/* fallthrough */
case NO_COMMON_FOUND:
do_check_stateless_delimiter(args, &reader);
state = FETCH_SEND_REQUEST; state = FETCH_SEND_REQUEST;
break;
} }
break; break;
case FETCH_GET_PACK: case FETCH_GET_PACK:
@ -1668,7 +1649,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
packfile_uris.nr ? &index_pack_args : NULL, packfile_uris.nr ? &index_pack_args : NULL,
sought, nr_sought, &fsck_options.gitmodules_found)) sought, nr_sought, &fsck_options.gitmodules_found))
die(_("git fetch-pack: fetch failed.")); die(_("git fetch-pack: fetch failed."));
do_check_stateless_delimiter(args, &reader); do_check_stateless_delimiter(args->stateless_rpc, &reader);
state = FETCH_DONE; state = FETCH_DONE;
break; break;
@ -1985,6 +1966,105 @@ cleanup:
return ref_cpy; return ref_cpy;
} }
static int add_to_object_array(const struct object_id *oid, void *data)
{
struct object_array *a = data;
add_object_array(lookup_object(the_repository, oid), "", a);
return 0;
}
static void clear_common_flag(struct oidset *s)
{
struct oidset_iter iter;
const struct object_id *oid;
oidset_iter_init(s, &iter);
while ((oid = oidset_iter_next(&iter))) {
struct object *obj = lookup_object(the_repository, oid);
obj->flags &= ~COMMON;
}
}
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 fetch_negotiator negotiator;
struct packet_reader reader;
struct object_array nt_object_array = OBJECT_ARRAY_INIT;
struct strbuf req_buf = STRBUF_INIT;
int haves_to_send = INITIAL_FLUSH;
int in_vain = 0;
int seen_ack = 0;
int last_iteration = 0;
timestamp_t min_generation = GENERATION_NUMBER_INFINITY;
fetch_negotiator_init(the_repository, &negotiator);
mark_tips(&negotiator, negotiation_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,
add_to_object_array,
&nt_object_array);
while (!last_iteration) {
int haves_added;
struct object_id common_oid;
int received_ready = 0;
strbuf_reset(&req_buf);
write_fetch_command_and_capabilities(&req_buf, server_options);
packet_buf_write(&req_buf, "wait-for-done");
haves_added = add_haves(&negotiator, &req_buf, &haves_to_send);
in_vain += haves_added;
if (!haves_added || (seen_ack && in_vain >= MAX_IN_VAIN))
last_iteration = 1;
/* Send request */
packet_buf_flush(&req_buf);
if (write_in_full(fd[1], req_buf.buf, req_buf.len) < 0)
die_errno(_("unable to write request to remote"));
/* Process ACKs/NAKs */
process_section_header(&reader, "acknowledgments", 0);
while (process_ack(&negotiator, &reader, &common_oid,
&received_ready)) {
struct commit *commit = lookup_commit(the_repository,
&common_oid);
if (commit) {
timestamp_t generation;
parse_commit_or_die(commit);
commit->object.flags |= COMMON;
generation = commit_graph_generation(commit);
if (generation < min_generation)
min_generation = generation;
}
in_vain = 0;
seen_ack = 1;
oidset_insert(acked_commits, &common_oid);
}
if (received_ready)
die(_("unexpected 'ready' from remote"));
else
do_check_stateless_delimiter(stateless_rpc, &reader);
if (can_all_from_reach_with_flag(&nt_object_array, COMMON,
REACH_SCRATCH, 0,
min_generation))
last_iteration = 1;
}
clear_common_flag(acked_commits);
strbuf_release(&req_buf);
}
int report_unmatched_refs(struct ref **sought, int nr_sought) int report_unmatched_refs(struct ref **sought, int nr_sought)
{ {
int i, ret = 0; int i, ret = 0;

Просмотреть файл

@ -5,6 +5,7 @@
#include "run-command.h" #include "run-command.h"
#include "protocol.h" #include "protocol.h"
#include "list-objects-filter-options.h" #include "list-objects-filter-options.h"
#include "oidset.h"
struct oid_array; struct oid_array;
@ -81,6 +82,19 @@ struct ref *fetch_pack(struct fetch_pack_args *args,
struct string_list *pack_lockfiles, struct string_list *pack_lockfiles,
enum protocol_version version); enum protocol_version version);
/*
* Execute the --negotiate-only mode of "git fetch", adding all known common
* commits to acked_commits.
*
* 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,
const struct string_list *server_options,
int stateless_rpc,
int fd[],
struct oidset *acked_commits);
/* /*
* Print an appropriate error message for each sought ref that wasn't * Print an appropriate error message for each sought ref that wasn't
* matched. Return 0 if all sought refs were matched, otherwise 1. * matched. Return 0 if all sought refs were matched, otherwise 1.

Просмотреть файл

@ -60,7 +60,7 @@ struct object_array {
/* /*
* object flag allocation: * object flag allocation:
* revision.h: 0---------10 15 23------26 * revision.h: 0---------10 15 23------26
* fetch-pack.c: 01 * fetch-pack.c: 01 67
* negotiator/default.c: 2--5 * negotiator/default.c: 2--5
* walker.c: 0-2 * walker.c: 0-2
* upload-pack.c: 4 11-----14 16-----19 * upload-pack.c: 4 11-----14 16-----19

Просмотреть файл

@ -56,7 +56,9 @@ static void feed_object(const struct object_id *oid, FILE *fh, int negative)
/* /*
* Make a pack stream and spit it out into file descriptor fd * Make a pack stream and spit it out into file descriptor fd
*/ */
static int pack_objects(int fd, struct ref *refs, struct oid_array *extra, struct send_pack_args *args) static int pack_objects(int fd, struct ref *refs, struct oid_array *advertised,
struct oid_array *negotiated,
struct send_pack_args *args)
{ {
/* /*
* The child becomes pack-objects --revs; we feed * The child becomes pack-objects --revs; we feed
@ -94,8 +96,10 @@ static int pack_objects(int fd, struct ref *refs, struct oid_array *extra, struc
* parameters by writing to the pipe. * parameters by writing to the pipe.
*/ */
po_in = xfdopen(po.in, "w"); po_in = xfdopen(po.in, "w");
for (i = 0; i < extra->nr; i++) for (i = 0; i < advertised->nr; i++)
feed_object(&extra->oid[i], po_in, 1); feed_object(&advertised->oid[i], po_in, 1);
for (i = 0; i < negotiated->nr; i++)
feed_object(&negotiated->oid[i], po_in, 1);
while (refs) { while (refs) {
if (!is_null_oid(&refs->old_oid)) if (!is_null_oid(&refs->old_oid))
@ -409,11 +413,55 @@ static void reject_invalid_nonce(const char *nonce, int len)
} }
} }
static void get_commons_through_negotiation(const char *url,
const struct ref *remote_refs,
struct oid_array *commons)
{
struct child_process child = CHILD_PROCESS_INIT;
const struct ref *ref;
int len = the_hash_algo->hexsz + 1; /* hash + NL */
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)
strvec_pushf(&child.args, "--negotiation-tip=%s", oid_to_hex(&ref->new_oid));
strvec_push(&child.args, url);
if (start_command(&child))
die(_("send-pack: unable to fork off fetch subprocess"));
do {
char hex_hash[GIT_MAX_HEXSZ + 1];
int read_len = read_in_full(child.out, hex_hash, len);
struct object_id oid;
const char *end;
if (!read_len)
break;
if (read_len != len)
die("invalid length read %d", read_len);
if (parse_oid_hex(hex_hash, &oid, &end) || *end != '\n')
die("invalid hash");
oid_array_append(commons, &oid);
} while (1);
if (finish_command(&child)) {
/*
* The information that push negotiation provides is useful but
* not mandatory.
*/
warning(_("push negotiation failed; proceeding anyway with push"));
}
}
int send_pack(struct send_pack_args *args, int send_pack(struct send_pack_args *args,
int fd[], struct child_process *conn, int fd[], struct child_process *conn,
struct ref *remote_refs, struct ref *remote_refs,
struct oid_array *extra_have) struct oid_array *extra_have)
{ {
struct oid_array commons = OID_ARRAY_INIT;
int in = fd[0]; int in = fd[0];
int out = fd[1]; int out = fd[1];
struct strbuf req_buf = STRBUF_INIT; struct strbuf req_buf = STRBUF_INIT;
@ -426,6 +474,7 @@ int send_pack(struct send_pack_args *args,
int quiet_supported = 0; int quiet_supported = 0;
int agent_supported = 0; int agent_supported = 0;
int advertise_sid = 0; int advertise_sid = 0;
int push_negotiate = 0;
int use_atomic = 0; int use_atomic = 0;
int atomic_supported = 0; int atomic_supported = 0;
int use_push_options = 0; int use_push_options = 0;
@ -437,6 +486,10 @@ int send_pack(struct send_pack_args *args,
const char *push_cert_nonce = NULL; const char *push_cert_nonce = NULL;
struct packet_reader reader; struct packet_reader reader;
git_config_get_bool("push.negotiate", &push_negotiate);
if (push_negotiate)
get_commons_through_negotiation(args->url, remote_refs, &commons);
git_config_get_bool("transfer.advertisesid", &advertise_sid); git_config_get_bool("transfer.advertisesid", &advertise_sid);
/* Does the other end support the reporting? */ /* Does the other end support the reporting? */
@ -625,7 +678,7 @@ int send_pack(struct send_pack_args *args,
PACKET_READ_DIE_ON_ERR_PACKET); PACKET_READ_DIE_ON_ERR_PACKET);
if (need_pack_data && cmds_sent) { if (need_pack_data && cmds_sent) {
if (pack_objects(out, remote_refs, extra_have, args) < 0) { if (pack_objects(out, remote_refs, extra_have, &commons, args) < 0) {
if (args->stateless_rpc) if (args->stateless_rpc)
close(out); close(out);
if (git_connection_is_socket(conn)) if (git_connection_is_socket(conn))

Просмотреть файл

@ -191,6 +191,41 @@ test_expect_success 'fetch with pushInsteadOf (should not rewrite)' '
) )
' '
grep_wrote () {
object_count=$1
file_name=$2
grep 'write_pack_file/wrote.*"value":"'$1'"' $2
}
test_expect_success 'push with negotiation' '
# Without negotiation
mk_empty testrepo &&
git push testrepo $the_first_commit:refs/remotes/origin/first_commit &&
git -C testrepo config receive.hideRefs refs/remotes/origin/first_commit &&
echo now pushing without negotiation &&
GIT_TRACE2_EVENT="$(pwd)/event" git -c protocol.version=2 push testrepo refs/heads/main:refs/remotes/origin/main &&
grep_wrote 5 event && # 2 commits, 2 trees, 1 blob
# Same commands, but with negotiation
rm event &&
mk_empty testrepo &&
git push testrepo $the_first_commit:refs/remotes/origin/first_commit &&
git -C testrepo config receive.hideRefs refs/remotes/origin/first_commit &&
GIT_TRACE2_EVENT="$(pwd)/event" git -c protocol.version=2 -c push.negotiate=1 push testrepo refs/heads/main:refs/remotes/origin/main &&
grep_wrote 2 event # 1 commit, 1 tree
'
test_expect_success 'push with negotiation proceeds anyway even if negotiation fails' '
rm event &&
mk_empty testrepo &&
git push testrepo $the_first_commit:refs/remotes/origin/first_commit &&
git -C testrepo config receive.hideRefs refs/remotes/origin/first_commit &&
GIT_TEST_PROTOCOL_VERSION=0 GIT_TRACE2_EVENT="$(pwd)/event" \
git -c push.negotiate=1 push testrepo refs/heads/main:refs/remotes/origin/main 2>err &&
grep_wrote 5 event && # 2 commits, 2 trees, 1 blob
test_i18ngrep "push negotiation failed" err
'
test_expect_success 'push without wildcard' ' test_expect_success 'push without wildcard' '
mk_empty testrepo && mk_empty testrepo &&

Просмотреть файл

@ -16,7 +16,7 @@ test_expect_success 'test capability advertisement' '
version 2 version 2
agent=git/$(git version | cut -d" " -f3) agent=git/$(git version | cut -d" " -f3)
ls-refs=unborn ls-refs=unborn
fetch=shallow fetch=shallow wait-for-done
server-option server-option
object-format=$(test_oid algo) object-format=$(test_oid algo)
object-info object-info

Просмотреть файл

@ -585,6 +585,49 @@ test_expect_success 'deepen-relative' '
test_cmp expected actual test_cmp expected actual
' '
setup_negotiate_only () {
SERVER="$1"
URI="$2"
rm -rf "$SERVER" client
git init "$SERVER"
test_commit -C "$SERVER" one
test_commit -C "$SERVER" two
git clone "$URI" client
test_commit -C client three
}
test_expect_success 'file:// --negotiate-only' '
SERVER="server" &&
URI="file://$(pwd)/server" &&
setup_negotiate_only "$SERVER" "$URI" &&
git -c protocol.version=2 -C client fetch \
--no-tags \
--negotiate-only \
--negotiation-tip=$(git -C client rev-parse HEAD) \
origin >out &&
COMMON=$(git -C "$SERVER" rev-parse two) &&
grep "$COMMON" out
'
test_expect_success 'file:// --negotiate-only with protocol v0' '
SERVER="server" &&
URI="file://$(pwd)/server" &&
setup_negotiate_only "$SERVER" "$URI" &&
test_must_fail git -c protocol.version=0 -C client fetch \
--no-tags \
--negotiate-only \
--negotiation-tip=$(git -C client rev-parse HEAD) \
origin 2>err &&
test_i18ngrep "negotiate-only requires protocol v2" err
'
# Test protocol v2 with 'http://' transport # Test protocol v2 with 'http://' transport
# #
. "$TEST_DIRECTORY"/lib-httpd.sh . "$TEST_DIRECTORY"/lib-httpd.sh
@ -1035,6 +1078,52 @@ test_expect_success 'packfile-uri with transfer.fsckobjects fails when .gitmodul
test_i18ngrep "disallowed submodule name" err test_i18ngrep "disallowed submodule name" err
' '
test_expect_success 'http:// --negotiate-only' '
SERVER="$HTTPD_DOCUMENT_ROOT_PATH/server" &&
URI="$HTTPD_URL/smart/server" &&
setup_negotiate_only "$SERVER" "$URI" &&
git -c protocol.version=2 -C client fetch \
--no-tags \
--negotiate-only \
--negotiation-tip=$(git -C client rev-parse HEAD) \
origin >out &&
COMMON=$(git -C "$SERVER" rev-parse two) &&
grep "$COMMON" out
'
test_expect_success 'http:// --negotiate-only without wait-for-done support' '
SERVER="server" &&
URI="$HTTPD_URL/one_time_perl/server" &&
setup_negotiate_only "$SERVER" "$URI" &&
echo "s/ wait-for-done/ xxxx-xxx-xxxx/" \
>"$HTTPD_ROOT_PATH/one-time-perl" &&
test_must_fail git -c protocol.version=2 -C client fetch \
--no-tags \
--negotiate-only \
--negotiation-tip=$(git -C client rev-parse HEAD) \
origin 2>err &&
test_i18ngrep "server does not support wait-for-done" err
'
test_expect_success 'http:// --negotiate-only with protocol v0' '
SERVER="$HTTPD_DOCUMENT_ROOT_PATH/server" &&
URI="$HTTPD_URL/smart/server" &&
setup_negotiate_only "$SERVER" "$URI" &&
test_must_fail git -c protocol.version=0 -C client fetch \
--no-tags \
--negotiate-only \
--negotiation-tip=$(git -C client rev-parse HEAD) \
origin 2>err &&
test_i18ngrep "negotiate-only requires protocol v2" err
'
# DO NOT add non-httpd-specific tests here, because the last part of this # DO NOT add non-httpd-specific tests here, because the last part of this
# test script is only executed when httpd is available and enabled. # test script is only executed when httpd is available and enabled.

Просмотреть файл

@ -684,6 +684,16 @@ static int fetch(struct transport *transport,
return transport->vtable->fetch(transport, nr_heads, to_fetch); return transport->vtable->fetch(transport, nr_heads, to_fetch);
} }
/*
* If we reach here, then the server, the client, and/or the transport
* helper does not support protocol v2. --negotiate-only requires
* protocol v2.
*/
if (data->transport_options.acked_commits) {
warning(_("--negotiate-only requires protocol v2"));
return -1;
}
if (!data->get_refs_list_called) if (!data->get_refs_list_called)
get_refs_list_using_list(transport, 0); get_refs_list_using_list(transport, 0);

Просмотреть файл

@ -392,16 +392,29 @@ static int fetch_refs_via_pack(struct transport *transport,
else if (data->version <= protocol_v1) else if (data->version <= protocol_v1)
die_if_server_options(transport); die_if_server_options(transport);
if (data->options.acked_commits) {
if (data->version < protocol_v2) {
warning(_("--negotiate-only requires protocol v2"));
ret = -1;
} else if (!server_supports_feature("fetch", "wait-for-done", 0)) {
warning(_("server does not support wait-for-done"));
ret = -1;
} else {
negotiate_using_fetch(data->options.negotiation_tips,
transport->server_options,
transport->stateless_rpc,
data->fd,
data->options.acked_commits);
ret = 0;
}
goto cleanup;
}
refs = fetch_pack(&args, data->fd, refs = fetch_pack(&args, data->fd,
refs_tmp ? refs_tmp : transport->remote_refs, refs_tmp ? refs_tmp : transport->remote_refs,
to_fetch, nr_heads, &data->shallow, to_fetch, nr_heads, &data->shallow,
&transport->pack_lockfiles, data->version); &transport->pack_lockfiles, data->version);
close(data->fd[0]);
close(data->fd[1]);
if (finish_connect(data->conn))
ret = -1;
data->conn = NULL;
data->got_remote_heads = 0; data->got_remote_heads = 0;
data->options.self_contained_and_connected = data->options.self_contained_and_connected =
args.self_contained_and_connected; args.self_contained_and_connected;
@ -412,6 +425,13 @@ static int fetch_refs_via_pack(struct transport *transport,
if (report_unmatched_refs(to_fetch, nr_heads)) if (report_unmatched_refs(to_fetch, nr_heads))
ret = -1; ret = -1;
cleanup:
close(data->fd[0]);
close(data->fd[1]);
if (finish_connect(data->conn))
ret = -1;
data->conn = NULL;
free_refs(refs_tmp); free_refs(refs_tmp);
free_refs(refs); free_refs(refs);
return ret; return ret;

Просмотреть файл

@ -47,6 +47,12 @@ struct git_transport_options {
* transport_set_option(). * transport_set_option().
*/ */
struct oid_array *negotiation_tips; struct oid_array *negotiation_tips;
/*
* If allocated, whenever transport_fetch_refs() is called, add known
* common commits to this oidset instead of fetching any packfiles.
*/
struct oidset *acked_commits;
}; };
enum transport_family { enum transport_family {

Просмотреть файл

@ -103,6 +103,7 @@ struct upload_pack_data {
unsigned use_ofs_delta : 1; unsigned use_ofs_delta : 1;
unsigned no_progress : 1; unsigned no_progress : 1;
unsigned use_include_tag : 1; unsigned use_include_tag : 1;
unsigned wait_for_done : 1;
unsigned allow_filter : 1; unsigned allow_filter : 1;
unsigned allow_filter_fallback : 1; unsigned allow_filter_fallback : 1;
unsigned long tree_filter_max_depth; unsigned long tree_filter_max_depth;
@ -1496,6 +1497,10 @@ static void process_args(struct packet_reader *request,
data->done = 1; data->done = 1;
continue; continue;
} }
if (!strcmp(arg, "wait-for-done")) {
data->wait_for_done = 1;
continue;
}
/* Shallow related arguments */ /* Shallow related arguments */
if (process_shallow(arg, &data->shallows)) if (process_shallow(arg, &data->shallows))
@ -1578,7 +1583,7 @@ static int send_acks(struct upload_pack_data *data, struct oid_array *acks)
oid_to_hex(&acks->oid[i])); oid_to_hex(&acks->oid[i]));
} }
if (ok_to_give_up(data)) { if (!data->wait_for_done && ok_to_give_up(data)) {
/* Send Ready */ /* Send Ready */
packet_writer_write(&data->writer, "ready\n"); packet_writer_write(&data->writer, "ready\n");
return 1; return 1;
@ -1668,10 +1673,13 @@ int upload_pack_v2(struct repository *r, struct strvec *keys,
case FETCH_PROCESS_ARGS: case FETCH_PROCESS_ARGS:
process_args(request, &data); process_args(request, &data);
if (!data.want_obj.nr) { if (!data.want_obj.nr && !data.wait_for_done) {
/* /*
* Request didn't contain any 'want' lines, * Request didn't contain any 'want' lines (and
* guess they didn't want anything. * the request does not contain
* "wait-for-done", in which it is reasonable
* to just send 'have's without 'want's); guess
* they didn't want anything.
*/ */
state = FETCH_DONE; state = FETCH_DONE;
} else if (data.haves.nr) { } else if (data.haves.nr) {
@ -1723,7 +1731,7 @@ int upload_pack_advertise(struct repository *r,
int allow_sideband_all_value; int allow_sideband_all_value;
char *str = NULL; char *str = NULL;
strbuf_addstr(value, "shallow"); strbuf_addstr(value, "shallow wait-for-done");
if (!repo_config_get_bool(the_repository, if (!repo_config_get_bool(the_repository,
"uploadpack.allowfilter", "uploadpack.allowfilter",