Skip to content

Commit 477673d

Browse files
jonathantanmygitster
authored andcommitted
send-pack: support push negotiation
Teach Git the push.negotiate config variable. Signed-off-by: Jonathan Tan <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 9c1e657 commit 477673d

File tree

3 files changed

+99
-4
lines changed

3 files changed

+99
-4
lines changed

Documentation/config/push.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,10 @@ push.useForceIfIncludes::
120120
`--force-if-includes` as an option to linkgit:git-push[1]
121121
in the command line. Adding `--no-force-if-includes` at the
122122
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

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ static void feed_object(const struct object_id *oid, FILE *fh, int negative)
5656
/*
5757
* Make a pack stream and spit it out into file descriptor fd
5858
*/
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)
6062
{
6163
/*
6264
* 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
9496
* parameters by writing to the pipe.
9597
*/
9698
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);
99103

100104
while (refs) {
101105
if (!is_null_oid(&refs->old_oid))
@@ -409,11 +413,55 @@ static void reject_invalid_nonce(const char *nonce, int len)
409413
}
410414
}
411415

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+
412459
int send_pack(struct send_pack_args *args,
413460
int fd[], struct child_process *conn,
414461
struct ref *remote_refs,
415462
struct oid_array *extra_have)
416463
{
464+
struct oid_array commons = OID_ARRAY_INIT;
417465
int in = fd[0];
418466
int out = fd[1];
419467
struct strbuf req_buf = STRBUF_INIT;
@@ -426,6 +474,7 @@ int send_pack(struct send_pack_args *args,
426474
int quiet_supported = 0;
427475
int agent_supported = 0;
428476
int advertise_sid = 0;
477+
int push_negotiate = 0;
429478
int use_atomic = 0;
430479
int atomic_supported = 0;
431480
int use_push_options = 0;
@@ -437,6 +486,10 @@ int send_pack(struct send_pack_args *args,
437486
const char *push_cert_nonce = NULL;
438487
struct packet_reader reader;
439488

489+
git_config_get_bool("push.negotiate", &push_negotiate);
490+
if (push_negotiate)
491+
get_commons_through_negotiation(args->url, remote_refs, &commons);
492+
440493
git_config_get_bool("transfer.advertisesid", &advertise_sid);
441494

442495
/* Does the other end support the reporting? */
@@ -625,7 +678,7 @@ int send_pack(struct send_pack_args *args,
625678
PACKET_READ_DIE_ON_ERR_PACKET);
626679

627680
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) {
629682
if (args->stateless_rpc)
630683
close(out);
631684
if (git_connection_is_socket(conn))

t/t5516-fetch-push.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,41 @@ test_expect_success 'fetch with pushInsteadOf (should not rewrite)' '
191191
)
192192
'
193193

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+
194229
test_expect_success 'push without wildcard' '
195230
mk_empty testrepo &&
196231

0 commit comments

Comments
 (0)