{"thread":{"id":"56782","subject":"[PATCH 0/3] bundle-uri: \"dumb\" static CDN offloading, spec & server implementation","startedAt":"2021-10-25T21:26:10Z","lastAt":"2022-04-22T09:37:55Z","messageCount":77,"participants":["Ævar Arnfjörð Bjarmason","Derrick Stolee","Philip Oakley"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"439593","messageId":"cover-0.3-00000000000-20211025T211159Z-avarab@gmail.com","threadId":"56782","inReplyTo":null,"subject":"[PATCH 0/3] bundle-uri: \"dumb\" static CDN offloading, spec & server implementation","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-25T21:25:43Z","receivedAt":"2021-10-25T21:26:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This implements a new \"bundle-uri\" protocol v2 extension, which allows\nservers to advertise *.bundle files which clients can pre-seed their\nfull \"clone\"'s or incremental \"fetch\"'s from.\n\nThis is both an alternative to, and complimentary to the existing\n\"packfile-uri\" mechanism, i.e. servers and/or clients can pick one or\nboth, but would generally pick one over the other.\n\nThis \"bundle-uri\" mechanism has the advantage of being dumber, and\noffloads more complexity from the server side to the client\nside.\n\nUnlike with packfile-uri a conforming server doesn't need produce a\nPACK that (hopefully, otherwise there's not much point) excludes OIDs\nthat it knows it'll provide via a packfile-uri.\n\nTo the server a \"bundle-uri\" negotiation the same as a \"normal\" one,\nthe client just happens to provide OIDs it found in bundles as \"have\"\nlines.\n\nIn my WIP client patches I even have a (trivial to implement) mode\nwhere a client can choose to pretend that a server reported that a\ngiven set of bundle URIs can be used to pre-seed its \"clone\" or\n\"fetch\".\n\nA client can thus use use a CDN it controls to optimistically pre-seed\na clone from a server that knows nothing about \"bundle-uri\", sort of\nlike a \"git clone --reference <path> --dissociate\", except with a\n<uri> instead of a <path>.\n\nNeed re-clone a bunch of large repositories on CI boxes from\ngit.example.com, but git.example.com doesn't support \"bundle-uri\", and\nyou've got a slow outbound connection? Just point to a pre-seeding CDN\nyou control.\n\nThere are disadvantages to this over packfile-uri, JGit has a mature\nimplementation of it, and I doubt that e.g. Google will ever want to\nuse this, since that feature was tailor-made for their use case.\n\nE.g. a repository that has a *.pack sitting on disk can't re-use and\nstream it out with sendfile() as it could with a \"packfile-uri\",\ninstead it would need to point to some duplicate of that data in\n*.bundle form (or on-the-fly generate a header for the *.pack).\n\nThe goal of this feature isn't to win over packfile-uri users, but to\ngive users who wouldn't consider it due to its tight coupling to have\naccess to CDN offloading.\n\nThe error optimistic recovery of \"bundle-uri\" and looseer coupling\nbetween server and CDN means that it should be easy to use this for\nuse where the CDN is something like say Debian's mirror network.\n\nWe're coming up on 2.34.0-rc0, so this certainly won't be in 2.34.0,\nbut I'm submitting this now per discussion during '#git-devel' standup\ntoday.\n\nThere was a discussion on the RFC version of the larger series of\npatches to implement this \"bundle-uri\"[1].\n\nI've updated the protocol-v2.txt changes in 2/3 a lot in response to\nthat, in particular I've specified and implemented early client\ndisconnection behavior, so bundle-uri SHOULD never cause\nclient<->server dialog to hang (at most we'll need to re-connect, if\nwe need to fall back from a failed bundle-uri).\n\n1. https://lore.kernel.org/git/RFC-cover-00.13-0000000000-20210805T150534Z-avarab@gmail.com/\n\nÆvar Arnfjörð Bjarmason (3):\n  leak tests: mark t5701-git-serve.sh as passing SANITIZE=leak\n  protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"\n  bundle-uri client: add \"bundle-uri\" parsing + tests\n\n Documentation/technical/protocol-v2.txt | 209 ++++++++++++++++++++++++\n Makefile                                |   2 +\n bundle-uri.c                            | 179 ++++++++++++++++++++\n bundle-uri.h                            |  30 ++++\n serve.c                                 |   6 +\n t/helper/test-bundle-uri.c              |  83 ++++++++++\n t/helper/test-tool.c                    |   1 +\n t/helper/test-tool.h                    |   1 +\n t/t5701-git-serve.sh                    | 125 +++++++++++++-\n t/t5750-bundle-uri-parse.sh             | 153 +++++++++++++++++\n 10 files changed, 788 insertions(+), 1 deletion(-)\n create mode 100644 bundle-uri.c\n create mode 100644 bundle-uri.h\n create mode 100644 t/helper/test-bundle-uri.c\n create mode 100755 t/t5750-bundle-uri-parse.sh\n\n-- \n2.33.1.1511.gd15d1b313a6\n\n"},{"id":"439594","messageId":"patch-1.3-7639b9bbac5-20211025T211159Z-avarab@gmail.com","threadId":"56782","inReplyTo":"cover-0.3-00000000000-20211025T211159Z-avarab@gmail.com","subject":"[PATCH 1/3] leak tests: mark t5701-git-serve.sh as passing SANITIZE=leak","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-25T21:25:44Z","receivedAt":"2021-10-25T21:26:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"The \"t5701-git-serve.sh\" test passes when run under a git compiled\nwith SANITIZE=leak, let's mark it as such to add it to the\n\"linux-leaks\" CI job.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t5701-git-serve.sh | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh\nindex aa1827d841d..1896f671cb3 100755\n--- a/t/t5701-git-serve.sh\n+++ b/t/t5701-git-serve.sh\n@@ -5,6 +5,7 @@ test_description='test protocol v2 server commands'\n GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n+TEST_PASSES_SANITIZE_LEAK=true\n . ./test-lib.sh\n \n test_expect_success 'test capability advertisement' '\n-- \n2.33.1.1511.gd15d1b313a6\n\n"},{"id":"439595","messageId":"patch-2.3-3ac0539c053-20211025T211159Z-avarab@gmail.com","threadId":"56782","inReplyTo":"cover-0.3-00000000000-20211025T211159Z-avarab@gmail.com","subject":"[PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-25T21:25:45Z","receivedAt":"2021-10-25T21:26:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a server-side implementation of a new \"bundle-uri\" command to\nprotocol v2. As discussed in the updated \"protocol-v2.txt\" this will\nallow conforming clients to optionally seed their initial clones or\nincremental fetches from URLs containing \"*.bundle\" files created with\n\"git bundle create\".\n\nThe use-cases are similar to those of the existing \"Packfile URIs\",\nand the two feature can be combined within a single request, but\n\"bundle-uri\" has a few advantages over packfile-uris in some some\ncommon scenarios, discussed below.\n\nThis change does not give us a working \"bundle-uri\" client. I have\nthose patches as a follow-up, but let's first establish what the\nprotocol for this should be like first. The client implementation will\nthen implement this specification.\n\nWith this change when the uploadpack.bundleURI config is set to a\nURI (or URIs, if set >1 times), advertise a \"bundle-uri\" command. Then\nwhen the client requests \"bundle-uri\" emit those URIs back at them.\n\nDifferences between this and the existing packfile-uri facility:\n\n A. There is no \"real\" support for packfile-uri in git.git. The\n    uploadpack.blobPackfileUri setting allows carving out a list of\n    blobs (actually any OIDs), but as alluded to in bfc2a36ff2a (Doc:\n    clarify contents of packfile sent as URI, 2021-01-20) the only\n    \"real\" implementation is JGit based.\n\n B. The uploadpack.blobPackfileUri is a MUST where this is a\n    \"CAN\". I.e. once a client says they support packfile-uri of given\n    list of protocols the server will send them a PACK response\n    assuming they've downloaded the URI they client was sent, if the\n    client doesn't do that they don't have a valid repository.\n\n    Pointing at a bundle and having the client send us \"have\"\n    lines (or not, maybe they couldn't fetch it, or decided they\n    didn't want to) is more flexible, and can gracefully recover\n    e.g. if the CDN isn't reachable (maybe you do support \"https\", but\n    the CDN provider is down, or blocked your whole country).\n\n C. The client, after executing \"ls-refs\" will disconnect if it has\n    also grabbed the \"bundle-uris\" and knows the server won't send it\n    anything it doesn't already have (or expect to have, if it's\n    downloading the bundles concurrent to an early disconnect).\n\n    This is in (small) contrast to packfile-uri where a client would\n    enter a negotiation dialog, which may or may not result in a\n    packfile-uri and/or an inline PACK.\n\n D. Because of \"C\" clients can, if the bundles are up-to-date, get an\n    up-to-date repository with just \"bundle-uri\" and \"ls-refs\" commands,\n    with no need to enter a dialog with \"git upload-pack\".\n\n    That small dialog is unlikely to matter for performance purposes,\n    this section is just noting differences between \"bundle-uri\" and\n    \"packfile-uri\".\n\nAs noted above the features are compatible, a client that supports\n\"bundle-uri\" and \"packfile-uri\" might download a bundle, and then\nproceed with a \"fetch\" dialog, that dialog might then result in\n\"packfile-uri\" response.\n\nIn practice server operators are unlikely to want to mix the two,\nsince the main benefit of either approach is the ability to offload\nlarge \"clone\" responses to CDNs. A server operator would have little\nreason not to go with one approach or the other.\n\nThere was a suggestion of implementing a similar feature long ago[1]\nby Jeff King. The main difference between it and this approach is that\nwe've since gained protocol v2, so we can add this as an optional path\nin the dialog between client and server. The 2011 implementation\nhooked into the transport mechanism to try to clone from a bundle\ndirectly. See also [2] and [3] for some later mentions of that\napproach.\n\nSee also [4] for the series that implemented\nuploadpack.blobPackfileUri, and [5] for a series on top that did the\n.gitmodules check in that context. See [6] for the \"ls-refs unborn\"\nfeature which modified code in similar areas of the request flow.\n\n1. https://lore.kernel.org/git/20111110074330.GA27925@sigill.intra.peff.net/\n2. https://lore.kernel.org/git/20190514092900.GA11679@sigill.intra.peff.net/\n3. https://lore.kernel.org/git/YFJWz5yIGng+a16k@coredump.intra.peff.net/\n4. https://lore.kernel.org/git/cover.1591821067.git.jonathantanmy@google.com/\n   Merged as 34e849b05a4 (Merge branch 'jt/cdn-offload', 2020-06-25)\n5. https://lore.kernel.org/git/cover.1614021092.git.jonathantanmy@google.com/\n   Merged as 6ee353d42f3 (Merge branch 'jt/transfer-fsck-across-packs',\n   2021-03-01)\n6. 69571dfe219 (Merge branch 'jt/clone-unborn-head', 2021-02-17)\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/technical/protocol-v2.txt | 209 ++++++++++++++++++++++++\n Makefile                                |   1 +\n bundle-uri.c                            |  55 +++++++\n bundle-uri.h                            |  14 ++\n serve.c                                 |   6 +\n t/t5701-git-serve.sh                    | 124 +++++++++++++-\n 6 files changed, 408 insertions(+), 1 deletion(-)\n create mode 100644 bundle-uri.c\n create mode 100644 bundle-uri.h\n\ndiff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\nindex 21e8258ccf3..4bc15a976cd 100644\n--- a/Documentation/technical/protocol-v2.txt\n+++ b/Documentation/technical/protocol-v2.txt\n@@ -566,3 +566,212 @@ and associated requested information, each separated by a single space.\n \tattr = \"size\"\n \n \tobj-info = obj-id SP obj-size\n+\n+bundle-uri\n+~~~~~~~~~~\n+\n+If the 'bundle-uri' capability is advertised, the server supports the\n+`bundle-uri' command.\n+\n+The capability is currently advertised with no value (i.e. not\n+\"bundle-uri=somevalue\"), a value may be added in the future for\n+supporting command-wide extensions. Clients MUST ignore any unknown\n+capability values and proceed with the 'bundle-uri` dialog they\n+support.\n+\n+The 'bundle-uri' command is intended to be issued before `fetch` to\n+get URIs to bundle files (see linkgit:git-bundle[1]) to \"seed\" and\n+inform the subsequent `fetch` command.\n+\n+The client CAN issue `bundle-uri` before or after any other valid\n+command. To be useful to clients it's expected that it'll be issued\n+after an `ls-refs` and before `fetch`, but CAN be issued at any time\n+in the dialog.\n+\n+DISCUSSION of bundle-uri\n+^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+The intent of the feature is optimize for server resource consumption\n+in the common case by changing the common case of fetching a very\n+large PACK during linkgit:git-clone[1] into a smaller incremental\n+fetch.\n+\n+It also allows servers to achieve better caching in combination with\n+an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n+\n+By having new clones or fetches be a more predictable and common\n+negotiation against the tips of recently produces *.bundle file(s).\n+Servers might even pre-generate the results of such negotiations for\n+the `uploadpack.packObjectsHook` as new pushes come in.\n+\n+I.e. the server would anticipate that fresh clones will download a\n+known bundle, followed by catching up to the current state of the\n+repository using ref tips found in that bundle (or bundles).\n+\n+PROTOCOL for bundle-uri\n+^^^^^^^^^^^^^^^^^^^^^^^\n+\n+A `bundle-uri` request takes no arguments, and as noted above does not\n+currently advertise a capability value. Both may be added in the\n+future.\n+\n+When the client issues a `command=bundle-uri` the response is a list\n+of URIs the server would like the client to fetch out-of-bounds before\n+proceeding with the `fetch` request in this format:\n+\n+\toutput = bundle-uri-line\n+\t\t bundle-uri-line* flush-pkt\n+\n+\tbundle-uri-line = PKT-LINE(bundle-uri)\n+\t\t\t  *(SP bundle-feature-key *(=bundle-feature-val))\n+\t\t\t  LF\n+\n+\tbundle-uri = A URI such as a https://, ssh:// etc. URI\n+\n+\tbundle-feature-key = Any printable ASCII characters except SP or \"=\"\n+\tbundle-feature-val = Any printable ASCII characters except SP or \"=\"\n+\n+No `bundle-feature-key`=`bundle-feature-value` fields are currently\n+defined. See the discussion of features below.\n+\n+Clients are still expected to fully parse the line according to the\n+above format, lines that do not conform to the format SHOULD be\n+discarded. The user MAY be warned in such a case.\n+\n+bundle-uri CLIENT AND SERVER EXPECTATIONS\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+\".bundle\" FORMAT\n+++++++++++++++++\n+\n+The advertised bundle(s) MUST be in a format that \"git bundle verify\"\n+would accept. I.e. they MUST contain one or more reference tips for\n+use by the client, MUST indicate prerequisites (in any) with standard\n+\"-\" prefixes, and MUST indicate their \"object-format\", if\n+applicable. Create \"*.bundle\" files with \"git bundle create\".\n+\n+bundle-uri CLIENT ERROR RECOVERY\n+++++++++++++++++++++++++++++++++\n+\n+A client MUST above all gracefully degrade on errors, whether that\n+error is because of bad missing/data in the bundle URI(s), because\n+that client is too dumb to e.g. understand and fully parse out bundle\n+headers and their prerequisite relationships, or something else.\n+\n+Server operators should feel confident in turning on \"bundle-uri\" and\n+not worry if e.g. their CDN goes down that clones or fetches will run\n+into hard failures. Even if the server bundle bundle(s) are\n+incomplete, or bad in some way the client should still end up with a\n+functioning repository, just as if it had chosen not to use this\n+protocol extension.\n+\n+All subsequent discussion on client and server interaction MUST keep\n+this in mind.\n+\n+bundle-uri SERVER TO CLIENT\n++++++++++++++++++++++++++++\n+\n+The ordering of the returned bundle uris is not significant. Clients\n+MUST parse their headers to discover their contained OIDS and\n+prerequisites. A client MUST consider the content of the bundle(s)\n+themselves and their header as the ultimate source of truth.\n+\n+A server MAY even return bundle(s) that don't have any direct\n+relationship to the repository being cloned (either through accident,\n+or intentional \"clever\" configuration), and expect a client to sort\n+out what data they'd like from the bundle(s), if any.\n+\n+bundle-uri CLIENT TO SERVER\n++++++++++++++++++++++++++++\n+\n+The client SHOULD provide reference tips found in the bundle header(s)\n+as 'have' lines in any subsequent `fetch` request. A client MAY also\n+ignore the bundle(s) entirely if doing so is deemed worse for some\n+reason, e.g. if the bundles can't be downloaded, it doesn't like the\n+tips it finds etc.\n+\n+WHEN ADVERTISED BUNDLE(S) REQUIRE NO FURTHER NEGOTIATION\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n+\n+If after issuing `bundle-uri` and `ls-refs`, and getting the header(s)\n+of the bundle(s) the client finds that the ref tips it wants can be\n+retrieved entirety from advertised bundle(s), it MAY disconnect. The\n+results of such a 'clone' or 'fetch' should be indistinguishable from\n+the state attained without using bundle-uri.\n+\n+EARLY CLIENT DISCONNECTIONS AND ERROR RECOVERY\n+++++++++++++++++++++++++++++++++++++++++++++++\n+\n+A client MAY perform an early disconnect while still downloading the\n+bundle(s) (having streamed and parsed their headers). In such a case\n+the client MUST gracefully recover from any errors related to\n+finishing the download and validation of the bundle(s).\n+\n+I.e. a client might need to re-connect and issue a 'fetch' command,\n+and possibly fall back to not making use of 'bundle-uri' at all.\n+\n+This \"MAY\" behavior is specified as such (and not a \"SHOULD\") on the\n+assumption that a server advertising bundle uris is more likely than\n+not to be serving up a relatively large repository, and to be pointing\n+to URIs that have a good chance of being in working order. A client\n+MAY e.g. look at the payload size of the bundles as a heuristic to see\n+if an early disconnect is worth it, should falling back on a full\n+\"fetch\" dialog be necessary.\n+\n+WHEN ADVERTISED BUNDLE(S) REQUIRE FURTHER NEGOTIATION\n++++++++++++++++++++++++++++++++++++++++++++++++++++++\n+\n+A client SHOULD commence a negotiation of a PACK from the server via\n+the \"fetch\" command using the OID tips found in advertised bundles,\n+even if's still in the process of downloading those bundle(s).\n+\n+This allows for aggressive early disconnects from any interactive\n+server dialog. The client blindly trusts that the advertised OID tips\n+are relevant, and issues them as 'have' lines, it then requests any\n+tips it would like (usually from the \"ls-refs\" advertisement) via\n+'want' lines. The server will then compute a (hopefully small) PACK\n+with the expected difference between the tips from the bundle(s) and\n+the data requested.\n+\n+The only connection the client then needs to keep active is to the\n+concurrently downloading static bundle(s), when those and the\n+incremental PACK are retrieved they should be inflated and\n+validated. Any errors at this point should be gracefully recovered\n+from, see above.\n+\n+bundle-uri PROTOCOL FEATURES\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+As noted above no `bundle-feature-key`=`bundle-feature-value` fields\n+are currently defined.\n+\n+They are intended for future per-URI metadata which older clients MUST\n+ignore and gracefully degrade on. Any fields they do recognize they\n+CAN also ignore.\n+\n+Any backwards-incompatible addition of pre-URI key-value will be\n+guarded by a new value or values in 'bundle-uri' capability\n+advertisement itself, and/or by new future `bundle-uri` request\n+arguments.\n+\n+While no per-URI key-value are currently supported currently they're\n+intended to support future features such as:\n+\n+ * Add a \"hash=<val>\" or \"size=<bytes>\" advertise the expected hash or\n+   size of the bundle file.\n+\n+ * Advertise that one or more bundle files are the same (to e.g. have\n+   clients round-robin or otherwise choose one of N possible files).\n+\n+ * A \"oid=<OID>\" shortcut and \"prerequisite=<OID>\" shortcut. For\n+   expressing the common case of a bundle with one tip and no\n+   prerequisites, or one tip and one prerequisite.\n++\n+This would allow for optimizing the common case of servers who'd like\n+to provide one \"big bundle\" containing only their \"main\" branch,\n+and/or incremental updates thereof.\n++\n+A client receiving such a a response MAY assume that they can skip\n+retrieving the header from a bundle at the indicated URI, and thus\n+save themselves and the server(s) the request(s) needed to inspect the\n+headers of that bundle or bundles.\ndiff --git a/Makefile b/Makefile\nindex 381bed2c1d2..e41ac60829d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -846,6 +846,7 @@ LIB_OBJS += blob.o\n LIB_OBJS += bloom.o\n LIB_OBJS += branch.o\n LIB_OBJS += bulk-checkin.o\n+LIB_OBJS += bundle-uri.o\n LIB_OBJS += bundle.o\n LIB_OBJS += cache-tree.o\n LIB_OBJS += cbtree.o\ndiff --git a/bundle-uri.c b/bundle-uri.c\nnew file mode 100644\nindex 00000000000..ff054ddc690\n--- /dev/null\n+++ b/bundle-uri.c\n@@ -0,0 +1,55 @@\n+#include \"cache.h\"\n+#include \"bundle-uri.h\"\n+#include \"pkt-line.h\"\n+#include \"config.h\"\n+\n+static void send_bundle_uris(struct packet_writer *writer,\n+\t\t\t     struct string_list *uris)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, uris)\n+\t\tpacket_writer_write(writer, \"%s\", item->string);\n+}\n+\n+static int advertise_bundle_uri = -1;\n+static struct string_list bundle_uris = STRING_LIST_INIT_DUP;\n+static int bundle_uri_config(const char *var, const char *value, void *data)\n+{\n+\tif (!strcmp(var, \"uploadpack.bundleuri\")) {\n+\t\tadvertise_bundle_uri = 1;\n+\t\tstring_list_append(&bundle_uris, value);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int bundle_uri_advertise(struct repository *r, struct strbuf *value)\n+{\n+\tif (advertise_bundle_uri != -1)\n+\t\tgoto cached;\n+\n+\tgit_config(bundle_uri_config, NULL);\n+\tadvertise_bundle_uri = !!bundle_uris.nr;\n+\n+cached:\n+\treturn advertise_bundle_uri;\n+}\n+\n+int bundle_uri_command(struct repository *r,\n+\t\t       struct packet_reader *request)\n+{\n+\tstruct packet_writer writer;\n+\tpacket_writer_init(&writer, 1);\n+\n+\twhile (packet_reader_read(request) == PACKET_READ_NORMAL)\n+\t\tdie(_(\"bundle-uri: unexpected argument: '%s'\"), request->line);\n+\tif (request->status != PACKET_READ_FLUSH)\n+\t\tdie(_(\"bundle-uri: expected flush after arguments\"));\n+\n+\tsend_bundle_uris(&writer, &bundle_uris);\n+\n+\tpacket_writer_flush(&writer);\n+\n+\treturn 0;\n+}\ndiff --git a/bundle-uri.h b/bundle-uri.h\nnew file mode 100644\nindex 00000000000..b8762e6a8e4\n--- /dev/null\n+++ b/bundle-uri.h\n@@ -0,0 +1,14 @@\n+#ifndef BUNDLE_URI_H\n+#define BUNDLE_URI_H\n+\n+struct repository;\n+struct packet_reader;\n+struct packet_writer;\n+\n+/**\n+ * API used by serve.[ch].\n+ */\n+int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n+int bundle_uri_command(struct repository *r, struct packet_reader *request);\n+\n+#endif /* BUNDLE_URI_H */\ndiff --git a/serve.c b/serve.c\nindex b3fe9b5126a..f3e0203d2c6 100644\n--- a/serve.c\n+++ b/serve.c\n@@ -8,6 +8,7 @@\n #include \"protocol-caps.h\"\n #include \"serve.h\"\n #include \"upload-pack.h\"\n+#include \"bundle-uri.h\"\n \n static int advertise_sid = -1;\n static int client_hash_algo = GIT_HASH_SHA1;\n@@ -136,6 +137,11 @@ static struct protocol_capability capabilities[] = {\n \t\t.advertise = always_advertise,\n \t\t.command = cap_object_info,\n \t},\n+\t{\n+\t\t.name = \"bundle-uri\",\n+\t\t.advertise = bundle_uri_advertise,\n+\t\t.command = bundle_uri_command,\n+\t},\n };\n \n void protocol_v2_advertise_capabilities(void)\ndiff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh\nindex 1896f671cb3..9d053f77a93 100755\n--- a/t/t5701-git-serve.sh\n+++ b/t/t5701-git-serve.sh\n@@ -13,7 +13,7 @@ test_expect_success 'test capability advertisement' '\n \twrong_algo sha1:sha256\n \twrong_algo sha256:sha1\n \tEOF\n-\tcat >expect <<-EOF &&\n+\tcat >expect.base <<-EOF &&\n \tversion 2\n \tagent=git/$(git version | cut -d\" \" -f3)\n \tls-refs=unborn\n@@ -21,8 +21,11 @@ test_expect_success 'test capability advertisement' '\n \tserver-option\n \tobject-format=$(test_oid algo)\n \tobject-info\n+\tEOF\n+\tcat >expect.trailer <<-EOF &&\n \t0000\n \tEOF\n+\tcat expect.base expect.trailer >expect &&\n \n \tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n \t\t--advertise-capabilities >out &&\n@@ -342,4 +345,123 @@ test_expect_success 'basics of object-info' '\n \ttest_cmp expect actual\n '\n \n+# Test the basics of bundle-uri\n+#\n+test_expect_success 'test capability advertisement with uploadpack.bundleURI' '\n+\ttest_config uploadpack.bundleURI FAKE &&\n+\n+\tcat >expect.extra <<-EOF &&\n+\tbundle-uri\n+\tEOF\n+\tcat expect.base \\\n+\t    expect.extra \\\n+\t    expect.trailer >expect &&\n+\n+\tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n+\t\t--advertise-capabilities >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: dies if not enabled' '\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\t0000\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tfatal: invalid command '\"'\"'bundle-uri'\"'\"'\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tERR serve: invalid command '\"'\"'bundle-uri'\"'\"'\n+\tEOF\n+\n+\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out\n+'\n+\n+\n+test_expect_success 'basics of bundle-uri: enabled with single URI' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: enabled with single URI' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: enabled with two URIs' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/recent.bdl --add &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\thttps://cdn.example.com/recent.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: unknown future feature(s)' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/fake.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0001\n+\tsome-feature\n+\twe-do-not\n+\tknow=about\n+\t0000\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tfatal: bundle-uri: unexpected argument: '\"'\"'some-feature'\"'\"'\n+\tEOF\n+\n+\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out\n+'\n+\n test_done\n-- \n2.33.1.1511.gd15d1b313a6\n\n"},{"id":"439596","messageId":"patch-3.3-64224ec2cba-20211025T211159Z-avarab@gmail.com","threadId":"56782","inReplyTo":"cover-0.3-00000000000-20211025T211159Z-avarab@gmail.com","subject":"[PATCH 3/3] bundle-uri client: add \"bundle-uri\" parsing + tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-25T21:25:46Z","receivedAt":"2021-10-25T21:26:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a \"test-tool bundle-uri parse\" which parses the format defined in\nthe newly specified \"bundle-uri\" command.\n\nAs note in the \"bundle-uri\" section in protocol-v2.txt we haven't\nspecified any key-values yet, just URI lines, but we should parse\ntheir format for conformity with the spec.\n\nWe need to make sure our future client doesn't die if this optional\ndata is ever provided by the server, and that we've covered all the\nedge cases with these key-values in our specification. Let's add and\ntest a bundle_uri_parse_line() to do that.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile                    |   1 +\n bundle-uri.c                | 124 +++++++++++++++++++++++++++++\n bundle-uri.h                |  16 ++++\n t/helper/test-bundle-uri.c  |  83 +++++++++++++++++++\n t/helper/test-tool.c        |   1 +\n t/helper/test-tool.h        |   1 +\n t/t5750-bundle-uri-parse.sh | 153 ++++++++++++++++++++++++++++++++++++\n 7 files changed, 379 insertions(+)\n create mode 100644 t/helper/test-bundle-uri.c\n create mode 100755 t/t5750-bundle-uri-parse.sh\n\ndiff --git a/Makefile b/Makefile\nindex e41ac60829d..de66a016c78 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -691,6 +691,7 @@ PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))\n TEST_BUILTINS_OBJS += test-advise.o\n TEST_BUILTINS_OBJS += test-bitmap.o\n TEST_BUILTINS_OBJS += test-bloom.o\n+TEST_BUILTINS_OBJS += test-bundle-uri.o\n TEST_BUILTINS_OBJS += test-chmtime.o\n TEST_BUILTINS_OBJS += test-config.o\n TEST_BUILTINS_OBJS += test-crontab.o\ndiff --git a/bundle-uri.c b/bundle-uri.c\nindex ff054ddc690..9827fc5da17 100644\n--- a/bundle-uri.c\n+++ b/bundle-uri.c\n@@ -53,3 +53,127 @@ int bundle_uri_command(struct repository *r,\n \n \treturn 0;\n }\n+\n+/**\n+ * General API for {transport,connect}.c etc.\n+ */\n+int bundle_uri_parse_line(struct string_list *bundle_uri, const char *line)\n+{\n+\tsize_t i;\n+\tstruct string_list columns = STRING_LIST_INIT_DUP;\n+\tconst char *uri;\n+\tstruct string_list *uri_columns = NULL;\n+\tint ret = 0;\n+\n+\tif (!strlen(line))\n+\t\treturn error(_(\"bundle-uri: got an empty line\"));\n+\n+\t/*\n+\t * Right now we don't understand anything beyond the first SP,\n+\t * but let's be tolerant and ignore any future unknown\n+\t * fields. See the \"MUST\" note about \"bundle-feature-key\" in\n+\t * Documentation/technical/protocol-v2.txt\n+\t */\n+\tif (string_list_split(&columns, line, ' ', -1) < 1)\n+\t\treturn error(_(\"bundle-uri: line not in SP-delimited format: %s\"), line);\n+\n+\t/*\n+\t * We represent a \"<uri>[ <key-values>...]\" line with the URI\n+\t * being the .string in a string list, and the .util being an\n+\t * optional string list of key (.string) and values\n+\t * (.util). If the top-level .util is NULL there's no\n+\t * key-value pairs....\n+\t */\n+\turi = columns.items[0].string;\n+\tif (!strlen(uri)) {\n+\t\tret = error(_(\"bundle-uri: got an empty URI component\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/*\n+\t * ... we're going to need that non-NULL .util .\n+\t */\n+\tif (columns.nr > 1) {\n+\t\turi_columns = xcalloc(1, sizeof(struct string_list));\n+\t\tstring_list_init_dup(uri_columns);\n+\t}\n+\n+\t/*\n+\t * Let's parse the optional \"kv\" format, even if we don't\n+\t * understand any of the keys or values yet.\n+\t */\n+\tfor (i = 1; i < columns.nr; i++) {\n+\t\tstruct string_list kv = STRING_LIST_INIT_DUP;\n+\t\tconst char *arg = columns.items[i].string;\n+\t\tint fields = string_list_split(&kv, arg, '=', 2);\n+\t\tint err = 0;\n+\n+\t\tswitch (fields) {\n+\t\tcase 0:\n+\t\t\tBUG(\"should have no fields=0\");\n+\t\tcase 1:\n+\t\t\tif (!strlen(arg)) {\n+\t\t\t\terr = error(\"bundle-uri: column %lu: got an empty attribute (full line was '%s')\",\n+\t\t\t\t\t    i, line);\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\t/*\n+\t\t\t * We could dance around with\n+\t\t\t * string_list_append_nodup() and skip\n+\t\t\t * string_list_clear(&kv, 0) here, but let's\n+\t\t\t * keep it simple.\n+\t\t\t */\n+\t\t\tstring_list_append(uri_columns, arg);\n+\t\t\tbreak;\n+\t\tcase 2:\n+\t\t{\n+\t\t\tconst char *k = kv.items[0].string;\n+\t\t\tconst char *v = kv.items[1].string;\n+\n+\t\t\tstring_list_append(uri_columns, k)->util = xstrdup(v);\n+\t\t\tbreak;\n+\t\t}\n+\t\tdefault:\n+\t\t\terr = error(\"bundle-uri: column %lu: '%s' more than one '=' character (full line was '%s')\",\n+\t\t\t\t    i, arg, line);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tstring_list_clear(&kv, 0);\n+\t\tif (err) {\n+\t\t\tret = err;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\n+\t/*\n+\t * Per the spec we'll only consider bundle-uri lines OK if\n+\t * there were no parsing problems, even if the problems were\n+\t * with attributes whose content we don't understand.\n+\t */\n+\tif (ret && uri_columns) {\n+\t\tstring_list_clear(uri_columns, 1);\n+\t\tfree(uri_columns);\n+\t} else if (!ret) {\n+\t\tstring_list_append(bundle_uri, uri)->util = uri_columns;\n+\t}\n+\n+cleanup:\n+\tstring_list_clear(&columns, 0);\n+\treturn ret;\n+}\n+\n+static void bundle_uri_string_list_clear_cb(void *util, const char *string)\n+{\n+\tstruct string_list *fields = util;\n+\tif (!fields)\n+\t\treturn;\n+\tstring_list_clear(fields, 1);\n+\tfree(fields);\n+}\n+\n+void bundle_uri_string_list_clear(struct string_list *bundle_uri)\n+{\n+\tstring_list_clear_func(bundle_uri, bundle_uri_string_list_clear_cb);\n+}\ndiff --git a/bundle-uri.h b/bundle-uri.h\nindex b8762e6a8e4..c23d7316555 100644\n--- a/bundle-uri.h\n+++ b/bundle-uri.h\n@@ -4,6 +4,7 @@\n struct repository;\n struct packet_reader;\n struct packet_writer;\n+struct string_list;\n \n /**\n  * API used by serve.[ch].\n@@ -11,4 +12,19 @@ struct packet_writer;\n int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n int bundle_uri_command(struct repository *r, struct packet_reader *request);\n \n+/**\n+ * General API for {transport,connect}.c etc.\n+ */\n+\n+/**\n+ * bundle_uri_parse_line() returns 0 when a valid bundle-uri has been\n+ * added to `bundle_uri`, <0 on error.\n+ */\n+int bundle_uri_parse_line(struct string_list *bundle_uri, const char *line);\n+\n+/**\n+ * Clear the `bundle_uri` list. Just a very thin wrapper on\n+ * string_list_clear().\n+ */\n+void bundle_uri_string_list_clear(struct string_list *bundle_uri);\n #endif /* BUNDLE_URI_H */\ndiff --git a/t/helper/test-bundle-uri.c b/t/helper/test-bundle-uri.c\nnew file mode 100644\nindex 00000000000..805a86c0130\n--- /dev/null\n+++ b/t/helper/test-bundle-uri.c\n@@ -0,0 +1,83 @@\n+#include \"test-tool.h\"\n+#include \"parse-options.h\"\n+#include \"bundle-uri.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n+\n+static int cmd__bundle_uri_parse(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool bundle-uri parse <in\",\n+\t\tNULL\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tint err = 0;\n+\tstruct string_list_item *item;\n+\tsize_t line_nr = 0;\n+\n+\targc = parse_options(argc, argv, NULL, options, usage, 0);\n+\tif (argc)\n+\t\tgoto usage;\n+\n+\twhile (strbuf_getline(&sb, stdin) != EOF) {\n+\t\tline_nr++;\n+\t\tif (bundle_uri_parse_line(&list, sb.buf) < 0)\n+\t\t\terr = error(\"bad line: '%s'\", sb.buf);\n+\t}\n+\n+\tfor_each_string_list_item(item, &list) {\n+\t\tstruct string_list_item *kv_item;\n+\t\tstruct string_list *kv = item->util;\n+\n+\t\tfprintf(stdout, \"%s\", item->string);\n+\t\tif (!kv) {\n+\t\t\tfprintf(stdout, \"\\n\");\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfor_each_string_list_item(kv_item, kv) {\n+\t\t\tconst char *k = kv_item->string;\n+\t\t\tconst char *v = kv_item->util;\n+\n+\t\t\tif (v)\n+\t\t\t\tfprintf(stdout, \" [kv: %s => %s]\", k, v);\n+\t\t\telse\n+\t\t\t\tfprintf(stdout, \" [attr: %s]\", k);\n+\t\t}\n+\t\tfprintf(stdout, \"\\n\");\n+\t}\n+\tstrbuf_release(&sb);\n+\n+\tbundle_uri_string_list_clear(&list);\n+\n+\treturn err < 0 ? 1 : 0;\n+usage:\n+\tusage_with_options(usage, options);\n+}\n+\n+int cmd__bundle_uri(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool bundle-uri <subcommand> [<options>]\",\n+\t\tNULL\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0);\n+\tif (argc == 1)\n+\t\tgoto usage;\n+\n+\tif (!strcmp(argv[1], \"parse\"))\n+\t\treturn cmd__bundle_uri_parse(argc - 1, argv + 1);\n+\terror(\"there is no test-tool bundle-uri tool '%s'\", argv[1]);\n+\n+usage:\n+\tusage_with_options(usage, options);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 3ce5585e53a..b6e1ee7b253 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -17,6 +17,7 @@ static struct test_cmd cmds[] = {\n \t{ \"advise\", cmd__advise_if_enabled },\n \t{ \"bitmap\", cmd__bitmap },\n \t{ \"bloom\", cmd__bloom },\n+\t{ \"bundle-uri\", cmd__bundle_uri },\n \t{ \"chmtime\", cmd__chmtime },\n \t{ \"config\", cmd__config },\n \t{ \"crontab\", cmd__crontab },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex 9f0f5228508..ef839ac7262 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -7,6 +7,7 @@\n int cmd__advise_if_enabled(int argc, const char **argv);\n int cmd__bitmap(int argc, const char **argv);\n int cmd__bloom(int argc, const char **argv);\n+int cmd__bundle_uri(int argc, const char **argv);\n int cmd__chmtime(int argc, const char **argv);\n int cmd__config(int argc, const char **argv);\n int cmd__crontab(int argc, const char **argv);\ndiff --git a/t/t5750-bundle-uri-parse.sh b/t/t5750-bundle-uri-parse.sh\nnew file mode 100755\nindex 00000000000..70fd1b398e9\n--- /dev/null\n+++ b/t/t5750-bundle-uri-parse.sh\n@@ -0,0 +1,153 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri bundle_uri_parse_line()\"\n+\n+TEST_NO_CREATE_REPO=1\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'bundle_uri_parse_line() just URIs' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle.bdl\n+\thttps://example.com/bundle.bdl\n+\tfile:///usr/share/git/bundle.bdl\n+\tEOF\n+\n+\t# For the simple case\n+\tcp in expect &&\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() with attributes' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl attr\n+\thttp://example.com/bundle2.bdl ibute\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: attr]\n+\thttp://example.com/bundle2.bdl [attr: ibute]\n+\tEOF\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() with attributes and key-value attributes' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl x a=b y c=d z e=f a=b\n+\tEOF\n+\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: x] [kv: a => b] [attr: y] [kv: c => d] [attr: z] [kv: e => f] [kv: a => b]\n+\tEOF\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: extra SP' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl one-space\n+\thttp://example.com/bundle2.bdl  two-space\n+\thttp://example.com/bundle3.bdl   three-space\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: column 1: got an empty attribute (full line was '\\''http://example.com/bundle2.bdl  two-space'\\'')\n+\terror: bad line: '\\''http://example.com/bundle2.bdl  two-space'\\''\n+\terror: bundle-uri: column 1: got an empty attribute (full line was '\\''http://example.com/bundle3.bdl   three-space'\\'')\n+\terror: bad line: '\\''http://example.com/bundle3.bdl   three-space'\\''\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: one-space]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty lines' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\n+\thttp://example.com/bundle2.bdl a=b\n+\n+\thttp://example.com/bundle3.bdl\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: got an empty line\n+\terror: bad line: '\\'''\\''\n+\terror: bundle-uri: got an empty line\n+\terror: bad line: '\\'''\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\thttp://example.com/bundle2.bdl [kv: a => b]\n+\thttp://example.com/bundle3.bdl\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty URIs' '\n+\tsed \"s/> //\" >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\t>  a=b\n+\thttp://example.com/bundle3.bdl a=b\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: got an empty URI component\n+\terror: bad line: '\\'' a=b'\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\thttp://example.com/bundle3.bdl [kv: a => b]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: multiple = in key-values' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl k=v=extra\n+\thttp://example.com/bundle2.bdl a=b k=v=extra c=d\n+\thttp://example.com/bundle3.bdl kv=ok\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: column 1: '\\''k=v=extra'\\'' more than one '\\''='\\'' character (full line was '\\''http://example.com/bundle1.bdl k=v=extra'\\'')\n+\terror: bad line: '\\''http://example.com/bundle1.bdl k=v=extra'\\''\n+\terror: bundle-uri: column 2: '\\''k=v=extra'\\'' more than one '\\''='\\'' character (full line was '\\''http://example.com/bundle2.bdl a=b k=v=extra c=d'\\'')\n+\terror: bad line: '\\''http://example.com/bundle2.bdl a=b k=v=extra c=d'\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle3.bdl [kv: kv => ok]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.33.1.1511.gd15d1b313a6\n\n"},{"id":"439645","messageId":"f2269fc7-1688-d62e-02bb-01c5b5e33143@gmail.com","threadId":"56782","inReplyTo":"patch-2.3-3ac0539c053-20211025T211159Z-avarab@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-26T14:00:10Z","receivedAt":"2021-10-26T14:00:17Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n> diff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\n> +bundle-uri CLIENT ERROR RECOVERY\n> +++++++++++++++++++++++++++++++++\n> +\n> +A client MUST above all gracefully degrade on errors, whether that\n> +error is because of bad missing/data in the bundle URI(s), because\n> +that client is too dumb to e.g. understand and fully parse out bundle\n> +headers and their prerequisite relationships, or something else.\n\n\"too dumb\" seems a bit informal to me, especially because you\nimmediately elaborate on its meaning. You could rewrite as follows:\n\n  ...because\n  that client can't understand or fully parse out bundle\n  headers and their prerequisite relationships, or something else.\n\n> +Server operators should feel confident in turning on \"bundle-uri\" and\n> +not worry if e.g. their CDN goes down that clones or fetches will run\n> +into hard failures. Even if the server bundle bundle(s) are\n> +incomplete, or bad in some way the client should still end up with a\n> +functioning repository, just as if it had chosen not to use this\n> +protocol extension.\n\nAlso, insertions of \"e.g.\" in the middle of a sentence don't flow well.\n\n  Server operators should feel confident in turning on \"bundle-uri\" and\n  not worry that failures such as the CDN being unavailable will cause\n  clones or fetches to have hard failures. Even if the server bundle(s)\n  are invalid, the client should still end up with a functioning\n  repository, just as if it had chosen not to use this protocol extension.\n\n(Note: I also removed a \"bundle bundle(s)\" that was split across a line\nbreak.)\n\n> +bundle-uri SERVER TO CLIENT\n> ++++++++++++++++++++++++++++\n> +\n> +The ordering of the returned bundle uris is not significant. Clients\n\nI'm late to noticing, but shouldn't \"URI\" be all-caps when not used in\nthe literal capability string \"bundle-uri\"?\n\n> +bundle-uri CLIENT TO SERVER\n> ++++++++++++++++++++++++++++\n> +\n> +The client SHOULD provide reference tips found in the bundle header(s)\n> +as 'have' lines in any subsequent `fetch` request. A client MAY also\n> +ignore the bundle(s) entirely if doing so is deemed worse for some\n> +reason, e.g. if the bundles can't be downloaded, it doesn't like the\n> +tips it finds etc.\n\nI would just stop after \"is deemed worse for some reason.\" because one\nexample is obvious and the other is unclear how the client would detect\nthat situation. (Maybe: tip commit timestamps are really old?)\n\n> +\n> +WHEN ADVERTISED BUNDLE(S) REQUIRE NO FURTHER NEGOTIATION\n> +++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n> +\n> +If after issuing `bundle-uri` and `ls-refs`, and getting the header(s)\n> +of the bundle(s) the client finds that the ref tips it wants can be\n> +retrieved entirety from advertised bundle(s), it MAY disconnect. The\n\ns/entirety/entirely/\n\n> +results of such a 'clone' or 'fetch' should be indistinguishable from\n> +the state attained without using bundle-uri.\n> +\n> +EARLY CLIENT DISCONNECTIONS AND ERROR RECOVERY\n> +++++++++++++++++++++++++++++++++++++++++++++++\n> +\n> +A client MAY perform an early disconnect while still downloading the\n> +bundle(s) (having streamed and parsed their headers). In such a case\n> +the client MUST gracefully recover from any errors related to\n> +finishing the download and validation of the bundle(s).\n> +\n> +I.e. a client might need to re-connect and issue a 'fetch' command,\n> +and possibly fall back to not making use of 'bundle-uri' at all.\n\nUse \"For example,\" over starting a sentence with \"i.e.\". The examples\nof \"i.e.\" and \"e.g.\" already in this document show proper use, which\ninvolves parentheses.\n\n> +This \"MAY\" behavior is specified as such (and not a \"SHOULD\") on the\n> +assumption that a server advertising bundle uris is more likely than\n> +not to be serving up a relatively large repository, and to be pointing\n> +to URIs that have a good chance of being in working order. A client\n> +MAY e.g. look at the payload size of the bundles as a heuristic to see\n\nAgain, here, the entire sentence is an example. This \"e.g.\" can be\nremoved with no loss of meaning.\n\n> +if an early disconnect is worth it, should falling back on a full\n> +\"fetch\" dialog be necessary.\n\n\n> +While no per-URI key-value are currently supported currently they're\n> +intended to support future features such as:\n> +\n> + * Add a \"hash=<val>\" or \"size=<bytes>\" advertise the expected hash or\n> +   size of the bundle file.\n\nI suppose if one wanted to add this server-to-bundle coupling, then some\nclients might appreciate it.\n\n> + * Advertise that one or more bundle files are the same (to e.g. have\n> +   clients round-robin or otherwise choose one of N possible files).\n\n  * Advertise that one or more bundle files are the same, to allow for\n    redundancy without causing duplicated effort.\n\n> +static void send_bundle_uris(struct packet_writer *writer,\n> +\t\t\t     struct string_list *uris)\n> +{\n> +\tstruct string_list_item *item;\n> +\n> +\tfor_each_string_list_item(item, uris)\n> +\t\tpacket_writer_write(writer, \"%s\", item->string);\n> +}\n> +\n> +static int advertise_bundle_uri = -1;\n> +static struct string_list bundle_uris = STRING_LIST_INIT_DUP;\n\nI see you put send_bundle_uris() before the global bundle_uris so\nit can be independent, but do you expect anyone to call send_bundle_uris()\nvia a different list?\n\nShould we find a different place to store this data?\n\n> +static int bundle_uri_config(const char *var, const char *value, void *data)\n> +{\n> +\tif (!strcmp(var, \"uploadpack.bundleuri\")) {\n> +\t\tadvertise_bundle_uri = 1;\n> +\t\tstring_list_append(&bundle_uris, value);\n> +\t}\n> +\n> +\treturn 0;\n> +}\n\nHere, we are dictating that the URI list is available as a multi-valued\nconfig \"uploadpack.bundleuri\".\n\n1. Should this be updated in Documentation/config/uploadpack.txt?\n\n2. This seems difficult to extend to your possible future features as\n   listed in the protocol docs, mainly because this can only store the\n   flat URI string. To add things like hash values, sizes, and prereqs,\n   you would need more data included and grouped on a per-URI basis.\n   What plans do you have to make extensions here while remaining\n   somewhat compatible with downgrading Git versions?\n\n> @@ -136,6 +137,11 @@ static struct protocol_capability capabilities[] = {\n>  \t\t.advertise = always_advertise,\n>  \t\t.command = cap_object_info,\n>  \t},\n> +\t{\n> +\t\t.name = \"bundle-uri\",\n> +\t\t.advertise = bundle_uri_advertise,\n> +\t\t.command = bundle_uri_command,\n> +\t},\n>  };\n\nI really appreciate that it is this simple to extend protocol v2.\n\n> +test_expect_success 'basics of bundle-uri: dies if not enabled' '\n> +\ttest-tool pkt-line pack >in <<-EOF &&\n> +\tcommand=bundle-uri\n> +\t0000\n> +\tEOF\n> +\n> +\tcat >err.expect <<-\\EOF &&\n> +\tfatal: invalid command '\"'\"'bundle-uri'\"'\"'\n> +\tEOF\n> +\n> +\tcat >expect <<-\\EOF &&\n> +\tERR serve: invalid command '\"'\"'bundle-uri'\"'\"'\n> +\tEOF\n> +\n> +\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n> +\ttest_cmp err.expect err.actual &&\n> +\ttest_must_be_empty out\n> +'\n> +\n> +\n\nhyper-nit: double newline.\n\nThe implementation seems simple enough, which I like. I'm a bit\nyour current use of Git config as the back-end, only because it is\ndifficult to be future-proof. As the functionality stands today, the\ncurrent config design works just fine. Perhaps we don't need to\nworry about the future, because we can design a new, complementary\nstorage for that extra data. It seems worth exploring for a little\nwhile, though. Perhaps we should take a page out of 'git-remote'\nand how it stores named remotes with sub-items for metadata. The\nnames probably don't need to ever be exposed to users, but it could\nbe beneficial to anyone implementing this scheme.\n\n[bundle \"main\"]\n\turi = https://example.com/my-bundle\n\turi = https://redundant-cdn.com/my-bundle\n\tsize = 120523\n\tsha256 = {64hexchars}\n\n[bundle \"fork\"]\n\turi = https://cdn.org/my-fork\n\tsize = 334\n\tsha256 = {...}\n\tprereq = {oid}\n\nThis kind of layout has an immediate grouping of data that should\nhelp any future plan. Notice how I included multiple \"uri\" lines\nin the \"main\", which helps with your plan for duplicate URIs.\n\nThanks,\n-Stolee\n"},{"id":"439646","messageId":"3de22cec-55db-6ff3-c009-a150647fef38@gmail.com","threadId":"56782","inReplyTo":"patch-3.3-64224ec2cba-20211025T211159Z-avarab@gmail.com","subject":"Re: [PATCH 3/3] bundle-uri client: add \"bundle-uri\" parsing + tests","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-26T14:05:30Z","receivedAt":"2021-10-26T14:05:38Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n> Add a \"test-tool bundle-uri parse\" which parses the format defined in\n> the newly specified \"bundle-uri\" command.\n> \n> As note in the \"bundle-uri\" section in protocol-v2.txt we haven't\n> specified any key-values yet, just URI lines, but we should parse\n> their format for conformity with the spec.\n> \n> We need to make sure our future client doesn't die if this optional\n> data is ever provided by the server, and that we've covered all the\n> edge cases with these key-values in our specification. Let's add and\n> test a bundle_uri_parse_line() to do that.\n\nWhile this implementation is interesting, and the tests available are\nuseful for validating the protocol, I would like to see how this\nintegrates into a full 'git clone' operation to be sure that the API\nis correct. How much more work is it to implement the full end-to-end\nscenario?\n\nThanks,\n-Stolee\n"},{"id":"439651","messageId":"211026.86cznrzu8g.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"f2269fc7-1688-d62e-02bb-01c5b5e33143@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-26T15:00:08Z","receivedAt":"2021-10-26T15:22:30Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Oct 26 2021, Derrick Stolee wrote:\n\n> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>> diff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\n>> +bundle-uri CLIENT ERROR RECOVERY\n>> +++++++++++++++++++++++++++++++++\n>> +\n>> +A client MUST above all gracefully degrade on errors, whether that\n>> +error is because of bad missing/data in the bundle URI(s), because\n>> +that client is too dumb to e.g. understand and fully parse out bundle\n>> +headers and their prerequisite relationships, or something else.\n>\n> \"too dumb\" seems a bit informal to me, especially because you\n> immediately elaborate on its meaning. You could rewrite as follows:\n>\n>   ...because\n>   that client can't understand or fully parse out bundle\n>   headers and their prerequisite relationships, or something else.\n\nThanks, I've snipped all your subsequent comments on\nphrasing/clarifications etc, except insofar as they have questions I\nneed to address (as opposed to just my bad grammar/phrasing etc).\n\nThanks a lot for them, will go through them closely for any subsequent\nre-roll & address them.\n\n> [...]\n>> +While no per-URI key-value are currently supported currently they're\n>> +intended to support future features such as:\n>> +\n>> + * Add a \"hash=<val>\" or \"size=<bytes>\" advertise the expected hash or\n>> +   size of the bundle file.\n>\n> I suppose if one wanted to add this server-to-bundle coupling, then some\n> clients might appreciate it.\n\nFor packfile-uri there's a hard dependency on the server transferring\nthe hash of the PACK file.\n\nI've intentionally omitted it, the reasons are covered in [1], which I\nrealize now should really be part of this early series.\n\nBasically having it as a hard requirement isn't necessary for security\nor payload validation. Any server who's worried about their transport\nintegrity would point to a https URI under their control, any\nchecksumming and validation we'll need we'll get from the transport\nlayer and the client's reachability check.\n\nHaving it would mean that you need closer cooperation by default between\nserver and CDN than I'm aiming for, i.e. a server should be able to\npoint to some URI somewhere updated by a dumb hourly cronjob, without\nneeding to pass information back & forth about what the \"current\" URL\nis. The client will discover all that.\n\nBut I left that \"hash=*\" in because it could be optionally added, in\ncase someone really wants it for some reason...\n\n1. https://lore.kernel.org/git/RFC-patch-13.13-1e657ed27a-20210805T150534Z-avarab@gmail.com/\n\n>> + * Advertise that one or more bundle files are the same (to e.g. have\n>> +   clients round-robin or otherwise choose one of N possible files).\n>\n>   * Advertise that one or more bundle files are the same, to allow for\n>     redundancy without causing duplicated effort.\n\n*nod*\n\n>> +static void send_bundle_uris(struct packet_writer *writer,\n>> +\t\t\t     struct string_list *uris)\n>> +{\n>> +\tstruct string_list_item *item;\n>> +\n>> +\tfor_each_string_list_item(item, uris)\n>> +\t\tpacket_writer_write(writer, \"%s\", item->string);\n>> +}\n>> +\n>> +static int advertise_bundle_uri = -1;\n>> +static struct string_list bundle_uris = STRING_LIST_INIT_DUP;\n>\n> I see you put send_bundle_uris() before the global bundle_uris so\n> it can be independent, but do you expect anyone to call send_bundle_uris()\n> via a different list?\n\nNo, I'll move that around or rather fold it into bundle_uri_command()\ndirectly.\n\nI think I'd originally copied the structure of send_ref() and ls_refs()\nfrom ls-refs.c, but it doesn't make much sense anymore here for this\n2-line function. Thanks.\n\n> Should we find a different place to store this data?\n>\n>> +static int bundle_uri_config(const char *var, const char *value, void *data)\n>> +{\n>> +\tif (!strcmp(var, \"uploadpack.bundleuri\")) {\n>> +\t\tadvertise_bundle_uri = 1;\n>> +\t\tstring_list_append(&bundle_uris, value);\n>> +\t}\n>> +\n>> +\treturn 0;\n>> +}\n>\n> Here, we are dictating that the URI list is available as a multi-valued\n> config \"uploadpack.bundleuri\".\n>\n> 1. Should this be updated in Documentation/config/uploadpack.txt?\n\nDefinitely. I'll either incorporate that or re-structure this leading\nseries so that it's more design-doc/protocol focused, in any case all of\nthis ends up documented in the right places eventually...\n\n> 2. This seems difficult to extend to your possible future features as\n>    listed in the protocol docs, mainly because this can only store the\n>    flat URI string. To add things like hash values, sizes, and prereqs,\n>    you would need more data included and grouped on a per-URI basis.\n>    What plans do you have to make extensions here while remaining\n>    somewhat compatible with downgrading Git versions?\n\n[...addressed below...]\n\n>> @@ -136,6 +137,11 @@ static struct protocol_capability capabilities[] = {\n>>  \t\t.advertise = always_advertise,\n>>  \t\t.command = cap_object_info,\n>>  \t},\n>> +\t{\n>> +\t\t.name = \"bundle-uri\",\n>> +\t\t.advertise = bundle_uri_advertise,\n>> +\t\t.command = bundle_uri_command,\n>> +\t},\n>>  };\n>\n> I really appreciate that it is this simple to extend protocol v2.\n\nYeah! FWIW I've got some WIP patches to make it easier still, i.e. some\nfurther simplification & validation in the serve.[ch] API.\n\n>> +test_expect_success 'basics of bundle-uri: dies if not enabled' '\n>> +\ttest-tool pkt-line pack >in <<-EOF &&\n>> +\tcommand=bundle-uri\n>> +\t0000\n>> +\tEOF\n>> +\n>> +\tcat >err.expect <<-\\EOF &&\n>> +\tfatal: invalid command '\"'\"'bundle-uri'\"'\"'\n>> +\tEOF\n>> +\n>> +\tcat >expect <<-\\EOF &&\n>> +\tERR serve: invalid command '\"'\"'bundle-uri'\"'\"'\n>> +\tEOF\n>> +\n>> +\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n>> +\ttest_cmp err.expect err.actual &&\n>> +\ttest_must_be_empty out\n>> +'\n>> +\n>> +\n>\n> hyper-nit: double newline.\n>\n> The implementation seems simple enough, which I like. I'm a bit\n\nI mentally inserted the missing \"skeptical/uncertain\" etc. here :)\n\n> your current use of Git config as the back-end, only because it is\n> difficult to be future-proof. As the functionality stands today, the\n> current config design works just fine. Perhaps we don't need to\n> worry about the future, because we can design a new, complementary\n> storage for that extra data. It seems worth exploring for a little\n> while, though. Perhaps we should take a page out of 'git-remote'\n> and how it stores named remotes with sub-items for metadata. The\n> names probably don't need to ever be exposed to users, but it could\n> be beneficial to anyone implementing this scheme.\n>\n> [bundle \"main\"]\n> \turi = https://example.com/my-bundle\n> \turi = https://redundant-cdn.com/my-bundle\n> \tsize = 120523\n> \tsha256 = {64hexchars}\n>\n> [bundle \"fork\"]\n> \turi = https://cdn.org/my-fork\n> \tsize = 334\n> \tsha256 = {...}\n> \tprereq = {oid}\n>\n> This kind of layout has an immediate grouping of data that should\n> help any future plan. Notice how I included multiple \"uri\" lines\n> in the \"main\", which helps with your plan for duplicate URIs.\n\nAt first sight I like that config schema much better than my current\none, in particular how it makes the future-proofed \"these N urls are one\nlogical URL\" case simpler.\n\nBut overall I'm a bit on the fence, and leaning towards keeping what I\nhave, not out of any lazynes or wanting to just keep what I have mind\nyou.\n\nBut rather that the main benefit of the current one is that it's a 1=1\nmapping to the line-based protocol, and you can say update your URLs as:\n\n    git config --replace-all uploadpack.bundleUri \"$first_url\" &&\n    git config --add uploadpack.bundleUri \"$second_url\"\n\nHaving usually you'd know the URL you'd like to replace, so you can use\nthe [value-pattern] of --replace-all, if it's a named section or other\nsplit-out structure that become a two-step lookup.\n\nAlso for testing I've got a (trivial) plumbing tool I'll submit called\n\"git ls-remote-bundle-uri\" (could be folded into something else I guess)\nto dump the server-side config, it's nice that you can pretty much\ndirectly copy/paste it into config without needing to adjust it.\n\nHaving said all that I'm not sure I feel strongly about it either way,\nwhat do you think given the above?\n\nI think most \"real\" server operators will use this as\nGIT_CONFIG_COUNT=<n> GIT_CONFIG_{KEY,VALUE}_<1..n>, which can of course\nwork with any config schema, but if you've got code generating it on the\nother side naming the targets is probably a slight hassle / confusing.\n\nThere's also the small matter of it being consistent with the\npackfile-uri config in its current form, but that shouldn't be a reason\nnot to come up with something better. If anything any better suggestion\n(if we go for that) could be supported by it too...\n"},{"id":"439705","messageId":"b22b894b-af03-c108-a3d5-0072c252f513@gmail.com","threadId":"56782","inReplyTo":"211026.86cznrzu8g.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-27T01:55:29Z","receivedAt":"2021-10-27T01:55:37Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/26/2021 11:00 AM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Tue, Oct 26 2021, Derrick Stolee wrote:\n>> The implementation seems simple enough, which I like. I'm a bit\n> \n> I mentally inserted the missing \"skeptical/uncertain\" etc. here :)\n\nMore \"uncertain\" than \"skeptical\". The current plan works perfectly\nfor the current implementation, so there is an element of YAGNI that\ncould easily lead us to avoid overthinking this.\n \n>> your current use of Git config as the back-end, only because it is\n>> difficult to be future-proof. As the functionality stands today, the\n>> current config design works just fine. Perhaps we don't need to\n>> worry about the future, because we can design a new, complementary\n>> storage for that extra data. It seems worth exploring for a little\n>> while, though. Perhaps we should take a page out of 'git-remote'\n>> and how it stores named remotes with sub-items for metadata. The\n>> names probably don't need to ever be exposed to users, but it could\n>> be beneficial to anyone implementing this scheme.\n>>\n>> [bundle \"main\"]\n>> \turi = https://example.com/my-bundle\n>> \turi = https://redundant-cdn.com/my-bundle\n>> \tsize = 120523\n>> \tsha256 = {64hexchars}\n>>\n>> [bundle \"fork\"]\n>> \turi = https://cdn.org/my-fork\n>> \tsize = 334\n>> \tsha256 = {...}\n>> \tprereq = {oid}\n>>\n>> This kind of layout has an immediate grouping of data that should\n>> help any future plan. Notice how I included multiple \"uri\" lines\n>> in the \"main\", which helps with your plan for duplicate URIs.\n> \n> At first sight I like that config schema much better than my current\n> one, in particular how it makes the future-proofed \"these N urls are one\n> logical URL\" case simpler.\n> \n> But overall I'm a bit on the fence, and leaning towards keeping what I\n> have, not out of any lazynes or wanting to just keep what I have mind\n> you.\n> \n> But rather that the main benefit of the current one is that it's a 1=1\n> mapping to the line-based protocol, and you can say update your URLs as:\n> \n>     git config --replace-all uploadpack.bundleUri \"$first_url\" &&\n>     git config --add uploadpack.bundleUri \"$second_url\"\n> \n> Having usually you'd know the URL you'd like to replace, so you can use\n> the [value-pattern] of --replace-all, if it's a named section or other\n> split-out structure that become a two-step lookup.\n\nDon't forget to use --fixed-value for exact string matching instead of\nregex matching!\n\n> Also for testing I've got a (trivial) plumbing tool I'll submit called\n> \"git ls-remote-bundle-uri\" (could be folded into something else I guess)\n> to dump the server-side config, it's nice that you can pretty much\n> directly copy/paste it into config without needing to adjust it.\n\nWith the appropriate helper structs and methods in the product code,\nsuch helper tools will still be simple without being a second place\nthat is directly aware of how the values are stored to disk. I don't\njudge your prototype work that helps you build the feature, but it's\nsimultaneously not a reason to stick to a design.\n\n> Having said all that I'm not sure I feel strongly about it either way,\n> what do you think given the above?\n\nI'm not feeling too strong about it right now. The current design\ndoes not need anything extra, but it also purposefully leaves certain\nthings open for extension in the future.\n\nThe thing I worry about is that there will be two supported ways to\nstore a list of bundle URIs: a flat list of URIs in the multi-valued\nuploadPack.bundleURI config value, but then also a second option that\nallows the extensions that arise. It's a layer of complication that\nwould be nice to avoid if there was an easy way to do it, but the\nschema I sketched earlier isn't simple enough to merit a switch right\nnow. Perhaps someone else will have an idea that accomplishes the\nsame goal, but also is less complicated?\n \n> I think most \"real\" server operators will use this as\n> GIT_CONFIG_COUNT=<n> GIT_CONFIG_{KEY,VALUE}_<1..n>, which can of course\n> work with any config schema, but if you've got code generating it on the\n> other side naming the targets is probably a slight hassle / confusing.\n\nYou'd really overload the environment this way? That's not how I would\napproach it, but maybe there is a benefit over writing to the repository's\nconfig file. I suppose that you could store the data in a database and\nlink it to the repository at runtime instead.\n\n> There's also the small matter of it being consistent with the\n> packfile-uri config in its current form, but that shouldn't be a reason\n> not to come up with something better. If anything any better suggestion\n> (if we go for that) could be supported by it too...\n\nWhat do you mean about being consistent with packfile-uri? This layer\nthat we care about isn't even implemented in git.git.\n\nThanks,\n-Stolee\n"},{"id":"439706","messageId":"7fab28bf-54e7-d0e9-110a-53fad6244cc9@gmail.com","threadId":"56782","inReplyTo":"patch-2.3-3ac0539c053-20211025T211159Z-avarab@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-27T02:01:15Z","receivedAt":"2021-10-27T02:02:39Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n> Add a server-side implementation of a new \"bundle-uri\" command to\n> protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n> allow conforming clients to optionally seed their initial clones or\n> incremental fetches from URLs containing \"*.bundle\" files created with\n> \"git bundle create\".\n\n...\n\n> +DISCUSSION of bundle-uri\n> +^^^^^^^^^^^^^^^^^^^^^^^^\n> +\n> +The intent of the feature is optimize for server resource consumption\n> +in the common case by changing the common case of fetching a very\n> +large PACK during linkgit:git-clone[1] into a smaller incremental\n> +fetch.\n> +\n> +It also allows servers to achieve better caching in combination with\n> +an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n> +\n> +By having new clones or fetches be a more predictable and common\n> +negotiation against the tips of recently produces *.bundle file(s).\n> +Servers might even pre-generate the results of such negotiations for\n> +the `uploadpack.packObjectsHook` as new pushes come in.\n> +\n> +I.e. the server would anticipate that fresh clones will download a\n> +known bundle, followed by catching up to the current state of the\n> +repository using ref tips found in that bundle (or bundles).\n> +\n> +PROTOCOL for bundle-uri\n> +^^^^^^^^^^^^^^^^^^^^^^^\n> +\n> +A `bundle-uri` request takes no arguments, and as noted above does not\n> +currently advertise a capability value. Both may be added in the\n> +future.\n\nOne thing I realized was missing from this proposal is any interaction\nwith partial clone. It would be disappointing if we could not advertise\nbundles of commit-and-tree packfiles for blobless partial clones.\n\nThere is currently no way for the client to signal the filter type\nduring this command. Not having any way to extend to include that\nseems like an oversight we should remedy before committing to a\nprotocol that can't be extended.\n\n(This also seems like a good enough reason to group the URIs into a\nstruct-like storage, because the filter type could be stored next to\nthe URI.)\n\nThanks,\n-Stolee\n"},{"id":"439746","messageId":"211027.86r1c6yh52.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"7fab28bf-54e7-d0e9-110a-53fad6244cc9@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-27T08:29:35Z","receivedAt":"2021-10-27T09:02:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Oct 26 2021, Derrick Stolee wrote:\n\n> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>> Add a server-side implementation of a new \"bundle-uri\" command to\n>> protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n>> allow conforming clients to optionally seed their initial clones or\n>> incremental fetches from URLs containing \"*.bundle\" files created with\n>> \"git bundle create\".\n>\n> ...\n>\n>> +DISCUSSION of bundle-uri\n>> +^^^^^^^^^^^^^^^^^^^^^^^^\n>> +\n>> +The intent of the feature is optimize for server resource consumption\n>> +in the common case by changing the common case of fetching a very\n>> +large PACK during linkgit:git-clone[1] into a smaller incremental\n>> +fetch.\n>> +\n>> +It also allows servers to achieve better caching in combination with\n>> +an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n>> +\n>> +By having new clones or fetches be a more predictable and common\n>> +negotiation against the tips of recently produces *.bundle file(s).\n>> +Servers might even pre-generate the results of such negotiations for\n>> +the `uploadpack.packObjectsHook` as new pushes come in.\n>> +\n>> +I.e. the server would anticipate that fresh clones will download a\n>> +known bundle, followed by catching up to the current state of the\n>> +repository using ref tips found in that bundle (or bundles).\n>> +\n>> +PROTOCOL for bundle-uri\n>> +^^^^^^^^^^^^^^^^^^^^^^^\n>> +\n>> +A `bundle-uri` request takes no arguments, and as noted above does not\n>> +currently advertise a capability value. Both may be added in the\n>> +future.\n>\n> One thing I realized was missing from this proposal is any interaction\n> with partial clone. It would be disappointing if we could not advertise\n> bundles of commit-and-tree packfiles for blobless partial clones.\n>\n> There is currently no way for the client to signal the filter type\n> during this command. Not having any way to extend to include that\n> seems like an oversight we should remedy before committing to a\n> protocol that can't be extended.\n>\n> (This also seems like a good enough reason to group the URIs into a\n> struct-like storage, because the filter type could be stored next to\n> the URI.)\n\nI'll update the docs to note that. I'd definitely like to leave out any\nimplementation of filter/shallow for an initial iteration of this for\nsimplicity, but the protocol keyword/behavior is open-ended enough to\npermit any extension.\n\nI.e. the server can start advertising \"bundle-uri=shallow\", and future\nclients can request arbitrary key-value pairs in addition to just\n\"bundle-uri\" now.\n\nHaving said that I think that *probably* this is something that'll never\nbe implemented, but maybe I'll eat my words there.\n\nThe reason is that once we're in the \"fetch\" dialog with the server, as\nwe are with \"filter\" and \"shallow\" I'd think that we'd be better of just\nsending a packfile-uri, since that's tailor-made for that use-case.\n\nBut I suppose we could also advertise e.g.:\n\n    <bundle-uri> tip=<oid> depth=1\n\nWhich a client that noticed that it noticed say the --single-branch at\n<oid> but with depth=1 could use before it ever got to \"fetch\".\n\nBut (and I haven't looked into this really) I'd think that would quickly\nget you into having a bundle with a PACK payload that wouldn't be\nrepresentable with the current bundle header format, which I think we'd\nalways want a 1=1 mapping of. I.e. you can specify a prereq, but not\nleave out trees/blobs etc.\n\nSo thoughts on that most welcome, in particular how it could be made\nfuture-proof.\n"},{"id":"439791","messageId":"756524c1-a3b9-29b5-bb72-f75a0c76ea1f@gmail.com","threadId":"56782","inReplyTo":"211027.86r1c6yh52.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-27T16:31:20Z","receivedAt":"2021-10-27T16:31:26Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/27/2021 4:29 AM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Tue, Oct 26 2021, Derrick Stolee wrote:\n> \n>> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>>> Add a server-side implementation of a new \"bundle-uri\" command to\n>>> protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n>>> allow conforming clients to optionally seed their initial clones or\n>>> incremental fetches from URLs containing \"*.bundle\" files created with\n>>> \"git bundle create\".\n>>\n>> ...\n>>\n>>> +DISCUSSION of bundle-uri\n>>> +^^^^^^^^^^^^^^^^^^^^^^^^\n>>> +\n>>> +The intent of the feature is optimize for server resource consumption\n>>> +in the common case by changing the common case of fetching a very\n>>> +large PACK during linkgit:git-clone[1] into a smaller incremental\n>>> +fetch.\n>>> +\n>>> +It also allows servers to achieve better caching in combination with\n>>> +an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n>>> +\n>>> +By having new clones or fetches be a more predictable and common\n>>> +negotiation against the tips of recently produces *.bundle file(s).\n>>> +Servers might even pre-generate the results of such negotiations for\n>>> +the `uploadpack.packObjectsHook` as new pushes come in.\n>>> +\n>>> +I.e. the server would anticipate that fresh clones will download a\n>>> +known bundle, followed by catching up to the current state of the\n>>> +repository using ref tips found in that bundle (or bundles).\n>>> +\n>>> +PROTOCOL for bundle-uri\n>>> +^^^^^^^^^^^^^^^^^^^^^^^\n>>> +\n>>> +A `bundle-uri` request takes no arguments, and as noted above does not\n>>> +currently advertise a capability value. Both may be added in the\n>>> +future.\n>>\n>> One thing I realized was missing from this proposal is any interaction\n>> with partial clone. It would be disappointing if we could not advertise\n>> bundles of commit-and-tree packfiles for blobless partial clones.\n>>\n>> There is currently no way for the client to signal the filter type\n>> during this command. Not having any way to extend to include that\n>> seems like an oversight we should remedy before committing to a\n>> protocol that can't be extended.\n>>\n>> (This also seems like a good enough reason to group the URIs into a\n>> struct-like storage, because the filter type could be stored next to\n>> the URI.)\n> \n> I'll update the docs to note that. I'd definitely like to leave out any\n> implementation of filter/shallow for an initial iteration of this for\n> simplicity, but the protocol keyword/behavior is open-ended enough to\n> permit any extension.\n\nIt would be good to be explicit about how this would work. Looking at\nit fresh, it seems that the server could send multiple bundle URIs with\nthe extra metadata to say which ones have a filter (and what that filter\nis). The client could then check if a bundle matches the given filter.\n\nBut this is a bit inverted: the filter mechanism currently has the client\nrequest a given filter and the server responds with _at least_ that much\ndata. This allows the server to ignore things like pathspec-filters or\ncertain size-based filters. If the client just ignores a bundle URI\nbecause it doesn't match the exact filter, this could lead the client to\nask for the data without a bundle, even if it would be faster to just\ndownload the advertised bundle.\n\nFor this reason, I think it would be valuable for the client to tell\nthe server the intended filter, and the server responds with bundle\nURIs that contain a subset of the information that would be provided\nby a later fetch request with that filter.\n\n> I.e. the server can start advertising \"bundle-uri=shallow\", and future\n> clients can request arbitrary key-value pairs in addition to just\n> \"bundle-uri\" now.\n> \n> Having said that I think that *probably* this is something that'll never\n> be implemented, but maybe I'll eat my words there.\n\nYou continue focusing on the shallow option, which I agree is not\nimportant. The filter option, specifically --filter=blob:none, seems\nto be critical to have a short-term plan for implementing with this\nin mind.\n\nThanks,\n-Stolee\n"},{"id":"439800","messageId":"211027.86mtmuxsey.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"b22b894b-af03-c108-a3d5-0072c252f513@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-27T17:49:31Z","receivedAt":"2021-10-27T17:56:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Oct 26 2021, Derrick Stolee wrote:\n\n> On 10/26/2021 11:00 AM, Ævar Arnfjörð Bjarmason wrote:\n\n[I'll reply to the rest later, either here or in related threads. I.e. I\nmight end up entirely revamping the config etc. format]\n\n>> There's also the small matter of it being consistent with the\n>> packfile-uri config in its current form, but that shouldn't be a reason\n>> not to come up with something better. If anything any better suggestion\n>> (if we go for that) could be supported by it too...\n>\n> What do you mean about being consistent with packfile-uri? This layer\n> that we care about isn't even implemented in git.git.\n\nIt's rather limited, but we do support a uploadpack.BlobPackFileUri as a\nserver-side feature for upload-pack. I.e.:\n\n    uploadpack.BlobPackFileUri=<OID> <pack-hash> <packfile-uri>\n\nSee Documentation/technical/packfile-uri.txt.\n\nThe <pack-hash> is part of the protocol, but the <OID> is just an aid to\nupload-pack to peel out that OID when it serves up the PACK, the <OID>\nbeing what you get from the URI.\n\nIn terms of server implementation it's rather proof-of-concept-ish,\ni.e. it's not really all that useful unless your use case is carving out\na small number of really big blobs. JGit's is much more mature, and\nthere's some patches on-list recently to make the git.git one more\npractically useful[1].\n\n1. https://lore.kernel.org/git/cover.1634634814.git.tenglong@alibaba-inc.com/\n\n"},{"id":"439813","messageId":"211027.86ilxixoxz.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"756524c1-a3b9-29b5-bb72-f75a0c76ea1f@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-27T18:01:47Z","receivedAt":"2021-10-27T19:11:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Oct 27 2021, Derrick Stolee wrote:\n\n> On 10/27/2021 4:29 AM, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> On Tue, Oct 26 2021, Derrick Stolee wrote:\n>> \n>>> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>>>> Add a server-side implementation of a new \"bundle-uri\" command to\n>>>> protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n>>>> allow conforming clients to optionally seed their initial clones or\n>>>> incremental fetches from URLs containing \"*.bundle\" files created with\n>>>> \"git bundle create\".\n>>>\n>>> ...\n>>>\n>>>> +DISCUSSION of bundle-uri\n>>>> +^^^^^^^^^^^^^^^^^^^^^^^^\n>>>> +\n>>>> +The intent of the feature is optimize for server resource consumption\n>>>> +in the common case by changing the common case of fetching a very\n>>>> +large PACK during linkgit:git-clone[1] into a smaller incremental\n>>>> +fetch.\n>>>> +\n>>>> +It also allows servers to achieve better caching in combination with\n>>>> +an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n>>>> +\n>>>> +By having new clones or fetches be a more predictable and common\n>>>> +negotiation against the tips of recently produces *.bundle file(s).\n>>>> +Servers might even pre-generate the results of such negotiations for\n>>>> +the `uploadpack.packObjectsHook` as new pushes come in.\n>>>> +\n>>>> +I.e. the server would anticipate that fresh clones will download a\n>>>> +known bundle, followed by catching up to the current state of the\n>>>> +repository using ref tips found in that bundle (or bundles).\n>>>> +\n>>>> +PROTOCOL for bundle-uri\n>>>> +^^^^^^^^^^^^^^^^^^^^^^^\n>>>> +\n>>>> +A `bundle-uri` request takes no arguments, and as noted above does not\n>>>> +currently advertise a capability value. Both may be added in the\n>>>> +future.\n>>>\n>>> One thing I realized was missing from this proposal is any interaction\n>>> with partial clone. It would be disappointing if we could not advertise\n>>> bundles of commit-and-tree packfiles for blobless partial clones.\n>>>\n>>> There is currently no way for the client to signal the filter type\n>>> during this command. Not having any way to extend to include that\n>>> seems like an oversight we should remedy before committing to a\n>>> protocol that can't be extended.\n>>>\n>>> (This also seems like a good enough reason to group the URIs into a\n>>> struct-like storage, because the filter type could be stored next to\n>>> the URI.)\n>> \n>> I'll update the docs to note that. I'd definitely like to leave out any\n>> implementation of filter/shallow for an initial iteration of this for\n>> simplicity, but the protocol keyword/behavior is open-ended enough to\n>> permit any extension.\n>\n> It would be good to be explicit about how this would work. Looking at\n> it fresh, it seems that the server could send multiple bundle URIs with\n> the extra metadata to say which ones have a filter (and what that filter\n> is). The client could then check if a bundle matches the given filter.\n>\n> But this is a bit inverted: the filter mechanism currently has the client\n> request a given filter and the server responds with _at least_ that much\n> data. This allows the server to ignore things like pathspec-filters or\n> certain size-based filters. If the client just ignores a bundle URI\n> because it doesn't match the exact filter, this could lead the client to\n> ask for the data without a bundle, even if it would be faster to just\n> download the advertised bundle.\n>\n> For this reason, I think it would be valuable for the client to tell\n> the server the intended filter, and the server responds with bundle\n> URIs that contain a subset of the information that would be provided\n> by a later fetch request with that filter.\n>\n>> I.e. the server can start advertising \"bundle-uri=shallow\", and future\n>> clients can request arbitrary key-value pairs in addition to just\n>> \"bundle-uri\" now.\n>> \n>> Having said that I think that *probably* this is something that'll never\n>> be implemented, but maybe I'll eat my words there.\n\nI didn't mean to elide past \"filter\", but was just using \"shallow\" as a\nshort-hand for one thing in the \"fetch\" dialog that a client can mention\nthat'll impact PACK generation, just like filter.\n\nHaving thought about this a bit more, I think it should be an invariant\nin any bundle-uri design that the server shouldn't communicate any\nside-channel information whatsoever about a bundle it advertises, if\nthat information can't be discovered in the header of that bundle file.\n\nMind you, this means throwing out large parts of my current proposed\nover-the-wire design, but I think for the better. I.e. the whole\nresponse part where we communicate:\n\n    (bundle-uri (SP bundle-feature-key (=bundle-feature-val)?)* LF)*\n    flush-pkt\n\nWould just become something like:\n\n    (bundle-uri delim-pkt bundle-header? delim-pkt)*\n    flush-pkt\n\nI.e. we'd optionally transfer the content of the bundle header (content\nup to the first \"\\n\\n\") to the client, but *only* ever as a shorthand\nfor saving the client a roundtrip.\n\nThe pointed-to bundle is still 100% the source of truth, and when\nretrieving the bundle-uri we'd ignore whatever \"bundle-header\" we got\nearlier (except insofar as we'd like to say emit a warning() if the two\ndon't match).\n\n(I'd not thought too carefully about these shallow/filter etc. edge\ncases, my main intended use-case has been pre-seeding full clones, and\nhaving this feedback to make me think about it is very valuable).\n\n> You continue focusing on the shallow option, which I agree is not\n> important. The filter option, specifically --filter=blob:none, seems\n> to be critical to have a short-term plan for implementing with this\n> in mind.\n\nPer the above this then just becomes a question of \"how do we produce a\nbundle with those attributes?\".\n\nI *think* that currently there isn't a way to do that, i.e. the PACK\npayload of a bundle is the output of \"git pack-objects\", but due to it\nincluding refs, tips and prerequisites.\n\nI don't think you can say \"this bundle has no blobs\". The\n\"prerequisites\" hard map to the same thing you could put on a\n\"want/have\" line during PACK negotiation.\n\nI think we could/should fix that, i.e. we can bump the bundle format\nversion and have it encode some extended prerequisites/filter/shallow\netc information. You'd then have a 1=1 match between the features of\ngit-upload-pack and what you can transfer via the bundle side-channel.\n\nBut the more I think about it, the more strongly I feel that we should\nalways add that to the bundle *format*, and not as some side-channel\ninformation in this \"bundle-uri\" protocol keyword.\n\nTo me *the* point of this feature is to have servers provide a shorthand\nfor something that's been a well-established trick you can do today, and\nof which there are any number of pre-existing implementations.\n\nI'm not trying to break any new ground here, just make \"git\n[fetch|clone]\" support a well-known trick as a first-class feature via\nprotocol v2.\n\nI'm not the first person to whip up some custom\n\"git-clone-via-bundle.sh\" that takes bundle URI(s) and a repo URI,\nwget's the bundle, calls \"git bundle unbundle\", updates ref tips, and\nthen does a \"git fetch\".\n\nThe benefit of making that a first-class protocol feature over a full\nnegotiation is essentially synonymous with how it's easier in practice\nto widely deploy static assets on CDNs v.s. guaranteeing the same\nnetwork locality, caching etc. when serving up the same payload by\nrunning a custom binary.\n\nOne reason not to add any side-channel information not found in the\nbundle header(s) is that we can also guarantee that there won't be any\nfeature gap between the \"transfer.injectBundleURI\" config key I've\nalready got implemented (and is in the earlier RFC version of this\nseries). I.e. you can do:\n\n    # You can specify this N number of times to inject N bundles\n    git clone \\\n\t-c transfer.injectBundleURI=\"https://something.local/some-repo.bundle\" \\\n        https://example.com/some-repo.git\n\nTo inject CDN support to any remote server that doesn't know about\n\"bundle-uri\", or add to the bundles of a server that does. That URI can\neven be a file:// if you add \"-c fetch.uriProtocols=file\".\n\nI realize that all of the above does *not* answer part of your question\nabout filters, which I think I can accurately rephrase as:\n\n    Ok, so you can dump a static list of bundle URIs from config, but\n    that's always going to be a small list, what about the combinatorial\n    explosion arbitrary upload-pack options? Filters, shallow,\n    include-tag etc.\n\nMy main answer to that is that YAGNI. If you need to spew out an URL for\na PACK after a client describes any of its arbitrary wants, needs,\nfilters etc. you've exactly re-invented what \"packfile-uri\" is today. I\nthink that feature is very useful, and I've got no intention of trying\nto replace it.\n\nI think the sweet spot for \"bundle-uri\" is to advertise a small number\nof bundles that encompass common clone/fetch patterns. I.e. something\nlike a bundle for a full clone with the repo data up to today, and maybe\na couple of other bundles covering a time period that clients would be\nlikely to incrementally update within, e.g. 1 week ago..today &&\nyesterday..now.\n\nI agree that adding say \"full clone, --depth=1\" and \"full clone, no\nblobs\" etc. to that might be *very* useful for some deployments, but per\nthe above I think we should really add that to the bundle format first,\nnot protocol v2.\n"},{"id":"439815","messageId":"e3c923b4-cce0-372c-31f4-9f091239ad8b@gmail.com","threadId":"56782","inReplyTo":"211027.86ilxixoxz.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-27T19:23:22Z","receivedAt":"2021-10-27T19:23:27Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/27/2021 2:01 PM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Wed, Oct 27 2021, Derrick Stolee wrote:\n> \n>> On 10/27/2021 4:29 AM, Ævar Arnfjörð Bjarmason wrote:\n>>>\n>>> On Tue, Oct 26 2021, Derrick Stolee wrote:\n>>>\n>>>> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>>>>> Add a server-side implementation of a new \"bundle-uri\" command to\n>>>>> protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n>>>>> allow conforming clients to optionally seed their initial clones or\n>>>>> incremental fetches from URLs containing \"*.bundle\" files created with\n>>>>> \"git bundle create\".\n>>>>\n>>>> ...\n>>>>\n>>>>> +DISCUSSION of bundle-uri\n>>>>> +^^^^^^^^^^^^^^^^^^^^^^^^\n>>>>> +\n>>>>> +The intent of the feature is optimize for server resource consumption\n>>>>> +in the common case by changing the common case of fetching a very\n>>>>> +large PACK during linkgit:git-clone[1] into a smaller incremental\n>>>>> +fetch.\n>>>>> +\n>>>>> +It also allows servers to achieve better caching in combination with\n>>>>> +an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n>>>>> +\n>>>>> +By having new clones or fetches be a more predictable and common\n>>>>> +negotiation against the tips of recently produces *.bundle file(s).\n>>>>> +Servers might even pre-generate the results of such negotiations for\n>>>>> +the `uploadpack.packObjectsHook` as new pushes come in.\n>>>>> +\n>>>>> +I.e. the server would anticipate that fresh clones will download a\n>>>>> +known bundle, followed by catching up to the current state of the\n>>>>> +repository using ref tips found in that bundle (or bundles).\n>>>>> +\n>>>>> +PROTOCOL for bundle-uri\n>>>>> +^^^^^^^^^^^^^^^^^^^^^^^\n>>>>> +\n>>>>> +A `bundle-uri` request takes no arguments, and as noted above does not\n>>>>> +currently advertise a capability value. Both may be added in the\n>>>>> +future.\n>>>>\n>>>> One thing I realized was missing from this proposal is any interaction\n>>>> with partial clone. It would be disappointing if we could not advertise\n>>>> bundles of commit-and-tree packfiles for blobless partial clones.\n>>>>\n>>>> There is currently no way for the client to signal the filter type\n>>>> during this command. Not having any way to extend to include that\n>>>> seems like an oversight we should remedy before committing to a\n>>>> protocol that can't be extended.\n>>>>\n>>>> (This also seems like a good enough reason to group the URIs into a\n>>>> struct-like storage, because the filter type could be stored next to\n>>>> the URI.)\n>>>\n>>> I'll update the docs to note that. I'd definitely like to leave out any\n>>> implementation of filter/shallow for an initial iteration of this for\n>>> simplicity, but the protocol keyword/behavior is open-ended enough to\n>>> permit any extension.\n>>\n>> It would be good to be explicit about how this would work. Looking at\n>> it fresh, it seems that the server could send multiple bundle URIs with\n>> the extra metadata to say which ones have a filter (and what that filter\n>> is). The client could then check if a bundle matches the given filter.\n>>\n>> But this is a bit inverted: the filter mechanism currently has the client\n>> request a given filter and the server responds with _at least_ that much\n>> data. This allows the server to ignore things like pathspec-filters or\n>> certain size-based filters. If the client just ignores a bundle URI\n>> because it doesn't match the exact filter, this could lead the client to\n>> ask for the data without a bundle, even if it would be faster to just\n>> download the advertised bundle.\n>>\n>> For this reason, I think it would be valuable for the client to tell\n>> the server the intended filter, and the server responds with bundle\n>> URIs that contain a subset of the information that would be provided\n>> by a later fetch request with that filter.\n>>\n>>> I.e. the server can start advertising \"bundle-uri=shallow\", and future\n>>> clients can request arbitrary key-value pairs in addition to just\n>>> \"bundle-uri\" now.\n>>>\n>>> Having said that I think that *probably* this is something that'll never\n>>> be implemented, but maybe I'll eat my words there.\n> \n> I didn't mean to elide past \"filter\", but was just using \"shallow\" as a\n> short-hand for one thing in the \"fetch\" dialog that a client can mention\n> that'll impact PACK generation, just like filter.\n> \n> Having thought about this a bit more, I think it should be an invariant\n> in any bundle-uri design that the server shouldn't communicate any\n> side-channel information whatsoever about a bundle it advertises, if\n> that information can't be discovered in the header of that bundle file.\n> \n> Mind you, this means throwing out large parts of my current proposed\n> over-the-wire design, but I think for the better. I.e. the whole\n> response part where we communicate:\n> \n>     (bundle-uri (SP bundle-feature-key (=bundle-feature-val)?)* LF)*\n>     flush-pkt\n> \n> Would just become something like:\n> \n>     (bundle-uri delim-pkt bundle-header? delim-pkt)*\n>     flush-pkt\n> \n> I.e. we'd optionally transfer the content of the bundle header (content\n> up to the first \"\\n\\n\") to the client, but *only* ever as a shorthand\n> for saving the client a roundtrip.\n\nIt still seems like we're better off letting the client request a\nfilter and have the server present URIs that the client can use,\nand the server can choose to ignore the filter or provide URIs that\nare specific to that filter. Sending the full list and making the\nclient decide what it wants seems like it will be more complicated\nthan necessary.\n\nHowever, I'll withhold complete judgement until I see a full proposal\nin a v2.\n\n> I think the sweet spot for \"bundle-uri\" is to advertise a small number\n> of bundles that encompass common clone/fetch patterns. I.e. something\n> like a bundle for a full clone with the repo data up to today, and maybe\n> a couple of other bundles covering a time period that clients would be\n> likely to incrementally update within, e.g. 1 week ago..today &&\n> yesterday..now.\n> \n> I agree that adding say \"full clone, --depth=1\" and \"full clone, no\n> blobs\" etc. to that might be *very* useful for some deployments, but per\n> the above I think we should really add that to the bundle format first,\n> not protocol v2.\n \nI'm focusing my interest in this topic not on \"how can we make what we\nalready do faster?\" but \"how can we unlock scale not previously\npossible?\" Allowing blobless clones is an important part of this, in\nmy opinion, so it is my _default_ mode of operating.\n\nThanks,\n-Stolee\n"},{"id":"439831","messageId":"211027.86a6iuxk3x.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"e3c923b4-cce0-372c-31f4-9f091239ad8b@gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-27T20:22:28Z","receivedAt":"2021-10-27T20:56:24Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Oct 27 2021, Derrick Stolee wrote:\n\n> On 10/27/2021 2:01 PM, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> On Wed, Oct 27 2021, Derrick Stolee wrote:\n>> \n>>> On 10/27/2021 4:29 AM, Ævar Arnfjörð Bjarmason wrote:\n>>>>\n>>>> On Tue, Oct 26 2021, Derrick Stolee wrote:\n>>>>\n>>>>> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>>>>>> Add a server-side implementation of a new \"bundle-uri\" command to\n>>>>>> protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n>>>>>> allow conforming clients to optionally seed their initial clones or\n>>>>>> incremental fetches from URLs containing \"*.bundle\" files created with\n>>>>>> \"git bundle create\".\n>>>>>\n>>>>> ...\n>>>>>\n>>>>>> +DISCUSSION of bundle-uri\n>>>>>> +^^^^^^^^^^^^^^^^^^^^^^^^\n>>>>>> +\n>>>>>> +The intent of the feature is optimize for server resource consumption\n>>>>>> +in the common case by changing the common case of fetching a very\n>>>>>> +large PACK during linkgit:git-clone[1] into a smaller incremental\n>>>>>> +fetch.\n>>>>>> +\n>>>>>> +It also allows servers to achieve better caching in combination with\n>>>>>> +an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n>>>>>> +\n>>>>>> +By having new clones or fetches be a more predictable and common\n>>>>>> +negotiation against the tips of recently produces *.bundle file(s).\n>>>>>> +Servers might even pre-generate the results of such negotiations for\n>>>>>> +the `uploadpack.packObjectsHook` as new pushes come in.\n>>>>>> +\n>>>>>> +I.e. the server would anticipate that fresh clones will download a\n>>>>>> +known bundle, followed by catching up to the current state of the\n>>>>>> +repository using ref tips found in that bundle (or bundles).\n>>>>>> +\n>>>>>> +PROTOCOL for bundle-uri\n>>>>>> +^^^^^^^^^^^^^^^^^^^^^^^\n>>>>>> +\n>>>>>> +A `bundle-uri` request takes no arguments, and as noted above does not\n>>>>>> +currently advertise a capability value. Both may be added in the\n>>>>>> +future.\n>>>>>\n>>>>> One thing I realized was missing from this proposal is any interaction\n>>>>> with partial clone. It would be disappointing if we could not advertise\n>>>>> bundles of commit-and-tree packfiles for blobless partial clones.\n>>>>>\n>>>>> There is currently no way for the client to signal the filter type\n>>>>> during this command. Not having any way to extend to include that\n>>>>> seems like an oversight we should remedy before committing to a\n>>>>> protocol that can't be extended.\n>>>>>\n>>>>> (This also seems like a good enough reason to group the URIs into a\n>>>>> struct-like storage, because the filter type could be stored next to\n>>>>> the URI.)\n>>>>\n>>>> I'll update the docs to note that. I'd definitely like to leave out any\n>>>> implementation of filter/shallow for an initial iteration of this for\n>>>> simplicity, but the protocol keyword/behavior is open-ended enough to\n>>>> permit any extension.\n>>>\n>>> It would be good to be explicit about how this would work. Looking at\n>>> it fresh, it seems that the server could send multiple bundle URIs with\n>>> the extra metadata to say which ones have a filter (and what that filter\n>>> is). The client could then check if a bundle matches the given filter.\n>>>\n>>> But this is a bit inverted: the filter mechanism currently has the client\n>>> request a given filter and the server responds with _at least_ that much\n>>> data. This allows the server to ignore things like pathspec-filters or\n>>> certain size-based filters. If the client just ignores a bundle URI\n>>> because it doesn't match the exact filter, this could lead the client to\n>>> ask for the data without a bundle, even if it would be faster to just\n>>> download the advertised bundle.\n>>>\n>>> For this reason, I think it would be valuable for the client to tell\n>>> the server the intended filter, and the server responds with bundle\n>>> URIs that contain a subset of the information that would be provided\n>>> by a later fetch request with that filter.\n>>>\n>>>> I.e. the server can start advertising \"bundle-uri=shallow\", and future\n>>>> clients can request arbitrary key-value pairs in addition to just\n>>>> \"bundle-uri\" now.\n>>>>\n>>>> Having said that I think that *probably* this is something that'll never\n>>>> be implemented, but maybe I'll eat my words there.\n>> \n>> I didn't mean to elide past \"filter\", but was just using \"shallow\" as a\n>> short-hand for one thing in the \"fetch\" dialog that a client can mention\n>> that'll impact PACK generation, just like filter.\n>> \n>> Having thought about this a bit more, I think it should be an invariant\n>> in any bundle-uri design that the server shouldn't communicate any\n>> side-channel information whatsoever about a bundle it advertises, if\n>> that information can't be discovered in the header of that bundle file.\n>> \n>> Mind you, this means throwing out large parts of my current proposed\n>> over-the-wire design, but I think for the better. I.e. the whole\n>> response part where we communicate:\n>> \n>>     (bundle-uri (SP bundle-feature-key (=bundle-feature-val)?)* LF)*\n>>     flush-pkt\n>> \n>> Would just become something like:\n>> \n>>     (bundle-uri delim-pkt bundle-header? delim-pkt)*\n>>     flush-pkt\n>> \n>> I.e. we'd optionally transfer the content of the bundle header (content\n>> up to the first \"\\n\\n\") to the client, but *only* ever as a shorthand\n>> for saving the client a roundtrip.\n>\n> It still seems like we're better off letting the client request a\n> filter and have the server present URIs that the client can use,\n> and the server can choose to ignore the filter or provide URIs that\n> are specific to that filter.[...]\n\nI've tested this a bit now and think there's no way to create such a\nbundle currently. I.e. try:\n\n    git clone --filter=blob:none --single-branch --no-tags https://github.com/git/git.git\n    cd git\n    git config --unset remote.origin.partialclonefilter\n    git config --unset remote.origin.promisor\n\nYou'll get:\n    \n    $ GIT_TRACE_PACKET=1 git bundle create --version=3 master.bdl master\n    Enumerating objects: 306784, done.\n    Counting objects: 100% (306784/306784), done.\n    Compressing objects: 100% (69265/69265), done.\n    fatal: unable to read c85385dc03228450cb7fb6d306252038a91b47e6\n    error: pack-objects died\n\nIf you didn't do that config munging we'd create the pack, but it would\nbe inflated with the blobs (after going back and getting them from the\nserver).\n\nSo aside from any questions of how you'd hypothetically communicate your\ndesire to get such bundle from the server, I don't think it could serve\none up.\n\nSo I think this is moot until the bundle format itself could support\nit. I'll need to \"git bundle [verify|unbundle]\" whatever I get on the\nother end.\n\nI really don't mean this in any way as dodging the desirability of this\nfeature. I'd really like to have it too. I think implementing it should\nbe relatively simple, and I've got an implementation in mind that makes\nthis future-proof for anything else we'd like to add.\n\nI.e. if you look at that v3 format bundle you'll see:\n    \n    $ head -c 100 master.bdl\n    # v3 git bundle\n    @object-format=sha1\n    e9e5ba39a78c8f5057262d49e261b42a8660d5b9 refs/heads/master\n    \n    PACK\n\nWouldn't this just be a matter of including extra lines with:\n\n    # I'm assuming that the promisor url can be assumed to be \"the url\n    # we cloned this from\", but maybe we need @remoteURL=https://....\n    @promisor=true\n    @filter = blob:none\n\nI.e. exactly corresponding to the .git/config we'd end up with,\nconfig. We'd then (I think) create .git/objects/pack/*.promisor with the\nOIDs of each of the inflated tips (I'm not familiar with .promisor\nfiles).\n\nAnd a thing I need to include in the bundle-uri protocol is that the\nclient should not just include a \"bundle-uri\" attribute, but have a\n\"value\" describing the bundle format it accepts. I.e. now:\n\n    bundle-uri=v3,object-format\n\nAnd for supporting the above:\n\n    bundle-uri=v3,object-format,promisor,filter\n\nI.e. currently we die on any bundle capability except \"object-format\",\nif we're going to discover what to send we'd like a less crappy way than\nparsing the version from the \"agent\" field.\n\n> Sending the full list and making the client decide what it wants seems\n> like it will be more complicated than necessary. However, I'll\n> withhold complete judgement until I see a full proposal in a v2.\n\nI'm very thankful for the thorough review, and it's exciting that you'd\nlike to use this feature in some form, and I'll definitely do my best to\nsupport (and if not, future-proof) any use-cases you have in mind.\n\nBut I really don't get how this wouldn't effectively be functionally\nindistinguishable from packfile-uri, sans a nit here and there.\n\nI can see the convenience of having say 100 bundles, advertising 5, and\nthen after a full negotiation dialog pointing the equivalent of a\npackfile-uri at a *.bundle file, just because that's what you happen to\nhave around already. If bundle-uri is your main static file distribution\nyou don't want a duplicate *.pack (without the bundle header) just for\nthat.\n\nI think a logical extension of the packfile-uri feature for those that\nneed extended negotiation before deciding on the static URL would be to\nteach the packfile-uri downloader to ignore an optional bundle header of\nany PACK it finds at a URL (which would not be the same as this\nproposal), just to support that use-case.\n\nBut, isn't that essentially what you'd want in those cases?\n\nSpewing a \"here's my bundles\" at a client gets it started quickly, and\nalso has the side-benefit of making those assets more cachable, as well\nas creating a known base for the caching of any subsequent \"...and a\nPACK negotiation to make it fully up-to-date\" request.\n\nThe bundles are also our de-facto sneakernet and format, and can be used\nfor incremental replication. All of which is also a sweet spot for\nbundle-uri, i.e. the combination of being able to re-use already\n\"replicated\" files for CDN-ing, and providing wider access to CDN\nfeatures for \"dumb\" servers.\n\nBut once we're in dialog with a client to discuss its arbitrary filter\npreferences before giving it a URL we're going to be most likely\nimplementing that as a mode of upload-pack.c anyway, and when it spews\nout an optional URL at the end of that dialog....\n\n>> I think the sweet spot for \"bundle-uri\" is to advertise a small number\n>> of bundles that encompass common clone/fetch patterns. I.e. something\n>> like a bundle for a full clone with the repo data up to today, and maybe\n>> a couple of other bundles covering a time period that clients would be\n>> likely to incrementally update within, e.g. 1 week ago..today &&\n>> yesterday..now.\n>> \n>> I agree that adding say \"full clone, --depth=1\" and \"full clone, no\n>> blobs\" etc. to that might be *very* useful for some deployments, but per\n>> the above I think we should really add that to the bundle format first,\n>> not protocol v2.\n>  \n> I'm focusing my interest in this topic not on \"how can we make what we\n> already do faster?\" but \"how can we unlock scale not previously\n> possible?\" Allowing blobless clones is an important part of this, in\n> my opinion, so it is my _default_ mode of operating.\n\n*nod*, I'm keen to support it using something like what's described\nabove. I.e. stick it as new headers in the bundle format, then be able\nto advertise those for common cases. I'd think most of these would be a\nfew combinations (usually something copy/pasted from a relevant dev\nguide), e.g. \"all history, no blobs\", \"main branch, no tags, no blobs\"\netc.\n\nAside from this bundle-uri protocol proposal being able to sneakernet a\nrepo you cloned like that around as-is seems highly desirable, and just\na feature gap we added when those features were added to \"git fetch\" and\nfriends, but not \"git bundle\".\n"},{"id":"440036","messageId":"4039d500-1924-9610-495f-69e196f17322@gmail.com","threadId":"56782","inReplyTo":"211027.86a6iuxk3x.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-29T18:30:54Z","receivedAt":"2021-10-29T18:31:01Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/27/2021 4:22 PM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Wed, Oct 27 2021, Derrick Stolee wrote:\n>> It still seems like we're better off letting the client request a\n>> filter and have the server present URIs that the client can use,\n>> and the server can choose to ignore the filter or provide URIs that\n>> are specific to that filter.[...]\n> \n> I've tested this a bit now and think there's no way to create such a\n> bundle currently. I.e. try:\n> \n>     git clone --filter=blob:none --single-branch --no-tags https://github.com/git/git.git\n>     cd git\n>     git config --unset remote.origin.partialclonefilter\n>     git config --unset remote.origin.promisor\n> \n> You'll get:\n>     \n>     $ GIT_TRACE_PACKET=1 git bundle create --version=3 master.bdl master\n>     Enumerating objects: 306784, done.\n>     Counting objects: 100% (306784/306784), done.\n>     Compressing objects: 100% (69265/69265), done.\n>     fatal: unable to read c85385dc03228450cb7fb6d306252038a91b47e6\n>     error: pack-objects died\n> \n> If you didn't do that config munging we'd create the pack, but it would\n> be inflated with the blobs (after going back and getting them from the\n> server).\n> \n> So aside from any questions of how you'd hypothetically communicate your\n> desire to get such bundle from the server, I don't think it could serve\n> one up.\n> \n> So I think this is moot until the bundle format itself could support\n> it. I'll need to \"git bundle [verify|unbundle]\" whatever I get on the\n> other end.\n\nThank you for demonstrating that bundles don't currently work with\nfilters. This would need to be remedied, but can be done independently.\n\nFor now, let's just make sure that there is a paved path for extending\nwhatever is decided here to work with filters in the future.\n\nAs for the overall design, I have some thoughts that I'm going to\nbreak out into a new message responding to your cover letter.\n\nThanks,\n-Stolee\n"},{"id":"440047","messageId":"c9c4e1e7-aaa2-bbad-355b-8439fad93fa7@gmail.com","threadId":"56782","inReplyTo":"cover-0.3-00000000000-20211025T211159Z-avarab@gmail.com","subject":"Re: [PATCH 0/3] bundle-uri: \"dumb\" static CDN offloading, spec & server implementation","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-10-29T18:46:19Z","receivedAt":"2021-10-29T18:46:46Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n> This implements a new \"bundle-uri\" protocol v2 extension, which allows\n> servers to advertise *.bundle files which clients can pre-seed their\n> full \"clone\"'s or incremental \"fetch\"'s from.\n> \n> This is both an alternative to, and complimentary to the existing\n> \"packfile-uri\" mechanism, i.e. servers and/or clients can pick one or\n> both, but would generally pick one over the other.\n> \n> This \"bundle-uri\" mechanism has the advantage of being dumber, and\n> offloads more complexity from the server side to the client\n> side.\n\nGenerally, I like that using bundles presents an easier way to serve\nstatic content from an alternative source and then let Git's fetch\nnegotiation catch up with the remainder.\n\nHowever, after inspecting your design and talking to some GitHub\nengineers who know more about CDNs and general internet things than I\ndo, I want to propose an alternative design. I think this new design\nis simultaneously more flexible as well as promotes further decoupling\nof the origin Git server and the bundle contents.\n\nYour proposed design extends protocol v2 to let the client request a\nlist of bundle URIs from the origin server. However, this still requires\nthe origin server to know about this list. Further, your implementation\nfocuses on the server side without integrating with the client.\n\nI propose that we flip this around. The \"bundle server\" should know\nwhich bundles are available at which URIs, and the client should contact\nthe bundle server directly for a \"table of contents\" that lists these\nURIs, along with metadata related to each URI. The origin Git server\nthen would only need to store the list of bundle servers and the URIs\nto their table of contents. The client could then pick from among those\nbundle servers (probably by ping time, or randomly) to start the bundle\ndownloads.\n\nTo summarize, there are two pieces here, that can be implemented at\ndifferent times:\n\n1. Create a specification for a \"bundle server\" that doesn't need to\n   speak the Git protocol at all. This could be a REST API specification\n   using well-established standards such as JSON for the table of\n   contents.\n\n2. Create a way for the origin Git server to advertise known bundle\n   servers to clients so they can automatically benefit from faster\n   downloads without needing to know about bundle servers.\n\nThere are a few key benefits to this approach:\n\n * Further decoupling. The origin Git server doesn't need to know how\n   the bundle server organizes its bundles. This allows maximum flexibility\n   depending on whether the bundles are stored in something like a CDN\n   (where bundles can't be too big) or some kind of blob storage (where\n   they can have arbitrarily large size).\n\n * The bundle servers could be run completely independently from the\n   origin Git server. Organizations could run their own bundle servers to\n   host data in the same building as their build farms. As long as they\n   can configure the bundle location at clone/fetch time, the origin Git\n   server doesn't need to be involved.\n\nWhile I didn't go so far as to create a clear standard or implement a\nprototype in the Git codebase, I created a very simple prototype [1] using\na python script that parses a JSON table of contents and downloads\nbundles into the Git repository. Then, I made a 'clone.sh' script that\ninitializes a repository using the bundle fetcher and fetching the\nremainder from the origin Git server. I even computed static bundles for\nthe git.git repository based on where 'master' has been over several days\nin the past month, to give an example of incremental bundles. You can\ntest the approach all the way to including the fetch to github.com (note\nhow the GitHub servers were not modified in any way for this).\n\n[1] https://github.com/derrickstolee/bundles\n\nThere are a lot of limitations to the prototype, but it hopefully\ndemonstrates the possibility of using something other than the Git protocol\nto solve these problems.\n\nLet me know if you are interested in switching your approach to something\nmore like what I propose here. There are many more questions about what\ninformation could/should be located in the table of contents and how it can\nbe extended in the future. I'm interested to explore that space with you.\n\nThanks,\n-Stolee\n"},{"id":"440119","messageId":"211030.86mtmr3po0.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"c9c4e1e7-aaa2-bbad-355b-8439fad93fa7@gmail.com","subject":"Re: [PATCH 0/3] bundle-uri: \"dumb\" static CDN offloading, spec & server implementation","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-30T07:21:38Z","receivedAt":"2021-10-30T08:03:35Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Oct 29 2021, Derrick Stolee wrote:\n\n> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>> This implements a new \"bundle-uri\" protocol v2 extension, which allows\n>> servers to advertise *.bundle files which clients can pre-seed their\n>> full \"clone\"'s or incremental \"fetch\"'s from.\n>> \n>> This is both an alternative to, and complimentary to the existing\n>> \"packfile-uri\" mechanism, i.e. servers and/or clients can pick one or\n>> both, but would generally pick one over the other.\n>> \n>> This \"bundle-uri\" mechanism has the advantage of being dumber, and\n>> offloads more complexity from the server side to the client\n>> side.\n>\n> Generally, I like that using bundles presents an easier way to serve\n> static content from an alternative source and then let Git's fetch\n> negotiation catch up with the remainder.\n>\n> However, after inspecting your design and talking to some GitHub\n> engineers who know more about CDNs and general internet things than I\n> do, I want to propose an alternative design. I think this new design\n> is simultaneously more flexible as well as promotes further decoupling\n> of the origin Git server and the bundle contents.\n>\n> Your proposed design extends protocol v2 to let the client request a\n> list of bundle URIs from the origin server. However, this still requires\n> the origin server to know about this list. [...]\n\nInteresting, more below...\n\n> Further, your implementation focuses on the server side without\n> integrating with the client.\n\nDo you mean these 3 patches we're discussing now? Yes, that's the\nserver-side and protocol specification only, because I figured talking\nabout just the spec might be helpful.\n\nBut as noted in the CL and previously on-list I have a larger set of\npatches to implement the client behavior, an old RFC version of that\nhere (I've since changed some things...):\nhttps://lore.kernel.org/git/RFC-cover-00.13-0000000000-20210805T150534Z-avarab@gmail.com/\n\nI mean, you commented on those too, so I'm not sure if that's what you\nmeant, but just for context...\n\n> I propose that we flip this around. The \"bundle server\" should know\n> which bundles are available at which URIs, and the client should contact\n> the bundle server directly for a \"table of contents\" that lists these\n> URIs, along with metadata related to each URI. The origin Git server\n> then would only need to store the list of bundle servers and the URIs\n> to their table of contents. The client could then pick from among those\n> bundle servers (probably by ping time, or randomly) to start the bundle\n> downloads.\n\nI hadn't considered the server not advertising the list, but pointing to\nanother URI that has the list. I was thinking that the server would be\nclose enough to whatever's generating the list that updating the list\nthere wouldn't be a meaningful limitation for anyone.\n\nBut you seem to have a use-case for it, I'd be curious to hear why\nspecifically, but in any case that's easy to support in the client\npatches I have.\n\nThere's a point at which we get the list of URIs from the server, to\nsupport your case the client would just advertise the one TOC URI.\n\nThen similarly to the \"packfile-uri\" special-case of handling a *.bundle\ninstead of a PACK that I noted in [1], the downloader would just spot\n\"oh this isn't a bundle, but list of URIs, and then fetch those (even\nrecursively), and eventually get to *.bundle files.\n\n> To summarize, there are two pieces here, that can be implemented at\n> different times:\n>\n> 1. Create a specification for a \"bundle server\" that doesn't need to\n>    speak the Git protocol at all. This could be a REST API specification\n>    using well-established standards such as JSON for the table of\n>    contents.\n>\n> 2. Create a way for the origin Git server to advertise known bundle\n>    servers to clients so they can automatically benefit from faster\n>    downloads without needing to know about bundle servers.\n>\n> There are a few key benefits to this approach:\n>\n>  * Further decoupling. The origin Git server doesn't need to know how\n>    the bundle server organizes its bundles. This allows maximum flexibility\n>    depending on whether the bundles are stored in something like a CDN\n>    (where bundles can't be too big) or some kind of blob storage (where\n>    they can have arbitrarily large size).\n>\n>  * The bundle servers could be run completely independently from the\n>    origin Git server. Organizations could run their own bundle servers to\n>    host data in the same building as their build farms. As long as they\n>    can configure the bundle location at clone/fetch time, the origin Git\n>    server doesn't need to be involved.\n>\n> While I didn't go so far as to create a clear standard or implement a\n> prototype in the Git codebase, I created a very simple prototype [1] using\n> a python script that parses a JSON table of contents and downloads\n> bundles into the Git repository. Then, I made a 'clone.sh' script that\n> initializes a repository using the bundle fetcher and fetching the\n> remainder from the origin Git server. I even computed static bundles for\n> the git.git repository based on where 'master' has been over several days\n> in the past month, to give an example of incremental bundles. You can\n> test the approach all the way to including the fetch to github.com (note\n> how the GitHub servers were not modified in any way for this).\n>\n> [1] https://github.com/derrickstolee/bundles\n>\n> There are a lot of limitations to the prototype, but it hopefully\n> demonstrates the possibility of using something other than the Git protocol\n> to solve these problems.\n\nIn your proposal the TOC bundle itself doesn't need to speak the git\nprotocol.\n\nBut as as soon as we specify such a thing all of that becomes a part of\nthe git protocol at large in any meaningful way, i.e. git.git's client\nand any other client that wants to implement the full protocol at large\nwould now need to understand not only pkt-line but also ship a JSON\ndecoder etc.\n\nI don't see an inherent problem with us wanting to support some nested\nencoding format as part of the protocol, but JSON seems like a\nparticularly bad choice. It's specified as UTF-8 only (or rather, \"a\nUnicode enoding\"), so you can't stick both valid UTF-8 and binary data\ninto it.\n\nOur refs on the other hand don't conform to that, so having a JSON\nformat means you can never have something that refers to refnames, which\ngiven that we're talking about bundles, whose own header already has\nthat information.\n\n> Let me know if you are interested in switching your approach to something\n> more like what I propose here. There are many more questions about what\n> information could/should be located in the table of contents and how it can\n> be extended in the future. I'm interested to explore that space with you.\n\nAs noted above, the TOC part of this seems interesting, and I don't see\na reason not to implement that.\n\nBut as noted in [1] I really don't see why it would be a good idea to\nimplement a side-format that's encoding a limited subset of what you'd\nfind in bundle headers.\n\nSpecifically on the meta-information you're proposing:\n\n== requires\n\nIn your example you've added a monolithic \"requires\" relationship\nbetween bundles, saying \"This assumes that the bundles can be ordered\".\n\nBut that's not something you can assume for actual bundle files,\ni.e. the prerequisite relationship is per-reftip, it's not the case that\na given bundle requires another bundle, it's the case that tips found in\nthem may or may not depend on other prerequisites.\n\nIf you're creating bundles that contain only one tip there's a 1=1\nmapping to what you're proposing with \"requires\", but otherwise there\nisn't.\n\n== timestamp\n\n\"This allows us to reexamine the table of contents and only download the\nbundles that are newer than that timestamp.\"\n\nWe're usually going to be fetching these over http(s), why duplicate\nwhat you can already get if the server just takes care to create unique\nfilenames (e.g. as a function of the SHA of their contents), and then\nprovides appropriate caching headers to a client so that they'll be\ncached forever?\n\nI think that gives you everything you'd like out of the \"timestamp\" and\nmore, the \"more\" being that since it's part of a protocol that's already\nstandard you'd have e.g. intermediate caching proxies understanding this\nimplicitly, in addition to the git client itself.\n\nSo on a network that's say locally unpacking https connections to a\npublic CDN you could have a local caching proxy for your N local\nclients, as opposed to a custom \"timestamp\" value, which only each local\ngit client will understand.\n\n== Generally\n\nSorry, I've got to run, so I haven't addressed all the things you\nbrought up, but generally while I think that the TOC idea is a good one.\n\nI don't see a reason for why most/all of the other bits shouldn't be\nleaning into either the bundle header (and for any TOC shortcut, dump it\nas-is, as noted in [1]), or in the case of \"timestamp\" lean into the\nproperties of the transport protocol.\n\nAnd just generally on overall protocol complexity, wouldn't it be OK if\nany such TOC is just in pkt-line format?\n\nWe could just provide a git plumbing tool to spew that out, and having\nsome static server job call that once and ever more serve up a a\nplain-file doesn't seem like a big restriction, and would mean that any\ngit client code wouldn't need to deal with another encoding format.\n\n1. https://lore.kernel.org/git/211027.86a6iuxk3x.gmgdl@evledraar.gmail.com/\n"},{"id":"440124","messageId":"c46db4a8-c705-6c6c-bcb3-b2d294120dce@iee.email","threadId":"56782","inReplyTo":"211027.86ilxixoxz.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 2/3] protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2021-10-30T14:51:19Z","receivedAt":"2021-10-30T14:51:25Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"An aside:\n\nOn 27/10/2021 19:01, Ævar Arnfjörð Bjarmason wrote:\n> I don't think you can say \"this bundle has no blobs\". The\n> \"prerequisites\" hard map to the same thing you could put on a\n> \"want/have\" line during PACK negotiation.\n>\n> I think we could/should fix that, i.e. we can \n\n> bump the bundle format\n> version and have it encode some extended prerequisites/filter/shallow\n> etc information.\nIf the format is bumped, could we also include the\nHEAD=<particular-branch> info within that format.\nThe `guess the HEAD` algorithm isn't ideal and shows up in user\nquestions every now and again.\n\n>  You'd then have a 1=1 match between the features of\n> git-upload-pack and what you can transfer via the bundle side-channel.\n--\nPhilip\n"},{"id":"440271","messageId":"05262630-2e53-3edf-8fa1-fd1a41cc2d28@gmail.com","threadId":"56782","inReplyTo":"211030.86mtmr3po0.gmgdl@evledraar.gmail.com","subject":"Re: [PATCH 0/3] bundle-uri: \"dumb\" static CDN offloading, spec & server implementation","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-11-01T21:00:55Z","receivedAt":"2021-11-01T21:01:00Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 10/30/2021 3:21 AM, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Oct 29 2021, Derrick Stolee wrote:\n> \n>> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>>> This implements a new \"bundle-uri\" protocol v2 extension, which allows\n>>> servers to advertise *.bundle files which clients can pre-seed their\n>>> full \"clone\"'s or incremental \"fetch\"'s from.\n>>>\n>>> This is both an alternative to, and complimentary to the existing\n>>> \"packfile-uri\" mechanism, i.e. servers and/or clients can pick one or\n>>> both, but would generally pick one over the other.\n>>>\n>>> This \"bundle-uri\" mechanism has the advantage of being dumber, and\n>>> offloads more complexity from the server side to the client\n>>> side.\n>>\n>> Generally, I like that using bundles presents an easier way to serve\n>> static content from an alternative source and then let Git's fetch\n>> negotiation catch up with the remainder.\n>>\n>> However, after inspecting your design and talking to some GitHub\n>> engineers who know more about CDNs and general internet things than I\n>> do, I want to propose an alternative design. I think this new design\n>> is simultaneously more flexible as well as promotes further decoupling\n>> of the origin Git server and the bundle contents.\n>>\n>> Your proposed design extends protocol v2 to let the client request a\n>> list of bundle URIs from the origin server. However, this still requires\n>> the origin server to know about this list. [...]\n> \n> Interesting, more below...\n> \n>> Further, your implementation focuses on the server side without\n>> integrating with the client.\n> \n> Do you mean these 3 patches we're discussing now? Yes, that's the\n> server-side and protocol specification only, because I figured talking\n> about just the spec might be helpful.\n> \n> But as noted in the CL and previously on-list I have a larger set of\n> patches to implement the client behavior, an old RFC version of that\n> here (I've since changed some things...):\n> https://lore.kernel.org/git/RFC-cover-00.13-0000000000-20210805T150534Z-avarab@gmail.com/\n> \n> I mean, you commented on those too, so I'm not sure if that's what you\n> meant, but just for context...\n\nYeah, I'm not able to keep all of that in my head, and I focused on\nwhat you presented in this thread.\n\n>> I propose that we flip this around. The \"bundle server\" should know\n>> which bundles are available at which URIs, and the client should contact\n>> the bundle server directly for a \"table of contents\" that lists these\n>> URIs, along with metadata related to each URI. The origin Git server\n>> then would only need to store the list of bundle servers and the URIs\n>> to their table of contents. The client could then pick from among those\n>> bundle servers (probably by ping time, or randomly) to start the bundle\n>> downloads.\n> \n> I hadn't considered the server not advertising the list, but pointing to\n> another URI that has the list. I was thinking that the server would be\n> close enough to whatever's generating the list that updating the list\n> there wouldn't be a meaningful limitation for anyone.\n> \n> But you seem to have a use-case for it, I'd be curious to hear why\n> specifically, but in any case that's easy to support in the client\n> patches I have.\n\nShow me the client patches and then I can determine if I think that\nis sufficiently flexible.\n\nIn general, I want to expand the scope of this feature beyond \"bundles\non a CDN\" and towards \"alternative sources of Git object data\" which\n_could_ be a CDN, but could also be geodistributed HTTP servers that\nmanage their own copy of the Git data (periodically fetching from the\norigin). These could be self-hosted by organizations with needs for\nlow-latency, high-throughput downloads of object data.\n\nFor a concrete example, a group with a build farm could create their\nown bundle server on the same LAN as the build machines, and they could\nmirror whatever Git service they want. This requires the users setting\nup the server and telling the machines about the URL for the table of\ncontents at clone/fetch time.\n\nBy having the origin Git server advertise the table of contents, hosts\nsuch as GitHub, GitLab, and others could have their own CDN solutions\nthat clients discover automatically. This is clearly the environment\nyou are targeting. Allowing a redirection to another table of contents\nfurther decouples what is responsible for the bundle organization away\nfrom the origin Git server.\n\nOne thing neither of us have touched is authentication, so we'll want\nto find out how to access private information securely in this model.\nAuthentication doesn't matter for CDNs, but would matter for other\nhosting models.\n\n> There's a point at which we get the list of URIs from the server, to\n> support your case the client would just advertise the one TOC URI.\n> \n> Then similarly to the \"packfile-uri\" special-case of handling a *.bundle\n> instead of a PACK that I noted in [1], the downloader would just spot\n> \"oh this isn't a bundle, but list of URIs, and then fetch those (even\n> recursively), and eventually get to *.bundle files.\n\nThis recursive \"follow the contents\" approach seems to be a nice\ngeneral approach. I would still want to have some kind of specification\nabout what could be seen at these URIs before modifying the protocol v2\nspecification.\n\n>> To summarize, there are two pieces here, that can be implemented at\n>> different times:\n>>\n>> 1. Create a specification for a \"bundle server\" that doesn't need to\n>>    speak the Git protocol at all. This could be a REST API specification\n>>    using well-established standards such as JSON for the table of\n>>    contents.\n>>\n>> 2. Create a way for the origin Git server to advertise known bundle\n>>    servers to clients so they can automatically benefit from faster\n>>    downloads without needing to know about bundle servers.\n>>\n>> There are a few key benefits to this approach:\n>>\n>>  * Further decoupling. The origin Git server doesn't need to know how\n>>    the bundle server organizes its bundles. This allows maximum flexibility\n>>    depending on whether the bundles are stored in something like a CDN\n>>    (where bundles can't be too big) or some kind of blob storage (where\n>>    they can have arbitrarily large size).\n>>\n>>  * The bundle servers could be run completely independently from the\n>>    origin Git server. Organizations could run their own bundle servers to\n>>    host data in the same building as their build farms. As long as they\n>>    can configure the bundle location at clone/fetch time, the origin Git\n>>    server doesn't need to be involved.\n>>\n>> While I didn't go so far as to create a clear standard or implement a\n>> prototype in the Git codebase, I created a very simple prototype [1] using\n>> a python script that parses a JSON table of contents and downloads\n>> bundles into the Git repository. Then, I made a 'clone.sh' script that\n>> initializes a repository using the bundle fetcher and fetching the\n>> remainder from the origin Git server. I even computed static bundles for\n>> the git.git repository based on where 'master' has been over several days\n>> in the past month, to give an example of incremental bundles. You can\n>> test the approach all the way to including the fetch to github.com (note\n>> how the GitHub servers were not modified in any way for this).\n>>\n>> [1] https://github.com/derrickstolee/bundles\n>>\n>> There are a lot of limitations to the prototype, but it hopefully\n>> demonstrates the possibility of using something other than the Git protocol\n>> to solve these problems.\n> \n> In your proposal the TOC bundle itself doesn't need to speak the git\n> protocol.\n\nYes. I see this as a HUGE opportunity for flexibility.\n\n> But as as soon as we specify such a thing all of that becomes a part of\n> the git protocol at large in any meaningful way, i.e. git.git's client\n> and any other client that wants to implement the full protocol at large\n> would now need to understand not only pkt-line but also ship a JSON\n> decoder etc.\n\nI use JSON as my example because it is easy to implement in other\nlanguages. It's easy to use in practically any other language than C.\n\nThe reason to use something like JSON is that it already encodes a way\nto include optional, structured information in the list of results. I\nfocused on using that instead of specifics of a pkt-line protocol.\n\nYou have some flexibility in your protocol, but it's unclear how the\noptional data would work without careful testing and actually building\na way to store and communicate the optional data.\n\n> I don't see an inherent problem with us wanting to support some nested\n> encoding format as part of the protocol, but JSON seems like a\n> particularly bad choice. It's specified as UTF-8 only (or rather, \"a\n> Unicode enoding\"), so you can't stick both valid UTF-8 and binary data\n> into it.\n> \n> Our refs on the other hand don't conform to that, so having a JSON\n> format means you can never have something that refers to refnames, which\n> given that we're talking about bundles, whose own header already has\n> that information.\n\nAs I imagine it, we won't want the bundles to store real refnames,\nanyway, since we just need pointers into the commit history to start\nthe incremental fetch of the real refs after the bundles are downloaded.\nThe prototype I made doesn't even store the bundled refs in \"refs/heads\"\nor \"refs/remotes\".\n\n>> Let me know if you are interested in switching your approach to something\n>> more like what I propose here. There are many more questions about what\n>> information could/should be located in the table of contents and how it can\n>> be extended in the future. I'm interested to explore that space with you.\n> \n> As noted above, the TOC part of this seems interesting, and I don't see\n> a reason not to implement that.\n> \n> But as noted in [1] I really don't see why it would be a good idea to\n> implement a side-format that's encoding a limited subset of what you'd\n> find in bundle headers.\n\nYou say \"bundle headers\" a lot as if we are supposed to download and\nexamine the start of the bundle file before completing the download.\nIs that what you mean? Or, do you mean that somehow you are communicating\nthe bundle header in your list of URIs and I just missed it?\n\nIf you do mean that the bundle header _must_ be included in the table\nof contents, then I think that is a blocker for scaling this approach,\nsince the bundle header includes the 'prerequisite' and 'reference'\nrecords, which could be substantial. At scale, info/refs already takes\ntoo much data to want to do often, let alone multiple times in a single\nrequest.\n\nI think the table of contents should provide enough information for the\nclient to decide if they should initiate the download at all, but be\nflexible to multiple strategies for organizing the data.\n\n> Specifically on the meta-information you're proposing:\n> \n> == requires\n> == timestamp\n\nPart of the point of my proposal was to show how things can work in a\ndifferent way, especially with a flexible format like JSON. One possible\nway to organize bundles is in a linear list of files that form snapshots\nof the history at a given timestamp, storing the new objects since the\nprevious bundle. A client could store a local timestamp as a heuristic\nfor whether they need to download a bundle, but if they are missing\nreachable objects, then the 'requires' tag gives them a bundle that\ncould fill in those gaps (they might need to follow a list until closing\nall the gaps, depending on many factors).\n\nSo, that's the high-level \"one could organize bundles like this\" plan.\nIt is _not_ intended as the only way to do it, but I also believe it is\nthe most efficient. It's also why things like \"requires\" and \"timestamp\"\nare intended to be optional metadata.\n\n> == requires\n> In your example you've added a monolithic \"requires\" relationship\n> between bundles, saying \"This assumes that the bundles can be ordered\".\n> \n> But that's not something you can assume for actual bundle files,\n> i.e. the prerequisite relationship is per-reftip, it's not the case that\n> a given bundle requires another bundle, it's the case that tips found in\n> them may or may not depend on other prerequisites.\n\nNo, not all lists of bundles can be ordered. For instance, if you\nuse the \"here is where the 'main' branch was for each month, minus\nthe previous month\" as one list of bundles and then another \"here is\nwhere the 'dev' branch was for each month, minus the 'dev' for the\nprevious month and the 'main' branch for the current month\" then you\nget a partial order instead.\n\nI agree that it is important to allow for full generality of cases like\nthis, but for large repositories, users will probably just want a\nsnapshot of all the ref tips.\n\n> If you're creating bundles that contain only one tip there's a 1=1\n> mapping to what you're proposing with \"requires\", but otherwise there\n> isn't.\n> \n> \n> \"This allows us to reexamine the table of contents and only download the\n> bundles that are newer than that timestamp.\"\n> \n> We're usually going to be fetching these over http(s), why duplicate\n> what you can already get if the server just takes care to create unique\n> filenames (e.g. as a function of the SHA of their contents), and then\n> provides appropriate caching headers to a client so that they'll be\n> cached forever?\n\nThis assumes that a bundle URI will always be available forever, and that\nthe table of contents will never shift with any future reorganization.\nIf the snapshot layout that I specified was always additive, then the URI\nwould be sufficient (although we would need to keep a full list of every\nURI ever downloaded) but also a single timestamp would be sufficient.\n\nThe issue arises if the bundle server reorganizes the data somehow, or\nworse, the back-end of the bundle server is completely replaced with a\ndifferent server that had a different view of the refs at these timestamps.\n\nNow, the 'requires' links provide a way to reconcile missing objects\nafter downloading only the new bundles, without downloading the full list.\n\n> I think that gives you everything you'd like out of the \"timestamp\" and\n> more, the \"more\" being that since it's part of a protocol that's already\n> standard you'd have e.g. intermediate caching proxies understanding this\n> implicitly, in addition to the git client itself.\n> \n> So on a network that's say locally unpacking https connections to a\n> public CDN you could have a local caching proxy for your N local\n> clients, as opposed to a custom \"timestamp\" value, which only each local\n> git client will understand.\n\nI don't understand most of what you are saying here, and perhaps that\nis my lack of understanding of possible network services that you are\nreferencing.\n\nWhat I'm trying to communicate is that a URI is not enough information\nto make a decision about whether or not the Git client should start\ndownloading that data. Providing clues about the bundle content can\nbe helpful.\n\nIn talking with some others about this, they were thinking about\nadvertising the ref tips at the bundle boundaries. This would be a\nlist of \"have\"s and \"have not\"s as OIDs that were used to generate\nthe bundle. However, in my opinion, this only works if you are focused\non snapshots of a single ref instead of a large set of moving refs\n(think thousands of refs). In that environment, timestamps are rather\neffective so it's nice to have the option.\n\nI'm also not saying that you need to implement an organization such\nas the one I'm proposing. I am only strongly recommending that you\nbuild it with enough generality that it is possible.\n\n> == Generally\n> \n> Sorry, I've got to run, so I haven't addressed all the things you\n> brought up, but generally while I think that the TOC idea is a good one.\n> \n> I don't see a reason for why most/all of the other bits shouldn't be\n> leaning into either the bundle header (and for any TOC shortcut, dump it\n> as-is, as noted in [1]), or in the case of \"timestamp\" lean into the\n> properties of the transport protocol.\n> \n> And just generally on overall protocol complexity, wouldn't it be OK if\n> any such TOC is just in pkt-line format?\n\nThe complexity either lives in the code that parses a well-known format\nor the design of a format that is sufficiently general to handle extra\nmetadata that we could use for the future. Pick your poison.\n\n> We could just provide a git plumbing tool to spew that out, and having\n> some static server job call that once and ever more serve up a a\n> plain-file doesn't seem like a big restriction, and would mean that any\n> git client code wouldn't need to deal with another encoding format.\n\nI agree that it would be good to have a Git command that prepares bundles\nfor publishing on a static webserver, complete with table of contents.\nIt would be especially good if this incrementally updated the table with\nnew bundles as time goes on.\n\nThanks,\n-Stolee\n"},{"id":"440280","messageId":"211102.8635of2zet.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"05262630-2e53-3edf-8fa1-fd1a41cc2d28@gmail.com","subject":"Re: [PATCH 0/3] bundle-uri: \"dumb\" static CDN offloading, spec & server implementation","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-11-01T23:18:21Z","receivedAt":"2021-11-02T00:07:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Nov 01 2021, Derrick Stolee wrote:\n\n> On 10/30/2021 3:21 AM, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> On Fri, Oct 29 2021, Derrick Stolee wrote:\n>> \n>>> On 10/25/2021 5:25 PM, Ævar Arnfjörð Bjarmason wrote:\n>>>> This implements a new \"bundle-uri\" protocol v2 extension, which allows\n>>>> servers to advertise *.bundle files which clients can pre-seed their\n>>>> full \"clone\"'s or incremental \"fetch\"'s from.\n>>>>\n>>>> This is both an alternative to, and complimentary to the existing\n>>>> \"packfile-uri\" mechanism, i.e. servers and/or clients can pick one or\n>>>> both, but would generally pick one over the other.\n>>>>\n>>>> This \"bundle-uri\" mechanism has the advantage of being dumber, and\n>>>> offloads more complexity from the server side to the client\n>>>> side.\n>>>\n>>> Generally, I like that using bundles presents an easier way to serve\n>>> static content from an alternative source and then let Git's fetch\n>>> negotiation catch up with the remainder.\n>>>\n>>> However, after inspecting your design and talking to some GitHub\n>>> engineers who know more about CDNs and general internet things than I\n>>> do, I want to propose an alternative design. I think this new design\n>>> is simultaneously more flexible as well as promotes further decoupling\n>>> of the origin Git server and the bundle contents.\n>>>\n>>> Your proposed design extends protocol v2 to let the client request a\n>>> list of bundle URIs from the origin server. However, this still requires\n>>> the origin server to know about this list. [...]\n>> \n>> Interesting, more below...\n>> \n>>> Further, your implementation focuses on the server side without\n>>> integrating with the client.\n>> \n>> Do you mean these 3 patches we're discussing now? Yes, that's the\n>> server-side and protocol specification only, because I figured talking\n>> about just the spec might be helpful.\n>> \n>> But as noted in the CL and previously on-list I have a larger set of\n>> patches to implement the client behavior, an old RFC version of that\n>> here (I've since changed some things...):\n>> https://lore.kernel.org/git/RFC-cover-00.13-0000000000-20210805T150534Z-avarab@gmail.com/\n>> \n>> I mean, you commented on those too, so I'm not sure if that's what you\n>> meant, but just for context...\n>\n> Yeah, I'm not able to keep all of that in my head, and I focused on\n> what you presented in this thread.\n\n[...]\n\n>>> I propose that we flip this around. The \"bundle server\" should know\n>>> which bundles are available at which URIs, and the client should contact\n>>> the bundle server directly for a \"table of contents\" that lists these\n>>> URIs, along with metadata related to each URI. The origin Git server\n>>> then would only need to store the list of bundle servers and the URIs\n>>> to their table of contents. The client could then pick from among those\n>>> bundle servers (probably by ping time, or randomly) to start the bundle\n>>> downloads.\n>> \n>> I hadn't considered the server not advertising the list, but pointing to\n>> another URI that has the list. I was thinking that the server would be\n>> close enough to whatever's generating the list that updating the list\n>> there wouldn't be a meaningful limitation for anyone.\n>> \n>> But you seem to have a use-case for it, I'd be curious to hear why\n>> specifically, but in any case that's easy to support in the client\n>> patches I have.\n>\n> Show me the client patches and then I can determine if I think that\n> is sufficiently flexible.\n\nI'm working on that larger re-roll, hoping to have something to submit\nby the end of the week. I've snipped out much of the below, to the\nextent that it's probably better on my part to reply to it with working\ncode than just continued discussion..\n\n> In general, I want to expand the scope of this feature beyond \"bundles\n> on a CDN\" and towards \"alternative sources of Git object data\" which\n> _could_ be a CDN, but could also be geodistributed HTTP servers that\n> manage their own copy of the Git data (periodically fetching from the\n> origin). These could be self-hosted by organizations with needs for\n> low-latency, high-throughput downloads of object data.\n\n*Nod*. FWIW I share those goal[...]\n\n> One thing neither of us have touched is authentication, so we'll want\n> to find out how to access private information securely in this model.\n> Authentication doesn't matter for CDNs, but would matter for other\n> hosting models.\n\nWe've got authentication already for packfile-uri in the form of bearer\ntokens, and I was planning to do the same (there's recent related\npatches on-list to strip out those URLs during logging for that reason).\n\nI was planning to handle it the same way, i.e. a server implementation\nwould be responsible for spewing out a bundle-uri that's\nauthenticated/non-public.\n\nIn practice I think that probably something that just allows generating\nthe bundle-uri list via a hook would be flexible enough for both this &\nsome of the other use-cases you've had in mind, as long as we pass the\nprotocol-specific auth info down to it in some way.\n\n>> There's a point at which we get the list of URIs from the server, to\n>> support your case the client would just advertise the one TOC URI.\n>> \n>> Then similarly to the \"packfile-uri\" special-case of handling a *.bundle\n>> instead of a PACK that I noted in [1], the downloader would just spot\n>> \"oh this isn't a bundle, but list of URIs, and then fetch those (even\n>> recursively), and eventually get to *.bundle files.\n>\n> This recursive \"follow the contents\" approach seems to be a nice\n> general approach. I would still want to have some kind of specification\n> about what could be seen at these URIs before modifying the protocol v2\n> specification.\n\n*nod*, of course.\n\n> [...]\n> Yes. I see this as a HUGE opportunity for flexibility.\n>\n>> But as as soon as we specify such a thing all of that becomes a part of\n>> the git protocol at large in any meaningful way, i.e. git.git's client\n>> and any other client that wants to implement the full protocol at large\n>> would now need to understand not only pkt-line but also ship a JSON\n>> decoder etc.\n>\n> I use JSON as my example because it is easy to implement in other\n> languages. It's easy to use in practically any other language than C.\n>\n> The reason to use something like JSON is that it already encodes a way\n> to include optional, structured information in the list of results. I\n> focused on using that instead of specifics of a pkt-line protocol.\n>\n> You have some flexibility in your protocol, but it's unclear how the\n> optional data would work without careful testing and actually building\n> a way to store and communicate the optional data.\n\nYes, it's definitely not as out-of-the-box extensible as a nested\nstructure like JSON, but if we leave in space for arbitrary key-values\n(which I'm planning) then we should be able to extend it in the future,\na key/value could even be a serialized data of some sort...\n\n>> I don't see an inherent problem with us wanting to support some nested\n>> encoding format as part of the protocol, but JSON seems like a\n>> particularly bad choice. It's specified as UTF-8 only (or rather, \"a\n>> Unicode enoding\"), so you can't stick both valid UTF-8 and binary data\n>> into it.\n>> \n>> Our refs on the other hand don't conform to that, so having a JSON\n>> format means you can never have something that refers to refnames, which\n>> given that we're talking about bundles, whose own header already has\n>> that information.\n>\n> As I imagine it, we won't want the bundles to store real refnames,\n> anyway, since we just need pointers into the commit history to start\n> the incremental fetch of the real refs after the bundles are downloaded.\n> The prototype I made doesn't even store the bundled refs in \"refs/heads\"\n> or \"refs/remotes\".\n\n*nod*, FWIW I've got off-list patches to make that use-case much easier\nwith \"git bundle\", i.e. teaching it to take <oid>\\t<refname>\\n... on\n--stdin, not just <oid>\\n..., so you can pick arbitrary names.\n\n>>> Let me know if you are interested in switching your approach to something\n>>> more like what I propose here. There are many more questions about what\n>>> information could/should be located in the table of contents and how it can\n>>> be extended in the future. I'm interested to explore that space with you.\n>> \n>> As noted above, the TOC part of this seems interesting, and I don't see\n>> a reason not to implement that.\n>> \n>> But as noted in [1] I really don't see why it would be a good idea to\n>> implement a side-format that's encoding a limited subset of what you'd\n>> find in bundle headers.\n>\n> You say \"bundle headers\" a lot as if we are supposed to download and\n> examine the start of the bundle file before completing the download.\n> Is that what you mean? Or, do you mean that somehow you are communicating\n> the bundle header in your list of URIs and I just missed it?\n\nIt's something I've changed my mind on mid-way through this RFC, but\nyes, as described in\nhttps://lore.kernel.org/git/211027.86a6iuxk3x.gmgdl@evledraar.gmail.com/\nI was originally thinking of a design like\n\n    client: bundle-uri\n    server: https://example.com/bundle2.bdl\n    server: https://example.com/bundle1.bdl optional key=values\n\nBut am now thinking of/working on something like:\n\n    client: command=bundle-uri\n    client: v3\n    client: object-format\n    client: want=headers <pkt-line-flush>\n    server: https://example.com/bundle2.bdl <pkt-line-delim>\n    server: # v3 git bundle\n    server: @object-format=sha1\n    server: e9e5ba39a78c8f5057262d49e261b42a8660d5b9 refs/heads/master <pkt-line-delim>\n    server: https://example.com/bundle1.bdl <pkt-line-delim> [...]\n\nI.e. a client requests bundle-uris with N arguments, those communicate\nwhat sort of bundles it's able to understand, and that it would like the\nserver to give it headers as a shortcut, if it can.\n\nThe server replies with a list of URIs to bundles (or TOC's etc.), and\noptionally as a shortcut to the client includes the headers of those\nbundles.\n\nIt could also simply skip those, but then the client will need to go and\nfetch the headers from the pointed-to resources, or decide it doesn't\nneed to.\n\n> If you do mean that the bundle header _must_ be included in the table\n> of contents, then I think that is a blocker for scaling this approach,\n> since the bundle header includes the 'prerequisite' and 'reference'\n> records, which could be substantial. At scale, info/refs already takes\n> too much data to want to do often, let alone multiple times in a single\n> request.\n\nWe'll see when I submit the updated working patches for this, and in\ncoming up with testcases, but I think that you can attain a sweet spot\nin most repositories by advertising some of your common/recent tips, as\nopposed to a big dump of all the refs.\n\n> [...]\n> Part of the point of my proposal was to show how things can work in a\n> different way, especially with a flexible format like JSON. One possible\n> way to organize bundles is in a linear list of files that form snapshots\n> of the history at a given timestamp, storing the new objects since the\n> previous bundle. A client could store a local timestamp as a heuristic\n> for whether they need to download a bundle, but if they are missing\n> reachable objects, then the 'requires' tag gives them a bundle that\n> could fill in those gaps (they might need to follow a list until closing\n> all the gaps, depending on many factors).\n>\n> So, that's the high-level \"one could organize bundles like this\" plan.\n> It is _not_ intended as the only way to do it, but I also believe it is\n> the most efficient. It's also why things like \"requires\" and \"timestamp\"\n> are intended to be optional metadata.\n\n[more below]\n\n> [...]\n>> If you're creating bundles that contain only one tip there's a 1=1\n>> mapping to what you're proposing with \"requires\", but otherwise there\n>> isn't.\n>> \n>> \n>> \"This allows us to reexamine the table of contents and only download the\n>> bundles that are newer than that timestamp.\"\n>> \n>> We're usually going to be fetching these over http(s), why duplicate\n>> what you can already get if the server just takes care to create unique\n>> filenames (e.g. as a function of the SHA of their contents), and then\n>> provides appropriate caching headers to a client so that they'll be\n>> cached forever?\n>\n> This assumes that a bundle URI will always be available forever, and that\n> the table of contents will never shift with any future reorganization.\n> If the snapshot layout that I specified was always additive, then the URI\n> would be sufficient (although we would need to keep a full list of every\n> URI ever downloaded) but also a single timestamp would be sufficient.\n>\n> The issue arises if the bundle server reorganizes the data somehow, or\n> worse, the back-end of the bundle server is completely replaced with a\n> different server that had a different view of the refs at these timestamps.\n>\n> Now, the 'requires' links provide a way to reconcile missing objects\n> after downloading only the new bundles, without downloading the full list.\n\n[also on this]\n\n>> I think that gives you everything you'd like out of the \"timestamp\" and\n>> more, the \"more\" being that since it's part of a protocol that's already\n>> standard you'd have e.g. intermediate caching proxies understanding this\n>> implicitly, in addition to the git client itself.\n>> \n>> So on a network that's say locally unpacking https connections to a\n>> public CDN you could have a local caching proxy for your N local\n>> clients, as opposed to a custom \"timestamp\" value, which only each local\n>> git client will understand.\n>\n> I don't understand most of what you are saying here, and perhaps that\n> is my lack of understanding of possible network services that you are\n> referencing.\n>\n> What I'm trying to communicate is that a URI is not enough information\n> to make a decision about whether or not the Git client should start\n> downloading that data. Providing clues about the bundle content can\n> be helpful.\n>\n> In talking with some others about this, they were thinking about\n> advertising the ref tips at the bundle boundaries. This would be a\n> list of \"have\"s and \"have not\"s as OIDs that were used to generate\n> the bundle. However, in my opinion, this only works if you are focused\n> on snapshots of a single ref instead of a large set of moving refs\n> (think thousands of refs). In that environment, timestamps are rather\n> effective so it's nice to have the option.\n>\n> I'm also not saying that you need to implement an organization such\n> as the one I'm proposing. I am only strongly recommending that you\n> build it with enough generality that it is possible.\n\nOn the \"This assumes that a bundle URI will always be available forever\"\n& generally on piggy-backing on the HTTP protocol. I mean that you can\njust advertise a:\n\n    https://example.com/big-bundle.bdl\n\nI.e. a URL that never changes, and if you serve it up with appropriate\ncaching headers a client may or may not request it, e.g. a \"clone\"\nprobably will every time, but we MAY also just ignore it based on some\nother client heuristic.\n\nBut then let's say you serve up (opaque SHA1s of the content) with\nappropriate Cache-Control headers[1][2]:\n\n    https://example.com/18719eddecbdf01d6c4166402d62e178482d83d4.bdl\n    https://example.com/9cfaf0ef69c3c3024ff5fe92ba84bf7f6caefa2a.bdl\n\nNow a client only needs to grab those once, and if the server operator\nhas set Cache-Control appropriately we'll only need to request the\nheader for each resource once, e.g. for a use-case of having a \"big\nbundle\" we update monthly, and some weekly updates etc.\n\nOne reason I highly prefer this sort of approach is that it works well\nout of the box with other software.\n\nWant to make your local git clients faster? Just pipe those URLs into\nwget, and if you've got a squid/varnish or other http cache sitting in\nfront of your clients doing that will pre-seed your local cache.\n\nI may also turn out to be wrong, but early tests with\npipelining/streaming the headers are very promising (see e.g. [3] for\nthe API). I.e. for the common case of N bundles on the same CDN you can\npipeline them all & stream them in parallel, and if you don't like what\nyou see in some of them you don't need to continue to download the PACK\npayload itself.\n\n1. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n2. https://bitsup.blogspot.com/2016/05/cache-control-immutable.html\n3. https://curl.se/libcurl/c/http2-download.html\n"},{"id":"451138","messageId":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"cover-0.3-00000000000-20211025T211159Z-avarab@gmail.com","subject":"[RFC PATCH v2 00/13] bundle-uri: a \"dumb CDN\" for git","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:12Z","receivedAt":"2022-03-11T16:24:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Per recent discussion[1] this is my not-quite-feature-complete version\nof the bundle-uri capability.  This was sent to the list in some form\nbeforfe in [2] and [3].\n\nRecently Derrick Stolee has sent an alternate implementation of some\nof the same ideas in [4]. Per [1] we're planning to work together on\ngetting a version of this into git that makes everyone happy, sending\nwhat I've got here is the first step in that.\n\nA high-level summary of the important differences in my approach &\nDerrick's (which I hope I'm summarizing fairly here) is that his\napproach optionally adds a bundle TOC format, that format allows you\nto define topology relationships between bundles to guide a\n(returning) client in what it needs to fetch.\n\nWhereas the idea in this series is to lean entirely on the client\ndownloading bundles & inferring what needs to be done via the\ntip/prereqs listed in the header format of the (existing, not changed\nhere) bundle format.\n\nBoth have pros & cons, I started trying to summarize those, but let's\nleave that for later.\n\nThere's also some high-level \"journey\" differences in the\ntwo. E.g. Derrick implemented the ability to have \"git bundle\" itself\nfetch bundles remotely, I don't have that and instead lean entirely on\nthe protocol and \"fetch\". Those differences really aren't important,\nand we can have our cake & eat it too on that front. I.e. end up with\nsome sensible intersection (or union) of the tooling.\n\nI ran out of time to finish up some of what I had on this topic this\nweek, but figured (especially since I'd promised to get it done this\nweek) to send what I have now for discussion.\n\nThings missing & reader's notes:\n\n * I had some amendmends to the protocol I meant to distill further\n   into the protocol docs at [5]. Basically omitting the ability to\n   transmit key-values and to have it just be a list of URIs with an\n   optional <header> for each one, which is purely a server-to-client\n   aid (i.e. those headers will be what you'll find in the pointed-to\n   bundles).\n\n* This series goes up to \"clone\", but I also have incremental fetch\n  patches. I ran into an (easily solvable bug) in that & thought it\n  was best to omit it for now. It'll be here soon.\n\n  Basically for incremental fetch we'll in parallel get the headers\n  for remote bundles, and then do an early abort of those downloads\n  that we deem that we don't need.\n\n  Clever (but all standard & boring) use of HTTP caching headers\n  between client & servers then allows the client to not request the\n  same thing again and again. I.e. want less server load on your CDN?\n  Just have the bundles be unique URLs and set appropriate caching\n  headers.\n\n* 13/13 demonstrates some of those ideas at a high level.\n\n* A problem with this implementation (and Derrick's, I believe) is\n  that it keeps a server waiting while we twiddle our thumbs and wget\n  (or equivalent) the bundle(s) locally. If you e.g. clone\n  \"chromium.git\" the server will get tired of waiting, drop the\n  connection, and unless the bundle is 100% up-to-date the \"clone\"\n  will fail.\n\n  The solution to this is to get the bundle headers in parallel, and\n  as soon as we've got them present the OIDs in the headers as \"HAVE\"\n  to the server, which'll then send us an incremental PACK (and even\n  possibly a packfile-uri) for the difference between those bundle(s)\n  and what its tips are.\n\n  We can then simply disconnect, download/index-pack everything, and\n  do a connectivity checkat the end.\n\n  This requires some careful error handling, e.g. if the resulting\n  repo isn't valid we'll need to retry, and the current negotiation\n  API isn't very happy to negotiate on the basis of OIDs we don't have\n  in the object store (but the protocol handles it just fine).\n\n  So while I have local patches for (some of) that I opted to leave it\n  out for now. Once we have incremental fetch it's *mostly* an\n  optimization anyway (except in cases of e.g. chromium.git), so for\n  the initial protocol etc. discussion it's probably best to leave it\n  out.\n\n * This currently fails CI on Windows & 32 bit Linux for the most\n   trivial of reasons, need to adjust printf formats to use PRIuMAX\n   etc., but I ran out of time, sorry.\n\n1. https://lore.kernel.org/git/ddebc223-1e13-e758-f9b1-d3f23961e459@github.com/\n2. https://lore.kernel.org/git/patch-3.3-64224ec2cba-20211025T211159Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/RFC-cover-00.13-0000000000-20210805T150534Z-avarab@gmail.com/\n4. https://lore.kernel.org/git/pull.1160.git.1645641063.gitgitgadget@gmail.com/\n5. https://lore.kernel.org/git/211027.86ilxixoxz.gmgdl@evledraar.gmail.com/\n\nÆvar Arnfjörð Bjarmason (13):\n  protocol v2: add server-side \"bundle-uri\" skeleton\n  bundle-uri docs: add design notes\n  bundle-uri client: add \"bundle-uri\" parsing + tests\n  connect.c: refactor sending of agent & object-format\n  bundle-uri client: add minimal NOOP client\n  bundle-uri client: add \"git ls-remote-bundle-uri\"\n  bundle-uri client: add transfer.injectBundleURI support\n  bundle-uri client: add boolean transfer.bundleURI setting\n  fetch-pack: add a deref_without_lazy_fetch_extended()\n  fetch-pack: move --keep=* option filling to a function\n  bundle.h: make \"fd\" version of read_bundle_header() public\n  bundle-uri client: support for bundle-uri with \"clone\"\n  bundle-uri: make the download program configurable\n\n Documentation/config/transfer.txt          |  33 ++\n Documentation/git-ls-remote-bundle-uri.txt |  62 +++\n Documentation/git-ls-remote.txt            |   1 +\n Documentation/technical/bundle-uri.txt     | 119 ++++++\n Documentation/technical/protocol-v2.txt    | 214 +++++++++++\n Makefile                                   |   3 +\n builtin.h                                  |   1 +\n builtin/clone.c                            |   7 +\n builtin/ls-remote-bundle-uri.c             |  90 +++++\n bundle-uri.c                               | 183 +++++++++\n bundle-uri.h                               |  29 ++\n bundle.c                                   |   8 +-\n bundle.h                                   |   2 +\n command-list.txt                           |   1 +\n connect.c                                  |  80 +++-\n fetch-pack.c                               | 306 ++++++++++++++-\n fetch-pack.h                               |   6 +\n git.c                                      |   1 +\n remote.h                                   |   4 +\n serve.c                                    |   6 +\n t/helper/test-bundle-uri.c                 |  83 ++++\n t/helper/test-tool.c                       |   1 +\n t/helper/test-tool.h                       |   1 +\n t/lib-t5730-protocol-v2-bundle-uri.sh      | 424 +++++++++++++++++++++\n t/t5701-git-serve.sh                       | 124 +++++-\n t/t5730-protocol-v2-bundle-uri-file.sh     |  36 ++\n t/t5731-protocol-v2-bundle-uri-git.sh      |  17 +\n t/t5732-protocol-v2-bundle-uri-http.sh     |  17 +\n t/t5750-bundle-uri-parse.sh                | 153 ++++++++\n transport-helper.c                         |  13 +\n transport-internal.h                       |   7 +\n transport.c                                | 120 ++++++\n transport.h                                |  22 ++\n 33 files changed, 2141 insertions(+), 33 deletions(-)\n create mode 100644 Documentation/git-ls-remote-bundle-uri.txt\n create mode 100644 Documentation/technical/bundle-uri.txt\n create mode 100644 builtin/ls-remote-bundle-uri.c\n create mode 100644 bundle-uri.c\n create mode 100644 bundle-uri.h\n create mode 100644 t/helper/test-bundle-uri.c\n create mode 100644 t/lib-t5730-protocol-v2-bundle-uri.sh\n create mode 100755 t/t5730-protocol-v2-bundle-uri-file.sh\n create mode 100755 t/t5731-protocol-v2-bundle-uri-git.sh\n create mode 100755 t/t5732-protocol-v2-bundle-uri-http.sh\n create mode 100755 t/t5750-bundle-uri-parse.sh\n\nRange-diff against v1:\n 2:  3ac0539c053 !  1:  2fc87ce092b protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"\n    @@ Metadata\n     Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    protocol v2: specify static seeding of clone/fetch via \"bundle-uri\"\n    +    protocol v2: add server-side \"bundle-uri\" skeleton\n     \n    -    Add a server-side implementation of a new \"bundle-uri\" command to\n    -    protocol v2. As discussed in the updated \"protocol-v2.txt\" this will\n    -    allow conforming clients to optionally seed their initial clones or\n    -    incremental fetches from URLs containing \"*.bundle\" files created with\n    -    \"git bundle create\".\n    +    Add a skeleton server-side implementation of a new \"bundle-uri\"\n    +    command to protocol v2. This will allow conforming clients to\n    +    optionally seed their initial clones or incremental fetches from URLs\n    +    containing \"*.bundle\" files created with \"git bundle create\".\n     \n         The use-cases are similar to those of the existing \"Packfile URIs\",\n         and the two feature can be combined within a single request, but\n         \"bundle-uri\" has a few advantages over packfile-uris in some some\n         common scenarios, discussed below.\n     \n    -    This change does not give us a working \"bundle-uri\" client. I have\n    -    those patches as a follow-up, but let's first establish what the\n    -    protocol for this should be like first. The client implementation will\n    -    then implement this specification.\n    +    This change does not give us a working \"bundle-uri\" client, subsequent\n    +    commits will do that. Let's first establish what the protocol for this\n    +    should be like first. The client implementation will then implement\n    +    this specification.\n     \n         With this change when the uploadpack.bundleURI config is set to a\n         URI (or URIs, if set >1 times), advertise a \"bundle-uri\" command. Then\n    @@ Commit message\n         .gitmodules check in that context. See [6] for the \"ls-refs unborn\"\n         feature which modified code in similar areas of the request flow.\n     \n    +    Finally, there's currently a concurrent (submitted after the v1 of\n    +    this commit, but before the subsequent client parts of this\n    +    implementation) RFC of a somewhat similar \"bundle-uri\" facility at\n    +    [7].\n    +\n         1. https://lore.kernel.org/git/20111110074330.GA27925@sigill.intra.peff.net/\n         2. https://lore.kernel.org/git/20190514092900.GA11679@sigill.intra.peff.net/\n         3. https://lore.kernel.org/git/YFJWz5yIGng+a16k@coredump.intra.peff.net/\n    @@ Commit message\n            Merged as 6ee353d42f3 (Merge branch 'jt/transfer-fsck-across-packs',\n            2021-03-01)\n         6. 69571dfe219 (Merge branch 'jt/clone-unborn-head', 2021-02-17)\n    +    7. https://lore.kernel.org/git/pull.1160.git.1645641063.gitgitgadget@gmail.com/\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    @@ bundle-uri.h (new)\n     @@\n     +#ifndef BUNDLE_URI_H\n     +#define BUNDLE_URI_H\n    -+\n    -+struct repository;\n    -+struct packet_reader;\n    -+struct packet_writer;\n    ++#include \"repository.h\"\n    ++#include \"pkt-line.h\"\n    ++#include \"strbuf.h\"\n     +\n     +/**\n     + * API used by serve.[ch].\n -:  ----------- >  2:  84c4036a510 bundle-uri docs: add design notes\n 3:  64224ec2cba !  3:  3abfb2290fd bundle-uri client: add \"bundle-uri\" parsing + tests\n    @@ bundle-uri.c: int bundle_uri_command(struct repository *r,\n     \n      ## bundle-uri.h ##\n     @@\n    - struct repository;\n    - struct packet_reader;\n    - struct packet_writer;\n    -+struct string_list;\n    + #include \"repository.h\"\n    + #include \"pkt-line.h\"\n    + #include \"strbuf.h\"\n    ++#include \"string-list.h\"\n      \n      /**\n       * API used by serve.[ch].\n    -@@ bundle-uri.h: struct packet_writer;\n    +@@\n      int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n      int bundle_uri_command(struct repository *r, struct packet_reader *request);\n      \n 1:  7639b9bbac5 !  4:  f64aefa9ece leak tests: mark t5701-git-serve.sh as passing SANITIZE=leak\n    @@ Metadata\n     Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    leak tests: mark t5701-git-serve.sh as passing SANITIZE=leak\n    +    connect.c: refactor sending of agent & object-format\n     \n    -    The \"t5701-git-serve.sh\" test passes when run under a git compiled\n    -    with SANITIZE=leak, let's mark it as such to add it to the\n    -    \"linux-leaks\" CI job.\n    +    Refactor the sending of the \"agent\" and \"object-format\" capabilities\n    +    into a function.\n    +\n    +    This was added in its current form in ab67235bc4 (connect: parse v2\n    +    refs with correct hash algorithm, 2020-05-25). When we connect to a v2\n    +    server we need to know about its object-format, and it needs to know\n    +    about ours. Since most things in connect.c and transport.c piggy-back\n    +    on the eager getting of remote refs via the handshake() those commands\n    +    can make use of the just-sent-over object-format by ls-refs.\n    +\n    +    But I'm about to add a command that may come after ls-refs, and may\n    +    not, but we need the server to know about our user-agent and\n    +    object-format. So let's split this into a function.\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    - ## t/t5701-git-serve.sh ##\n    -@@ t/t5701-git-serve.sh: test_description='test protocol v2 server commands'\n    - GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n    - export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n    + ## connect.c ##\n    +@@ connect.c: void check_stateless_delimiter(int stateless_rpc,\n    + \t\tdie(\"%s\", error);\n    + }\n    + \n    ++static void send_capabilities(int fd_out, struct packet_reader *reader)\n    ++{\n    ++\tconst char *hash_name;\n    ++\n    ++\tif (server_supports_v2(\"agent\", 0))\n    ++\t\tpacket_write_fmt(fd_out, \"agent=%s\", git_user_agent_sanitized());\n    ++\n    ++\tif (server_feature_v2(\"object-format\", &hash_name)) {\n    ++\t\tint hash_algo = hash_algo_by_name(hash_name);\n    ++\t\tif (hash_algo == GIT_HASH_UNKNOWN)\n    ++\t\t\tdie(_(\"unknown object format '%s' specified by server\"), hash_name);\n    ++\t\treader->hash_algo = &hash_algos[hash_algo];\n    ++\t\tpacket_write_fmt(fd_out, \"object-format=%s\", reader->hash_algo->name);\n    ++\t} else {\n    ++\t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1];\n    ++\t}\n    ++}\n    ++\n    + struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n    + \t\t\t     struct ref **list, int for_push,\n    + \t\t\t     struct transport_ls_refs_options *transport_options,\n    +@@ connect.c: struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n    + \t\t\t     int stateless_rpc)\n    + {\n    + \tint i;\n    +-\tconst char *hash_name;\n    + \tstruct strvec *ref_prefixes = transport_options ?\n    + \t\t&transport_options->ref_prefixes : NULL;\n    + \tconst char **unborn_head_target = transport_options ?\n    +@@ connect.c: struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n    + \tif (server_supports_v2(\"ls-refs\", 1))\n    + \t\tpacket_write_fmt(fd_out, \"command=ls-refs\\n\");\n      \n    -+TEST_PASSES_SANITIZE_LEAK=true\n    - . ./test-lib.sh\n    +-\tif (server_supports_v2(\"agent\", 0))\n    +-\t\tpacket_write_fmt(fd_out, \"agent=%s\", git_user_agent_sanitized());\n    +-\n    +-\tif (server_feature_v2(\"object-format\", &hash_name)) {\n    +-\t\tint hash_algo = hash_algo_by_name(hash_name);\n    +-\t\tif (hash_algo == GIT_HASH_UNKNOWN)\n    +-\t\t\tdie(_(\"unknown object format '%s' specified by server\"), hash_name);\n    +-\t\treader->hash_algo = &hash_algos[hash_algo];\n    +-\t\tpacket_write_fmt(fd_out, \"object-format=%s\", reader->hash_algo->name);\n    +-\t} else {\n    +-\t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1];\n    +-\t}\n    ++\t/* Send capabilities */\n    ++\tsend_capabilities(fd_out, reader);\n      \n    - test_expect_success 'test capability advertisement' '\n    + \tif (server_options && server_options->nr &&\n    + \t    server_supports_v2(\"server-option\", 1))\n -:  ----------- >  5:  105ced66409 bundle-uri client: add minimal NOOP client\n -:  ----------- >  6:  617a6b16df8 bundle-uri client: add \"git ls-remote-bundle-uri\"\n -:  ----------- >  7:  b0ce379528e bundle-uri client: add transfer.injectBundleURI support\n -:  ----------- >  8:  44d96a0f5f8 bundle-uri client: add boolean transfer.bundleURI setting\n -:  ----------- >  9:  d9f5b486511 fetch-pack: add a deref_without_lazy_fetch_extended()\n -:  ----------- > 10:  31a22eb3bd4 fetch-pack: move --keep=* option filling to a function\n -:  ----------- > 11:  5ade9419454 bundle.h: make \"fd\" version of read_bundle_header() public\n -:  ----------- > 12:  bb0d681f5f0 bundle-uri client: support for bundle-uri with \"clone\"\n -:  ----------- > 13:  40f37c8b9d5 bundle-uri: make the download program configurable\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451139","messageId":"RFC-patch-v2-02.13-84c4036a510-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 02/13] bundle-uri docs: add design notes","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:14Z","receivedAt":"2022-03-11T16:24:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a design doc for the bundle-uri protocol extension to go along\nwith the packfile-uri extension added in cd8402e0fd8 (Documentation:\nadd Packfile URIs design doc, 2020-06-10).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/technical/bundle-uri.txt  | 119 ++++++++++++++++++++++++\n Documentation/technical/protocol-v2.txt |   5 +\n 2 files changed, 124 insertions(+)\n create mode 100644 Documentation/technical/bundle-uri.txt\n\ndiff --git a/Documentation/technical/bundle-uri.txt b/Documentation/technical/bundle-uri.txt\nnew file mode 100644\nindex 00000000000..5ae9a15eafe\n--- /dev/null\n+++ b/Documentation/technical/bundle-uri.txt\n@@ -0,0 +1,119 @@\n+Bundle URI Design Notes\n+=======================\n+\n+Protocol\n+--------\n+\n+See `bundle-uri` in the link:protocol-v2.html[protocol-v2]\n+documentation for a discussion of the bundle-uri command, and the\n+expectations of clients and servers.\n+\n+This document is a a more general discussion of how the `bundle-uri`\n+command fits in with the rest of the git ecosystem, its design goals\n+and non-goals, comparison to alternatives etc.\n+\n+Comparison with Packfile URIs\n+-----------------------------\n+\n+There is a similar \"Packfile URIs\" facility, see the\n+link:packfile-uri.html[packfile-uri] documentation for details.\n+\n+The Packfile URIs facility requires a much closer cooperation between\n+CDN and server than the bundle URI facility.\n+\n+I.e. the server MUST know what objects exist in the packfile URI it's\n+pointing to, as well as its pack checksum. Failure to do so will not\n+only result in a client error (the packfile hash won't match), but\n+even if it got past that would likely result in a corrupt repository\n+with tips pointing to unreachable objects.\n+\n+By comparison the bundle URIs are meant to be a \"dumb\" solution\n+friendly to e.g. having a weekly cronjob take a snapshot of a git\n+repository, that snapshot being uploaded to a network of FTP mirrors\n+(which may be inconsistent or out of date).\n+\n+The server does not need to know what state the side-channel download\n+is at, because the client will first validate it, and then optionally\n+negotiate with the server using what it discovers there.\n+\n+Using the local `transfer.injectBundleURI` configuration variable (see\n+linkgit:git-config[1]) the `bundle-uri` mechanism doesn't even need\n+the server to support it.\n+\n+Security\n+--------\n+\n+The omission of something equivalent to the packfile <OID> in the\n+Packfile URIs protocol is intentional, as having it would require\n+closer server and CDN cooperation than some server operators are\n+comfortable with.\n+\n+Furthermore, it is not needed for security. The server doesn't need to\n+trust its CDN. If the server were to attempt to send harmful content\n+to the client, the result would not validate against the server's\n+provided ref tips gotten from ls-refs.\n+\n+The lack of a such a hash does leave room open to a malicious CDN\n+operation to be annoying however. E.g. they could inject irrelevant\n+objects into the bundles, which would enlarge the downloaded\n+repository until a \"gc\" would eventually throw them away.\n+\n+In practice the lack of a hash is considered to be a non-issue. Anyone\n+concerned about such security problems between their server and their\n+CDN is going to be pointing to a \"https\" URL under their control. For\n+a client the \"threat\" is the same as without bundle-uri, i.e. a server\n+is free to be annoying today and send you garbage in the PACK that you\n+won't need.\n+\n+Security issues peculiar to bundle-uri\n+--------------------------------------\n+\n+Both packfile-uri and bundle-uri use the `fetch.uriProtocols`\n+configuration variable (see linkgit:git-config[1]) to configure which\n+protocols they support.\n+\n+By default this is set to \"http,https\" for both, but bundle-uri\n+supports adding \"file\" to that list. The server can thus point to\n+\"file://\" URIs it expects the client to have access to.\n+\n+This is primarily intended for use with the `transfer.injectBundleURI`\n+mechanism, but can also be useful e.g. in a centralized environment\n+where a server might point to a \"file:///mnt/bundles/big-repo.bdl\" it\n+knows to be mounted on the local machine (e.g. a racked server),\n+points to it in its \"bundle-uri\" response.\n+\n+The client can then add \"file\" to the `fetch.uriProtocols` list to\n+obey such responses. That does mean that a malicious server can point\n+to any arbitrary file on the local machine. The threat of this is\n+considered minimal, since anyone adding `file` to `fetch.uriProtocols`\n+likely knows what they're doing and controls both ands, and the worst\n+they can do is make a curl(1) pipe garbage into \"index-pack\" (which\n+will likely promptly die on the non-PACK-file).\n+\n+Security comparison with packfile-uri\n+-------------------------------------\n+\n+The initial implementation of packfile-uri needed special adjusting to\n+run \"git fsck\" on incoming .gitmodules files, this was to deal with a\n+general security issue in git, See CVE-2018-17456.\n+\n+The current packfile-uri mechanism requires special handling around\n+\"fsck\" to do such cross-PACK fsck's, this is because it first indexes\n+the \"incremental\" PACK, and then any PACK(s) provided via\n+packfile-uri, before finally doing a full connectivity check.\n+\n+This is effect doing the fsck one might do via \"clone\" and \"fetch\" in\n+reverse, or the equivalent of starting with the incremental \"fetch\",\n+followed by the \"clone\".\n+\n+Since the packfile-uri mechanism can result in the .gitmodules blob\n+referenced by such a \"fetch\" to be in the pack for the \"clone\" the\n+fetch-pack process needs to keep state between the indexing of\n+multiple packs, to remember to fsck the blob (via the \"clone\") later\n+after seeing it in a tree (from the \"fetch).\n+\n+There are no known security issues with the way packfile-uri does\n+this, but since bundle-uri effectively emulates what a which doesn't\n+support either \"bundle-uri\" or \"packfile-uri\" would do on clone/fetch,\n+any future security issues peculiar to the packfile-uri approach are\n+unlikely to be shared by it.\ndiff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\nindex 3ea96add398..3a51492049f 100644\n--- a/Documentation/technical/protocol-v2.txt\n+++ b/Documentation/technical/protocol-v2.txt\n@@ -775,3 +775,8 @@ A client receiving such a a response MAY assume that they can skip\n retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n+\n+bundle-uri SEE ALSO\n+^^^^^^^^^^^^^^^^^^^\n+\n+See the link:bundle-uri.html[Bundle URI Design Notes] for more.\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451140","messageId":"RFC-patch-v2-01.13-2fc87ce092b-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 01/13] protocol v2: add server-side \"bundle-uri\" skeleton","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:13Z","receivedAt":"2022-03-11T16:24:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a skeleton server-side implementation of a new \"bundle-uri\"\ncommand to protocol v2. This will allow conforming clients to\noptionally seed their initial clones or incremental fetches from URLs\ncontaining \"*.bundle\" files created with \"git bundle create\".\n\nThe use-cases are similar to those of the existing \"Packfile URIs\",\nand the two feature can be combined within a single request, but\n\"bundle-uri\" has a few advantages over packfile-uris in some some\ncommon scenarios, discussed below.\n\nThis change does not give us a working \"bundle-uri\" client, subsequent\ncommits will do that. Let's first establish what the protocol for this\nshould be like first. The client implementation will then implement\nthis specification.\n\nWith this change when the uploadpack.bundleURI config is set to a\nURI (or URIs, if set >1 times), advertise a \"bundle-uri\" command. Then\nwhen the client requests \"bundle-uri\" emit those URIs back at them.\n\nDifferences between this and the existing packfile-uri facility:\n\n A. There is no \"real\" support for packfile-uri in git.git. The\n    uploadpack.blobPackfileUri setting allows carving out a list of\n    blobs (actually any OIDs), but as alluded to in bfc2a36ff2a (Doc:\n    clarify contents of packfile sent as URI, 2021-01-20) the only\n    \"real\" implementation is JGit based.\n\n B. The uploadpack.blobPackfileUri is a MUST where this is a\n    \"CAN\". I.e. once a client says they support packfile-uri of given\n    list of protocols the server will send them a PACK response\n    assuming they've downloaded the URI they client was sent, if the\n    client doesn't do that they don't have a valid repository.\n\n    Pointing at a bundle and having the client send us \"have\"\n    lines (or not, maybe they couldn't fetch it, or decided they\n    didn't want to) is more flexible, and can gracefully recover\n    e.g. if the CDN isn't reachable (maybe you do support \"https\", but\n    the CDN provider is down, or blocked your whole country).\n\n C. The client, after executing \"ls-refs\" will disconnect if it has\n    also grabbed the \"bundle-uris\" and knows the server won't send it\n    anything it doesn't already have (or expect to have, if it's\n    downloading the bundles concurrent to an early disconnect).\n\n    This is in (small) contrast to packfile-uri where a client would\n    enter a negotiation dialog, which may or may not result in a\n    packfile-uri and/or an inline PACK.\n\n D. Because of \"C\" clients can, if the bundles are up-to-date, get an\n    up-to-date repository with just \"bundle-uri\" and \"ls-refs\" commands,\n    with no need to enter a dialog with \"git upload-pack\".\n\n    That small dialog is unlikely to matter for performance purposes,\n    this section is just noting differences between \"bundle-uri\" and\n    \"packfile-uri\".\n\nAs noted above the features are compatible, a client that supports\n\"bundle-uri\" and \"packfile-uri\" might download a bundle, and then\nproceed with a \"fetch\" dialog, that dialog might then result in\n\"packfile-uri\" response.\n\nIn practice server operators are unlikely to want to mix the two,\nsince the main benefit of either approach is the ability to offload\nlarge \"clone\" responses to CDNs. A server operator would have little\nreason not to go with one approach or the other.\n\nThere was a suggestion of implementing a similar feature long ago[1]\nby Jeff King. The main difference between it and this approach is that\nwe've since gained protocol v2, so we can add this as an optional path\nin the dialog between client and server. The 2011 implementation\nhooked into the transport mechanism to try to clone from a bundle\ndirectly. See also [2] and [3] for some later mentions of that\napproach.\n\nSee also [4] for the series that implemented\nuploadpack.blobPackfileUri, and [5] for a series on top that did the\n.gitmodules check in that context. See [6] for the \"ls-refs unborn\"\nfeature which modified code in similar areas of the request flow.\n\nFinally, there's currently a concurrent (submitted after the v1 of\nthis commit, but before the subsequent client parts of this\nimplementation) RFC of a somewhat similar \"bundle-uri\" facility at\n[7].\n\n1. https://lore.kernel.org/git/20111110074330.GA27925@sigill.intra.peff.net/\n2. https://lore.kernel.org/git/20190514092900.GA11679@sigill.intra.peff.net/\n3. https://lore.kernel.org/git/YFJWz5yIGng+a16k@coredump.intra.peff.net/\n4. https://lore.kernel.org/git/cover.1591821067.git.jonathantanmy@google.com/\n   Merged as 34e849b05a4 (Merge branch 'jt/cdn-offload', 2020-06-25)\n5. https://lore.kernel.org/git/cover.1614021092.git.jonathantanmy@google.com/\n   Merged as 6ee353d42f3 (Merge branch 'jt/transfer-fsck-across-packs',\n   2021-03-01)\n6. 69571dfe219 (Merge branch 'jt/clone-unborn-head', 2021-02-17)\n7. https://lore.kernel.org/git/pull.1160.git.1645641063.gitgitgadget@gmail.com/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/technical/protocol-v2.txt | 209 ++++++++++++++++++++++++\n Makefile                                |   1 +\n bundle-uri.c                            |  55 +++++++\n bundle-uri.h                            |  13 ++\n serve.c                                 |   6 +\n t/t5701-git-serve.sh                    | 124 +++++++++++++-\n 6 files changed, 407 insertions(+), 1 deletion(-)\n create mode 100644 bundle-uri.c\n create mode 100644 bundle-uri.h\n\ndiff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\nindex 8a877d27e23..3ea96add398 100644\n--- a/Documentation/technical/protocol-v2.txt\n+++ b/Documentation/technical/protocol-v2.txt\n@@ -566,3 +566,212 @@ and associated requested information, each separated by a single space.\n \tattr = \"size\"\n \n \tobj-info = obj-id SP obj-size\n+\n+bundle-uri\n+~~~~~~~~~~\n+\n+If the 'bundle-uri' capability is advertised, the server supports the\n+`bundle-uri' command.\n+\n+The capability is currently advertised with no value (i.e. not\n+\"bundle-uri=somevalue\"), a value may be added in the future for\n+supporting command-wide extensions. Clients MUST ignore any unknown\n+capability values and proceed with the 'bundle-uri` dialog they\n+support.\n+\n+The 'bundle-uri' command is intended to be issued before `fetch` to\n+get URIs to bundle files (see linkgit:git-bundle[1]) to \"seed\" and\n+inform the subsequent `fetch` command.\n+\n+The client CAN issue `bundle-uri` before or after any other valid\n+command. To be useful to clients it's expected that it'll be issued\n+after an `ls-refs` and before `fetch`, but CAN be issued at any time\n+in the dialog.\n+\n+DISCUSSION of bundle-uri\n+^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+The intent of the feature is optimize for server resource consumption\n+in the common case by changing the common case of fetching a very\n+large PACK during linkgit:git-clone[1] into a smaller incremental\n+fetch.\n+\n+It also allows servers to achieve better caching in combination with\n+an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n+\n+By having new clones or fetches be a more predictable and common\n+negotiation against the tips of recently produces *.bundle file(s).\n+Servers might even pre-generate the results of such negotiations for\n+the `uploadpack.packObjectsHook` as new pushes come in.\n+\n+I.e. the server would anticipate that fresh clones will download a\n+known bundle, followed by catching up to the current state of the\n+repository using ref tips found in that bundle (or bundles).\n+\n+PROTOCOL for bundle-uri\n+^^^^^^^^^^^^^^^^^^^^^^^\n+\n+A `bundle-uri` request takes no arguments, and as noted above does not\n+currently advertise a capability value. Both may be added in the\n+future.\n+\n+When the client issues a `command=bundle-uri` the response is a list\n+of URIs the server would like the client to fetch out-of-bounds before\n+proceeding with the `fetch` request in this format:\n+\n+\toutput = bundle-uri-line\n+\t\t bundle-uri-line* flush-pkt\n+\n+\tbundle-uri-line = PKT-LINE(bundle-uri)\n+\t\t\t  *(SP bundle-feature-key *(=bundle-feature-val))\n+\t\t\t  LF\n+\n+\tbundle-uri = A URI such as a https://, ssh:// etc. URI\n+\n+\tbundle-feature-key = Any printable ASCII characters except SP or \"=\"\n+\tbundle-feature-val = Any printable ASCII characters except SP or \"=\"\n+\n+No `bundle-feature-key`=`bundle-feature-value` fields are currently\n+defined. See the discussion of features below.\n+\n+Clients are still expected to fully parse the line according to the\n+above format, lines that do not conform to the format SHOULD be\n+discarded. The user MAY be warned in such a case.\n+\n+bundle-uri CLIENT AND SERVER EXPECTATIONS\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+\".bundle\" FORMAT\n+++++++++++++++++\n+\n+The advertised bundle(s) MUST be in a format that \"git bundle verify\"\n+would accept. I.e. they MUST contain one or more reference tips for\n+use by the client, MUST indicate prerequisites (in any) with standard\n+\"-\" prefixes, and MUST indicate their \"object-format\", if\n+applicable. Create \"*.bundle\" files with \"git bundle create\".\n+\n+bundle-uri CLIENT ERROR RECOVERY\n+++++++++++++++++++++++++++++++++\n+\n+A client MUST above all gracefully degrade on errors, whether that\n+error is because of bad missing/data in the bundle URI(s), because\n+that client is too dumb to e.g. understand and fully parse out bundle\n+headers and their prerequisite relationships, or something else.\n+\n+Server operators should feel confident in turning on \"bundle-uri\" and\n+not worry if e.g. their CDN goes down that clones or fetches will run\n+into hard failures. Even if the server bundle bundle(s) are\n+incomplete, or bad in some way the client should still end up with a\n+functioning repository, just as if it had chosen not to use this\n+protocol extension.\n+\n+All subsequent discussion on client and server interaction MUST keep\n+this in mind.\n+\n+bundle-uri SERVER TO CLIENT\n++++++++++++++++++++++++++++\n+\n+The ordering of the returned bundle uris is not significant. Clients\n+MUST parse their headers to discover their contained OIDS and\n+prerequisites. A client MUST consider the content of the bundle(s)\n+themselves and their header as the ultimate source of truth.\n+\n+A server MAY even return bundle(s) that don't have any direct\n+relationship to the repository being cloned (either through accident,\n+or intentional \"clever\" configuration), and expect a client to sort\n+out what data they'd like from the bundle(s), if any.\n+\n+bundle-uri CLIENT TO SERVER\n++++++++++++++++++++++++++++\n+\n+The client SHOULD provide reference tips found in the bundle header(s)\n+as 'have' lines in any subsequent `fetch` request. A client MAY also\n+ignore the bundle(s) entirely if doing so is deemed worse for some\n+reason, e.g. if the bundles can't be downloaded, it doesn't like the\n+tips it finds etc.\n+\n+WHEN ADVERTISED BUNDLE(S) REQUIRE NO FURTHER NEGOTIATION\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n+\n+If after issuing `bundle-uri` and `ls-refs`, and getting the header(s)\n+of the bundle(s) the client finds that the ref tips it wants can be\n+retrieved entirety from advertised bundle(s), it MAY disconnect. The\n+results of such a 'clone' or 'fetch' should be indistinguishable from\n+the state attained without using bundle-uri.\n+\n+EARLY CLIENT DISCONNECTIONS AND ERROR RECOVERY\n+++++++++++++++++++++++++++++++++++++++++++++++\n+\n+A client MAY perform an early disconnect while still downloading the\n+bundle(s) (having streamed and parsed their headers). In such a case\n+the client MUST gracefully recover from any errors related to\n+finishing the download and validation of the bundle(s).\n+\n+I.e. a client might need to re-connect and issue a 'fetch' command,\n+and possibly fall back to not making use of 'bundle-uri' at all.\n+\n+This \"MAY\" behavior is specified as such (and not a \"SHOULD\") on the\n+assumption that a server advertising bundle uris is more likely than\n+not to be serving up a relatively large repository, and to be pointing\n+to URIs that have a good chance of being in working order. A client\n+MAY e.g. look at the payload size of the bundles as a heuristic to see\n+if an early disconnect is worth it, should falling back on a full\n+\"fetch\" dialog be necessary.\n+\n+WHEN ADVERTISED BUNDLE(S) REQUIRE FURTHER NEGOTIATION\n++++++++++++++++++++++++++++++++++++++++++++++++++++++\n+\n+A client SHOULD commence a negotiation of a PACK from the server via\n+the \"fetch\" command using the OID tips found in advertised bundles,\n+even if's still in the process of downloading those bundle(s).\n+\n+This allows for aggressive early disconnects from any interactive\n+server dialog. The client blindly trusts that the advertised OID tips\n+are relevant, and issues them as 'have' lines, it then requests any\n+tips it would like (usually from the \"ls-refs\" advertisement) via\n+'want' lines. The server will then compute a (hopefully small) PACK\n+with the expected difference between the tips from the bundle(s) and\n+the data requested.\n+\n+The only connection the client then needs to keep active is to the\n+concurrently downloading static bundle(s), when those and the\n+incremental PACK are retrieved they should be inflated and\n+validated. Any errors at this point should be gracefully recovered\n+from, see above.\n+\n+bundle-uri PROTOCOL FEATURES\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+As noted above no `bundle-feature-key`=`bundle-feature-value` fields\n+are currently defined.\n+\n+They are intended for future per-URI metadata which older clients MUST\n+ignore and gracefully degrade on. Any fields they do recognize they\n+CAN also ignore.\n+\n+Any backwards-incompatible addition of pre-URI key-value will be\n+guarded by a new value or values in 'bundle-uri' capability\n+advertisement itself, and/or by new future `bundle-uri` request\n+arguments.\n+\n+While no per-URI key-value are currently supported currently they're\n+intended to support future features such as:\n+\n+ * Add a \"hash=<val>\" or \"size=<bytes>\" advertise the expected hash or\n+   size of the bundle file.\n+\n+ * Advertise that one or more bundle files are the same (to e.g. have\n+   clients round-robin or otherwise choose one of N possible files).\n+\n+ * A \"oid=<OID>\" shortcut and \"prerequisite=<OID>\" shortcut. For\n+   expressing the common case of a bundle with one tip and no\n+   prerequisites, or one tip and one prerequisite.\n++\n+This would allow for optimizing the common case of servers who'd like\n+to provide one \"big bundle\" containing only their \"main\" branch,\n+and/or incremental updates thereof.\n++\n+A client receiving such a a response MAY assume that they can skip\n+retrieving the header from a bundle at the indicated URI, and thus\n+save themselves and the server(s) the request(s) needed to inspect the\n+headers of that bundle or bundles.\ndiff --git a/Makefile b/Makefile\nindex 6f0b4b775fe..5a3d35109a1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -855,6 +855,7 @@ LIB_OBJS += blob.o\n LIB_OBJS += bloom.o\n LIB_OBJS += branch.o\n LIB_OBJS += bulk-checkin.o\n+LIB_OBJS += bundle-uri.o\n LIB_OBJS += bundle.o\n LIB_OBJS += cache-tree.o\n LIB_OBJS += cbtree.o\ndiff --git a/bundle-uri.c b/bundle-uri.c\nnew file mode 100644\nindex 00000000000..ff054ddc690\n--- /dev/null\n+++ b/bundle-uri.c\n@@ -0,0 +1,55 @@\n+#include \"cache.h\"\n+#include \"bundle-uri.h\"\n+#include \"pkt-line.h\"\n+#include \"config.h\"\n+\n+static void send_bundle_uris(struct packet_writer *writer,\n+\t\t\t     struct string_list *uris)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, uris)\n+\t\tpacket_writer_write(writer, \"%s\", item->string);\n+}\n+\n+static int advertise_bundle_uri = -1;\n+static struct string_list bundle_uris = STRING_LIST_INIT_DUP;\n+static int bundle_uri_config(const char *var, const char *value, void *data)\n+{\n+\tif (!strcmp(var, \"uploadpack.bundleuri\")) {\n+\t\tadvertise_bundle_uri = 1;\n+\t\tstring_list_append(&bundle_uris, value);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int bundle_uri_advertise(struct repository *r, struct strbuf *value)\n+{\n+\tif (advertise_bundle_uri != -1)\n+\t\tgoto cached;\n+\n+\tgit_config(bundle_uri_config, NULL);\n+\tadvertise_bundle_uri = !!bundle_uris.nr;\n+\n+cached:\n+\treturn advertise_bundle_uri;\n+}\n+\n+int bundle_uri_command(struct repository *r,\n+\t\t       struct packet_reader *request)\n+{\n+\tstruct packet_writer writer;\n+\tpacket_writer_init(&writer, 1);\n+\n+\twhile (packet_reader_read(request) == PACKET_READ_NORMAL)\n+\t\tdie(_(\"bundle-uri: unexpected argument: '%s'\"), request->line);\n+\tif (request->status != PACKET_READ_FLUSH)\n+\t\tdie(_(\"bundle-uri: expected flush after arguments\"));\n+\n+\tsend_bundle_uris(&writer, &bundle_uris);\n+\n+\tpacket_writer_flush(&writer);\n+\n+\treturn 0;\n+}\ndiff --git a/bundle-uri.h b/bundle-uri.h\nnew file mode 100644\nindex 00000000000..5a7e556a0ba\n--- /dev/null\n+++ b/bundle-uri.h\n@@ -0,0 +1,13 @@\n+#ifndef BUNDLE_URI_H\n+#define BUNDLE_URI_H\n+#include \"repository.h\"\n+#include \"pkt-line.h\"\n+#include \"strbuf.h\"\n+\n+/**\n+ * API used by serve.[ch].\n+ */\n+int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n+int bundle_uri_command(struct repository *r, struct packet_reader *request);\n+\n+#endif /* BUNDLE_URI_H */\ndiff --git a/serve.c b/serve.c\nindex b3fe9b5126a..f3e0203d2c6 100644\n--- a/serve.c\n+++ b/serve.c\n@@ -8,6 +8,7 @@\n #include \"protocol-caps.h\"\n #include \"serve.h\"\n #include \"upload-pack.h\"\n+#include \"bundle-uri.h\"\n \n static int advertise_sid = -1;\n static int client_hash_algo = GIT_HASH_SHA1;\n@@ -136,6 +137,11 @@ static struct protocol_capability capabilities[] = {\n \t\t.advertise = always_advertise,\n \t\t.command = cap_object_info,\n \t},\n+\t{\n+\t\t.name = \"bundle-uri\",\n+\t\t.advertise = bundle_uri_advertise,\n+\t\t.command = bundle_uri_command,\n+\t},\n };\n \n void protocol_v2_advertise_capabilities(void)\ndiff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh\nindex 1896f671cb3..9d053f77a93 100755\n--- a/t/t5701-git-serve.sh\n+++ b/t/t5701-git-serve.sh\n@@ -13,7 +13,7 @@ test_expect_success 'test capability advertisement' '\n \twrong_algo sha1:sha256\n \twrong_algo sha256:sha1\n \tEOF\n-\tcat >expect <<-EOF &&\n+\tcat >expect.base <<-EOF &&\n \tversion 2\n \tagent=git/$(git version | cut -d\" \" -f3)\n \tls-refs=unborn\n@@ -21,8 +21,11 @@ test_expect_success 'test capability advertisement' '\n \tserver-option\n \tobject-format=$(test_oid algo)\n \tobject-info\n+\tEOF\n+\tcat >expect.trailer <<-EOF &&\n \t0000\n \tEOF\n+\tcat expect.base expect.trailer >expect &&\n \n \tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n \t\t--advertise-capabilities >out &&\n@@ -342,4 +345,123 @@ test_expect_success 'basics of object-info' '\n \ttest_cmp expect actual\n '\n \n+# Test the basics of bundle-uri\n+#\n+test_expect_success 'test capability advertisement with uploadpack.bundleURI' '\n+\ttest_config uploadpack.bundleURI FAKE &&\n+\n+\tcat >expect.extra <<-EOF &&\n+\tbundle-uri\n+\tEOF\n+\tcat expect.base \\\n+\t    expect.extra \\\n+\t    expect.trailer >expect &&\n+\n+\tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n+\t\t--advertise-capabilities >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: dies if not enabled' '\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\t0000\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tfatal: invalid command '\"'\"'bundle-uri'\"'\"'\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tERR serve: invalid command '\"'\"'bundle-uri'\"'\"'\n+\tEOF\n+\n+\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out\n+'\n+\n+\n+test_expect_success 'basics of bundle-uri: enabled with single URI' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: enabled with single URI' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: enabled with two URIs' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/recent.bdl --add &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\thttps://cdn.example.com/recent.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: unknown future feature(s)' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/fake.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0001\n+\tsome-feature\n+\twe-do-not\n+\tknow=about\n+\t0000\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tfatal: bundle-uri: unexpected argument: '\"'\"'some-feature'\"'\"'\n+\tEOF\n+\n+\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out\n+'\n+\n test_done\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451141","messageId":"RFC-patch-v2-04.13-f64aefa9ece-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 04/13] connect.c: refactor sending of agent & object-format","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:16Z","receivedAt":"2022-03-11T16:24:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the sending of the \"agent\" and \"object-format\" capabilities\ninto a function.\n\nThis was added in its current form in ab67235bc4 (connect: parse v2\nrefs with correct hash algorithm, 2020-05-25). When we connect to a v2\nserver we need to know about its object-format, and it needs to know\nabout ours. Since most things in connect.c and transport.c piggy-back\non the eager getting of remote refs via the handshake() those commands\ncan make use of the just-sent-over object-format by ls-refs.\n\nBut I'm about to add a command that may come after ls-refs, and may\nnot, but we need the server to know about our user-agent and\nobject-format. So let's split this into a function.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n connect.c | 33 ++++++++++++++++++++-------------\n 1 file changed, 20 insertions(+), 13 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex afc79a6236e..e6d0b1d34bd 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -473,6 +473,24 @@ void check_stateless_delimiter(int stateless_rpc,\n \t\tdie(\"%s\", error);\n }\n \n+static void send_capabilities(int fd_out, struct packet_reader *reader)\n+{\n+\tconst char *hash_name;\n+\n+\tif (server_supports_v2(\"agent\", 0))\n+\t\tpacket_write_fmt(fd_out, \"agent=%s\", git_user_agent_sanitized());\n+\n+\tif (server_feature_v2(\"object-format\", &hash_name)) {\n+\t\tint hash_algo = hash_algo_by_name(hash_name);\n+\t\tif (hash_algo == GIT_HASH_UNKNOWN)\n+\t\t\tdie(_(\"unknown object format '%s' specified by server\"), hash_name);\n+\t\treader->hash_algo = &hash_algos[hash_algo];\n+\t\tpacket_write_fmt(fd_out, \"object-format=%s\", reader->hash_algo->name);\n+\t} else {\n+\t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1];\n+\t}\n+}\n+\n struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     struct ref **list, int for_push,\n \t\t\t     struct transport_ls_refs_options *transport_options,\n@@ -480,7 +498,6 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     int stateless_rpc)\n {\n \tint i;\n-\tconst char *hash_name;\n \tstruct strvec *ref_prefixes = transport_options ?\n \t\t&transport_options->ref_prefixes : NULL;\n \tconst char **unborn_head_target = transport_options ?\n@@ -490,18 +507,8 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \tif (server_supports_v2(\"ls-refs\", 1))\n \t\tpacket_write_fmt(fd_out, \"command=ls-refs\\n\");\n \n-\tif (server_supports_v2(\"agent\", 0))\n-\t\tpacket_write_fmt(fd_out, \"agent=%s\", git_user_agent_sanitized());\n-\n-\tif (server_feature_v2(\"object-format\", &hash_name)) {\n-\t\tint hash_algo = hash_algo_by_name(hash_name);\n-\t\tif (hash_algo == GIT_HASH_UNKNOWN)\n-\t\t\tdie(_(\"unknown object format '%s' specified by server\"), hash_name);\n-\t\treader->hash_algo = &hash_algos[hash_algo];\n-\t\tpacket_write_fmt(fd_out, \"object-format=%s\", reader->hash_algo->name);\n-\t} else {\n-\t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1];\n-\t}\n+\t/* Send capabilities */\n+\tsend_capabilities(fd_out, reader);\n \n \tif (server_options && server_options->nr &&\n \t    server_supports_v2(\"server-option\", 1))\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451142","messageId":"RFC-patch-v2-06.13-617a6b16df8-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 06/13] bundle-uri client: add \"git ls-remote-bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:18Z","receivedAt":"2022-03-11T16:24:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a git-ls-remote-bundle-uri command, this is a thin wrapper for\nissuing protocol v2 \"bundle-uri\" commands to a server, and to the\nparsing routines in bundle-uri.c.\n\nSince in the \"git clone\" case we'll have already done the handshake(),\nbut not here, introduce a \"got_advertisement\" state along with\n\"got_remote_heads\". It seems to me that the \"got_remote_heads\" is\nbadly named in the first place, and the whole logic of eagerly getting\nls-refs on handshake() or not could be refactored somewhat, but let's\nnot do that now, and instead just add another self-documenting state\nvariable.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-ls-remote-bundle-uri.txt |  62 ++++++++++\n Documentation/git-ls-remote.txt            |   1 +\n Makefile                                   |   1 +\n builtin.h                                  |   1 +\n builtin/clone.c                            |   2 +-\n builtin/ls-remote-bundle-uri.c             |  90 ++++++++++++++\n command-list.txt                           |   1 +\n git.c                                      |   1 +\n t/lib-t5730-protocol-v2-bundle-uri.sh      | 132 +++++++++++++++++++++\n transport.c                                |  43 +++++--\n transport.h                                |   6 +-\n 11 files changed, 329 insertions(+), 11 deletions(-)\n create mode 100644 Documentation/git-ls-remote-bundle-uri.txt\n create mode 100644 builtin/ls-remote-bundle-uri.c\n\ndiff --git a/Documentation/git-ls-remote-bundle-uri.txt b/Documentation/git-ls-remote-bundle-uri.txt\nnew file mode 100644\nindex 00000000000..793d7677f2f\n--- /dev/null\n+++ b/Documentation/git-ls-remote-bundle-uri.txt\n@@ -0,0 +1,62 @@\n+git-ls-remote-bundle-uri(1)\n+===========================\n+\n+NAME\n+----\n+git-ls-remote-bundle-uri - List 'bundle-uri' in a remote repository\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git ls-remote-bundle-uri' [-q |--quiet] [--uri] [--upload-pack=<exec>]\n+\t\t\t [[-o | --server-option=]<option>] <repository>\n+\n+\n+DESCRIPTION\n+-----------\n+\n+Displays the `bundle-uri`s advertised by a remote repository. See\n+`bundle-uri` in link:technical/protocol-v2.html[the Git Wire Protocol,\n+Version 2] documentation for what the output format looks like.\n+\n+OPTIONS\n+-------\n+\n+-q::\n+--quiet::\n+\tDo not print remote URL to stderr in cases where the remote\n+\tname is inferred from config.\n++\n+When the remote name is not inferred (e.g. `git ls-remote-bundle-uri\n+origin`, or `git ls-remote-bundle-uri https://[...]`) the remote URL\n+is not printed in any case.\n+\n+--uri::\n+\tPrint only the URIs, and not any of their optional attributes.\n+\n+--upload-pack=<exec>::\n+\tSpecify the full path of 'git-upload-pack' on the remote\n+\thost. This allows listing references from repositories accessed via\n+\tSSH and where the SSH daemon does not use the PATH configured by the\n+\tuser.\n+\n+-o <option>::\n+--server-option=<option>::\n+\tTransmit the given string to the server when communicating using\n+\tprotocol version 2.  The given string must not contain a NUL or LF\n+\tcharacter.\n+\tWhen multiple `--server-option=<option>` are given, they are all\n+\tsent to the other side in the order listed on the command line.\n+\n+<repository>::\n+\tThe \"remote\" repository to query.  This parameter can be\n+\teither a URL or the name of a remote (see the GIT URLS and\n+\tREMOTES sections of linkgit:git-fetch[1]).\n+\n+SEE ALSO\n+--------\n+linkgit:git-ls-remote[1].\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/git-ls-remote.txt b/Documentation/git-ls-remote.txt\nindex 492e573856f..86c07eff832 100644\n--- a/Documentation/git-ls-remote.txt\n+++ b/Documentation/git-ls-remote.txt\n@@ -114,6 +114,7 @@ c5db5456ae3b0873fc659c19fafdde22313cc441\trefs/tags/v0.99.2\n \n SEE ALSO\n --------\n+linkgit:git-ls-remote-bundle-uri[1].\n linkgit:git-check-ref-format[1].\n \n GIT\ndiff --git a/Makefile b/Makefile\nindex 4fec8e5af09..bdebf16a23f 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1125,6 +1125,7 @@ BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\n BUILTIN_OBJS += builtin/log.o\n BUILTIN_OBJS += builtin/ls-files.o\n+BUILTIN_OBJS += builtin/ls-remote-bundle-uri.o\n BUILTIN_OBJS += builtin/ls-remote.o\n BUILTIN_OBJS += builtin/ls-tree.o\n BUILTIN_OBJS += builtin/mailinfo.o\ndiff --git a/builtin.h b/builtin.h\nindex 83379f3832c..d093a1e7ffe 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_log(int argc, const char **argv, const char *prefix);\n int cmd_log_reflog(int argc, const char **argv, const char *prefix);\n int cmd_ls_files(int argc, const char **argv, const char *prefix);\n int cmd_ls_tree(int argc, const char **argv, const char *prefix);\n+int cmd_ls_remote_bundle_uri(int argc, const char **argv, const char *prefix);\n int cmd_ls_remote(int argc, const char **argv, const char *prefix);\n int cmd_mailinfo(int argc, const char **argv, const char *prefix);\n int cmd_mailsplit(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex b2c1b4142ee..007a59c6118 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1225,7 +1225,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \t * Populate transport->got_remote_bundle_uri and\n \t * transport->bundle_uri. We might get nothing.\n \t */\n-\ttransport_get_remote_bundle_uri(transport);\n+\ttransport_get_remote_bundle_uri(transport, 1);\n \n \tif (mapped_refs) {\n \t\tint hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport));\ndiff --git a/builtin/ls-remote-bundle-uri.c b/builtin/ls-remote-bundle-uri.c\nnew file mode 100644\nindex 00000000000..dadb21043c0\n--- /dev/null\n+++ b/builtin/ls-remote-bundle-uri.c\n@@ -0,0 +1,90 @@\n+#include \"builtin.h\"\n+#include \"cache.h\"\n+#include \"transport.h\"\n+#include \"ref-filter.h\"\n+#include \"remote.h\"\n+#include \"refs.h\"\n+\n+static const char * const ls_remote_bundle_uri_usage[] = {\n+\tN_(\"git ls-remote-bundle-uri <repository>\"),\n+\tNULL\n+};\n+\n+int cmd_ls_remote_bundle_uri(int argc, const char **argv, const char *prefix)\n+{\n+\tint quiet = 0;\n+\tint uri = 0;\n+\tconst char *uploadpack = NULL;\n+\tstruct string_list server_options = STRING_LIST_INIT_DUP;\n+\tstruct option options[] = {\n+\t\tOPT__QUIET(&quiet, N_(\"do not print remote URL\")),\n+\t\tOPT_BOOL(0, \"uri\", &uri, N_(\"limit to showing uri field\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &uploadpack, N_(\"exec\"),\n+\t\t\t   N_(\"path of git-upload-pack on the remote host\")),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options,\n+\t\t\t\tN_(\"server-specific\"),\n+\t\t\t\tN_(\"option to transmit\")),\n+\t\tOPT_END()\n+\t};\n+\tconst char *dest = NULL;\n+\tstruct remote *remote;\n+\tstruct transport *transport;\n+\tint status = 0;\n+\tstruct string_list_item *item;\n+\n+\targc = parse_options(argc, argv, prefix, options, ls_remote_bundle_uri_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tdest = argv[0];\n+\n+\tpacket_trace_identity(\"ls-remote-bundle-uri\");\n+\n+\tremote = remote_get(dest);\n+\tif (!remote) {\n+\t\tif (dest)\n+\t\t\tdie(_(\"bad repository '%s'\"), dest);\n+\t\tdie(_(\"no remote configured to get bundle URIs from\"));\n+\t}\n+\tif (!remote->url_nr)\n+\t\tdie(_(\"remote '%s' has no configured URL\"), dest);\n+\n+\ttransport = transport_get(remote, NULL);\n+\tif (uploadpack)\n+\t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK, uploadpack);\n+\tif (server_options.nr)\n+\t\ttransport->server_options = &server_options;\n+\n+\tif (!dest && !quiet)\n+\t\tfprintf(stderr, \"From %s\\n\", *remote->url);\n+\n+\tif (transport_get_remote_bundle_uri(transport, 0) < 0) {\n+\t\terror(_(\"could not get the bundle-uri list\"));\n+\t\tstatus = 1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tfor_each_string_list_item(item, &transport->bundle_uri) {\n+\t\tstruct string_list_item *kv_item;\n+\t\tstruct string_list *kv = item->util;\n+\n+\t\tfprintf(stdout, \"%s\", item->string);\n+\t\tif (uri || !kv) {\n+\t\t\tfprintf(stdout, \"\\n\");\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfor_each_string_list_item(kv_item, kv) {\n+\t\t\tconst char *k = kv_item->string;\n+\t\t\tconst char *v = kv_item->util;\n+\n+\t\t\tif (v)\n+\t\t\t\tfprintf(stdout, \" %s=%s\", k, v);\n+\t\t\telse\n+\t\t\t\tfprintf(stdout, \" %s\", k);\n+\t\t}\n+\t\tfprintf(stdout, \"\\n\");\n+\t}\n+\n+cleanup:\n+\tif (transport_disconnect(transport))\n+\t\treturn 1;\n+\treturn status;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex 9bd6f3c48f4..a50eebd4aa2 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-interpret-trailers                  purehelpers\n git-log                                 mainporcelain           info\n git-ls-files                            plumbinginterrogators\n git-ls-remote                           plumbinginterrogators\n+git-ls-remote-bundle-uri                plumbinginterrogators\n git-ls-tree                             plumbinginterrogators\n git-mailinfo                            purehelpers\n git-mailsplit                           purehelpers\ndiff --git a/git.c b/git.c\nindex a25940d72e8..22554f2e5c5 100644\n--- a/git.c\n+++ b/git.c\n@@ -550,6 +550,7 @@ static struct cmd_struct commands[] = {\n \t{ \"log\", cmd_log, RUN_SETUP },\n \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n+\t{ \"ls-remote-bundle-uri\", cmd_ls_remote_bundle_uri, RUN_SETUP_GENTLY },\n \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n \t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nindex 724acecda66..f0c41d60654 100644\n--- a/t/lib-t5730-protocol-v2-bundle-uri.sh\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -139,3 +139,135 @@ test_expect_success !T5730_HTTP \"bad client with $T5730_PROTOCOL:// using protoc\n \tgrep \"clone> test-bad-client$\" log >sent-bad-request &&\n \ttest_file_not_empty sent-bad-request\n '\n+\n+test_expect_success \"ls-remote-bundle-uri with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\t# All data about bundle URIs\n+\tcat >expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Only the URIs\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri --uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual2 &&\n+\ttest_cmp actual actual2\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tATTR=\"foo bar=baz\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED $ATTR\" &&\n+\n+\t# All data about bundle URIs\n+\tcat >expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED $ATTR\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri with $T5730_PROTOCOL:// using protocol v2: --uri\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tATTR=\"foo bar=baz\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED $ATTR\" &&\n+\n+\t# All data about bundle URIs\n+\tcat >expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t--uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri --[no-]quiet with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tCloning into '\"'\"'child'\"'\"'...\n+\tEOF\n+\n+\ttest_when_finished \"rm -rf child\" &&\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t clone \"$T5730_URI\" child \\\n+\t\t >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\t# Without --[no-]quiet\n+\tcat >out.expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED\n+\tEOF\n+\tcat >err.expect <<-EOF &&\n+\tFrom $T5730_URI\n+\tEOF\n+\tgit \\\n+\t\t-C child \\\n+\t\t -c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t>out.actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp out.expect out.actual &&\n+\n+\t# --no-quiet is the default\n+\tgit \\\n+\t\t-C child \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t--no-quiet \\\n+\t\t>out.actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp out.expect out.actual &&\n+\n+\t# --quiet quiets the \"From\" line\n+\tgit \\\n+\t\t-C child \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t--quiet \\\n+\t\t>out.actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp out.expect out.actual &&\n+\n+\t# --quiet is implicit if the remote is not implicit\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>out.actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp out.expect out.actual\n+'\ndiff --git a/transport.c b/transport.c\nindex 7c9a371ed7f..16147a170f8 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -191,6 +191,7 @@ struct git_transport_data {\n \tstruct git_transport_options options;\n \tstruct child_process *conn;\n \tint fd[2];\n+\tunsigned got_advertisement : 1;\n \tunsigned got_remote_heads : 1;\n \tenum protocol_version version;\n \tstruct oid_array extra_have;\n@@ -336,6 +337,7 @@ static struct ref *handshake(struct transport *transport, int for_push,\n \t\tBUG(\"unknown protocol version\");\n \t}\n \tdata->got_remote_heads = 1;\n+\tdata->got_advertisement = 1;\n \ttransport->hash_algo = reader.hash_algo;\n \n \tif (reader.line_peeked)\n@@ -357,6 +359,33 @@ static int get_bundle_uri(struct transport *transport)\n \tint stateless_rpc = transport->stateless_rpc;\n \tstring_list_init_dup(&transport->bundle_uri);\n \n+\tif (!data->got_advertisement) {\n+\t\tstruct ref *refs;\n+\t\tstruct git_transport_data *data = transport->data;\n+\t\tenum protocol_version version;\n+\n+\t\trefs = handshake(transport, 0, NULL, 0);\n+\t\tversion = data->version;\n+\n+\t\tswitch (version) {\n+\t\tcase protocol_v2:\n+\t\t\tassert(!refs);\n+\t\t\tbreak;\n+\t\tcase protocol_v0:\n+\t\tcase protocol_v1:\n+\t\tcase protocol_unknown_version:\n+\t\t\tassert(refs);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * \"Support\" protocol v0 and v2 without bundle-uri support by\n+\t * silently degrading to a NOOP.\n+\t */\n+\tif (!server_supports_v2(\"bundle-uri\", 0))\n+\t\treturn 0;\n+\n \tpacket_reader_init(&reader, data->fd[0], NULL, 0,\n \t\t\t   PACKET_READ_CHOMP_NEWLINE |\n \t\t\t   PACKET_READ_GENTLE_ON_EOF);\n@@ -1481,7 +1510,7 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n-int transport_get_remote_bundle_uri(struct transport *transport)\n+int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n {\n \tconst struct transport_vtable *vtable = transport->vtable;\n \n@@ -1489,20 +1518,16 @@ int transport_get_remote_bundle_uri(struct transport *transport)\n \tif (transport->got_remote_bundle_uri++)\n \t\treturn 0;\n \n-\t/*\n-\t * \"Support\" protocol v0 and v2 without bundle-uri support by\n-\t * silently degrading to a NOOP.\n-\t */\n-\tif (!server_supports_v2(\"bundle-uri\", 0))\n-\t\treturn 0;\n-\n \t/*\n \t * This is intentionally below the transport.injectBundleURI,\n \t * we want to be able to inject into protocol v0, or into the\n \t * dialog of a server who doesn't support this.\n \t */\n-\tif (!vtable->get_bundle_uri)\n+\tif (!vtable->get_bundle_uri) {\n+\t\tif (quiet)\n+\t\t\treturn -1;\n \t\treturn error(_(\"bundle-uri operation not supported by protocol\"));\n+\t}\n \n \tif (vtable->get_bundle_uri(transport) < 0)\n \t\treturn error(_(\"could not retrieve server-advertised bundle-uri list\"));\ndiff --git a/transport.h b/transport.h\nindex 7740306b850..4253eafd954 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -291,8 +291,12 @@ const struct ref *transport_get_remote_refs(struct transport *transport,\n /**\n  * Retrieve bundle URI(s) from a remote. Populates \"struct\n  * transport\"'s \"bundle_uri\" and \"got_remote_bundle_uri\".\n+ *\n+ * With `quiet=1` it will not complain if the serve doesn't support\n+ * the protocol, but only if we discover the server uses it, and\n+ * encounter issues then.\n  */\n-int transport_get_remote_bundle_uri(struct transport *transport);\n+int transport_get_remote_bundle_uri(struct transport *transport, int quiet);\n \n /*\n  * Fetch the hash algorithm used by a remote.\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451143","messageId":"RFC-patch-v2-05.13-105ced66409-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 05/13] bundle-uri client: add minimal NOOP client","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:17Z","receivedAt":"2022-03-11T16:24:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Set up all the needed client parts of the \"bundle-uri\" protocol\nextension, without actually doing anything with the bundle URIs.\n\nI.e. if the server says it supports \"bundle-uri\" we'll issue a\ncommand=bundle-uri after command=ls-refs when we're cloning. We'll\nparse the returned output using the code already tested for in\nt5750-bundle-uri-parse.sh.\n\nWhat we aren't doing is actually acting on that data, i.e. downloading\nthe bundle(s) before we get to doing the command=fetch, and adjusting\nour negotiation dialog appropriately. I'll do that in subsequent\ncommits.\n\nThere's a question of what level of encapsulation we should use here,\nI've opted to use connect.h in clone.c, but we could also e.g. make\ntransport_get_remote_refs() invoke this, i.e. make it implicitly get\nthe bundle-uri list for later steps.\n\nThis approach means that we don't \"support\" this in \"git fetch\" for\nnow. I'm starting with the case of initial clones, although as noted\nin preceding commits to the protocol documentation nothing about this\napproach precludes getting bundles on incremental fetches.\n\nFor the t5732-protocol-v2-bundle-uri-http.sh it's not easy to set\nenvironment variables for git-upload-pack (it's started by Apache), so\nlet's skip the test under T5730_HTTP, and add unused T5730_{FILE,GIT}\nprerequisites for consistency and future use.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/clone.c                        |   7 ++\n bundle-uri.c                           |   4 +\n connect.c                              |  47 +++++++++\n remote.h                               |   4 +\n t/lib-t5730-protocol-v2-bundle-uri.sh  | 141 +++++++++++++++++++++++++\n t/t5730-protocol-v2-bundle-uri-file.sh |  36 +++++++\n t/t5731-protocol-v2-bundle-uri-git.sh  |  17 +++\n t/t5732-protocol-v2-bundle-uri-http.sh |  17 +++\n transport-helper.c                     |  13 +++\n transport-internal.h                   |   7 ++\n transport.c                            |  48 +++++++++\n transport.h                            |  18 ++++\n 12 files changed, 359 insertions(+)\n create mode 100644 t/lib-t5730-protocol-v2-bundle-uri.sh\n create mode 100755 t/t5730-protocol-v2-bundle-uri-file.sh\n create mode 100755 t/t5731-protocol-v2-bundle-uri-git.sh\n create mode 100755 t/t5732-protocol-v2-bundle-uri-http.sh\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex a572cda5030..b2c1b4142ee 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -27,6 +27,7 @@\n #include \"iterator.h\"\n #include \"sigchain.h\"\n #include \"branch.h\"\n+#include \"connect.h\"\n #include \"remote.h\"\n #include \"run-command.h\"\n #include \"connected.h\"\n@@ -1220,6 +1221,12 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tif (refs)\n \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n \n+\t/*\n+\t * Populate transport->got_remote_bundle_uri and\n+\t * transport->bundle_uri. We might get nothing.\n+\t */\n+\ttransport_get_remote_bundle_uri(transport);\n+\n \tif (mapped_refs) {\n \t\tint hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport));\n \ndiff --git a/bundle-uri.c b/bundle-uri.c\nindex 9827fc5da17..c503ed51ca8 100644\n--- a/bundle-uri.c\n+++ b/bundle-uri.c\n@@ -26,6 +26,10 @@ static int bundle_uri_config(const char *var, const char *value, void *data)\n \n int bundle_uri_advertise(struct repository *r, struct strbuf *value)\n {\n+\tif (value &&\n+\t    git_env_bool(\"GIT_TEST_BUNDLE_URI_UNKNOWN_CAPABILITY_VALUE\", 0))\n+\t\tstrbuf_addstr(value, \"test-unknown-capability-value\");\n+\n \tif (advertise_bundle_uri != -1)\n \t\tgoto cached;\n \ndiff --git a/connect.c b/connect.c\nindex e6d0b1d34bd..a8fdb5255f7 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -15,6 +15,7 @@\n #include \"version.h\"\n #include \"protocol.h\"\n #include \"alias.h\"\n+#include \"bundle-uri.h\"\n \n static char *server_capabilities_v1;\n static struct strvec server_capabilities_v2 = STRVEC_INIT;\n@@ -491,6 +492,52 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)\n \t}\n }\n \n+int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,\n+\t\t\t  struct string_list *bundle_uri, int stateless_rpc)\n+{\n+\tint line_nr = 1;\n+\n+\t/* Assert bundle-uri support */\n+\tserver_supports_v2(\"bundle-uri\", 1);\n+\n+\t/* (Re-)send capabilities */\n+\tsend_capabilities(fd_out, reader);\n+\n+\t/* Send command */\n+\tpacket_write_fmt(fd_out, \"command=bundle-uri\\n\");\n+\tpacket_delim(fd_out);\n+\n+\t/* Send options */\n+\tif (git_env_bool(\"GIT_TEST_PROTOCOL_BAD_BUNDLE_URI\", 0))\n+\t\tpacket_write_fmt(fd_out, \"test-bad-client\\n\");\n+\tpacket_flush(fd_out);\n+\n+\t/* Process response from server */\n+\twhile (packet_reader_read(reader) == PACKET_READ_NORMAL) {\n+\t\tconst char *line = reader->line;\n+\t\tline_nr++;\n+\n+\t\tif (!bundle_uri_parse_line(bundle_uri, line))\n+\t\t\tcontinue;\n+\n+\t\treturn error(_(\"error on bundle-uri response line %d: %s\"),\n+\t\t\t     line_nr, line);\n+\t}\n+\n+\tif (reader->status != PACKET_READ_FLUSH)\n+\t\treturn error(_(\"expected flush after bundle-uri listing\"));\n+\n+\t/*\n+\t * Might die(), but obscure enough that that's OK, e.g. in\n+\t * serve.c we'll call BUG() on its equivalent (the\n+\t * PACKET_READ_RESPONSE_END check).\n+\t */\n+\tcheck_stateless_delimiter(stateless_rpc, reader,\n+\t\t\t\t  _(\"expected response end packet after ref listing\"));\n+\n+\treturn 0;\n+}\n+\n struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     struct ref **list, int for_push,\n \t\t\t     struct transport_ls_refs_options *transport_options,\ndiff --git a/remote.h b/remote.h\nindex 4a1209ae2c8..ca820b60948 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -236,6 +236,10 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     const struct string_list *server_options,\n \t\t\t     int stateless_rpc);\n \n+/* Used for protocol v2 in order to retrieve refs from a remote */\n+int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,\n+\t\t\t  struct string_list *bundle_uri, int stateless_rpc);\n+\n int resolve_remote_symref(struct ref *ref, struct ref *list);\n \n /*\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nnew file mode 100644\nindex 00000000000..724acecda66\n--- /dev/null\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -0,0 +1,141 @@\n+# Included from t573*-protocol-v2-bundle-uri-*.sh\n+\n+T5730_PARENT=\n+T5730_URI=\n+T5730_BUNDLE_URI=\n+case \"$T5730_PROTOCOL\" in\n+file)\n+\tT5730_PARENT=file_parent\n+\tT5730_URI=\"file://$PWD/file_parent\"\n+\tT5730_BUNDLE_URI=\"$T5730_URI/fake.bdl\"\n+\ttest_set_prereq T5730_FILE\n+\t;;\n+git)\n+\t. \"$TEST_DIRECTORY\"/lib-git-daemon.sh\n+\tstart_git_daemon --export-all --enable=receive-pack\n+\tT5730_PARENT=\"$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent\"\n+\tT5730_URI=\"$GIT_DAEMON_URL/parent\"\n+\tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n+\ttest_set_prereq T5730_GIT\n+\t;;\n+http)\n+\t. \"$TEST_DIRECTORY\"/lib-httpd.sh\n+\tstart_httpd\n+\tT5730_PARENT=\"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\"\n+\tT5730_URI=\"$HTTPD_URL/smart/http_parent\"\n+\tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n+\ttest_set_prereq T5730_HTTP\n+\t;;\n+*)\n+\tBUG \"Need to pass valid T5730_PROTOCOL (was $T5730_PROTOCOL)\"\n+\t;;\n+esac\n+\n+test_expect_success \"setup protocol v2 $T5730_PROTOCOL:// tests\" '\n+\tgit init \"$T5730_PARENT\" &&\n+\ttest_commit -C \"$T5730_PARENT\" one\n+'\n+\n+# Poor man's URI escaping. Good enough for the test suite whose trash\n+# directory has a space in it. See 93c3fcbe4d4 (git-svn: attempt to\n+# mimic SVN 1.7 URL canonicalization, 2012-07-28) for prior art.\n+test_uri_escape() {\n+\tsed 's/ /%20/g'\n+}\n+\n+case \"$T5730_PROTOCOL\" in\n+http)\n+\ttest_expect_success \"setup config for $T5730_PROTOCOL:// tests\" '\n+\t\tgit -C \"$T5730_PARENT\" config http.receivepack true\n+\t'\n+\t;;\n+*)\n+\t;;\n+esac\n+T5730_BUNDLE_URI_ESCAPED=$(echo \"$T5730_BUNDLE_URI\" | test_uri_escape)\n+\n+test_expect_success \"connect with $T5730_PROTOCOL:// using protocol v2: no bundle-uri\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote --symref \"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\n+\t# Server responded using protocol v2\n+\tgrep \"< version 2\" log &&\n+\n+\t! grep bundle-uri log\n+'\n+\n+test_expect_success \"connect with $T5730_PROTOCOL:// using protocol v2: have bundle-uri\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" \\\n+\t\tuploadpack.bundleURI \"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote --symref \"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\n+\t# Server responded using protocol v2\n+\tgrep \"< version 2\" log &&\n+\n+\t# Server advertised bundle-uri capability\n+\tgrep bundle-uri log\n+'\n+\n+test_expect_success !T5730_HTTP \"bad client with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tCloning into '\"'\"'child'\"'\"'...\n+\tEOF\n+\tcase \"$T5730_PROTOCOL\" in\n+\tfile)\n+\t\tcat >fatal-bundle-uri.expect <<-\\EOF\n+\t\tfatal: bundle-uri: unexpected argument: '\"'\"'test-bad-client'\"'\"'\n+\t\tEOF\n+\t\t;;\n+\t*)\n+\t\tcat >fatal.expect <<-\\EOF\n+\t\tfatal: read error: Connection reset by peer\n+\t\tEOF\n+\t\t;;\n+\tesac &&\n+\n+\ttest_when_finished \"rm -rf child\" &&\n+\ttest_must_fail ok=sigpipe env \\\n+\t\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\t\tGIT_TEST_PROTOCOL_BAD_BUNDLE_URI=true \\\n+\t\tgit -c protocol.version=2 \\\n+\t\tclone \"$T5730_URI\" child \\\n+\t\t>out 2>err &&\n+\ttest_must_be_empty out &&\n+\n+\tgrep -v -e ^fatal: -e ^error: err >err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\n+\tcase \"$T5730_PROTOCOL\" in\n+\tfile)\n+\t\t# Due to general race conditions with client/server replies we\n+\t\t# may or may not get \"fatal: the remote end hung up\n+\t\t# expectedly\" here\n+\t\tgrep \"^fatal: bundle-uri:\" err >fatal-bundle-uri.actual &&\n+\t\ttest_cmp fatal-bundle-uri.expect fatal-bundle-uri.actual\n+\t\t;;\n+\t*)\n+\t\tgrep \"^fatal:\" err >fatal.actual &&\n+\t\ttest_cmp fatal.expect fatal.actual\n+\t\t;;\n+\tesac &&\n+\n+\tgrep \"clone> test-bad-client$\" log >sent-bad-request &&\n+\ttest_file_not_empty sent-bad-request\n+'\ndiff --git a/t/t5730-protocol-v2-bundle-uri-file.sh b/t/t5730-protocol-v2-bundle-uri-file.sh\nnew file mode 100755\nindex 00000000000..89203d3a23c\n--- /dev/null\n+++ b/t/t5730-protocol-v2-bundle-uri-file.sh\n@@ -0,0 +1,36 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri with protocol v2 and 'file://' transport\"\n+\n+TEST_NO_CREATE_REPO=1\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+# Test protocol v2 with 'file://' transport\n+#\n+T5730_PROTOCOL=file\n+. \"$TEST_DIRECTORY\"/lib-t5730-protocol-v2-bundle-uri.sh\n+\n+test_expect_success \"unknown capability value with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" \\\n+\t\tuploadpack.bundleURI \"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tGIT_TEST_BUNDLE_URI_UNKNOWN_CAPABILITY_VALUE=true \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote --symref \"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\n+\t# Server responded using protocol v2\n+\tgrep \"< version 2\" log &&\n+\n+\tgrep \"> bundle-uri=test-unknown-capability-value\" log\n+'\n+\n+test_done\ndiff --git a/t/t5731-protocol-v2-bundle-uri-git.sh b/t/t5731-protocol-v2-bundle-uri-git.sh\nnew file mode 100755\nindex 00000000000..282847b311f\n--- /dev/null\n+++ b/t/t5731-protocol-v2-bundle-uri-git.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri with protocol v2 and 'git://' transport\"\n+\n+TEST_NO_CREATE_REPO=1\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+# Test protocol v2 with 'git://' transport\n+#\n+T5730_PROTOCOL=git\n+. \"$TEST_DIRECTORY\"/lib-t5730-protocol-v2-bundle-uri.sh\n+\n+test_done\ndiff --git a/t/t5732-protocol-v2-bundle-uri-http.sh b/t/t5732-protocol-v2-bundle-uri-http.sh\nnew file mode 100755\nindex 00000000000..fcc1cf3faef\n--- /dev/null\n+++ b/t/t5732-protocol-v2-bundle-uri-http.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri with protocol v2 and 'git://' transport\"\n+\n+TEST_NO_CREATE_REPO=1\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+# Test protocol v2 with 'git://' transport\n+#\n+T5730_PROTOCOL=http\n+. \"$TEST_DIRECTORY\"/lib-t5730-protocol-v2-bundle-uri.sh\n+\n+test_done\ndiff --git a/transport-helper.c b/transport-helper.c\nindex a0297b0986c..4ddf2a4be2a 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -1264,9 +1264,22 @@ static struct ref *get_refs_list_using_list(struct transport *transport,\n \treturn ret;\n }\n \n+static int get_bundle_uri(struct transport *transport)\n+{\n+\tget_helper(transport);\n+\n+\tif (process_connect(transport, 0)) {\n+\t\tdo_take_over(transport);\n+\t\treturn transport->vtable->get_bundle_uri(transport);\n+\t}\n+\n+\treturn -1;\n+}\n+\n static struct transport_vtable vtable = {\n \t.set_option\t= set_helper_option,\n \t.get_refs_list\t= get_refs_list,\n+\t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs,\n \t.push_refs\t= push_refs,\n \t.connect\t= connect_helper,\ndiff --git a/transport-internal.h b/transport-internal.h\nindex c4ca0b733ac..90ea749e5cf 100644\n--- a/transport-internal.h\n+++ b/transport-internal.h\n@@ -26,6 +26,13 @@ struct transport_vtable {\n \tstruct ref *(*get_refs_list)(struct transport *transport, int for_push,\n \t\t\t\t     struct transport_ls_refs_options *transport_options);\n \n+\t/**\n+\t * Populates the remote side's bundle-uri under protocol v2,\n+\t * if the \"bundle-uri\" capability was advertised. Returns 0 if\n+\t * OK, negative values on error.\n+\t */\n+\tint (*get_bundle_uri)(struct transport *transport);\n+\n \t/**\n \t * Fetch the objects for the given refs. Note that this gets\n \t * an array, and should ignore the list structure.\ndiff --git a/transport.c b/transport.c\nindex 253d6671b1f..7c9a371ed7f 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -22,6 +22,7 @@\n #include \"protocol.h\"\n #include \"object-store.h\"\n #include \"color.h\"\n+#include \"bundle-uri.h\"\n \n static int transport_use_color = -1;\n static char transport_colors[][COLOR_MAXLEN] = {\n@@ -349,6 +350,21 @@ static struct ref *get_refs_via_connect(struct transport *transport, int for_pus\n \treturn handshake(transport, for_push, options, 1);\n }\n \n+static int get_bundle_uri(struct transport *transport)\n+{\n+\tstruct git_transport_data *data = transport->data;\n+\tstruct packet_reader reader;\n+\tint stateless_rpc = transport->stateless_rpc;\n+\tstring_list_init_dup(&transport->bundle_uri);\n+\n+\tpacket_reader_init(&reader, data->fd[0], NULL, 0,\n+\t\t\t   PACKET_READ_CHOMP_NEWLINE |\n+\t\t\t   PACKET_READ_GENTLE_ON_EOF);\n+\n+\treturn get_remote_bundle_uri(data->fd[1], &reader,\n+\t\t\t\t     &transport->bundle_uri, stateless_rpc);\n+}\n+\n static int fetch_refs_via_pack(struct transport *transport,\n \t\t\t       int nr_heads, struct ref **to_fetch)\n {\n@@ -888,6 +904,7 @@ static int disconnect_git(struct transport *transport)\n \n static struct transport_vtable taken_over_vtable = {\n \t.get_refs_list\t= get_refs_via_connect,\n+\t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs_via_pack,\n \t.push_refs\t= git_transport_push,\n \t.disconnect\t= disconnect_git\n@@ -1041,6 +1058,7 @@ static struct transport_vtable bundle_vtable = {\n \n static struct transport_vtable builtin_smart_vtable = {\n \t.get_refs_list\t= get_refs_via_connect,\n+\t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs_via_pack,\n \t.push_refs\t= git_transport_push,\n \t.connect\t= connect_git,\n@@ -1054,6 +1072,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \n \tret->progress = isatty(2);\n \tstring_list_init_dup(&ret->pack_lockfiles);\n+\tstring_list_init_dup(&ret->bundle_uri);\n \n \tif (!remote)\n \t\tBUG(\"No remote provided to transport_get()\");\n@@ -1462,6 +1481,34 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n+int transport_get_remote_bundle_uri(struct transport *transport)\n+{\n+\tconst struct transport_vtable *vtable = transport->vtable;\n+\n+\t/* Lazily configured */\n+\tif (transport->got_remote_bundle_uri++)\n+\t\treturn 0;\n+\n+\t/*\n+\t * \"Support\" protocol v0 and v2 without bundle-uri support by\n+\t * silently degrading to a NOOP.\n+\t */\n+\tif (!server_supports_v2(\"bundle-uri\", 0))\n+\t\treturn 0;\n+\n+\t/*\n+\t * This is intentionally below the transport.injectBundleURI,\n+\t * we want to be able to inject into protocol v0, or into the\n+\t * dialog of a server who doesn't support this.\n+\t */\n+\tif (!vtable->get_bundle_uri)\n+\t\treturn error(_(\"bundle-uri operation not supported by protocol\"));\n+\n+\tif (vtable->get_bundle_uri(transport) < 0)\n+\t\treturn error(_(\"could not retrieve server-advertised bundle-uri list\"));\n+\treturn 0;\n+}\n+\n void transport_unlock_pack(struct transport *transport, unsigned int flags)\n {\n \tint in_signal_handler = !!(flags & TRANSPORT_UNLOCK_PACK_IN_SIGNAL_HANDLER);\n@@ -1492,6 +1539,7 @@ int transport_disconnect(struct transport *transport)\n \t\tret = transport->vtable->disconnect(transport);\n \tif (transport->got_remote_refs)\n \t\tfree_refs((void *)transport->remote_refs);\n+\tbundle_uri_string_list_clear(&transport->bundle_uri);\n \tfree(transport);\n \treturn ret;\n }\ndiff --git a/transport.h b/transport.h\nindex a0bc6a1e9eb..7740306b850 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -75,6 +75,18 @@ struct transport {\n \t */\n \tunsigned got_remote_refs : 1;\n \n+\t/**\n+\t * Indicates whether we already called get_bundle_uri_list(); set by\n+\t * transport.c::transport_get_remote_bundle_uri().\n+\t */\n+\tunsigned got_remote_bundle_uri : 1;\n+\n+\t/*\n+\t * The results of \"command=bundle-uri\", if both sides support\n+\t * the \"bundle-uri\" capability.\n+\t */\n+\tstruct string_list bundle_uri;\n+\n \t/*\n \t * Transports that call take-over destroys the data specific to\n \t * the transport type while doing so, and cannot be reused.\n@@ -276,6 +288,12 @@ void transport_ls_refs_options_release(struct transport_ls_refs_options *opts);\n const struct ref *transport_get_remote_refs(struct transport *transport,\n \t\t\t\t\t    struct transport_ls_refs_options *transport_options);\n \n+/**\n+ * Retrieve bundle URI(s) from a remote. Populates \"struct\n+ * transport\"'s \"bundle_uri\" and \"got_remote_bundle_uri\".\n+ */\n+int transport_get_remote_bundle_uri(struct transport *transport);\n+\n /*\n  * Fetch the hash algorithm used by a remote.\n  *\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451144","messageId":"RFC-patch-v2-07.13-b0ce379528e-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 07/13] bundle-uri client: add transfer.injectBundleURI support","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:19Z","receivedAt":"2022-03-11T16:24:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add the ability to inject \"fake\" bundle URIs into the newly supported\nbundle-uri dialog. As discussed in the added documentation this allows\nus to pretend as though the remote supports bundle URIs.\n\nThis will be useful both for ad-hoc testing, and for the real use-case\nof retrofitting bundle URI support on-the-fly, i.e. to have:\n\n\tgit -c transfer.injectBundleURI \"file://$(pwd)/local.bdl\" \\\n\tclone https://example.com/git.git\"\n\nBe similar in spirit to:\n\n\tgit clone --reference local-clone.git --disassociate \\\n\thttps://example.com/git.git\"\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/transfer.txt     | 20 ++++++++++++\n t/lib-t5730-protocol-v2-bundle-uri.sh | 46 +++++++++++++++++++++++++++\n transport.c                           | 33 +++++++++++++++++++\n 3 files changed, 99 insertions(+)\n\ndiff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt\nindex b49429eb4db..71b9b8f29e6 100644\n--- a/Documentation/config/transfer.txt\n+++ b/Documentation/config/transfer.txt\n@@ -77,3 +77,23 @@ transfer.unpackLimit::\n transfer.advertiseSID::\n \tBoolean. When true, client and server processes will advertise their\n \tunique session IDs to their remote counterpart. Defaults to false.\n+\n+transfer.injectBundleURI::\n+\tAllows for the injection of `bundle-uri` lines into the\n+\tprotocol v2 transport dialog (see `protocol.version` in\n+\tlinkgit:git-config[1]). See `bundle-uri` in\n+\tlink:technical/protocol-v2.html[the Git Wire Protocol, Version\n+\t2] documentation for what the format looks like.\n++\n+Can be given more than once, each key being injected as one line into\n+the dialog.\n++\n+This is useful for testing the `bundle-uri` facility, and to e.g. use\n+linkgit:git-clone[1] to clone from a server which does not support\n+`bundle-uri`, but where the clone can benefit from getting some or\n+most of the data from a static bundle retrieved from elsewhere.\n++\n+Impacts any command that uses the transport to communicate with remote\n+linkgit:git-upload-pack[1] processes, e.g. linkgit:git-clone[1],\n+linkgit:git-fetch[1] and the linkgit:git-ls-remote-bundle-uri[1]\n+inspection command, this includes the `file://` protocol.\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nindex f0c41d60654..3be47bacc5f 100644\n--- a/t/lib-t5730-protocol-v2-bundle-uri.sh\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -271,3 +271,49 @@ test_expect_success \"ls-remote-bundle-uri --[no-]quiet with $T5730_PROTOCOL:// u\n \ttest_must_be_empty err &&\n \ttest_cmp out.expect out.actual\n '\n+\n+test_expect_success \"ls-remote-bundle-uri with -c transfer.injectBundleURI using with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\thttps://injected.example.com/fake-1.bdl\n+\thttps://injected.example.com/fake-2.bdl\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c transfer.injectBundleURI=\"https://injected.example.com/fake-1.bdl\" \\\n+\t\t-c transfer.injectBundleURI=\"https://injected.example.com/fake-2.bdl\" \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\ttest_cmp expect actual &&\n+\ttest_path_is_missing log\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri with bad -c transfer.injectBundleURI protocol v2 with $T5730_PROTOCOL://\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bad (empty) transfer.injectBundleURI\n+\terror: could not get the bundle-uri list\n+\tEOF\n+\n+\ttest_must_fail env \\\n+\t\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\t\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c transfer.injectBundleURI \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>out 2>err.actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_path_is_missing log\n+'\ndiff --git a/transport.c b/transport.c\nindex 16147a170f8..16332f9d64a 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1510,14 +1510,47 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n+struct config_cb {\n+\tstruct transport *transport;\n+\tint configured;\n+\tint ret;\n+};\n+\n+static int bundle_uri_config(const char *var, const char *value, void *data)\n+{\n+\tstruct config_cb *cb = data;\n+\tstruct transport *transport = cb->transport;\n+\tstruct string_list *uri = &transport->bundle_uri;\n+\n+\tif (!strcmp(var, \"transfer.injectbundleuri\")) {\n+\t\tcb->configured = 1;\n+\t\tif (!value)\n+\t\t\tcb->ret = error(_(\"bad (empty) transfer.injectBundleURI\"));\n+\t\telse if (bundle_uri_parse_line(uri, value) < 0)\n+\t\t\tcb->ret = error(_(\"bad transfer.injectBundleURI: '%s'\"),\n+\t\t\t\t\tvalue);\n+\t\treturn 0;\n+\t}\n+\treturn 0;\n+}\n+\n int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n {\n \tconst struct transport_vtable *vtable = transport->vtable;\n+\tstruct config_cb cb = {\n+\t\t.transport = transport,\n+\t};\n \n \t/* Lazily configured */\n \tif (transport->got_remote_bundle_uri++)\n \t\treturn 0;\n \n+\tgit_config(bundle_uri_config, &cb);\n+\n+\t/* Our own config can fake it up with transport.injectBundleURI */\n+\tif (cb.configured)\n+\t\treturn cb.ret;\n+\n \t/*\n \t * This is intentionally below the transport.injectBundleURI,\n \t * we want to be able to inject into protocol v0, or into the\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451145","messageId":"RFC-patch-v2-03.13-3abfb2290fd-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 03/13] bundle-uri client: add \"bundle-uri\" parsing + tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:15Z","receivedAt":"2022-03-11T16:24:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a \"test-tool bundle-uri parse\" which parses the format defined in\nthe newly specified \"bundle-uri\" command.\n\nAs note in the \"bundle-uri\" section in protocol-v2.txt we haven't\nspecified any key-values yet, just URI lines, but we should parse\ntheir format for conformity with the spec.\n\nWe need to make sure our future client doesn't die if this optional\ndata is ever provided by the server, and that we've covered all the\nedge cases with these key-values in our specification. Let's add and\ntest a bundle_uri_parse_line() to do that.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile                    |   1 +\n bundle-uri.c                | 124 +++++++++++++++++++++++++++++\n bundle-uri.h                |  16 ++++\n t/helper/test-bundle-uri.c  |  83 +++++++++++++++++++\n t/helper/test-tool.c        |   1 +\n t/helper/test-tool.h        |   1 +\n t/t5750-bundle-uri-parse.sh | 153 ++++++++++++++++++++++++++++++++++++\n 7 files changed, 379 insertions(+)\n create mode 100644 t/helper/test-bundle-uri.c\n create mode 100755 t/t5750-bundle-uri-parse.sh\n\ndiff --git a/Makefile b/Makefile\nindex 5a3d35109a1..4fec8e5af09 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -696,6 +696,7 @@ PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))\n TEST_BUILTINS_OBJS += test-advise.o\n TEST_BUILTINS_OBJS += test-bitmap.o\n TEST_BUILTINS_OBJS += test-bloom.o\n+TEST_BUILTINS_OBJS += test-bundle-uri.o\n TEST_BUILTINS_OBJS += test-chmtime.o\n TEST_BUILTINS_OBJS += test-config.o\n TEST_BUILTINS_OBJS += test-crontab.o\ndiff --git a/bundle-uri.c b/bundle-uri.c\nindex ff054ddc690..9827fc5da17 100644\n--- a/bundle-uri.c\n+++ b/bundle-uri.c\n@@ -53,3 +53,127 @@ int bundle_uri_command(struct repository *r,\n \n \treturn 0;\n }\n+\n+/**\n+ * General API for {transport,connect}.c etc.\n+ */\n+int bundle_uri_parse_line(struct string_list *bundle_uri, const char *line)\n+{\n+\tsize_t i;\n+\tstruct string_list columns = STRING_LIST_INIT_DUP;\n+\tconst char *uri;\n+\tstruct string_list *uri_columns = NULL;\n+\tint ret = 0;\n+\n+\tif (!strlen(line))\n+\t\treturn error(_(\"bundle-uri: got an empty line\"));\n+\n+\t/*\n+\t * Right now we don't understand anything beyond the first SP,\n+\t * but let's be tolerant and ignore any future unknown\n+\t * fields. See the \"MUST\" note about \"bundle-feature-key\" in\n+\t * Documentation/technical/protocol-v2.txt\n+\t */\n+\tif (string_list_split(&columns, line, ' ', -1) < 1)\n+\t\treturn error(_(\"bundle-uri: line not in SP-delimited format: %s\"), line);\n+\n+\t/*\n+\t * We represent a \"<uri>[ <key-values>...]\" line with the URI\n+\t * being the .string in a string list, and the .util being an\n+\t * optional string list of key (.string) and values\n+\t * (.util). If the top-level .util is NULL there's no\n+\t * key-value pairs....\n+\t */\n+\turi = columns.items[0].string;\n+\tif (!strlen(uri)) {\n+\t\tret = error(_(\"bundle-uri: got an empty URI component\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/*\n+\t * ... we're going to need that non-NULL .util .\n+\t */\n+\tif (columns.nr > 1) {\n+\t\turi_columns = xcalloc(1, sizeof(struct string_list));\n+\t\tstring_list_init_dup(uri_columns);\n+\t}\n+\n+\t/*\n+\t * Let's parse the optional \"kv\" format, even if we don't\n+\t * understand any of the keys or values yet.\n+\t */\n+\tfor (i = 1; i < columns.nr; i++) {\n+\t\tstruct string_list kv = STRING_LIST_INIT_DUP;\n+\t\tconst char *arg = columns.items[i].string;\n+\t\tint fields = string_list_split(&kv, arg, '=', 2);\n+\t\tint err = 0;\n+\n+\t\tswitch (fields) {\n+\t\tcase 0:\n+\t\t\tBUG(\"should have no fields=0\");\n+\t\tcase 1:\n+\t\t\tif (!strlen(arg)) {\n+\t\t\t\terr = error(\"bundle-uri: column %lu: got an empty attribute (full line was '%s')\",\n+\t\t\t\t\t    i, line);\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\t/*\n+\t\t\t * We could dance around with\n+\t\t\t * string_list_append_nodup() and skip\n+\t\t\t * string_list_clear(&kv, 0) here, but let's\n+\t\t\t * keep it simple.\n+\t\t\t */\n+\t\t\tstring_list_append(uri_columns, arg);\n+\t\t\tbreak;\n+\t\tcase 2:\n+\t\t{\n+\t\t\tconst char *k = kv.items[0].string;\n+\t\t\tconst char *v = kv.items[1].string;\n+\n+\t\t\tstring_list_append(uri_columns, k)->util = xstrdup(v);\n+\t\t\tbreak;\n+\t\t}\n+\t\tdefault:\n+\t\t\terr = error(\"bundle-uri: column %lu: '%s' more than one '=' character (full line was '%s')\",\n+\t\t\t\t    i, arg, line);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tstring_list_clear(&kv, 0);\n+\t\tif (err) {\n+\t\t\tret = err;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\n+\t/*\n+\t * Per the spec we'll only consider bundle-uri lines OK if\n+\t * there were no parsing problems, even if the problems were\n+\t * with attributes whose content we don't understand.\n+\t */\n+\tif (ret && uri_columns) {\n+\t\tstring_list_clear(uri_columns, 1);\n+\t\tfree(uri_columns);\n+\t} else if (!ret) {\n+\t\tstring_list_append(bundle_uri, uri)->util = uri_columns;\n+\t}\n+\n+cleanup:\n+\tstring_list_clear(&columns, 0);\n+\treturn ret;\n+}\n+\n+static void bundle_uri_string_list_clear_cb(void *util, const char *string)\n+{\n+\tstruct string_list *fields = util;\n+\tif (!fields)\n+\t\treturn;\n+\tstring_list_clear(fields, 1);\n+\tfree(fields);\n+}\n+\n+void bundle_uri_string_list_clear(struct string_list *bundle_uri)\n+{\n+\tstring_list_clear_func(bundle_uri, bundle_uri_string_list_clear_cb);\n+}\ndiff --git a/bundle-uri.h b/bundle-uri.h\nindex 5a7e556a0ba..be6d1df97ff 100644\n--- a/bundle-uri.h\n+++ b/bundle-uri.h\n@@ -3,6 +3,7 @@\n #include \"repository.h\"\n #include \"pkt-line.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n \n /**\n  * API used by serve.[ch].\n@@ -10,4 +11,19 @@\n int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n int bundle_uri_command(struct repository *r, struct packet_reader *request);\n \n+/**\n+ * General API for {transport,connect}.c etc.\n+ */\n+\n+/**\n+ * bundle_uri_parse_line() returns 0 when a valid bundle-uri has been\n+ * added to `bundle_uri`, <0 on error.\n+ */\n+int bundle_uri_parse_line(struct string_list *bundle_uri, const char *line);\n+\n+/**\n+ * Clear the `bundle_uri` list. Just a very thin wrapper on\n+ * string_list_clear().\n+ */\n+void bundle_uri_string_list_clear(struct string_list *bundle_uri);\n #endif /* BUNDLE_URI_H */\ndiff --git a/t/helper/test-bundle-uri.c b/t/helper/test-bundle-uri.c\nnew file mode 100644\nindex 00000000000..805a86c0130\n--- /dev/null\n+++ b/t/helper/test-bundle-uri.c\n@@ -0,0 +1,83 @@\n+#include \"test-tool.h\"\n+#include \"parse-options.h\"\n+#include \"bundle-uri.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n+\n+static int cmd__bundle_uri_parse(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool bundle-uri parse <in\",\n+\t\tNULL\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tint err = 0;\n+\tstruct string_list_item *item;\n+\tsize_t line_nr = 0;\n+\n+\targc = parse_options(argc, argv, NULL, options, usage, 0);\n+\tif (argc)\n+\t\tgoto usage;\n+\n+\twhile (strbuf_getline(&sb, stdin) != EOF) {\n+\t\tline_nr++;\n+\t\tif (bundle_uri_parse_line(&list, sb.buf) < 0)\n+\t\t\terr = error(\"bad line: '%s'\", sb.buf);\n+\t}\n+\n+\tfor_each_string_list_item(item, &list) {\n+\t\tstruct string_list_item *kv_item;\n+\t\tstruct string_list *kv = item->util;\n+\n+\t\tfprintf(stdout, \"%s\", item->string);\n+\t\tif (!kv) {\n+\t\t\tfprintf(stdout, \"\\n\");\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfor_each_string_list_item(kv_item, kv) {\n+\t\t\tconst char *k = kv_item->string;\n+\t\t\tconst char *v = kv_item->util;\n+\n+\t\t\tif (v)\n+\t\t\t\tfprintf(stdout, \" [kv: %s => %s]\", k, v);\n+\t\t\telse\n+\t\t\t\tfprintf(stdout, \" [attr: %s]\", k);\n+\t\t}\n+\t\tfprintf(stdout, \"\\n\");\n+\t}\n+\tstrbuf_release(&sb);\n+\n+\tbundle_uri_string_list_clear(&list);\n+\n+\treturn err < 0 ? 1 : 0;\n+usage:\n+\tusage_with_options(usage, options);\n+}\n+\n+int cmd__bundle_uri(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool bundle-uri <subcommand> [<options>]\",\n+\t\tNULL\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0);\n+\tif (argc == 1)\n+\t\tgoto usage;\n+\n+\tif (!strcmp(argv[1], \"parse\"))\n+\t\treturn cmd__bundle_uri_parse(argc - 1, argv + 1);\n+\terror(\"there is no test-tool bundle-uri tool '%s'\", argv[1]);\n+\n+usage:\n+\tusage_with_options(usage, options);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex e6ec69cf326..dc73e68f329 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -17,6 +17,7 @@ static struct test_cmd cmds[] = {\n \t{ \"advise\", cmd__advise_if_enabled },\n \t{ \"bitmap\", cmd__bitmap },\n \t{ \"bloom\", cmd__bloom },\n+\t{ \"bundle-uri\", cmd__bundle_uri },\n \t{ \"chmtime\", cmd__chmtime },\n \t{ \"config\", cmd__config },\n \t{ \"crontab\", cmd__crontab },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex 20756eefdda..927b6b418cd 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -7,6 +7,7 @@\n int cmd__advise_if_enabled(int argc, const char **argv);\n int cmd__bitmap(int argc, const char **argv);\n int cmd__bloom(int argc, const char **argv);\n+int cmd__bundle_uri(int argc, const char **argv);\n int cmd__chmtime(int argc, const char **argv);\n int cmd__config(int argc, const char **argv);\n int cmd__crontab(int argc, const char **argv);\ndiff --git a/t/t5750-bundle-uri-parse.sh b/t/t5750-bundle-uri-parse.sh\nnew file mode 100755\nindex 00000000000..70fd1b398e9\n--- /dev/null\n+++ b/t/t5750-bundle-uri-parse.sh\n@@ -0,0 +1,153 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri bundle_uri_parse_line()\"\n+\n+TEST_NO_CREATE_REPO=1\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'bundle_uri_parse_line() just URIs' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle.bdl\n+\thttps://example.com/bundle.bdl\n+\tfile:///usr/share/git/bundle.bdl\n+\tEOF\n+\n+\t# For the simple case\n+\tcp in expect &&\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() with attributes' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl attr\n+\thttp://example.com/bundle2.bdl ibute\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: attr]\n+\thttp://example.com/bundle2.bdl [attr: ibute]\n+\tEOF\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() with attributes and key-value attributes' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl x a=b y c=d z e=f a=b\n+\tEOF\n+\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: x] [kv: a => b] [attr: y] [kv: c => d] [attr: z] [kv: e => f] [kv: a => b]\n+\tEOF\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: extra SP' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl one-space\n+\thttp://example.com/bundle2.bdl  two-space\n+\thttp://example.com/bundle3.bdl   three-space\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: column 1: got an empty attribute (full line was '\\''http://example.com/bundle2.bdl  two-space'\\'')\n+\terror: bad line: '\\''http://example.com/bundle2.bdl  two-space'\\''\n+\terror: bundle-uri: column 1: got an empty attribute (full line was '\\''http://example.com/bundle3.bdl   three-space'\\'')\n+\terror: bad line: '\\''http://example.com/bundle3.bdl   three-space'\\''\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: one-space]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty lines' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\n+\thttp://example.com/bundle2.bdl a=b\n+\n+\thttp://example.com/bundle3.bdl\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: got an empty line\n+\terror: bad line: '\\'''\\''\n+\terror: bundle-uri: got an empty line\n+\terror: bad line: '\\'''\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\thttp://example.com/bundle2.bdl [kv: a => b]\n+\thttp://example.com/bundle3.bdl\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty URIs' '\n+\tsed \"s/> //\" >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\t>  a=b\n+\thttp://example.com/bundle3.bdl a=b\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: got an empty URI component\n+\terror: bad line: '\\'' a=b'\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\thttp://example.com/bundle3.bdl [kv: a => b]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: multiple = in key-values' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl k=v=extra\n+\thttp://example.com/bundle2.bdl a=b k=v=extra c=d\n+\thttp://example.com/bundle3.bdl kv=ok\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: column 1: '\\''k=v=extra'\\'' more than one '\\''='\\'' character (full line was '\\''http://example.com/bundle1.bdl k=v=extra'\\'')\n+\terror: bad line: '\\''http://example.com/bundle1.bdl k=v=extra'\\''\n+\terror: bundle-uri: column 2: '\\''k=v=extra'\\'' more than one '\\''='\\'' character (full line was '\\''http://example.com/bundle2.bdl a=b k=v=extra c=d'\\'')\n+\terror: bad line: '\\''http://example.com/bundle2.bdl a=b k=v=extra c=d'\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle3.bdl [kv: kv => ok]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451146","messageId":"RFC-patch-v2-08.13-44d96a0f5f8-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 08/13] bundle-uri client: add boolean transfer.bundleURI setting","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:20Z","receivedAt":"2022-03-11T16:24:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"The yet-to-be introduced client support for bundle-uri will always\nfall back on a full clone, but we'd still like to be able to ignore a\nserver's bundle-uri advertisement entirely.\n\nThis is useful for testing, and if a server is pointing to bad\nbundles, they take a while to time out etc.\n\nSince we might see the config in any order we need to clear out any\naccumulated bundle_uri list when we see transfer.bundleURI=false\nsetting, and not add any more things to the list.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/transfer.txt |  6 ++++++\n transport.c                       | 21 +++++++++++++++++----\n 2 files changed, 23 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt\nindex 71b9b8f29e6..ae85ca5760b 100644\n--- a/Documentation/config/transfer.txt\n+++ b/Documentation/config/transfer.txt\n@@ -78,6 +78,12 @@ transfer.advertiseSID::\n \tBoolean. When true, client and server processes will advertise their\n \tunique session IDs to their remote counterpart. Defaults to false.\n \n+transfer.bundleURI::\n+\tWhen set to `false` ignores any server advertisement of\n+\t`bundle-uri` and proceed with a \"normal\" clone/fetch even if\n+\tusing bundles to bootstap is possible. Defaults to `true`,\n+\ti.e. bundle-uri is tried whenever a server offers it.\n+\n transfer.injectBundleURI::\n \tAllows for the injection of `bundle-uri` lines into the\n \tprotocol v2 transport dialog (see `protocol.version` in\ndiff --git a/transport.c b/transport.c\nindex 16332f9d64a..7085bfb3db8 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1510,19 +1510,28 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n-struct config_cb {\n+struct bundle_config_cb {\n \tstruct transport *transport;\n \tint configured;\n \tint ret;\n+\tint disabled;\n };\n \n static int bundle_uri_config(const char *var, const char *value, void *data)\n {\n-\tstruct config_cb *cb = data;\n+\tstruct bundle_config_cb *cb = data;\n \tstruct transport *transport = cb->transport;\n \tstruct string_list *uri = &transport->bundle_uri;\n \n-\tif (!strcmp(var, \"transfer.injectbundleuri\")) {\n+\tif (!strcmp(var, \"transfer.bundleuri\")) {\n+\t\tcb->disabled = !git_config_bool(var, value);\n+\t\tif (cb->disabled)\n+\t\t\tbundle_uri_string_list_clear(uri);\n+\t\treturn 0;\n+\t}\n+\n+\tif (!cb->disabled &&\n+\t    !strcmp(var, \"transfer.injectbundleuri\")) {\n \t\tcb->configured = 1;\n \t\tif (!value)\n \t\t\tcb->ret = error(_(\"bad (empty) transfer.injectBundleURI\"));\n@@ -1537,7 +1546,7 @@ static int bundle_uri_config(const char *var, const char *value, void *data)\n int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n {\n \tconst struct transport_vtable *vtable = transport->vtable;\n-\tstruct config_cb cb = {\n+\tstruct bundle_config_cb cb = {\n \t\t.transport = transport,\n \t};\n \n@@ -1547,6 +1556,10 @@ int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n \n \tgit_config(bundle_uri_config, &cb);\n \n+\t/* Don't use bundle-uri at all */\n+\tif (cb.disabled)\n+\t\treturn 0;\n+\n \t/* Our own config can fake it up with transport.injectBundleURI */\n \tif (cb.configured)\n \t\treturn cb.ret;\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451147","messageId":"RFC-patch-v2-09.13-d9f5b486511-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 09/13] fetch-pack: add a deref_without_lazy_fetch_extended()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:21Z","receivedAt":"2022-03-11T16:25:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a version of the deref_without_lazy_fetch function which can be\ncalled with custom oi_flags and to grab information about the\n\"object_type\". This will be used for the bundle-uri client in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n fetch-pack.c | 25 ++++++++++++++++++-------\n 1 file changed, 18 insertions(+), 7 deletions(-)\n\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 87657907e78..a0558f70b0c 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -115,11 +115,12 @@ static void for_each_cached_alternate(struct fetch_negotiator *negotiator,\n \t\tcb(negotiator, cache.items[i]);\n }\n \n-static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n-\t\t\t\t\t       int mark_tags_complete)\n+static struct commit *deref_without_lazy_fetch_extended(const struct object_id *oid,\n+\t\t\t\t\t\t\tint mark_tags_complete,\n+\t\t\t\t\t\t\tenum object_type *type,\n+\t\t\t\t\t\t\tunsigned int oi_flags)\n {\n-\tenum object_type type;\n-\tstruct object_info info = { .typep = &type };\n+\tstruct object_info info = { .typep = type };\n \tstruct commit *commit;\n \n \tcommit = lookup_commit_in_graph(the_repository, oid);\n@@ -128,9 +129,9 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n \n \twhile (1) {\n \t\tif (oid_object_info_extended(the_repository, oid, &info,\n-\t\t\t\t\t     OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK))\n+\t\t\t\t\t     oi_flags))\n \t\t\treturn NULL;\n-\t\tif (type == OBJ_TAG) {\n+\t\tif (*type == OBJ_TAG) {\n \t\t\tstruct tag *tag = (struct tag *)\n \t\t\t\tparse_object(the_repository, oid);\n \n@@ -144,7 +145,7 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n \t\t}\n \t}\n \n-\tif (type == OBJ_COMMIT) {\n+\tif (*type == OBJ_COMMIT) {\n \t\tstruct commit *commit = lookup_commit(the_repository, oid);\n \t\tif (!commit || repo_parse_commit(the_repository, commit))\n \t\t\treturn NULL;\n@@ -154,6 +155,16 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n \treturn NULL;\n }\n \n+\n+static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n+\t\t\t\t\t       int mark_tags_complete)\n+{\n+\tenum object_type type;\n+\tunsigned flags = OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK;\n+\treturn deref_without_lazy_fetch_extended(oid, mark_tags_complete,\n+\t\t\t\t\t\t &type, flags);\n+}\n+\n static int rev_list_insert_ref(struct fetch_negotiator *negotiator,\n \t\t\t       const struct object_id *oid)\n {\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451148","messageId":"RFC-patch-v2-10.13-31a22eb3bd4-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 10/13] fetch-pack: move --keep=* option filling to a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:22Z","receivedAt":"2022-03-11T16:25:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Move the populating of the --keep=* option argument to \"index-pack\" to\na static function, a subsequent commit will make use of it in another\nfunction.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n fetch-pack.c | 20 ++++++++++++--------\n 1 file changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex a0558f70b0c..0010867e5f5 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -842,6 +842,16 @@ static void parse_gitmodules_oids(int fd, struct oidset *gitmodules_oids)\n \t} while (1);\n }\n \n+static void add_index_pack_keep_option(struct strvec *args)\n+{\n+\tchar hostname[HOST_NAME_MAX + 1];\n+\n+\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\tstrvec_pushf(args, \"--keep=fetch-pack %\"PRIuMAX \" on %s\",\n+\t\t     (uintmax_t)getpid(), hostname);\n+}\n+\n /*\n  * If packfile URIs were provided, pass a non-NULL pointer to index_pack_args.\n  * The strings to pass as the --index-pack-arg arguments to http-fetch will be\n@@ -911,14 +921,8 @@ static int get_pack(struct fetch_pack_args *args,\n \t\t\tstrvec_push(&cmd.args, \"-v\");\n \t\tif (args->use_thin_pack)\n \t\t\tstrvec_push(&cmd.args, \"--fix-thin\");\n-\t\tif ((do_keep || index_pack_args) && (args->lock_pack || unpack_limit)) {\n-\t\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\t\tstrvec_pushf(&cmd.args,\n-\t\t\t\t     \"--keep=fetch-pack %\"PRIuMAX \" on %s\",\n-\t\t\t\t     (uintmax_t)getpid(), hostname);\n-\t\t}\n+\t\tif ((do_keep || index_pack_args) && (args->lock_pack || unpack_limit))\n+\t\t\tadd_index_pack_keep_option(&cmd.args);\n \t\tif (!index_pack_args && args->check_self_contained_and_connected)\n \t\t\tstrvec_push(&cmd.args, \"--check-self-contained-and-connected\");\n \t\telse\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451149","messageId":"RFC-patch-v2-13.13-40f37c8b9d5-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 13/13] bundle-uri: make the download program configurable","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:25Z","receivedAt":"2022-03-11T16:25:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in a preceding commit we really should be using libcurl's C\nAPI by default in get_bundle_uri(), but testing with a command-line\nprogram can be very handy, and useful e.g. to implement custom or\nad-hoc caching.\n\nE.g. using part of the recipe noted in a preceding commit to create\nthe \"git-master-only.bdl\" and \"git-master-to-next.bdl\" files, we can\nimplement a trivial caching shellscript as:\n\n\tcat >get-bundle.sh <<-\\EOF &&\n\t#!/bin/sh\n\tset -xe\n\n\turi=\"$1\"\n\n\tbundle_cache_key () {\n\t\techo \"Computing cache key for URI '$1' (only getting the header)\" >&2\n\n\t\tcurl --silent --output - -- \"$1\" |\n\t\tsed -n -e '/^$/q' -e 'p' |\n\t\tgit hash-object --stdin\n\t}\n\n\tget_cached_bundle_uri() {\n\t\tcache_key=$(bundle_cache_key \"$1\")\n\n\t\tpath=\"/tmp/bundle-cache-$cache_key.bdl\"\n\n\t\tif test -e \"$path\"\n\t\tthen\n\t\t\techo \"Using cache '$path' for URI '$1'\" >&2\n\t\t\tcat \"$path\"\n\t\telse\n\t\t\techo \"Downloading bundle URI $1\" >&2\n\t\t\tcurl --silent --output - -- \"$uri\" | tee \"$path\"\n\t\tfi\n\t}\n\n\tget_cached_bundle_uri \"$1\"\n\tEOF\n\tchmod +x get-bundle.sh &&\n\trm -rf /tmp/git.git &&\n\t./git \\\n\t\t-c protocol.version=2 \\\n\t\t-c fetch.uriProtocols=file \\\n\t\t-c transfer.bundleURI.downloader=./get-bundle.sh \\\n\t\t-c transfer.injectBundleURI=\"file:///tmp/git-master-only.bdl\" \\\n\t\t-c transfer.injectBundleURI=\"file:///tmp/git-master-to-next.bdl\" \\\n\t\tclone --bare --no-tags --single-branch --branch next --template= \\\n\t\t--verbose --verbose \\\n\t\thttps://github.com/git/git.git /tmp/git.git\n\nNow, clearly that specific example is rather pointless. We're getting\na local file anyway, so \"cat\"-ing another local file doesn't make any\ndifference, it's even slightly slower & more redundant as we're having\nto get it twice with \"curl\".\n\nBut the point is that this can be trivially improved for use in any\narbitrary custom caching strategy. E.g.:\n\n * A less dumber implementation that would stream the remote URL,\n   check the header as we go, and disconnect if we've got that content\n   locally.\n * Ditto, but using an ETag or other strategy.\n * N boxes could share a cache an NFS with a shared mount, or N\n   disconnected git processes could use a common cache without the\n   need for a front-line HTTP proxy server.\n\n * It would be trivial to extend this to guard against a \"thundering\n   herd\" (e.g. concurrent CI) downloading the same bundle N times. As\n   soon as we'd get the header we'd create a $cache_key.lock as we\n   download the rest, and other concurrent clients spotting that would\n   wait, then eventually cache \"$cache_key\".\n\n   Still racy as N clients could download the header in parallel, but\n   way less so (the header will be a tiny part of the payload).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/transfer.txt | 7 +++++++\n fetch-pack.c                      | 6 ++++++\n 2 files changed, 13 insertions(+)\n\ndiff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt\nindex ae85ca5760b..5310cd96cb9 100644\n--- a/Documentation/config/transfer.txt\n+++ b/Documentation/config/transfer.txt\n@@ -84,6 +84,13 @@ transfer.bundleURI::\n \tusing bundles to bootstap is possible. Defaults to `true`,\n \ti.e. bundle-uri is tried whenever a server offers it.\n \n+transfer.bundleURI.downloader::\n+\tWhen set to `<program>` will be invoked when\n+\t`transfer.bundleURI` is in effect to download URIs containing\n+\tbundles. Expected to take one `URI` as an argument, and to\n+\temit the bundle on STDOUT. Defaults to \"curl --silent --output\n+\t- --\". I.e. we'll invoke \"curl --silent --output - -- <URI>\".\n+\n transfer.injectBundleURI::\n \tAllows for the injection of `bundle-uri` lines into the\n \tprotocol v2 transport dialog (see `protocol.version` in\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 4f1a7acb20d..6e22605f06c 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1116,12 +1116,18 @@ static int get_bundle_uri(struct string_list_item *item, unsigned int nth,\n \tconst char *uri = item->string;\n \tFILE *out;\n \tint out_fd;\n+\tconst char *tmp;\n \n \tstrvec_push(&cmd.args, \"curl\");\n \tstrvec_push(&cmd.args, \"--silent\");\n \tstrvec_push(&cmd.args, \"--output\");\n \tstrvec_push(&cmd.args, \"-\");\n \tstrvec_push(&cmd.args, \"--\");\n+\tif (!git_config_get_string_tmp(\"transfer.bundleURI.downloader\", &tmp)) {\n+\t\tstrvec_clear(&cmd.args);\n+\t\tstrvec_push(&cmd.args, tmp);\n+\t\tcmd.use_shell = 1;\n+\t}\n \tstrvec_push(&cmd.args, item->string);\n \tcmd.git_cmd = 0;\n \tcmd.no_stdin = 1;\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451150","messageId":"RFC-patch-v2-11.13-5ade9419454-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 11/13] bundle.h: make \"fd\" version of read_bundle_header() public","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:23Z","receivedAt":"2022-03-11T16:25:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change the parse_bundle_header() function to be non-static, and rename\nit to parse_bundle_header_fd(). The parse_bundle_header() function is\nalready public, and it's a thin wrapper around this function. This\nwill be used by code that wants to pass a fd to the bundle API.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n bundle.c | 8 ++++----\n bundle.h | 2 ++\n 2 files changed, 6 insertions(+), 4 deletions(-)\n\ndiff --git a/bundle.c b/bundle.c\nindex a0bb687b0f4..bfe32f543a6 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -61,8 +61,8 @@ static int parse_bundle_signature(struct bundle_header *header, const char *line\n \treturn -1;\n }\n \n-static int parse_bundle_header(int fd, struct bundle_header *header,\n-\t\t\t       const char *report_path)\n+int read_bundle_header_fd(int fd, struct bundle_header *header,\n+\t\t\t  const char *report_path)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint status = 0;\n@@ -138,7 +138,7 @@ int read_bundle_header(const char *path, struct bundle_header *header)\n \n \tif (fd < 0)\n \t\treturn error(_(\"could not open '%s'\"), path);\n-\treturn parse_bundle_header(fd, header, path);\n+\treturn read_bundle_header_fd(fd, header, path);\n }\n \n int is_bundle(const char *path, int quiet)\n@@ -148,7 +148,7 @@ int is_bundle(const char *path, int quiet)\n \n \tif (fd < 0)\n \t\treturn 0;\n-\tfd = parse_bundle_header(fd, &header, quiet ? NULL : path);\n+\tfd = read_bundle_header_fd(fd, &header, quiet ? NULL : path);\n \tif (fd >= 0)\n \t\tclose(fd);\n \tbundle_header_release(&header);\ndiff --git a/bundle.h b/bundle.h\nindex 06009fe6b1f..2893defbc33 100644\n--- a/bundle.h\n+++ b/bundle.h\n@@ -22,6 +22,8 @@ void bundle_header_release(struct bundle_header *header);\n \n int is_bundle(const char *path, int quiet);\n int read_bundle_header(const char *path, struct bundle_header *header);\n+int read_bundle_header_fd(int fd, struct bundle_header *header,\n+\t\t\t  const char *report_path);\n int create_bundle(struct repository *r, const char *path,\n \t\t  int argc, const char **argv, struct strvec *pack_options,\n \t\t  int version);\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451151","messageId":"RFC-patch-v2-12.13-bb0d681f5f0-20220311T155841Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 12/13] bundle-uri client: support for bundle-uri with \"clone\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-03-11T16:24:24Z","receivedAt":"2022-03-11T16:25:34Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"In an earlier commit (\"bundle-uri client: add minimal NOOP client\") a\ntransport_get_remote_bundle_uri() call was added to builtin/clone.c to\nget any advertised bundle URIs from the server during cloning, but\nnothing was being done with them yet.\n\nThis implements real support for bundle-uri during the \"clone\"\nphase. It's not used at all by \"fetch\", but the code to support it is\nmostly here already, and will be finished later.\n\nUsing the new transfer.injectBundleURI support it's easy to test this\nmethod of cloning on a live server that doesn't support bundle-uri. In\na git.git checkout.\n\nFirst let's prepare two bundles:\n\n    git bundle create /tmp/git-master-only.bdl origin/master\n    git bundle create /tmp/git-master-to-next.bdl origin/master..origin/next\n\nAnd next, let's do a \"fake\" clone where we bootstrap from these\nbundles. The fetch.uriProtocols is needed because we'd otherwise\nignore \"file://\" URIs. This uses --no-tags --single-branch for\nsimplicity:\n\n    rm -rf /tmp/git.git &&\n    git \\\n\t-c protocol.version=2 \\\n        -c fetch.uriProtocols=file \\\n        -c transfer.injectBundleURI=\"file:///tmp/git-master-only.bdl\" \\\n\t-c transfer.injectBundleURI=\"file:///tmp/git-master-to-next.bdl\" \\\n\tclone --bare --no-tags --single-branch --branch next --template= \\\n\t--verbose --verbose \\\n\thttps://github.com/git/git.git /tmp/git.git\n\nWe'll then get output like:\n\n    Receiving bundle (1/2): 100% (300529/300529), 87.57 MiB | 32.70 MiB/s, done.\n    Resolving deltas: 100% (226765/226765), done.\n    have eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687 commit via bundle-uri\n    Receiving bundle (2/2): 100% (725/725), 221.11 KiB | 22.11 MiB/s, done.\n    Resolving deltas: 100% (539/539), completed with 153 local objects.\n    have e1b32706d8dd5db1dc2e13f8e391651214f1d987 commit via bundle-uri\n    Marking e1b32706d8dd5db1dc2e13f8e391651214f1d987 as complete\n    already have e1b32706d8dd5db1dc2e13f8e391651214f1d987 (refs/heads/next)\n    Checking connectivity: 301210, done.\n\nI.e. we did an ls-refs on connection to the server, then retrieved the\nadvertised bundles (faked up via config in this case).\n\nWe then got all the data leading up to the current \"master\" from\nthere, and also the commit that's currently on \"next. In this case we\nfound that we didn't need to proceed further with the dialog.\n\nI.e. other than an ls-refs and the server waiting until we downloaded\nthe bundles, the server didn't need to do any work creating a PACK for\nus.\n\nIf we change \"--branch next\" into \"--branch seen\" in the above command\nwe'll get the same output at the start until the \"want\" line, then:\n\n    [...]\n    want 93021c12c9f91e0d750d3ca8750a62416f4ea81a (refs/heads/seen)\n    POST git-upload-pack (212 bytes)\n    remote: Enumerating objects: 2265, done.\n    remote: Counting objects: 100% (1576/1576), done.\n    remote: Compressing objects: 100% (233/233), done.\n    remote: Total 2265 (delta 1378), reused 1480 (delta 1341), pack-reused 689\n    Receiving objects: 100% (2265/2265), 2.17 MiB | 10.77 MiB/s, done.\n    Resolving deltas: 100% (1673/1673), completed with 339 local objects.\n    Checking connectivity: 303225, done.\n\nI.e. the server needed to send us an incremental update on top after\nwe'd unpacked the bundles, but this was a fairly minimal set of ~2k\nobjects. It didn't need to service a full clone.\n\nWe can see the savings on the server by setting up a local server at\nthe tip of \"next\":\n\n    rm -rf /tmp/git-server.git &&\n    git init --bare /tmp/git-server.git &&\n    git -C /tmp/git-server.git bundle unbundle /tmp/git-master-only.bdl &&\n    git -C /tmp/git-server.git bundle unbundle /tmp/git-master-to-next.bdl\n    git -C /tmp/git-server.git update-ref refs/heads/master $(git ls-remote /tmp/git-master-only.bdl | cut -f 1) &&\n    git -C /tmp/git-server.git update-ref refs/heads/next $(git ls-remote /tmp/git-master-to-next.bdl | cut -f 1) &&\n    git -C /tmp/git-server.git for-each-ref\n\nLet's then clone from it, and record the time we spend.\n\n    rm -rf /tmp/git.git /tmp/{client,server}.time &&\n    /usr/bin/time -o /tmp/client.time -v git \\\n\t-c protocol.version=2 \\\n        -c fetch.uriProtocols=file \\\n        -c transfer.injectBundleURI=\"file:///tmp/git-master-only.bdl\" \\\n\tclone \\\n\t--upload-pack '/usr/bin/time -o /tmp/server.time -v git-upload-pack' \\\n\t--bare --no-tags --single-branch --branch next --template= \\\n\t--verbose --verbose \\\n\tfile:///tmp/git-server.git /tmp/git.git &&\n    for i in client server\n    do\n        echo $i: &&\n        grep -e seconds -e wall -e Maximum -e context /tmp/$i.time\n    done\n\nThis gives us something like these results:\n\n    client:\n        User time (seconds): 46.34\n        System time (seconds): 0.67\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.67\n        Maximum resident set size (kbytes): 207096\n        Voluntary context switches: 116058\n        Involuntary context switches: 220\n    server:\n        User time (seconds): 0.13\n        System time (seconds): 0.00\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:14.08\n        Maximum resident set size (kbytes): 53168\n        Voluntary context switches: 255\n        Involuntary context switches: 7\n\nWhereas doing a normal \"clone\" (by e.g. adding \"-c\ntransfer.bundleURI=false\" to the above) will give something like:\n\n    client:\n        User time (seconds): 47.24\n        System time (seconds): 0.92\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.55\n        Maximum resident set size (kbytes): 288104\n        Voluntary context switches: 136350\n        Involuntary context switches: 296\n    server:\n        User time (seconds): 5.73\n        System time (seconds): 0.24\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:07.45\n        Maximum resident set size (kbytes): 288104\n        Voluntary context switches: 26568\n        Involuntary context switches: 111\n\nI.e. we can see that the win on the client in this case is negative,\nbut we use around over 2% of the CPU time on the server, and around\n20% of the memory. The client-visible time is a bit slower, by around\n2%.\n\nIn practice I think this will be more of a win-win. These results are\non an unloaded local machine, and don't account for the benefit of the\nserver being more likely to have a network-local version of most of\nthe repository via dumb CDNs.\n\nReal servers are also usually in a messier state of having various\nloose objects and more fragmented pack collections, and needing to\nspend CPU to assemble these. Frequent repacking and e.g. local caching\ne.g. via the uploadpack.packObjectsHook helps, but using this should\nmake it more accessible to run a highly performance git server.\n\nThis feature also makes things like resumable clones rather trivial to\nimplement, this approach was discussed in the past[1] as a means to\nget that feature.\n\n1. https://lore.kernel.org/git/20111110074330.GA27925@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n fetch-pack.c                          | 255 ++++++++++++++++++++++++++\n fetch-pack.h                          |   6 +\n t/lib-t5730-protocol-v2-bundle-uri.sh | 107 ++++++++++-\n transport.c                           |   1 +\n 4 files changed, 368 insertions(+), 1 deletion(-)\n\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 0010867e5f5..4f1a7acb20d 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -26,6 +26,7 @@\n #include \"commit-reach.h\"\n #include \"commit-graph.h\"\n #include \"sigchain.h\"\n+#include \"bundle.h\"\n \n static int transfer_unpack_limit = -1;\n static int fetch_unpack_limit = -1;\n@@ -1020,6 +1021,133 @@ static int get_pack(struct fetch_pack_args *args,\n \treturn 0;\n }\n \n+static int unbundle_bundle_uri(const char *bundle_uri, unsigned int nth,\n+\t\t\t       unsigned int total_nr, FILE *in, int in_fd,\n+\t\t\t       struct oid_array *bundle_oids,\n+\t\t\t       unsigned int use_thin_pack)\n+{\n+\tstruct child_process cmd = CHILD_PROCESS_INIT;\n+\tstruct bundle_header header = BUNDLE_HEADER_INIT;\n+\tint ret = 0;\n+\tstruct string_list_item *item;\n+\tstruct strbuf progress_title = STRBUF_INIT;\n+\tint code;\n+\n+\tret = read_bundle_header_fd(in_fd, &header, bundle_uri);\n+\tif (ret < 0) {\n+\t\tret = error(\"could not read_bundle_header(%s)\", bundle_uri);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tfor_each_string_list_item(item, &header.references) {\n+\t\t/*\n+\t\t * The bundle's idea of the ref name is\n+\t\t * item->string.\n+\t\t *\n+\t\t * Here's where we could do concurrent negotiation\n+\t\t * with the server (and possibly start the fetch!)\n+\t\t * before or while we unpack the bundle with\n+\t\t * index-pack.\n+\t\t *\n+\t\t * The negotiator would need a small change to trust\n+\t\t * arbitrary OIDs instead of assuming it has existing\n+\t\t * in-repo \"struct commit *\", but ad-hoc testing\n+\t\t * reveals that it'll work & speed up the fetch even\n+\t\t * more, as we could proceed in parallel with the full\n+\t\t * bundle fetching as soon as we get the headers.\n+\t\t */\n+\t\tstruct object_id *oid = item->util;\n+\n+\t\toid_array_append(bundle_oids, oid);\n+\t}\n+\n+\tif (git_env_bool(\"GIT_TEST_BUNDLE_URI_FAIL_UNBUNDLE\", 0))\n+\t\tlseek(in_fd, 0, SEEK_SET);\n+\n+\tstrbuf_addf(&progress_title, \"Receiving bundle (%d/%d)\", nth, total_nr);\n+\tstrvec_pushl(&cmd.args, \"index-pack\", \"--stdin\", \"-v\",\n+\t\t     \"--progress-title\", progress_title.buf, NULL);\n+\n+\tif (header.prerequisites.nr && use_thin_pack)\n+\t\tstrvec_push(&cmd.args, \"--fix-thin\");\n+\tstrvec_push(&cmd.args, \"--check-self-contained-and-connected\");\n+\tadd_index_pack_keep_option(&cmd.args);\n+\n+\tcmd.git_cmd = 1;\n+\tcmd.in = in_fd;\n+\tcmd.no_stdout = 1;\n+\tcmd.git_cmd = 1;\n+\n+\tif (start_command(&cmd)) {\n+\t\tret = error(_(\"fetch-pack: unable to spawn index-pack\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\tcode = finish_command(&cmd);\n+\n+\tif (header.prerequisites.nr && code == 1)\n+\t\t/*\n+\t\t * index-pack returns -1 on\n+\t\t * --check-self-contained-and-connected to indicate\n+\t\t * that the pack was indeed not self contained and\n+\t\t * connected. We know from the bundle header\n+\t\t * prerequisites.\n+\t\t */\n+\t\tcode = 0;\n+\n+\tif (code) {\n+\t\tret = error(_(\"fetch-pack: unable to finish index-pack, exited with %d\"), code);\n+\t\tgoto cleanup;\n+\t}\n+\n+cleanup:\n+\tstrbuf_release(&progress_title);\n+\tbundle_header_release(&header);\n+\treturn ret;\n+}\n+\n+static int get_bundle_uri(struct string_list_item *item, unsigned int nth,\n+\t\t\t  unsigned int total_nr, struct oid_array *bundle_oids,\n+\t\t\t  unsigned int use_thin_pack)\n+{\n+\tstruct child_process cmd = CHILD_PROCESS_INIT;\n+\tstruct strbuf tempfile = STRBUF_INIT;\n+\tint ret = 0;\n+\tconst char *uri = item->string;\n+\tFILE *out;\n+\tint out_fd;\n+\n+\tstrvec_push(&cmd.args, \"curl\");\n+\tstrvec_push(&cmd.args, \"--silent\");\n+\tstrvec_push(&cmd.args, \"--output\");\n+\tstrvec_push(&cmd.args, \"-\");\n+\tstrvec_push(&cmd.args, \"--\");\n+\tstrvec_push(&cmd.args, item->string);\n+\tcmd.git_cmd = 0;\n+\tcmd.no_stdin = 1;\n+\tcmd.out = -1;\n+\n+\tif (start_command(&cmd)) {\n+\t\tret = error(\"fetch-pack: unable to spawn http-fetch\");\n+\t\tgoto cleanup;\n+\t}\n+\n+\tout = xfdopen(cmd.out, \"r\");\n+\tout_fd = fileno(out);\n+\tret = unbundle_bundle_uri(uri, nth, total_nr, out, out_fd,\n+\t\t\t\t  bundle_oids, use_thin_pack);\n+\n+\tif (finish_command(&cmd)) {\n+\t\tret = error(\"fetch-pack: unable to finish http-fetch\");\n+\t\tgoto cleanup;\n+\t}\n+\n+cleanup:\n+\tstrbuf_release(&tempfile);\n+\n+\treturn ret;\n+}\n+\n static int cmp_ref_by_name(const void *a_, const void *b_)\n {\n \tconst struct ref *a = *((const struct ref **)a_);\n@@ -1577,6 +1705,130 @@ static void do_check_stateless_delimiter(int stateless_rpc,\n \t\t\t\t  _(\"git fetch-pack: expected response end packet\"));\n }\n \n+static int get_bundle_uri_add_known_common(struct string_list_item *item,\n+\t\t\t\t\t   unsigned int nth, unsigned int total_nr,\n+\t\t\t\t\t   struct fetch_negotiator *negotiator,\n+\t\t\t\t\t   struct fetch_pack_args *args,\n+\t\t\t\t\t   unsigned int use_thin_pack)\n+{\n+\tint i;\n+\tstruct oid_array bundle_oids = OID_ARRAY_INIT;\n+\n+\t/*\n+\t * We don't use OBJECT_INFO_QUICK here unlike in the rest of\n+\t * the fetch routines, that's because the rest of them don't\n+\t * need to consider a commit object that's just been\n+\t * downloaded for further negotiation, but bundle-uri does for\n+\t * adding newly downloaded OIDs to the negotiator.\n+\t */\n+\tunsigned oi_flags = OBJECT_INFO_SKIP_FETCH_OBJECT;\n+\n+\tif (get_bundle_uri(item, nth, total_nr, &bundle_oids, use_thin_pack) < 0)\n+\t\treturn error(_(\"could not get the bundle URI #%d\"), nth);\n+\n+\tfor (i = 0; i < bundle_oids.nr; i++) {\n+\t\tstruct object_id *oid = &bundle_oids.oid[i];\n+\t\tenum object_type type = OBJ_NONE;\n+\t\tstruct commit *c = deref_without_lazy_fetch_extended(oid, 0,\n+\t\t\t\t\t\t\t\t     &type,\n+\t\t\t\t\t\t\t\t     oi_flags);\n+\t\tif (!c) {\n+\t\t\tif (type == OBJ_BLOB || type == OBJ_TREE) {\n+\t\t\t\tprint_verbose(args, \"have %s %s via bundle-uri (ignoring due to type)\",\n+\t\t\t\t\t      oid_to_hex(oid), type_name(type));\n+\t\t\t\tcontinue;\n+\t\t\t} else if (type) {\n+\t\t\t\t/*\n+\t\t\t\t * OBJ_TAG should have been peeled,\n+\t\t\t\t * and OBJ_COMMIT should have a\n+\t\t\t\t * non-NULL \"c\".\n+\t\t\t\t *\n+\t\t\t\t * Should be a BUG() if we were not\n+\t\t\t\t * bending over backwards to make\n+\t\t\t\t * bundle-uri soft-fail.\n+\t\t\t\t */\n+\t\t\t\treturn error(_(\"bundle-uri says it has %s, got it at unexpected type %s\"),\n+\t\t\t\t\t     oid_to_hex(oid), type_name(type));\n+\t\t\t}\n+\t\t}\n+\n+\t\tprint_verbose(args, \"have %s %s via bundle-uri\",\n+\t\t\t      oid_to_hex(oid), type_name(type));\n+\n+\t\tnegotiator->known_common(negotiator, c);\n+\t\tmark_complete(oid);\n+\t}\n+\treturn 0;\n+}\n+\n+static void do_fetch_pack_v2_bundle_uri(struct fetch_pack_args *args,\n+\t\t\t\t\tstruct string_list  *bundle_uri,\n+\t\t\t\t\tstruct fetch_negotiator *negotiator)\n+{\n+\tstruct string_list_item *item;\n+\tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tstruct string_list default_protocols = STRING_LIST_INIT_NODUP;\n+\tstruct string_list *ok_protocols;\n+\n+\tif (!bundle_uri)\n+\t\treturn;\n+\n+\tif (!bundle_uri->nr)\n+\t\treturn;\n+\n+\tif (uri_protocols.nr) {\n+\t\tok_protocols = &uri_protocols;\n+\t} else {\n+\t\tstring_list_append(&default_protocols, \"http\");\n+\t\tstring_list_append(&default_protocols, \"https\");\n+\t\tok_protocols = &default_protocols;\n+\t}\n+\n+\tfor_each_string_list_item(item, bundle_uri) {\n+\t\tconst char *uri = item->string;\n+\t\tint protocol_ok = 0;\n+\t\tstruct string_list_item *item2;\n+\n+\t\tfor_each_string_list_item(item2, ok_protocols) {\n+\t\t\tconst char *s = item2->string;\n+\t\t\tconst char *p;\n+\n+\t\t\tif (skip_prefix(item->string, s, &p) &&\n+\t\t\t    starts_with(p, \"://\")) {\n+\t\t\t\tprotocol_ok = 1;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!protocol_ok) {\n+\t\t\tprint_verbose(args, \"skipping bundle-uri not on protocol whitelist: %s\",\n+\t\t\t\t      item->string);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tstring_list_append(&list, uri)->util = item->util;\n+\t}\n+\n+\tif (list.nr) {\n+\t\tint i;\n+\t\tunsigned int total_nr = list.nr;\n+\n+\t\ttrace2_region_enter(\"fetch-pack\", \"bundle-uri\", the_repository);\n+\t\tfor (i = 0; i < total_nr; i++) {\n+\t\t\tstruct string_list_item item = list.items[i];\n+\t\t\tunsigned int nth = i + 1;\n+\n+\t\t\tget_bundle_uri_add_known_common(&item, nth, total_nr,\n+\t\t\t\t\t\t\tnegotiator, args,\n+\t\t\t\t\t\t\targs->use_thin_pack);\n+\t\t}\n+\t\ttrace2_region_leave(\"fetch-pack\", \"bundle-uri\", the_repository);\n+\t}\n+\n+\tstring_list_clear(&default_protocols, 0);;\n+}\n+\n+\n static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \t\t\t\t    int fd[2],\n \t\t\t\t    const struct ref *orig_ref,\n@@ -1600,10 +1852,13 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n \tint i;\n \tstruct strvec index_pack_args = STRVEC_INIT;\n+\tstruct string_list *bundle_uri = args->bundle_uri;\n \n \tnegotiator = &negotiator_alloc;\n \tfetch_negotiator_init(r, negotiator);\n \n+\tdo_fetch_pack_v2_bundle_uri(args, bundle_uri, negotiator);\n+\n \tpacket_reader_init(&reader, fd[0], NULL, 0,\n \t\t\t   PACKET_READ_CHOMP_NEWLINE |\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\ndiff --git a/fetch-pack.h b/fetch-pack.h\nindex 7f94a2a5831..b19fd7d93be 100644\n--- a/fetch-pack.h\n+++ b/fetch-pack.h\n@@ -24,6 +24,12 @@ struct fetch_pack_args {\n \t */\n \tconst struct oid_array *negotiation_tips;\n \n+\t/*\n+\t * A pointer to the already populated transport.bundle_uri\n+\t * struct.\n+\t */\n+\tstruct string_list *bundle_uri;\n+\n \tunsigned deepen_relative:1;\n \tunsigned quiet:1;\n \tunsigned keep_pack:1;\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nindex 3be47bacc5f..ab9b725f038 100644\n--- a/t/lib-t5730-protocol-v2-bundle-uri.sh\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -7,6 +7,8 @@ case \"$T5730_PROTOCOL\" in\n file)\n \tT5730_PARENT=file_parent\n \tT5730_URI=\"file://$PWD/file_parent\"\n+\tT5730_URI_BDL_PROTO=\"file://\"\n+\tT5730_URI_BDL=\"$T5730_URI_BDL_PROTO$PWD/file_parent\"\n \tT5730_BUNDLE_URI=\"$T5730_URI/fake.bdl\"\n \ttest_set_prereq T5730_FILE\n \t;;\n@@ -15,6 +17,8 @@ git)\n \tstart_git_daemon --export-all --enable=receive-pack\n \tT5730_PARENT=\"$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent\"\n \tT5730_URI=\"$GIT_DAEMON_URL/parent\"\n+\tT5730_URI_BDL_PROTO=\"file://\"\n+\tT5730_URI_BDL=\"$T5730_URI_BDL_PROTO$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent\"\n \tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n \ttest_set_prereq T5730_GIT\n \t;;\n@@ -24,6 +28,8 @@ http)\n \tT5730_PARENT=\"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\"\n \tT5730_URI=\"$HTTPD_URL/smart/http_parent\"\n \tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n+\tT5730_URI_BDL_PROTO=\"http://\"\n+\tT5730_URI_BDL=\"$HTTPD_URL/dumb/http_parent\"\n \ttest_set_prereq T5730_HTTP\n \t;;\n *)\n@@ -33,7 +39,20 @@ esac\n \n test_expect_success \"setup protocol v2 $T5730_PROTOCOL:// tests\" '\n \tgit init \"$T5730_PARENT\" &&\n-\ttest_commit -C \"$T5730_PARENT\" one\n+\ttest_commit -C \"$T5730_PARENT\" one &&\n+\ttest_commit -C \"$T5730_PARENT\" two &&\n+\ttest_commit -C \"$T5730_PARENT\" three &&\n+\ttest_commit -C \"$T5730_PARENT\" four &&\n+\ttest_commit -C \"$T5730_PARENT\" five &&\n+\ttest_commit -C \"$T5730_PARENT\" six &&\n+\n+\tmkdir \"$T5730_PARENT\"/bdl &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/1.bdl one &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/1-2.bdl one..two &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/2-3.bdl two..three &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/3-4.bdl three..four &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/4-5.bdl four..five &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/5-6.bdl five..six\n '\n \n # Poor man's URI escaping. Good enough for the test suite whose trash\n@@ -317,3 +336,89 @@ test_expect_success \"ls-remote-bundle-uri with bad -c transfer.injectBundleURI p\n \ttest_cmp err.expect err.actual &&\n \ttest_path_is_missing log\n '\n+\n+test_cmp_repo_refs() {\n+\tone=\"$1\"\n+\ttwo=\"$2\"\n+\tshift 2\n+\n+\tgit -C \"$one\" for-each-ref \"$@\" >expect &&\n+\tgit -C \"$two\" for-each-ref \"$@\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success \"clone with bundle-uri protocol v2 over $T5730_PROTOCOL:// 1.bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\n+\ttest_when_finished \"rm -rf log child\" &&\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tclone --verbose --verbose \\\n+\t\t\"$T5730_URI\" child >out 2>err &&\n+\tgrep -F \"Receiving bundle (1/1)\" err &&\n+\tgrep \"clone> want \" log &&\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n+'\n+\n+test_expect_success \"fetch with bundle-uri protocol v2 over $T5730_PROTOCOL:// 1.bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\n+\ttest_when_finished \"rm -rf log child\" &&\n+\tgit init --bare child &&\n+\tgit -C child remote add --mirror=fetch origin \"$T5730_URI\" &&\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit -C child \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tfetch --verbose --verbose >out 2>err &&\n+\t# Fetch is not supported yet\n+\t! grep -F \"Receiving bundle (1/1)\" err &&\n+\tgrep \"fetch> want \" log &&\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n+'\n+\n+test_expect_success \"clone with bundle-uri protocol v2 with $T5730_PROTOCOL:// 1 + 1-2 + [...].bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1-2.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/2-3.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/3-4.bdl | test_uri_escape)\" --add &&\n+\n+\ttest_when_finished \"rm -rf log child\" &&\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tclone --verbose --verbose \\\n+\t\t\"$T5730_URI\" child >out 2>err &&\n+\tgrep -F \"Receiving bundle (4/4)\" err &&\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags &&\n+\tgrep \"clone> want \" log\n+'\n+\n+test_expect_success \"clone with bundle-uri protocol v2 with $T5730_PROTOCOL:// ALL.bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1-2.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/2-3.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/3-4.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/4-5.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/5-6.bdl | test_uri_escape)\" --add &&\n+\n+\ttest_when_finished \"rm -rf log child\" &&\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tclone --verbose --verbose \\\n+\t\t\"$T5730_URI\" child >out 2>err &&\n+\tgrep -F \"Receiving bundle (6/6)\" err &&\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags &&\n+\t! grep \"clone> want \" log\n+'\ndiff --git a/transport.c b/transport.c\nindex 7085bfb3db8..ebc3ed9b608 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -426,6 +426,7 @@ static int fetch_refs_via_pack(struct transport *transport,\n \targs.server_options = transport->server_options;\n \targs.negotiation_tips = data->options.negotiation_tips;\n \targs.reject_shallow_remote = transport->smart_options->reject_shallow;\n+\targs.bundle_uri = &transport->bundle_uri;\n \n \tif (!data->got_remote_heads) {\n \t\tint i;\n-- \n2.35.1.1337.g7e32d794afe\n\n"},{"id":"451165","messageId":"d745488d-d70e-0b1d-ce08-ce276a9d4310@github.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"Re: [RFC PATCH v2 00/13] bundle-uri: a \"dumb CDN\" for git","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-03-11T21:28:17Z","receivedAt":"2022-03-11T22:43:06Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 3/11/2022 11:24 AM, Ævar Arnfjörð Bjarmason wrote:\n> Per recent discussion[1] this is my not-quite-feature-complete version\n> of the bundle-uri capability.  This was sent to the list in some form\n> beforfe in [2] and [3].\n> \n> Recently Derrick Stolee has sent an alternate implementation of some\n> of the same ideas in [4]. Per [1] we're planning to work together on\n> getting a version of this into git that makes everyone happy, sending\n> what I've got here is the first step in that.\n\nThanks! It's good to see your intended end-to-end for comparison\nbefore we start the combined effort. I look forward to the additional\ndetails coming next week, because there are a lot of optimizations in\nthere that will inform our direction.\n \n> A high-level summary of the important differences in my approach &\n> Derrick's (which I hope I'm summarizing fairly here) is that his\n> approach optionally adds a bundle TOC format, that format allows you\n> to define topology relationships between bundles to guide a\n> (returning) client in what it needs to fetch.\n> \n> Whereas the idea in this series is to lean entirely on the client\n> downloading bundles & inferring what needs to be done via the\n> tip/prereqs listed in the header format of the (existing, not changed\n> here) bundle format.\n> \n> Both have pros & cons, I started trying to summarize those, but let's\n> leave that for later.\n\nI know you were in a rush to deliver this, so I'm going to assume\nthat \"leave that for later\" was \"I didn't have time to write that\nhere\". The very high-level comparison that I gathered from our\nchat were these points:\n\n 1. My version uses the TOC and its \"timestamp\" heuristic to\n    minimize how much data the client needs to download on a \"no-op\"\n    fetch. Your version requires that the client downloads some\n    initial range of data from each advertised URI.\n\n 2. My version lets the TOC sit at one well-known URI that can be\n    advertised independently of the origin server. I don't see an\n    equivalent in yours (so far).\n\n 3. The TOC in my version allows the server advertisement be an \"OR\"\n    (download from any of these locations... some might be closer to\n    you than others) and yours is an \"AND\" (this is the list of\n    bundles... you probably need all of them for a full clone). This\n    difference is something that can be worked into the advertisement\n    to allow both modes, if that is a valuable mechanism.\n\nI'm also interested to see how you allow for someone to create their\nown local bundle server that is independent of the origin Git server\n(so the bundle-uri advertisement does not help them see the bundles).\nPerhaps that's just not part of your design, so will be part of the\ncombined effort.\n\n> There's also some high-level \"journey\" differences in the\n> two. E.g. Derrick implemented the ability to have \"git bundle\" itself\n> fetch bundles remotely, I don't have that and instead lean entirely on\n> the protocol and \"fetch\". Those differences really aren't important,\n> and we can have our cake & eat it too on that front. I.e. end up with\n> some sensible intersection (or union) of the tooling.\n\nI agree that this is something to work out later. I think it is nice\nto allow the user a way to download bundles from a specific URI if\nthey happen to have one, but that could easily be embedded into a\n'git fetch <bundle-URI>' kind of command. Please redesign this as you\nsee fit when combining efforts.\n\n> I ran out of time to finish up some of what I had on this topic this\n> week, but figured (especially since I'd promised to get it done this\n> week) to send what I have now for discussion.\n> \n> Things missing & reader's notes:\n> \n>  * I had some amendmends to the protocol I meant to distill further\n>    into the protocol docs at [5]. Basically omitting the ability to\n>    transmit key-values and to have it just be a list of URIs with an\n>    optional <header> for each one, which is purely a server-to-client\n>    aid (i.e. those headers will be what you'll find in the pointed-to\n>    bundles).\n\nAs long as early versions of the client can ignore the extra key-value\npairs advertised by later versions without issue, it makes sense to\navoid this in early versions.\n\nIt would be nice to delay the use of advertising these headers inline\nwith the advertisement until more of the idea is made concrete. In\nparticular, the more we can strip out things in early versions that\ncan be applied later as an optimization, the better. I'm thinking\nspecifically about how your incremental fetch story will download only\nthe headers of the bundles to discover which ones are necessary. That\ncan also be used in the TOC with a timestamp heuristic to discover\nthat the client already has all of the information from the latest\nbundle, even though the server timestamp advanced. Showing the value\nto that case _plus_ the \"AND\" case of bundle-uri advertisements\nwould be a nice justification for the complication involved there.\n\n> * This series goes up to \"clone\", but I also have incremental fetch\n>   patches. I ran into an (easily solvable bug) in that & thought it\n>   was best to omit it for now. It'll be here soon.\n> \n>   Basically for incremental fetch we'll in parallel get the headers\n>   for remote bundles, and then do an early abort of those downloads\n>   that we deem that we don't need.\n\nThis is the main thing I was missing in our earlier discussions\n(in August and October): this feature of downloading the headers for\nthe remote bundles is critical for allowing incremental fetch to\nwork in your model. It's a clever way to solve the problem.\n\nI'm interested to see how well it performs in real-world scenarios.\n\nI'm imagining a way to incrementally build things from simplest to\nmost complicated, and it goes in this order:\n\n 0. Implement 'git clone --bundle-uri=<X> <url>' where we expect\n    a bundle at the given uri <X>.\n\n 1. Implement 'git clone <url>' to understand a bundle-uri\n    advertisement with AND (get all bundles and unbundle them in\n    some order before fetching) and OR (get any _one_ of these\n    full bundles) logic.\n\n 2. Extend the bundle downloading to understand a TOC, allowing\n    the OR advertisement to advertise TOC (perhaps guarded with\n    some metadata in the advertisement).\n\n 3. For the TOC model, allow 'git fetch' to update from the TOC.\n\n 4. Extend the AND advertisements to do parallel header-only\n    downloads to integrate with 'git fetch'. The implementation\n    of these pieces also improve performance of the TOC model.\n\nThis is me just spitballing of a way that we can make incremental\nprogress towards this feature without needing to go super-deep\ninto one model or the other before we are able to test this\nexample against real-world bundle server prototypes.\n\n>   Clever (but all standard & boring) use of HTTP caching headers\n>   between client & servers then allows the client to not request the\n>   same thing again and again. I.e. want less server load on your CDN?\n>   Just have the bundles be unique URLs and set appropriate caching\n>   headers.\n\nI was hoping that the TOC model would avoid the need for cleverness\nat all, but I'm interested to see what we can do with these tricks\nin all cases.\n \n> * A problem with this implementation (and Derrick's, I believe) is\n>   that it keeps a server waiting while we twiddle our thumbs and wget\n>   (or equivalent) the bundle(s) locally. If you e.g. clone\n>   \"chromium.git\" the server will get tired of waiting, drop the\n>   connection, and unless the bundle is 100% up-to-date the \"clone\"\n>   will fail.\n\nThis is absolutely the case with my implementation. I call it out\nwith comments, but I didn't have a solution in mind other than\n\"disconnect if necessary, then reconnect.\"\n \n>   The solution to this is to get the bundle headers in parallel, and\n>   as soon as we've got them present the OIDs in the headers as \"HAVE\"\n>   to the server, which'll then send us an incremental PACK (and even\n>   possibly a packfile-uri) for the difference between those bundle(s)\n>   and what its tips are.\n> \n>   We can then simply disconnect, download/index-pack everything, and\n>   do a connectivity checkat the end.\n\nThis is a clever idea and is absolutely something that can be done\nas a later step (even after step 4 from my outline above).\n\n>   This requires some careful error handling, e.g. if the resulting\n>   repo isn't valid we'll need to retry, and the current negotiation\n>   API isn't very happy to negotiate on the basis of OIDs we don't have\n>   in the object store (but the protocol handles it just fine).\n\nThis is exactly why we shouldn't over-index on that idea too early,\nbut definitely keep it in our back pockets for a future improvement.\n\nThanks for the detailed cover letter. I hope to look more closely\nat the patches themselves for feedback early next week.\n\nThanks,\n-Stolee\n"},{"id":"453855","messageId":"RFC-patch-v2-01.36-95c53a3e779-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 01/36] connect.c: refactor sending of agent & object-format","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:18Z","receivedAt":"2022-04-18T17:24:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the sending of the \"agent\" and \"object-format\" capabilities\ninto a function.\n\nThis was added in its current form in ab67235bc4 (connect: parse v2\nrefs with correct hash algorithm, 2020-05-25). When we connect to a v2\nserver we need to know about its object-format, and it needs to know\nabout ours. Since most things in connect.c and transport.c piggy-back\non the eager getting of remote refs via the handshake() those commands\ncan make use of the just-sent-over object-format by ls-refs.\n\nBut I'm about to add a command that may come after ls-refs, and may\nnot, but we need the server to know about our user-agent and\nobject-format. So let's split this into a function.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n connect.c | 33 ++++++++++++++++++++-------------\n 1 file changed, 20 insertions(+), 13 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex afc79a6236e..e6d0b1d34bd 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -473,6 +473,24 @@ void check_stateless_delimiter(int stateless_rpc,\n \t\tdie(\"%s\", error);\n }\n \n+static void send_capabilities(int fd_out, struct packet_reader *reader)\n+{\n+\tconst char *hash_name;\n+\n+\tif (server_supports_v2(\"agent\", 0))\n+\t\tpacket_write_fmt(fd_out, \"agent=%s\", git_user_agent_sanitized());\n+\n+\tif (server_feature_v2(\"object-format\", &hash_name)) {\n+\t\tint hash_algo = hash_algo_by_name(hash_name);\n+\t\tif (hash_algo == GIT_HASH_UNKNOWN)\n+\t\t\tdie(_(\"unknown object format '%s' specified by server\"), hash_name);\n+\t\treader->hash_algo = &hash_algos[hash_algo];\n+\t\tpacket_write_fmt(fd_out, \"object-format=%s\", reader->hash_algo->name);\n+\t} else {\n+\t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1];\n+\t}\n+}\n+\n struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     struct ref **list, int for_push,\n \t\t\t     struct transport_ls_refs_options *transport_options,\n@@ -480,7 +498,6 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     int stateless_rpc)\n {\n \tint i;\n-\tconst char *hash_name;\n \tstruct strvec *ref_prefixes = transport_options ?\n \t\t&transport_options->ref_prefixes : NULL;\n \tconst char **unborn_head_target = transport_options ?\n@@ -490,18 +507,8 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \tif (server_supports_v2(\"ls-refs\", 1))\n \t\tpacket_write_fmt(fd_out, \"command=ls-refs\\n\");\n \n-\tif (server_supports_v2(\"agent\", 0))\n-\t\tpacket_write_fmt(fd_out, \"agent=%s\", git_user_agent_sanitized());\n-\n-\tif (server_feature_v2(\"object-format\", &hash_name)) {\n-\t\tint hash_algo = hash_algo_by_name(hash_name);\n-\t\tif (hash_algo == GIT_HASH_UNKNOWN)\n-\t\t\tdie(_(\"unknown object format '%s' specified by server\"), hash_name);\n-\t\treader->hash_algo = &hash_algos[hash_algo];\n-\t\tpacket_write_fmt(fd_out, \"object-format=%s\", reader->hash_algo->name);\n-\t} else {\n-\t\treader->hash_algo = &hash_algos[GIT_HASH_SHA1];\n-\t}\n+\t/* Send capabilities */\n+\tsend_capabilities(fd_out, reader);\n \n \tif (server_options && server_options->nr &&\n \t    server_supports_v2(\"server-option\", 1))\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453856","messageId":"RFC-patch-v2-02.36-8f6e4f12e8a-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 02/36] dir API: add a generalized path_match_flags() function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:19Z","receivedAt":"2022-04-18T17:24:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a path_match_flags() function and have the two sets of\nstarts_with_dot_{,dot_}slash() functions added in\n63e95beb085 (submodule: port resolve_relative_url from shell to C,\n2016-04-15) and a2b26ffb1a8 (fsck: convert gitmodules url to URL\npassed to curl, 2020-04-18) be thin wrappers for it.\n\nAs the latter of those notes the fsck version was copied from the\ninitial builtin/submodule--helper.c version.\n\nSince the code added in a2b26ffb1a8 was doing really doing the same as\nwin32_is_dir_sep() added in 1cadad6f658 (git clone <url>\nC:\\cygwin\\home\\USER\\repo' is working (again), 2018-12-15) let's move\nthe latter to git-compat-util.h is a is_xplatform_dir_sep(). We can\nthen call either it or the platform-specific is_dir_sep() from this\nnew function.\n\nLet's likewise change code in various other places that was hardcoding\nchecks for \"'/' || '\\\\'\" with the new is_xplatform_dir_sep(). As can\nbe seen in those callers some of them still concern themselves with\n':' (Mac OS classic?), but let's leave the question of whether that\nshould be consolidated for some other time.\n\nAs we expect to make wider use of the \"native\" case in the future,\ndefine and use two starts_with_dot_{,dot_}slash_native() convenience\nwrappers. This makes the diff in builtin/submodule--helper.c much\nsmaller.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/submodule--helper.c | 20 ++++++------\n compat/mingw.c              |  2 +-\n compat/win32/path-utils.h   |  6 +---\n dir.c                       | 29 +++++++++++++++++\n dir.h                       | 63 +++++++++++++++++++++++++++++++++++++\n fsck.c                      | 23 ++++----------\n git-compat-util.h           |  8 ++++-\n path.c                      |  2 +-\n submodule-config.c          |  6 ++--\n 9 files changed, 121 insertions(+), 38 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2c87ef9364f..b68102bb3ed 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -72,16 +72,6 @@ static char *get_default_remote(void)\n \treturn repo_get_default_remote(the_repository);\n }\n \n-static int starts_with_dot_slash(const char *str)\n-{\n-\treturn str[0] == '.' && is_dir_sep(str[1]);\n-}\n-\n-static int starts_with_dot_dot_slash(const char *str)\n-{\n-\treturn str[0] == '.' && str[1] == '.' && is_dir_sep(str[2]);\n-}\n-\n /*\n  * Returns 1 if it was the last chop before ':'.\n  */\n@@ -108,6 +98,16 @@ static int chop_last_dir(char **remoteurl, int is_relative)\n \treturn 0;\n }\n \n+static int starts_with_dot_slash(const char *const path)\n+{\n+\treturn starts_with_dot_slash_native(path);;\n+}\n+\n+static int starts_with_dot_dot_slash(const char *const path)\n+{\n+\treturn starts_with_dot_dot_slash_native(path);\n+}\n+\n /*\n  * The `url` argument is the URL that navigates to the submodule origin\n  * repo. When relative, this URL is relative to the superproject origin\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 6fe80fdf014..b94b473d978 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2830,7 +2830,7 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)\n \t\t\t}\n \n \t\t\tc = path[i];\n-\t\t\tif (c && c != '.' && c != ':' && c != '/' && c != '\\\\')\n+\t\t\tif (c && c != '.' && c != ':' && !is_xplatform_dir_sep(c))\n \t\t\t\tgoto not_a_reserved_name;\n \n \t\t\t/* contains reserved name */\ndiff --git a/compat/win32/path-utils.h b/compat/win32/path-utils.h\nindex bba2b644080..65fa3b9263a 100644\n--- a/compat/win32/path-utils.h\n+++ b/compat/win32/path-utils.h\n@@ -6,11 +6,7 @@ int win32_has_dos_drive_prefix(const char *path);\n \n int win32_skip_dos_drive_prefix(char **path);\n #define skip_dos_drive_prefix win32_skip_dos_drive_prefix\n-static inline int win32_is_dir_sep(int c)\n-{\n-\treturn c == '/' || c == '\\\\';\n-}\n-#define is_dir_sep win32_is_dir_sep\n+#define is_dir_sep is_xplatform_dir_sep\n static inline char *win32_find_last_dir_sep(const char *path)\n {\n \tchar *ret = NULL;\ndiff --git a/dir.c b/dir.c\nindex f2b0f242101..d25aa6ade55 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -3890,3 +3890,32 @@ void relocate_gitdir(const char *path, const char *old_git_dir, const char *new_\n \n \tconnect_work_tree_and_git_dir(path, new_git_dir, 0);\n }\n+\n+int path_match_flags(const char *const str, const enum path_match_flags flags)\n+{\n+\tconst char *p = str;\n+\n+\tif (flags & PATH_MATCH_NATIVE &&\n+\t    flags & PATH_MATCH_XPLATFORM)\n+\t\tBUG(\"path_match_flags() must get one match kind, not multiple!\");\n+\telse if (!(flags & PATH_MATCH_KINDS_MASK))\n+\t\tBUG(\"path_match_flags() must get at least one match kind!\");\n+\n+\tif (flags & PATH_MATCH_STARTS_WITH_DOT_SLASH &&\n+\t    flags & PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH)\n+\t\tBUG(\"path_match_flags() must get one platform kind, not multiple!\");\n+\telse if (!(flags & PATH_MATCH_PLATFORM_MASK))\n+\t\tBUG(\"path_match_flags() must get at least one platform kind!\");\n+\n+\tif (*p++ != '.')\n+\t\treturn 0;\n+\tif (flags & PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH &&\n+\t    *p++ != '.')\n+\t\treturn 0;\n+\n+\tif (flags & PATH_MATCH_NATIVE)\n+\t\treturn is_dir_sep(*p);\n+\telse if (flags & PATH_MATCH_XPLATFORM)\n+\t\treturn is_xplatform_dir_sep(*p);\n+\tBUG(\"unreachable\");\n+}\ndiff --git a/dir.h b/dir.h\nindex 8e02dfb505d..7bc862030cf 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -578,4 +578,67 @@ void connect_work_tree_and_git_dir(const char *work_tree,\n void relocate_gitdir(const char *path,\n \t\t     const char *old_git_dir,\n \t\t     const char *new_git_dir);\n+\n+/**\n+ * The \"enum path_matches_kind\" determines how path_match_flags() will\n+ * behave. The flags come in sets, and one (and only one) must be\n+ * provided out of each \"set\":\n+ *\n+ * PATH_MATCH_NATIVE:\n+ *\tPath separator is is_dir_sep()\n+ * PATH_MATCH_XPLATFORM:\n+ *\tPath separator is is_xplatform_dir_sep()\n+ *\n+ * Do we use is_dir_sep() to check for a directory separator\n+ * (*_NATIVE), or do we always check for '/' or '\\' (*_XPLATFORM). The\n+ * \"*_NATIVE\" version on Windows is the same as \"*_XPLATFORM\",\n+ * everywhere else \"*_NATIVE\" means \"only /\".\n+ *\n+ * PATH_MATCH_STARTS_WITH_DOT_SLASH:\n+ *\tMatch a path starting with \"./\"\n+ * PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH:\n+ *\tMatch a path starting with \"../\"\n+ *\n+ * The \"/\" in the above is adjusted based on the \"*_NATIVE\" and\n+ * \"*_XPLATFORM\" flags.\n+ */\n+enum path_match_flags {\n+\tPATH_MATCH_NATIVE = 1 << 0,\n+\tPATH_MATCH_XPLATFORM = 1 << 1,\n+\tPATH_MATCH_STARTS_WITH_DOT_SLASH = 1 << 2,\n+\tPATH_MATCH_STARTS_WITH_DOT_DOT_SLASH = 1 << 3,\n+};\n+#define PATH_MATCH_KINDS_MASK (PATH_MATCH_STARTS_WITH_DOT_SLASH | \\\n+\tPATH_MATCH_STARTS_WITH_DOT_DOT_SLASH)\n+#define PATH_MATCH_PLATFORM_MASK (PATH_MATCH_NATIVE | PATH_MATCH_XPLATFORM)\n+\n+/**\n+ * path_match_flags() checks if a given \"path\" matches a given \"enum\n+ * path_match_flags\" criteria.\n+ */\n+int path_match_flags(const char *const path, const enum path_match_flags f);\n+\n+/**\n+ * starts_with_dot_slash_native(): convenience wrapper for\n+ * path_match_flags() with PATH_MATCH_STARTS_WITH_DOT_SLASH and\n+ * PATH_MATCH_NATIVE.\n+ */\n+static inline int starts_with_dot_slash_native(const char *const path)\n+{\n+\tconst enum path_match_flags what = PATH_MATCH_STARTS_WITH_DOT_SLASH;\n+\n+\treturn path_match_flags(path, what | PATH_MATCH_NATIVE);\n+}\n+\n+/**\n+ * starts_with_dot_slash_native(): convenience wrapper for\n+ * path_match_flags() with PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH and\n+ * PATH_MATCH_NATIVE.\n+ */\n+static inline int starts_with_dot_dot_slash_native(const char *const path)\n+{\n+\tconst enum path_match_flags what = PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH;\n+\n+\treturn path_match_flags(path, what | PATH_MATCH_NATIVE);\n+}\n #endif\ndiff --git a/fsck.c b/fsck.c\nindex 3ec500d707a..dd4822ba1be 100644\n--- a/fsck.c\n+++ b/fsck.c\n@@ -975,27 +975,16 @@ int fsck_tag_standalone(const struct object_id *oid, const char *buffer,\n \treturn ret;\n }\n \n-/*\n- * Like builtin/submodule--helper.c's starts_with_dot_slash, but without\n- * relying on the platform-dependent is_dir_sep helper.\n- *\n- * This is for use in checking whether a submodule URL is interpreted as\n- * relative to the current directory on any platform, since \\ is a\n- * directory separator on Windows but not on other platforms.\n- */\n-static int starts_with_dot_slash(const char *str)\n+static int starts_with_dot_slash(const char *const path)\n {\n-\treturn str[0] == '.' && (str[1] == '/' || str[1] == '\\\\');\n+\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_SLASH |\n+\t\t\t\tPATH_MATCH_XPLATFORM);\n }\n \n-/*\n- * Like starts_with_dot_slash, this is a variant of submodule--helper's\n- * helper of the same name with the twist that it accepts backslash as a\n- * directory separator even on non-Windows platforms.\n- */\n-static int starts_with_dot_dot_slash(const char *str)\n+static int starts_with_dot_dot_slash(const char *const path)\n {\n-\treturn str[0] == '.' && starts_with_dot_slash(str + 1);\n+\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH |\n+\t\t\t\tPATH_MATCH_XPLATFORM);\n }\n \n static int submodule_url_is_relative(const char *url)\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex 58fd813bd01..ba3436db9a1 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -236,6 +236,12 @@\n #include <sys/sysctl.h>\n #endif\n \n+/* Used by compat/win32/path-utils.h, and more */\n+static inline int is_xplatform_dir_sep(int c)\n+{\n+\treturn c == '/' || c == '\\\\';\n+}\n+\n #if defined(__CYGWIN__)\n #include \"compat/win32/path-utils.h\"\n #endif\n@@ -416,11 +422,11 @@ static inline int git_skip_dos_drive_prefix(char **path)\n #define skip_dos_drive_prefix git_skip_dos_drive_prefix\n #endif\n \n-#ifndef is_dir_sep\n static inline int git_is_dir_sep(int c)\n {\n \treturn c == '/';\n }\n+#ifndef is_dir_sep\n #define is_dir_sep git_is_dir_sep\n #endif\n \ndiff --git a/path.c b/path.c\nindex d73146b6cd2..2ab78278943 100644\n--- a/path.c\n+++ b/path.c\n@@ -1413,7 +1413,7 @@ int is_ntfs_dotgit(const char *name)\n \n \tfor (;;) {\n \t\tc = *(name++);\n-\t\tif (!c || c == '\\\\' || c == '/' || c == ':')\n+\t\tif (!c || is_xplatform_dir_sep(c) || c == ':')\n \t\t\treturn 1;\n \t\tif (c != '.' && c != ' ')\n \t\t\treturn 0;\ndiff --git a/submodule-config.c b/submodule-config.c\nindex 29668b0620d..ce3beaf5d4f 100644\n--- a/submodule-config.c\n+++ b/submodule-config.c\n@@ -204,17 +204,17 @@ int check_submodule_name(const char *name)\n \t\treturn -1;\n \n \t/*\n-\t * Look for '..' as a path component. Check both '/' and '\\\\' as\n+\t * Look for '..' as a path component. Check is_xplatform_dir_sep() as\n \t * separators rather than is_dir_sep(), because we want the name rules\n \t * to be consistent across platforms.\n \t */\n \tgoto in_component; /* always start inside component */\n \twhile (*name) {\n \t\tchar c = *name++;\n-\t\tif (c == '/' || c == '\\\\') {\n+\t\tif (is_xplatform_dir_sep(c)) {\n in_component:\n \t\t\tif (name[0] == '.' && name[1] == '.' &&\n-\t\t\t    (!name[2] || name[2] == '/' || name[2] == '\\\\'))\n+\t\t\t    (!name[2] || is_xplatform_dir_sep(name[2])))\n \t\t\t\treturn -1;\n \t\t}\n \t}\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453857","messageId":"RFC-patch-v2-03.36-7823a177fd7-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 03/36] fetch-pack: add a deref_without_lazy_fetch_extended()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:20Z","receivedAt":"2022-04-18T17:24:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a version of the deref_without_lazy_fetch function which can be\ncalled with custom oi_flags and to grab information about the\n\"object_type\". This will be used for the bundle-uri client in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n fetch-pack.c | 25 ++++++++++++++++++-------\n 1 file changed, 18 insertions(+), 7 deletions(-)\n\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 4e1e88eea09..d0aa3a5c229 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -115,11 +115,12 @@ static void for_each_cached_alternate(struct fetch_negotiator *negotiator,\n \t\tcb(negotiator, cache.items[i]);\n }\n \n-static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n-\t\t\t\t\t       int mark_tags_complete)\n+static struct commit *deref_without_lazy_fetch_extended(const struct object_id *oid,\n+\t\t\t\t\t\t\tint mark_tags_complete,\n+\t\t\t\t\t\t\tenum object_type *type,\n+\t\t\t\t\t\t\tunsigned int oi_flags)\n {\n-\tenum object_type type;\n-\tstruct object_info info = { .typep = &type };\n+\tstruct object_info info = { .typep = type };\n \tstruct commit *commit;\n \n \tcommit = lookup_commit_in_graph(the_repository, oid);\n@@ -128,9 +129,9 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n \n \twhile (1) {\n \t\tif (oid_object_info_extended(the_repository, oid, &info,\n-\t\t\t\t\t     OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK))\n+\t\t\t\t\t     oi_flags))\n \t\t\treturn NULL;\n-\t\tif (type == OBJ_TAG) {\n+\t\tif (*type == OBJ_TAG) {\n \t\t\tstruct tag *tag = (struct tag *)\n \t\t\t\tparse_object(the_repository, oid);\n \n@@ -144,7 +145,7 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n \t\t}\n \t}\n \n-\tif (type == OBJ_COMMIT) {\n+\tif (*type == OBJ_COMMIT) {\n \t\tstruct commit *commit = lookup_commit(the_repository, oid);\n \t\tif (!commit || repo_parse_commit(the_repository, commit))\n \t\t\treturn NULL;\n@@ -154,6 +155,16 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n \treturn NULL;\n }\n \n+\n+static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n+\t\t\t\t\t       int mark_tags_complete)\n+{\n+\tenum object_type type;\n+\tunsigned flags = OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK;\n+\treturn deref_without_lazy_fetch_extended(oid, mark_tags_complete,\n+\t\t\t\t\t\t &type, flags);\n+}\n+\n static int rev_list_insert_ref(struct fetch_negotiator *negotiator,\n \t\t\t       const struct object_id *oid)\n {\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453858","messageId":"RFC-patch-v2-04.36-0315bda0dac-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 04/36] fetch-pack: move --keep=* option filling to a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:21Z","receivedAt":"2022-04-18T17:24:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Move the populating of the --keep=* option argument to \"index-pack\" to\na static function, a subsequent commit will make use of it in another\nfunction.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n fetch-pack.c | 20 ++++++++++++--------\n 1 file changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex d0aa3a5c229..b1d90d1914f 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -847,6 +847,16 @@ static void parse_gitmodules_oids(int fd, struct oidset *gitmodules_oids)\n \t} while (1);\n }\n \n+static void add_index_pack_keep_option(struct strvec *args)\n+{\n+\tchar hostname[HOST_NAME_MAX + 1];\n+\n+\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\tstrvec_pushf(args, \"--keep=fetch-pack %\"PRIuMAX \" on %s\",\n+\t\t     (uintmax_t)getpid(), hostname);\n+}\n+\n /*\n  * If packfile URIs were provided, pass a non-NULL pointer to index_pack_args.\n  * The strings to pass as the --index-pack-arg arguments to http-fetch will be\n@@ -916,14 +926,8 @@ static int get_pack(struct fetch_pack_args *args,\n \t\t\tstrvec_push(&cmd.args, \"-v\");\n \t\tif (args->use_thin_pack)\n \t\t\tstrvec_push(&cmd.args, \"--fix-thin\");\n-\t\tif ((do_keep || index_pack_args) && (args->lock_pack || unpack_limit)) {\n-\t\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\t\tstrvec_pushf(&cmd.args,\n-\t\t\t\t     \"--keep=fetch-pack %\"PRIuMAX \" on %s\",\n-\t\t\t\t     (uintmax_t)getpid(), hostname);\n-\t\t}\n+\t\tif ((do_keep || index_pack_args) && (args->lock_pack || unpack_limit))\n+\t\t\tadd_index_pack_keep_option(&cmd.args);\n \t\tif (!index_pack_args && args->check_self_contained_and_connected)\n \t\t\tstrvec_push(&cmd.args, \"--check-self-contained-and-connected\");\n \t\telse\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453859","messageId":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com","subject":"[RFC PATCH v2 00/36] bundle-uri: a \"dumb CDN\" for git + TOC format","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:17Z","receivedAt":"2022-04-18T17:24:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This RFC series is a start at trying to combine the two differing RFC\nversions of bundle URIs I [1] and Derrick Stolee [2] were kicking\naround.\n\n= Layout\n\nThis series arranged in the following way:\n\n* 01-08: \"Prep\" patches from both [1] and [2] which in principle could\n  graduate first to \"master\".\n\n  I.e. they're prep fixes added for the two bundle-uri\n  implementations, but which either justify themselves, or e.g. expose\n  a now-static function via an API.\n\n  I tried to move things into the \"justify themselves\" category\n  whenever possible, but may have overdone it e.g. for 02/36\n  (originally an idea/commit of Derrick's, but I changed the\n  authorship as pretty much all of it at this point is something I\n  changed).\n\n  For the \"prep\" changes that are only needed for later changes in the\n  series perhaps we should just squash them if they're small enough.\n\n* 09-16: My RFC series at [1], minus things extracted to the below and\n  docs (see below).\n\n* 17-33: Derrick's RFC series at [2], minos <ditto above>\n\n* 34-36: I peeled off the Documentation/technical/bundle-uri.txt from\n  both to put at the end, and renamed Derrick's to\n  \"bundle-uri-TOC.txt\" so most of the change wouldn't be a massive\n  diff.\n\n  These obviously need to be unified too, but I figured doing so was\n  better once we shake out what features/interfaces we want to keep.\n\n= Overall state\n\nDerrick: Sorry about the long delay in submitting this. I spent a lot\nof time trying to get further in semantically merging the two in terms\nof getting some some sensible user-usable end-result, but ultimately\nwasn't happy with how opinionated that result was without looping you\nin earlier.\n\nSo I figured that a better start was a a version of this where we\ncould test the two against one another, see where feature\ndifferences/parity etc. were, and depending on those discussions\neventually unify the config, tests, features etc. for the two.\n\n= About the range-diff\n\nThe range-diff was produced by rebasing my [1] on \"master\", fixing\nconflicts, then rebasing a version of Derrick's [2] on it where I'd\nmunged it in some minimal way to reduce conflicts between the two, but\nwithout \"really\" changing anything (mainly moving added functions to\nslightly different places in the same files to reduce textual\nconflicts).\n\nWhen in doubt don't trust the range-diff, I figured it was better than\nnot including one at all, and it does a good job of pointing towards\nthe main areas of differences v.s. [1] and [2]. But it's not \"real\",\nand will e.g. omit changes I made while getting to the point where I\ncould run the range-diff at all...\n\n= Outstanding issues\n\nAside from the large issue of needing to more sensibly combine these\ntwo, there are:\n\n * A CI failure on Windows: https://github.com/avar/git/actions/runs/2184857264\n\n   This is in my [1], the implementation currently shells out to\n   \"curl\", which is failing somehow. Note that the tests are \"failing\"\n   on e.g. linux-musl too in that there's no /usr/bin/curl there, but\n   in a way where they'll recover and fall back to a non-bundle-uri\n   clone. So the same tests are testing graceful recovery elsewhere.\n\n * I didn't include the \"incremental\" fetch in my version. As noted in\n   \"overall state\" I figured starting with a smaller version in this\n   already-huge 33-patch series was better, and while I have that\n   locally adding it is even more code...\n\n1. https://lore.kernel.org/git/RFC-cover-v2-00.13-00000000000-20220311T155841Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/pull.1160.git.1645641063.gitgitgadget@gmail.com/\n\nDerrick Stolee (21):\n  http: make http_get_file() external\n  remote: move relative_url()\n  remote: allow relative_url() to return an absolute url\n  remote-curl: add 'get' capability\n  bundle: implement 'fetch' command for direct bundles\n  bundle: parse table of contents during 'fetch'\n  bundle: add --filter option to 'fetch'\n  bundle: allow relative URLs in table of contents\n  bundle: make it easy to call 'git bundle fetch'\n  clone: add --bundle-uri option\n  clone: --bundle-uri cannot be combined with --depth\n  bundle: only fetch bundles if timestamp is new\n  fetch: fetch bundles before fetching original data\n  protocol-caps: implement cap_features()\n  serve: understand but do not advertise 'features' capability\n  serve: advertise 'features' when config exists\n  connect: implement get_recommended_features()\n  transport: add connections for 'features' capability\n  clone: use server-recommended bundle URI\n  t5601: basic bundle URI test\n  docs: document bundle URI standard\n\nÆvar Arnfjörð Bjarmason (15):\n  connect.c: refactor sending of agent & object-format\n  dir API: add a generalized path_match_flags() function\n  fetch-pack: add a deref_without_lazy_fetch_extended()\n  fetch-pack: move --keep=* option filling to a function\n  bundle.h: make \"fd\" version of read_bundle_header() public\n  protocol v2: add server-side \"bundle-uri\" skeleton\n  bundle-uri client: add \"bundle-uri\" parsing + tests\n  bundle-uri client: add minimal NOOP client\n  bundle-uri client: add \"git ls-remote-bundle-uri\"\n  bundle-uri client: add transfer.injectBundleURI support\n  bundle-uri client: add boolean transfer.bundleURI setting\n  bundle-uri client: support for bundle-uri with \"clone\"\n  bundle-uri: make the download program configurable\n  protocol v2: add server-side \"bundle-uri\" skeleton (docs)\n  bundle-uri docs: add design notes\n\n Documentation/config/transfer.txt          |  33 ++\n Documentation/git-bundle.txt               |   1 +\n Documentation/git-ls-remote-bundle-uri.txt |  62 +++\n Documentation/git-ls-remote.txt            |   1 +\n Documentation/gitremote-helpers.txt        |   6 +\n Documentation/technical/bundle-uri-TOC.txt | 404 +++++++++++++++++\n Documentation/technical/bundle-uri.txt     | 119 +++++\n Documentation/technical/protocol-v2.txt    | 214 +++++++++\n Makefile                                   |   3 +\n builtin.h                                  |   1 +\n builtin/bundle.c                           | 481 +++++++++++++++++++++\n builtin/clone.c                            |  57 +++\n builtin/fetch.c                            |  17 +\n builtin/ls-remote-bundle-uri.c             |  90 ++++\n builtin/submodule--helper.c                | 141 +-----\n bundle-uri.c                               | 183 ++++++++\n bundle-uri.h                               |  29 ++\n bundle.c                                   |  29 +-\n bundle.h                                   |  11 +\n command-list.txt                           |   1 +\n compat/mingw.c                             |   2 +-\n compat/win32/path-utils.h                  |   6 +-\n connect.c                                  | 116 ++++-\n dir.c                                      |  29 ++\n dir.h                                      |  63 +++\n fetch-pack.c                               | 306 ++++++++++++-\n fetch-pack.h                               |   6 +\n fsck.c                                     |  23 +-\n git-compat-util.h                          |   8 +-\n git.c                                      |   1 +\n http.c                                     |   4 +-\n http.h                                     |   9 +\n path.c                                     |   2 +-\n protocol-caps.c                            |  66 +++\n protocol-caps.h                            |   1 +\n remote-curl.c                              |  32 ++\n remote.c                                   |  99 +++++\n remote.h                                   |  40 ++\n serve.c                                    |  29 ++\n submodule-config.c                         |   6 +-\n t/helper/test-bundle-uri.c                 |  83 ++++\n t/helper/test-tool.c                       |   1 +\n t/helper/test-tool.h                       |   1 +\n t/lib-t5730-protocol-v2-bundle-uri.sh      | 458 ++++++++++++++++++++\n t/t5601-clone.sh                           |  15 +\n t/t5701-git-serve.sh                       | 133 +++++-\n t/t5730-protocol-v2-bundle-uri-file.sh     |  36 ++\n t/t5731-protocol-v2-bundle-uri-git.sh      |  17 +\n t/t5732-protocol-v2-bundle-uri-http.sh     |  17 +\n t/t5750-bundle-uri-parse.sh                | 153 +++++++\n transport-helper.c                         |  26 ++\n transport-internal.h                       |  16 +\n transport.c                                | 158 +++++++\n transport.h                                |  27 ++\n 54 files changed, 3680 insertions(+), 192 deletions(-)\n create mode 100644 Documentation/git-ls-remote-bundle-uri.txt\n create mode 100644 Documentation/technical/bundle-uri-TOC.txt\n create mode 100644 Documentation/technical/bundle-uri.txt\n create mode 100644 builtin/ls-remote-bundle-uri.c\n create mode 100644 bundle-uri.c\n create mode 100644 bundle-uri.h\n create mode 100644 t/helper/test-bundle-uri.c\n create mode 100644 t/lib-t5730-protocol-v2-bundle-uri.sh\n create mode 100755 t/t5730-protocol-v2-bundle-uri-file.sh\n create mode 100755 t/t5731-protocol-v2-bundle-uri-git.sh\n create mode 100755 t/t5732-protocol-v2-bundle-uri-http.sh\n create mode 100755 t/t5750-bundle-uri-parse.sh\n\nRange-diff against v1:\n 4:  034d371472e =  1:  95c53a3e779 connect.c: refactor sending of agent & object-format\n15:  02563939040 !  2:  8f6e4f12e8a dir: extract starts_with_dot[_dot]_slash()\n    @@\n      ## Metadata ##\n    -Author: Derrick Stolee <derrickstolee@github.com>\n    +Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    dir: extract starts_with_dot[_dot]_slash()\n    +    dir API: add a generalized path_match_flags() function\n     \n    -    We will want to use this logic to assist checking if paths are absolute\n    -    or relative, so extract it into a helpful place. This creates a\n    -    collision with similar methods in builtin/fsck.c, but those methods have\n    -    important differences. Prepend \"fsck_\" to those methods to emphasize\n    -    that they are custom to the fsck builtin.\n    +    Add a path_match_flags() function and have the two sets of\n    +    starts_with_dot_{,dot_}slash() functions added in\n    +    63e95beb085 (submodule: port resolve_relative_url from shell to C,\n    +    2016-04-15) and a2b26ffb1a8 (fsck: convert gitmodules url to URL\n    +    passed to curl, 2020-04-18) be thin wrappers for it.\n    +\n    +    As the latter of those notes the fsck version was copied from the\n    +    initial builtin/submodule--helper.c version.\n    +\n    +    Since the code added in a2b26ffb1a8 was doing really doing the same as\n    +    win32_is_dir_sep() added in 1cadad6f658 (git clone <url>\n    +    C:\\cygwin\\home\\USER\\repo' is working (again), 2018-12-15) let's move\n    +    the latter to git-compat-util.h is a is_xplatform_dir_sep(). We can\n    +    then call either it or the platform-specific is_dir_sep() from this\n    +    new function.\n    +\n    +    Let's likewise change code in various other places that was hardcoding\n    +    checks for \"'/' || '\\\\'\" with the new is_xplatform_dir_sep(). As can\n    +    be seen in those callers some of them still concern themselves with\n    +    ':' (Mac OS classic?), but let's leave the question of whether that\n    +    should be consolidated for some other time.\n    +\n    +    As we expect to make wider use of the \"native\" case in the future,\n    +    define and use two starts_with_dot_{,dot_}slash_native() convenience\n    +    wrappers. This makes the diff in builtin/submodule--helper.c much\n    +    smaller.\n     \n         Signed-off-by: Derrick Stolee <derrickstolee@github.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/submodule--helper.c ##\n     @@ builtin/submodule--helper.c: static char *get_default_remote(void)\n    @@ builtin/submodule--helper.c: static char *get_default_remote(void)\n      /*\n       * Returns 1 if it was the last chop before ':'.\n       */\n    +@@ builtin/submodule--helper.c: static int chop_last_dir(char **remoteurl, int is_relative)\n    + \treturn 0;\n    + }\n    + \n    ++static int starts_with_dot_slash(const char *const path)\n    ++{\n    ++\treturn starts_with_dot_slash_native(path);;\n    ++}\n    ++\n    ++static int starts_with_dot_dot_slash(const char *const path)\n    ++{\n    ++\treturn starts_with_dot_dot_slash_native(path);\n    ++}\n    ++\n    + /*\n    +  * The `url` argument is the URL that navigates to the submodule origin\n    +  * repo. When relative, this URL is relative to the superproject origin\n    +\n    + ## compat/mingw.c ##\n    +@@ compat/mingw.c: int is_valid_win32_path(const char *path, int allow_literal_nul)\n    + \t\t\t}\n    + \n    + \t\t\tc = path[i];\n    +-\t\t\tif (c && c != '.' && c != ':' && c != '/' && c != '\\\\')\n    ++\t\t\tif (c && c != '.' && c != ':' && !is_xplatform_dir_sep(c))\n    + \t\t\t\tgoto not_a_reserved_name;\n    + \n    + \t\t\t/* contains reserved name */\n    +\n    + ## compat/win32/path-utils.h ##\n    +@@ compat/win32/path-utils.h: int win32_has_dos_drive_prefix(const char *path);\n    + \n    + int win32_skip_dos_drive_prefix(char **path);\n    + #define skip_dos_drive_prefix win32_skip_dos_drive_prefix\n    +-static inline int win32_is_dir_sep(int c)\n    +-{\n    +-\treturn c == '/' || c == '\\\\';\n    +-}\n    +-#define is_dir_sep win32_is_dir_sep\n    ++#define is_dir_sep is_xplatform_dir_sep\n    + static inline char *win32_find_last_dir_sep(const char *path)\n    + {\n    + \tchar *ret = NULL;\n    +\n    + ## dir.c ##\n    +@@ dir.c: void relocate_gitdir(const char *path, const char *old_git_dir, const char *new_\n    + \n    + \tconnect_work_tree_and_git_dir(path, new_git_dir, 0);\n    + }\n    ++\n    ++int path_match_flags(const char *const str, const enum path_match_flags flags)\n    ++{\n    ++\tconst char *p = str;\n    ++\n    ++\tif (flags & PATH_MATCH_NATIVE &&\n    ++\t    flags & PATH_MATCH_XPLATFORM)\n    ++\t\tBUG(\"path_match_flags() must get one match kind, not multiple!\");\n    ++\telse if (!(flags & PATH_MATCH_KINDS_MASK))\n    ++\t\tBUG(\"path_match_flags() must get at least one match kind!\");\n    ++\n    ++\tif (flags & PATH_MATCH_STARTS_WITH_DOT_SLASH &&\n    ++\t    flags & PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH)\n    ++\t\tBUG(\"path_match_flags() must get one platform kind, not multiple!\");\n    ++\telse if (!(flags & PATH_MATCH_PLATFORM_MASK))\n    ++\t\tBUG(\"path_match_flags() must get at least one platform kind!\");\n    ++\n    ++\tif (*p++ != '.')\n    ++\t\treturn 0;\n    ++\tif (flags & PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH &&\n    ++\t    *p++ != '.')\n    ++\t\treturn 0;\n    ++\n    ++\tif (flags & PATH_MATCH_NATIVE)\n    ++\t\treturn is_dir_sep(*p);\n    ++\telse if (flags & PATH_MATCH_XPLATFORM)\n    ++\t\treturn is_xplatform_dir_sep(*p);\n    ++\tBUG(\"unreachable\");\n    ++}\n     \n      ## dir.h ##\n     @@ dir.h: void connect_work_tree_and_git_dir(const char *work_tree,\n    @@ dir.h: void connect_work_tree_and_git_dir(const char *work_tree,\n      \t\t     const char *old_git_dir,\n      \t\t     const char *new_git_dir);\n     +\n    -+static inline int starts_with_dot_slash(const char *str)\n    ++/**\n    ++ * The \"enum path_matches_kind\" determines how path_match_flags() will\n    ++ * behave. The flags come in sets, and one (and only one) must be\n    ++ * provided out of each \"set\":\n    ++ *\n    ++ * PATH_MATCH_NATIVE:\n    ++ *\tPath separator is is_dir_sep()\n    ++ * PATH_MATCH_XPLATFORM:\n    ++ *\tPath separator is is_xplatform_dir_sep()\n    ++ *\n    ++ * Do we use is_dir_sep() to check for a directory separator\n    ++ * (*_NATIVE), or do we always check for '/' or '\\' (*_XPLATFORM). The\n    ++ * \"*_NATIVE\" version on Windows is the same as \"*_XPLATFORM\",\n    ++ * everywhere else \"*_NATIVE\" means \"only /\".\n    ++ *\n    ++ * PATH_MATCH_STARTS_WITH_DOT_SLASH:\n    ++ *\tMatch a path starting with \"./\"\n    ++ * PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH:\n    ++ *\tMatch a path starting with \"../\"\n    ++ *\n    ++ * The \"/\" in the above is adjusted based on the \"*_NATIVE\" and\n    ++ * \"*_XPLATFORM\" flags.\n    ++ */\n    ++enum path_match_flags {\n    ++\tPATH_MATCH_NATIVE = 1 << 0,\n    ++\tPATH_MATCH_XPLATFORM = 1 << 1,\n    ++\tPATH_MATCH_STARTS_WITH_DOT_SLASH = 1 << 2,\n    ++\tPATH_MATCH_STARTS_WITH_DOT_DOT_SLASH = 1 << 3,\n    ++};\n    ++#define PATH_MATCH_KINDS_MASK (PATH_MATCH_STARTS_WITH_DOT_SLASH | \\\n    ++\tPATH_MATCH_STARTS_WITH_DOT_DOT_SLASH)\n    ++#define PATH_MATCH_PLATFORM_MASK (PATH_MATCH_NATIVE | PATH_MATCH_XPLATFORM)\n    ++\n    ++/**\n    ++ * path_match_flags() checks if a given \"path\" matches a given \"enum\n    ++ * path_match_flags\" criteria.\n    ++ */\n    ++int path_match_flags(const char *const path, const enum path_match_flags f);\n    ++\n    ++/**\n    ++ * starts_with_dot_slash_native(): convenience wrapper for\n    ++ * path_match_flags() with PATH_MATCH_STARTS_WITH_DOT_SLASH and\n    ++ * PATH_MATCH_NATIVE.\n    ++ */\n    ++static inline int starts_with_dot_slash_native(const char *const path)\n     +{\n    -+\treturn str[0] == '.' && is_dir_sep(str[1]);\n    ++\tconst enum path_match_flags what = PATH_MATCH_STARTS_WITH_DOT_SLASH;\n    ++\n    ++\treturn path_match_flags(path, what | PATH_MATCH_NATIVE);\n     +}\n     +\n    -+static inline int starts_with_dot_dot_slash(const char *str)\n    ++/**\n    ++ * starts_with_dot_slash_native(): convenience wrapper for\n    ++ * path_match_flags() with PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH and\n    ++ * PATH_MATCH_NATIVE.\n    ++ */\n    ++static inline int starts_with_dot_dot_slash_native(const char *const path)\n     +{\n    -+\treturn str[0] == '.' && str[1] == '.' && is_dir_sep(str[2]);\n    -+}\n    ++\tconst enum path_match_flags what = PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH;\n     +\n    ++\treturn path_match_flags(path, what | PATH_MATCH_NATIVE);\n    ++}\n      #endif\n     \n      ## fsck.c ##\n     @@ fsck.c: int fsck_tag_standalone(const struct object_id *oid, const char *buffer,\n    + \treturn ret;\n      }\n      \n    - /*\n    +-/*\n     - * Like builtin/submodule--helper.c's starts_with_dot_slash, but without\n    -+ * Like dir.h's starts_with_dot_slash, but without\n    -  * relying on the platform-dependent is_dir_sep helper.\n    -  *\n    -  * This is for use in checking whether a submodule URL is interpreted as\n    -  * relative to the current directory on any platform, since \\ is a\n    -  * directory separator on Windows but not on other platforms.\n    -  */\n    +- * relying on the platform-dependent is_dir_sep helper.\n    +- *\n    +- * This is for use in checking whether a submodule URL is interpreted as\n    +- * relative to the current directory on any platform, since \\ is a\n    +- * directory separator on Windows but not on other platforms.\n    +- */\n     -static int starts_with_dot_slash(const char *str)\n    -+static int fsck_starts_with_dot_slash(const char *str)\n    ++static int starts_with_dot_slash(const char *const path)\n      {\n    - \treturn str[0] == '.' && (str[1] == '/' || str[1] == '\\\\');\n    +-\treturn str[0] == '.' && (str[1] == '/' || str[1] == '\\\\');\n    ++\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_SLASH |\n    ++\t\t\t\tPATH_MATCH_XPLATFORM);\n      }\n      \n    - /*\n    +-/*\n     - * Like starts_with_dot_slash, this is a variant of submodule--helper's\n     - * helper of the same name with the twist that it accepts backslash as a\n    -+ * Like fsck_starts_with_dot_slash, this is a variant of dir.h's\n    -+ * helper with the twist that it accepts backslash as a\n    -  * directory separator even on non-Windows platforms.\n    -  */\n    +- * directory separator even on non-Windows platforms.\n    +- */\n     -static int starts_with_dot_dot_slash(const char *str)\n    -+static int fsck_starts_with_dot_dot_slash(const char *str)\n    ++static int starts_with_dot_dot_slash(const char *const path)\n      {\n     -\treturn str[0] == '.' && starts_with_dot_slash(str + 1);\n    -+\treturn str[0] == '.' && fsck_starts_with_dot_slash(str + 1);\n    ++\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH |\n    ++\t\t\t\tPATH_MATCH_XPLATFORM);\n      }\n      \n      static int submodule_url_is_relative(const char *url)\n    +\n    + ## git-compat-util.h ##\n    +@@\n    + #include <sys/sysctl.h>\n    + #endif\n    + \n    ++/* Used by compat/win32/path-utils.h, and more */\n    ++static inline int is_xplatform_dir_sep(int c)\n    ++{\n    ++\treturn c == '/' || c == '\\\\';\n    ++}\n    ++\n    + #if defined(__CYGWIN__)\n    + #include \"compat/win32/path-utils.h\"\n    + #endif\n    +@@ git-compat-util.h: static inline int git_skip_dos_drive_prefix(char **path)\n    + #define skip_dos_drive_prefix git_skip_dos_drive_prefix\n    + #endif\n    + \n    +-#ifndef is_dir_sep\n    + static inline int git_is_dir_sep(int c)\n      {\n    --\treturn starts_with_dot_slash(url) || starts_with_dot_dot_slash(url);\n    -+\treturn fsck_starts_with_dot_slash(url) || fsck_starts_with_dot_dot_slash(url);\n    + \treturn c == '/';\n      }\n    ++#ifndef is_dir_sep\n    + #define is_dir_sep git_is_dir_sep\n    + #endif\n      \n    - /*\n    +\n    + ## path.c ##\n    +@@ path.c: int is_ntfs_dotgit(const char *name)\n    + \n    + \tfor (;;) {\n    + \t\tc = *(name++);\n    +-\t\tif (!c || c == '\\\\' || c == '/' || c == ':')\n    ++\t\tif (!c || is_xplatform_dir_sep(c) || c == ':')\n    + \t\t\treturn 1;\n    + \t\tif (c != '.' && c != ' ')\n    + \t\t\treturn 0;\n    +\n    + ## submodule-config.c ##\n    +@@ submodule-config.c: int check_submodule_name(const char *name)\n    + \t\treturn -1;\n    + \n    + \t/*\n    +-\t * Look for '..' as a path component. Check both '/' and '\\\\' as\n    ++\t * Look for '..' as a path component. Check is_xplatform_dir_sep() as\n    + \t * separators rather than is_dir_sep(), because we want the name rules\n    + \t * to be consistent across platforms.\n    + \t */\n    + \tgoto in_component; /* always start inside component */\n    + \twhile (*name) {\n    + \t\tchar c = *name++;\n    +-\t\tif (c == '/' || c == '\\\\') {\n    ++\t\tif (is_xplatform_dir_sep(c)) {\n    + in_component:\n    + \t\t\tif (name[0] == '.' && name[1] == '.' &&\n    +-\t\t\t    (!name[2] || name[2] == '/' || name[2] == '\\\\'))\n    ++\t\t\t    (!name[2] || is_xplatform_dir_sep(name[2])))\n    + \t\t\t\treturn -1;\n    + \t\t}\n    + \t}\n 9:  e93306308f9 =  3:  7823a177fd7 fetch-pack: add a deref_without_lazy_fetch_extended()\n10:  d7633791083 =  4:  0315bda0dac fetch-pack: move --keep=* option filling to a function\n18:  0bfc59ad308 =  5:  6e1f4296896 http: make http_get_file() external\n16:  eea2816bc8f !  6:  97a9f38f08d remote: move relative_url()\n    @@ Commit message\n         similar functionality in the bundle URI feature, extract this to be\n         available in remote.h.\n     \n    -    The code is exactly the same. The prototype is different only in\n    -    whitespace. The documentation comment only adds explicit instructions on\n    -    what happens when supplying two absolute URLs.\n    +    The code is almost exactly the same, except for the following trivial\n    +    differences:\n    +\n    +     * Fix whitespace and wrapping issues with the prototype and argument\n    +       lists.\n    +\n    +     * Let's call starts_with_dot_{,dot_}slash_native() instead of the\n    +       functionally identical \"starts_with_dot_{,dot_}slash()\" wrappers\n    +       \"builtin/submodule--helper.c\".\n     \n         Signed-off-by: Derrick Stolee <derrickstolee@github.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/submodule--helper.c ##\n     @@ builtin/submodule--helper.c: static char *get_default_remote(void)\n    @@ builtin/submodule--helper.c: static char *get_default_remote(void)\n     -\treturn 0;\n     -}\n     -\n    +-static int starts_with_dot_slash(const char *const path)\n    +-{\n    +-\treturn starts_with_dot_slash_native(path);;\n    +-}\n    +-\n    +-static int starts_with_dot_dot_slash(const char *const path)\n    +-{\n    +-\treturn starts_with_dot_dot_slash_native(path);\n    +-}\n    +-\n     -/*\n     - * The `url` argument is the URL that navigates to the submodule origin\n     - * repo. When relative, this URL is relative to the superproject origin\n    @@ builtin/submodule--helper.c: static char *get_default_remote(void)\n      static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet)\n      {\n      \tchar *remoteurl, *resolved_url;\n    +@@ builtin/submodule--helper.c: static int module_foreach(int argc, const char **argv, const char *prefix)\n    + \treturn 0;\n    + }\n    + \n    ++static int starts_with_dot_slash(const char *const path)\n    ++{\n    ++\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_SLASH |\n    ++\t\t\t\tPATH_MATCH_XPLATFORM);\n    ++}\n    ++\n    ++static int starts_with_dot_dot_slash(const char *const path)\n    ++{\n    ++\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH |\n    ++\t\t\t\tPATH_MATCH_XPLATFORM);\n    ++}\n    ++\n    + struct init_cb {\n    + \tconst char *prefix;\n    + \tconst char *superprefix;\n     \n      ## remote.c ##\n     @@\n    @@ remote.c: void remote_state_clear(struct remote_state *remote_state)\n     +\treturn 0;\n     +}\n     +\n    -+/*\n    -+ * NEEDSWORK: Given how chop_last_dir() works, this function is broken\n    -+ * when a local part has a colon in its path component, too.\n    -+ */\n    -+char *relative_url(const char *remote_url,\n    -+\t\t   const char *url,\n    ++char *relative_url(const char *remote_url, const char *url,\n     +\t\t   const char *up_path)\n     +{\n     +\tint is_relative = 0;\n    @@ remote.c: void remote_state_clear(struct remote_state *remote_state)\n     +\t\t * Prepend a './' to ensure all relative\n     +\t\t * remoteurls start with './' or '../'\n     +\t\t */\n    -+\t\tif (!starts_with_dot_slash(remoteurl) &&\n    -+\t\t    !starts_with_dot_dot_slash(remoteurl)) {\n    ++\t\tif (!starts_with_dot_slash_native(remoteurl) &&\n    ++\t\t    !starts_with_dot_dot_slash_native(remoteurl)) {\n     +\t\t\tstrbuf_reset(&sb);\n     +\t\t\tstrbuf_addf(&sb, \"./%s\", remoteurl);\n     +\t\t\tfree(remoteurl);\n    @@ remote.c: void remote_state_clear(struct remote_state *remote_state)\n     +\t * last directory in remoteurl.\n     +\t */\n     +\twhile (url) {\n    -+\t\tif (starts_with_dot_dot_slash(url)) {\n    ++\t\tif (starts_with_dot_dot_slash_native(url)) {\n     +\t\t\turl += 3;\n     +\t\t\tcolonsep |= chop_last_dir(&remoteurl, is_relative);\n    -+\t\t} else if (starts_with_dot_slash(url))\n    ++\t\t} else if (starts_with_dot_slash_native(url))\n     +\t\t\turl += 2;\n     +\t\telse\n     +\t\t\tbreak;\n    @@ remote.c: void remote_state_clear(struct remote_state *remote_state)\n     +\t\tstrbuf_setlen(&sb, sb.len - 1);\n     +\tfree(remoteurl);\n     +\n    -+\tif (starts_with_dot_slash(sb.buf))\n    ++\tif (starts_with_dot_slash_native(sb.buf))\n     +\t\tout = xstrdup(sb.buf + 2);\n     +\telse\n     +\t\tout = xstrdup(sb.buf);\n    @@ remote.h: int parseopt_push_cas_option(const struct option *, const char *arg, i\n     + * http://a.com/b  ../../../c       http:/c          error out\n     + * http://a.com/b  ../../../../c    http:c           error out\n     + * http://a.com/b  ../../../../../c    .:c           error out\n    ++ * NEEDSWORK: Given how chop_last_dir() works, this function is broken\n    ++ * when a local part has a colon in its path component, too.\n     + */\n    -+char *relative_url(const char *remote_url,\n    -+\t\t   const char *url,\n    ++char *relative_url(const char *remote_url, const char *url,\n     +\t\t   const char *up_path);\n     +\n      #endif\n17:  68b10e64382 !  7:  2917cdd8277 remote: allow relative_url() to return an absolute url\n    @@ Commit message\n         concatenate 'remote_url' with 'url'. Instead, we want to return 'url' in\n         this case.\n     \n    +    The documentation now discusses what happens when supplying two\n    +    absolute URLs.\n    +\n         Signed-off-by: Derrick Stolee <derrickstolee@github.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## remote.c ##\n    -@@ remote.c: char *relative_url(const char *remote_url,\n    +@@ remote.c: char *relative_url(const char *remote_url, const char *url,\n      \tint is_relative = 0;\n      \tint colonsep = 0;\n      \tchar *out;\n    @@ remote.h: void apply_push_cas(struct push_cas_option *, struct remote *, struct\n       * http://a.com/b  ../../../../c    http:c           error out\n       * http://a.com/b  ../../../../../c    .:c           error out\n     + * http://a.com/b  http://d.org/e   http://d.org/e   as is\n    +  * NEEDSWORK: Given how chop_last_dir() works, this function is broken\n    +  * when a local part has a colon in its path component, too.\n       */\n    - char *relative_url(const char *remote_url,\n    - \t\t   const char *url,\n11:  1f5a48c712c =  8:  2b236af147b bundle.h: make \"fd\" version of read_bundle_header() public\n 1:  3875bf2a294 !  9:  1496b89ea6a protocol v2: add server-side \"bundle-uri\" skeleton\n    @@ Commit message\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    - ## Documentation/technical/protocol-v2.txt ##\n    -@@ Documentation/technical/protocol-v2.txt: and associated requested information, each separated by a single space.\n    - \tattr = \"size\"\n    - \n    - \tobj-info = obj-id SP obj-size\n    -+\n    -+bundle-uri\n    -+~~~~~~~~~~\n    -+\n    -+If the 'bundle-uri' capability is advertised, the server supports the\n    -+`bundle-uri' command.\n    -+\n    -+The capability is currently advertised with no value (i.e. not\n    -+\"bundle-uri=somevalue\"), a value may be added in the future for\n    -+supporting command-wide extensions. Clients MUST ignore any unknown\n    -+capability values and proceed with the 'bundle-uri` dialog they\n    -+support.\n    -+\n    -+The 'bundle-uri' command is intended to be issued before `fetch` to\n    -+get URIs to bundle files (see linkgit:git-bundle[1]) to \"seed\" and\n    -+inform the subsequent `fetch` command.\n    -+\n    -+The client CAN issue `bundle-uri` before or after any other valid\n    -+command. To be useful to clients it's expected that it'll be issued\n    -+after an `ls-refs` and before `fetch`, but CAN be issued at any time\n    -+in the dialog.\n    -+\n    -+DISCUSSION of bundle-uri\n    -+^^^^^^^^^^^^^^^^^^^^^^^^\n    -+\n    -+The intent of the feature is optimize for server resource consumption\n    -+in the common case by changing the common case of fetching a very\n    -+large PACK during linkgit:git-clone[1] into a smaller incremental\n    -+fetch.\n    -+\n    -+It also allows servers to achieve better caching in combination with\n    -+an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n    -+\n    -+By having new clones or fetches be a more predictable and common\n    -+negotiation against the tips of recently produces *.bundle file(s).\n    -+Servers might even pre-generate the results of such negotiations for\n    -+the `uploadpack.packObjectsHook` as new pushes come in.\n    -+\n    -+I.e. the server would anticipate that fresh clones will download a\n    -+known bundle, followed by catching up to the current state of the\n    -+repository using ref tips found in that bundle (or bundles).\n    -+\n    -+PROTOCOL for bundle-uri\n    -+^^^^^^^^^^^^^^^^^^^^^^^\n    -+\n    -+A `bundle-uri` request takes no arguments, and as noted above does not\n    -+currently advertise a capability value. Both may be added in the\n    -+future.\n    -+\n    -+When the client issues a `command=bundle-uri` the response is a list\n    -+of URIs the server would like the client to fetch out-of-bounds before\n    -+proceeding with the `fetch` request in this format:\n    -+\n    -+\toutput = bundle-uri-line\n    -+\t\t bundle-uri-line* flush-pkt\n    -+\n    -+\tbundle-uri-line = PKT-LINE(bundle-uri)\n    -+\t\t\t  *(SP bundle-feature-key *(=bundle-feature-val))\n    -+\t\t\t  LF\n    -+\n    -+\tbundle-uri = A URI such as a https://, ssh:// etc. URI\n    -+\n    -+\tbundle-feature-key = Any printable ASCII characters except SP or \"=\"\n    -+\tbundle-feature-val = Any printable ASCII characters except SP or \"=\"\n    -+\n    -+No `bundle-feature-key`=`bundle-feature-value` fields are currently\n    -+defined. See the discussion of features below.\n    -+\n    -+Clients are still expected to fully parse the line according to the\n    -+above format, lines that do not conform to the format SHOULD be\n    -+discarded. The user MAY be warned in such a case.\n    -+\n    -+bundle-uri CLIENT AND SERVER EXPECTATIONS\n    -+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    -+\n    -+\".bundle\" FORMAT\n    -+++++++++++++++++\n    -+\n    -+The advertised bundle(s) MUST be in a format that \"git bundle verify\"\n    -+would accept. I.e. they MUST contain one or more reference tips for\n    -+use by the client, MUST indicate prerequisites (in any) with standard\n    -+\"-\" prefixes, and MUST indicate their \"object-format\", if\n    -+applicable. Create \"*.bundle\" files with \"git bundle create\".\n    -+\n    -+bundle-uri CLIENT ERROR RECOVERY\n    -+++++++++++++++++++++++++++++++++\n    -+\n    -+A client MUST above all gracefully degrade on errors, whether that\n    -+error is because of bad missing/data in the bundle URI(s), because\n    -+that client is too dumb to e.g. understand and fully parse out bundle\n    -+headers and their prerequisite relationships, or something else.\n    -+\n    -+Server operators should feel confident in turning on \"bundle-uri\" and\n    -+not worry if e.g. their CDN goes down that clones or fetches will run\n    -+into hard failures. Even if the server bundle bundle(s) are\n    -+incomplete, or bad in some way the client should still end up with a\n    -+functioning repository, just as if it had chosen not to use this\n    -+protocol extension.\n    -+\n    -+All subsequent discussion on client and server interaction MUST keep\n    -+this in mind.\n    -+\n    -+bundle-uri SERVER TO CLIENT\n    -++++++++++++++++++++++++++++\n    -+\n    -+The ordering of the returned bundle uris is not significant. Clients\n    -+MUST parse their headers to discover their contained OIDS and\n    -+prerequisites. A client MUST consider the content of the bundle(s)\n    -+themselves and their header as the ultimate source of truth.\n    -+\n    -+A server MAY even return bundle(s) that don't have any direct\n    -+relationship to the repository being cloned (either through accident,\n    -+or intentional \"clever\" configuration), and expect a client to sort\n    -+out what data they'd like from the bundle(s), if any.\n    -+\n    -+bundle-uri CLIENT TO SERVER\n    -++++++++++++++++++++++++++++\n    -+\n    -+The client SHOULD provide reference tips found in the bundle header(s)\n    -+as 'have' lines in any subsequent `fetch` request. A client MAY also\n    -+ignore the bundle(s) entirely if doing so is deemed worse for some\n    -+reason, e.g. if the bundles can't be downloaded, it doesn't like the\n    -+tips it finds etc.\n    -+\n    -+WHEN ADVERTISED BUNDLE(S) REQUIRE NO FURTHER NEGOTIATION\n    -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n    -+\n    -+If after issuing `bundle-uri` and `ls-refs`, and getting the header(s)\n    -+of the bundle(s) the client finds that the ref tips it wants can be\n    -+retrieved entirety from advertised bundle(s), it MAY disconnect. The\n    -+results of such a 'clone' or 'fetch' should be indistinguishable from\n    -+the state attained without using bundle-uri.\n    -+\n    -+EARLY CLIENT DISCONNECTIONS AND ERROR RECOVERY\n    -+++++++++++++++++++++++++++++++++++++++++++++++\n    -+\n    -+A client MAY perform an early disconnect while still downloading the\n    -+bundle(s) (having streamed and parsed their headers). In such a case\n    -+the client MUST gracefully recover from any errors related to\n    -+finishing the download and validation of the bundle(s).\n    -+\n    -+I.e. a client might need to re-connect and issue a 'fetch' command,\n    -+and possibly fall back to not making use of 'bundle-uri' at all.\n    -+\n    -+This \"MAY\" behavior is specified as such (and not a \"SHOULD\") on the\n    -+assumption that a server advertising bundle uris is more likely than\n    -+not to be serving up a relatively large repository, and to be pointing\n    -+to URIs that have a good chance of being in working order. A client\n    -+MAY e.g. look at the payload size of the bundles as a heuristic to see\n    -+if an early disconnect is worth it, should falling back on a full\n    -+\"fetch\" dialog be necessary.\n    -+\n    -+WHEN ADVERTISED BUNDLE(S) REQUIRE FURTHER NEGOTIATION\n    -++++++++++++++++++++++++++++++++++++++++++++++++++++++\n    -+\n    -+A client SHOULD commence a negotiation of a PACK from the server via\n    -+the \"fetch\" command using the OID tips found in advertised bundles,\n    -+even if's still in the process of downloading those bundle(s).\n    -+\n    -+This allows for aggressive early disconnects from any interactive\n    -+server dialog. The client blindly trusts that the advertised OID tips\n    -+are relevant, and issues them as 'have' lines, it then requests any\n    -+tips it would like (usually from the \"ls-refs\" advertisement) via\n    -+'want' lines. The server will then compute a (hopefully small) PACK\n    -+with the expected difference between the tips from the bundle(s) and\n    -+the data requested.\n    -+\n    -+The only connection the client then needs to keep active is to the\n    -+concurrently downloading static bundle(s), when those and the\n    -+incremental PACK are retrieved they should be inflated and\n    -+validated. Any errors at this point should be gracefully recovered\n    -+from, see above.\n    -+\n    -+bundle-uri PROTOCOL FEATURES\n    -+^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    -+\n    -+As noted above no `bundle-feature-key`=`bundle-feature-value` fields\n    -+are currently defined.\n    -+\n    -+They are intended for future per-URI metadata which older clients MUST\n    -+ignore and gracefully degrade on. Any fields they do recognize they\n    -+CAN also ignore.\n    -+\n    -+Any backwards-incompatible addition of pre-URI key-value will be\n    -+guarded by a new value or values in 'bundle-uri' capability\n    -+advertisement itself, and/or by new future `bundle-uri` request\n    -+arguments.\n    -+\n    -+While no per-URI key-value are currently supported currently they're\n    -+intended to support future features such as:\n    -+\n    -+ * Add a \"hash=<val>\" or \"size=<bytes>\" advertise the expected hash or\n    -+   size of the bundle file.\n    -+\n    -+ * Advertise that one or more bundle files are the same (to e.g. have\n    -+   clients round-robin or otherwise choose one of N possible files).\n    -+\n    -+ * A \"oid=<OID>\" shortcut and \"prerequisite=<OID>\" shortcut. For\n    -+   expressing the common case of a bundle with one tip and no\n    -+   prerequisites, or one tip and one prerequisite.\n    -++\n    -+This would allow for optimizing the common case of servers who'd like\n    -+to provide one \"big bundle\" containing only their \"main\" branch,\n    -+and/or incremental updates thereof.\n    -++\n    -+A client receiving such a a response MAY assume that they can skip\n    -+retrieving the header from a bundle at the indicated URI, and thus\n    -+save themselves and the server(s) the request(s) needed to inspect the\n    -+headers of that bundle or bundles.\n    -\n      ## Makefile ##\n     @@ Makefile: LIB_OBJS += blob.o\n      LIB_OBJS += bloom.o\n 3:  edff9c34c0f ! 10:  2a487981328 bundle-uri client: add \"bundle-uri\" parsing + tests\n    @@ bundle-uri.c: int bundle_uri_command(struct repository *r,\n     +\t\t\tBUG(\"should have no fields=0\");\n     +\t\tcase 1:\n     +\t\t\tif (!strlen(arg)) {\n    -+\t\t\t\terr = error(\"bundle-uri: column %lu: got an empty attribute (full line was '%s')\",\n    -+\t\t\t\t\t    i, line);\n    ++\t\t\t\terr = error(\"bundle-uri: column %\"PRIuMAX\": got an empty attribute (full line was '%s')\",\n    ++\t\t\t\t\t    (uintmax_t)i, line);\n     +\t\t\t\tbreak;\n     +\t\t\t}\n     +\t\t\t/*\n    @@ bundle-uri.c: int bundle_uri_command(struct repository *r,\n     +\t\t\tbreak;\n     +\t\t}\n     +\t\tdefault:\n    -+\t\t\terr = error(\"bundle-uri: column %lu: '%s' more than one '=' character (full line was '%s')\",\n    -+\t\t\t\t    i, arg, line);\n    ++\t\t\terr = error(\"bundle-uri: column %\"PRIuMAX\": '%s' more than one '=' character (full line was '%s')\",\n    ++\t\t\t\t    (uintmax_t)i, arg, line);\n     +\t\t\tbreak;\n     +\t\t}\n     +\n 5:  e3a235fb11a ! 11:  b85c2a1c0df bundle-uri client: add minimal NOOP client\n    @@ t/lib-t5730-protocol-v2-bundle-uri.sh (new)\n     +\t\t;;\n     +\t*)\n     +\t\tgrep \"^fatal:\" err >fatal.actual &&\n    -+\t\ttest_cmp fatal.expect fatal.actual\n    ++\t\t# Due to the same race conditions this might be\n    ++\t\t# \"fatal: read error: Connection reset by peer\", \"fatal: the remote end\n    ++\t\t# hung up unexpectedly\" etc.\n    ++\t\tcat fatal.actual &&\n    ++\t\ttest_file_not_empty fatal.actual\n     +\t\t;;\n     +\tesac &&\n     +\n 6:  9d67cb640c1 ! 12:  54149dcb0aa bundle-uri client: add \"git ls-remote-bundle-uri\"\n    @@ t/lib-t5730-protocol-v2-bundle-uri.sh: test_expect_success !T5730_HTTP \"bad clie\n     +\n     +test_expect_success \"ls-remote-bundle-uri --[no-]quiet with $T5730_PROTOCOL:// using protocol v2\" '\n     +\ttest_when_finished \"rm -f log\" &&\n    -+\n    -+\tcat >err.expect <<-\\EOF &&\n    -+\tCloning into '\"'\"'child'\"'\"'...\n    -+\tEOF\n    -+\n     +\ttest_when_finished \"rm -rf child\" &&\n    -+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n    ++\tenv GIT_TRACE_PACKET=\"$PWD/log\" \\\n     +\tgit \\\n     +\t\t-c protocol.version=2 \\\n    -+\t\t clone \"$T5730_URI\" child \\\n    -+\t\t >out 2>err.actual &&\n    -+\ttest_cmp err.expect err.actual &&\n    -+\ttest_must_be_empty out &&\n    ++\t\t clone \"$T5730_URI\" child &&\n     +\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n     +\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n 7:  f78d970975b = 13:  5c50daa92bb bundle-uri client: add transfer.injectBundleURI support\n 8:  716470488c5 = 14:  e66aa1f18b4 bundle-uri client: add boolean transfer.bundleURI setting\n12:  6176c4554ce ! 15:  f19d2bcbc66 bundle-uri client: support for bundle-uri with \"clone\"\n    @@ t/lib-t5730-protocol-v2-bundle-uri.sh: test_expect_success \"ls-remote-bundle-uri\n     +\ttest_cmp expect actual\n     +}\n     +\n    -+test_expect_success \"clone with bundle-uri protocol v2 over $T5730_PROTOCOL:// 1.bdl via $T5730_URI_BDL_PROTO\" '\n    ++show_cr () {\n    ++\ttr '\\015' Q | sed -e \"s/Q/<CR>\\\\$LF/g\"\n    ++}\n    ++\n    ++test_expect_success CURL \"clone with bundle-uri protocol v2 over $T5730_PROTOCOL:// 1.bdl via $T5730_URI_BDL_PROTO\" '\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n     +\n    -+\ttest_when_finished \"rm -rf log child\" &&\n    ++\ttest_when_finished \"rm -rf event log child\" &&\n    ++\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n     +\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n     +\tgit \\\n     +\t\t-c protocol.version=2 \\\n     +\t\t-c fetch.uriProtocols=file,http \\\n     +\t\tclone --verbose --verbose \\\n    -+\t\t\"$T5730_URI\" child >out 2>err &&\n    -+\tgrep -F \"Receiving bundle (1/1)\" err &&\n    ++\t\t\"$T5730_URI\" child &&\n    ++\ttest_region progress \"Receiving bundle (1/1)\" event &&\n     +\tgrep \"clone> want \" log &&\n     +\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n     +'\n    @@ t/lib-t5730-protocol-v2-bundle-uri.sh: test_expect_success \"ls-remote-bundle-uri\n     +\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n     +\n    -+\ttest_when_finished \"rm -rf log child\" &&\n    ++\ttest_when_finished \"rm -rf event log child\" &&\n     +\tgit init --bare child &&\n     +\tgit -C child remote add --mirror=fetch origin \"$T5730_URI\" &&\n    ++\n    ++\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n     +\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n     +\tgit -C child \\\n     +\t\t-c protocol.version=2 \\\n     +\t\t-c fetch.uriProtocols=file,http \\\n    -+\t\tfetch --verbose --verbose >out 2>err &&\n    -+\t# Fetch is not supported yet\n    -+\t! grep -F \"Receiving bundle (1/1)\" err &&\n    -+\tgrep \"fetch> want \" log &&\n    ++\t\tfetch --verbose --verbose &&\n    ++\n    ++\tif test_have_prereq CURL\n    ++\tthen\n    ++\t\t# Fetch is not supported yet\n    ++\t\t! test_region progress \"Receiving bundle (1/1)\" event &&\n    ++\t\tgrep \"fetch> want \" log\n    ++\telse\n    ++\t\t! grep \"fetch-pack: unable to spawn\" event\n    ++\tfi &&\n    ++\n     +\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n     +'\n     +\n    @@ t/lib-t5730-protocol-v2-bundle-uri.sh: test_expect_success \"ls-remote-bundle-uri\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/2-3.bdl | test_uri_escape)\" --add &&\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/3-4.bdl | test_uri_escape)\" --add &&\n     +\n    -+\ttest_when_finished \"rm -rf log child\" &&\n    ++\ttest_when_finished \"rm -rf event log child\" &&\n    ++\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n     +\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n     +\tgit \\\n     +\t\t-c protocol.version=2 \\\n     +\t\t-c fetch.uriProtocols=file,http \\\n     +\t\tclone --verbose --verbose \\\n    -+\t\t\"$T5730_URI\" child >out 2>err &&\n    -+\tgrep -F \"Receiving bundle (4/4)\" err &&\n    ++\t\t\"$T5730_URI\" child &&\n    ++\n    ++\tif test_have_prereq CURL\n    ++\tthen\n    ++\t\ttest_region progress \"Receiving bundle (1/4)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (2/4)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (3/4)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (4/4)\" event\n    ++\telse\n    ++\t\tgrep \"fetch-pack: unable to spawn\" event\n    ++\tfi &&\n    ++\n     +\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags &&\n     +\tgrep \"clone> want \" log\n     +'\n    @@ t/lib-t5730-protocol-v2-bundle-uri.sh: test_expect_success \"ls-remote-bundle-uri\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/4-5.bdl | test_uri_escape)\" --add &&\n     +\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/5-6.bdl | test_uri_escape)\" --add &&\n     +\n    -+\ttest_when_finished \"rm -rf log child\" &&\n    ++\ttest_when_finished \"rm -rf event log child\" &&\n    ++\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n     +\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n     +\tgit \\\n     +\t\t-c protocol.version=2 \\\n     +\t\t-c fetch.uriProtocols=file,http \\\n     +\t\tclone --verbose --verbose \\\n    -+\t\t\"$T5730_URI\" child >out 2>err &&\n    -+\tgrep -F \"Receiving bundle (6/6)\" err &&\n    -+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags &&\n    -+\t! grep \"clone> want \" log\n    ++\t\t\"$T5730_URI\" child &&\n    ++\n    ++\tif test_have_prereq CURL\n    ++\tthen\n    ++\t\ttest_region progress \"Receiving bundle (1/6)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (2/6)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (3/6)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (4/6)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (5/6)\" event &&\n    ++\t\ttest_region progress \"Receiving bundle (6/6)\" event &&\n    ++\t\t! grep \"clone> want \" log\n    ++\telse\n    ++\t\tgrep \"fetch-pack: unable to spawn\" event\n    ++\tfi &&\n    ++\n    ++\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n     +'\n     \n      ## transport.c ##\n13:  be59495d81b = 16:  632c68b224f bundle-uri: make the download program configurable\n19:  4398efebcec = 17:  8ac5bfca545 remote-curl: add 'get' capability\n20:  5cbaa40b365 ! 18:  ff9a7afaccd bundle: implement 'fetch' command for direct bundles\n    @@ Commit message\n     \n         Signed-off-by: Derrick Stolee <derrickstolee@github.com>\n     \n    + ## Documentation/git-bundle.txt ##\n    +@@ Documentation/git-bundle.txt: SYNOPSIS\n    + 'git bundle' create [-q | --quiet | --progress | --all-progress] [--all-progress-implied]\n    + \t\t    [--version=<version>] <file> <git-rev-list-args>\n    + 'git bundle' verify [-q | --quiet] <file>\n    ++'git bundle' fetch [--filter=<spec>] <uri>\n    + 'git bundle' list-heads <file> [<refname>...]\n    + 'git bundle' unbundle [--progress] <file> [<refname>...]\n    + \n    +\n      ## builtin/bundle.c ##\n     @@\n      #include \"parse-options.h\"\n21:  6c055bc2613 ! 19:  a5245a31a12 bundle: parse table of contents during 'fetch'\n    @@ Commit message\n         RFC-TODO: create tests that check these erroneous cases.\n     \n         Signed-off-by: Derrick Stolee <derrickstolee@github.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/bundle.c ##\n     @@\n    @@ builtin/bundle.c: struct remote_bundle_info {\n     +\t */\n     +\tunsigned pushed:1;\n      };\n    - \n    ++#define REMOTE_BUNDLE_INFO_INIT { \\\n    ++\t.file = STRBUF_INIT, \\\n    ++}\n    ++\n     +static int remote_bundle_cmp(const void *unused_cmp_data,\n     +\t\t\t     const struct hashmap_entry *a,\n     +\t\t\t     const struct hashmap_entry *b,\n    @@ builtin/bundle.c: struct remote_bundle_info {\n     +\tstruct hashmap *toc = data;\n     +\tconst char *key1, *key2, *id_end;\n     +\tstruct strbuf id = STRBUF_INIT;\n    -+\tstruct remote_bundle_info info_lookup;\n    ++\tstruct remote_bundle_info info_lookup = REMOTE_BUNDLE_INFO_INIT;\n     +\tstruct remote_bundle_info *info;\n     +\n     +\tif (!skip_prefix(key, \"bundle.\", &key1))\n    @@ builtin/bundle.c: struct remote_bundle_info {\n     +\t/* Return 0 here to ignore unknown options. */\n     +\treturn 0;\n     +}\n    -+\n    + \n      static void download_uri_to_file(const char *uri, const char *file)\n      {\n    - \tstruct child_process cp = CHILD_PROCESS_INIT;\n     @@ builtin/bundle.c: static void unbundle_fetched_bundle(struct remote_bundle_info *info)\n      \n      static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n    @@ builtin/bundle.c: static int cmd_bundle_fetch(int argc, const char **argv, const\n      \t\t\t * push it onto the stack.\n      \t\t\t */\n     +\t\t\tstruct remote_bundle_info *info;\n    -+\t\t\tstruct remote_bundle_info info_lookup = { 0 };\n    ++\t\t\tstruct remote_bundle_info info_lookup = REMOTE_BUNDLE_INFO_INIT;\n     +\t\t\tinfo_lookup.id = stack->requires_id;\n     +\n     +\t\t\thashmap_entry_init(&info_lookup.ent, strhash(info_lookup.id));\n22:  af61b29a571 = 20:  c3a60a9bc72 bundle: add --filter option to 'fetch'\n23:  de4845ef1c0 = 21:  c08406cd9c2 bundle: allow relative URLs in table of contents\n24:  19d25702355 = 22:  1350c19c3a1 bundle: make it easy to call 'git bundle fetch'\n25:  a20e4a5b207 ! 23:  62623324d2f clone: add --bundle-uri option\n    @@ builtin/clone.c: static struct option builtin_clone_options[] = {\n      \tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n      \t\t    N_(\"initialize sparse-checkout file to include only files at root\")),\n     +\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n    -+\t\t   N_(\"uri\"), N_(\"A URI for downloading bundles before fetching from origin remote\")),\n    ++\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n      \tOPT_END()\n      };\n      \n26:  277091d5eeb = 24:  d79901dddb0 clone: --bundle-uri cannot be combined with --depth\n28:  7b414176313 ! 25:  ab349b1cbea bundle: only fetch bundles if timestamp is new\n    @@ Commit message\n     \n         RFC-TODO: Add 'fetch.bundleTimestamp' to Documentation/config/\n     \n    +    RFC-TODO @ Ævar: I replaced the git_config_get_timestamp() with\n    +    parse_expiry_date(), but as noted perhaps we want *nix epochs here\n    +    only, in that case we could add an \"isdigit\" loop here.\n    +\n         Signed-off-by: Derrick Stolee <derrickstolee@github.com>\n     \n      ## builtin/bundle.c ##\n    @@ builtin/bundle.c: static int cmd_bundle_fetch(int argc, const char **argv, const\n     +\tconst char *timestamp_key = \"fetch.bundletimestamp\";\n     +\ttimestamp_t stored_time = 0;\n     +\ttimestamp_t max_time = 0;\n    ++\tconst char *value;\n      \n      \tstruct option options[] = {\n      \t\tOPT_BOOL(0, \"progress\", &progress,\n    @@ builtin/bundle.c: static int cmd_bundle_fetch(int argc, const char **argv, const\n      \tif (!startup_info->have_repository)\n      \t\tdie(_(\"'fetch' requires a repository\"));\n      \n    -+\tgit_config_get_timestamp(timestamp_key, &stored_time);\n    ++\t/*\n    ++\t * TODO: Is it important re\n    ++\t * https://lore.kernel.org/git/220311.86pmmshahy.gmgdl@evledraar.gmail.com/\n    ++\t * that we don't accept \"2.days.ago\" etc., and only *nix\n    ++\t * epochs?\n    ++\t */\n    ++\tif (!git_config_get_string_tmp(timestamp_key, &value) &&\n    ++\t    parse_expiry_date(value, &stored_time))\n    ++\t\treturn error(_(\"'%s' for '%s' is not a valid timestamp\"),\n    ++\t\t\t     value, timestamp_key);\n     +\n      \t/*\n      \t * Step 1: determine protocol for uri, and download contents to\n29:  857f9be78e5 = 26:  0a238641247 fetch: fetch bundles before fetching original data\n30:  85ebf44038e = 27:  5e8cec1e193 protocol-caps: implement cap_features()\n31:  e30d9a9f95d = 28:  145c660ca52 serve: understand but do not advertise 'features' capability\n32:  cf07392921d = 29:  2c9886c64ea serve: advertise 'features' when config exists\n33:  1e8c52dbe47 = 30:  e834e633e84 connect: implement get_recommended_features()\n34:  b8044bb09f0 = 31:  6611dd08f93 transport: add connections for 'features' capability\n35:  3aa4d42d2ac = 32:  2b424bedfc5 clone: use server-recommended bundle URI\n36:  6e4da9ccc85 ! 33:  52ee1e08dec t5601: basic bundle URI test\n    @@ t/t5601-clone.sh: test_expect_success 'reject cloning shallow repository using H\n     +\tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n     +\t\tgit -c protocol.version=2 clone \\\n     +\t\t$HTTPD_URL/smart/repo2.git repo &&\n    -+\ttest_subcommand_inexact git bundle unbundle <trace.txt\n    ++\tcat >pat <<-\\EOF &&\n    ++\t\"event\":\"child_start\".*\"argv\":\\[\"git\",\"bundle\",\"unbundle\"\n    ++\tEOF\n    ++\tgrep -f pat trace.txt\n     +'\n     +\n      # DO NOT add non-httpd-specific tests here, because the last part of this\n -:  ----------- > 34:  f872793cac2 protocol v2: add server-side \"bundle-uri\" skeleton (docs)\n 2:  6bc2316d2fd = 35:  cfda9323aaa bundle-uri docs: add design notes\n14:  54c4ccafd9a ! 36:  764f82a0c0a docs: document bundle URI standard\n    @@ Documentation/technical/bundle-uri-TOC.txt (new)\n     +\n     +[2] https://github.com/microsoft/VFSForGit/blob/master/Protocol.md\n     +    The GVFS Protocol\n    - \\ No newline at end of file\n27:  1173ceeb08a <  -:  ----------- config: add git_config_get_timestamp()\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453860","messageId":"RFC-patch-v2-05.36-6e1f4296896-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 05/36] http: make http_get_file() external","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:22Z","receivedAt":"2022-04-18T17:24:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThis method will be used in an upcoming extension of git-remote-curl to\ndownload a single file over HTTP(S) by request.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n http.c | 4 ++--\n http.h | 9 +++++++++\n 2 files changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/http.c b/http.c\nindex 229da4d1488..04e73149357 100644\n--- a/http.c\n+++ b/http.c\n@@ -1945,8 +1945,8 @@ int http_get_strbuf(const char *url,\n  * If a previous interrupted download is detected (i.e. a previous temporary\n  * file is still around) the download is resumed.\n  */\n-static int http_get_file(const char *url, const char *filename,\n-\t\t\t struct http_get_options *options)\n+int http_get_file(const char *url, const char *filename,\n+\t\t  struct http_get_options *options)\n {\n \tint ret;\n \tstruct strbuf tmpfile = STRBUF_INIT;\ndiff --git a/http.h b/http.h\nindex df1590e53a4..ba303cfb372 100644\n--- a/http.h\n+++ b/http.h\n@@ -163,6 +163,15 @@ struct http_get_options {\n  */\n int http_get_strbuf(const char *url, struct strbuf *result, struct http_get_options *options);\n \n+/*\n+ * Downloads a URL and stores the result in the given file.\n+ *\n+ * If a previous interrupted download is detected (i.e. a previous temporary\n+ * file is still around) the download is resumed.\n+ */\n+int http_get_file(const char *url, const char *filename,\n+\t\t  struct http_get_options *options);\n+\n int http_fetch_ref(const char *base, struct ref *ref);\n \n /* Helpers for fetching packs */\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453861","messageId":"RFC-patch-v2-07.36-2917cdd8277-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 07/36] remote: allow relative_url() to return an absolute url","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:24Z","receivedAt":"2022-04-18T17:24:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nWhen the 'url' parameter was absolute, the previous implementation would\nconcatenate 'remote_url' with 'url'. Instead, we want to return 'url' in\nthis case.\n\nThe documentation now discusses what happens when supplying two\nabsolute URLs.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n remote.c | 12 ++++++++++--\n remote.h |  1 +\n 2 files changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 87656138645..7576f673fcd 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2761,10 +2761,18 @@ char *relative_url(const char *remote_url, const char *url,\n \tint is_relative = 0;\n \tint colonsep = 0;\n \tchar *out;\n-\tchar *remoteurl = xstrdup(remote_url);\n+\tchar *remoteurl;\n \tstruct strbuf sb = STRBUF_INIT;\n-\tsize_t len = strlen(remoteurl);\n+\tsize_t len;\n+\n+\tif (!url_is_local_not_ssh(url) || is_absolute_path(url))\n+\t\treturn xstrdup(url);\n+\n+\tlen = strlen(remote_url);\n+\tif (!len)\n+\t\tBUG(\"invalid empty remote_url\");\n \n+\tremoteurl = xstrdup(remote_url);\n \tif (is_dir_sep(remoteurl[len-1]))\n \t\tremoteurl[len-1] = '\\0';\n \ndiff --git a/remote.h b/remote.h\nindex f18fd27e530..dd4402436f1 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -434,6 +434,7 @@ void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);\n  * http://a.com/b  ../../../c       http:/c          error out\n  * http://a.com/b  ../../../../c    http:c           error out\n  * http://a.com/b  ../../../../../c    .:c           error out\n+ * http://a.com/b  http://d.org/e   http://d.org/e   as is\n  * NEEDSWORK: Given how chop_last_dir() works, this function is broken\n  * when a local part has a colon in its path component, too.\n  */\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453862","messageId":"RFC-patch-v2-06.36-97a9f38f08d-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 06/36] remote: move relative_url()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:23Z","receivedAt":"2022-04-18T17:24:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThis method was initially written in 63e95beb0 (submodule: port\nresolve_relative_url from shell to C, 2016-05-15). As we will need\nsimilar functionality in the bundle URI feature, extract this to be\navailable in remote.h.\n\nThe code is almost exactly the same, except for the following trivial\ndifferences:\n\n * Fix whitespace and wrapping issues with the prototype and argument\n   lists.\n\n * Let's call starts_with_dot_{,dot_}slash_native() instead of the\n   functionally identical \"starts_with_dot_{,dot_}slash()\" wrappers\n   \"builtin/submodule--helper.c\".\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/submodule--helper.c | 141 +++---------------------------------\n remote.c                    |  91 +++++++++++++++++++++++\n remote.h                    |  31 ++++++++\n 3 files changed, 134 insertions(+), 129 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex b68102bb3ed..86f38e489b0 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -72,135 +72,6 @@ static char *get_default_remote(void)\n \treturn repo_get_default_remote(the_repository);\n }\n \n-/*\n- * Returns 1 if it was the last chop before ':'.\n- */\n-static int chop_last_dir(char **remoteurl, int is_relative)\n-{\n-\tchar *rfind = find_last_dir_sep(*remoteurl);\n-\tif (rfind) {\n-\t\t*rfind = '\\0';\n-\t\treturn 0;\n-\t}\n-\n-\trfind = strrchr(*remoteurl, ':');\n-\tif (rfind) {\n-\t\t*rfind = '\\0';\n-\t\treturn 1;\n-\t}\n-\n-\tif (is_relative || !strcmp(\".\", *remoteurl))\n-\t\tdie(_(\"cannot strip one component off url '%s'\"),\n-\t\t\t*remoteurl);\n-\n-\tfree(*remoteurl);\n-\t*remoteurl = xstrdup(\".\");\n-\treturn 0;\n-}\n-\n-static int starts_with_dot_slash(const char *const path)\n-{\n-\treturn starts_with_dot_slash_native(path);;\n-}\n-\n-static int starts_with_dot_dot_slash(const char *const path)\n-{\n-\treturn starts_with_dot_dot_slash_native(path);\n-}\n-\n-/*\n- * The `url` argument is the URL that navigates to the submodule origin\n- * repo. When relative, this URL is relative to the superproject origin\n- * URL repo. The `up_path` argument, if specified, is the relative\n- * path that navigates from the submodule working tree to the superproject\n- * working tree. Returns the origin URL of the submodule.\n- *\n- * Return either an absolute URL or filesystem path (if the superproject\n- * origin URL is an absolute URL or filesystem path, respectively) or a\n- * relative file system path (if the superproject origin URL is a relative\n- * file system path).\n- *\n- * When the output is a relative file system path, the path is either\n- * relative to the submodule working tree, if up_path is specified, or to\n- * the superproject working tree otherwise.\n- *\n- * NEEDSWORK: This works incorrectly on the domain and protocol part.\n- * remote_url      url              outcome          expectation\n- * http://a.com/b  ../c             http://a.com/c   as is\n- * http://a.com/b/ ../c             http://a.com/c   same as previous line, but\n- *                                                   ignore trailing slash in url\n- * http://a.com/b  ../../c          http://c         error out\n- * http://a.com/b  ../../../c       http:/c          error out\n- * http://a.com/b  ../../../../c    http:c           error out\n- * http://a.com/b  ../../../../../c    .:c           error out\n- * NEEDSWORK: Given how chop_last_dir() works, this function is broken\n- * when a local part has a colon in its path component, too.\n- */\n-static char *relative_url(const char *remote_url,\n-\t\t\t\tconst char *url,\n-\t\t\t\tconst char *up_path)\n-{\n-\tint is_relative = 0;\n-\tint colonsep = 0;\n-\tchar *out;\n-\tchar *remoteurl = xstrdup(remote_url);\n-\tstruct strbuf sb = STRBUF_INIT;\n-\tsize_t len = strlen(remoteurl);\n-\n-\tif (is_dir_sep(remoteurl[len-1]))\n-\t\tremoteurl[len-1] = '\\0';\n-\n-\tif (!url_is_local_not_ssh(remoteurl) || is_absolute_path(remoteurl))\n-\t\tis_relative = 0;\n-\telse {\n-\t\tis_relative = 1;\n-\t\t/*\n-\t\t * Prepend a './' to ensure all relative\n-\t\t * remoteurls start with './' or '../'\n-\t\t */\n-\t\tif (!starts_with_dot_slash(remoteurl) &&\n-\t\t    !starts_with_dot_dot_slash(remoteurl)) {\n-\t\t\tstrbuf_reset(&sb);\n-\t\t\tstrbuf_addf(&sb, \"./%s\", remoteurl);\n-\t\t\tfree(remoteurl);\n-\t\t\tremoteurl = strbuf_detach(&sb, NULL);\n-\t\t}\n-\t}\n-\t/*\n-\t * When the url starts with '../', remove that and the\n-\t * last directory in remoteurl.\n-\t */\n-\twhile (url) {\n-\t\tif (starts_with_dot_dot_slash(url)) {\n-\t\t\turl += 3;\n-\t\t\tcolonsep |= chop_last_dir(&remoteurl, is_relative);\n-\t\t} else if (starts_with_dot_slash(url))\n-\t\t\turl += 2;\n-\t\telse\n-\t\t\tbreak;\n-\t}\n-\tstrbuf_reset(&sb);\n-\tstrbuf_addf(&sb, \"%s%s%s\", remoteurl, colonsep ? \":\" : \"/\", url);\n-\tif (ends_with(url, \"/\"))\n-\t\tstrbuf_setlen(&sb, sb.len - 1);\n-\tfree(remoteurl);\n-\n-\tif (starts_with_dot_slash(sb.buf))\n-\t\tout = xstrdup(sb.buf + 2);\n-\telse\n-\t\tout = xstrdup(sb.buf);\n-\n-\tif (!up_path || !is_relative) {\n-\t\tstrbuf_release(&sb);\n-\t\treturn out;\n-\t}\n-\n-\tstrbuf_reset(&sb);\n-\tstrbuf_addf(&sb, \"%s%s\", up_path, out);\n-\tfree(out);\n-\treturn strbuf_detach(&sb, NULL);\n-}\n-\n static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet)\n {\n \tchar *remoteurl, *resolved_url;\n@@ -592,6 +463,18 @@ static int module_foreach(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int starts_with_dot_slash(const char *const path)\n+{\n+\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_SLASH |\n+\t\t\t\tPATH_MATCH_XPLATFORM);\n+}\n+\n+static int starts_with_dot_dot_slash(const char *const path)\n+{\n+\treturn path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH |\n+\t\t\t\tPATH_MATCH_XPLATFORM);\n+}\n+\n struct init_cb {\n \tconst char *prefix;\n \tconst char *superprefix;\ndiff --git a/remote.c b/remote.c\nindex 42a4e7106e1..87656138645 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -14,6 +14,7 @@\n #include \"strvec.h\"\n #include \"commit-reach.h\"\n #include \"advice.h\"\n+#include \"connect.h\"\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n@@ -2727,3 +2728,93 @@ void remote_state_clear(struct remote_state *remote_state)\n \thashmap_clear_and_free(&remote_state->remotes_hash, struct remote, ent);\n \thashmap_clear_and_free(&remote_state->branches_hash, struct remote, ent);\n }\n+\n+/*\n+ * Returns 1 if it was the last chop before ':'.\n+ */\n+static int chop_last_dir(char **remoteurl, int is_relative)\n+{\n+\tchar *rfind = find_last_dir_sep(*remoteurl);\n+\tif (rfind) {\n+\t\t*rfind = '\\0';\n+\t\treturn 0;\n+\t}\n+\n+\trfind = strrchr(*remoteurl, ':');\n+\tif (rfind) {\n+\t\t*rfind = '\\0';\n+\t\treturn 1;\n+\t}\n+\n+\tif (is_relative || !strcmp(\".\", *remoteurl))\n+\t\tdie(_(\"cannot strip one component off url '%s'\"),\n+\t\t\t*remoteurl);\n+\n+\tfree(*remoteurl);\n+\t*remoteurl = xstrdup(\".\");\n+\treturn 0;\n+}\n+\n+char *relative_url(const char *remote_url, const char *url,\n+\t\t   const char *up_path)\n+{\n+\tint is_relative = 0;\n+\tint colonsep = 0;\n+\tchar *out;\n+\tchar *remoteurl = xstrdup(remote_url);\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tsize_t len = strlen(remoteurl);\n+\n+\tif (is_dir_sep(remoteurl[len-1]))\n+\t\tremoteurl[len-1] = '\\0';\n+\n+\tif (!url_is_local_not_ssh(remoteurl) || is_absolute_path(remoteurl))\n+\t\tis_relative = 0;\n+\telse {\n+\t\tis_relative = 1;\n+\t\t/*\n+\t\t * Prepend a './' to ensure all relative\n+\t\t * remoteurls start with './' or '../'\n+\t\t */\n+\t\tif (!starts_with_dot_slash_native(remoteurl) &&\n+\t\t    !starts_with_dot_dot_slash_native(remoteurl)) {\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tstrbuf_addf(&sb, \"./%s\", remoteurl);\n+\t\t\tfree(remoteurl);\n+\t\t\tremoteurl = strbuf_detach(&sb, NULL);\n+\t\t}\n+\t}\n+\t/*\n+\t * When the url starts with '../', remove that and the\n+\t * last directory in remoteurl.\n+\t */\n+\twhile (url) {\n+\t\tif (starts_with_dot_dot_slash_native(url)) {\n+\t\t\turl += 3;\n+\t\t\tcolonsep |= chop_last_dir(&remoteurl, is_relative);\n+\t\t} else if (starts_with_dot_slash_native(url))\n+\t\t\turl += 2;\n+\t\telse\n+\t\t\tbreak;\n+\t}\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s%s%s\", remoteurl, colonsep ? \":\" : \"/\", url);\n+\tif (ends_with(url, \"/\"))\n+\t\tstrbuf_setlen(&sb, sb.len - 1);\n+\tfree(remoteurl);\n+\n+\tif (starts_with_dot_slash_native(sb.buf))\n+\t\tout = xstrdup(sb.buf + 2);\n+\telse\n+\t\tout = xstrdup(sb.buf);\n+\n+\tif (!up_path || !is_relative) {\n+\t\tstrbuf_release(&sb);\n+\t\treturn out;\n+\t}\n+\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s%s\", up_path, out);\n+\tfree(out);\n+\treturn strbuf_detach(&sb, NULL);\n+}\ndiff --git a/remote.h b/remote.h\nindex 4a1209ae2c8..f18fd27e530 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -409,4 +409,35 @@ int parseopt_push_cas_option(const struct option *, const char *arg, int unset);\n int is_empty_cas(const struct push_cas_option *);\n void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);\n \n+/*\n+ * The `url` argument is the URL that navigates to the submodule origin\n+ * repo. When relative, this URL is relative to the superproject origin\n+ * URL repo. The `up_path` argument, if specified, is the relative\n+ * path that navigates from the submodule working tree to the superproject\n+ * working tree. Returns the origin URL of the submodule.\n+ *\n+ * Return either an absolute URL or filesystem path (if the superproject\n+ * origin URL is an absolute URL or filesystem path, respectively) or a\n+ * relative file system path (if the superproject origin URL is a relative\n+ * file system path).\n+ *\n+ * When the output is a relative file system path, the path is either\n+ * relative to the submodule working tree, if up_path is specified, or to\n+ * the superproject working tree otherwise.\n+ *\n+ * NEEDSWORK: This works incorrectly on the domain and protocol part.\n+ * remote_url      url              outcome          expectation\n+ * http://a.com/b  ../c             http://a.com/c   as is\n+ * http://a.com/b/ ../c             http://a.com/c   same as previous line, but\n+ *                                                   ignore trailing slash in url\n+ * http://a.com/b  ../../c          http://c         error out\n+ * http://a.com/b  ../../../c       http:/c          error out\n+ * http://a.com/b  ../../../../c    http:c           error out\n+ * http://a.com/b  ../../../../../c    .:c           error out\n+ * NEEDSWORK: Given how chop_last_dir() works, this function is broken\n+ * when a local part has a colon in its path component, too.\n+ */\n+char *relative_url(const char *remote_url, const char *url,\n+\t\t   const char *up_path);\n+\n #endif\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453863","messageId":"RFC-patch-v2-08.36-2b236af147b-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 08/36] bundle.h: make \"fd\" version of read_bundle_header() public","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:25Z","receivedAt":"2022-04-18T17:24:35Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change the parse_bundle_header() function to be non-static, and rename\nit to parse_bundle_header_fd(). The parse_bundle_header() function is\nalready public, and it's a thin wrapper around this function. This\nwill be used by code that wants to pass a fd to the bundle API.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n bundle.c | 8 ++++----\n bundle.h | 2 ++\n 2 files changed, 6 insertions(+), 4 deletions(-)\n\ndiff --git a/bundle.c b/bundle.c\nindex d50cfb5aa7e..5fa41a52f11 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -66,8 +66,8 @@ static int parse_bundle_signature(struct bundle_header *header, const char *line\n \treturn -1;\n }\n \n-static int parse_bundle_header(int fd, struct bundle_header *header,\n-\t\t\t       const char *report_path)\n+int read_bundle_header_fd(int fd, struct bundle_header *header,\n+\t\t\t  const char *report_path)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint status = 0;\n@@ -143,7 +143,7 @@ int read_bundle_header(const char *path, struct bundle_header *header)\n \n \tif (fd < 0)\n \t\treturn error(_(\"could not open '%s'\"), path);\n-\treturn parse_bundle_header(fd, header, path);\n+\treturn read_bundle_header_fd(fd, header, path);\n }\n \n int is_bundle(const char *path, int quiet)\n@@ -153,7 +153,7 @@ int is_bundle(const char *path, int quiet)\n \n \tif (fd < 0)\n \t\treturn 0;\n-\tfd = parse_bundle_header(fd, &header, quiet ? NULL : path);\n+\tfd = read_bundle_header_fd(fd, &header, quiet ? NULL : path);\n \tif (fd >= 0)\n \t\tclose(fd);\n \tbundle_header_release(&header);\ndiff --git a/bundle.h b/bundle.h\nindex 7fef2108f43..0c052f54964 100644\n--- a/bundle.h\n+++ b/bundle.h\n@@ -24,6 +24,8 @@ void bundle_header_release(struct bundle_header *header);\n \n int is_bundle(const char *path, int quiet);\n int read_bundle_header(const char *path, struct bundle_header *header);\n+int read_bundle_header_fd(int fd, struct bundle_header *header,\n+\t\t\t  const char *report_path);\n int create_bundle(struct repository *r, const char *path,\n \t\t  int argc, const char **argv, struct strvec *pack_options,\n \t\t  int version);\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453864","messageId":"RFC-patch-v2-09.36-1496b89ea6a-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 09/36] protocol v2: add server-side \"bundle-uri\" skeleton","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:26Z","receivedAt":"2022-04-18T17:24:38Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a skeleton server-side implementation of a new \"bundle-uri\"\ncommand to protocol v2. This will allow conforming clients to\noptionally seed their initial clones or incremental fetches from URLs\ncontaining \"*.bundle\" files created with \"git bundle create\".\n\nThe use-cases are similar to those of the existing \"Packfile URIs\",\nand the two feature can be combined within a single request, but\n\"bundle-uri\" has a few advantages over packfile-uris in some some\ncommon scenarios, discussed below.\n\nThis change does not give us a working \"bundle-uri\" client, subsequent\ncommits will do that. Let's first establish what the protocol for this\nshould be like first. The client implementation will then implement\nthis specification.\n\nWith this change when the uploadpack.bundleURI config is set to a\nURI (or URIs, if set >1 times), advertise a \"bundle-uri\" command. Then\nwhen the client requests \"bundle-uri\" emit those URIs back at them.\n\nDifferences between this and the existing packfile-uri facility:\n\n A. There is no \"real\" support for packfile-uri in git.git. The\n    uploadpack.blobPackfileUri setting allows carving out a list of\n    blobs (actually any OIDs), but as alluded to in bfc2a36ff2a (Doc:\n    clarify contents of packfile sent as URI, 2021-01-20) the only\n    \"real\" implementation is JGit based.\n\n B. The uploadpack.blobPackfileUri is a MUST where this is a\n    \"CAN\". I.e. once a client says they support packfile-uri of given\n    list of protocols the server will send them a PACK response\n    assuming they've downloaded the URI they client was sent, if the\n    client doesn't do that they don't have a valid repository.\n\n    Pointing at a bundle and having the client send us \"have\"\n    lines (or not, maybe they couldn't fetch it, or decided they\n    didn't want to) is more flexible, and can gracefully recover\n    e.g. if the CDN isn't reachable (maybe you do support \"https\", but\n    the CDN provider is down, or blocked your whole country).\n\n C. The client, after executing \"ls-refs\" will disconnect if it has\n    also grabbed the \"bundle-uris\" and knows the server won't send it\n    anything it doesn't already have (or expect to have, if it's\n    downloading the bundles concurrent to an early disconnect).\n\n    This is in (small) contrast to packfile-uri where a client would\n    enter a negotiation dialog, which may or may not result in a\n    packfile-uri and/or an inline PACK.\n\n D. Because of \"C\" clients can, if the bundles are up-to-date, get an\n    up-to-date repository with just \"bundle-uri\" and \"ls-refs\" commands,\n    with no need to enter a dialog with \"git upload-pack\".\n\n    That small dialog is unlikely to matter for performance purposes,\n    this section is just noting differences between \"bundle-uri\" and\n    \"packfile-uri\".\n\nAs noted above the features are compatible, a client that supports\n\"bundle-uri\" and \"packfile-uri\" might download a bundle, and then\nproceed with a \"fetch\" dialog, that dialog might then result in\n\"packfile-uri\" response.\n\nIn practice server operators are unlikely to want to mix the two,\nsince the main benefit of either approach is the ability to offload\nlarge \"clone\" responses to CDNs. A server operator would have little\nreason not to go with one approach or the other.\n\nThere was a suggestion of implementing a similar feature long ago[1]\nby Jeff King. The main difference between it and this approach is that\nwe've since gained protocol v2, so we can add this as an optional path\nin the dialog between client and server. The 2011 implementation\nhooked into the transport mechanism to try to clone from a bundle\ndirectly. See also [2] and [3] for some later mentions of that\napproach.\n\nSee also [4] for the series that implemented\nuploadpack.blobPackfileUri, and [5] for a series on top that did the\n.gitmodules check in that context. See [6] for the \"ls-refs unborn\"\nfeature which modified code in similar areas of the request flow.\n\nFinally, there's currently a concurrent (submitted after the v1 of\nthis commit, but before the subsequent client parts of this\nimplementation) RFC of a somewhat similar \"bundle-uri\" facility at\n[7].\n\n1. https://lore.kernel.org/git/20111110074330.GA27925@sigill.intra.peff.net/\n2. https://lore.kernel.org/git/20190514092900.GA11679@sigill.intra.peff.net/\n3. https://lore.kernel.org/git/YFJWz5yIGng+a16k@coredump.intra.peff.net/\n4. https://lore.kernel.org/git/cover.1591821067.git.jonathantanmy@google.com/\n   Merged as 34e849b05a4 (Merge branch 'jt/cdn-offload', 2020-06-25)\n5. https://lore.kernel.org/git/cover.1614021092.git.jonathantanmy@google.com/\n   Merged as 6ee353d42f3 (Merge branch 'jt/transfer-fsck-across-packs',\n   2021-03-01)\n6. 69571dfe219 (Merge branch 'jt/clone-unborn-head', 2021-02-17)\n7. https://lore.kernel.org/git/pull.1160.git.1645641063.gitgitgadget@gmail.com/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile             |   1 +\n bundle-uri.c         |  55 +++++++++++++++++++\n bundle-uri.h         |  13 +++++\n serve.c              |   6 +++\n t/t5701-git-serve.sh | 124 ++++++++++++++++++++++++++++++++++++++++++-\n 5 files changed, 198 insertions(+), 1 deletion(-)\n create mode 100644 bundle-uri.c\n create mode 100644 bundle-uri.h\n\ndiff --git a/Makefile b/Makefile\nindex f8bccfab5e9..8f27310836d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -887,6 +887,7 @@ LIB_OBJS += blob.o\n LIB_OBJS += bloom.o\n LIB_OBJS += branch.o\n LIB_OBJS += bulk-checkin.o\n+LIB_OBJS += bundle-uri.o\n LIB_OBJS += bundle.o\n LIB_OBJS += cache-tree.o\n LIB_OBJS += cbtree.o\ndiff --git a/bundle-uri.c b/bundle-uri.c\nnew file mode 100644\nindex 00000000000..ff054ddc690\n--- /dev/null\n+++ b/bundle-uri.c\n@@ -0,0 +1,55 @@\n+#include \"cache.h\"\n+#include \"bundle-uri.h\"\n+#include \"pkt-line.h\"\n+#include \"config.h\"\n+\n+static void send_bundle_uris(struct packet_writer *writer,\n+\t\t\t     struct string_list *uris)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, uris)\n+\t\tpacket_writer_write(writer, \"%s\", item->string);\n+}\n+\n+static int advertise_bundle_uri = -1;\n+static struct string_list bundle_uris = STRING_LIST_INIT_DUP;\n+static int bundle_uri_config(const char *var, const char *value, void *data)\n+{\n+\tif (!strcmp(var, \"uploadpack.bundleuri\")) {\n+\t\tadvertise_bundle_uri = 1;\n+\t\tstring_list_append(&bundle_uris, value);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int bundle_uri_advertise(struct repository *r, struct strbuf *value)\n+{\n+\tif (advertise_bundle_uri != -1)\n+\t\tgoto cached;\n+\n+\tgit_config(bundle_uri_config, NULL);\n+\tadvertise_bundle_uri = !!bundle_uris.nr;\n+\n+cached:\n+\treturn advertise_bundle_uri;\n+}\n+\n+int bundle_uri_command(struct repository *r,\n+\t\t       struct packet_reader *request)\n+{\n+\tstruct packet_writer writer;\n+\tpacket_writer_init(&writer, 1);\n+\n+\twhile (packet_reader_read(request) == PACKET_READ_NORMAL)\n+\t\tdie(_(\"bundle-uri: unexpected argument: '%s'\"), request->line);\n+\tif (request->status != PACKET_READ_FLUSH)\n+\t\tdie(_(\"bundle-uri: expected flush after arguments\"));\n+\n+\tsend_bundle_uris(&writer, &bundle_uris);\n+\n+\tpacket_writer_flush(&writer);\n+\n+\treturn 0;\n+}\ndiff --git a/bundle-uri.h b/bundle-uri.h\nnew file mode 100644\nindex 00000000000..5a7e556a0ba\n--- /dev/null\n+++ b/bundle-uri.h\n@@ -0,0 +1,13 @@\n+#ifndef BUNDLE_URI_H\n+#define BUNDLE_URI_H\n+#include \"repository.h\"\n+#include \"pkt-line.h\"\n+#include \"strbuf.h\"\n+\n+/**\n+ * API used by serve.[ch].\n+ */\n+int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n+int bundle_uri_command(struct repository *r, struct packet_reader *request);\n+\n+#endif /* BUNDLE_URI_H */\ndiff --git a/serve.c b/serve.c\nindex b3fe9b5126a..f3e0203d2c6 100644\n--- a/serve.c\n+++ b/serve.c\n@@ -8,6 +8,7 @@\n #include \"protocol-caps.h\"\n #include \"serve.h\"\n #include \"upload-pack.h\"\n+#include \"bundle-uri.h\"\n \n static int advertise_sid = -1;\n static int client_hash_algo = GIT_HASH_SHA1;\n@@ -136,6 +137,11 @@ static struct protocol_capability capabilities[] = {\n \t\t.advertise = always_advertise,\n \t\t.command = cap_object_info,\n \t},\n+\t{\n+\t\t.name = \"bundle-uri\",\n+\t\t.advertise = bundle_uri_advertise,\n+\t\t.command = bundle_uri_command,\n+\t},\n };\n \n void protocol_v2_advertise_capabilities(void)\ndiff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh\nindex 1896f671cb3..9d053f77a93 100755\n--- a/t/t5701-git-serve.sh\n+++ b/t/t5701-git-serve.sh\n@@ -13,7 +13,7 @@ test_expect_success 'test capability advertisement' '\n \twrong_algo sha1:sha256\n \twrong_algo sha256:sha1\n \tEOF\n-\tcat >expect <<-EOF &&\n+\tcat >expect.base <<-EOF &&\n \tversion 2\n \tagent=git/$(git version | cut -d\" \" -f3)\n \tls-refs=unborn\n@@ -21,8 +21,11 @@ test_expect_success 'test capability advertisement' '\n \tserver-option\n \tobject-format=$(test_oid algo)\n \tobject-info\n+\tEOF\n+\tcat >expect.trailer <<-EOF &&\n \t0000\n \tEOF\n+\tcat expect.base expect.trailer >expect &&\n \n \tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n \t\t--advertise-capabilities >out &&\n@@ -342,4 +345,123 @@ test_expect_success 'basics of object-info' '\n \ttest_cmp expect actual\n '\n \n+# Test the basics of bundle-uri\n+#\n+test_expect_success 'test capability advertisement with uploadpack.bundleURI' '\n+\ttest_config uploadpack.bundleURI FAKE &&\n+\n+\tcat >expect.extra <<-EOF &&\n+\tbundle-uri\n+\tEOF\n+\tcat expect.base \\\n+\t    expect.extra \\\n+\t    expect.trailer >expect &&\n+\n+\tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n+\t\t--advertise-capabilities >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: dies if not enabled' '\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\t0000\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tfatal: invalid command '\"'\"'bundle-uri'\"'\"'\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tERR serve: invalid command '\"'\"'bundle-uri'\"'\"'\n+\tEOF\n+\n+\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out\n+'\n+\n+\n+test_expect_success 'basics of bundle-uri: enabled with single URI' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: enabled with single URI' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: enabled with two URIs' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/repo.bdl &&\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/recent.bdl --add &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0000\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\thttps://cdn.example.com/repo.bdl\n+\thttps://cdn.example.com/recent.bdl\n+\t0000\n+\tEOF\n+\n+\ttest-tool serve-v2 --stateless-rpc <in >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'basics of bundle-uri: unknown future feature(s)' '\n+\ttest_config uploadpack.bundleURI https://cdn.example.com/fake.bdl &&\n+\n+\ttest-tool pkt-line pack >in <<-EOF &&\n+\tcommand=bundle-uri\n+\tobject-format=$(test_oid algo)\n+\t0001\n+\tsome-feature\n+\twe-do-not\n+\tknow=about\n+\t0000\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tfatal: bundle-uri: unexpected argument: '\"'\"'some-feature'\"'\"'\n+\tEOF\n+\n+\ttest_must_fail test-tool serve-v2 --stateless-rpc <in >out 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_must_be_empty out\n+'\n+\n test_done\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453865","messageId":"RFC-patch-v2-10.36-2a487981328-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 10/36] bundle-uri client: add \"bundle-uri\" parsing + tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:27Z","receivedAt":"2022-04-18T17:24:41Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a \"test-tool bundle-uri parse\" which parses the format defined in\nthe newly specified \"bundle-uri\" command.\n\nAs note in the \"bundle-uri\" section in protocol-v2.txt we haven't\nspecified any key-values yet, just URI lines, but we should parse\ntheir format for conformity with the spec.\n\nWe need to make sure our future client doesn't die if this optional\ndata is ever provided by the server, and that we've covered all the\nedge cases with these key-values in our specification. Let's add and\ntest a bundle_uri_parse_line() to do that.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile                    |   1 +\n bundle-uri.c                | 124 +++++++++++++++++++++++++++++\n bundle-uri.h                |  16 ++++\n t/helper/test-bundle-uri.c  |  83 +++++++++++++++++++\n t/helper/test-tool.c        |   1 +\n t/helper/test-tool.h        |   1 +\n t/t5750-bundle-uri-parse.sh | 153 ++++++++++++++++++++++++++++++++++++\n 7 files changed, 379 insertions(+)\n create mode 100644 t/helper/test-bundle-uri.c\n create mode 100755 t/t5750-bundle-uri-parse.sh\n\ndiff --git a/Makefile b/Makefile\nindex 8f27310836d..c8a14793005 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -706,6 +706,7 @@ PROGRAMS += $(patsubst %.o,git-%$X,$(PROGRAM_OBJS))\n TEST_BUILTINS_OBJS += test-advise.o\n TEST_BUILTINS_OBJS += test-bitmap.o\n TEST_BUILTINS_OBJS += test-bloom.o\n+TEST_BUILTINS_OBJS += test-bundle-uri.o\n TEST_BUILTINS_OBJS += test-chmtime.o\n TEST_BUILTINS_OBJS += test-config.o\n TEST_BUILTINS_OBJS += test-crontab.o\ndiff --git a/bundle-uri.c b/bundle-uri.c\nindex ff054ddc690..33386769f55 100644\n--- a/bundle-uri.c\n+++ b/bundle-uri.c\n@@ -53,3 +53,127 @@ int bundle_uri_command(struct repository *r,\n \n \treturn 0;\n }\n+\n+/**\n+ * General API for {transport,connect}.c etc.\n+ */\n+int bundle_uri_parse_line(struct string_list *bundle_uri, const char *line)\n+{\n+\tsize_t i;\n+\tstruct string_list columns = STRING_LIST_INIT_DUP;\n+\tconst char *uri;\n+\tstruct string_list *uri_columns = NULL;\n+\tint ret = 0;\n+\n+\tif (!strlen(line))\n+\t\treturn error(_(\"bundle-uri: got an empty line\"));\n+\n+\t/*\n+\t * Right now we don't understand anything beyond the first SP,\n+\t * but let's be tolerant and ignore any future unknown\n+\t * fields. See the \"MUST\" note about \"bundle-feature-key\" in\n+\t * Documentation/technical/protocol-v2.txt\n+\t */\n+\tif (string_list_split(&columns, line, ' ', -1) < 1)\n+\t\treturn error(_(\"bundle-uri: line not in SP-delimited format: %s\"), line);\n+\n+\t/*\n+\t * We represent a \"<uri>[ <key-values>...]\" line with the URI\n+\t * being the .string in a string list, and the .util being an\n+\t * optional string list of key (.string) and values\n+\t * (.util). If the top-level .util is NULL there's no\n+\t * key-value pairs....\n+\t */\n+\turi = columns.items[0].string;\n+\tif (!strlen(uri)) {\n+\t\tret = error(_(\"bundle-uri: got an empty URI component\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/*\n+\t * ... we're going to need that non-NULL .util .\n+\t */\n+\tif (columns.nr > 1) {\n+\t\turi_columns = xcalloc(1, sizeof(struct string_list));\n+\t\tstring_list_init_dup(uri_columns);\n+\t}\n+\n+\t/*\n+\t * Let's parse the optional \"kv\" format, even if we don't\n+\t * understand any of the keys or values yet.\n+\t */\n+\tfor (i = 1; i < columns.nr; i++) {\n+\t\tstruct string_list kv = STRING_LIST_INIT_DUP;\n+\t\tconst char *arg = columns.items[i].string;\n+\t\tint fields = string_list_split(&kv, arg, '=', 2);\n+\t\tint err = 0;\n+\n+\t\tswitch (fields) {\n+\t\tcase 0:\n+\t\t\tBUG(\"should have no fields=0\");\n+\t\tcase 1:\n+\t\t\tif (!strlen(arg)) {\n+\t\t\t\terr = error(\"bundle-uri: column %\"PRIuMAX\": got an empty attribute (full line was '%s')\",\n+\t\t\t\t\t    (uintmax_t)i, line);\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\t/*\n+\t\t\t * We could dance around with\n+\t\t\t * string_list_append_nodup() and skip\n+\t\t\t * string_list_clear(&kv, 0) here, but let's\n+\t\t\t * keep it simple.\n+\t\t\t */\n+\t\t\tstring_list_append(uri_columns, arg);\n+\t\t\tbreak;\n+\t\tcase 2:\n+\t\t{\n+\t\t\tconst char *k = kv.items[0].string;\n+\t\t\tconst char *v = kv.items[1].string;\n+\n+\t\t\tstring_list_append(uri_columns, k)->util = xstrdup(v);\n+\t\t\tbreak;\n+\t\t}\n+\t\tdefault:\n+\t\t\terr = error(\"bundle-uri: column %\"PRIuMAX\": '%s' more than one '=' character (full line was '%s')\",\n+\t\t\t\t    (uintmax_t)i, arg, line);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tstring_list_clear(&kv, 0);\n+\t\tif (err) {\n+\t\t\tret = err;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\n+\t/*\n+\t * Per the spec we'll only consider bundle-uri lines OK if\n+\t * there were no parsing problems, even if the problems were\n+\t * with attributes whose content we don't understand.\n+\t */\n+\tif (ret && uri_columns) {\n+\t\tstring_list_clear(uri_columns, 1);\n+\t\tfree(uri_columns);\n+\t} else if (!ret) {\n+\t\tstring_list_append(bundle_uri, uri)->util = uri_columns;\n+\t}\n+\n+cleanup:\n+\tstring_list_clear(&columns, 0);\n+\treturn ret;\n+}\n+\n+static void bundle_uri_string_list_clear_cb(void *util, const char *string)\n+{\n+\tstruct string_list *fields = util;\n+\tif (!fields)\n+\t\treturn;\n+\tstring_list_clear(fields, 1);\n+\tfree(fields);\n+}\n+\n+void bundle_uri_string_list_clear(struct string_list *bundle_uri)\n+{\n+\tstring_list_clear_func(bundle_uri, bundle_uri_string_list_clear_cb);\n+}\ndiff --git a/bundle-uri.h b/bundle-uri.h\nindex 5a7e556a0ba..be6d1df97ff 100644\n--- a/bundle-uri.h\n+++ b/bundle-uri.h\n@@ -3,6 +3,7 @@\n #include \"repository.h\"\n #include \"pkt-line.h\"\n #include \"strbuf.h\"\n+#include \"string-list.h\"\n \n /**\n  * API used by serve.[ch].\n@@ -10,4 +11,19 @@\n int bundle_uri_advertise(struct repository *r, struct strbuf *value);\n int bundle_uri_command(struct repository *r, struct packet_reader *request);\n \n+/**\n+ * General API for {transport,connect}.c etc.\n+ */\n+\n+/**\n+ * bundle_uri_parse_line() returns 0 when a valid bundle-uri has been\n+ * added to `bundle_uri`, <0 on error.\n+ */\n+int bundle_uri_parse_line(struct string_list *bundle_uri, const char *line);\n+\n+/**\n+ * Clear the `bundle_uri` list. Just a very thin wrapper on\n+ * string_list_clear().\n+ */\n+void bundle_uri_string_list_clear(struct string_list *bundle_uri);\n #endif /* BUNDLE_URI_H */\ndiff --git a/t/helper/test-bundle-uri.c b/t/helper/test-bundle-uri.c\nnew file mode 100644\nindex 00000000000..805a86c0130\n--- /dev/null\n+++ b/t/helper/test-bundle-uri.c\n@@ -0,0 +1,83 @@\n+#include \"test-tool.h\"\n+#include \"parse-options.h\"\n+#include \"bundle-uri.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n+\n+static int cmd__bundle_uri_parse(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool bundle-uri parse <in\",\n+\t\tNULL\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tint err = 0;\n+\tstruct string_list_item *item;\n+\tsize_t line_nr = 0;\n+\n+\targc = parse_options(argc, argv, NULL, options, usage, 0);\n+\tif (argc)\n+\t\tgoto usage;\n+\n+\twhile (strbuf_getline(&sb, stdin) != EOF) {\n+\t\tline_nr++;\n+\t\tif (bundle_uri_parse_line(&list, sb.buf) < 0)\n+\t\t\terr = error(\"bad line: '%s'\", sb.buf);\n+\t}\n+\n+\tfor_each_string_list_item(item, &list) {\n+\t\tstruct string_list_item *kv_item;\n+\t\tstruct string_list *kv = item->util;\n+\n+\t\tfprintf(stdout, \"%s\", item->string);\n+\t\tif (!kv) {\n+\t\t\tfprintf(stdout, \"\\n\");\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfor_each_string_list_item(kv_item, kv) {\n+\t\t\tconst char *k = kv_item->string;\n+\t\t\tconst char *v = kv_item->util;\n+\n+\t\t\tif (v)\n+\t\t\t\tfprintf(stdout, \" [kv: %s => %s]\", k, v);\n+\t\t\telse\n+\t\t\t\tfprintf(stdout, \" [attr: %s]\", k);\n+\t\t}\n+\t\tfprintf(stdout, \"\\n\");\n+\t}\n+\tstrbuf_release(&sb);\n+\n+\tbundle_uri_string_list_clear(&list);\n+\n+\treturn err < 0 ? 1 : 0;\n+usage:\n+\tusage_with_options(usage, options);\n+}\n+\n+int cmd__bundle_uri(int argc, const char **argv)\n+{\n+\tconst char *usage[] = {\n+\t\t\"test-tool bundle-uri <subcommand> [<options>]\",\n+\t\tNULL\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, options, usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION |\n+\t\t\t     PARSE_OPT_KEEP_ARGV0);\n+\tif (argc == 1)\n+\t\tgoto usage;\n+\n+\tif (!strcmp(argv[1], \"parse\"))\n+\t\treturn cmd__bundle_uri_parse(argc - 1, argv + 1);\n+\terror(\"there is no test-tool bundle-uri tool '%s'\", argv[1]);\n+\n+usage:\n+\tusage_with_options(usage, options);\n+}\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex 0424f7adf5d..bff823fbd3e 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -17,6 +17,7 @@ static struct test_cmd cmds[] = {\n \t{ \"advise\", cmd__advise_if_enabled },\n \t{ \"bitmap\", cmd__bitmap },\n \t{ \"bloom\", cmd__bloom },\n+\t{ \"bundle-uri\", cmd__bundle_uri },\n \t{ \"chmtime\", cmd__chmtime },\n \t{ \"config\", cmd__config },\n \t{ \"crontab\", cmd__crontab },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex c876e8246fb..eb747e07dd1 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -7,6 +7,7 @@\n int cmd__advise_if_enabled(int argc, const char **argv);\n int cmd__bitmap(int argc, const char **argv);\n int cmd__bloom(int argc, const char **argv);\n+int cmd__bundle_uri(int argc, const char **argv);\n int cmd__chmtime(int argc, const char **argv);\n int cmd__config(int argc, const char **argv);\n int cmd__crontab(int argc, const char **argv);\ndiff --git a/t/t5750-bundle-uri-parse.sh b/t/t5750-bundle-uri-parse.sh\nnew file mode 100755\nindex 00000000000..70fd1b398e9\n--- /dev/null\n+++ b/t/t5750-bundle-uri-parse.sh\n@@ -0,0 +1,153 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri bundle_uri_parse_line()\"\n+\n+TEST_NO_CREATE_REPO=1\n+TEST_PASSES_SANITIZE_LEAK=true\n+. ./test-lib.sh\n+\n+test_expect_success 'bundle_uri_parse_line() just URIs' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle.bdl\n+\thttps://example.com/bundle.bdl\n+\tfile:///usr/share/git/bundle.bdl\n+\tEOF\n+\n+\t# For the simple case\n+\tcp in expect &&\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() with attributes' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl attr\n+\thttp://example.com/bundle2.bdl ibute\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: attr]\n+\thttp://example.com/bundle2.bdl [attr: ibute]\n+\tEOF\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() with attributes and key-value attributes' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl x a=b y c=d z e=f a=b\n+\tEOF\n+\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: x] [kv: a => b] [attr: y] [kv: c => d] [attr: z] [kv: e => f] [kv: a => b]\n+\tEOF\n+\n+\ttest-tool bundle-uri parse <in >actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: extra SP' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl one-space\n+\thttp://example.com/bundle2.bdl  two-space\n+\thttp://example.com/bundle3.bdl   three-space\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: column 1: got an empty attribute (full line was '\\''http://example.com/bundle2.bdl  two-space'\\'')\n+\terror: bad line: '\\''http://example.com/bundle2.bdl  two-space'\\''\n+\terror: bundle-uri: column 1: got an empty attribute (full line was '\\''http://example.com/bundle3.bdl   three-space'\\'')\n+\terror: bad line: '\\''http://example.com/bundle3.bdl   three-space'\\''\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl [attr: one-space]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty lines' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\n+\thttp://example.com/bundle2.bdl a=b\n+\n+\thttp://example.com/bundle3.bdl\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: got an empty line\n+\terror: bad line: '\\'''\\''\n+\terror: bundle-uri: got an empty line\n+\terror: bad line: '\\'''\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\thttp://example.com/bundle2.bdl [kv: a => b]\n+\thttp://example.com/bundle3.bdl\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: empty URIs' '\n+\tsed \"s/> //\" >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\t>  a=b\n+\thttp://example.com/bundle3.bdl a=b\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: got an empty URI component\n+\terror: bad line: '\\'' a=b'\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl\n+\thttp://example.com/bundle3.bdl [kv: a => b]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'bundle_uri_parse_line() parsing edge cases: multiple = in key-values' '\n+\tcat >in <<-\\EOF &&\n+\thttp://example.com/bundle1.bdl k=v=extra\n+\thttp://example.com/bundle2.bdl a=b k=v=extra c=d\n+\thttp://example.com/bundle3.bdl kv=ok\n+\tEOF\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bundle-uri: column 1: '\\''k=v=extra'\\'' more than one '\\''='\\'' character (full line was '\\''http://example.com/bundle1.bdl k=v=extra'\\'')\n+\terror: bad line: '\\''http://example.com/bundle1.bdl k=v=extra'\\''\n+\terror: bundle-uri: column 2: '\\''k=v=extra'\\'' more than one '\\''='\\'' character (full line was '\\''http://example.com/bundle2.bdl a=b k=v=extra c=d'\\'')\n+\terror: bad line: '\\''http://example.com/bundle2.bdl a=b k=v=extra c=d'\\''\n+\tEOF\n+\n+\t# We fail, but try to continue parsing regardless\n+\tcat >expect <<-\\EOF &&\n+\thttp://example.com/bundle3.bdl [kv: kv => ok]\n+\tEOF\n+\n+\ttest_must_fail test-tool bundle-uri parse <in >actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453866","messageId":"RFC-patch-v2-11.36-b85c2a1c0df-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 11/36] bundle-uri client: add minimal NOOP client","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:28Z","receivedAt":"2022-04-18T17:24:44Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Set up all the needed client parts of the \"bundle-uri\" protocol\nextension, without actually doing anything with the bundle URIs.\n\nI.e. if the server says it supports \"bundle-uri\" we'll issue a\ncommand=bundle-uri after command=ls-refs when we're cloning. We'll\nparse the returned output using the code already tested for in\nt5750-bundle-uri-parse.sh.\n\nWhat we aren't doing is actually acting on that data, i.e. downloading\nthe bundle(s) before we get to doing the command=fetch, and adjusting\nour negotiation dialog appropriately. I'll do that in subsequent\ncommits.\n\nThere's a question of what level of encapsulation we should use here,\nI've opted to use connect.h in clone.c, but we could also e.g. make\ntransport_get_remote_refs() invoke this, i.e. make it implicitly get\nthe bundle-uri list for later steps.\n\nThis approach means that we don't \"support\" this in \"git fetch\" for\nnow. I'm starting with the case of initial clones, although as noted\nin preceding commits to the protocol documentation nothing about this\napproach precludes getting bundles on incremental fetches.\n\nFor the t5732-protocol-v2-bundle-uri-http.sh it's not easy to set\nenvironment variables for git-upload-pack (it's started by Apache), so\nlet's skip the test under T5730_HTTP, and add unused T5730_{FILE,GIT}\nprerequisites for consistency and future use.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/clone.c                        |   7 ++\n bundle-uri.c                           |   4 +\n connect.c                              |  47 ++++++++\n remote.h                               |   4 +\n t/lib-t5730-protocol-v2-bundle-uri.sh  | 145 +++++++++++++++++++++++++\n t/t5730-protocol-v2-bundle-uri-file.sh |  36 ++++++\n t/t5731-protocol-v2-bundle-uri-git.sh  |  17 +++\n t/t5732-protocol-v2-bundle-uri-http.sh |  17 +++\n transport-helper.c                     |  13 +++\n transport-internal.h                   |   7 ++\n transport.c                            |  48 ++++++++\n transport.h                            |  18 +++\n 12 files changed, 363 insertions(+)\n create mode 100644 t/lib-t5730-protocol-v2-bundle-uri.sh\n create mode 100755 t/t5730-protocol-v2-bundle-uri-file.sh\n create mode 100755 t/t5731-protocol-v2-bundle-uri-git.sh\n create mode 100755 t/t5732-protocol-v2-bundle-uri-http.sh\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 52316563795..709f1502f91 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -27,6 +27,7 @@\n #include \"iterator.h\"\n #include \"sigchain.h\"\n #include \"branch.h\"\n+#include \"connect.h\"\n #include \"remote.h\"\n #include \"run-command.h\"\n #include \"connected.h\"\n@@ -1235,6 +1236,12 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tif (refs)\n \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n \n+\t/*\n+\t * Populate transport->got_remote_bundle_uri and\n+\t * transport->bundle_uri. We might get nothing.\n+\t */\n+\ttransport_get_remote_bundle_uri(transport);\n+\n \tif (mapped_refs) {\n \t\tint hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport));\n \ndiff --git a/bundle-uri.c b/bundle-uri.c\nindex 33386769f55..09493140299 100644\n--- a/bundle-uri.c\n+++ b/bundle-uri.c\n@@ -26,6 +26,10 @@ static int bundle_uri_config(const char *var, const char *value, void *data)\n \n int bundle_uri_advertise(struct repository *r, struct strbuf *value)\n {\n+\tif (value &&\n+\t    git_env_bool(\"GIT_TEST_BUNDLE_URI_UNKNOWN_CAPABILITY_VALUE\", 0))\n+\t\tstrbuf_addstr(value, \"test-unknown-capability-value\");\n+\n \tif (advertise_bundle_uri != -1)\n \t\tgoto cached;\n \ndiff --git a/connect.c b/connect.c\nindex e6d0b1d34bd..a8fdb5255f7 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -15,6 +15,7 @@\n #include \"version.h\"\n #include \"protocol.h\"\n #include \"alias.h\"\n+#include \"bundle-uri.h\"\n \n static char *server_capabilities_v1;\n static struct strvec server_capabilities_v2 = STRVEC_INIT;\n@@ -491,6 +492,52 @@ static void send_capabilities(int fd_out, struct packet_reader *reader)\n \t}\n }\n \n+int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,\n+\t\t\t  struct string_list *bundle_uri, int stateless_rpc)\n+{\n+\tint line_nr = 1;\n+\n+\t/* Assert bundle-uri support */\n+\tserver_supports_v2(\"bundle-uri\", 1);\n+\n+\t/* (Re-)send capabilities */\n+\tsend_capabilities(fd_out, reader);\n+\n+\t/* Send command */\n+\tpacket_write_fmt(fd_out, \"command=bundle-uri\\n\");\n+\tpacket_delim(fd_out);\n+\n+\t/* Send options */\n+\tif (git_env_bool(\"GIT_TEST_PROTOCOL_BAD_BUNDLE_URI\", 0))\n+\t\tpacket_write_fmt(fd_out, \"test-bad-client\\n\");\n+\tpacket_flush(fd_out);\n+\n+\t/* Process response from server */\n+\twhile (packet_reader_read(reader) == PACKET_READ_NORMAL) {\n+\t\tconst char *line = reader->line;\n+\t\tline_nr++;\n+\n+\t\tif (!bundle_uri_parse_line(bundle_uri, line))\n+\t\t\tcontinue;\n+\n+\t\treturn error(_(\"error on bundle-uri response line %d: %s\"),\n+\t\t\t     line_nr, line);\n+\t}\n+\n+\tif (reader->status != PACKET_READ_FLUSH)\n+\t\treturn error(_(\"expected flush after bundle-uri listing\"));\n+\n+\t/*\n+\t * Might die(), but obscure enough that that's OK, e.g. in\n+\t * serve.c we'll call BUG() on its equivalent (the\n+\t * PACKET_READ_RESPONSE_END check).\n+\t */\n+\tcheck_stateless_delimiter(stateless_rpc, reader,\n+\t\t\t\t  _(\"expected response end packet after ref listing\"));\n+\n+\treturn 0;\n+}\n+\n struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     struct ref **list, int for_push,\n \t\t\t     struct transport_ls_refs_options *transport_options,\ndiff --git a/remote.h b/remote.h\nindex dd4402436f1..571338510a8 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -236,6 +236,10 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \t\t\t     const struct string_list *server_options,\n \t\t\t     int stateless_rpc);\n \n+/* Used for protocol v2 in order to retrieve refs from a remote */\n+int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,\n+\t\t\t  struct string_list *bundle_uri, int stateless_rpc);\n+\n int resolve_remote_symref(struct ref *ref, struct ref *list);\n \n /*\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nnew file mode 100644\nindex 00000000000..7a90c80f0b1\n--- /dev/null\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -0,0 +1,145 @@\n+# Included from t573*-protocol-v2-bundle-uri-*.sh\n+\n+T5730_PARENT=\n+T5730_URI=\n+T5730_BUNDLE_URI=\n+case \"$T5730_PROTOCOL\" in\n+file)\n+\tT5730_PARENT=file_parent\n+\tT5730_URI=\"file://$PWD/file_parent\"\n+\tT5730_BUNDLE_URI=\"$T5730_URI/fake.bdl\"\n+\ttest_set_prereq T5730_FILE\n+\t;;\n+git)\n+\t. \"$TEST_DIRECTORY\"/lib-git-daemon.sh\n+\tstart_git_daemon --export-all --enable=receive-pack\n+\tT5730_PARENT=\"$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent\"\n+\tT5730_URI=\"$GIT_DAEMON_URL/parent\"\n+\tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n+\ttest_set_prereq T5730_GIT\n+\t;;\n+http)\n+\t. \"$TEST_DIRECTORY\"/lib-httpd.sh\n+\tstart_httpd\n+\tT5730_PARENT=\"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\"\n+\tT5730_URI=\"$HTTPD_URL/smart/http_parent\"\n+\tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n+\ttest_set_prereq T5730_HTTP\n+\t;;\n+*)\n+\tBUG \"Need to pass valid T5730_PROTOCOL (was $T5730_PROTOCOL)\"\n+\t;;\n+esac\n+\n+test_expect_success \"setup protocol v2 $T5730_PROTOCOL:// tests\" '\n+\tgit init \"$T5730_PARENT\" &&\n+\ttest_commit -C \"$T5730_PARENT\" one\n+'\n+\n+# Poor man's URI escaping. Good enough for the test suite whose trash\n+# directory has a space in it. See 93c3fcbe4d4 (git-svn: attempt to\n+# mimic SVN 1.7 URL canonicalization, 2012-07-28) for prior art.\n+test_uri_escape() {\n+\tsed 's/ /%20/g'\n+}\n+\n+case \"$T5730_PROTOCOL\" in\n+http)\n+\ttest_expect_success \"setup config for $T5730_PROTOCOL:// tests\" '\n+\t\tgit -C \"$T5730_PARENT\" config http.receivepack true\n+\t'\n+\t;;\n+*)\n+\t;;\n+esac\n+T5730_BUNDLE_URI_ESCAPED=$(echo \"$T5730_BUNDLE_URI\" | test_uri_escape)\n+\n+test_expect_success \"connect with $T5730_PROTOCOL:// using protocol v2: no bundle-uri\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote --symref \"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\n+\t# Server responded using protocol v2\n+\tgrep \"< version 2\" log &&\n+\n+\t! grep bundle-uri log\n+'\n+\n+test_expect_success \"connect with $T5730_PROTOCOL:// using protocol v2: have bundle-uri\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" \\\n+\t\tuploadpack.bundleURI \"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote --symref \"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\n+\t# Server responded using protocol v2\n+\tgrep \"< version 2\" log &&\n+\n+\t# Server advertised bundle-uri capability\n+\tgrep bundle-uri log\n+'\n+\n+test_expect_success !T5730_HTTP \"bad client with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tcat >err.expect <<-\\EOF &&\n+\tCloning into '\"'\"'child'\"'\"'...\n+\tEOF\n+\tcase \"$T5730_PROTOCOL\" in\n+\tfile)\n+\t\tcat >fatal-bundle-uri.expect <<-\\EOF\n+\t\tfatal: bundle-uri: unexpected argument: '\"'\"'test-bad-client'\"'\"'\n+\t\tEOF\n+\t\t;;\n+\t*)\n+\t\tcat >fatal.expect <<-\\EOF\n+\t\tfatal: read error: Connection reset by peer\n+\t\tEOF\n+\t\t;;\n+\tesac &&\n+\n+\ttest_when_finished \"rm -rf child\" &&\n+\ttest_must_fail ok=sigpipe env \\\n+\t\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\t\tGIT_TEST_PROTOCOL_BAD_BUNDLE_URI=true \\\n+\t\tgit -c protocol.version=2 \\\n+\t\tclone \"$T5730_URI\" child \\\n+\t\t>out 2>err &&\n+\ttest_must_be_empty out &&\n+\n+\tgrep -v -e ^fatal: -e ^error: err >err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\n+\tcase \"$T5730_PROTOCOL\" in\n+\tfile)\n+\t\t# Due to general race conditions with client/server replies we\n+\t\t# may or may not get \"fatal: the remote end hung up\n+\t\t# expectedly\" here\n+\t\tgrep \"^fatal: bundle-uri:\" err >fatal-bundle-uri.actual &&\n+\t\ttest_cmp fatal-bundle-uri.expect fatal-bundle-uri.actual\n+\t\t;;\n+\t*)\n+\t\tgrep \"^fatal:\" err >fatal.actual &&\n+\t\t# Due to the same race conditions this might be\n+\t\t# \"fatal: read error: Connection reset by peer\", \"fatal: the remote end\n+\t\t# hung up unexpectedly\" etc.\n+\t\tcat fatal.actual &&\n+\t\ttest_file_not_empty fatal.actual\n+\t\t;;\n+\tesac &&\n+\n+\tgrep \"clone> test-bad-client$\" log >sent-bad-request &&\n+\ttest_file_not_empty sent-bad-request\n+'\ndiff --git a/t/t5730-protocol-v2-bundle-uri-file.sh b/t/t5730-protocol-v2-bundle-uri-file.sh\nnew file mode 100755\nindex 00000000000..89203d3a23c\n--- /dev/null\n+++ b/t/t5730-protocol-v2-bundle-uri-file.sh\n@@ -0,0 +1,36 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri with protocol v2 and 'file://' transport\"\n+\n+TEST_NO_CREATE_REPO=1\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+# Test protocol v2 with 'file://' transport\n+#\n+T5730_PROTOCOL=file\n+. \"$TEST_DIRECTORY\"/lib-t5730-protocol-v2-bundle-uri.sh\n+\n+test_expect_success \"unknown capability value with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" \\\n+\t\tuploadpack.bundleURI \"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tGIT_TEST_BUNDLE_URI_UNKNOWN_CAPABILITY_VALUE=true \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote --symref \"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\n+\t# Server responded using protocol v2\n+\tgrep \"< version 2\" log &&\n+\n+\tgrep \"> bundle-uri=test-unknown-capability-value\" log\n+'\n+\n+test_done\ndiff --git a/t/t5731-protocol-v2-bundle-uri-git.sh b/t/t5731-protocol-v2-bundle-uri-git.sh\nnew file mode 100755\nindex 00000000000..282847b311f\n--- /dev/null\n+++ b/t/t5731-protocol-v2-bundle-uri-git.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri with protocol v2 and 'git://' transport\"\n+\n+TEST_NO_CREATE_REPO=1\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+# Test protocol v2 with 'git://' transport\n+#\n+T5730_PROTOCOL=git\n+. \"$TEST_DIRECTORY\"/lib-t5730-protocol-v2-bundle-uri.sh\n+\n+test_done\ndiff --git a/t/t5732-protocol-v2-bundle-uri-http.sh b/t/t5732-protocol-v2-bundle-uri-http.sh\nnew file mode 100755\nindex 00000000000..fcc1cf3faef\n--- /dev/null\n+++ b/t/t5732-protocol-v2-bundle-uri-http.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description=\"Test bundle-uri with protocol v2 and 'git://' transport\"\n+\n+TEST_NO_CREATE_REPO=1\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+# Test protocol v2 with 'git://' transport\n+#\n+T5730_PROTOCOL=http\n+. \"$TEST_DIRECTORY\"/lib-t5730-protocol-v2-bundle-uri.sh\n+\n+test_done\ndiff --git a/transport-helper.c b/transport-helper.c\nindex b4dbbabb0c2..398712c76f3 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -1267,9 +1267,22 @@ static struct ref *get_refs_list_using_list(struct transport *transport,\n \treturn ret;\n }\n \n+static int get_bundle_uri(struct transport *transport)\n+{\n+\tget_helper(transport);\n+\n+\tif (process_connect(transport, 0)) {\n+\t\tdo_take_over(transport);\n+\t\treturn transport->vtable->get_bundle_uri(transport);\n+\t}\n+\n+\treturn -1;\n+}\n+\n static struct transport_vtable vtable = {\n \t.set_option\t= set_helper_option,\n \t.get_refs_list\t= get_refs_list,\n+\t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs,\n \t.push_refs\t= push_refs,\n \t.connect\t= connect_helper,\ndiff --git a/transport-internal.h b/transport-internal.h\nindex c4ca0b733ac..90ea749e5cf 100644\n--- a/transport-internal.h\n+++ b/transport-internal.h\n@@ -26,6 +26,13 @@ struct transport_vtable {\n \tstruct ref *(*get_refs_list)(struct transport *transport, int for_push,\n \t\t\t\t     struct transport_ls_refs_options *transport_options);\n \n+\t/**\n+\t * Populates the remote side's bundle-uri under protocol v2,\n+\t * if the \"bundle-uri\" capability was advertised. Returns 0 if\n+\t * OK, negative values on error.\n+\t */\n+\tint (*get_bundle_uri)(struct transport *transport);\n+\n \t/**\n \t * Fetch the objects for the given refs. Note that this gets\n \t * an array, and should ignore the list structure.\ndiff --git a/transport.c b/transport.c\nindex 3d64a43ab39..9a31e3f996b 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -22,6 +22,7 @@\n #include \"protocol.h\"\n #include \"object-store.h\"\n #include \"color.h\"\n+#include \"bundle-uri.h\"\n \n static int transport_use_color = -1;\n static char transport_colors[][COLOR_MAXLEN] = {\n@@ -359,6 +360,21 @@ static struct ref *get_refs_via_connect(struct transport *transport, int for_pus\n \treturn handshake(transport, for_push, options, 1);\n }\n \n+static int get_bundle_uri(struct transport *transport)\n+{\n+\tstruct git_transport_data *data = transport->data;\n+\tstruct packet_reader reader;\n+\tint stateless_rpc = transport->stateless_rpc;\n+\tstring_list_init_dup(&transport->bundle_uri);\n+\n+\tpacket_reader_init(&reader, data->fd[0], NULL, 0,\n+\t\t\t   PACKET_READ_CHOMP_NEWLINE |\n+\t\t\t   PACKET_READ_GENTLE_ON_EOF);\n+\n+\treturn get_remote_bundle_uri(data->fd[1], &reader,\n+\t\t\t\t     &transport->bundle_uri, stateless_rpc);\n+}\n+\n static int fetch_refs_via_pack(struct transport *transport,\n \t\t\t       int nr_heads, struct ref **to_fetch)\n {\n@@ -899,6 +915,7 @@ static int disconnect_git(struct transport *transport)\n \n static struct transport_vtable taken_over_vtable = {\n \t.get_refs_list\t= get_refs_via_connect,\n+\t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs_via_pack,\n \t.push_refs\t= git_transport_push,\n \t.disconnect\t= disconnect_git\n@@ -1052,6 +1069,7 @@ static struct transport_vtable bundle_vtable = {\n \n static struct transport_vtable builtin_smart_vtable = {\n \t.get_refs_list\t= get_refs_via_connect,\n+\t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs_via_pack,\n \t.push_refs\t= git_transport_push,\n \t.connect\t= connect_git,\n@@ -1065,6 +1083,7 @@ struct transport *transport_get(struct remote *remote, const char *url)\n \n \tret->progress = isatty(2);\n \tstring_list_init_dup(&ret->pack_lockfiles);\n+\tstring_list_init_dup(&ret->bundle_uri);\n \n \tif (!remote)\n \t\tBUG(\"No remote provided to transport_get()\");\n@@ -1473,6 +1492,34 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n+int transport_get_remote_bundle_uri(struct transport *transport)\n+{\n+\tconst struct transport_vtable *vtable = transport->vtable;\n+\n+\t/* Lazily configured */\n+\tif (transport->got_remote_bundle_uri++)\n+\t\treturn 0;\n+\n+\t/*\n+\t * \"Support\" protocol v0 and v2 without bundle-uri support by\n+\t * silently degrading to a NOOP.\n+\t */\n+\tif (!server_supports_v2(\"bundle-uri\", 0))\n+\t\treturn 0;\n+\n+\t/*\n+\t * This is intentionally below the transport.injectBundleURI,\n+\t * we want to be able to inject into protocol v0, or into the\n+\t * dialog of a server who doesn't support this.\n+\t */\n+\tif (!vtable->get_bundle_uri)\n+\t\treturn error(_(\"bundle-uri operation not supported by protocol\"));\n+\n+\tif (vtable->get_bundle_uri(transport) < 0)\n+\t\treturn error(_(\"could not retrieve server-advertised bundle-uri list\"));\n+\treturn 0;\n+}\n+\n void transport_unlock_pack(struct transport *transport, unsigned int flags)\n {\n \tint in_signal_handler = !!(flags & TRANSPORT_UNLOCK_PACK_IN_SIGNAL_HANDLER);\n@@ -1503,6 +1550,7 @@ int transport_disconnect(struct transport *transport)\n \t\tret = transport->vtable->disconnect(transport);\n \tif (transport->got_remote_refs)\n \t\tfree_refs((void *)transport->remote_refs);\n+\tbundle_uri_string_list_clear(&transport->bundle_uri);\n \tfree(transport);\n \treturn ret;\n }\ndiff --git a/transport.h b/transport.h\nindex 12bc08fc339..90568845876 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -76,6 +76,18 @@ struct transport {\n \t */\n \tunsigned got_remote_refs : 1;\n \n+\t/**\n+\t * Indicates whether we already called get_bundle_uri_list(); set by\n+\t * transport.c::transport_get_remote_bundle_uri().\n+\t */\n+\tunsigned got_remote_bundle_uri : 1;\n+\n+\t/*\n+\t * The results of \"command=bundle-uri\", if both sides support\n+\t * the \"bundle-uri\" capability.\n+\t */\n+\tstruct string_list bundle_uri;\n+\n \t/*\n \t * Transports that call take-over destroys the data specific to\n \t * the transport type while doing so, and cannot be reused.\n@@ -280,6 +292,12 @@ void transport_ls_refs_options_release(struct transport_ls_refs_options *opts);\n const struct ref *transport_get_remote_refs(struct transport *transport,\n \t\t\t\t\t    struct transport_ls_refs_options *transport_options);\n \n+/**\n+ * Retrieve bundle URI(s) from a remote. Populates \"struct\n+ * transport\"'s \"bundle_uri\" and \"got_remote_bundle_uri\".\n+ */\n+int transport_get_remote_bundle_uri(struct transport *transport);\n+\n /*\n  * Fetch the hash algorithm used by a remote.\n  *\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453867","messageId":"RFC-patch-v2-12.36-54149dcb0aa-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 12/36] bundle-uri client: add \"git ls-remote-bundle-uri\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:29Z","receivedAt":"2022-04-18T17:24:48Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a git-ls-remote-bundle-uri command, this is a thin wrapper for\nissuing protocol v2 \"bundle-uri\" commands to a server, and to the\nparsing routines in bundle-uri.c.\n\nSince in the \"git clone\" case we'll have already done the handshake(),\nbut not here, introduce a \"got_advertisement\" state along with\n\"got_remote_heads\". It seems to me that the \"got_remote_heads\" is\nbadly named in the first place, and the whole logic of eagerly getting\nls-refs on handshake() or not could be refactored somewhat, but let's\nnot do that now, and instead just add another self-documenting state\nvariable.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-ls-remote-bundle-uri.txt |  62 +++++++++++\n Documentation/git-ls-remote.txt            |   1 +\n Makefile                                   |   1 +\n builtin.h                                  |   1 +\n builtin/clone.c                            |   2 +-\n builtin/ls-remote-bundle-uri.c             |  90 +++++++++++++++\n command-list.txt                           |   1 +\n git.c                                      |   1 +\n t/lib-t5730-protocol-v2-bundle-uri.sh      | 124 +++++++++++++++++++++\n transport.c                                |  43 +++++--\n transport.h                                |   6 +-\n 11 files changed, 321 insertions(+), 11 deletions(-)\n create mode 100644 Documentation/git-ls-remote-bundle-uri.txt\n create mode 100644 builtin/ls-remote-bundle-uri.c\n\ndiff --git a/Documentation/git-ls-remote-bundle-uri.txt b/Documentation/git-ls-remote-bundle-uri.txt\nnew file mode 100644\nindex 00000000000..793d7677f2f\n--- /dev/null\n+++ b/Documentation/git-ls-remote-bundle-uri.txt\n@@ -0,0 +1,62 @@\n+git-ls-remote-bundle-uri(1)\n+===========================\n+\n+NAME\n+----\n+git-ls-remote-bundle-uri - List 'bundle-uri' in a remote repository\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git ls-remote-bundle-uri' [-q |--quiet] [--uri] [--upload-pack=<exec>]\n+\t\t\t [[-o | --server-option=]<option>] <repository>\n+\n+\n+DESCRIPTION\n+-----------\n+\n+Displays the `bundle-uri`s advertised by a remote repository. See\n+`bundle-uri` in link:technical/protocol-v2.html[the Git Wire Protocol,\n+Version 2] documentation for what the output format looks like.\n+\n+OPTIONS\n+-------\n+\n+-q::\n+--quiet::\n+\tDo not print remote URL to stderr in cases where the remote\n+\tname is inferred from config.\n++\n+When the remote name is not inferred (e.g. `git ls-remote-bundle-uri\n+origin`, or `git ls-remote-bundle-uri https://[...]`) the remote URL\n+is not printed in any case.\n+\n+--uri::\n+\tPrint only the URIs, and not any of their optional attributes.\n+\n+--upload-pack=<exec>::\n+\tSpecify the full path of 'git-upload-pack' on the remote\n+\thost. This allows listing references from repositories accessed via\n+\tSSH and where the SSH daemon does not use the PATH configured by the\n+\tuser.\n+\n+-o <option>::\n+--server-option=<option>::\n+\tTransmit the given string to the server when communicating using\n+\tprotocol version 2.  The given string must not contain a NUL or LF\n+\tcharacter.\n+\tWhen multiple `--server-option=<option>` are given, they are all\n+\tsent to the other side in the order listed on the command line.\n+\n+<repository>::\n+\tThe \"remote\" repository to query.  This parameter can be\n+\teither a URL or the name of a remote (see the GIT URLS and\n+\tREMOTES sections of linkgit:git-fetch[1]).\n+\n+SEE ALSO\n+--------\n+linkgit:git-ls-remote[1].\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/git-ls-remote.txt b/Documentation/git-ls-remote.txt\nindex 492e573856f..86c07eff832 100644\n--- a/Documentation/git-ls-remote.txt\n+++ b/Documentation/git-ls-remote.txt\n@@ -114,6 +114,7 @@ c5db5456ae3b0873fc659c19fafdde22313cc441\trefs/tags/v0.99.2\n \n SEE ALSO\n --------\n+linkgit:git-ls-remote-bundle-uri[1].\n linkgit:git-check-ref-format[1].\n \n GIT\ndiff --git a/Makefile b/Makefile\nindex c8a14793005..badb87bed78 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1161,6 +1161,7 @@ BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\n BUILTIN_OBJS += builtin/log.o\n BUILTIN_OBJS += builtin/ls-files.o\n+BUILTIN_OBJS += builtin/ls-remote-bundle-uri.o\n BUILTIN_OBJS += builtin/ls-remote.o\n BUILTIN_OBJS += builtin/ls-tree.o\n BUILTIN_OBJS += builtin/mailinfo.o\ndiff --git a/builtin.h b/builtin.h\nindex 40e9ecc8485..c80bec94abe 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -173,6 +173,7 @@ int cmd_log(int argc, const char **argv, const char *prefix);\n int cmd_log_reflog(int argc, const char **argv, const char *prefix);\n int cmd_ls_files(int argc, const char **argv, const char *prefix);\n int cmd_ls_tree(int argc, const char **argv, const char *prefix);\n+int cmd_ls_remote_bundle_uri(int argc, const char **argv, const char *prefix);\n int cmd_ls_remote(int argc, const char **argv, const char *prefix);\n int cmd_mailinfo(int argc, const char **argv, const char *prefix);\n int cmd_mailsplit(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 709f1502f91..e11f4019b87 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1240,7 +1240,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \t * Populate transport->got_remote_bundle_uri and\n \t * transport->bundle_uri. We might get nothing.\n \t */\n-\ttransport_get_remote_bundle_uri(transport);\n+\ttransport_get_remote_bundle_uri(transport, 1);\n \n \tif (mapped_refs) {\n \t\tint hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport));\ndiff --git a/builtin/ls-remote-bundle-uri.c b/builtin/ls-remote-bundle-uri.c\nnew file mode 100644\nindex 00000000000..dadb21043c0\n--- /dev/null\n+++ b/builtin/ls-remote-bundle-uri.c\n@@ -0,0 +1,90 @@\n+#include \"builtin.h\"\n+#include \"cache.h\"\n+#include \"transport.h\"\n+#include \"ref-filter.h\"\n+#include \"remote.h\"\n+#include \"refs.h\"\n+\n+static const char * const ls_remote_bundle_uri_usage[] = {\n+\tN_(\"git ls-remote-bundle-uri <repository>\"),\n+\tNULL\n+};\n+\n+int cmd_ls_remote_bundle_uri(int argc, const char **argv, const char *prefix)\n+{\n+\tint quiet = 0;\n+\tint uri = 0;\n+\tconst char *uploadpack = NULL;\n+\tstruct string_list server_options = STRING_LIST_INIT_DUP;\n+\tstruct option options[] = {\n+\t\tOPT__QUIET(&quiet, N_(\"do not print remote URL\")),\n+\t\tOPT_BOOL(0, \"uri\", &uri, N_(\"limit to showing uri field\")),\n+\t\tOPT_STRING(0, \"upload-pack\", &uploadpack, N_(\"exec\"),\n+\t\t\t   N_(\"path of git-upload-pack on the remote host\")),\n+\t\tOPT_STRING_LIST('o', \"server-option\", &server_options,\n+\t\t\t\tN_(\"server-specific\"),\n+\t\t\t\tN_(\"option to transmit\")),\n+\t\tOPT_END()\n+\t};\n+\tconst char *dest = NULL;\n+\tstruct remote *remote;\n+\tstruct transport *transport;\n+\tint status = 0;\n+\tstruct string_list_item *item;\n+\n+\targc = parse_options(argc, argv, prefix, options, ls_remote_bundle_uri_usage,\n+\t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n+\tdest = argv[0];\n+\n+\tpacket_trace_identity(\"ls-remote-bundle-uri\");\n+\n+\tremote = remote_get(dest);\n+\tif (!remote) {\n+\t\tif (dest)\n+\t\t\tdie(_(\"bad repository '%s'\"), dest);\n+\t\tdie(_(\"no remote configured to get bundle URIs from\"));\n+\t}\n+\tif (!remote->url_nr)\n+\t\tdie(_(\"remote '%s' has no configured URL\"), dest);\n+\n+\ttransport = transport_get(remote, NULL);\n+\tif (uploadpack)\n+\t\ttransport_set_option(transport, TRANS_OPT_UPLOADPACK, uploadpack);\n+\tif (server_options.nr)\n+\t\ttransport->server_options = &server_options;\n+\n+\tif (!dest && !quiet)\n+\t\tfprintf(stderr, \"From %s\\n\", *remote->url);\n+\n+\tif (transport_get_remote_bundle_uri(transport, 0) < 0) {\n+\t\terror(_(\"could not get the bundle-uri list\"));\n+\t\tstatus = 1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tfor_each_string_list_item(item, &transport->bundle_uri) {\n+\t\tstruct string_list_item *kv_item;\n+\t\tstruct string_list *kv = item->util;\n+\n+\t\tfprintf(stdout, \"%s\", item->string);\n+\t\tif (uri || !kv) {\n+\t\t\tfprintf(stdout, \"\\n\");\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfor_each_string_list_item(kv_item, kv) {\n+\t\t\tconst char *k = kv_item->string;\n+\t\t\tconst char *v = kv_item->util;\n+\n+\t\t\tif (v)\n+\t\t\t\tfprintf(stdout, \" %s=%s\", k, v);\n+\t\t\telse\n+\t\t\t\tfprintf(stdout, \" %s\", k);\n+\t\t}\n+\t\tfprintf(stdout, \"\\n\");\n+\t}\n+\n+cleanup:\n+\tif (transport_disconnect(transport))\n+\t\treturn 1;\n+\treturn status;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex 9bd6f3c48f4..a50eebd4aa2 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-interpret-trailers                  purehelpers\n git-log                                 mainporcelain           info\n git-ls-files                            plumbinginterrogators\n git-ls-remote                           plumbinginterrogators\n+git-ls-remote-bundle-uri                plumbinginterrogators\n git-ls-tree                             plumbinginterrogators\n git-mailinfo                            purehelpers\n git-mailsplit                           purehelpers\ndiff --git a/git.c b/git.c\nindex 3d8e48cf555..352e76cedaf 100644\n--- a/git.c\n+++ b/git.c\n@@ -551,6 +551,7 @@ static struct cmd_struct commands[] = {\n \t{ \"log\", cmd_log, RUN_SETUP },\n \t{ \"ls-files\", cmd_ls_files, RUN_SETUP },\n \t{ \"ls-remote\", cmd_ls_remote, RUN_SETUP_GENTLY },\n+\t{ \"ls-remote-bundle-uri\", cmd_ls_remote_bundle_uri, RUN_SETUP_GENTLY },\n \t{ \"ls-tree\", cmd_ls_tree, RUN_SETUP },\n \t{ \"mailinfo\", cmd_mailinfo, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"mailsplit\", cmd_mailsplit, NO_PARSEOPT },\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nindex 7a90c80f0b1..d0b15a47ec2 100644\n--- a/t/lib-t5730-protocol-v2-bundle-uri.sh\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -143,3 +143,127 @@ test_expect_success !T5730_HTTP \"bad client with $T5730_PROTOCOL:// using protoc\n \tgrep \"clone> test-bad-client$\" log >sent-bad-request &&\n \ttest_file_not_empty sent-bad-request\n '\n+\n+test_expect_success \"ls-remote-bundle-uri with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\t# All data about bundle URIs\n+\tcat >expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual &&\n+\ttest_cmp expect actual &&\n+\n+\t# Only the URIs\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri --uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual2 &&\n+\ttest_cmp actual actual2\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tATTR=\"foo bar=baz\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED $ATTR\" &&\n+\n+\t# All data about bundle URIs\n+\tcat >expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED $ATTR\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri with $T5730_PROTOCOL:// using protocol v2: --uri\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\tATTR=\"foo bar=baz\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED $ATTR\" &&\n+\n+\t# All data about bundle URIs\n+\tcat >expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t--uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri --[no-]quiet with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\ttest_when_finished \"rm -rf child\" &&\n+\tenv GIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t clone \"$T5730_URI\" child &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\t# Without --[no-]quiet\n+\tcat >out.expect <<-EOF &&\n+\t$T5730_BUNDLE_URI_ESCAPED\n+\tEOF\n+\tcat >err.expect <<-EOF &&\n+\tFrom $T5730_URI\n+\tEOF\n+\tgit \\\n+\t\t-C child \\\n+\t\t -c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t>out.actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp out.expect out.actual &&\n+\n+\t# --no-quiet is the default\n+\tgit \\\n+\t\t-C child \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t--no-quiet \\\n+\t\t>out.actual 2>err.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_cmp out.expect out.actual &&\n+\n+\t# --quiet quiets the \"From\" line\n+\tgit \\\n+\t\t-C child \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t--quiet \\\n+\t\t>out.actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp out.expect out.actual &&\n+\n+\t# --quiet is implicit if the remote is not implicit\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>out.actual 2>err &&\n+\ttest_must_be_empty err &&\n+\ttest_cmp out.expect out.actual\n+'\ndiff --git a/transport.c b/transport.c\nindex 9a31e3f996b..e648d3110bb 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -198,6 +198,7 @@ struct git_transport_data {\n \tstruct git_transport_options options;\n \tstruct child_process *conn;\n \tint fd[2];\n+\tunsigned got_advertisement : 1;\n \tunsigned got_remote_heads : 1;\n \tenum protocol_version version;\n \tstruct oid_array extra_have;\n@@ -346,6 +347,7 @@ static struct ref *handshake(struct transport *transport, int for_push,\n \t\tBUG(\"unknown protocol version\");\n \t}\n \tdata->got_remote_heads = 1;\n+\tdata->got_advertisement = 1;\n \ttransport->hash_algo = reader.hash_algo;\n \n \tif (reader.line_peeked)\n@@ -367,6 +369,33 @@ static int get_bundle_uri(struct transport *transport)\n \tint stateless_rpc = transport->stateless_rpc;\n \tstring_list_init_dup(&transport->bundle_uri);\n \n+\tif (!data->got_advertisement) {\n+\t\tstruct ref *refs;\n+\t\tstruct git_transport_data *data = transport->data;\n+\t\tenum protocol_version version;\n+\n+\t\trefs = handshake(transport, 0, NULL, 0);\n+\t\tversion = data->version;\n+\n+\t\tswitch (version) {\n+\t\tcase protocol_v2:\n+\t\t\tassert(!refs);\n+\t\t\tbreak;\n+\t\tcase protocol_v0:\n+\t\tcase protocol_v1:\n+\t\tcase protocol_unknown_version:\n+\t\t\tassert(refs);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * \"Support\" protocol v0 and v2 without bundle-uri support by\n+\t * silently degrading to a NOOP.\n+\t */\n+\tif (!server_supports_v2(\"bundle-uri\", 0))\n+\t\treturn 0;\n+\n \tpacket_reader_init(&reader, data->fd[0], NULL, 0,\n \t\t\t   PACKET_READ_CHOMP_NEWLINE |\n \t\t\t   PACKET_READ_GENTLE_ON_EOF);\n@@ -1492,7 +1521,7 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n-int transport_get_remote_bundle_uri(struct transport *transport)\n+int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n {\n \tconst struct transport_vtable *vtable = transport->vtable;\n \n@@ -1500,20 +1529,16 @@ int transport_get_remote_bundle_uri(struct transport *transport)\n \tif (transport->got_remote_bundle_uri++)\n \t\treturn 0;\n \n-\t/*\n-\t * \"Support\" protocol v0 and v2 without bundle-uri support by\n-\t * silently degrading to a NOOP.\n-\t */\n-\tif (!server_supports_v2(\"bundle-uri\", 0))\n-\t\treturn 0;\n-\n \t/*\n \t * This is intentionally below the transport.injectBundleURI,\n \t * we want to be able to inject into protocol v0, or into the\n \t * dialog of a server who doesn't support this.\n \t */\n-\tif (!vtable->get_bundle_uri)\n+\tif (!vtable->get_bundle_uri) {\n+\t\tif (quiet)\n+\t\t\treturn -1;\n \t\treturn error(_(\"bundle-uri operation not supported by protocol\"));\n+\t}\n \n \tif (vtable->get_bundle_uri(transport) < 0)\n \t\treturn error(_(\"could not retrieve server-advertised bundle-uri list\"));\ndiff --git a/transport.h b/transport.h\nindex 90568845876..ed5ebcf1466 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -295,8 +295,12 @@ const struct ref *transport_get_remote_refs(struct transport *transport,\n /**\n  * Retrieve bundle URI(s) from a remote. Populates \"struct\n  * transport\"'s \"bundle_uri\" and \"got_remote_bundle_uri\".\n+ *\n+ * With `quiet=1` it will not complain if the serve doesn't support\n+ * the protocol, but only if we discover the server uses it, and\n+ * encounter issues then.\n  */\n-int transport_get_remote_bundle_uri(struct transport *transport);\n+int transport_get_remote_bundle_uri(struct transport *transport, int quiet);\n \n /*\n  * Fetch the hash algorithm used by a remote.\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453868","messageId":"RFC-patch-v2-14.36-e66aa1f18b4-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 14/36] bundle-uri client: add boolean transfer.bundleURI setting","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:31Z","receivedAt":"2022-04-18T17:24:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"The yet-to-be introduced client support for bundle-uri will always\nfall back on a full clone, but we'd still like to be able to ignore a\nserver's bundle-uri advertisement entirely.\n\nThis is useful for testing, and if a server is pointing to bad\nbundles, they take a while to time out etc.\n\nSince we might see the config in any order we need to clear out any\naccumulated bundle_uri list when we see transfer.bundleURI=false\nsetting, and not add any more things to the list.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/transfer.txt |  6 ++++++\n transport.c                       | 21 +++++++++++++++++----\n 2 files changed, 23 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt\nindex 71b9b8f29e6..ae85ca5760b 100644\n--- a/Documentation/config/transfer.txt\n+++ b/Documentation/config/transfer.txt\n@@ -78,6 +78,12 @@ transfer.advertiseSID::\n \tBoolean. When true, client and server processes will advertise their\n \tunique session IDs to their remote counterpart. Defaults to false.\n \n+transfer.bundleURI::\n+\tWhen set to `false` ignores any server advertisement of\n+\t`bundle-uri` and proceed with a \"normal\" clone/fetch even if\n+\tusing bundles to bootstap is possible. Defaults to `true`,\n+\ti.e. bundle-uri is tried whenever a server offers it.\n+\n transfer.injectBundleURI::\n \tAllows for the injection of `bundle-uri` lines into the\n \tprotocol v2 transport dialog (see `protocol.version` in\ndiff --git a/transport.c b/transport.c\nindex 342e39d81f3..9e20b531215 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1521,19 +1521,28 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n-struct config_cb {\n+struct bundle_config_cb {\n \tstruct transport *transport;\n \tint configured;\n \tint ret;\n+\tint disabled;\n };\n \n static int bundle_uri_config(const char *var, const char *value, void *data)\n {\n-\tstruct config_cb *cb = data;\n+\tstruct bundle_config_cb *cb = data;\n \tstruct transport *transport = cb->transport;\n \tstruct string_list *uri = &transport->bundle_uri;\n \n-\tif (!strcmp(var, \"transfer.injectbundleuri\")) {\n+\tif (!strcmp(var, \"transfer.bundleuri\")) {\n+\t\tcb->disabled = !git_config_bool(var, value);\n+\t\tif (cb->disabled)\n+\t\t\tbundle_uri_string_list_clear(uri);\n+\t\treturn 0;\n+\t}\n+\n+\tif (!cb->disabled &&\n+\t    !strcmp(var, \"transfer.injectbundleuri\")) {\n \t\tcb->configured = 1;\n \t\tif (!value)\n \t\t\tcb->ret = error(_(\"bad (empty) transfer.injectBundleURI\"));\n@@ -1548,7 +1557,7 @@ static int bundle_uri_config(const char *var, const char *value, void *data)\n int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n {\n \tconst struct transport_vtable *vtable = transport->vtable;\n-\tstruct config_cb cb = {\n+\tstruct bundle_config_cb cb = {\n \t\t.transport = transport,\n \t};\n \n@@ -1558,6 +1567,10 @@ int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n \n \tgit_config(bundle_uri_config, &cb);\n \n+\t/* Don't use bundle-uri at all */\n+\tif (cb.disabled)\n+\t\treturn 0;\n+\n \t/* Our own config can fake it up with transport.injectBundleURI */\n \tif (cb.configured)\n \t\treturn cb.ret;\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453869","messageId":"RFC-patch-v2-15.36-f19d2bcbc66-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 15/36] bundle-uri client: support for bundle-uri with \"clone\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:32Z","receivedAt":"2022-04-18T17:24:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"In an earlier commit (\"bundle-uri client: add minimal NOOP client\") a\ntransport_get_remote_bundle_uri() call was added to builtin/clone.c to\nget any advertised bundle URIs from the server during cloning, but\nnothing was being done with them yet.\n\nThis implements real support for bundle-uri during the \"clone\"\nphase. It's not used at all by \"fetch\", but the code to support it is\nmostly here already, and will be finished later.\n\nUsing the new transfer.injectBundleURI support it's easy to test this\nmethod of cloning on a live server that doesn't support bundle-uri. In\na git.git checkout.\n\nFirst let's prepare two bundles:\n\n    git bundle create /tmp/git-master-only.bdl origin/master\n    git bundle create /tmp/git-master-to-next.bdl origin/master..origin/next\n\nAnd next, let's do a \"fake\" clone where we bootstrap from these\nbundles. The fetch.uriProtocols is needed because we'd otherwise\nignore \"file://\" URIs. This uses --no-tags --single-branch for\nsimplicity:\n\n    rm -rf /tmp/git.git &&\n    git \\\n\t-c protocol.version=2 \\\n        -c fetch.uriProtocols=file \\\n        -c transfer.injectBundleURI=\"file:///tmp/git-master-only.bdl\" \\\n\t-c transfer.injectBundleURI=\"file:///tmp/git-master-to-next.bdl\" \\\n\tclone --bare --no-tags --single-branch --branch next --template= \\\n\t--verbose --verbose \\\n\thttps://github.com/git/git.git /tmp/git.git\n\nWe'll then get output like:\n\n    Receiving bundle (1/2): 100% (300529/300529), 87.57 MiB | 32.70 MiB/s, done.\n    Resolving deltas: 100% (226765/226765), done.\n    have eb27b338a3e71c7c4079fbac8aeae3f8fbb5c687 commit via bundle-uri\n    Receiving bundle (2/2): 100% (725/725), 221.11 KiB | 22.11 MiB/s, done.\n    Resolving deltas: 100% (539/539), completed with 153 local objects.\n    have e1b32706d8dd5db1dc2e13f8e391651214f1d987 commit via bundle-uri\n    Marking e1b32706d8dd5db1dc2e13f8e391651214f1d987 as complete\n    already have e1b32706d8dd5db1dc2e13f8e391651214f1d987 (refs/heads/next)\n    Checking connectivity: 301210, done.\n\nI.e. we did an ls-refs on connection to the server, then retrieved the\nadvertised bundles (faked up via config in this case).\n\nWe then got all the data leading up to the current \"master\" from\nthere, and also the commit that's currently on \"next. In this case we\nfound that we didn't need to proceed further with the dialog.\n\nI.e. other than an ls-refs and the server waiting until we downloaded\nthe bundles, the server didn't need to do any work creating a PACK for\nus.\n\nIf we change \"--branch next\" into \"--branch seen\" in the above command\nwe'll get the same output at the start until the \"want\" line, then:\n\n    [...]\n    want 93021c12c9f91e0d750d3ca8750a62416f4ea81a (refs/heads/seen)\n    POST git-upload-pack (212 bytes)\n    remote: Enumerating objects: 2265, done.\n    remote: Counting objects: 100% (1576/1576), done.\n    remote: Compressing objects: 100% (233/233), done.\n    remote: Total 2265 (delta 1378), reused 1480 (delta 1341), pack-reused 689\n    Receiving objects: 100% (2265/2265), 2.17 MiB | 10.77 MiB/s, done.\n    Resolving deltas: 100% (1673/1673), completed with 339 local objects.\n    Checking connectivity: 303225, done.\n\nI.e. the server needed to send us an incremental update on top after\nwe'd unpacked the bundles, but this was a fairly minimal set of ~2k\nobjects. It didn't need to service a full clone.\n\nWe can see the savings on the server by setting up a local server at\nthe tip of \"next\":\n\n    rm -rf /tmp/git-server.git &&\n    git init --bare /tmp/git-server.git &&\n    git -C /tmp/git-server.git bundle unbundle /tmp/git-master-only.bdl &&\n    git -C /tmp/git-server.git bundle unbundle /tmp/git-master-to-next.bdl\n    git -C /tmp/git-server.git update-ref refs/heads/master $(git ls-remote /tmp/git-master-only.bdl | cut -f 1) &&\n    git -C /tmp/git-server.git update-ref refs/heads/next $(git ls-remote /tmp/git-master-to-next.bdl | cut -f 1) &&\n    git -C /tmp/git-server.git for-each-ref\n\nLet's then clone from it, and record the time we spend.\n\n    rm -rf /tmp/git.git /tmp/{client,server}.time &&\n    /usr/bin/time -o /tmp/client.time -v git \\\n\t-c protocol.version=2 \\\n        -c fetch.uriProtocols=file \\\n        -c transfer.injectBundleURI=\"file:///tmp/git-master-only.bdl\" \\\n\tclone \\\n\t--upload-pack '/usr/bin/time -o /tmp/server.time -v git-upload-pack' \\\n\t--bare --no-tags --single-branch --branch next --template= \\\n\t--verbose --verbose \\\n\tfile:///tmp/git-server.git /tmp/git.git &&\n    for i in client server\n    do\n        echo $i: &&\n        grep -e seconds -e wall -e Maximum -e context /tmp/$i.time\n    done\n\nThis gives us something like these results:\n\n    client:\n        User time (seconds): 46.34\n        System time (seconds): 0.67\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.67\n        Maximum resident set size (kbytes): 207096\n        Voluntary context switches: 116058\n        Involuntary context switches: 220\n    server:\n        User time (seconds): 0.13\n        System time (seconds): 0.00\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:14.08\n        Maximum resident set size (kbytes): 53168\n        Voluntary context switches: 255\n        Involuntary context switches: 7\n\nWhereas doing a normal \"clone\" (by e.g. adding \"-c\ntransfer.bundleURI=false\" to the above) will give something like:\n\n    client:\n        User time (seconds): 47.24\n        System time (seconds): 0.92\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.55\n        Maximum resident set size (kbytes): 288104\n        Voluntary context switches: 136350\n        Involuntary context switches: 296\n    server:\n        User time (seconds): 5.73\n        System time (seconds): 0.24\n        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:07.45\n        Maximum resident set size (kbytes): 288104\n        Voluntary context switches: 26568\n        Involuntary context switches: 111\n\nI.e. we can see that the win on the client in this case is negative,\nbut we use around over 2% of the CPU time on the server, and around\n20% of the memory. The client-visible time is a bit slower, by around\n2%.\n\nIn practice I think this will be more of a win-win. These results are\non an unloaded local machine, and don't account for the benefit of the\nserver being more likely to have a network-local version of most of\nthe repository via dumb CDNs.\n\nReal servers are also usually in a messier state of having various\nloose objects and more fragmented pack collections, and needing to\nspend CPU to assemble these. Frequent repacking and e.g. local caching\ne.g. via the uploadpack.packObjectsHook helps, but using this should\nmake it more accessible to run a highly performance git server.\n\nThis feature also makes things like resumable clones rather trivial to\nimplement, this approach was discussed in the past[1] as a means to\nget that feature.\n\n1. https://lore.kernel.org/git/20111110074330.GA27925@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n fetch-pack.c                          | 255 ++++++++++++++++++++++++++\n fetch-pack.h                          |   6 +\n t/lib-t5730-protocol-v2-bundle-uri.sh | 145 ++++++++++++++-\n transport.c                           |   1 +\n 4 files changed, 406 insertions(+), 1 deletion(-)\n\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex b1d90d1914f..316fb2fd65d 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -26,6 +26,7 @@\n #include \"commit-reach.h\"\n #include \"commit-graph.h\"\n #include \"sigchain.h\"\n+#include \"bundle.h\"\n \n static int transfer_unpack_limit = -1;\n static int fetch_unpack_limit = -1;\n@@ -1025,6 +1026,133 @@ static int get_pack(struct fetch_pack_args *args,\n \treturn 0;\n }\n \n+static int unbundle_bundle_uri(const char *bundle_uri, unsigned int nth,\n+\t\t\t       unsigned int total_nr, FILE *in, int in_fd,\n+\t\t\t       struct oid_array *bundle_oids,\n+\t\t\t       unsigned int use_thin_pack)\n+{\n+\tstruct child_process cmd = CHILD_PROCESS_INIT;\n+\tstruct bundle_header header = BUNDLE_HEADER_INIT;\n+\tint ret = 0;\n+\tstruct string_list_item *item;\n+\tstruct strbuf progress_title = STRBUF_INIT;\n+\tint code;\n+\n+\tret = read_bundle_header_fd(in_fd, &header, bundle_uri);\n+\tif (ret < 0) {\n+\t\tret = error(\"could not read_bundle_header(%s)\", bundle_uri);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tfor_each_string_list_item(item, &header.references) {\n+\t\t/*\n+\t\t * The bundle's idea of the ref name is\n+\t\t * item->string.\n+\t\t *\n+\t\t * Here's where we could do concurrent negotiation\n+\t\t * with the server (and possibly start the fetch!)\n+\t\t * before or while we unpack the bundle with\n+\t\t * index-pack.\n+\t\t *\n+\t\t * The negotiator would need a small change to trust\n+\t\t * arbitrary OIDs instead of assuming it has existing\n+\t\t * in-repo \"struct commit *\", but ad-hoc testing\n+\t\t * reveals that it'll work & speed up the fetch even\n+\t\t * more, as we could proceed in parallel with the full\n+\t\t * bundle fetching as soon as we get the headers.\n+\t\t */\n+\t\tstruct object_id *oid = item->util;\n+\n+\t\toid_array_append(bundle_oids, oid);\n+\t}\n+\n+\tif (git_env_bool(\"GIT_TEST_BUNDLE_URI_FAIL_UNBUNDLE\", 0))\n+\t\tlseek(in_fd, 0, SEEK_SET);\n+\n+\tstrbuf_addf(&progress_title, \"Receiving bundle (%d/%d)\", nth, total_nr);\n+\tstrvec_pushl(&cmd.args, \"index-pack\", \"--stdin\", \"-v\",\n+\t\t     \"--progress-title\", progress_title.buf, NULL);\n+\n+\tif (header.prerequisites.nr && use_thin_pack)\n+\t\tstrvec_push(&cmd.args, \"--fix-thin\");\n+\tstrvec_push(&cmd.args, \"--check-self-contained-and-connected\");\n+\tadd_index_pack_keep_option(&cmd.args);\n+\n+\tcmd.git_cmd = 1;\n+\tcmd.in = in_fd;\n+\tcmd.no_stdout = 1;\n+\tcmd.git_cmd = 1;\n+\n+\tif (start_command(&cmd)) {\n+\t\tret = error(_(\"fetch-pack: unable to spawn index-pack\"));\n+\t\tgoto cleanup;\n+\t}\n+\n+\tcode = finish_command(&cmd);\n+\n+\tif (header.prerequisites.nr && code == 1)\n+\t\t/*\n+\t\t * index-pack returns -1 on\n+\t\t * --check-self-contained-and-connected to indicate\n+\t\t * that the pack was indeed not self contained and\n+\t\t * connected. We know from the bundle header\n+\t\t * prerequisites.\n+\t\t */\n+\t\tcode = 0;\n+\n+\tif (code) {\n+\t\tret = error(_(\"fetch-pack: unable to finish index-pack, exited with %d\"), code);\n+\t\tgoto cleanup;\n+\t}\n+\n+cleanup:\n+\tstrbuf_release(&progress_title);\n+\tbundle_header_release(&header);\n+\treturn ret;\n+}\n+\n+static int get_bundle_uri(struct string_list_item *item, unsigned int nth,\n+\t\t\t  unsigned int total_nr, struct oid_array *bundle_oids,\n+\t\t\t  unsigned int use_thin_pack)\n+{\n+\tstruct child_process cmd = CHILD_PROCESS_INIT;\n+\tstruct strbuf tempfile = STRBUF_INIT;\n+\tint ret = 0;\n+\tconst char *uri = item->string;\n+\tFILE *out;\n+\tint out_fd;\n+\n+\tstrvec_push(&cmd.args, \"curl\");\n+\tstrvec_push(&cmd.args, \"--silent\");\n+\tstrvec_push(&cmd.args, \"--output\");\n+\tstrvec_push(&cmd.args, \"-\");\n+\tstrvec_push(&cmd.args, \"--\");\n+\tstrvec_push(&cmd.args, item->string);\n+\tcmd.git_cmd = 0;\n+\tcmd.no_stdin = 1;\n+\tcmd.out = -1;\n+\n+\tif (start_command(&cmd)) {\n+\t\tret = error(\"fetch-pack: unable to spawn http-fetch\");\n+\t\tgoto cleanup;\n+\t}\n+\n+\tout = xfdopen(cmd.out, \"r\");\n+\tout_fd = fileno(out);\n+\tret = unbundle_bundle_uri(uri, nth, total_nr, out, out_fd,\n+\t\t\t\t  bundle_oids, use_thin_pack);\n+\n+\tif (finish_command(&cmd)) {\n+\t\tret = error(\"fetch-pack: unable to finish http-fetch\");\n+\t\tgoto cleanup;\n+\t}\n+\n+cleanup:\n+\tstrbuf_release(&tempfile);\n+\n+\treturn ret;\n+}\n+\n static int cmp_ref_by_name(const void *a_, const void *b_)\n {\n \tconst struct ref *a = *((const struct ref **)a_);\n@@ -1586,6 +1714,130 @@ static void do_check_stateless_delimiter(int stateless_rpc,\n \t\t\t\t  _(\"git fetch-pack: expected response end packet\"));\n }\n \n+static int get_bundle_uri_add_known_common(struct string_list_item *item,\n+\t\t\t\t\t   unsigned int nth, unsigned int total_nr,\n+\t\t\t\t\t   struct fetch_negotiator *negotiator,\n+\t\t\t\t\t   struct fetch_pack_args *args,\n+\t\t\t\t\t   unsigned int use_thin_pack)\n+{\n+\tint i;\n+\tstruct oid_array bundle_oids = OID_ARRAY_INIT;\n+\n+\t/*\n+\t * We don't use OBJECT_INFO_QUICK here unlike in the rest of\n+\t * the fetch routines, that's because the rest of them don't\n+\t * need to consider a commit object that's just been\n+\t * downloaded for further negotiation, but bundle-uri does for\n+\t * adding newly downloaded OIDs to the negotiator.\n+\t */\n+\tunsigned oi_flags = OBJECT_INFO_SKIP_FETCH_OBJECT;\n+\n+\tif (get_bundle_uri(item, nth, total_nr, &bundle_oids, use_thin_pack) < 0)\n+\t\treturn error(_(\"could not get the bundle URI #%d\"), nth);\n+\n+\tfor (i = 0; i < bundle_oids.nr; i++) {\n+\t\tstruct object_id *oid = &bundle_oids.oid[i];\n+\t\tenum object_type type = OBJ_NONE;\n+\t\tstruct commit *c = deref_without_lazy_fetch_extended(oid, 0,\n+\t\t\t\t\t\t\t\t     &type,\n+\t\t\t\t\t\t\t\t     oi_flags);\n+\t\tif (!c) {\n+\t\t\tif (type == OBJ_BLOB || type == OBJ_TREE) {\n+\t\t\t\tprint_verbose(args, \"have %s %s via bundle-uri (ignoring due to type)\",\n+\t\t\t\t\t      oid_to_hex(oid), type_name(type));\n+\t\t\t\tcontinue;\n+\t\t\t} else if (type) {\n+\t\t\t\t/*\n+\t\t\t\t * OBJ_TAG should have been peeled,\n+\t\t\t\t * and OBJ_COMMIT should have a\n+\t\t\t\t * non-NULL \"c\".\n+\t\t\t\t *\n+\t\t\t\t * Should be a BUG() if we were not\n+\t\t\t\t * bending over backwards to make\n+\t\t\t\t * bundle-uri soft-fail.\n+\t\t\t\t */\n+\t\t\t\treturn error(_(\"bundle-uri says it has %s, got it at unexpected type %s\"),\n+\t\t\t\t\t     oid_to_hex(oid), type_name(type));\n+\t\t\t}\n+\t\t}\n+\n+\t\tprint_verbose(args, \"have %s %s via bundle-uri\",\n+\t\t\t      oid_to_hex(oid), type_name(type));\n+\n+\t\tnegotiator->known_common(negotiator, c);\n+\t\tmark_complete(oid);\n+\t}\n+\treturn 0;\n+}\n+\n+static void do_fetch_pack_v2_bundle_uri(struct fetch_pack_args *args,\n+\t\t\t\t\tstruct string_list  *bundle_uri,\n+\t\t\t\t\tstruct fetch_negotiator *negotiator)\n+{\n+\tstruct string_list_item *item;\n+\tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tstruct string_list default_protocols = STRING_LIST_INIT_NODUP;\n+\tstruct string_list *ok_protocols;\n+\n+\tif (!bundle_uri)\n+\t\treturn;\n+\n+\tif (!bundle_uri->nr)\n+\t\treturn;\n+\n+\tif (uri_protocols.nr) {\n+\t\tok_protocols = &uri_protocols;\n+\t} else {\n+\t\tstring_list_append(&default_protocols, \"http\");\n+\t\tstring_list_append(&default_protocols, \"https\");\n+\t\tok_protocols = &default_protocols;\n+\t}\n+\n+\tfor_each_string_list_item(item, bundle_uri) {\n+\t\tconst char *uri = item->string;\n+\t\tint protocol_ok = 0;\n+\t\tstruct string_list_item *item2;\n+\n+\t\tfor_each_string_list_item(item2, ok_protocols) {\n+\t\t\tconst char *s = item2->string;\n+\t\t\tconst char *p;\n+\n+\t\t\tif (skip_prefix(item->string, s, &p) &&\n+\t\t\t    starts_with(p, \"://\")) {\n+\t\t\t\tprotocol_ok = 1;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!protocol_ok) {\n+\t\t\tprint_verbose(args, \"skipping bundle-uri not on protocol whitelist: %s\",\n+\t\t\t\t      item->string);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tstring_list_append(&list, uri)->util = item->util;\n+\t}\n+\n+\tif (list.nr) {\n+\t\tint i;\n+\t\tunsigned int total_nr = list.nr;\n+\n+\t\ttrace2_region_enter(\"fetch-pack\", \"bundle-uri\", the_repository);\n+\t\tfor (i = 0; i < total_nr; i++) {\n+\t\t\tstruct string_list_item item = list.items[i];\n+\t\t\tunsigned int nth = i + 1;\n+\n+\t\t\tget_bundle_uri_add_known_common(&item, nth, total_nr,\n+\t\t\t\t\t\t\tnegotiator, args,\n+\t\t\t\t\t\t\targs->use_thin_pack);\n+\t\t}\n+\t\ttrace2_region_leave(\"fetch-pack\", \"bundle-uri\", the_repository);\n+\t}\n+\n+\tstring_list_clear(&default_protocols, 0);;\n+}\n+\n+\n static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \t\t\t\t    int fd[2],\n \t\t\t\t    const struct ref *orig_ref,\n@@ -1609,6 +1861,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \tstruct string_list packfile_uris = STRING_LIST_INIT_DUP;\n \tint i;\n \tstruct strvec index_pack_args = STRVEC_INIT;\n+\tstruct string_list *bundle_uri = args->bundle_uri;\n \n \tnegotiator = &negotiator_alloc;\n \tif (args->refetch)\n@@ -1616,6 +1869,8 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,\n \telse\n \t\tfetch_negotiator_init(r, negotiator);\n \n+\tdo_fetch_pack_v2_bundle_uri(args, bundle_uri, negotiator);\n+\n \tpacket_reader_init(&reader, fd[0], NULL, 0,\n \t\t\t   PACKET_READ_CHOMP_NEWLINE |\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\ndiff --git a/fetch-pack.h b/fetch-pack.h\nindex 8c7752fc821..5d8c8b03e1f 100644\n--- a/fetch-pack.h\n+++ b/fetch-pack.h\n@@ -24,6 +24,12 @@ struct fetch_pack_args {\n \t */\n \tconst struct oid_array *negotiation_tips;\n \n+\t/*\n+\t * A pointer to the already populated transport.bundle_uri\n+\t * struct.\n+\t */\n+\tstruct string_list *bundle_uri;\n+\n \tunsigned deepen_relative:1;\n \tunsigned quiet:1;\n \tunsigned keep_pack:1;\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nindex 28c095c1224..0235ba50d6f 100644\n--- a/t/lib-t5730-protocol-v2-bundle-uri.sh\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -7,6 +7,8 @@ case \"$T5730_PROTOCOL\" in\n file)\n \tT5730_PARENT=file_parent\n \tT5730_URI=\"file://$PWD/file_parent\"\n+\tT5730_URI_BDL_PROTO=\"file://\"\n+\tT5730_URI_BDL=\"$T5730_URI_BDL_PROTO$PWD/file_parent\"\n \tT5730_BUNDLE_URI=\"$T5730_URI/fake.bdl\"\n \ttest_set_prereq T5730_FILE\n \t;;\n@@ -15,6 +17,8 @@ git)\n \tstart_git_daemon --export-all --enable=receive-pack\n \tT5730_PARENT=\"$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent\"\n \tT5730_URI=\"$GIT_DAEMON_URL/parent\"\n+\tT5730_URI_BDL_PROTO=\"file://\"\n+\tT5730_URI_BDL=\"$T5730_URI_BDL_PROTO$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent\"\n \tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n \ttest_set_prereq T5730_GIT\n \t;;\n@@ -24,6 +28,8 @@ http)\n \tT5730_PARENT=\"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\"\n \tT5730_URI=\"$HTTPD_URL/smart/http_parent\"\n \tT5730_BUNDLE_URI=\"https://example.com/fake.bdl\"\n+\tT5730_URI_BDL_PROTO=\"http://\"\n+\tT5730_URI_BDL=\"$HTTPD_URL/dumb/http_parent\"\n \ttest_set_prereq T5730_HTTP\n \t;;\n *)\n@@ -33,7 +39,20 @@ esac\n \n test_expect_success \"setup protocol v2 $T5730_PROTOCOL:// tests\" '\n \tgit init \"$T5730_PARENT\" &&\n-\ttest_commit -C \"$T5730_PARENT\" one\n+\ttest_commit -C \"$T5730_PARENT\" one &&\n+\ttest_commit -C \"$T5730_PARENT\" two &&\n+\ttest_commit -C \"$T5730_PARENT\" three &&\n+\ttest_commit -C \"$T5730_PARENT\" four &&\n+\ttest_commit -C \"$T5730_PARENT\" five &&\n+\ttest_commit -C \"$T5730_PARENT\" six &&\n+\n+\tmkdir \"$T5730_PARENT\"/bdl &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/1.bdl one &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/1-2.bdl one..two &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/2-3.bdl two..three &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/3-4.bdl three..four &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/4-5.bdl four..five &&\n+\tgit -C \"$T5730_PARENT\" bundle create bdl/5-6.bdl five..six\n '\n \n # Poor man's URI escaping. Good enough for the test suite whose trash\n@@ -313,3 +332,127 @@ test_expect_success \"ls-remote-bundle-uri with bad -c transfer.injectBundleURI p\n \ttest_cmp err.expect err.actual &&\n \ttest_path_is_missing log\n '\n+\n+test_cmp_repo_refs() {\n+\tone=\"$1\"\n+\ttwo=\"$2\"\n+\tshift 2\n+\n+\tgit -C \"$one\" for-each-ref \"$@\" >expect &&\n+\tgit -C \"$two\" for-each-ref \"$@\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n+show_cr () {\n+\ttr '\\015' Q | sed -e \"s/Q/<CR>\\\\$LF/g\"\n+}\n+\n+test_expect_success CURL \"clone with bundle-uri protocol v2 over $T5730_PROTOCOL:// 1.bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\n+\ttest_when_finished \"rm -rf event log child\" &&\n+\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tclone --verbose --verbose \\\n+\t\t\"$T5730_URI\" child &&\n+\ttest_region progress \"Receiving bundle (1/1)\" event &&\n+\tgrep \"clone> want \" log &&\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n+'\n+\n+test_expect_success \"fetch with bundle-uri protocol v2 over $T5730_PROTOCOL:// 1.bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\n+\ttest_when_finished \"rm -rf event log child\" &&\n+\tgit init --bare child &&\n+\tgit -C child remote add --mirror=fetch origin \"$T5730_URI\" &&\n+\n+\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit -C child \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tfetch --verbose --verbose &&\n+\n+\tif test_have_prereq CURL\n+\tthen\n+\t\t# Fetch is not supported yet\n+\t\t! test_region progress \"Receiving bundle (1/1)\" event &&\n+\t\tgrep \"fetch> want \" log\n+\telse\n+\t\t! grep \"fetch-pack: unable to spawn\" event\n+\tfi &&\n+\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n+'\n+\n+test_expect_success \"clone with bundle-uri protocol v2 with $T5730_PROTOCOL:// 1 + 1-2 + [...].bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1-2.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/2-3.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/3-4.bdl | test_uri_escape)\" --add &&\n+\n+\ttest_when_finished \"rm -rf event log child\" &&\n+\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tclone --verbose --verbose \\\n+\t\t\"$T5730_URI\" child &&\n+\n+\tif test_have_prereq CURL\n+\tthen\n+\t\ttest_region progress \"Receiving bundle (1/4)\" event &&\n+\t\ttest_region progress \"Receiving bundle (2/4)\" event &&\n+\t\ttest_region progress \"Receiving bundle (3/4)\" event &&\n+\t\ttest_region progress \"Receiving bundle (4/4)\" event\n+\telse\n+\t\tgrep \"fetch-pack: unable to spawn\" event\n+\tfi &&\n+\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags &&\n+\tgrep \"clone> want \" log\n+'\n+\n+test_expect_success \"clone with bundle-uri protocol v2 with $T5730_PROTOCOL:// ALL.bdl via $T5730_URI_BDL_PROTO\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1.bdl | test_uri_escape)\" &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/1-2.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/2-3.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/3-4.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/4-5.bdl | test_uri_escape)\" --add &&\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \"$(echo $T5730_URI_BDL/bdl/5-6.bdl | test_uri_escape)\" --add &&\n+\n+\ttest_when_finished \"rm -rf event log child\" &&\n+\tGIT_TRACE2_EVENT=\"$PWD/event\" \\\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c fetch.uriProtocols=file,http \\\n+\t\tclone --verbose --verbose \\\n+\t\t\"$T5730_URI\" child &&\n+\n+\tif test_have_prereq CURL\n+\tthen\n+\t\ttest_region progress \"Receiving bundle (1/6)\" event &&\n+\t\ttest_region progress \"Receiving bundle (2/6)\" event &&\n+\t\ttest_region progress \"Receiving bundle (3/6)\" event &&\n+\t\ttest_region progress \"Receiving bundle (4/6)\" event &&\n+\t\ttest_region progress \"Receiving bundle (5/6)\" event &&\n+\t\ttest_region progress \"Receiving bundle (6/6)\" event &&\n+\t\t! grep \"clone> want \" log\n+\telse\n+\t\tgrep \"fetch-pack: unable to spawn\" event\n+\tfi &&\n+\n+\ttest_cmp_repo_refs \"$T5730_PARENT\" child refs/heads refs/tags\n+'\ndiff --git a/transport.c b/transport.c\nindex 9e20b531215..7e5e1192f95 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -437,6 +437,7 @@ static int fetch_refs_via_pack(struct transport *transport,\n \targs.server_options = transport->server_options;\n \targs.negotiation_tips = data->options.negotiation_tips;\n \targs.reject_shallow_remote = transport->smart_options->reject_shallow;\n+\targs.bundle_uri = &transport->bundle_uri;\n \n \tif (!data->got_remote_heads) {\n \t\tint i;\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453870","messageId":"RFC-patch-v2-13.36-5c50daa92bb-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 13/36] bundle-uri client: add transfer.injectBundleURI support","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:30Z","receivedAt":"2022-04-18T17:25:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add the ability to inject \"fake\" bundle URIs into the newly supported\nbundle-uri dialog. As discussed in the added documentation this allows\nus to pretend as though the remote supports bundle URIs.\n\nThis will be useful both for ad-hoc testing, and for the real use-case\nof retrofitting bundle URI support on-the-fly, i.e. to have:\n\n\tgit -c transfer.injectBundleURI \"file://$(pwd)/local.bdl\" \\\n\tclone https://example.com/git.git\"\n\nBe similar in spirit to:\n\n\tgit clone --reference local-clone.git --disassociate \\\n\thttps://example.com/git.git\"\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/transfer.txt     | 20 ++++++++++++\n t/lib-t5730-protocol-v2-bundle-uri.sh | 46 +++++++++++++++++++++++++++\n transport.c                           | 33 +++++++++++++++++++\n 3 files changed, 99 insertions(+)\n\ndiff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt\nindex b49429eb4db..71b9b8f29e6 100644\n--- a/Documentation/config/transfer.txt\n+++ b/Documentation/config/transfer.txt\n@@ -77,3 +77,23 @@ transfer.unpackLimit::\n transfer.advertiseSID::\n \tBoolean. When true, client and server processes will advertise their\n \tunique session IDs to their remote counterpart. Defaults to false.\n+\n+transfer.injectBundleURI::\n+\tAllows for the injection of `bundle-uri` lines into the\n+\tprotocol v2 transport dialog (see `protocol.version` in\n+\tlinkgit:git-config[1]). See `bundle-uri` in\n+\tlink:technical/protocol-v2.html[the Git Wire Protocol, Version\n+\t2] documentation for what the format looks like.\n++\n+Can be given more than once, each key being injected as one line into\n+the dialog.\n++\n+This is useful for testing the `bundle-uri` facility, and to e.g. use\n+linkgit:git-clone[1] to clone from a server which does not support\n+`bundle-uri`, but where the clone can benefit from getting some or\n+most of the data from a static bundle retrieved from elsewhere.\n++\n+Impacts any command that uses the transport to communicate with remote\n+linkgit:git-upload-pack[1] processes, e.g. linkgit:git-clone[1],\n+linkgit:git-fetch[1] and the linkgit:git-ls-remote-bundle-uri[1]\n+inspection command, this includes the `file://` protocol.\ndiff --git a/t/lib-t5730-protocol-v2-bundle-uri.sh b/t/lib-t5730-protocol-v2-bundle-uri.sh\nindex d0b15a47ec2..28c095c1224 100644\n--- a/t/lib-t5730-protocol-v2-bundle-uri.sh\n+++ b/t/lib-t5730-protocol-v2-bundle-uri.sh\n@@ -267,3 +267,49 @@ test_expect_success \"ls-remote-bundle-uri --[no-]quiet with $T5730_PROTOCOL:// u\n \ttest_must_be_empty err &&\n \ttest_cmp out.expect out.actual\n '\n+\n+test_expect_success \"ls-remote-bundle-uri with -c transfer.injectBundleURI using with $T5730_PROTOCOL:// using protocol v2\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tcat >expect <<-\\EOF &&\n+\thttps://injected.example.com/fake-1.bdl\n+\thttps://injected.example.com/fake-2.bdl\n+\tEOF\n+\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c transfer.injectBundleURI=\"https://injected.example.com/fake-1.bdl\" \\\n+\t\t-c transfer.injectBundleURI=\"https://injected.example.com/fake-2.bdl\" \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>actual 2>err &&\n+\ttest_cmp expect actual &&\n+\ttest_path_is_missing log\n+'\n+\n+test_expect_success \"ls-remote-bundle-uri with bad -c transfer.injectBundleURI protocol v2 with $T5730_PROTOCOL://\" '\n+\ttest_when_finished \"rm -f log\" &&\n+\n+\ttest_config -C \"$T5730_PARENT\" uploadpack.bundleURI \\\n+\t\t\"$T5730_BUNDLE_URI_ESCAPED\" &&\n+\n+\tcat >err.expect <<-\\EOF &&\n+\terror: bad (empty) transfer.injectBundleURI\n+\terror: could not get the bundle-uri list\n+\tEOF\n+\n+\ttest_must_fail env \\\n+\t\tGIT_TRACE_PACKET=\"$PWD/log\" \\\n+\t\tgit \\\n+\t\t-c protocol.version=2 \\\n+\t\t-c transfer.injectBundleURI \\\n+\t\tls-remote-bundle-uri \\\n+\t\t\"$T5730_URI\" \\\n+\t\t>out 2>err.actual &&\n+\ttest_must_be_empty out &&\n+\ttest_cmp err.expect err.actual &&\n+\ttest_path_is_missing log\n+'\ndiff --git a/transport.c b/transport.c\nindex e648d3110bb..342e39d81f3 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1521,14 +1521,47 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs)\n \treturn rc;\n }\n \n+struct config_cb {\n+\tstruct transport *transport;\n+\tint configured;\n+\tint ret;\n+};\n+\n+static int bundle_uri_config(const char *var, const char *value, void *data)\n+{\n+\tstruct config_cb *cb = data;\n+\tstruct transport *transport = cb->transport;\n+\tstruct string_list *uri = &transport->bundle_uri;\n+\n+\tif (!strcmp(var, \"transfer.injectbundleuri\")) {\n+\t\tcb->configured = 1;\n+\t\tif (!value)\n+\t\t\tcb->ret = error(_(\"bad (empty) transfer.injectBundleURI\"));\n+\t\telse if (bundle_uri_parse_line(uri, value) < 0)\n+\t\t\tcb->ret = error(_(\"bad transfer.injectBundleURI: '%s'\"),\n+\t\t\t\t\tvalue);\n+\t\treturn 0;\n+\t}\n+\treturn 0;\n+}\n+\n int transport_get_remote_bundle_uri(struct transport *transport, int quiet)\n {\n \tconst struct transport_vtable *vtable = transport->vtable;\n+\tstruct config_cb cb = {\n+\t\t.transport = transport,\n+\t};\n \n \t/* Lazily configured */\n \tif (transport->got_remote_bundle_uri++)\n \t\treturn 0;\n \n+\tgit_config(bundle_uri_config, &cb);\n+\n+\t/* Our own config can fake it up with transport.injectBundleURI */\n+\tif (cb.configured)\n+\t\treturn cb.ret;\n+\n \t/*\n \t * This is intentionally below the transport.injectBundleURI,\n \t * we want to be able to inject into protocol v0, or into the\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453871","messageId":"RFC-patch-v2-16.36-632c68b224f-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 16/36] bundle-uri: make the download program configurable","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:33Z","receivedAt":"2022-04-18T17:25:35Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"As noted in a preceding commit we really should be using libcurl's C\nAPI by default in get_bundle_uri(), but testing with a command-line\nprogram can be very handy, and useful e.g. to implement custom or\nad-hoc caching.\n\nE.g. using part of the recipe noted in a preceding commit to create\nthe \"git-master-only.bdl\" and \"git-master-to-next.bdl\" files, we can\nimplement a trivial caching shellscript as:\n\n\tcat >get-bundle.sh <<-\\EOF &&\n\t#!/bin/sh\n\tset -xe\n\n\turi=\"$1\"\n\n\tbundle_cache_key () {\n\t\techo \"Computing cache key for URI '$1' (only getting the header)\" >&2\n\n\t\tcurl --silent --output - -- \"$1\" |\n\t\tsed -n -e '/^$/q' -e 'p' |\n\t\tgit hash-object --stdin\n\t}\n\n\tget_cached_bundle_uri() {\n\t\tcache_key=$(bundle_cache_key \"$1\")\n\n\t\tpath=\"/tmp/bundle-cache-$cache_key.bdl\"\n\n\t\tif test -e \"$path\"\n\t\tthen\n\t\t\techo \"Using cache '$path' for URI '$1'\" >&2\n\t\t\tcat \"$path\"\n\t\telse\n\t\t\techo \"Downloading bundle URI $1\" >&2\n\t\t\tcurl --silent --output - -- \"$uri\" | tee \"$path\"\n\t\tfi\n\t}\n\n\tget_cached_bundle_uri \"$1\"\n\tEOF\n\tchmod +x get-bundle.sh &&\n\trm -rf /tmp/git.git &&\n\t./git \\\n\t\t-c protocol.version=2 \\\n\t\t-c fetch.uriProtocols=file \\\n\t\t-c transfer.bundleURI.downloader=./get-bundle.sh \\\n\t\t-c transfer.injectBundleURI=\"file:///tmp/git-master-only.bdl\" \\\n\t\t-c transfer.injectBundleURI=\"file:///tmp/git-master-to-next.bdl\" \\\n\t\tclone --bare --no-tags --single-branch --branch next --template= \\\n\t\t--verbose --verbose \\\n\t\thttps://github.com/git/git.git /tmp/git.git\n\nNow, clearly that specific example is rather pointless. We're getting\na local file anyway, so \"cat\"-ing another local file doesn't make any\ndifference, it's even slightly slower & more redundant as we're having\nto get it twice with \"curl\".\n\nBut the point is that this can be trivially improved for use in any\narbitrary custom caching strategy. E.g.:\n\n * A less dumber implementation that would stream the remote URL,\n   check the header as we go, and disconnect if we've got that content\n   locally.\n * Ditto, but using an ETag or other strategy.\n * N boxes could share a cache an NFS with a shared mount, or N\n   disconnected git processes could use a common cache without the\n   need for a front-line HTTP proxy server.\n\n * It would be trivial to extend this to guard against a \"thundering\n   herd\" (e.g. concurrent CI) downloading the same bundle N times. As\n   soon as we'd get the header we'd create a $cache_key.lock as we\n   download the rest, and other concurrent clients spotting that would\n   wait, then eventually cache \"$cache_key\".\n\n   Still racy as N clients could download the header in parallel, but\n   way less so (the header will be a tiny part of the payload).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/transfer.txt | 7 +++++++\n fetch-pack.c                      | 6 ++++++\n 2 files changed, 13 insertions(+)\n\ndiff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt\nindex ae85ca5760b..5310cd96cb9 100644\n--- a/Documentation/config/transfer.txt\n+++ b/Documentation/config/transfer.txt\n@@ -84,6 +84,13 @@ transfer.bundleURI::\n \tusing bundles to bootstap is possible. Defaults to `true`,\n \ti.e. bundle-uri is tried whenever a server offers it.\n \n+transfer.bundleURI.downloader::\n+\tWhen set to `<program>` will be invoked when\n+\t`transfer.bundleURI` is in effect to download URIs containing\n+\tbundles. Expected to take one `URI` as an argument, and to\n+\temit the bundle on STDOUT. Defaults to \"curl --silent --output\n+\t- --\". I.e. we'll invoke \"curl --silent --output - -- <URI>\".\n+\n transfer.injectBundleURI::\n \tAllows for the injection of `bundle-uri` lines into the\n \tprotocol v2 transport dialog (see `protocol.version` in\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 316fb2fd65d..7e696142c4d 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1121,12 +1121,18 @@ static int get_bundle_uri(struct string_list_item *item, unsigned int nth,\n \tconst char *uri = item->string;\n \tFILE *out;\n \tint out_fd;\n+\tconst char *tmp;\n \n \tstrvec_push(&cmd.args, \"curl\");\n \tstrvec_push(&cmd.args, \"--silent\");\n \tstrvec_push(&cmd.args, \"--output\");\n \tstrvec_push(&cmd.args, \"-\");\n \tstrvec_push(&cmd.args, \"--\");\n+\tif (!git_config_get_string_tmp(\"transfer.bundleURI.downloader\", &tmp)) {\n+\t\tstrvec_clear(&cmd.args);\n+\t\tstrvec_push(&cmd.args, tmp);\n+\t\tcmd.use_shell = 1;\n+\t}\n \tstrvec_push(&cmd.args, item->string);\n \tcmd.git_cmd = 0;\n \tcmd.no_stdin = 1;\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453872","messageId":"RFC-patch-v2-18.36-ff9a7afaccd-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 18/36] bundle: implement 'fetch' command for direct bundles","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:35Z","receivedAt":"2022-04-18T17:25:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThe 'git bundle fetch <uri>' command will be used to download one or\nmore bundles from a specified '<uri>'. The implementation being added\nhere focuses only on downloading a file from '<uri>' and unbundling it\nif it is a valid bundle file.\n\nIf it is not a bundle file, then we currently die(), but a later change\nwill attempt to interpret it as a table of contents with possibly\nmultiple bundles listed, along with other metadata for each bundle.\n\nThat explains a bit why cmd_bundle_fetch() has three steps carefully\ncommented, including a \"stack\" that currently can only hold one bundle.\nWe will later update this while loop to push onto the stack when\nnecessary.\n\nRFC-TODO: Add documentation to Documentation/git-bundle.txt\n\nRFC-TODO: Add direct tests of 'git bundle fetch' when the URI is a\nbundle file.\n\nRFC-TODO: Split out the docs and subcommand boilerplate into its own\ncommit.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n Documentation/git-bundle.txt |   1 +\n builtin/bundle.c             | 261 +++++++++++++++++++++++++++++++++++\n 2 files changed, 262 insertions(+)\n\ndiff --git a/Documentation/git-bundle.txt b/Documentation/git-bundle.txt\nindex 7685b570455..bf5cd90391c 100644\n--- a/Documentation/git-bundle.txt\n+++ b/Documentation/git-bundle.txt\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git bundle' create [-q | --quiet | --progress | --all-progress] [--all-progress-implied]\n \t\t    [--version=<version>] <file> <git-rev-list-args>\n 'git bundle' verify [-q | --quiet] <file>\n+'git bundle' fetch [--filter=<spec>] <uri>\n 'git bundle' list-heads <file> [<refname>...]\n 'git bundle' unbundle [--progress] <file> [<refname>...]\n \ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex 2adad545a2e..6b6107d83cf 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -3,6 +3,10 @@\n #include \"parse-options.h\"\n #include \"cache.h\"\n #include \"bundle.h\"\n+#include \"run-command.h\"\n+#include \"hashmap.h\"\n+#include \"object-store.h\"\n+#include \"refs.h\"\n \n /*\n  * Basic handler for bundle files to connect repositories via sneakernet.\n@@ -14,6 +18,7 @@\n static const char * const builtin_bundle_usage[] = {\n   N_(\"git bundle create [<options>] <file> <git-rev-list args>\"),\n   N_(\"git bundle verify [<options>] <file>\"),\n+  N_(\"git bundle fetch [<options>] <uri>\"),\n   N_(\"git bundle list-heads <file> [<refname>...]\"),\n   N_(\"git bundle unbundle <file> [<refname>...]\"),\n   NULL\n@@ -29,6 +34,11 @@ static const char * const builtin_bundle_verify_usage[] = {\n   NULL\n };\n \n+static const char * const builtin_bundle_fetch_usage[] = {\n+\tN_(\"git bundle fetch [--filter=<spec>] <uri>\"),\n+\tNULL\n+};\n+\n static const char * const builtin_bundle_list_heads_usage[] = {\n   N_(\"git bundle list-heads <file> [<refname>...]\"),\n   NULL\n@@ -132,6 +142,255 @@ static int cmd_bundle_verify(int argc, const char **argv, const char *prefix) {\n \treturn ret;\n }\n \n+/**\n+ * The remote_bundle_info struct contains the necessary data for\n+ * the list of bundles advertised by a table of contents. If the\n+ * bundle URI instead contains a single bundle, then this struct\n+ * can represent a single bundle without a 'uri' but with a\n+ * tempfile storing its current location on disk.\n+ */\n+struct remote_bundle_info {\n+\tstruct hashmap_entry ent;\n+\n+\t/**\n+\t * The 'id' is a name given to the bundle for reference\n+\t * by other bundle infos.\n+\t */\n+\tchar *id;\n+\n+\t/**\n+\t * The 'uri' is the location of the remote bundle so\n+\t * it can be downloaded on-demand. This will be NULL\n+\t * if there was no table of contents.\n+\t */\n+\tchar *uri;\n+\n+\t/**\n+\t * The 'next_id' string, if non-NULL, contains the 'id'\n+\t * for a bundle that contains the prerequisites for this\n+\t * bundle. Used by table of contents to allow fetching\n+\t * a portion of a repository incrementally.\n+\t */\n+\tchar *next_id;\n+\n+\t/**\n+\t * A table of contents can include a timestamp for the\n+\t * bundle as a heuristic for describing a list of bundles\n+\t * in order of recency.\n+\t */\n+\ttimestamp_t timestamp;\n+\n+\t/**\n+\t * If the bundle has been downloaded, then 'file' is a\n+\t * filename storing its contents. Otherwise, 'file' is\n+\t * an empty string.\n+\t */\n+\tstruct strbuf file;\n+\n+\t/**\n+\t * The 'stack_next' pointer allows this struct to form\n+\t * a stack.\n+\t */\n+\tstruct remote_bundle_info *stack_next;\n+};\n+\n+static void download_uri_to_file(const char *uri, const char *file)\n+{\n+\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\tFILE *child_in;\n+\n+\tstrvec_pushl(&cp.args, \"git-remote-https\", \"origin\", uri, NULL);\n+\tcp.in = -1;\n+\tcp.out = -1;\n+\n+\tif (start_command(&cp))\n+\t\tdie(_(\"failed to start remote helper\"));\n+\n+\tchild_in = fdopen(cp.in, \"w\");\n+\tif (!child_in)\n+\t\tdie(_(\"cannot write to child process\"));\n+\n+\tfprintf(child_in, \"get %s %s\\n\\n\", uri, file);\n+\tfclose(child_in);\n+\n+\tif (finish_command(&cp))\n+\t\tdie(_(\"remote helper failed\"));\n+}\n+\n+static void find_temp_filename(struct strbuf *name)\n+{\n+\tint fd;\n+\t/*\n+\t * Find a temporray filename that is available. This is briefly\n+\t * racy, but unlikely to collide.\n+\t */\n+\tfd = odb_mkstemp(name, \"bundles/tmp_uri_XXXXXX\");\n+\tif (fd < 0)\n+\t\tdie(_(\"failed to create temporary file\"));\n+\tclose(fd);\n+\tunlink(name->buf);\n+}\n+\n+static void unbundle_fetched_bundle(struct remote_bundle_info *info)\n+{\n+\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\tFILE *f;\n+\tstruct strbuf line = STRBUF_INIT;\n+\tstruct strbuf bundle_ref = STRBUF_INIT;\n+\tsize_t bundle_prefix_len;\n+\n+\tstrvec_pushl(&cp.args, \"bundle\", \"unbundle\",\n+\t\t\t\tinfo->file.buf, NULL);\n+\tcp.git_cmd = 1;\n+\tcp.out = -1;\n+\n+\tif (start_command(&cp))\n+\t\tdie(_(\"failed to start 'unbundle' process\"));\n+\n+\tstrbuf_addstr(&bundle_ref, \"refs/bundles/\");\n+\tbundle_prefix_len = bundle_ref.len;\n+\n+\tf = fdopen(cp.out, \"r\");\n+\twhile (strbuf_getline(&line, f) != EOF) {\n+\t\tstruct object_id oid, old_oid;\n+\t\tconst char *refname, *branch_name, *end;\n+\t\tchar *space;\n+\t\tint has_old;\n+\n+\t\tstrbuf_trim_trailing_newline(&line);\n+\n+\t\tspace = strchr(line.buf, ' ');\n+\n+\t\tif (!space)\n+\t\t\tcontinue;\n+\n+\t\trefname = space + 1;\n+\t\t*space = '\\0';\n+\t\tparse_oid_hex(line.buf, &oid, &end);\n+\n+\t\tif (!skip_prefix(refname, \"refs/heads/\", &branch_name))\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_setlen(&bundle_ref, bundle_prefix_len);\n+\t\tstrbuf_addstr(&bundle_ref, branch_name);\n+\n+\t\thas_old = !read_ref(bundle_ref.buf, &old_oid);\n+\n+\t\tupdate_ref(\"bundle fetch\", bundle_ref.buf, &oid,\n+\t\t\t\thas_old ? &old_oid : NULL,\n+\t\t\t\tREF_SKIP_OID_VERIFICATION,\n+\t\t\t\tUPDATE_REFS_MSG_ON_ERR);\n+\t}\n+\n+\tif (finish_command(&cp))\n+\t\tdie(_(\"failed to unbundle bundle from '%s'\"), info->uri);\n+\n+\tunlink_or_warn(info->file.buf);\n+}\n+\n+static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n+{\n+\tint ret = 0;\n+\tint progress = isatty(2);\n+\tchar *bundle_uri;\n+\tstruct remote_bundle_info first_file = {\n+\t\t.file = STRBUF_INIT,\n+\t};\n+\tstruct remote_bundle_info *stack = NULL;\n+\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"progress\", &progress,\n+\t\t\t N_(\"show progress meter\")),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options_cmd_bundle(argc, argv, prefix,\n+\t\t\tbuiltin_bundle_fetch_usage, options, &bundle_uri);\n+\n+\tif (!startup_info->have_repository)\n+\t\tdie(_(\"'fetch' requires a repository\"));\n+\n+\t/*\n+\t * Step 1: determine protocol for uri, and download contents to\n+\t * a temporary location.\n+\t */\n+\tfirst_file.uri = bundle_uri;\n+\tfind_temp_filename(&first_file.file);\n+\tdownload_uri_to_file(bundle_uri, first_file.file.buf);\n+\n+\t/*\n+\t * Step 2: Check if the file is a bundle (if so, add it to the\n+\t * stack and move to step 3).\n+\t */\n+\n+\tif (is_bundle(first_file.file.buf, 1)) {\n+\t\t/* The simple case: only one file, no stack to worry about. */\n+\t\tstack = &first_file;\n+\t} else {\n+\t\t/* TODO: Expect and parse a table of contents. */\n+\t\tdie(_(\"unexpected data at bundle URI\"));\n+\t}\n+\n+\t/*\n+\t * Step 3: For each bundle in the stack:\n+\t * \ti. If not downloaded to a temporary file, download it.\n+\t * \tii. Once downloaded, check that its prerequisites are in\n+\t * \t    the object database. If not, then push its dependent\n+\t * \t    bundle onto the stack. (Fail if no such bundle exists.)\n+\t * \tiii. If all prerequisites are present, then unbundle the\n+\t * \t     temporary file and pop the bundle from the stack.\n+\t */\n+\twhile (stack) {\n+\t\tint valid = 1;\n+\t\tint bundle_fd;\n+\t\tstruct string_list_item *prereq;\n+\t\tstruct bundle_header header = BUNDLE_HEADER_INIT;\n+\n+\t\tif (!stack->file.len) {\n+\t\t\tfind_temp_filename(&stack->file);\n+\t\t\tdownload_uri_to_file(stack->uri, stack->file.buf);\n+\t\t\tif (!is_bundle(stack->file.buf, 1))\n+\t\t\t\tdie(_(\"file downloaded from '%s' is not a bundle\"), stack->uri);\n+\t\t}\n+\n+\t\tbundle_header_init(&header);\n+\t\tbundle_fd = read_bundle_header(stack->file.buf, &header);\n+\t\tif (bundle_fd < 0)\n+\t\t\tdie(_(\"failed to read bundle from '%s'\"), stack->uri);\n+\n+\t\tfor_each_string_list_item(prereq, &header.prerequisites) {\n+\t\t\tstruct object_info info = OBJECT_INFO_INIT;\n+\t\t\tstruct object_id *oid = prereq->util;\n+\n+\t\t\tif (oid_object_info_extended(the_repository, oid, &info,\n+\t\t\t\t\t\t     OBJECT_INFO_QUICK)) {\n+\t\t\t\tvalid = 0;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t\tclose(bundle_fd);\n+\t\tbundle_header_release(&header);\n+\n+\t\tif (valid) {\n+\t\t\tunbundle_fetched_bundle(stack);\n+\t\t} else if (stack->next_id) {\n+\t\t\t/*\n+\t\t\t * Load the next bundle from the hashtable and\n+\t\t\t * push it onto the stack.\n+\t\t\t */\n+\t\t} else {\n+\t\t\tdie(_(\"bundle from '%s' has missing prerequisites and no dependent bundle\"),\n+\t\t\t    stack->uri);\n+\t\t}\n+\n+\t\tstack = stack->stack_next;\n+\t}\n+\n+\tfree(bundle_uri);\n+\treturn ret;\n+}\n+\n static int cmd_bundle_list_heads(int argc, const char **argv, const char *prefix) {\n \tstruct bundle_header header = BUNDLE_HEADER_INIT;\n \tint bundle_fd = -1;\n@@ -212,6 +471,8 @@ int cmd_bundle(int argc, const char **argv, const char *prefix)\n \t\tresult = cmd_bundle_create(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"verify\"))\n \t\tresult = cmd_bundle_verify(argc, argv, prefix);\n+\telse if (!strcmp(argv[0], \"fetch\"))\n+\t\tresult = cmd_bundle_fetch(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"list-heads\"))\n \t\tresult = cmd_bundle_list_heads(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"unbundle\"))\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453873","messageId":"RFC-patch-v2-17.36-8ac5bfca545-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 17/36] remote-curl: add 'get' capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:34Z","receivedAt":"2022-04-18T17:25:42Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nA future change will want a way to download a file over HTTP(S) using\nthe simplest of download mechanisms. We do not want to assume that the\nserver on the other side understands anything about the Git protocol but\ncould be a simple static web server.\n\nCreate the new 'get' capability for the remote helpers which advertises\nthat the 'get' command is avalable. A caller can send a line containing\n'get <url> <path>' to download the file at <url> into the file at\n<path>.\n\nRFC-TODO: This change requires tests directly on the remote helper.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n Documentation/gitremote-helpers.txt |  6 ++++++\n remote-curl.c                       | 32 +++++++++++++++++++++++++++++\n 2 files changed, 38 insertions(+)\n\ndiff --git a/Documentation/gitremote-helpers.txt b/Documentation/gitremote-helpers.txt\nindex 6f1e269ae43..f82588601a9 100644\n--- a/Documentation/gitremote-helpers.txt\n+++ b/Documentation/gitremote-helpers.txt\n@@ -168,6 +168,9 @@ Supported commands: 'list', 'import'.\n \tCan guarantee that when a clone is requested, the received\n \tpack is self contained and is connected.\n \n+'get'::\n+\tCan use the 'get' command to download a file from a given URI.\n+\n If a helper advertises 'connect', Git will use it if possible and\n fall back to another capability if the helper requests so when\n connecting (see the 'connect' command under COMMANDS).\n@@ -418,6 +421,9 @@ Supported if the helper has the \"connect\" capability.\n +\n Supported if the helper has the \"stateless-connect\" capability.\n \n+'get' <uri> <path>::\n+\tDownloads the file from the given `<uri>` to the given `<path>`.\n+\n If a fatal error occurs, the program writes the error message to\n stderr and exits. The caller should expect that a suitable error\n message has been printed if the child closes the connection without\ndiff --git a/remote-curl.c b/remote-curl.c\nindex 67f178b1120..53750d88e76 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -1276,6 +1276,33 @@ static void parse_fetch(struct strbuf *buf)\n \tstrbuf_reset(buf);\n }\n \n+static void parse_get(struct strbuf *buf)\n+{\n+\tstruct http_get_options opts = { 0 };\n+\tstruct strbuf url = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst char *p, *space;\n+\n+\tif (!skip_prefix(buf->buf, \"get \", &p))\n+\t\tdie(_(\"http transport does not support %s\"), buf->buf);\n+\n+\tspace = strchr(p, ' ');\n+\n+\tif (!space)\n+\t\tdie(_(\"protocol error: expected '<url> <path>', missing space\"));\n+\n+\tstrbuf_add(&url, p, space - p);\n+\tstrbuf_addstr(&path, space + 1);\n+\n+\thttp_get_file(url.buf, path.buf, &opts);\n+\n+\tstrbuf_release(&url);\n+\tstrbuf_release(&path);\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+\tstrbuf_reset(buf);\n+}\n+\n static int push_dav(int nr_spec, const char **specs)\n {\n \tstruct child_process child = CHILD_PROCESS_INIT;\n@@ -1549,9 +1576,14 @@ int cmd_main(int argc, const char **argv)\n \t\t\t\tprintf(\"unsupported\\n\");\n \t\t\tfflush(stdout);\n \n+\t\t} else if (skip_prefix(buf.buf, \"get \", &arg)) {\n+\t\t\tparse_get(&buf);\n+\t\t\tfflush(stdout);\n+\n \t\t} else if (!strcmp(buf.buf, \"capabilities\")) {\n \t\t\tprintf(\"stateless-connect\\n\");\n \t\t\tprintf(\"fetch\\n\");\n+\t\t\tprintf(\"get\\n\");\n \t\t\tprintf(\"option\\n\");\n \t\t\tprintf(\"push\\n\");\n \t\t\tprintf(\"check-connectivity\\n\");\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453874","messageId":"RFC-patch-v2-19.36-a5245a31a12-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 19/36] bundle: parse table of contents during 'fetch'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:36Z","receivedAt":"2022-04-18T17:25:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nIn order to support a flexible bundle URI feature, we allow the server\nto return a \"table of contents\" file that is formatted according to Git\nconfig file standards. These files can describe multiple bundles,\nintended to assist with using bundle URIs for fetching or with partial\nclone.\n\nHere is an example table of contents file:\n\n[bundle \"tableofcontents\"]\n\tversion = 1\n\n[bundle \"2022-02-09-1644442601-daily\"]\n\turi = 2022-02-09-1644442601-daily.bundle\n\ttimestamp = 1644442601\n\trequires = 2022-02-02-1643842562\n\n[bundle \"2022-02-02-1643842562\"]\n\turi = 2022-02-02-1643842562.bundle\n\ttimestamp = 1643842562\n\n[bundle \"2022-02-09-1644442631-daily-blobless\"]\n\turi = 2022-02-09-1644442631-daily-blobless.bundle\n\ttimestamp = 1644442631\n\trequires = 2022-02-02-1643842568-blobless\n\tfilter = blob:none\n\n[bundle \"2022-02-02-1643842568-blobless\"]\n\turi = 2022-02-02-1643842568-blobless.bundle\n\ttimestamp = 1643842568\n\tfilter = blob:none\n\n(End of example.)\n\nThis file contains some important fixed values, such as\n\n * bundle.tableofcontents.version = 1\n\nAlso, different bundles are referenced by <id>, using keys with names\n\n * bundle.<id>.uri: the URI to download this bundle. This could be an\n   absolute URI or a URI relative to the bundle file's URI.\n * bundle.<id>.timestamp: the timestamp when this file was generated.\n * bundle.<id>.filter: the partial clone filter applied on this bundle.\n * bundle.<id>.requires: the ID for the previous bundle.\n\nThe current change does not parse the '.filter' option, but does use the\n'.requires' in the 'while (stack)' loop.\n\nThe process is that 'git bundle fetch' will parse the table of contents\nand pick the most-recent bundle and download that one. That bundle\nheader has a ref listing, including (possibly) a list of commits that\nare missing from the bundle. If any of those commits are missing, then\nGit downloads the bundle specified by the '.requires' value and tries\nagain.  Eventually, Git should download a bundle where all missing\ncommits actually exist in the current repository, or Git downloads a\nbundle with no missing commits.\n\nOf course, the server could be advertising incorrect information, so it\ncould advertise bundles that never satisfy the missing objects. It could\nalso create a directed cycle in its '.requires' specifications. In each\nof these cases, Git will die with a \"bundle '<id>' still invalid after\ndownloading required bundle\" message or a \"bundle from '<uri>' has\nmissing prerequisites and no dependent bundle\" message.\n\nRFC-TODO: add a direct test of table of contents parsing in this change.\nRFC-TODO: create tests that check these erroneous cases.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/bundle.c | 172 +++++++++++++++++++++++++++++++++++++++++++++--\n 1 file changed, 165 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex 6b6107d83cf..255d7aa774b 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -7,6 +7,8 @@\n #include \"hashmap.h\"\n #include \"object-store.h\"\n #include \"refs.h\"\n+#include \"config.h\"\n+#include \"packfile.h\"\n \n /*\n  * Basic handler for bundle files to connect repositories via sneakernet.\n@@ -166,12 +168,21 @@ struct remote_bundle_info {\n \tchar *uri;\n \n \t/**\n-\t * The 'next_id' string, if non-NULL, contains the 'id'\n+\t * The 'requires_id' string, if non-NULL, contains the 'id'\n \t * for a bundle that contains the prerequisites for this\n \t * bundle. Used by table of contents to allow fetching\n \t * a portion of a repository incrementally.\n \t */\n-\tchar *next_id;\n+\tchar *requires_id;\n+\n+\t/**\n+\t * The 'filter_str' string, if non-NULL, specifies the\n+\t * filter capability exists in this bundle with the given\n+\t * specification. Allows selecting bundles that match the\n+\t * client's desired filter. If NULL, then no filter exists\n+\t * on the bundle.\n+\t */\n+\tchar *filter_str;\n \n \t/**\n \t * A table of contents can include a timestamp for the\n@@ -192,7 +203,108 @@ struct remote_bundle_info {\n \t * a stack.\n \t */\n \tstruct remote_bundle_info *stack_next;\n+\n+\t/**\n+\t * 'pushed' is set when first pushing the required bundle\n+\t * onto the stack. Used to error out when verifying the\n+\t * prerequisites and avoiding an infinite loop.\n+\t */\n+\tunsigned pushed:1;\n };\n+#define REMOTE_BUNDLE_INFO_INIT { \\\n+\t.file = STRBUF_INIT, \\\n+}\n+\n+static int remote_bundle_cmp(const void *unused_cmp_data,\n+\t\t\t     const struct hashmap_entry *a,\n+\t\t\t     const struct hashmap_entry *b,\n+\t\t\t     const void *key)\n+{\n+\tconst struct remote_bundle_info *ee1 =\n+\t\t\tcontainer_of(a, struct remote_bundle_info, ent);\n+\tconst struct remote_bundle_info *ee2 =\n+\t\t\tcontainer_of(b, struct remote_bundle_info, ent);\n+\n+\treturn strcmp(ee1->id, ee2->id);\n+}\n+\n+static int parse_toc_config(const char *key, const char *value, void *data)\n+{\n+\tstruct hashmap *toc = data;\n+\tconst char *key1, *key2, *id_end;\n+\tstruct strbuf id = STRBUF_INIT;\n+\tstruct remote_bundle_info info_lookup = REMOTE_BUNDLE_INFO_INIT;\n+\tstruct remote_bundle_info *info;\n+\n+\tif (!skip_prefix(key, \"bundle.\", &key1))\n+\t\treturn -1;\n+\n+\tif (skip_prefix(key1, \"tableofcontents.\", &key2)) {\n+\t\tif (!strcmp(key2, \"version\")) {\n+\t\t\tint version = git_config_int(key, value);\n+\n+\t\t\tif (version != 1) {\n+\t\t\t\twarning(_(\"table of contents version %d not understood\"), version);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn 0;\n+\t}\n+\n+\tid_end = strchr(key1, '.');\n+\n+\t/*\n+\t * If this key is of the form \"bundle.<x>\" with no third item,\n+\t * then we do not know about it. We should ignore it. Later versions\n+\t * might start caring about this data on an optional basis. Increase\n+\t * the version number to add keys that must be understood.\n+\t */\n+\tif (!id_end)\n+\t\treturn 0;\n+\n+\tstrbuf_add(&id, key1, id_end - key1);\n+\tkey2 = id_end + 1;\n+\n+\tinfo_lookup.id = id.buf;\n+\thashmap_entry_init(&info_lookup.ent, strhash(info_lookup.id));\n+\tif (!(info = hashmap_get_entry(toc, &info_lookup, ent, NULL))) {\n+\t\tCALLOC_ARRAY(info, 1);\n+\t\tinfo->id = strbuf_detach(&id, NULL);\n+\t\tstrbuf_init(&info->file, 0);\n+\t\thashmap_entry_init(&info->ent, strhash(info->id));\n+\t\thashmap_add(toc, &info->ent);\n+\t}\n+\n+\tif (!strcmp(key2, \"uri\")) {\n+\t\tif (info->uri)\n+\t\t\twarning(_(\"duplicate 'uri' value for id '%s'\"), info->id);\n+\t\telse\n+\t\t\tinfo->uri = xstrdup(value);\n+\t\treturn 0;\n+\t} else if (!strcmp(key2, \"timestamp\")) {\n+\t\tif (info->timestamp)\n+\t\t\twarning(_(\"duplicate 'timestamp' value for id '%s'\"), info->id);\n+\t\telse\n+\t\t\tinfo->timestamp = git_config_int64(key, value);\n+\t\treturn 0;\n+\t} else if (!strcmp(key2, \"requires\")) {\n+\t\tif (info->requires_id)\n+\t\t\twarning(_(\"duplicate 'requires' value for id '%s'\"), info->id);\n+\t\telse\n+\t\t\tinfo->requires_id = xstrdup(value);\n+\t\treturn 0;\n+\t} else if (!strcmp(key2, \"filter\")) {\n+\t\tif (info->filter_str)\n+\t\t\twarning(_(\"duplicate 'filter' value for id '%s'\"), info->id);\n+\t\telse\n+\t\t\tinfo->filter_str = xstrdup(value);\n+\t\treturn 0;\n+\t}\n+\n+\t/* Return 0 here to ignore unknown options. */\n+\treturn 0;\n+}\n \n static void download_uri_to_file(const char *uri, const char *file)\n {\n@@ -290,13 +402,14 @@ static void unbundle_fetched_bundle(struct remote_bundle_info *info)\n \n static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n {\n-\tint ret = 0;\n+\tint ret = 0, used_hashmap = 0;\n \tint progress = isatty(2);\n \tchar *bundle_uri;\n \tstruct remote_bundle_info first_file = {\n \t\t.file = STRBUF_INIT,\n \t};\n \tstruct remote_bundle_info *stack = NULL;\n+\tstruct hashmap toc = { 0 };\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"progress\", &progress,\n@@ -320,15 +433,31 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \n \t/*\n \t * Step 2: Check if the file is a bundle (if so, add it to the\n-\t * stack and move to step 3).\n+\t * stack and move to step 3). Otherwise, expect it to be a table\n+\t * of contents. Use the table to populate a hashtable of bundles\n+\t * and push the most recent bundle to the stack.\n \t */\n \n \tif (is_bundle(first_file.file.buf, 1)) {\n \t\t/* The simple case: only one file, no stack to worry about. */\n \t\tstack = &first_file;\n \t} else {\n-\t\t/* TODO: Expect and parse a table of contents. */\n-\t\tdie(_(\"unexpected data at bundle URI\"));\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct remote_bundle_info *info;\n+\t\ttimestamp_t max_time = 0;\n+\n+\t\t/* populate a hashtable with all relevant bundles. */\n+\t\tused_hashmap = 1;\n+\t\thashmap_init(&toc, remote_bundle_cmp, NULL, 0);\n+\t\tgit_config_from_file(parse_toc_config, first_file.file.buf, &toc);\n+\n+\t\t/* initialize stack using timestamp heuristic. */\n+\t\thashmap_for_each_entry(&toc, &iter, info, ent) {\n+\t\t\tif (info->timestamp > max_time || !stack) {\n+\t\t\t\tstack = info;\n+\t\t\t\tmax_time = info->timestamp;\n+\t\t\t}\n+\t\t}\n \t}\n \n \t/*\n@@ -358,6 +487,7 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t\tif (bundle_fd < 0)\n \t\t\tdie(_(\"failed to read bundle from '%s'\"), stack->uri);\n \n+\t\treprepare_packed_git(the_repository);\n \t\tfor_each_string_list_item(prereq, &header.prerequisites) {\n \t\t\tstruct object_info info = OBJECT_INFO_INIT;\n \t\t\tstruct object_id *oid = prereq->util;\n@@ -374,11 +504,28 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \n \t\tif (valid) {\n \t\t\tunbundle_fetched_bundle(stack);\n-\t\t} else if (stack->next_id) {\n+\t\t} else if (stack->pushed) {\n+\t\t\tdie(_(\"bundle '%s' still invalid after downloading required bundle\"), stack->id);\n+\t\t} else if (stack->requires_id) {\n \t\t\t/*\n \t\t\t * Load the next bundle from the hashtable and\n \t\t\t * push it onto the stack.\n \t\t\t */\n+\t\t\tstruct remote_bundle_info *info;\n+\t\t\tstruct remote_bundle_info info_lookup = REMOTE_BUNDLE_INFO_INIT;\n+\t\t\tinfo_lookup.id = stack->requires_id;\n+\n+\t\t\thashmap_entry_init(&info_lookup.ent, strhash(info_lookup.id));\n+\t\t\tif ((info = hashmap_get_entry(&toc, &info_lookup, ent, NULL))) {\n+\t\t\t\t/* Push onto the stack */\n+\t\t\t\tstack->pushed = 1;\n+\t\t\t\tinfo->stack_next = stack;\n+\t\t\t\tstack = info;\n+\t\t\t\tcontinue;\n+\t\t\t} else {\n+\t\t\t\tdie(_(\"unable to find bundle '%s' required by bundle '%s'\"),\n+\t\t\t\t    stack->requires_id, stack->id);\n+\t\t\t}\n \t\t} else {\n \t\t\tdie(_(\"bundle from '%s' has missing prerequisites and no dependent bundle\"),\n \t\t\t    stack->uri);\n@@ -387,6 +534,17 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t\tstack = stack->stack_next;\n \t}\n \n+\tif (used_hashmap) {\n+\t\tstruct hashmap_iter iter;\n+\t\tstruct remote_bundle_info *info;\n+\t\thashmap_for_each_entry(&toc, &iter, info, ent) {\n+\t\t\tfree(info->id);\n+\t\t\tfree(info->uri);\n+\t\t\tfree(info->requires_id);\n+\t\t\tfree(info->filter_str);\n+\t\t}\n+\t\thashmap_clear_and_free(&toc, struct remote_bundle_info, ent);\n+\t}\n \tfree(bundle_uri);\n \treturn ret;\n }\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453875","messageId":"RFC-patch-v2-20.36-c3a60a9bc72-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 20/36] bundle: add --filter option to 'fetch'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:37Z","receivedAt":"2022-04-18T17:25:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nWhen a repository uses an object filter for partial clone, the 'git\nbundle fetch' command should try to download bundles that match that\nfilter.\n\nTeach 'git bundle fetch' to take a '--filter' option and then only\nconsider bundles that match that filter (or lack thereof). This allows\nthe bundle server to advertise different sets of bundles for different\nfilters.\n\nAdd some verification to be sure that the bundle we downloaded actually\nuses that filter. This is especially important when no filter is\nrequested but the downloaded bundle _does_ have a filter.\n\nRFC-TODO: add tests for the happy path.\n\nRFC-TODO: add tests for these validations.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/bundle.c | 26 ++++++++++++++++++++++++++\n 1 file changed, 26 insertions(+)\n\ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex 255d7aa774b..711e0863a16 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -9,6 +9,7 @@\n #include \"refs.h\"\n #include \"config.h\"\n #include \"packfile.h\"\n+#include \"list-objects-filter-options.h\"\n \n /*\n  * Basic handler for bundle files to connect repositories via sneakernet.\n@@ -410,10 +411,13 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t};\n \tstruct remote_bundle_info *stack = NULL;\n \tstruct hashmap toc = { 0 };\n+\tconst char *filter = NULL;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"progress\", &progress,\n \t\t\t N_(\"show progress meter\")),\n+\t\tOPT_STRING(0, \"filter\", &filter,\n+\t\t\t   N_(\"filter-spec\"), N_(\"only install bundles matching this filter\")),\n \t\tOPT_END()\n \t};\n \n@@ -453,6 +457,17 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \n \t\t/* initialize stack using timestamp heuristic. */\n \t\thashmap_for_each_entry(&toc, &iter, info, ent) {\n+\t\t\t/* Skip if filter does not match. */\n+\t\t\tif (!filter && info->filter_str)\n+\t\t\t\tcontinue;\n+\t\t\tif (filter &&\n+\t\t\t    (!info->filter_str || strcasecmp(filter, info->filter_str)))\n+\t\t\t\tcontinue;\n+\n+\t\t\t/*\n+\t\t\t * Now that the filter matches, start with the\n+\t\t\t * bundle with largest timestamp.\n+\t\t\t */\n \t\t\tif (info->timestamp > max_time || !stack) {\n \t\t\t\tstack = info;\n \t\t\t\tmax_time = info->timestamp;\n@@ -472,6 +487,7 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \twhile (stack) {\n \t\tint valid = 1;\n \t\tint bundle_fd;\n+\t\tconst char *filter_str = NULL;\n \t\tstruct string_list_item *prereq;\n \t\tstruct bundle_header header = BUNDLE_HEADER_INIT;\n \n@@ -487,6 +503,16 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t\tif (bundle_fd < 0)\n \t\t\tdie(_(\"failed to read bundle from '%s'\"), stack->uri);\n \n+\t\tif (header.filter.choice)\n+\t\t\tfilter_str = list_objects_filter_spec(&header.filter);\n+\n+\t\tif (filter && (!filter_str || strcasecmp(filter, filter_str)))\n+\t\t\tdie(_(\"bundle from '%s' does not match expected filter\"),\n+\t\t\t    stack->uri);\n+\t\tif (!filter && filter_str)\n+\t\t\tdie(_(\"bundle from '%s' has an unexpected filter\"),\n+\t\t\t    stack->uri);\n+\n \t\treprepare_packed_git(the_repository);\n \t\tfor_each_string_list_item(prereq, &header.prerequisites) {\n \t\t\tstruct object_info info = OBJECT_INFO_INIT;\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453876","messageId":"RFC-patch-v2-21.36-c08406cd9c2-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 21/36] bundle: allow relative URLs in table of contents","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:38Z","receivedAt":"2022-04-18T17:25:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nWhen hosting bundle data, it can be helpful to distribute that data\nacross multiple CDNs. This might require a change in the base URI, all\nthe way to the domain name. If all bundles require an absolute URI in\ntheir 'uri' value, then every push to a CDN would require altering the\ntable of contents to match the expected domain and exact location within\nit.\n\nAllow the table of contents to specify a relative URI for the bundles.\nThis allows easier distribution of bundle data.\n\nRFC-TODO: An earlier change referenced relative URLs, but it was not\nimplemented until this change.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/bundle.c | 7 +++++++\n 1 file changed, 7 insertions(+)\n\ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex 711e0863a16..c55d5215181 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -10,6 +10,7 @@\n #include \"config.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"remote.h\"\n \n /*\n  * Basic handler for bundle files to connect repositories via sneakernet.\n@@ -457,6 +458,8 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \n \t\t/* initialize stack using timestamp heuristic. */\n \t\thashmap_for_each_entry(&toc, &iter, info, ent) {\n+\t\t\tchar *old_uri;\n+\n \t\t\t/* Skip if filter does not match. */\n \t\t\tif (!filter && info->filter_str)\n \t\t\t\tcontinue;\n@@ -464,6 +467,10 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t    (!info->filter_str || strcasecmp(filter, info->filter_str)))\n \t\t\t\tcontinue;\n \n+\t\t\told_uri = info->uri;\n+\t\t\tinfo->uri = relative_url(bundle_uri, info->uri, NULL);\n+\t\t\tfree(old_uri);\n+\n \t\t\t/*\n \t\t\t * Now that the filter matches, start with the\n \t\t\t * bundle with largest timestamp.\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453877","messageId":"RFC-patch-v2-22.36-1350c19c3a1-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 22/36] bundle: make it easy to call 'git bundle fetch'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:39Z","receivedAt":"2022-04-18T17:25:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nFuture changes will integrate 'git bundle fetch' into the 'git clone'\nand 'git fetch' operations. Make it easy to fetch bundles via a helper\nmethod.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n bundle.c | 21 +++++++++++++++++++++\n bundle.h |  9 +++++++++\n 2 files changed, 30 insertions(+)\n\ndiff --git a/bundle.c b/bundle.c\nindex 5fa41a52f11..7e88f5bc942 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -639,3 +639,24 @@ int unbundle(struct repository *r, struct bundle_header *header,\n \t\treturn error(_(\"index-pack died\"));\n \treturn 0;\n }\n+\n+int fetch_bundle_uri(const char *bundle_uri,\n+\t\t     const char *filter)\n+{\n+\tint res = 0;\n+\tstruct strvec args = STRVEC_INIT;\n+\n+\tstrvec_pushl(&args, \"bundle\", \"fetch\", NULL);\n+\n+\tif (filter)\n+\t\tstrvec_pushf(&args, \"--filter=%s\", filter);\n+\tstrvec_push(&args, bundle_uri);\n+\n+\tif (run_command_v_opt(args.v, RUN_GIT_CMD)) {\n+\t\twarning(_(\"failed to download bundle from uri '%s'\"), bundle_uri);\n+\t\tres = 1;\n+\t}\n+\n+\tstrvec_clear(&args);\n+\treturn res;\n+}\ndiff --git a/bundle.h b/bundle.h\nindex 0c052f54964..c647dec7c93 100644\n--- a/bundle.h\n+++ b/bundle.h\n@@ -46,4 +46,13 @@ int unbundle(struct repository *r, struct bundle_header *header,\n int list_bundle_refs(struct bundle_header *header,\n \t\tint argc, const char **argv);\n \n+struct list_objects_filter_options;\n+/**\n+ * Fetch bundles from the given URI with the given filter.\n+ *\n+ * Uses 'git bundle fetch' as a subprocess.\n+ */\n+int fetch_bundle_uri(const char *bundle_uri,\n+\t\t     const char *filter);\n+\n #endif\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453878","messageId":"RFC-patch-v2-23.36-62623324d2f-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 23/36] clone: add --bundle-uri option","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:40Z","receivedAt":"2022-04-18T17:25:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nCloning a remote repository is one of the most expensive operations in\nGit. The server can spend a lot of CPU time generating a pack-file for\nthe client's request. The amount of data can clog the network for a long\ntime, and the Git protocol is not resumable. For users with poor network\nconnections or are located far away from the origin server, this can be\nespecially painful.\n\nThe 'git bundle fetch' command allows users to bootstrap a repository\nusing a set of bundles. However, this would require them to use 'git\ninit' first, followed by the 'git bundle fetch', and finally add a\nremote, fetch, and checkout the branch they want.\n\nInstead, integrate this workflow directly into 'git clone' with the\n--bundle-uri' option. If the user is aware of a bundle server, then they\ncan tell Git to bootstrap the new repository with these bundles before\nfetching the remaining objects from the origin server.\n\nRFC-TODO: Document this option in git-clone.txt.\n\nRFC-TODO: I added a comment about the location of this code being\nnecessary for the later step of auto-discovering the bundle URI from the\norigin server. This is probably not actually a requirement, but rather a\npain point around how I implemented the feature. If a --bundle-uri\noption is specified, but SSH is used for the clone, then the SSH\nconnection is left open while Git downloads bundles from another server.\nThis is sub-optimal and should be reconsidered when fully reviewed.\n\nRFC-TODO: create tests for this option with a variety of URI types.\n\nRFC-TODO: a simple end-to-end test is available at the end of the\nseries.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/clone.c | 32 ++++++++++++++++++++++++++++++++\n 1 file changed, 32 insertions(+)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex e11f4019b87..51141c979fa 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -78,6 +78,7 @@ static int option_filter_submodules = -1;    /* unspecified */\n static int config_filter_submodules = -1;    /* unspecified */\n static struct string_list server_options = STRING_LIST_INIT_NODUP;\n static int option_remote_submodules;\n+static const char *bundle_uri;\n \n static int recurse_submodules_cb(const struct option *opt,\n \t\t\t\t const char *arg, int unset)\n@@ -161,6 +162,8 @@ static struct option builtin_clone_options[] = {\n \t\t    N_(\"any cloned submodules will use their remote-tracking branch\")),\n \tOPT_BOOL(0, \"sparse\", &option_sparse_checkout,\n \t\t    N_(\"initialize sparse-checkout file to include only files at root\")),\n+\tOPT_STRING(0, \"bundle-uri\", &bundle_uri,\n+\t\t   N_(\"uri\"), N_(\"a URI for downloading bundles before fetching from origin remote\")),\n \tOPT_END()\n };\n \n@@ -1233,6 +1236,35 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n+\t/*\n+\t * NOTE: The bundle URI download takes place after transport_get_remote_refs()\n+\t * because a later change will introduce a check for recommended features,\n+\t * which might include a recommended bundle URI.\n+\t */\n+\n+\t/*\n+\t * Before fetching from the remote, download and install bundle\n+\t * data from the --bundle-uri option.\n+\t */\n+\tif (bundle_uri) {\n+\t\tconst char *filter = NULL;\n+\n+\t\tif (filter_options.filter_spec.nr)\n+\t\t\tfilter = expand_list_objects_filter_spec(&filter_options);\n+\t\t/*\n+\t\t * Set the config for fetching from this bundle URI in the\n+\t\t * future, but do it before fetch_bundle_uri() which might\n+\t\t * un-set it (for instance, if there is no table of contents).\n+\t\t */\n+\t\tgit_config_set(\"fetch.bundleuri\", bundle_uri);\n+\t\tif (filter)\n+\t\t\tgit_config_set(\"fetch.bundlefilter\", filter);\n+\n+\t\tif (!fetch_bundle_uri(bundle_uri, filter))\n+\t\t\twarning(_(\"failed to fetch objects from bundle URI '%s'\"),\n+\t\t\t\tbundle_uri);\n+\t}\n+\n \tif (refs)\n \t\tmapped_refs = wanted_peer_refs(refs, &remote->fetch);\n \n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453879","messageId":"RFC-patch-v2-26.36-0a238641247-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 26/36] fetch: fetch bundles before fetching original data","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:43Z","receivedAt":"2022-04-18T17:26:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nIf a user cloned using a bundle URI, then they might want to re-use it\nto download new bundles during 'git fetch' before fetching the remaining\nobjects from the origin server. Use the 'fetch.bundleURI' config as the\nindicator for whether this extra step should happen.\n\nDo not fetch bundles if --dry-run is specified.\n\nRFC-TODO: add tests.\n\nRFC-TODO: update Documentation/git-fetch.txt\n\nRFC-TODO: update Documentation/config/\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/fetch.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e3791f09ed5..ac684bdf252 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -29,6 +29,7 @@\n #include \"commit-graph.h\"\n #include \"shallow.h\"\n #include \"worktree.h\"\n+#include \"bundle.h\"\n \n #define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000)\n \n@@ -2180,6 +2181,22 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \t/* FETCH_HEAD never gets updated in --dry-run mode */\n \tif (dry_run)\n \t\twrite_fetch_head = 0;\n+\telse {\n+\t\t/*\n+\t\t * --dry-run mode skips bundle downloads, which might\n+\t\t * update some refs.\n+\t\t */\n+\t\tchar *bundle_uri = NULL;\n+\t\tgit_config_get_string(\"fetch.bundleuri\", &bundle_uri);\n+\n+\t\tif (bundle_uri) {\n+\t\t\tchar *filter = NULL;\n+\t\t\tgit_config_get_string(\"fetch.bundlefilter\", &filter);\n+\t\t\tfetch_bundle_uri(bundle_uri, filter);\n+\t\t\tfree(bundle_uri);\n+\t\t\tfree(filter);\n+\t\t}\n+\t}\n \n \tif (all) {\n \t\tif (argc == 1)\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453880","messageId":"RFC-patch-v2-25.36-ab349b1cbea-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 25/36] bundle: only fetch bundles if timestamp is new","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:42Z","receivedAt":"2022-04-18T17:26:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nIf a bundle server is providing a table of contents with timestamps for\nthe bundles, then we can store the most-recent timestamp and use that as\na test if the bundle server has any new information. Teach 'git bundle\nfetch' to store the timestamp in the config file as\n'fetch.bundleTimestamp' and compare the existing value to the\nmost-recent timestamp in the bundle server's table of contents. If the\nnew timestamp is at most the stored timestamp, then exit early (with\nsuccess). If the new timestamp is greater than the stored timestamp,\nthen continue with the normal fetch logic of downloading the most-recent\nbundle until all missing objects are satisfied. Store that new timestamp\nin the config for next time.\n\nRFC-TODO: Update documentation of 'git bundle fetch' to match his new\nbehavior.\n\nRFC-TODO: Add 'fetch.bundleTimestamp' to Documentation/config/\n\nRFC-TODO @ Ævar: I replaced the git_config_get_timestamp() with\nparse_expiry_date(), but as noted perhaps we want *nix epochs here\nonly, in that case we could add an \"isdigit\" loop here.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/bundle.c | 31 ++++++++++++++++++++++++++++++-\n 1 file changed, 30 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/bundle.c b/builtin/bundle.c\nindex c55d5215181..4c51b014f0b 100644\n--- a/builtin/bundle.c\n+++ b/builtin/bundle.c\n@@ -413,6 +413,10 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \tstruct remote_bundle_info *stack = NULL;\n \tstruct hashmap toc = { 0 };\n \tconst char *filter = NULL;\n+\tconst char *timestamp_key = \"fetch.bundletimestamp\";\n+\ttimestamp_t stored_time = 0;\n+\ttimestamp_t max_time = 0;\n+\tconst char *value;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"progress\", &progress,\n@@ -428,6 +432,17 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \tif (!startup_info->have_repository)\n \t\tdie(_(\"'fetch' requires a repository\"));\n \n+\t/*\n+\t * TODO: Is it important re\n+\t * https://lore.kernel.org/git/220311.86pmmshahy.gmgdl@evledraar.gmail.com/\n+\t * that we don't accept \"2.days.ago\" etc., and only *nix\n+\t * epochs?\n+\t */\n+\tif (!git_config_get_string_tmp(timestamp_key, &value) &&\n+\t    parse_expiry_date(value, &stored_time))\n+\t\treturn error(_(\"'%s' for '%s' is not a valid timestamp\"),\n+\t\t\t     value, timestamp_key);\n+\n \t/*\n \t * Step 1: determine protocol for uri, and download contents to\n \t * a temporary location.\n@@ -449,7 +464,6 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t} else {\n \t\tstruct hashmap_iter iter;\n \t\tstruct remote_bundle_info *info;\n-\t\ttimestamp_t max_time = 0;\n \n \t\t/* populate a hashtable with all relevant bundles. */\n \t\tused_hashmap = 1;\n@@ -480,6 +494,13 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t\t\t\tmax_time = info->timestamp;\n \t\t\t}\n \t\t}\n+\n+\t\ttrace2_data_intmax(\"bundle\", the_repository, \"max_time\", max_time);\n+\t\ttrace2_data_intmax(\"bundle\", the_repository, \"stored_time\", stored_time);\n+\n+\t\t/* Skip fetching bundles if data isn't new enough. */\n+\t\tif (max_time <= stored_time)\n+\t\t\tgoto cleanup;\n \t}\n \n \t/*\n@@ -567,6 +588,14 @@ static int cmd_bundle_fetch(int argc, const char **argv, const char *prefix)\n \t\tstack = stack->stack_next;\n \t}\n \n+\tif (max_time) {\n+\t\tstruct strbuf tstr = STRBUF_INIT;\n+\t\tstrbuf_addf(&tstr, \"%\"PRIuMAX\"\", max_time);\n+\t\tgit_config_set_gently(timestamp_key, tstr.buf);\n+\t\tstrbuf_release(&tstr);\n+\t}\n+\n+cleanup:\n \tif (used_hashmap) {\n \t\tstruct hashmap_iter iter;\n \t\tstruct remote_bundle_info *info;\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453881","messageId":"RFC-patch-v2-24.36-d79901dddb0-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 24/36] clone: --bundle-uri cannot be combined with --depth","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:41Z","receivedAt":"2022-04-18T17:26:08Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThe previous change added the '--bundle-uri' option, but did not check\nif the --depth parameter was included. Since bundles are not compatible\nwith shallow clones, provide an error message to the user who is\nattempting this combination.\n\nI am leaving this as its own change, separate from the one that\nimplements '--bundle-uri', because this is more of an advisory for the\nuser. There is nothing wrong with bootstrapping with bundles and then\nfetching a shallow clone. However, that is likely going to involve too\nmuch work for the client _and_ the server. The client will download all\nof this bundle information containing the full history of the\nrepository only to ignore most of it. The server will get a shallow\nfetch request, but with a list of haves that might cause a more painful\ncomputation of that shallow pack-file.\n\nRFC-TODO: add a test case for this error message.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/clone.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 51141c979fa..af64bd273b7 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -926,6 +926,11 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \t\toption_no_checkout = 1;\n \t}\n \n+\tif (bundle_uri) {\n+\t\tif (deepen)\n+\t\t\tdie(_(\"--bundle-uri is incompatible with --depth, --shallow-since, and --shallow-exclude\"));\n+\t}\n+\n \trepo_name = argv[0];\n \n \tpath = get_repo_path(repo_name, &is_bundle);\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453882","messageId":"RFC-patch-v2-28.36-145c660ca52-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 28/36] serve: understand but do not advertise 'features' capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:45Z","receivedAt":"2022-04-18T17:26:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThe previous change implemented cap_features() to return a set of\n'key=value' pairs when this capability is run. Add the capability to our\nlist of understood capabilities.\n\nThis change does not advertise the capability. When deploying a new\ncapability to a distributed fleet of Git servers, it is important to\ndelay advertising the capability until all nodes understand it. A later\nchange will advertise it when appropriate, but as a separate change to\nsimplify this transition.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n serve.c | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/serve.c b/serve.c\nindex f3e0203d2c6..3368d16efda 100644\n--- a/serve.c\n+++ b/serve.c\n@@ -19,6 +19,12 @@ static int always_advertise(struct repository *r,\n \treturn 1;\n }\n \n+static int never_advertise(struct repository *r,\n+\t\t\t   struct strbuf *value)\n+{\n+\treturn 0;\n+}\n+\n static int agent_advertise(struct repository *r,\n \t\t\t   struct strbuf *value)\n {\n@@ -113,6 +119,11 @@ static struct protocol_capability capabilities[] = {\n \t\t.advertise = ls_refs_advertise,\n \t\t.command = ls_refs,\n \t},\n+\t{\n+\t\t.name = \"features\",\n+\t\t.advertise = never_advertise,\n+\t\t.command = cap_features,\n+\t},\n \t{\n \t\t.name = \"fetch\",\n \t\t.advertise = upload_pack_advertise,\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453883","messageId":"RFC-patch-v2-27.36-5e8cec1e193-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 27/36] protocol-caps: implement cap_features()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:44Z","receivedAt":"2022-04-18T17:26:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThe 'features' capability sends a list of \"key=value\" pairs from the\nserver. These are a set of fixed config values, all prefixed with\n\"serve.\" to avoid conflicting with other config values of similar names.\n\nThe initial set chosen here are:\n\n* bundleURI: Allow advertising one or more bundle servers by URI.\n\n* partialCloneFilter: Advertise one or more recommended partial clone\n  filters.\n\n* sparseCheckout: Advertise that this repository recommends using the\n  sparse-checkout feature in cone mode.\n\nThe client will have the choice to enable these features.\n\nRFC-TODO: Create Documentation/config/serve.txt\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n protocol-caps.c | 66 +++++++++++++++++++++++++++++++++++++++++++++++++\n protocol-caps.h |  1 +\n 2 files changed, 67 insertions(+)\n\ndiff --git a/protocol-caps.c b/protocol-caps.c\nindex bbde91810ac..88b01c4133e 100644\n--- a/protocol-caps.c\n+++ b/protocol-caps.c\n@@ -8,6 +8,7 @@\n #include \"object-store.h\"\n #include \"string-list.h\"\n #include \"strbuf.h\"\n+#include \"config.h\"\n \n struct requested_info {\n \tunsigned size : 1;\n@@ -111,3 +112,68 @@ int cap_object_info(struct repository *r, struct packet_reader *request)\n \n \treturn 0;\n }\n+\n+static void send_lines(struct repository *r, struct packet_writer *writer,\n+\t\t       struct string_list *str_list)\n+{\n+\tstruct string_list_item *item;\n+\n+\tif (!str_list->nr)\n+\t\treturn;\n+\n+\tfor_each_string_list_item (item, str_list) {\n+\t\tpacket_writer_write(writer, \"%s\", item->string);\n+\t}\n+}\n+\n+int cap_features(struct repository *r, struct packet_reader *request)\n+{\n+\tstruct packet_writer writer;\n+\tstruct string_list feature_list = STRING_LIST_INIT_DUP;\n+\tint i = 0;\n+\tconst char *keys[] = {\n+\t\t\"bundleuri\",\n+\t\t\"partialclonefilter\",\n+\t\t\"sparsecheckout\",\n+\t\tNULL\n+\t};\n+\tstruct strbuf serve_feature = STRBUF_INIT;\n+\tstruct strbuf key_equals_value = STRBUF_INIT;\n+\tsize_t len;\n+\tstrbuf_add(&serve_feature, \"serve.\", 6);\n+\tlen = serve_feature.len;\n+\n+\tpacket_writer_init(&writer, 1);\n+\n+\twhile (keys[i]) {\n+\t\tstruct string_list_item *item;\n+\t\tconst struct string_list *values = NULL;\n+\t\tstrbuf_setlen(&serve_feature, len);\n+\t\tstrbuf_addstr(&serve_feature, keys[i]);\n+\n+\t\tvalues = repo_config_get_value_multi(r, serve_feature.buf);\n+\n+\t\tif (values) {\n+\t\t\tfor_each_string_list_item(item, values) {\n+\t\t\t\tstrbuf_reset(&key_equals_value);\n+\t\t\t\tstrbuf_addstr(&key_equals_value, keys[i]);\n+\t\t\t\tstrbuf_addch(&key_equals_value, '=');\n+\t\t\t\tstrbuf_addstr(&key_equals_value, item->string);\n+\n+\t\t\t\tstring_list_append(&feature_list, key_equals_value.buf);\n+\t\t\t}\n+\t\t}\n+\n+\t\ti++;\n+\t}\n+\tstrbuf_release(&serve_feature);\n+\tstrbuf_release(&key_equals_value);\n+\n+\tsend_lines(r, &writer, &feature_list);\n+\n+\tstring_list_clear(&feature_list, 1);\n+\n+\tpacket_flush(1);\n+\n+\treturn 0;\n+}\ndiff --git a/protocol-caps.h b/protocol-caps.h\nindex 15c4550360c..681d2106d88 100644\n--- a/protocol-caps.h\n+++ b/protocol-caps.h\n@@ -4,5 +4,6 @@\n struct repository;\n struct packet_reader;\n int cap_object_info(struct repository *r, struct packet_reader *request);\n+int cap_features(struct repository *r, struct packet_reader *request);\n \n #endif /* PROTOCOL_CAPS_H */\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453884","messageId":"RFC-patch-v2-30.36-e834e633e84-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 30/36] connect: implement get_recommended_features()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:47Z","receivedAt":"2022-04-18T17:26:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThis method allows a client to request and parse the 'features' capability\nof protocol v2. The response is expected to be a list of 'key=value'\nlines, but this implementation does no checking of the lines, expecting\na later parse of the lines to be careful of the existence of that '='\ncharacter.\n\nThis change is based on an earlier patch [1] written for a similar\ncapability.\n\n[1] https://lore.kernel.org/git/RFC-patch-04.13-21caf01775-20210805T150534Z-avarab@gmail.com/\n\nCo-authored-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n connect.c | 36 ++++++++++++++++++++++++++++++++++++\n remote.h  |  4 ++++\n 2 files changed, 40 insertions(+)\n\ndiff --git a/connect.c b/connect.c\nindex a8fdb5255f7..1739d1f8a5f 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -591,6 +591,42 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,\n \treturn list;\n }\n \n+int get_recommended_features(int fd_out, struct packet_reader *reader,\n+\t\t\t     struct string_list *list, int stateless_rpc)\n+{\n+\tint line_nr = 1;\n+\n+\tserver_supports_v2(\"features\", 1);\n+\n+\t/* (Re-)send capabilities */\n+\tsend_capabilities(fd_out, reader);\n+\n+\t/* Send command */\n+\tpacket_write_fmt(fd_out, \"command=features\\n\");\n+\tpacket_delim(fd_out);\n+\tpacket_flush(fd_out);\n+\n+\t/* Process response from server */\n+\twhile (packet_reader_read(reader) == PACKET_READ_NORMAL) {\n+\t\tconst char *line = reader->line;\n+\t\tline_nr++;\n+\n+\t\tstring_list_append(list, line);\n+\t}\n+\n+\tif (reader->status != PACKET_READ_FLUSH)\n+\t\treturn error(_(\"expected flush after features listing\"));\n+\n+\t/*\n+\t * Might die(), but obscure enough that that's OK, e.g. in\n+\t * serve.c, we'll call BUG() on its equivalent (the\n+\t * PACKET_READ_RESPONSE_END check).\n+\t */\n+\tcheck_stateless_delimiter(stateless_rpc, reader,\n+\t\t_(\"expected response end packet after features listing\"));\n+\treturn 0;\n+}\n+\n const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp, int *offset)\n {\n \tint len;\ndiff --git a/remote.h b/remote.h\nindex 571338510a8..bccb8484dbd 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -242,6 +242,10 @@ int get_remote_bundle_uri(int fd_out, struct packet_reader *reader,\n \n int resolve_remote_symref(struct ref *ref, struct ref *list);\n \n+/* Used for protocol v2 in order to retrieve recommended features */\n+int get_recommended_features(int fd_out, struct packet_reader *reader,\n+\t\t\t     struct string_list *list, int stateless_rpc);\n+\n /*\n  * Remove and free all but the first of any entries in the input list\n  * that map the same remote reference to the same local reference.  If\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453885","messageId":"RFC-patch-v2-29.36-2c9886c64ea-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 29/36] serve: advertise 'features' when config exists","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:46Z","receivedAt":"2022-04-18T17:26:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThe 'features' capability allows a server to recommend some Git features\nat a high level. Previous changes implemented the capability so servers\nunderstand it, but it was never advertised.\n\nNow, allow it to be advertised, but only when the capability will\nactually _do_ something. That is, advertise if and only if a config\nvalue exists with the prefix \"serve.\". This avoids unnecessary round\ntrips for an empty result.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n serve.c              | 18 +++++++++++++++---\n t/t5701-git-serve.sh |  9 +++++++++\n 2 files changed, 24 insertions(+), 3 deletions(-)\n\ndiff --git a/serve.c b/serve.c\nindex 3368d16efda..6237bf63d60 100644\n--- a/serve.c\n+++ b/serve.c\n@@ -19,12 +19,24 @@ static int always_advertise(struct repository *r,\n \treturn 1;\n }\n \n-static int never_advertise(struct repository *r,\n-\t\t\t   struct strbuf *value)\n+static int key_serve_prefix(const char *key, const char *value, void *data)\n {\n+\tint *signal = data;\n+\tif (!strncmp(key, \"serve.\", 6)) {\n+\t\t*signal = 1;\n+\t\treturn 1;\n+\t}\n \treturn 0;\n }\n \n+static int has_serve_config(struct repository *r,\n+\t\t\t    struct strbuf *value)\n+{\n+\tint signal = 0;\n+\trepo_config(r, key_serve_prefix, &signal);\n+\treturn signal;\n+}\n+\n static int agent_advertise(struct repository *r,\n \t\t\t   struct strbuf *value)\n {\n@@ -121,7 +133,7 @@ static struct protocol_capability capabilities[] = {\n \t},\n \t{\n \t\t.name = \"features\",\n-\t\t.advertise = never_advertise,\n+\t\t.advertise = has_serve_config,\n \t\t.command = cap_features,\n \t},\n \t{\ndiff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh\nindex 9d053f77a93..befc800593e 100755\n--- a/t/t5701-git-serve.sh\n+++ b/t/t5701-git-serve.sh\n@@ -33,6 +33,15 @@ test_expect_success 'test capability advertisement' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'test capability advertisement' '\n+\ttest_when_finished git config --unset serve.bundleuri &&\n+\tgit config serve.bundleuri \"file://$(pwd)\" &&\n+\tGIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \\\n+\t\t--advertise-capabilities >out &&\n+\ttest-tool pkt-line unpack <out >actual &&\n+\tgrep features actual\n+'\n+\n test_expect_success 'stateless-rpc flag does not list capabilities' '\n \t# Empty request\n \ttest-tool pkt-line pack >in <<-EOF &&\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453886","messageId":"RFC-patch-v2-33.36-52ee1e08dec-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 33/36] t5601: basic bundle URI test","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:50Z","receivedAt":"2022-04-18T17:26:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nThis test demonstrates an end-to-end form of the bundle URI feature\ngiven by an HTTP server advertising the 'features' capability with a\nbundle URI that is a bundle file on that same HTTP server. We verify\nthat we unbundled a bundle, which could only have happened if we\nsuccessfully downloaded that file.\n\nRFC-TODO: Create similar tests throughout the series that perform\nsimilar tests, including examples with table of contents and partial\nclones.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n t/t5601-clone.sh | 15 +++++++++++++++\n 1 file changed, 15 insertions(+)\n\ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex 4a61f2c901e..e6119f78aea 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -767,6 +767,21 @@ test_expect_success 'reject cloning shallow repository using HTTP' '\n \tgit clone --no-reject-shallow $HTTPD_URL/smart/repo.git repo\n '\n \n+test_expect_success 'auto-discover bundle URI from HTTP clone' '\n+\ttest_when_finished rm -rf repo \"$HTTPD_DOCUMENT_ROOT_PATH/repo2.git\" &&\n+\tgit -C src bundle create \"$HTTPD_DOCUMENT_ROOT_PATH/everything.bundle\" --all &&\n+\tgit clone --bare --no-local src \"$HTTPD_DOCUMENT_ROOT_PATH/repo2.git\" &&\n+\tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/repo2.git\" config \\\n+\t\tserve.bundleuri $HTTPD_URL/everything.bundle &&\n+\tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n+\t\tgit -c protocol.version=2 clone \\\n+\t\t$HTTPD_URL/smart/repo2.git repo &&\n+\tcat >pat <<-\\EOF &&\n+\t\"event\":\"child_start\".*\"argv\":\\[\"git\",\"bundle\",\"unbundle\"\n+\tEOF\n+\tgrep -f pat trace.txt\n+'\n+\n # DO NOT add non-httpd-specific tests here, because the last part of this\n # test script is only executed when httpd is available and enabled.\n \n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453887","messageId":"RFC-patch-v2-32.36-2b424bedfc5-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 32/36] clone: use server-recommended bundle URI","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:49Z","receivedAt":"2022-04-18T17:26:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nAfter the ref advertisement initializes the connection between the\nclient and the remote, use the 'features' capability (if available) to\nget a list of recommended features from the server.\n\nIn this change, we only update the bundle URI setting. The bundles are\ndownloaded immediately afterwards if the bundle URI becomes non-null.\n\nRFC-TODO: don't overwrite a given --bundle-uri option.\nRFC-TODO: implement the other capabilities.\nRFC-TODO: guard this entire request behind opt-in config.\nRFC-TODO: prevent using an HTTP(S) URI when in an SSH clone.\nRFC-TODO: prevent using a local path for the bundle URI.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n builtin/clone.c | 25 +++++++++++++++++++------\n 1 file changed, 19 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex af64bd273b7..81c14a9f5d7 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -890,6 +890,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tint err = 0, complete_refs_before_fetch = 1;\n \tint submodule_progress;\n \tint filter_submodules = 0;\n+\tstruct string_list *feature_list = NULL;\n \n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n@@ -1241,11 +1242,23 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \n \trefs = transport_get_remote_refs(transport, &transport_ls_refs_options);\n \n-\t/*\n-\t * NOTE: The bundle URI download takes place after transport_get_remote_refs()\n-\t * because a later change will introduce a check for recommended features,\n-\t * which might include a recommended bundle URI.\n-\t */\n+\tfeature_list = transport_remote_features(transport);\n+\n+\tif (feature_list) {\n+\t\tstruct string_list_item *item;\n+\t\tfor_each_string_list_item(item, feature_list) {\n+\t\t\tchar *value;\n+\t\t\tchar *equals = strchr(item->string, '=');\n+\n+\t\t\tif (!equals)\n+\t\t\t\tcontinue;\n+\t\t\t*equals = '\\0';\n+\t\t\tvalue = equals + 1;\n+\n+\t\t\tif (!strcmp(item->string, \"bundleuri\"))\n+\t\t\t\tbundle_uri = value;\n+\t\t}\n+\t}\n \n \t/*\n \t * Before fetching from the remote, download and install bundle\n@@ -1265,7 +1278,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \t\tif (filter)\n \t\t\tgit_config_set(\"fetch.bundlefilter\", filter);\n \n-\t\tif (!fetch_bundle_uri(bundle_uri, filter))\n+\t\tif (fetch_bundle_uri(bundle_uri, filter))\n \t\t\twarning(_(\"failed to fetch objects from bundle URI '%s'\"),\n \t\t\t\tbundle_uri);\n \t}\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453888","messageId":"RFC-patch-v2-31.36-6611dd08f93-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 31/36] transport: add connections for 'features' capability","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:48Z","receivedAt":"2022-04-18T17:26:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nTo allow 'git clone' to check the 'features' capability, we need to fill\nin some boilerplate methods that help detect if the capability exists\nand then to execute the get_recommended_features() method with the\nproper context. This involves jumping through some vtables.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\n---\n transport-helper.c   | 13 +++++++++++++\n transport-internal.h |  9 +++++++++\n transport.c          | 38 ++++++++++++++++++++++++++++++++++++++\n transport.h          |  5 +++++\n 4 files changed, 65 insertions(+)\n\ndiff --git a/transport-helper.c b/transport-helper.c\nindex 398712c76f3..782aa1f43a2 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -1160,6 +1160,18 @@ static int push_refs(struct transport *transport,\n \treturn -1;\n }\n \n+static int get_features(struct transport *transport,\n+\t\t      struct string_list *list)\n+{\n+\tget_helper(transport);\n+\n+\tif (process_connect(transport, 0)) {\n+\t\tdo_take_over(transport);\n+\t\treturn transport->vtable->get_features(transport, list);\n+\t}\n+\n+\treturn -1;\n+}\n \n static int has_attribute(const char *attrs, const char *attr)\n {\n@@ -1285,6 +1297,7 @@ static struct transport_vtable vtable = {\n \t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs,\n \t.push_refs\t= push_refs,\n+\t.get_features\t= get_features,\n \t.connect\t= connect_helper,\n \t.disconnect\t= release_helper\n };\ndiff --git a/transport-internal.h b/transport-internal.h\nindex 90ea749e5cf..969cb30f510 100644\n--- a/transport-internal.h\n+++ b/transport-internal.h\n@@ -5,6 +5,7 @@ struct ref;\n struct transport;\n struct strvec;\n struct transport_ls_refs_options;\n+struct string_list;\n \n struct transport_vtable {\n \t/**\n@@ -58,6 +59,14 @@ struct transport_vtable {\n \t * process involved generating new commits.\n \t **/\n \tint (*push_refs)(struct transport *transport, struct ref *refs, int flags);\n+\n+\t/**\n+\t * get_features() requests a list of recommended features and\n+\t * populates the given string_list with those 'key=value' pairs.\n+\t */\n+\tint (*get_features)(struct transport *transport,\n+\t\t\t    struct string_list *list);\n+\n \tint (*connect)(struct transport *connection, const char *name,\n \t\t       const char *executable, int fd[2]);\n \ndiff --git a/transport.c b/transport.c\nindex 7e5e1192f95..42a726dc066 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -205,6 +205,20 @@ struct git_transport_data {\n \tstruct oid_array shallow;\n };\n \n+static int get_features(struct transport *transport,\n+\t\t      struct string_list *list)\n+{\n+\tstruct git_transport_data *data = transport->data;\n+\tstruct packet_reader reader;\n+\n+\tpacket_reader_init(&reader, data->fd[0], NULL, 0,\n+\t\t\t   PACKET_READ_CHOMP_NEWLINE |\n+\t\t\t   PACKET_READ_GENTLE_ON_EOF);\n+\n+\treturn get_recommended_features(data->fd[1], &reader, list,\n+\t\t\t\t\ttransport->stateless_rpc);\n+}\n+\n static int set_git_option(struct git_transport_options *opts,\n \t\t\t  const char *name, const char *value)\n {\n@@ -948,6 +962,7 @@ static struct transport_vtable taken_over_vtable = {\n \t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs_via_pack,\n \t.push_refs\t= git_transport_push,\n+\t.get_features\t= get_features,\n \t.disconnect\t= disconnect_git\n };\n \n@@ -1102,6 +1117,7 @@ static struct transport_vtable builtin_smart_vtable = {\n \t.get_bundle_uri = get_bundle_uri,\n \t.fetch_refs\t= fetch_refs_via_pack,\n \t.push_refs\t= git_transport_push,\n+\t.get_features\t= get_features,\n \t.connect\t= connect_git,\n \t.disconnect\t= disconnect_git\n };\n@@ -1606,6 +1622,28 @@ void transport_unlock_pack(struct transport *transport, unsigned int flags)\n \t\tstring_list_clear(&transport->pack_lockfiles, 0);\n }\n \n+struct string_list *transport_remote_features(struct transport *transport)\n+{\n+\tconst struct transport_vtable *vtable = transport->vtable;\n+\tstruct string_list *list = NULL;\n+\n+\tif (!server_supports_v2(\"features\", 0))\n+\t\treturn NULL;\n+\n+\tif (!vtable->get_features) {\n+\t\twarning(_(\"'features' not supported by this remote\"));\n+\t\treturn NULL;\n+\t}\n+\n+\tCALLOC_ARRAY(list, 1);\n+\tstring_list_init_dup(list);\n+\n+\tif (vtable->get_features(transport, list))\n+\t\twarning(_(\"failed to get recommended features from remote\"));\n+\n+\treturn list;\n+}\n+\n int transport_connect(struct transport *transport, const char *name,\n \t\t      const char *exec, int fd[2])\n {\ndiff --git a/transport.h b/transport.h\nindex ed5ebcf1466..7afc02eb683 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -322,6 +322,11 @@ int transport_fetch_refs(struct transport *transport, struct ref *refs);\n  */\n void transport_unlock_pack(struct transport *transport, unsigned int flags);\n \n+/**\n+ * Get recommended config from remote.\n+ */\n+struct string_list *transport_remote_features(struct transport *transport);\n+\n int transport_disconnect(struct transport *transport);\n char *transport_anonymize_url(const char *url);\n void transport_take_over(struct transport *transport,\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453889","messageId":"RFC-patch-v2-34.36-f872793cac2-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 34/36] protocol v2: add server-side \"bundle-uri\" skeleton (docs)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:51Z","receivedAt":"2022-04-18T17:26:25Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/technical/protocol-v2.txt | 209 ++++++++++++++++++++++++\n 1 file changed, 209 insertions(+)\n\ndiff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\nindex 8a877d27e23..3ea96add398 100644\n--- a/Documentation/technical/protocol-v2.txt\n+++ b/Documentation/technical/protocol-v2.txt\n@@ -566,3 +566,212 @@ and associated requested information, each separated by a single space.\n \tattr = \"size\"\n \n \tobj-info = obj-id SP obj-size\n+\n+bundle-uri\n+~~~~~~~~~~\n+\n+If the 'bundle-uri' capability is advertised, the server supports the\n+`bundle-uri' command.\n+\n+The capability is currently advertised with no value (i.e. not\n+\"bundle-uri=somevalue\"), a value may be added in the future for\n+supporting command-wide extensions. Clients MUST ignore any unknown\n+capability values and proceed with the 'bundle-uri` dialog they\n+support.\n+\n+The 'bundle-uri' command is intended to be issued before `fetch` to\n+get URIs to bundle files (see linkgit:git-bundle[1]) to \"seed\" and\n+inform the subsequent `fetch` command.\n+\n+The client CAN issue `bundle-uri` before or after any other valid\n+command. To be useful to clients it's expected that it'll be issued\n+after an `ls-refs` and before `fetch`, but CAN be issued at any time\n+in the dialog.\n+\n+DISCUSSION of bundle-uri\n+^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+The intent of the feature is optimize for server resource consumption\n+in the common case by changing the common case of fetching a very\n+large PACK during linkgit:git-clone[1] into a smaller incremental\n+fetch.\n+\n+It also allows servers to achieve better caching in combination with\n+an `uploadpack.packObjectsHook` (see linkgit:git-config[1]).\n+\n+By having new clones or fetches be a more predictable and common\n+negotiation against the tips of recently produces *.bundle file(s).\n+Servers might even pre-generate the results of such negotiations for\n+the `uploadpack.packObjectsHook` as new pushes come in.\n+\n+I.e. the server would anticipate that fresh clones will download a\n+known bundle, followed by catching up to the current state of the\n+repository using ref tips found in that bundle (or bundles).\n+\n+PROTOCOL for bundle-uri\n+^^^^^^^^^^^^^^^^^^^^^^^\n+\n+A `bundle-uri` request takes no arguments, and as noted above does not\n+currently advertise a capability value. Both may be added in the\n+future.\n+\n+When the client issues a `command=bundle-uri` the response is a list\n+of URIs the server would like the client to fetch out-of-bounds before\n+proceeding with the `fetch` request in this format:\n+\n+\toutput = bundle-uri-line\n+\t\t bundle-uri-line* flush-pkt\n+\n+\tbundle-uri-line = PKT-LINE(bundle-uri)\n+\t\t\t  *(SP bundle-feature-key *(=bundle-feature-val))\n+\t\t\t  LF\n+\n+\tbundle-uri = A URI such as a https://, ssh:// etc. URI\n+\n+\tbundle-feature-key = Any printable ASCII characters except SP or \"=\"\n+\tbundle-feature-val = Any printable ASCII characters except SP or \"=\"\n+\n+No `bundle-feature-key`=`bundle-feature-value` fields are currently\n+defined. See the discussion of features below.\n+\n+Clients are still expected to fully parse the line according to the\n+above format, lines that do not conform to the format SHOULD be\n+discarded. The user MAY be warned in such a case.\n+\n+bundle-uri CLIENT AND SERVER EXPECTATIONS\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+\".bundle\" FORMAT\n+++++++++++++++++\n+\n+The advertised bundle(s) MUST be in a format that \"git bundle verify\"\n+would accept. I.e. they MUST contain one or more reference tips for\n+use by the client, MUST indicate prerequisites (in any) with standard\n+\"-\" prefixes, and MUST indicate their \"object-format\", if\n+applicable. Create \"*.bundle\" files with \"git bundle create\".\n+\n+bundle-uri CLIENT ERROR RECOVERY\n+++++++++++++++++++++++++++++++++\n+\n+A client MUST above all gracefully degrade on errors, whether that\n+error is because of bad missing/data in the bundle URI(s), because\n+that client is too dumb to e.g. understand and fully parse out bundle\n+headers and their prerequisite relationships, or something else.\n+\n+Server operators should feel confident in turning on \"bundle-uri\" and\n+not worry if e.g. their CDN goes down that clones or fetches will run\n+into hard failures. Even if the server bundle bundle(s) are\n+incomplete, or bad in some way the client should still end up with a\n+functioning repository, just as if it had chosen not to use this\n+protocol extension.\n+\n+All subsequent discussion on client and server interaction MUST keep\n+this in mind.\n+\n+bundle-uri SERVER TO CLIENT\n++++++++++++++++++++++++++++\n+\n+The ordering of the returned bundle uris is not significant. Clients\n+MUST parse their headers to discover their contained OIDS and\n+prerequisites. A client MUST consider the content of the bundle(s)\n+themselves and their header as the ultimate source of truth.\n+\n+A server MAY even return bundle(s) that don't have any direct\n+relationship to the repository being cloned (either through accident,\n+or intentional \"clever\" configuration), and expect a client to sort\n+out what data they'd like from the bundle(s), if any.\n+\n+bundle-uri CLIENT TO SERVER\n++++++++++++++++++++++++++++\n+\n+The client SHOULD provide reference tips found in the bundle header(s)\n+as 'have' lines in any subsequent `fetch` request. A client MAY also\n+ignore the bundle(s) entirely if doing so is deemed worse for some\n+reason, e.g. if the bundles can't be downloaded, it doesn't like the\n+tips it finds etc.\n+\n+WHEN ADVERTISED BUNDLE(S) REQUIRE NO FURTHER NEGOTIATION\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n+\n+If after issuing `bundle-uri` and `ls-refs`, and getting the header(s)\n+of the bundle(s) the client finds that the ref tips it wants can be\n+retrieved entirety from advertised bundle(s), it MAY disconnect. The\n+results of such a 'clone' or 'fetch' should be indistinguishable from\n+the state attained without using bundle-uri.\n+\n+EARLY CLIENT DISCONNECTIONS AND ERROR RECOVERY\n+++++++++++++++++++++++++++++++++++++++++++++++\n+\n+A client MAY perform an early disconnect while still downloading the\n+bundle(s) (having streamed and parsed their headers). In such a case\n+the client MUST gracefully recover from any errors related to\n+finishing the download and validation of the bundle(s).\n+\n+I.e. a client might need to re-connect and issue a 'fetch' command,\n+and possibly fall back to not making use of 'bundle-uri' at all.\n+\n+This \"MAY\" behavior is specified as such (and not a \"SHOULD\") on the\n+assumption that a server advertising bundle uris is more likely than\n+not to be serving up a relatively large repository, and to be pointing\n+to URIs that have a good chance of being in working order. A client\n+MAY e.g. look at the payload size of the bundles as a heuristic to see\n+if an early disconnect is worth it, should falling back on a full\n+\"fetch\" dialog be necessary.\n+\n+WHEN ADVERTISED BUNDLE(S) REQUIRE FURTHER NEGOTIATION\n++++++++++++++++++++++++++++++++++++++++++++++++++++++\n+\n+A client SHOULD commence a negotiation of a PACK from the server via\n+the \"fetch\" command using the OID tips found in advertised bundles,\n+even if's still in the process of downloading those bundle(s).\n+\n+This allows for aggressive early disconnects from any interactive\n+server dialog. The client blindly trusts that the advertised OID tips\n+are relevant, and issues them as 'have' lines, it then requests any\n+tips it would like (usually from the \"ls-refs\" advertisement) via\n+'want' lines. The server will then compute a (hopefully small) PACK\n+with the expected difference between the tips from the bundle(s) and\n+the data requested.\n+\n+The only connection the client then needs to keep active is to the\n+concurrently downloading static bundle(s), when those and the\n+incremental PACK are retrieved they should be inflated and\n+validated. Any errors at this point should be gracefully recovered\n+from, see above.\n+\n+bundle-uri PROTOCOL FEATURES\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+As noted above no `bundle-feature-key`=`bundle-feature-value` fields\n+are currently defined.\n+\n+They are intended for future per-URI metadata which older clients MUST\n+ignore and gracefully degrade on. Any fields they do recognize they\n+CAN also ignore.\n+\n+Any backwards-incompatible addition of pre-URI key-value will be\n+guarded by a new value or values in 'bundle-uri' capability\n+advertisement itself, and/or by new future `bundle-uri` request\n+arguments.\n+\n+While no per-URI key-value are currently supported currently they're\n+intended to support future features such as:\n+\n+ * Add a \"hash=<val>\" or \"size=<bytes>\" advertise the expected hash or\n+   size of the bundle file.\n+\n+ * Advertise that one or more bundle files are the same (to e.g. have\n+   clients round-robin or otherwise choose one of N possible files).\n+\n+ * A \"oid=<OID>\" shortcut and \"prerequisite=<OID>\" shortcut. For\n+   expressing the common case of a bundle with one tip and no\n+   prerequisites, or one tip and one prerequisite.\n++\n+This would allow for optimizing the common case of servers who'd like\n+to provide one \"big bundle\" containing only their \"main\" branch,\n+and/or incremental updates thereof.\n++\n+A client receiving such a a response MAY assume that they can skip\n+retrieving the header from a bundle at the indicated URI, and thus\n+save themselves and the server(s) the request(s) needed to inspect the\n+headers of that bundle or bundles.\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453890","messageId":"RFC-patch-v2-35.36-cfda9323aaa-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 35/36] bundle-uri docs: add design notes","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:52Z","receivedAt":"2022-04-18T17:26:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a design doc for the bundle-uri protocol extension to go along\nwith the packfile-uri extension added in cd8402e0fd8 (Documentation:\nadd Packfile URIs design doc, 2020-06-10).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/technical/bundle-uri.txt  | 119 ++++++++++++++++++++++++\n Documentation/technical/protocol-v2.txt |   5 +\n 2 files changed, 124 insertions(+)\n create mode 100644 Documentation/technical/bundle-uri.txt\n\ndiff --git a/Documentation/technical/bundle-uri.txt b/Documentation/technical/bundle-uri.txt\nnew file mode 100644\nindex 00000000000..5ae9a15eafe\n--- /dev/null\n+++ b/Documentation/technical/bundle-uri.txt\n@@ -0,0 +1,119 @@\n+Bundle URI Design Notes\n+=======================\n+\n+Protocol\n+--------\n+\n+See `bundle-uri` in the link:protocol-v2.html[protocol-v2]\n+documentation for a discussion of the bundle-uri command, and the\n+expectations of clients and servers.\n+\n+This document is a a more general discussion of how the `bundle-uri`\n+command fits in with the rest of the git ecosystem, its design goals\n+and non-goals, comparison to alternatives etc.\n+\n+Comparison with Packfile URIs\n+-----------------------------\n+\n+There is a similar \"Packfile URIs\" facility, see the\n+link:packfile-uri.html[packfile-uri] documentation for details.\n+\n+The Packfile URIs facility requires a much closer cooperation between\n+CDN and server than the bundle URI facility.\n+\n+I.e. the server MUST know what objects exist in the packfile URI it's\n+pointing to, as well as its pack checksum. Failure to do so will not\n+only result in a client error (the packfile hash won't match), but\n+even if it got past that would likely result in a corrupt repository\n+with tips pointing to unreachable objects.\n+\n+By comparison the bundle URIs are meant to be a \"dumb\" solution\n+friendly to e.g. having a weekly cronjob take a snapshot of a git\n+repository, that snapshot being uploaded to a network of FTP mirrors\n+(which may be inconsistent or out of date).\n+\n+The server does not need to know what state the side-channel download\n+is at, because the client will first validate it, and then optionally\n+negotiate with the server using what it discovers there.\n+\n+Using the local `transfer.injectBundleURI` configuration variable (see\n+linkgit:git-config[1]) the `bundle-uri` mechanism doesn't even need\n+the server to support it.\n+\n+Security\n+--------\n+\n+The omission of something equivalent to the packfile <OID> in the\n+Packfile URIs protocol is intentional, as having it would require\n+closer server and CDN cooperation than some server operators are\n+comfortable with.\n+\n+Furthermore, it is not needed for security. The server doesn't need to\n+trust its CDN. If the server were to attempt to send harmful content\n+to the client, the result would not validate against the server's\n+provided ref tips gotten from ls-refs.\n+\n+The lack of a such a hash does leave room open to a malicious CDN\n+operation to be annoying however. E.g. they could inject irrelevant\n+objects into the bundles, which would enlarge the downloaded\n+repository until a \"gc\" would eventually throw them away.\n+\n+In practice the lack of a hash is considered to be a non-issue. Anyone\n+concerned about such security problems between their server and their\n+CDN is going to be pointing to a \"https\" URL under their control. For\n+a client the \"threat\" is the same as without bundle-uri, i.e. a server\n+is free to be annoying today and send you garbage in the PACK that you\n+won't need.\n+\n+Security issues peculiar to bundle-uri\n+--------------------------------------\n+\n+Both packfile-uri and bundle-uri use the `fetch.uriProtocols`\n+configuration variable (see linkgit:git-config[1]) to configure which\n+protocols they support.\n+\n+By default this is set to \"http,https\" for both, but bundle-uri\n+supports adding \"file\" to that list. The server can thus point to\n+\"file://\" URIs it expects the client to have access to.\n+\n+This is primarily intended for use with the `transfer.injectBundleURI`\n+mechanism, but can also be useful e.g. in a centralized environment\n+where a server might point to a \"file:///mnt/bundles/big-repo.bdl\" it\n+knows to be mounted on the local machine (e.g. a racked server),\n+points to it in its \"bundle-uri\" response.\n+\n+The client can then add \"file\" to the `fetch.uriProtocols` list to\n+obey such responses. That does mean that a malicious server can point\n+to any arbitrary file on the local machine. The threat of this is\n+considered minimal, since anyone adding `file` to `fetch.uriProtocols`\n+likely knows what they're doing and controls both ands, and the worst\n+they can do is make a curl(1) pipe garbage into \"index-pack\" (which\n+will likely promptly die on the non-PACK-file).\n+\n+Security comparison with packfile-uri\n+-------------------------------------\n+\n+The initial implementation of packfile-uri needed special adjusting to\n+run \"git fsck\" on incoming .gitmodules files, this was to deal with a\n+general security issue in git, See CVE-2018-17456.\n+\n+The current packfile-uri mechanism requires special handling around\n+\"fsck\" to do such cross-PACK fsck's, this is because it first indexes\n+the \"incremental\" PACK, and then any PACK(s) provided via\n+packfile-uri, before finally doing a full connectivity check.\n+\n+This is effect doing the fsck one might do via \"clone\" and \"fetch\" in\n+reverse, or the equivalent of starting with the incremental \"fetch\",\n+followed by the \"clone\".\n+\n+Since the packfile-uri mechanism can result in the .gitmodules blob\n+referenced by such a \"fetch\" to be in the pack for the \"clone\" the\n+fetch-pack process needs to keep state between the indexing of\n+multiple packs, to remember to fsck the blob (via the \"clone\") later\n+after seeing it in a tree (from the \"fetch).\n+\n+There are no known security issues with the way packfile-uri does\n+this, but since bundle-uri effectively emulates what a which doesn't\n+support either \"bundle-uri\" or \"packfile-uri\" would do on clone/fetch,\n+any future security issues peculiar to the packfile-uri approach are\n+unlikely to be shared by it.\ndiff --git a/Documentation/technical/protocol-v2.txt b/Documentation/technical/protocol-v2.txt\nindex 3ea96add398..3a51492049f 100644\n--- a/Documentation/technical/protocol-v2.txt\n+++ b/Documentation/technical/protocol-v2.txt\n@@ -775,3 +775,8 @@ A client receiving such a a response MAY assume that they can skip\n retrieving the header from a bundle at the indicated URI, and thus\n save themselves and the server(s) the request(s) needed to inspect the\n headers of that bundle or bundles.\n+\n+bundle-uri SEE ALSO\n+^^^^^^^^^^^^^^^^^^^\n+\n+See the link:bundle-uri.html[Bundle URI Design Notes] for more.\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"453891","messageId":"RFC-patch-v2-36.36-764f82a0c0a-20220418T165545Z-avarab@gmail.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"[RFC PATCH v2 36/36] docs: document bundle URI standard","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-18T17:23:53Z","receivedAt":"2022-04-18T17:26:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Derrick Stolee <derrickstolee@github.com>\n\nIntroduce the idea of bundle URIs to the Git codebase through an\naspirational design document. This document includes the full design\nintended to include the feature in its fully-implemented form. This will\ntake several steps as detailed in the Implementation Plan section.\n\nBy committing this document now, it can be used to motivate changes\nnecessary to reach these final goals. The design can still be altered as\nnew information is discovered.\n\nSigned-off-by: Derrick Stolee <derrickstolee@github.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/technical/bundle-uri-TOC.txt | 404 +++++++++++++++++++++\n 1 file changed, 404 insertions(+)\n create mode 100644 Documentation/technical/bundle-uri-TOC.txt\n\ndiff --git a/Documentation/technical/bundle-uri-TOC.txt b/Documentation/technical/bundle-uri-TOC.txt\nnew file mode 100644\nindex 00000000000..4763449e88b\n--- /dev/null\n+++ b/Documentation/technical/bundle-uri-TOC.txt\n@@ -0,0 +1,404 @@\n+Bundle URIs\n+===========\n+\n+Bundle URIs are locations where Git can download one or more bundles in\n+order to bootstrap the object database in advance of fetching the remaining\n+objects from a remote.\n+\n+One goal is to speed up clones and fetches for users with poor network\n+connectivity to the origin server. Another benefit is to allow heavy users,\n+such as CI build farms, to use local resources for the majority of Git data\n+and thereby reducing the load on the origin server.\n+\n+To enable the bundle URI feature, users can specify a bundle URI using\n+command-line options or the origin server can advertise one or more URIs\n+via a protocol v2 capability.\n+\n+Server requirements\n+-------------------\n+\n+To provide a server-side implementation of bundle servers, no other parts\n+of the Git protocol are required. This allows server maintainers to use\n+static content solutions such as CDNs in order to serve the bundle files.\n+\n+At the current scope of the bundle URI feature, all URIs are expected to\n+be HTTP(S) URLs where content is downloaded to a local file using a `GET`\n+request to that URL. The server could include authentication requirements\n+to those requests with the aim of triggering the configured credential\n+helper for secure access.\n+\n+Assuming a `200 OK` response from the server, the content at the URL is\n+expected to be of one of two forms:\n+\n+1. Bundle: A Git bundle file of version 2 or higher.\n+\n+2. Table of Contents: A plain-text file that is parsable using Git's\n+   config file parser. This file describes one or more bundles that are\n+   accessible from other URIs.\n+\n+Any other data provided by the server is considered erroneous.\n+\n+Table of Contents Format\n+------------------------\n+\n+If the content at a bundle URI is not a bundle, then it is expected to be\n+a plaintext file that is parseable using Git's config parser. This file\n+can contain any list of key/value pairs, but only a fixed set will be\n+considered by Git.\n+\n+bundle.tableOfContents.version::\n+\tThis value provides a version number for the table of contents. If\n+\ta future Git change enables a feature that needs the Git client to\n+\treact to a new key in the table of contents file, then this version\n+\twill increment. The only current version number is 1, and if any\n+\tother value is specified then Git will fail to use this file.\n+\n+bundle.tableOfContents.forFetch::\n+\tThis boolean value is a signal to the Git client that the bundle\n+\tserver has designed its bundle organization to assist `git fetch`\n+\tcommands in addition to `git clone` commands. If this is missing,\n+\tGit should not use this table of contents for `git fetch` as it\n+\tmay lead to excess data downloads.\n+\n+The remaining keys include an `<id>` segment which is a server-designated\n+name for each available bundle.\n+\n+bundle.<id>.uri::\n+\tThis string value is the URI for downloading bundle `<id>`. If\n+\tthe URI begins with a protocol (`http://` or `https://`) then the\n+\tURI is absolute. Otherwise, the URI is interpreted as relative to\n+\tthe URI used for the table of contents. If the URI begins with `/`,\n+\tthen that relative path is relative to the domain name used for\n+\tthe table of contents. (This use of relative paths is intended to\n+\tmake it easier to distribute a set of bundles across a large\n+\tnumber of servers or CDNs with different domain names.)\n+\n+bundle.<id>.timestamp::\n+\t(Optional) This value is the number of seconds since Unix epoch\n+\t(UTC) that this bundle was created. This is used as an approximation\n+\tof a point in time that the bundle matches the data available at\n+\tthe origin server.\n+\n+bundle.<id>.requires::\n+\t(Optional) This string value represents the ID of another bundle.\n+\tWhen present, the server is indicating that this bundle contains a\n+\tthin packfile. If the client does not have all necessary objects\n+\tto unbundle this packfile, then the client can download the bundle\n+\twith the `requires` ID and try again. (Note: it may be beneficial\n+\tto allow the server to specify multiple `requires` bundles.)\n+\n+bundle.<id>.filter::\n+\t(Optional) This string value represents an object filter that\n+\tshould also appear in the header of this bundle. The server uses\n+\tthis value to differentiate different kinds of bundles from which\n+\tthe client can choose those that match their object filters.\n+\n+Here is an example table of contents:\n+\n+```\n+[bundle \"tableofcontents\"]\n+\tversion = 1\n+\tforFetch = true\n+\n+[bundle \"2022-02-09-1644442601-daily\"]\n+\turi = https://gitbundleserver.z13.web.core.windows.net/git/git/2022-02-09-1644442601-daily.bundle\n+\ttimestamp = 1644442601\n+\trequires = 2022-02-02-1643842562\n+\n+[bundle \"2022-02-02-1643842562\"]\n+\turi = https://gitbundleserver.z13.web.core.windows.net/git/git/2022-02-02-1643842562.bundle\n+\ttimestamp = 1643842562\n+\n+[bundle \"2022-02-09-1644442631-daily-blobless\"]\n+\turi = 2022-02-09-1644442631-daily-blobless.bundle\n+\ttimestamp = 1644442631\n+\trequires = 2022-02-02-1643842568-blobless\n+\tfilter = blob:none\n+\n+[bundle \"2022-02-02-1643842568-blobless\"]\n+\turi = /git/git/2022-02-02-1643842568-blobless.bundle\n+\ttimestamp = 1643842568\n+\tfilter = blob:none\n+```\n+\n+This example uses all of the keys in the specification. Suppose that the\n+table of contents was found at the URI\n+`https://gitbundleserver.z13.web.core.windows.net/git/git/` and so the\n+two blobless bundles have the following fully-expanded URIs:\n+\n+* `https://gitbundleserver.z13.web.core.windows.net/git/git/2022-02-09-1644442631-daily-blobless.bundle`\n+* `https://gitbundleserver.z13.web.core.windows.net/git/git/2022-02-02-1643842568-blobless.bundle`\n+\n+Advertising Bundle URIs\n+-----------------------\n+\n+If a user knows a bundle URI for the repository they are cloning, then they\n+can specify that URI manually through a command-line option. However, a\n+Git host may want to advertise bundle URIs during the clone operation,\n+helping users unaware of the feature.\n+\n+Note: The exact details of this section are not final. This is a possible\n+way that Git could auto-discover bundle URIs, but is not a committed\n+direction until that feature is implemented.\n+\n+The only thing required for this feature is that the server can advertise\n+one or more bundle URIs. One way to implement this is to create a new\n+protocol v2 capability that advertises recommended features, including\n+bundle URIs.\n+\n+The client could choose an arbitrary bundle URI as an option _or_ select\n+the URI with lowest latency by some exploratory checks. It is up to the\n+server operator to decide if having multiple URIs is preferable to a\n+single URI that is geodistributed through server-side infrastructure.\n+\n+Cloning with Bundle URIs\n+------------------------\n+\n+The primary need for bundle URIs is to speed up clones. The Git client\n+will interact with bundle URIs according to the following flow:\n+\n+1. The user specifies a bundle URI with the `--bundle-uri` command-line\n+   option _or_ the client discovers a bundle URI that was advertised by\n+   the remote server.\n+\n+2. The client downloads the file at the bundle URI. If it is a bundle, then\n+   it is unbundled with the refs being stored in `refs/bundle/*`.\n+\n+3. If the file is instead a table of contents, then the bundles with\n+   matching `filter` settings are sorted by `timestamp` (if present),\n+   and the most-recent bundle is downloaded.\n+\n+4. If the current bundle header mentions negative commid OIDs that are not\n+   in the object database, then download the `requires` bundle and try\n+   again.\n+\n+5. After inspecting a bundle with no negative commit OIDs (or all OIDs are\n+   already in the object database somehow), then unbundle all of the\n+   bundles in reverse order, placing references within `refs/bundle/*`.\n+\n+6. The client performs a fetch negotiation with the origin server, using\n+   the `refs/bundle/*` references as `have`s and the server's ref\n+   advertisement as `want`s. This results in a pack-file containing the\n+   remaining objects requested by the clone but not in the bundles.\n+\n+Note that during a clone we expect that all bundles will be required. The\n+client could be extended to download all bundles in parallel, though they\n+need to be unbundled in the correct order.\n+\n+If a table of contents is used and it contains\n+`bundle.tableOfContents.forFetch = true`, then the client can store a\n+config value indicating to reuse this URI for later `git fetch` commands.\n+In this case, the client will also want to store the maximum timestamp of\n+a downloaded bundle.\n+\n+Fetching with Bundle URIs\n+-------------------------\n+\n+When the client fetches new data, it can decide to fetch from bundle\n+servers before fetching from the origin remote. This could be done via\n+a command-line option, but it is more likely useful to use a config value\n+such as the one specified during the clone.\n+\n+The fetch operation follows the same procedure to download bundles from a\n+table of contents (although we do _not_ want to use parallel downloads\n+here). We expect that the process will end because all negative commit\n+OIDs in a thin bundle are already in the object database.\n+\n+A further optimization is that the client can avoid downloading any\n+bundles if their timestamps are not larger than the stored timestamp.\n+After fetching new bundles, this local timestamp value is updated.\n+\n+Choices for Bundle Server Organization\n+--------------------------------------\n+\n+With this standard, there are many options available to the bundle server\n+in how it organizes Git data into bundles.\n+\n+* Bundles can have whatever name the server desires. This name could refer\n+  to immutable data by using a hash of the bundle contents. However, this\n+  means that a new URI will be needed after every update of the content.\n+  This might be acceptable if the server is advertising the URI (and the\n+  server is aware of new bundles being generated) but would not be\n+  ergonomic for users using the command line option.\n+\n+* If the server intends to only serve full clones, then the advertised URI\n+  could be a bundle file without a filter that is updated at some cadence.\n+\n+* If the server intends to serve clones, but wants clients to choose full\n+  or blobless partial clones, then the server can use a table of contents\n+  that lists two non-thin bundles and the client chooses between them only\n+  by the `bundle.<id>.filter` values.\n+\n+* If the server intends to improve clones with parallel downloads, then it\n+  can use a table of contents and split the repository into time intervals\n+  of approximately similar-sized bundles. Using `bundle.<id>.timestamp`\n+  and `bundle.<id>.requires` values helps the client decide the order to\n+  unbundle the bundles.\n+\n+* If the server intends to serve fetches, then it can use a table of\n+  contents to advertise a list of bundles that are updated regularly. The\n+  most recent bundles could be generated on short intervals, such as hourly.\n+  These small bundles could be merged together at some rate, such as 24\n+  hourly bundles merging into a single daily bundle. At some point, it may\n+  be beneficial to create a bundle that stores the majority of the history,\n+  such as all data older than 30 days.\n+\n+These recommendations are intended only as suggestions. Each repository is\n+different and every Git server has different needs. Hopefully the bundle\n+URI feature and its table of contents is flexible enough to satisfy all\n+needs. If not, then the format can be extended.\n+\n+Error Conditions\n+----------------\n+\n+If the Git client discovers something unexpected while downloading\n+information according to a bundle URI or the table of contents found at\n+that location, then Git can ignore that data and continue as if it was not\n+given a bundle URI. The remote Git server is the ultimate source of truth,\n+not the bundle URI.\n+\n+Here are a few example error conditions:\n+\n+* The client fails to connect with a server at the given URI or a connection\n+  is lost without any chance to recover.\n+\n+* The client receives a response other than `200 OK` (such as `404 Not Found`,\n+  `401 Not Authorized`, or `500 Internal Server Error`).\n+\n+* The client receives data that is not parsable as a bundle or table of\n+  contents.\n+\n+* The table of contents describes a directed cycle in the\n+  `bundle.<id>.requires` links.\n+\n+* A bundle includes a filter that does not match expectations.\n+\n+* The client cannot unbundle the bundles because the negative commit OIDs\n+  are not in the object database and there are no more\n+  `bundle.<id>.requires` links to follow.\n+\n+There are also situations that could be seen as wasteful, but are not\n+error conditions:\n+\n+* The downloaded bundles contain more information than is requested by\n+  the clone or fetch request. A primary example is if the user requests\n+  a clone with `--single-branch` but downloads bundles that store every\n+  reachable commit from all `refs/heads/*` references. This might be\n+  initially wasteful, but perhaps these objects will become reachable by\n+  a later ref update that the client cares about.\n+\n+* A bundle download during a `git fetch` contains objects already in the\n+  object database. This is probably unavoidable if we are using bundles\n+  for fetches, since the client will almost always be slightly ahead of\n+  the bundle servers after performing its \"catch-up\" fetch to the remote\n+  server. This extra work is most wasteful when the client is fetching\n+  much more frequently than the server is computing bundles, such as if\n+  the client is using hourly prefetches with background maintenance, but\n+  the server is computing bundles weekly. For this reason, the client\n+  should not use bundle URIs for fetch unless the server has explicitly\n+  recommended it through the `bundle.tableOfContents.forFetch = true`\n+  value.\n+\n+Implementation Plan\n+-------------------\n+\n+This design document is being submitted on its own as an aspirational\n+document, with the goal of implementing all of the mentioned client\n+features over the course of several patch series. Here is a potential\n+outline for submitting these features for full review:\n+\n+1. Update the `git bundle create` command to take a `--filter` option,\n+   allowing bundles to store packfiles restricted to an object filter.\n+   This is necessary for using bundle URIs to benefit partial clones.\n+\n+2. Integrate bundle URIs into `git clone` with a `--bundle-uri` option.\n+   This will include the full understanding of a table of contents, but\n+   will not integrate with `git fetch` or allow the server to advertise\n+   URIs.\n+\n+3. Integrate bundle URIs into `git fetch`, triggered by config values that\n+   are set during `git clone` if the server indicates that the bundle\n+   strategy works for fetches.\n+\n+4. Create a new \"recommended features\" capability in protocol v2 where the\n+   server can recommend features such as bundle URIs, partial clone, and\n+   sparse-checkout. These features will be extremely limited in scope and\n+   blocked by opt-in config options. The design for this portion could be\n+   replaced by a \"bundle-uri\" capability that only advertises bundle URIs\n+   and no other information.\n+\n+Related Work: Packfile URIs\n+---------------------------\n+\n+The Git protocol already has a capability where the Git server can list\n+a set of URLs along with the packfile response when serving a client\n+request. The client is then expected to download the packfiles at those\n+locations in order to have a complete understanding of the response.\n+\n+This mechanism is used by the Gerrit server (implemented with JGit) and\n+has been effective at reducing CPU load and improving user performance for\n+clones.\n+\n+A major downside to this mechanism is that the origin server needs to know\n+_exactly_ what is in those packfiles, and the packfiles need to be available\n+to the user for some time after the server has responded. This coupling\n+between the origin and the packfile data is difficult to manage.\n+\n+Further, this implementation is extremely hard to make work with fetches.\n+\n+Related Work: GVFS Cache Servers\n+--------------------------------\n+\n+The GVFS Protocol [2] is a set of HTTP endpoints designed independently of\n+the Git project before Git's partial clone was created. One feature of this\n+protocol is the idea of a \"cache server\" which can be colocated with build\n+machines or developer offices to transfer Git data without overloading the\n+central server.\n+\n+The endpoint that VFS for Git is famous for is the `GET /gvfs/objects/{oid}`\n+endpoint, which allows downloading an object on-demand. This is a critical\n+piece of the filesystem virtualization of that product.\n+\n+However, a more subtle need is the `GET /gvfs/prefetch?lastPackTimestamp=<t>`\n+endpoint. Given an optional timestamp, the cache server responds with a list\n+of precomputed packfiles containing the commits and trees that were introduced\n+in those time intervals.\n+\n+The cache server computes these \"prefetch\" packfiles using the following\n+strategy:\n+\n+1. Every hour, an \"hourly\" pack is generated with a given timestamp.\n+2. Nightly, the previous 24 hourly packs are rolled up into a \"daily\" pack.\n+3. Nightly, all prefetch packs more than 30 days old are rolled up into\n+   one pack.\n+\n+When a user runs `gvfs clone` or `scalar clone` against a repo with cache\n+servers, the client requests all prefetch packfiles, which is at most\n+`24 + 30 + 1` packfiles downloading only commits and trees. The client\n+then follows with a request to the origin server for the references, and\n+attempts to checkout that tip reference. (There is an extra endpoint that\n+helps get all reachable trees from a given commit, in case that commit\n+was not already in a prefetch packfile.)\n+\n+During a `git fetch`, a hook requests the prefetch endpoint using the\n+most-recent timestamp from a previously-downloaded prefetch packfile.\n+Only the list of packfiles with later timestamps are downloaded. Most\n+users fetch hourly, so they get at most one hourly prefetch pack. Users\n+whose machines have been off or otherwise have not fetched in over 30 days\n+might redownload all prefetch packfiles. This is rare.\n+\n+It is important to note that the clients always contact the origin server\n+for the refs advertisement, so the refs are frequently \"ahead\" of the\n+prefetched pack data. The missing objects are downloaded on-demand using\n+the `GET gvfs/objects/{oid}` requests, when needed by a command such as\n+`git checkout` or `git log`. Some Git optimizations disable checks that\n+would cause these on-demand downloads to be too aggressive.\n+\n+See Also\n+--------\n+\n+[1] https://lore.kernel.org/git/RFC-cover-00.13-0000000000-20210805T150534Z-avarab@gmail.com/\n+    An earlier RFC for a bundle URI feature.\n+\n+[2] https://github.com/microsoft/VFSForGit/blob/master/Protocol.md\n+    The GVFS Protocol\n-- \n2.36.0.rc2.902.g60576bbc845\n\n"},{"id":"454092","messageId":"04b363f3-58d7-5cfc-6261-0ebcca18ecec@github.com","threadId":"56782","inReplyTo":"RFC-patch-v2-02.36-8f6e4f12e8a-20220418T165545Z-avarab@gmail.com","subject":"Re: [RFC PATCH v2 02/36] dir API: add a generalized path_match_flags() function","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-04-21T17:26:40Z","receivedAt":"2022-04-21T17:26:46Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 4/18/2022 1:23 PM, Ævar Arnfjörð Bjarmason wrote:\n> Add a path_match_flags() function and have the two sets of\n> starts_with_dot_{,dot_}slash() functions added in\n> 63e95beb085 (submodule: port resolve_relative_url from shell to C,\n> 2016-04-15) and a2b26ffb1a8 (fsck: convert gitmodules url to URL\n> passed to curl, 2020-04-18) be thin wrappers for it.\n> \n> As the latter of those notes the fsck version was copied from the\n> initial builtin/submodule--helper.c version.\n> \n> Since the code added in a2b26ffb1a8 was doing really doing the same as\n\ns/doing really doing/really doing/\n\n> win32_is_dir_sep() added in 1cadad6f658 (git clone <url>\n> C:\\cygwin\\home\\USER\\repo' is working (again), 2018-12-15) let's move\n> the latter to git-compat-util.h is a is_xplatform_dir_sep(). We can\n> then call either it or the platform-specific is_dir_sep() from this\n> new function.\n> \n> Let's likewise change code in various other places that was hardcoding\n> checks for \"'/' || '\\\\'\" with the new is_xplatform_dir_sep(). As can\n> be seen in those callers some of them still concern themselves with\n> ':' (Mac OS classic?), but let's leave the question of whether that\n> should be consolidated for some other time.\n\nThis feels like it could be its own change before the refactor\nof the starts_with_dot_{,dot}slash() functions. The diff is pretty\nbig and all over the place.\n\nIf you start with the addition of is_xplatform_dir_sep() (and maybe\nthe change of how is_dir_sep() is created) then the rest of the\nchange is more focused.\n \n> As we expect to make wider use of the \"native\" case in the future,\n> define and use two starts_with_dot_{,dot_}slash_native() convenience\n> wrappers. This makes the diff in builtin/submodule--helper.c much\n> smaller.\n\n> +static int starts_with_dot_slash(const char *const path)\n> +{\n> +\treturn starts_with_dot_slash_native(path);;\n\nDouble semi-colon.\n\n> +int path_match_flags(const char *const str, const enum path_match_flags flags)\n\nI feel like \"path_match_flags()\" is too generic of a name here.\n\nMaybe something like \"path_starts_with_dotslash_flags()\" would be\nsufficiently descriptive.\n\n> +{\n> +\tconst char *p = str;\n> +\n> +\tif (flags & PATH_MATCH_NATIVE &&\n> +\t    flags & PATH_MATCH_XPLATFORM)\n> +\t\tBUG(\"path_match_flags() must get one match kind, not multiple!\");\n> +\telse if (!(flags & PATH_MATCH_KINDS_MASK))\n> +\t\tBUG(\"path_match_flags() must get at least one match kind!\");\n> +\n> +\tif (flags & PATH_MATCH_STARTS_WITH_DOT_SLASH &&\n> +\t    flags & PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH)\n> +\t\tBUG(\"path_match_flags() must get one platform kind, not multiple!\");\n> +\telse if (!(flags & PATH_MATCH_PLATFORM_MASK))\n> +\t\tBUG(\"path_match_flags() must get at least one platform kind!\");\n\nThese would be easier and more robust if we had a simple\npopcount function. It's not worth extracting one out of\newah/ewok.h just for this, though.\n\n> +\tif (*p++ != '.')\n> +\t\treturn 0;\n> +\tif (flags & PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH &&\n> +\t    *p++ != '.')\n> +\t\treturn 0;\n> +\n> +\tif (flags & PATH_MATCH_NATIVE)\n> +\t\treturn is_dir_sep(*p);\n> +\telse if (flags & PATH_MATCH_XPLATFORM)\n> +\t\treturn is_xplatform_dir_sep(*p);\n> +\tBUG(\"unreachable\");\n> +}\n\nThanks,\n-Stolee\n"},{"id":"454093","messageId":"de4a440c-3358-6756-f7fd-144a90fa476a@github.com","threadId":"56782","inReplyTo":"RFC-patch-v2-03.36-7823a177fd7-20220418T165545Z-avarab@gmail.com","subject":"Re: [RFC PATCH v2 03/36] fetch-pack: add a deref_without_lazy_fetch_extended()","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-04-21T17:28:39Z","receivedAt":"2022-04-21T17:28:43Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 4/18/2022 1:23 PM, Ævar Arnfjörð Bjarmason wrote:\n> -static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n> -\t\t\t\t\t       int mark_tags_complete)\n> +static struct commit *deref_without_lazy_fetch_extended(const struct object_id *oid,\n> +\t\t\t\t\t\t\tint mark_tags_complete,\n> +\t\t\t\t\t\t\tenum object_type *type,\n> +\t\t\t\t\t\t\tunsigned int oi_flags)\n>  {\n> -\tenum object_type type;\n> -\tstruct object_info info = { .typep = &type };\n> +\tstruct object_info info = { .typep = type };\n>  \tstruct commit *commit;\n\nSince we now dereference 'type', should we have a BUG() statement here\nif type is NULL?\n\n>  \n>  \tcommit = lookup_commit_in_graph(the_repository, oid);\n> @@ -128,9 +129,9 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n>  \n>  \twhile (1) {\n>  \t\tif (oid_object_info_extended(the_repository, oid, &info,\n> -\t\t\t\t\t     OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_QUICK))\n> +\t\t\t\t\t     oi_flags))\n>  \t\t\treturn NULL;\n> -\t\tif (type == OBJ_TAG) {\n> +\t\tif (*type == OBJ_TAG) {\n>  \t\t\tstruct tag *tag = (struct tag *)\n>  \t\t\t\tparse_object(the_repository, oid);\n>  \n> @@ -144,7 +145,7 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n>  \t\t}\n>  \t}\n>  \n> -\tif (type == OBJ_COMMIT) {\n> +\tif (*type == OBJ_COMMIT) {\n>  \t\tstruct commit *commit = lookup_commit(the_repository, oid);\n>  \t\tif (!commit || repo_parse_commit(the_repository, commit))\n>  \t\t\treturn NULL;\n> @@ -154,6 +155,16 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n>  \treturn NULL;\n>  }\n>  \n> +\n\nnit: extraneous newline.\n\n> +static struct commit *deref_without_lazy_fetch(const struct object_id *oid,\n> +\t\t\t\t\t       int mark_tags_complete)\n\nThanks,\n-Stolee\n"},{"id":"454160","messageId":"dd9836e8-bfc9-9a52-199a-3ffce26101f8@github.com","threadId":"56782","inReplyTo":"RFC-cover-v2-00.36-00000000000-20220418T165545Z-avarab@gmail.com","subject":"Re: [RFC PATCH v2 00/36] bundle-uri: a \"dumb CDN\" for git + TOC format","fromName":"Derrick Stolee","fromEmail":"derrickstolee@github.com","sentAt":"2022-04-21T19:54:27Z","receivedAt":"2022-04-21T19:54:34Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 4/18/2022 1:23 PM, Ævar Arnfjörð Bjarmason wrote:\n> This RFC series is a start at trying to combine the two differing RFC\n> versions of bundle URIs I [1] and Derrick Stolee [2] were kicking\n> around.\n> \n> = Layout\n> \n> This series arranged in the following way:\n> \n> * 01-08: \"Prep\" patches from both [1] and [2] which in principle could\n>   graduate first to \"master\".\n> \n>   I.e. they're prep fixes added for the two bundle-uri\n>   implementations, but which either justify themselves, or e.g. expose\n>   a now-static function via an API.\n> \n>   I tried to move things into the \"justify themselves\" category\n>   whenever possible, but may have overdone it e.g. for 02/36\n>   (originally an idea/commit of Derrick's, but I changed the\n>   authorship as pretty much all of it at this point is something I\n>   changed).\n> \n>   For the \"prep\" changes that are only needed for later changes in the\n>   series perhaps we should just squash them if they're small enough.\n\nI focused today on reading these first 8 patches with the intention that\nthey can be submitted for full review and merging on their own. I think\nthey don't fully succeed in justifying themselves (since not all public\nmethods have callers) but it would be best to have these refactors\nsettled before getting into the nitty gritty of the bundle URI feature.\n\nI mostly had a few nits here and there. I noticed that you did not always\nadd your sign-off after mine, so please correct that when you send the\nnext version (assuming you are planning to do so).\n\nI'll now start digging into the bigger parts of the series, but I expect\nit to take a lot longer to look at everything.\n\nThanks,\n-Stolee\n"},{"id":"454242","messageId":"220422.864k2l5uhe.gmgdl@evledraar.gmail.com","threadId":"56782","inReplyTo":"dd9836e8-bfc9-9a52-199a-3ffce26101f8@github.com","subject":"Re: [RFC PATCH v2 00/36] bundle-uri: a \"dumb CDN\" for git + TOC format","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2022-04-22T09:37:10Z","receivedAt":"2022-04-22T09:37:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Apr 21 2022, Derrick Stolee wrote:\n\n> On 4/18/2022 1:23 PM, Ævar Arnfjörð Bjarmason wrote:\n>> This RFC series is a start at trying to combine the two differing RFC\n>> versions of bundle URIs I [1] and Derrick Stolee [2] were kicking\n>> around.\n>> \n>> = Layout\n>> \n>> This series arranged in the following way:\n>> \n>> * 01-08: \"Prep\" patches from both [1] and [2] which in principle could\n>>   graduate first to \"master\".\n>> \n>>   I.e. they're prep fixes added for the two bundle-uri\n>>   implementations, but which either justify themselves, or e.g. expose\n>>   a now-static function via an API.\n>> \n>>   I tried to move things into the \"justify themselves\" category\n>>   whenever possible, but may have overdone it e.g. for 02/36\n>>   (originally an idea/commit of Derrick's, but I changed the\n>>   authorship as pretty much all of it at this point is something I\n>>   changed).\n>> \n>>   For the \"prep\" changes that are only needed for later changes in the\n>>   series perhaps we should just squash them if they're small enough.\n>\n> I focused today on reading these first 8 patches with the intention that\n> they can be submitted for full review and merging on their own. I think\n> they don't fully succeed in justifying themselves (since not all public\n> methods have callers) but it would be best to have these refactors\n> settled before getting into the nitty gritty of the bundle URI feature.\n>\n> I mostly had a few nits here and there. I noticed that you did not always\n> add your sign-off after mine, so please correct that when you send the\n> next version (assuming you are planning to do so).\n\nWilldo, sorry.\n\nFWIW the ones with missing sign-off are also those I didn't modify\n(extensively), so while I should fix it it might help as a marker for\nstuff I changed right now...\n"}]}