fetch-pack: implement ref-in-want
authorBrandon Williams <bmwill@google.com>
Wed, 27 Jun 2018 22:30:23 +0000 (15:30 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 28 Jun 2018 16:33:30 +0000 (09:33 -0700)
Implement ref-in-want on the client side so that when a server supports
the "ref-in-want" feature, a client will send "want-ref" lines for each
reference the client wants to fetch. This feature allows clients to
tolerate inconsistencies that exist when a remote repository's refs
change during the course of negotiation.

This allows a client to request to request a particular ref without
specifying the OID of the ref. This means that instead of hitting an
error when a ref no longer points at the OID it did at the beginning of
negotiation, negotiation can continue and the value of that ref will be
sent at the termination of negotiation, just before a packfile is sent.

More information on the ref-in-want feature can be found in
Documentation/technical/protocol-v2.txt.

Signed-off-by: Brandon Williams <bmwill@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
fetch-pack.c
remote.c
remote.h
t/t5703-upload-pack-ref-in-want.sh
index 73890b8943595b2fe31a5b56cd390539ed26c23e..0b4a9f288f973a13ab8c55b420d52143ee1d026d 100644 (file)
@@ -1102,9 +1102,10 @@ static void add_shallow_requests(struct strbuf *req_buf,
 
 static void add_wants(const struct ref *wants, struct strbuf *req_buf)
 {
+       int use_ref_in_want = server_supports_feature("fetch", "ref-in-want", 0);
+
        for ( ; wants ; wants = wants->next) {
                const struct object_id *remote = &wants->old_oid;
-               const char *remote_hex;
                struct object *o;
 
                /*
@@ -1122,8 +1123,10 @@ static void add_wants(const struct ref *wants, struct strbuf *req_buf)
                        continue;
                }
 
-               remote_hex = oid_to_hex(remote);
-               packet_buf_write(req_buf, "want %s\n", remote_hex);
+               if (!use_ref_in_want || wants->exact_oid)
+                       packet_buf_write(req_buf, "want %s\n", oid_to_hex(remote));
+               else
+                       packet_buf_write(req_buf, "want-ref %s\n", wants->name);
        }
 }
 
@@ -1334,6 +1337,32 @@ static void receive_shallow_info(struct fetch_pack_args *args,
        args->deepen = 1;
 }
 
+static void receive_wanted_refs(struct packet_reader *reader, struct ref *refs)
+{
+       process_section_header(reader, "wanted-refs", 0);
+       while (packet_reader_read(reader) == PACKET_READ_NORMAL) {
+               struct object_id oid;
+               const char *end;
+               struct ref *r = NULL;
+
+               if (parse_oid_hex(reader->line, &oid, &end) || *end++ != ' ')
+                       die("expected wanted-ref, got '%s'", reader->line);
+
+               for (r = refs; r; r = r->next) {
+                       if (!strcmp(end, r->name)) {
+                               oidcpy(&r->old_oid, &oid);
+                               break;
+                       }
+               }
+
+               if (!r)
+                       die("unexpected wanted-ref: '%s'", reader->line);
+       }
+
+       if (reader->status != PACKET_READ_DELIM)
+               die("error processing wanted refs: %d", reader->status);
+}
+
 enum fetch_state {
        FETCH_CHECK_LOCAL = 0,
        FETCH_SEND_REQUEST,
@@ -1408,6 +1437,9 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
                        if (process_section_header(&reader, "shallow-info", 1))
                                receive_shallow_info(args, &reader);
 
+                       if (process_section_header(&reader, "wanted-refs", 1))
+                               receive_wanted_refs(&reader, ref);
+
                        /* get the pack */
                        process_section_header(&reader, "packfile", 0);
                        if (get_pack(args, fd, pack_lockfile))
index abe80c13972c718bd5d738063a2f526f2d12ee87..2c2376fff5296f0c8dc02674570847ba05cda120 100644 (file)
--- a/remote.c
+++ b/remote.c
@@ -1735,6 +1735,7 @@ int get_fetch_map(const struct ref *remote_refs,
                if (refspec->exact_sha1) {
                        ref_map = alloc_ref(name);
                        get_oid_hex(name, &ref_map->old_oid);
+                       ref_map->exact_oid = 1;
                } else {
                        ref_map = get_remote_ref(remote_refs, name);
                }
index 45ecc6cefafdea435d0fee39691f7458b940b877..976292152c020127e6c88d54e5448ef21e0eb4e8 100644 (file)
--- a/remote.h
+++ b/remote.h
@@ -73,6 +73,7 @@ struct ref {
                force:1,
                forced_update:1,
                expect_old_sha1:1,
+               exact_oid:1,
                deletion:1;
 
        enum {
index 32527a59c4dd52e89ca354fe71ecdeb20b9ce728..a73c55a47eeedd82a1ac1201348a02a9bd3b43fd 100755 (executable)
@@ -211,6 +211,18 @@ test_expect_success 'server is initially ahead - no ref in want' '
        grep "ERR upload-pack: not our ref" err
 '
 
+test_expect_success 'server is initially ahead - ref in want' '
+       git -C "$REPO" config uploadpack.allowRefInWant true &&
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       inconsistency master 1234567890123456789012345678901234567890 &&
+       git -C local fetch &&
+
+       git -C "$REPO" rev-parse --verify master >expected &&
+       git -C local rev-parse --verify refs/remotes/origin/master >actual &&
+       test_cmp expected actual
+'
+
 test_expect_success 'server is initially behind - no ref in want' '
        git -C "$REPO" config uploadpack.allowRefInWant false &&
        rm -rf local &&
@@ -223,6 +235,143 @@ test_expect_success 'server is initially behind - no ref in want' '
        test_cmp expected actual
 '
 
+test_expect_success 'server is initially behind - ref in want' '
+       git -C "$REPO" config uploadpack.allowRefInWant true &&
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       inconsistency master "master^" &&
+       git -C local fetch &&
+
+       git -C "$REPO" rev-parse --verify "master" >expected &&
+       git -C local rev-parse --verify refs/remotes/origin/master >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'server loses a ref - ref in want' '
+       git -C "$REPO" config uploadpack.allowRefInWant true &&
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       echo "s/master/raster/" >"$HTTPD_ROOT_PATH/one-time-sed" &&
+       test_must_fail git -C local fetch 2>err &&
+
+       grep "ERR unknown ref refs/heads/raster" err
+'
+
 stop_httpd
 
+REPO="$(pwd)/repo"
+LOCAL_PRISTINE="$(pwd)/local_pristine"
+
+# $REPO
+# c(o/foo) d(o/bar)
+#        \ /
+#         b   e(baz)  f(master)
+#          \__  |  __/
+#             \ | /
+#               a
+#
+# $LOCAL_PRISTINE
+#              s32(side)
+#              |
+#              .
+#              .
+#              |
+#              a(master)
+test_expect_success 'setup repos for fetching with ref-in-want tests' '
+       (
+               git init "$REPO" &&
+               cd "$REPO" &&
+               test_commit a &&
+
+               # Local repo with many commits (so that negotiation will take
+               # more than 1 request/response pair)
+               rm -rf "$LOCAL_PRISTINE" &&
+               git clone "file://$REPO" "$LOCAL_PRISTINE" &&
+               cd "$LOCAL_PRISTINE" &&
+               git checkout -b side &&
+               for i in $(seq 1 33); do test_commit s$i; done &&
+
+               # Add novel commits to upstream
+               git checkout master &&
+               cd "$REPO" &&
+               git checkout -b o/foo &&
+               test_commit b &&
+               test_commit c &&
+               git checkout -b o/bar b &&
+               test_commit d &&
+               git checkout -b baz a &&
+               test_commit e &&
+               git checkout master &&
+               test_commit f
+       ) &&
+       git -C "$REPO" config uploadpack.allowRefInWant true &&
+       git -C "$LOCAL_PRISTINE" config protocol.version 2
+'
+
+test_expect_success 'fetching with exact OID' '
+       test_when_finished "rm -f log" &&
+
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin \
+               $(git -C "$REPO" rev-parse d):refs/heads/actual &&
+
+       git -C "$REPO" rev-parse "d" >expected &&
+       git -C local rev-parse refs/heads/actual >actual &&
+       test_cmp expected actual &&
+       grep "want $(git -C "$REPO" rev-parse d)" log
+'
+
+test_expect_success 'fetching multiple refs' '
+       test_when_finished "rm -f log" &&
+
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin master baz &&
+
+       git -C "$REPO" rev-parse "master" "baz" >expected &&
+       git -C local rev-parse refs/remotes/origin/master refs/remotes/origin/baz >actual &&
+       test_cmp expected actual &&
+       grep "want-ref refs/heads/master" log &&
+       grep "want-ref refs/heads/baz" log
+'
+
+test_expect_success 'fetching ref and exact OID' '
+       test_when_finished "rm -f log" &&
+
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin \
+               master $(git -C "$REPO" rev-parse b):refs/heads/actual &&
+
+       git -C "$REPO" rev-parse "master" "b" >expected &&
+       git -C local rev-parse refs/remotes/origin/master refs/heads/actual >actual &&
+       test_cmp expected actual &&
+       grep "want $(git -C "$REPO" rev-parse b)" log &&
+       grep "want-ref refs/heads/master" log
+'
+
+test_expect_success 'fetching with wildcard that does not match any refs' '
+       test_when_finished "rm -f log" &&
+
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       git -C local fetch origin refs/heads/none*:refs/heads/* >out &&
+       test_must_be_empty out
+'
+
+test_expect_success 'fetching with wildcard that matches multiple refs' '
+       test_when_finished "rm -f log" &&
+
+       rm -rf local &&
+       cp -r "$LOCAL_PRISTINE" local &&
+       GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin refs/heads/o*:refs/heads/o* &&
+
+       git -C "$REPO" rev-parse "o/foo" "o/bar" >expected &&
+       git -C local rev-parse "o/foo" "o/bar" >actual &&
+       test_cmp expected actual &&
+       grep "want-ref refs/heads/o/foo" log &&
+       grep "want-ref refs/heads/o/bar" log
+'
+
 test_done