fetch-pack: implement ref-in-want

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>

Brandon Williams committed Jun 27, 2018 at 15:30 UTC 733020517a1baa6f4f76bb7bf48d8d8d14eecd6c
4 files changed +186 -3
fetch-pack.c
+35 -3
@@ -1102,9 +1102,10 @@ static void add_shallow_requests(struct strbuf *req_buf,
1102
1103 static void add_wants(const struct ref *wants, struct strbuf *req_buf)
1104 {
1105 + int use_ref_in_want = server_supports_feature("fetch", "ref-in-want", 0);
1106 +
1107 for ( ; wants ; wants = wants->next) {
1108 const struct object_id *remote = &wants->old_oid;
1107 - const char *remote_hex;
1109 struct object *o;
1110
1111 /*
@@ -1122,8 +1123,10 @@ static void add_wants(const struct ref *wants, struct strbuf *req_buf)
1123 continue;
1124 }
1125
1125 - remote_hex = oid_to_hex(remote);
1126 - packet_buf_write(req_buf, "want %s\n", remote_hex);
1126 + if (!use_ref_in_want || wants->exact_oid)
1127 + packet_buf_write(req_buf, "want %s\n", oid_to_hex(remote));
1128 + else
1129 + packet_buf_write(req_buf, "want-ref %s\n", wants->name);
1130 }
1131 }
1132
@@ -1334,6 +1337,32 @@ static void receive_shallow_info(struct fetch_pack_args *args,
1337 args->deepen = 1;
1338 }
1339
1340 +static void receive_wanted_refs(struct packet_reader *reader, struct ref *refs)
1341 +{
1342 + process_section_header(reader, "wanted-refs", 0);
1343 + while (packet_reader_read(reader) == PACKET_READ_NORMAL) {
1344 + struct object_id oid;
1345 + const char *end;
1346 + struct ref *r = NULL;
1347 +
1348 + if (parse_oid_hex(reader->line, &oid, &end) || *end++ != ' ')
1349 + die("expected wanted-ref, got '%s'", reader->line);
1350 +
1351 + for (r = refs; r; r = r->next) {
1352 + if (!strcmp(end, r->name)) {
1353 + oidcpy(&r->old_oid, &oid);
1354 + break;
1355 + }
1356 + }
1357 +
1358 + if (!r)
1359 + die("unexpected wanted-ref: '%s'", reader->line);
1360 + }
1361 +
1362 + if (reader->status != PACKET_READ_DELIM)
1363 + die("error processing wanted refs: %d", reader->status);
1364 +}
1365 +
1366 enum fetch_state {
1367 FETCH_CHECK_LOCAL = 0,
1368 FETCH_SEND_REQUEST,
@@ -1408,6 +1437,9 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
1437 if (process_section_header(&reader, "shallow-info", 1))
1438 receive_shallow_info(args, &reader);
1439
1440 + if (process_section_header(&reader, "wanted-refs", 1))
1441 + receive_wanted_refs(&reader, ref);
1442 +
1443 /* get the pack */
1444 process_section_header(&reader, "packfile", 0);
1445 if (get_pack(args, fd, pack_lockfile))
remote.c
+1
@@ -1735,6 +1735,7 @@ int get_fetch_map(const struct ref *remote_refs,
1735 if (refspec->exact_sha1) {
1736 ref_map = alloc_ref(name);
1737 get_oid_hex(name, &ref_map->old_oid);
1738 + ref_map->exact_oid = 1;
1739 } else {
1740 ref_map = get_remote_ref(remote_refs, name);
1741 }
remote.h
+1
@@ -73,6 +73,7 @@ struct ref {
73 force:1,
74 forced_update:1,
75 expect_old_sha1:1,
76 + exact_oid:1,
77 deletion:1;
78
79 enum {
t/t5703-upload-pack-ref-in-want.sh
+149
@@ -211,6 +211,18 @@ test_expect_success 'server is initially ahead - no ref in want' '
211 grep "ERR upload-pack: not our ref" err
212 '
213
214 +test_expect_success 'server is initially ahead - ref in want' '
215 + git -C "$REPO" config uploadpack.allowRefInWant true &&
216 + rm -rf local &&
217 + cp -r "$LOCAL_PRISTINE" local &&
218 + inconsistency master 1234567890123456789012345678901234567890 &&
219 + git -C local fetch &&
220 +
221 + git -C "$REPO" rev-parse --verify master >expected &&
222 + git -C local rev-parse --verify refs/remotes/origin/master >actual &&
223 + test_cmp expected actual
224 +'
225 +
226 test_expect_success 'server is initially behind - no ref in want' '
227 git -C "$REPO" config uploadpack.allowRefInWant false &&
228 rm -rf local &&
@@ -223,6 +235,143 @@ test_expect_success 'server is initially behind - no ref in want' '
235 test_cmp expected actual
236 '
237
238 +test_expect_success 'server is initially behind - ref in want' '
239 + git -C "$REPO" config uploadpack.allowRefInWant true &&
240 + rm -rf local &&
241 + cp -r "$LOCAL_PRISTINE" local &&
242 + inconsistency master "master^" &&
243 + git -C local fetch &&
244 +
245 + git -C "$REPO" rev-parse --verify "master" >expected &&
246 + git -C local rev-parse --verify refs/remotes/origin/master >actual &&
247 + test_cmp expected actual
248 +'
249 +
250 +test_expect_success 'server loses a ref - ref in want' '
251 + git -C "$REPO" config uploadpack.allowRefInWant true &&
252 + rm -rf local &&
253 + cp -r "$LOCAL_PRISTINE" local &&
254 + echo "s/master/raster/" >"$HTTPD_ROOT_PATH/one-time-sed" &&
255 + test_must_fail git -C local fetch 2>err &&
256 +
257 + grep "ERR unknown ref refs/heads/raster" err
258 +'
259 +
260 stop_httpd
261
262 +REPO="$(pwd)/repo"
263 +LOCAL_PRISTINE="$(pwd)/local_pristine"
264 +
265 +# $REPO
266 +# c(o/foo) d(o/bar)
267 +# \ /
268 +# b e(baz) f(master)
269 +# \__ | __/
270 +# \ | /
271 +# a
272 +#
273 +# $LOCAL_PRISTINE
274 +# s32(side)
275 +# |
276 +# .
277 +# .
278 +# |
279 +# a(master)
280 +test_expect_success 'setup repos for fetching with ref-in-want tests' '
281 + (
282 + git init "$REPO" &&
283 + cd "$REPO" &&
284 + test_commit a &&
285 +
286 + # Local repo with many commits (so that negotiation will take
287 + # more than 1 request/response pair)
288 + rm -rf "$LOCAL_PRISTINE" &&
289 + git clone "file://$REPO" "$LOCAL_PRISTINE" &&
290 + cd "$LOCAL_PRISTINE" &&
291 + git checkout -b side &&
292 + for i in $(seq 1 33); do test_commit s$i; done &&
293 +
294 + # Add novel commits to upstream
295 + git checkout master &&
296 + cd "$REPO" &&
297 + git checkout -b o/foo &&
298 + test_commit b &&
299 + test_commit c &&
300 + git checkout -b o/bar b &&
301 + test_commit d &&
302 + git checkout -b baz a &&
303 + test_commit e &&
304 + git checkout master &&
305 + test_commit f
306 + ) &&
307 + git -C "$REPO" config uploadpack.allowRefInWant true &&
308 + git -C "$LOCAL_PRISTINE" config protocol.version 2
309 +'
310 +
311 +test_expect_success 'fetching with exact OID' '
312 + test_when_finished "rm -f log" &&
313 +
314 + rm -rf local &&
315 + cp -r "$LOCAL_PRISTINE" local &&
316 + GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin \
317 + $(git -C "$REPO" rev-parse d):refs/heads/actual &&
318 +
319 + git -C "$REPO" rev-parse "d" >expected &&
320 + git -C local rev-parse refs/heads/actual >actual &&
321 + test_cmp expected actual &&
322 + grep "want $(git -C "$REPO" rev-parse d)" log
323 +'
324 +
325 +test_expect_success 'fetching multiple refs' '
326 + test_when_finished "rm -f log" &&
327 +
328 + rm -rf local &&
329 + cp -r "$LOCAL_PRISTINE" local &&
330 + GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin master baz &&
331 +
332 + git -C "$REPO" rev-parse "master" "baz" >expected &&
333 + git -C local rev-parse refs/remotes/origin/master refs/remotes/origin/baz >actual &&
334 + test_cmp expected actual &&
335 + grep "want-ref refs/heads/master" log &&
336 + grep "want-ref refs/heads/baz" log
337 +'
338 +
339 +test_expect_success 'fetching ref and exact OID' '
340 + test_when_finished "rm -f log" &&
341 +
342 + rm -rf local &&
343 + cp -r "$LOCAL_PRISTINE" local &&
344 + GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin \
345 + master $(git -C "$REPO" rev-parse b):refs/heads/actual &&
346 +
347 + git -C "$REPO" rev-parse "master" "b" >expected &&
348 + git -C local rev-parse refs/remotes/origin/master refs/heads/actual >actual &&
349 + test_cmp expected actual &&
350 + grep "want $(git -C "$REPO" rev-parse b)" log &&
351 + grep "want-ref refs/heads/master" log
352 +'
353 +
354 +test_expect_success 'fetching with wildcard that does not match any refs' '
355 + test_when_finished "rm -f log" &&
356 +
357 + rm -rf local &&
358 + cp -r "$LOCAL_PRISTINE" local &&
359 + git -C local fetch origin refs/heads/none*:refs/heads/* >out &&
360 + test_must_be_empty out
361 +'
362 +
363 +test_expect_success 'fetching with wildcard that matches multiple refs' '
364 + test_when_finished "rm -f log" &&
365 +
366 + rm -rf local &&
367 + cp -r "$LOCAL_PRISTINE" local &&
368 + GIT_TRACE_PACKET="$(pwd)/log" git -C local fetch origin refs/heads/o*:refs/heads/o* &&
369 +
370 + git -C "$REPO" rev-parse "o/foo" "o/bar" >expected &&
371 + git -C local rev-parse "o/foo" "o/bar" >actual &&
372 + test_cmp expected actual &&
373 + grep "want-ref refs/heads/o/foo" log &&
374 + grep "want-ref refs/heads/o/bar" log
375 +'
376 +
377 test_done