send-pack: support push negotiation

Teach Git the push.negotiate config variable.

Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jonathan Tan 2021-05-04 14:16:02 -07:00 committed by Junio C Hamano
parent 9c1e657a8f
commit 477673d6f3
3 changed files with 99 additions and 4 deletions

View file

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

View file

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

View file

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