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>

Jonathan Tan committed May 4, 2021 at 14:16 UTC 477673d6f39b4829baa98ad88d6b65b35b79fd0f
3 files changed +99 -4
Documentation/config/push.txt
+7
@@ -120,3 +120,10 @@ push.useForceIfIncludes::
120 `--force-if-includes` as an option to linkgit:git-push[1]
121 in the command line. Adding `--no-force-if-includes` at the
122 time of push overrides this configuration setting.
123 +
124 +push.negotiate::
125 + If set to "true", attempt to reduce the size of the packfile
126 + sent by rounds of negotiation in which the client and the
127 + server attempt to find commits in common. If "false", Git will
128 + rely solely on the server's ref advertisement to find commits
129 + in common.
send-pack.c
+57 -4
@@ -56,7 +56,9 @@ static void feed_object(const struct object_id *oid, FILE *fh, int negative)
56 /*
57 * Make a pack stream and spit it out into file descriptor fd
58 */
59 -static int pack_objects(int fd, struct ref *refs, struct oid_array *extra, struct send_pack_args *args)
59 +static int pack_objects(int fd, struct ref *refs, struct oid_array *advertised,
60 + struct oid_array *negotiated,
61 + struct send_pack_args *args)
62 {
63 /*
64 * 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
96 * parameters by writing to the pipe.
97 */
98 po_in = xfdopen(po.in, "w");
97 - for (i = 0; i < extra->nr; i++)
98 - feed_object(&extra->oid[i], po_in, 1);
99 + for (i = 0; i < advertised->nr; i++)
100 + feed_object(&advertised->oid[i], po_in, 1);
101 + for (i = 0; i < negotiated->nr; i++)
102 + feed_object(&negotiated->oid[i], po_in, 1);
103
104 while (refs) {
105 if (!is_null_oid(&refs->old_oid))
@@ -409,11 +413,55 @@ static void reject_invalid_nonce(const char *nonce, int len)
413 }
414 }
415
416 +static void get_commons_through_negotiation(const char *url,
417 + const struct ref *remote_refs,
418 + struct oid_array *commons)
419 +{
420 + struct child_process child = CHILD_PROCESS_INIT;
421 + const struct ref *ref;
422 + int len = the_hash_algo->hexsz + 1; /* hash + NL */
423 +
424 + child.git_cmd = 1;
425 + child.no_stdin = 1;
426 + child.out = -1;
427 + strvec_pushl(&child.args, "fetch", "--negotiate-only", NULL);
428 + for (ref = remote_refs; ref; ref = ref->next)
429 + strvec_pushf(&child.args, "--negotiation-tip=%s", oid_to_hex(&ref->new_oid));
430 + strvec_push(&child.args, url);
431 +
432 + if (start_command(&child))
433 + die(_("send-pack: unable to fork off fetch subprocess"));
434 +
435 + do {
436 + char hex_hash[GIT_MAX_HEXSZ + 1];
437 + int read_len = read_in_full(child.out, hex_hash, len);
438 + struct object_id oid;
439 + const char *end;
440 +
441 + if (!read_len)
442 + break;
443 + if (read_len != len)
444 + die("invalid length read %d", read_len);
445 + if (parse_oid_hex(hex_hash, &oid, &end) || *end != '\n')
446 + die("invalid hash");
447 + oid_array_append(commons, &oid);
448 + } while (1);
449 +
450 + if (finish_command(&child)) {
451 + /*
452 + * The information that push negotiation provides is useful but
453 + * not mandatory.
454 + */
455 + warning(_("push negotiation failed; proceeding anyway with push"));
456 + }
457 +}
458 +
459 int send_pack(struct send_pack_args *args,
460 int fd[], struct child_process *conn,
461 struct ref *remote_refs,
462 struct oid_array *extra_have)
463 {
464 + struct oid_array commons = OID_ARRAY_INIT;
465 int in = fd[0];
466 int out = fd[1];
467 struct strbuf req_buf = STRBUF_INIT;
@@ -426,6 +474,7 @@ int send_pack(struct send_pack_args *args,
474 int quiet_supported = 0;
475 int agent_supported = 0;
476 int advertise_sid = 0;
477 + int push_negotiate = 0;
478 int use_atomic = 0;
479 int atomic_supported = 0;
480 int use_push_options = 0;
@@ -437,6 +486,10 @@ int send_pack(struct send_pack_args *args,
486 const char *push_cert_nonce = NULL;
487 struct packet_reader reader;
488
489 + git_config_get_bool("push.negotiate", &push_negotiate);
490 + if (push_negotiate)
491 + get_commons_through_negotiation(args->url, remote_refs, &commons);
492 +
493 git_config_get_bool("transfer.advertisesid", &advertise_sid);
494
495 /* Does the other end support the reporting? */
@@ -625,7 +678,7 @@ int send_pack(struct send_pack_args *args,
678 PACKET_READ_DIE_ON_ERR_PACKET);
679
680 if (need_pack_data && cmds_sent) {
628 - if (pack_objects(out, remote_refs, extra_have, args) < 0) {
681 + if (pack_objects(out, remote_refs, extra_have, &commons, args) < 0) {
682 if (args->stateless_rpc)
683 close(out);
684 if (git_connection_is_socket(conn))
t/t5516-fetch-push.sh
+35
@@ -191,6 +191,41 @@ test_expect_success 'fetch with pushInsteadOf (should not rewrite)' '
191 )
192 '
193
194 +grep_wrote () {
195 + object_count=$1
196 + file_name=$2
197 + grep 'write_pack_file/wrote.*"value":"'$1'"' $2
198 +}
199 +
200 +test_expect_success 'push with negotiation' '
201 + # Without negotiation
202 + mk_empty testrepo &&
203 + git push testrepo $the_first_commit:refs/remotes/origin/first_commit &&
204 + git -C testrepo config receive.hideRefs refs/remotes/origin/first_commit &&
205 + echo now pushing without negotiation &&
206 + GIT_TRACE2_EVENT="$(pwd)/event" git -c protocol.version=2 push testrepo refs/heads/main:refs/remotes/origin/main &&
207 + grep_wrote 5 event && # 2 commits, 2 trees, 1 blob
208 +
209 + # Same commands, but with negotiation
210 + rm event &&
211 + mk_empty testrepo &&
212 + git push testrepo $the_first_commit:refs/remotes/origin/first_commit &&
213 + git -C testrepo config receive.hideRefs refs/remotes/origin/first_commit &&
214 + GIT_TRACE2_EVENT="$(pwd)/event" git -c protocol.version=2 -c push.negotiate=1 push testrepo refs/heads/main:refs/remotes/origin/main &&
215 + grep_wrote 2 event # 1 commit, 1 tree
216 +'
217 +
218 +test_expect_success 'push with negotiation proceeds anyway even if negotiation fails' '
219 + rm event &&
220 + mk_empty testrepo &&
221 + git push testrepo $the_first_commit:refs/remotes/origin/first_commit &&
222 + git -C testrepo config receive.hideRefs refs/remotes/origin/first_commit &&
223 + GIT_TEST_PROTOCOL_VERSION=0 GIT_TRACE2_EVENT="$(pwd)/event" \
224 + git -c push.negotiate=1 push testrepo refs/heads/main:refs/remotes/origin/main 2>err &&
225 + grep_wrote 5 event && # 2 commits, 2 trees, 1 blob
226 + test_i18ngrep "push negotiation failed" err
227 +'
228 +
229 test_expect_success 'push without wildcard' '
230 mk_empty testrepo &&
231