upload-pack: advertise capabilities when cloning empty repos

When cloning an empty repository, protocol versions 0 and 1 currently offer nothing but the header and flush packets for the /info/refs endpoint. This means that no capabilities are provided, so the client side doesn't know what capabilities are present. However, this does pose a problem when working with SHA-256 repositories, since we use the capabilities to know the remote side's object format (hash algorithm). As of 8b214c2e9d ("clone: propagate object-format when cloning from void", 2023-04-05), this has been fixed for protocol v2, since there we always read the hash algorithm from the remote. Fortunately, the push version of the protocol already indicates a clue for how to solve this. When the /info/refs endpoint is accessed for a push and the remote is empty, we include a dummy "capabilities^{}" ref pointing to the all-zeros object ID. The protocol documentation already indicates this should _always_ be sent, even for fetches and clones, so let's just do that, which means we'll properly announce the hash algorithm as part of the capabilities. This just works with the existing code because we share the same ref code for fetches and clones, and libgit2, JGit, and dulwich do as well. There is one minor issue to fix, though. If we called send_ref with namespaces, we would return NULL with the capabilities entry, which would cause a crash. Instead, let's refactor out a function to print just the ref itself without stripping the namespace and use it for our special capabilities entry. Add several sets of tests for HTTP as well as for local clones. The behavior can be slightly different for HTTP versus a local or SSH clone because of the stateless-rpc functionality, so it's worth testing both. Signed-off-by: brian m. carlson <bk2204@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

