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