]> git.ipfire.org Git - thirdparty/git.git/commitdiff
send-pack: support push negotiation
authorJonathan Tan <jonathantanmy@google.com>
Tue, 4 May 2021 21:16:02 +0000 (14:16 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 5 May 2021 01:41:29 +0000 (10:41 +0900)
Teach Git the push.negotiate config variable.

Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/config/push.txt
send-pack.c
t/t5516-fetch-push.sh

index 21b256e0a4ec8af1dd1227ec2408ff924f988233..f2667b2689945399ab8eaa0e28db9b42c7fccec9 100644 (file)
@@ -120,3 +120,10 @@ push.useForceIfIncludes::
        `--force-if-includes` as an option to linkgit:git-push[1]
        in the command line. Adding `--no-force-if-includes` at the
        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.
index 5f215b13c7dc1a8a020094236625554db94591b2..9cb9f716509b00ec394bb901b61d679eaa04045d 100644 (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
  */
-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
@@ -94,8 +96,10 @@ static int pack_objects(int fd, struct ref *refs, struct oid_array *extra, struc
         * parameters by writing to the pipe.
         */
        po_in = xfdopen(po.in, "w");
-       for (i = 0; i < extra->nr; i++)
-               feed_object(&extra->oid[i], po_in, 1);
+       for (i = 0; i < advertised->nr; i++)
+               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) {
                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 fd[], struct child_process *conn,
              struct ref *remote_refs,
              struct oid_array *extra_have)
 {
+       struct oid_array commons = OID_ARRAY_INIT;
        int in = fd[0];
        int out = fd[1];
        struct strbuf req_buf = STRBUF_INIT;
@@ -426,6 +474,7 @@ int send_pack(struct send_pack_args *args,
        int quiet_supported = 0;
        int agent_supported = 0;
        int advertise_sid = 0;
+       int push_negotiate = 0;
        int use_atomic = 0;
        int atomic_supported = 0;
        int use_push_options = 0;
@@ -437,6 +486,10 @@ int send_pack(struct send_pack_args *args,
        const char *push_cert_nonce = NULL;
        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);
 
        /* 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);
 
        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)
                                close(out);
                        if (git_connection_is_socket(conn))
index f11742ed5999eeb710901226b81d628404021cd6..0916f7630207be03ce2af25368146c1302375e27 100755 (executable)
@@ -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' '
        mk_empty testrepo &&