brian m. carlson committed May 17, 2023 at 19:24 UTC 933e3a4ee205353d8f093d5dfcd226fa432c4e58
3 files changed +73 -7
t/t5551-http-fetch-smart.sh
+27
@@ -611,6 +611,33 @@ test_expect_success 'client falls back from v2 to v0 to match server' '
611 grep symref=HEAD:refs/heads/ trace
612 '
613
614 +test_expect_success 'create empty http-accessible SHA-256 repository' '
615 + mkdir "$HTTPD_DOCUMENT_ROOT_PATH/sha256.git" &&
616 + (cd "$HTTPD_DOCUMENT_ROOT_PATH/sha256.git" &&
617 + git --bare init --object-format=sha256
618 + )
619 +'
620 +
621 +test_expect_success 'clone empty SHA-256 repository with protocol v2' '
622 + rm -fr sha256 &&
623 + echo sha256 >expected &&
624 + git -c protocol.version=2 clone "$HTTPD_URL/smart/sha256.git" &&
625 + git -C sha256 rev-parse --show-object-format >actual &&
626 + test_cmp actual expected &&
627 + git ls-remote "$HTTPD_URL/smart/sha256.git" >actual &&
628 + test_must_be_empty actual
629 +'
630 +
631 +test_expect_success 'clone empty SHA-256 repository with protocol v0' '
632 + rm -fr sha256 &&
633 + echo sha256 >expected &&
634 + GIT_TRACE=1 GIT_TRACE_PACKET=1 git -c protocol.version=0 clone "$HTTPD_URL/smart/sha256.git" &&
635 + git -C sha256 rev-parse --show-object-format >actual &&
636 + test_cmp actual expected &&
637 + git ls-remote "$HTTPD_URL/smart/sha256.git" >actual &&
638 + test_must_be_empty actual
639 +'
640 +
641 test_expect_success 'passing hostname resolution information works' '
642 BOGUS_HOST=gitbogusexamplehost.invalid &&
643 BOGUS_HTTPD_URL=$HTTPD_PROTO://$BOGUS_HOST:$LIB_HTTPD_PORT &&
t/t5700-protocol-v1.sh
+29 -2
@@ -244,15 +244,28 @@ test_expect_success 'push with ssh:// using protocol v1' '
244 grep "push< version 1" log
245 '
246
247 +test_expect_success 'clone propagates object-format from empty repo' '
248 + test_when_finished "rm -fr src256 dst256" &&
249 +
250 + echo sha256 >expect &&
251 + git init --object-format=sha256 src256 &&
252 + git clone --no-local src256 dst256 &&
253 + git -C dst256 rev-parse --show-object-format >actual &&
254 +
255 + test_cmp expect actual
256 +'
257 +
258 # Test protocol v1 with 'http://' transport
259 #
260 . "$TEST_DIRECTORY"/lib-httpd.sh
261 start_httpd
262
252 -test_expect_success 'create repo to be served by http:// transport' '
263 +test_expect_success 'create repos to be served by http:// transport' '
264 git init "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" &&
265 git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config http.receivepack true &&
255 - test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" one
266 + test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" one &&
267 + git init --object-format=sha256 "$HTTPD_DOCUMENT_ROOT_PATH/sha256" &&
268 + git -C "$HTTPD_DOCUMENT_ROOT_PATH/sha256" config http.receivepack true
269 '
270
271 test_expect_success 'clone with http:// using protocol v1' '
@@ -269,6 +282,20 @@ test_expect_success 'clone with http:// using protocol v1' '
282 grep "git< version 1" log
283 '
284
285 +test_expect_success 'clone with http:// using protocol v1 with empty SHA-256 repo' '
286 + GIT_TRACE_PACKET=1 GIT_TRACE_CURL=1 git -c protocol.version=1 \
287 + clone "$HTTPD_URL/smart/sha256" sha256 2>log &&
288 +
289 + echo sha256 >expect &&
290 + git -C sha256 rev-parse --show-object-format >actual &&
291 + test_cmp expect actual &&
292 +
293 + # Client requested to use protocol v1
294 + grep "Git-Protocol: version=1" log &&
295 + # Server responded using protocol v1
296 + grep "git< version 1" log
297 +'
298 +
299 test_expect_success 'fetch with http:// using protocol v1' '
300 test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" two &&
301
upload-pack.c
+17 -5
@@ -120,6 +120,7 @@ struct upload_pack_data {
120 unsigned allow_ref_in_want : 1; /* v2 only */
121 unsigned allow_sideband_all : 1; /* v2 only */
122 unsigned advertise_sid : 1;
123 + unsigned sent_capabilities : 1;
124 };
125
126 static void upload_pack_data_init(struct upload_pack_data *data)
@@ -1206,18 +1207,17 @@ static void format_session_id(struct strbuf *buf, struct upload_pack_data *d) {
1207 strbuf_addf(buf, " session-id=%s", trace2_session_id());
1208 }
1209
1209 -static int send_ref(const char *refname, const struct object_id *oid,
1210 - int flag UNUSED, void *cb_data)
1210 +static void write_v0_ref(struct upload_pack_data *data,
1211 + const char *refname, const char *refname_nons,
1212 + const struct object_id *oid)
1213 {
1214 static const char *capabilities = "multi_ack thin-pack side-band"
1215 " side-band-64k ofs-delta shallow deepen-since deepen-not"
1216 " deepen-relative no-progress include-tag multi_ack_detailed";
1215 - const char *refname_nons = strip_namespace(refname);
1217 struct object_id peeled;
1217 - struct upload_pack_data *data = cb_data;
1218
1219 if (mark_our_ref(refname_nons, refname, oid, &data->hidden_refs))
1220 - return 0;
1220 + return;
1221
1222 if (capabilities) {
1223 struct strbuf symref_info = STRBUF_INIT;
@@ -1240,12 +1240,20 @@ static int send_ref(const char *refname, const struct object_id *oid,
1240 git_user_agent_sanitized());
1241 strbuf_release(&symref_info);
1242 strbuf_release(&session_id);
1243 + data->sent_capabilities = 1;
1244 } else {
1245 packet_fwrite_fmt(stdout, "%s %s\n", oid_to_hex(oid), refname_nons);
1246 }
1247 capabilities = NULL;
1248 if (!peel_iterated_oid(oid, &peeled))
1249 packet_fwrite_fmt(stdout, "%s %s^{}\n", oid_to_hex(&peeled), refname_nons);
1250 + return;
1251 +}
1252 +
1253 +static int send_ref(const char *refname, const struct object_id *oid,
1254 + int flag UNUSED, void *cb_data)
1255 +{
1256 + write_v0_ref(cb_data, refname, strip_namespace(refname), oid);
1257 return 0;
1258 }
1259
@@ -1379,6 +1387,10 @@ void upload_pack(const int advertise_refs, const int stateless_rpc,
1387 data.no_done = 1;
1388 head_ref_namespaced(send_ref, &data);
1389 for_each_namespaced_ref(send_ref, &data);
1390 + if (!data.sent_capabilities) {
1391 + const char *refname = "capabilities^{}";
1392 + write_v0_ref(&data, refname, refname, null_oid());
1393 + }
1394 /*
1395 * fflush stdout before calling advertise_shallow_grafts because send_ref
1396 * uses stdio.