{"thread":{"id":"49473","subject":"[PATCH 0/1] Limit client version advertisements","startedAt":"2018-10-02T22:00:12Z","lastAt":"2019-02-11T18:53:30Z","messageCount":52,"participants":["Josh Steadmon","Stefan Beller","steadmon@google.com","Jonathan Nieder","Junio C Hamano","SZEDER Gábor","Ævar Arnfjörð Bjarmason","Jonathan Tan"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"359474","messageId":"cover.1538516853.git.steadmon@google.com","threadId":"49473","inReplyTo":null,"subject":"[PATCH 0/1] Limit client version advertisements","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-02T21:59:50Z","receivedAt":"2018-10-02T22:00:12Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"As discussed in [1], clients will incorrectly advertise support for\nprotocol version 2 even when the service in question does not have a v2\nimplementation. This patch sets maximum protocol versions for\ngit-receive-pack, git-upload-archive, and git-upload-pack.\n\n[1]: https://public-inbox.org/git/20180927012455.234876-1-steadmon@google.com/\n\nJosh Steadmon (1):\n  protocol: limit max protocol version per service\n\n connect.c     | 11 ++++-------\n protocol.c    | 13 +++++++++++++\n protocol.h    |  7 +++++++\n remote-curl.c | 11 ++++-------\n 4 files changed, 28 insertions(+), 14 deletions(-)\n\n-- \n2.19.0.605.g01d371f741-goog\n\n"},{"id":"359475","messageId":"59357266bd86e8e0ace9217a97717129a6f76188.1538516853.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1538516853.git.steadmon@google.com","subject":"[PATCH 1/1] protocol: limit max protocol version per service","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-02T21:59:51Z","receivedAt":"2018-10-02T22:00:18Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"For services other than git-receive-pack, clients currently advertise\nthat they support the version set in the protocol.version config,\nregardless of whether or not there is actually an implementation of that\nservice for the given protocol version. This causes backwards-\ncompatibility problems when a new implementation for the given\nprotocol version is added.\n\nThis patch sets maximum allowed protocol versions for git-receive-pack,\ngit-upload-archive, and git-upload-pack.\n\nPreviously, git-receive-pack would downgrade from v2 to v0, but would\nallow v1 if set in protocol.version. Now, it will downgrade from v2 to\nv1.\n\nSigned-off-by: Josh Steadmon <steadmon@google.com>\n---\n connect.c     | 11 ++++-------\n protocol.c    | 13 +++++++++++++\n protocol.h    |  7 +++++++\n remote-curl.c | 11 ++++-------\n 4 files changed, 28 insertions(+), 14 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex 94547e5056..4a8cd78239 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1228,14 +1228,11 @@ struct child_process *git_connect(int fd[2], const char *url,\n \tstruct child_process *conn;\n \tenum protocol protocol;\n \tenum protocol_version version = get_protocol_version_config();\n+\tenum protocol_version max_version;\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n-\t\tversion = protocol_v0;\n+\tmax_version = determine_maximum_protocol_version(prog, version);\n+\tif (version > max_version)\n+\t\tversion = max_version;\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\ndiff --git a/protocol.c b/protocol.c\nindex 5e636785d1..1c553d8b99 100644\n--- a/protocol.c\n+++ b/protocol.c\n@@ -79,3 +79,16 @@ enum protocol_version determine_protocol_version_client(const char *server_respo\n \n \treturn version;\n }\n+\n+enum protocol_version determine_maximum_protocol_version(\n+\t\tconst char *service, enum protocol_version default_version)\n+{\n+\tif (!strcmp(service, \"git-receive-pack\"))\n+\t\treturn protocol_v1;\n+\telse if (!strcmp(service, \"git-upload-archive\"))\n+\t\treturn protocol_v1;\n+\telse if (!strcmp(service, \"git-upload-pack\"))\n+\t\treturn protocol_v2;\n+\n+\treturn default_version;\n+}\ndiff --git a/protocol.h b/protocol.h\nindex 2ad35e433c..eabc0c5fab 100644\n--- a/protocol.h\n+++ b/protocol.h\n@@ -31,4 +31,11 @@ extern enum protocol_version determine_protocol_version_server(void);\n  */\n extern enum protocol_version determine_protocol_version_client(const char *server_response);\n \n+/*\n+ * Used by a client to determine the maximum protocol version to advertise for a\n+ * particular service. If the service is unrecognized, return default_version.\n+ */\n+extern enum protocol_version determine_maximum_protocol_version(\n+\t\tconst char *service, enum protocol_version default_version);\n+\n #endif /* PROTOCOL_H */\ndiff --git a/remote-curl.c b/remote-curl.c\nindex fb28309e85..028adb76ae 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -344,6 +344,7 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \tint http_ret, maybe_smart = 0;\n \tstruct http_get_options http_options;\n \tenum protocol_version version = get_protocol_version_config();\n+\tenum protocol_version max_version;\n \n \tif (last && !strcmp(service, last->service))\n \t\treturn last;\n@@ -360,13 +361,9 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n \t}\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n-\t\tversion = protocol_v0;\n+\tmax_version = determine_maximum_protocol_version(service, version);\n+\tif (version > max_version)\n+\t\tversion = max_version;\n \n \t/* Add the extra Git-Protocol header */\n \tif (get_protocol_http_header(version, &protocol_header))\n-- \n2.19.0.605.g01d371f741-goog\n\n"},{"id":"359476","messageId":"CAGZ79kbD=P__8GU9rV87wREF_MbQA9i2ij6C2qXyaJfqHD3Szg@mail.gmail.com","threadId":"49473","inReplyTo":"59357266bd86e8e0ace9217a97717129a6f76188.1538516853.git.steadmon@google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-10-02T22:28:41Z","receivedAt":"2018-10-02T22:28:57Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"On Tue, Oct 2, 2018 at 3:00 PM Josh Steadmon <steadmon@google.com> wrote:\n>\n> For services other than git-receive-pack, clients currently advertise\n> that they support the version set in the protocol.version config,\n> regardless of whether or not there is actually an implementation of that\n> service for the given protocol version. This causes backwards-\n> compatibility problems when a new implementation for the given\n> protocol version is added.\n>\n> This patch sets maximum allowed protocol versions for git-receive-pack,\n> git-upload-archive, and git-upload-pack.\n>\n> Previously, git-receive-pack would downgrade from v2 to v0, but would\n> allow v1 if set in protocol.version. Now, it will downgrade from v2 to\n> v1.\n\nBut does git-receive-pack understand v1?\nAs to my understanding we have not even defined v1\nfor push (receive-pack) and archive --remote (upload-archive).\nv1 is only known to fetch (upload-pack).\n\n> +enum protocol_version determine_maximum_protocol_version(\n> +               const char *service, enum protocol_version default_version)\n> +{\n> +       if (!strcmp(service, \"git-receive-pack\"))\n> +               return protocol_v1;\n> +       else if (!strcmp(service, \"git-upload-archive\"))\n> +               return protocol_v1;\n\nso I would think these two would be _v0.\n... goes and checks ...\naa9bab29b8 (upload-pack, receive-pack: introduce protocol version 1,\n2017-10-16) seems to actually teach v1 to receive-pack as well,\nbut upload-archive was completely off radar, so I think returning\n(v1, v0, v2 in the order as in the code) would make sense?\n\nAsides from this, I thought there was a deliberate decision\nthat we'd want to avoid a strict order on the protocol versions,\nbut I could not find prior discussion on list to back up this claim. :/\n\nFor example we'd go with e.g. enums instead of integers\nfor version numbers, as then some internal setup could\nalso have things like protocol_v2018-10-02 or protocol_vWhatever;\nsome protocol version may be advantageous to the client, some to\nthe server, and we'd need to negotiate the best version that both\nare happy with. (e.g. the server may like version 0, 2 and 3, and\nthe client may like 0,2,4 as 3 is bad security wise for the client,\nso both would negotiate to 2 as their best case)\n\nFrom a maintenance perspective, do we want to keep\nthis part of the code central, as it ties protocol (as proxied\nby service name) to the max version number?\nI would think that we'd rather have the decision local to the\ncode, i.e. builtin/fetch would need to tell protocol.c that it\ncan do (0,1,2) and builtin/push can do (0,1), and then the\nnetworking layers of code would figure out by the input\nfrom the caller and the input from the user (configured\nprotocol.version) what is the best to go forward from\nthen on.\n\nBut I guess having the central place here is not to\nbad either. How will it cope with the desire of protocol v2\nto have only one end point (c.f. serve.{c,h} via builtin/serve\nas \"git serve\") ?\n\nStefan\n"},{"id":"359591","messageId":"20181003213349.GA32105@google.com","threadId":"49473","inReplyTo":"CAGZ79kbD=P__8GU9rV87wREF_MbQA9i2ij6C2qXyaJfqHD3Szg@mail.gmail.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-03T21:33:49Z","receivedAt":"2018-10-03T21:34:04Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.10.02 15:28, Stefan Beller wrote:\n> On Tue, Oct 2, 2018 at 3:00 PM Josh Steadmon <steadmon@google.com> wrote:\n> >\n> > For services other than git-receive-pack, clients currently advertise\n> > that they support the version set in the protocol.version config,\n> > regardless of whether or not there is actually an implementation of that\n> > service for the given protocol version. This causes backwards-\n> > compatibility problems when a new implementation for the given\n> > protocol version is added.\n> >\n> > This patch sets maximum allowed protocol versions for git-receive-pack,\n> > git-upload-archive, and git-upload-pack.\n> >\n> > Previously, git-receive-pack would downgrade from v2 to v0, but would\n> > allow v1 if set in protocol.version. Now, it will downgrade from v2 to\n> > v1.\n> \n> But does git-receive-pack understand v1?\n> As to my understanding we have not even defined v1\n> for push (receive-pack) and archive --remote (upload-archive).\n> v1 is only known to fetch (upload-pack).\n> \n> > +enum protocol_version determine_maximum_protocol_version(\n> > +               const char *service, enum protocol_version default_version)\n> > +{\n> > +       if (!strcmp(service, \"git-receive-pack\"))\n> > +               return protocol_v1;\n> > +       else if (!strcmp(service, \"git-upload-archive\"))\n> > +               return protocol_v1;\n> \n> so I would think these two would be _v0.\n> ... goes and checks ...\n> aa9bab29b8 (upload-pack, receive-pack: introduce protocol version 1,\n> 2017-10-16) seems to actually teach v1 to receive-pack as well,\n> but upload-archive was completely off radar, so I think returning\n> (v1, v0, v2 in the order as in the code) would make sense?\n\nI believe that git-upload-archive can still speak version 1 without any\ntrouble, but it at least doesn't break anything in the test suite to\nlimit this to v0 either.\n\n\n> Asides from this, I thought there was a deliberate decision\n> that we'd want to avoid a strict order on the protocol versions,\n> but I could not find prior discussion on list to back up this claim. :/\n> \n> For example we'd go with e.g. enums instead of integers\n> for version numbers, as then some internal setup could\n> also have things like protocol_v2018-10-02 or protocol_vWhatever;\n> some protocol version may be advantageous to the client, some to\n> the server, and we'd need to negotiate the best version that both\n> are happy with. (e.g. the server may like version 0, 2 and 3, and\n> the client may like 0,2,4 as 3 is bad security wise for the client,\n> so both would negotiate to 2 as their best case)\n\nIs there a method or design for advertising multiple acceptable versions\nfrom the client? From my understanding, we can only add a single\nversion=X field in the advertisement, but IIUC we can extend this fairly\neasily? Perhaps we can have \"version=X\" to mean the preferred version,\nand then a repeatable \"acceptable_version=Y\" field or similar?\n\n\n> From a maintenance perspective, do we want to keep\n> this part of the code central, as it ties protocol (as proxied\n> by service name) to the max version number?\n> I would think that we'd rather have the decision local to the\n> code, i.e. builtin/fetch would need to tell protocol.c that it\n> can do (0,1,2) and builtin/push can do (0,1), and then the\n> networking layers of code would figure out by the input\n> from the caller and the input from the user (configured\n> protocol.version) what is the best to go forward from\n> then on.\n\nI like having it centralized, because enforcing this in git_connect()\nand discover_refs() catches all the outgoing version advertisements, but\nthere's lots of code paths that lead to those two functions that would\nall have to have the acceptable version numbers plumbed through.\n\nI suppose we could also have a registry of services to version numbers,\nbut I tend to dislike non-local sources of data. But if the list likes\nthat approach better, I'll be happy to implement it.\n\n\n> But I guess having the central place here is not to\n> bad either. How will it cope with the desire of protocol v2\n> to have only one end point (c.f. serve.{c,h} via builtin/serve\n> as \"git serve\") ?\n\nI'm not sure about this. In my series to add a v2 archive command, I\nadded support for a new endpoint for proto v2 and I don't recall seeing\nany complaints, but that is still open for review.\n\nI suppose if we are strict about serving from a single endpoint, the\nversion registry makes more sense, and individual operations can declare\nacceptable version numbers before calling any network code?\n\n\n\nThanks for the review,\nJosh\n"},{"id":"359594","messageId":"CAGZ79kZP7y3v8hrhKKP_FT7+uyUSWO5s8w13-c67QWdzme1-Qw@mail.gmail.com","threadId":"49473","inReplyTo":"20181003213349.GA32105@google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-10-03T22:47:05Z","receivedAt":"2018-10-03T22:47:21Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"On Wed, Oct 3, 2018 at 2:34 PM Josh Steadmon <steadmon@google.com> wrote:\n\n>\n> I believe that git-upload-archive can still speak version 1 without any\n> trouble, but it at least doesn't break anything in the test suite to\n> limit this to v0 either.\n\nok, let me just play around with archive to follow along:\n\nRunning the excerpt that I found in one of the tests in t5000\n\n    GIT_TRACE_PACKET=1 git archive --remote=. HEAD >b5.tar\n\n(whoooa ... that spews a lot of text into my terminal, which makes\nsense as transported tar files unlike packets starting with PACK are\nnot cut short; so onwards:)\n\n    $ git init test && cd test\n    $ GIT_TRACE_PACKET=1 git archive --remote=. HEAD >b5.tar\n    ...\n    git< \\2fatal: no such ref: HEAD\n    ...\n    fatal: sent error to the client: git upload-archive: archiver died\nwith error\n    remote: git upload-archive: archiver died with error\n\nThis sounds similar to a bug that I have on my todo list for\nclone recursing into submodules. Maybe we need to talk\nabout HEAD-less repositories and how to solve handling them\nin general. Usually it doesn't happen except for corner cases like\nnow, so:\n\n    $ git commit --allow-empty -m \"commit\"\n    [master (root-commit) 24d7967] commit\n    $ GIT_TRACE_PACKET=1 git archive --remote=. HEAD >b5.tar\n15:28:00.762615 pkt-line.c:80           packet:          git> argument HEAD\n15:28:00.762704 pkt-line.c:80           packet:          git> 0000\n15:28:00.766336 pkt-line.c:80           packet:          git> ACK\n15:28:00.766428 pkt-line.c:80           packet:          git> 0000\n15:28:00.766483 pkt-line.c:80           packet:          git< ACK\n15:28:00.766508 pkt-line.c:80           packet:          git< 0000\n15:28:00.767694 pkt-line.c:80           packet:          git< \\2\n15:28:00.767524 pkt-line.c:80           packet:          git< argument HEAD\n15:28:00.767583 pkt-line.c:80           packet:          git< 0000\nremote: 15:28:00.767524 pkt-line.c:80           packet:          git<\nargument HEAD\nremote: 15:28:00.767583 pkt-line.c:80           packet:          git< 0000\n15:28:00.768758 pkt-line.c:80           packet:          git> 0000\n15:28:00.770475 pkt-line.c:80           packet:          git<\n\\1pax_global_header\n    ... \\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\ ...\n # not too bad but the tar file contains a lot of zeros\n    $\n\nAh I forgot the crucial part, so\n\n    $ GIT_TRACE_PACKET=1 git -c protocol.version=1 archive --remote=.\nHEAD >b5.tar\n15:33:10.030508 pkt-line.c:80           packet:          git> argument HEAD\n...\n\nThis pretty much looks like v0 as v1 would send a \"version 1\", c.f.\n\n    $ GIT_TRACE_PACKET=1 git -c protocol.version=1 fetch .\n15:34:26.111013 pkt-line.c:80           packet:  upload-pack> version 1\n15:34:26.111140 pkt-line.c:80           packet:        fetch< version 1\n....\n\n\n>\n> Is there a method or design for advertising multiple acceptable versions\n> from the client?\n\nI think the client can send multiple versions, looking through protocol.c\n(and not the documentation as I should for this:)\n\n  /*\n   * Determine which protocol version the client has requested.  Since\n   * multiple 'version' keys can be sent by the client, indicating that\n   * the client is okay to speak any of them, select the greatest version\n   * that the client has requested.  This is due to the assumption that\n   * the most recent protocol version will be the most state-of-the-art.\n   */\n    ...\n    const char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n    string_list_split(&list, git_protocol, ':', -1);\n    ...\n    for_each_string_list_item(item, &list) {\n        ...\n        if (skip_prefix(item->string, \"version=\", &value))\n            ...\n\nin determine_protocol_version_server which already had the client\nspeak to it, so I think at least the server can deal with multiple versions.\n\nBut given that we transport the version in env variables, we'd\nneed to be extra careful if we just did not see the version\nin the --remote=. above?\n\n> From my understanding, we can only add a single\n> version=X field in the advertisement, but IIUC we can extend this fairly\n> easily? Perhaps we can have \"version=X\" to mean the preferred version,\n> and then a repeatable \"acceptable_version=Y\" field or similar?\n\nJust re-use \"version X\", separated by colons as above.\n\n> > From a maintenance perspective, do we want to keep\n> > this part of the code central, as it ties protocol (as proxied\n> > by service name) to the max version number?\n> > I would think that we'd rather have the decision local to the\n> > code, i.e. builtin/fetch would need to tell protocol.c that it\n> > can do (0,1,2) and builtin/push can do (0,1), and then the\n> > networking layers of code would figure out by the input\n> > from the caller and the input from the user (configured\n> > protocol.version) what is the best to go forward from\n> > then on.\n>\n> I like having it centralized, because enforcing this in git_connect()\n> and discover_refs() catches all the outgoing version advertisements, but\n> there's lots of code paths that lead to those two functions that would\n> all have to have the acceptable version numbers plumbed through.\n\nMakes sense.\n\n> I suppose we could also have a registry of services to version numbers,\n> but I tend to dislike non-local sources of data. But if the list likes\n> that approach better, I'll be happy to implement it.\n\n> > But I guess having the central place here is not to\n> > bad either. How will it cope with the desire of protocol v2\n> > to have only one end point (c.f. serve.{c,h} via builtin/serve\n> > as \"git serve\") ?\n>\n> I'm not sure about this. In my series to add a v2 archive command, I\n> added support for a new endpoint for proto v2 and I don't recall seeing\n> any complaints, but that is still open for review.\n\nAh I guess new end points would imply that you can speak at least\na given version N.\n\n> I suppose if we are strict about serving from a single endpoint, the\n> version registry makes more sense, and individual operations can declare\n> acceptable version numbers before calling any network code?\n\nAh yeah, that makes sense!\n\nThanks for your explanations and prodding,\nStefan\n"},{"id":"359649","messageId":"20181005001817.GB32105@google.com","threadId":"49473","inReplyTo":"CAGZ79kZP7y3v8hrhKKP_FT7+uyUSWO5s8w13-c67QWdzme1-Qw@mail.gmail.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-05T00:18:17Z","receivedAt":"2018-10-05T00:18:28Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.10.03 15:47, Stefan Beller wrote:\n> On Wed, Oct 3, 2018 at 2:34 PM Josh Steadmon <steadmon@google.com> wrote:\n> >\n> > Is there a method or design for advertising multiple acceptable versions\n> > from the client?\n> \n> I think the client can send multiple versions, looking through protocol.c\n> (and not the documentation as I should for this:)\n> \n>   /*\n>    * Determine which protocol version the client has requested.  Since\n>    * multiple 'version' keys can be sent by the client, indicating that\n>    * the client is okay to speak any of them, select the greatest version\n>    * that the client has requested.  This is due to the assumption that\n>    * the most recent protocol version will be the most state-of-the-art.\n>    */\n>     ...\n>     const char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n>     string_list_split(&list, git_protocol, ':', -1);\n>     ...\n>     for_each_string_list_item(item, &list) {\n>         ...\n>         if (skip_prefix(item->string, \"version=\", &value))\n>             ...\n> \n> in determine_protocol_version_server which already had the client\n> speak to it, so I think at least the server can deal with multiple versions.\n> \n> But given that we transport the version in env variables, we'd\n> need to be extra careful if we just did not see the version\n> in the --remote=. above?\n\nSorry, I'm not sure I understand this. What about env variables requires\ncaution?\n\n\n> > From my understanding, we can only add a single\n> > version=X field in the advertisement, but IIUC we can extend this fairly\n> > easily? Perhaps we can have \"version=X\" to mean the preferred version,\n> > and then a repeatable \"acceptable_version=Y\" field or similar?\n> \n> Just re-use \"version X\", separated by colons as above.\n> \n> > > From a maintenance perspective, do we want to keep\n> > > this part of the code central, as it ties protocol (as proxied\n> > > by service name) to the max version number?\n> > > I would think that we'd rather have the decision local to the\n> > > code, i.e. builtin/fetch would need to tell protocol.c that it\n> > > can do (0,1,2) and builtin/push can do (0,1), and then the\n> > > networking layers of code would figure out by the input\n> > > from the caller and the input from the user (configured\n> > > protocol.version) what is the best to go forward from\n> > > then on.\n> >\n> > I like having it centralized, because enforcing this in git_connect()\n> > and discover_refs() catches all the outgoing version advertisements, but\n> > there's lots of code paths that lead to those two functions that would\n> > all have to have the acceptable version numbers plumbed through.\n> \n> Makes sense.\n> \n> > I suppose we could also have a registry of services to version numbers,\n> > but I tend to dislike non-local sources of data. But if the list likes\n> > that approach better, I'll be happy to implement it.\n> \n> > > But I guess having the central place here is not to\n> > > bad either. How will it cope with the desire of protocol v2\n> > > to have only one end point (c.f. serve.{c,h} via builtin/serve\n> > > as \"git serve\") ?\n> >\n> > I'm not sure about this. In my series to add a v2 archive command, I\n> > added support for a new endpoint for proto v2 and I don't recall seeing\n> > any complaints, but that is still open for review.\n> \n> Ah I guess new end points would imply that you can speak at least\n> a given version N.\n> \n> > I suppose if we are strict about serving from a single endpoint, the\n> > version registry makes more sense, and individual operations can declare\n> > acceptable version numbers before calling any network code?\n> \n> Ah yeah, that makes sense!\n\nThinking out loud here. Please let me know if I say something stupid :)\n\nSo we'll have (up to) three pieces of version information we'll care\nabout for version negotiation:\n\n1. (maybe) a client-side protocol.version config entry\n2. a list of acceptable proto versions for the currently running\n   operation on the client\n3. a list of acceptable proto versions for the server endpoint that\n   handles the request\n\nAccording to the doc on protocol.version: \"If set, clients will attempt\nto communicate with a server using the specified protocol version. If\nunset, no attempt will be made by the client to communicate using a\nparticular protocol version, this results in protocol version 0 being\nused.\"\n\nSo, if protocol.version is not set, or set to 0, the client should not\nattempt any sort of version negotiation. Otherwise, the client prefers a\nparticular version, but we don't guarantee that they will actually use\nthat version after the (unspecified) version negotiation procedure.\n\nIf protocol.version is set to something other than 0, we construct a\nlist of acceptable versions for the given operation. If the\nprotocol.version entry is present in that list, we move it to the front\nof the list to note that it is the preferred version. We send all of\nthese, in order, in the request.\n\nWhen the server endpoint begins to handle a request, it first constructs\na list of acceptable versions. If the client specifies a list of\nversions, we check them one-by-one to see if they are acceptable. If we\nfind a match, we use that version. If we don't find any matches or if\nthe client did not send a version list, we default to v0.\n\nSeem reasonable?\n\n\nThanks,\nJosh\n"},{"id":"359722","messageId":"CAGZ79kbSF5XM72SOQH7XNyjeCLqQ+AuxOqMPTspjOgk-jEgrzw@mail.gmail.com","threadId":"49473","inReplyTo":"20181005001817.GB32105@google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-10-05T19:25:12Z","receivedAt":"2018-10-05T19:25:28Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"> > But given that we transport the version in env variables, we'd\n> > need to be extra careful if we just did not see the version\n> > in the --remote=. above?\n>\n> Sorry, I'm not sure I understand this. What about env variables requires\n> caution?\n\nBy locally transporting the version via env variables means the absence of\n\"version X\" lines in the GIT_TRACE output is not a clear indication\nof version 0, I would think. It is a strong indicator, but now we'd have to dig\nand see if the env variables were set outside?\n\n\n> >\n> > > I suppose if we are strict about serving from a single endpoint, the\n> > > version registry makes more sense, and individual operations can declare\n> > > acceptable version numbers before calling any network code?\n> >\n> > Ah yeah, that makes sense!\n>\n> Thinking out loud here. Please let me know if I say something stupid :)\n>\n> So we'll have (up to) three pieces of version information we'll care\n> about for version negotiation:\n>\n> 1. (maybe) a client-side protocol.version config entry\n\n(and in case we don't, we have it implicit ly hardcoded, as\nwe have to choose the default for users that don't care themselves about\nthis setting)\n\n> 2. a list of acceptable proto versions for the currently running\n>    operation on the client\n> 3. a list of acceptable proto versions for the server endpoint that\n>    handles the request\n\nYes that matches my understanding. The issue is between (1) and (2)\nas (1) is in a generic config, whereas (2) is specific to the command,\nsuch that it may differ. And as a user you may want to express things\nlike: \"use the highest version\", which is done by setting (1) to \"version 2\"\ndespite (2) not having support of all commands for v2.\n\n> According to the doc on protocol.version: \"If set, clients will attempt\n> to communicate with a server using the specified protocol version. If\n> unset, no attempt will be made by the client to communicate using a\n> particular protocol version, this results in protocol version 0 being\n> used.\"\n>\n> So, if protocol.version is not set, or set to 0, the client should not\n> attempt any sort of version negotiation.\n\nYes, as version 0 is unaware of versions, i.e. there are old installations\nout there where all the versioning code is not there, so in case of an\nold client the new server *must* speak v0 to be able to communicate\n(and vice versa).\n\n\n> Otherwise, the client prefers a\n> particular version, but we don't guarantee that they will actually use\n> that version after the (unspecified) version negotiation procedure.\n>\n> If protocol.version is set to something other than 0, we construct a\n> list of acceptable versions for the given operation. If the\n> protocol.version entry is present in that list, we move it to the front\n> of the list to note that it is the preferred version. We send all of\n> these, in order, in the request.\n>\n> When the server endpoint begins to handle a request, it first constructs\n> a list of acceptable versions. If the client specifies a list of\n> versions, we check them one-by-one to see if they are acceptable. If we\n> find a match, we use that version. If we don't find any matches or if\n> the client did not send a version list, we default to v0.\n>\n> Seem reasonable?\n\nThis sounds super reasonable!\n\nThanks\nStefan\n"},{"id":"360112","messageId":"20181010235303.GB12177@google.com","threadId":"49473","inReplyTo":"CAGZ79kbSF5XM72SOQH7XNyjeCLqQ+AuxOqMPTspjOgk-jEgrzw@mail.gmail.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-10T23:53:03Z","receivedAt":"2018-10-10T23:53:12Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.10.05 12:25, Stefan Beller wrote:\n> > > > I suppose if we are strict about serving from a single endpoint, the\n> > > > version registry makes more sense, and individual operations can declare\n> > > > acceptable version numbers before calling any network code?\n> > >\n> > > Ah yeah, that makes sense!\n> >\n> > Thinking out loud here. Please let me know if I say something stupid :)\n> >\n> > So we'll have (up to) three pieces of version information we'll care\n> > about for version negotiation:\n> >\n> > 1. (maybe) a client-side protocol.version config entry\n> \n> (and in case we don't, we have it implicit ly hardcoded, as\n> we have to choose the default for users that don't care themselves about\n> this setting)\n> \n> > 2. a list of acceptable proto versions for the currently running\n> >    operation on the client\n> > 3. a list of acceptable proto versions for the server endpoint that\n> >    handles the request\n> \n> Yes that matches my understanding. The issue is between (1) and (2)\n> as (1) is in a generic config, whereas (2) is specific to the command,\n> such that it may differ. And as a user you may want to express things\n> like: \"use the highest version\", which is done by setting (1) to \"version 2\"\n> despite (2) not having support of all commands for v2.\n> \n> > According to the doc on protocol.version: \"If set, clients will attempt\n> > to communicate with a server using the specified protocol version. If\n> > unset, no attempt will be made by the client to communicate using a\n> > particular protocol version, this results in protocol version 0 being\n> > used.\"\n> >\n> > So, if protocol.version is not set, or set to 0, the client should not\n> > attempt any sort of version negotiation.\n> \n> Yes, as version 0 is unaware of versions, i.e. there are old installations\n> out there where all the versioning code is not there, so in case of an\n> old client the new server *must* speak v0 to be able to communicate\n> (and vice versa).\n> \n> \n> > Otherwise, the client prefers a\n> > particular version, but we don't guarantee that they will actually use\n> > that version after the (unspecified) version negotiation procedure.\n> >\n> > If protocol.version is set to something other than 0, we construct a\n> > list of acceptable versions for the given operation. If the\n> > protocol.version entry is present in that list, we move it to the front\n> > of the list to note that it is the preferred version. We send all of\n> > these, in order, in the request.\n> >\n> > When the server endpoint begins to handle a request, it first constructs\n> > a list of acceptable versions. If the client specifies a list of\n> > versions, we check them one-by-one to see if they are acceptable. If we\n> > find a match, we use that version. If we don't find any matches or if\n> > the client did not send a version list, we default to v0.\n> >\n> > Seem reasonable?\n> \n> This sounds super reasonable!\n\nSo this runs into problems with remote-curl (and possibly other remote\nhelpers):\n\nbuiltin/push.c can declare whatever allowed versions it wants, but those\nare not carried over when remote-curl is started to actually talk to the\nremote. What's worse, remote-curl starts its HTTP connection before it\nknows what command it's actually acting as a helper for.\n\nFor now, I'm going to try adding an --allowed_versions flag for the\nremote helpers, but if anyone has a better idea, let me know.\n\n\nThanks,\nJosh\n"},{"id":"360260","messageId":"cover.1539305180.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1538516853.git.steadmon@google.com","subject":"[PATCH v2 0/1] Advertise multiple supported proto versions","fromName":"","fromEmail":"steadmon@google.com","sentAt":"2018-10-12T01:02:14Z","receivedAt":"2018-10-12T01:02:35Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"From: Josh Steadmon <steadmon@google.com>\n\nThis is an alternate approach to the previous series. We add a registry\nof supported wire protocol versions that individual commands can use to\ndeclare supported versions before contacting a server. The client will\nthen advertise all supported versions, while the server will choose the\nfirst recognized version from the advertised list.\n\nCompared to the previous series, this approach is more convenient for\nprotocol_v2, which is intended to work on a single server endpoint.\nHowever, it has the drawback that every command that acts as a client\nmust register its supported versions; it is not always obvious which (if\nany) network operations a given command will perform.\n\nThank you to Stefan for his review of the previous series and for\nhelping me think through the requirements for this new approach.\n\nJosh Steadmon (1):\n  protocol: advertise multiple supported versions\n\n builtin/archive.c      |   3 ++\n builtin/clone.c        |   4 ++\n builtin/fetch-pack.c   |   4 ++\n builtin/fetch.c        |   5 ++\n builtin/ls-remote.c    |   5 ++\n builtin/pull.c         |   5 ++\n builtin/push.c         |   4 ++\n builtin/send-pack.c    |   3 ++\n connect.c              |  47 ++++++++---------\n protocol.c             | 115 ++++++++++++++++++++++++++++++++++++++---\n protocol.h             |  17 ++++++\n remote-curl.c          |  28 ++++++----\n t/t5570-git-daemon.sh  |   2 +-\n t/t5700-protocol-v1.sh |   8 +--\n t/t5702-protocol-v2.sh |  16 +++---\n transport-helper.c     |   6 +++\n 16 files changed, 217 insertions(+), 55 deletions(-)\n\n-- \n2.19.0.605.g01d371f741-goog\n\n"},{"id":"360261","messageId":"70986cec32880db16568d85c351b33e9a8e16f1c.1539305180.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1539305180.git.steadmon@google.com","subject":"[PATCH v2 1/1] protocol: advertise multiple supported versions","fromName":"","fromEmail":"steadmon@google.com","sentAt":"2018-10-12T01:02:15Z","receivedAt":"2018-10-12T01:02:43Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"From: Josh Steadmon <steadmon@google.com>\n\nCurrently the client advertises that it supports the wire protocol\nversion set in the protocol.version config. However, not all services\nsupport the same set of protocol versions. When connecting to\ngit-receive-pack, the client automatically downgrades to v0 if\nconfig.protocol is set to v2, but this check is not performed for other\nservices.\n\nThis patch creates a protocol version registry. Individual commands\nregister all the protocol versions they support prior to communicating\nwith a server. Versions should be listed in preference order; the\nversion specified in protocol.version will automatically be moved to the\nfront of the registry.\n\nThe protocol version registry is passed to remote helpers via the\nGIT_PROTOCOL environment variable.\n\nClients now advertise the full list of registered versions. Servers\nselect the first recognized version from this advertisement.\n\nSigned-off-by: Josh Steadmon <steadmon@google.com>\n---\n builtin/archive.c      |   3 ++\n builtin/clone.c        |   4 ++\n builtin/fetch-pack.c   |   4 ++\n builtin/fetch.c        |   5 ++\n builtin/ls-remote.c    |   5 ++\n builtin/pull.c         |   5 ++\n builtin/push.c         |   4 ++\n builtin/send-pack.c    |   3 ++\n connect.c              |  47 ++++++++---------\n protocol.c             | 115 ++++++++++++++++++++++++++++++++++++++---\n protocol.h             |  17 ++++++\n remote-curl.c          |  28 ++++++----\n t/t5570-git-daemon.sh  |   2 +-\n t/t5700-protocol-v1.sh |   8 +--\n t/t5702-protocol-v2.sh |  16 +++---\n transport-helper.c     |   6 +++\n 16 files changed, 217 insertions(+), 55 deletions(-)\n\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex e74f675390..8adb9f381b 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -8,6 +8,7 @@\n #include \"transport.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n \n static void create_output_file(const char *output_file)\n@@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, local_opts, NULL,\n \t\t\t     PARSE_OPT_KEEP_ALL);\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd2c3ef090..1651a950b6 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -900,6 +900,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tstruct argv_array ref_prefixes = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tfetch_if_missing = 0;\n \n \tpacket_trace_identity(\"clone\");\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex 1a1bc63566..cba935e4d3 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -57,6 +57,10 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)\n \n \tfetch_if_missing = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch-pack\");\n \n \tmemset(&args, 0, sizeof(args));\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 61bec5d213..2a20cf8bfd 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -21,6 +21,7 @@\n #include \"argv-array.h\"\n #include \"utf8.h\"\n #include \"packfile.h\"\n+#include \"protocol.h\"\n #include \"list-objects-filter-options.h\"\n \n static const char * const builtin_fetch_usage[] = {\n@@ -1476,6 +1477,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tint prune_tags_ok = 1;\n \tstruct argv_array argv_gc_auto = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch\");\n \n \tfetch_if_missing = 0;\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 1a25df7ee1..ea685e8bb9 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -1,5 +1,6 @@\n #include \"builtin.h\"\n #include \"cache.h\"\n+#include \"protocol.h\"\n #include \"transport.h\"\n #include \"ref-filter.h\"\n #include \"remote.h\"\n@@ -80,6 +81,10 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)\n \n \tmemset(&ref_array, 0, sizeof(ref_array));\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, ls_remote_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tdest = argv[0];\ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex 681c127a07..cb64146834 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"builtin.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"exec-cmd.h\"\n #include \"run-command.h\"\n #include \"sha1-array.h\"\n@@ -849,6 +850,10 @@ int cmd_pull(int argc, const char **argv, const char *prefix)\n \tstruct object_id rebase_fork_point;\n \tint autostash;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tif (!getenv(\"GIT_REFLOG_ACTION\"))\n \t\tset_reflog_message(argc, argv);\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex d09a42062c..10d8abe829 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -10,6 +10,7 @@\n #include \"remote.h\"\n #include \"transport.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"submodule.h\"\n #include \"submodule-config.h\"\n #include \"send-pack.h\"\n@@ -587,6 +588,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"push\");\n \tgit_config(git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8e3c7490f7..f48bd1306b 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -184,6 +184,9 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tgit_config(send_pack_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);\n \tif (argc > 0) {\ndiff --git a/connect.c b/connect.c\nindex 94547e5056..a170ba1e90 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n  */\n static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t\t\t\t     const char *path, const char *prog,\n-\t\t\t\t\t     enum protocol_version version,\n+\t\t\t\t\t     const struct strbuf *version_advert,\n \t\t\t\t\t     int flags)\n {\n \tstruct child_process *conn;\n@@ -1085,10 +1085,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t    target_host, 0);\n \n \t/* If using a new version put that stuff here after a second null byte */\n-\tif (version > 0) {\n+\tif (strcmp(version_advert->buf, \"version=0\")) {\n \t\tstrbuf_addch(&request, '\\0');\n-\t\tstrbuf_addf(&request, \"version=%d%c\",\n-\t\t\t    version, '\\0');\n+\t\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n \t}\n \n \tpacket_write(fd[1], request.buf, request.len);\n@@ -1104,14 +1103,14 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n  */\n static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \t\t\t     enum ssh_variant variant, const char *port,\n-\t\t\t     enum protocol_version version, int flags)\n+\t\t\t     const struct strbuf *version_advert, int flags)\n {\n \tif (variant == VARIANT_SSH &&\n-\t    version > 0) {\n+\t    strcmp(version_advert->buf, \"version=0\")) {\n \t\targv_array_push(args, \"-o\");\n \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n-\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t version);\n+\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t version_advert->buf);\n \t}\n \n \tif (flags & CONNECT_IPV4) {\n@@ -1164,7 +1163,7 @@ static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \n /* Prepare a child_process for use by Git's SSH-tunneled transport. */\n static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n-\t\t\t  const char *port, enum protocol_version version,\n+\t\t\t  const char *port, const struct strbuf *version_advert,\n \t\t\t  int flags)\n {\n \tconst char *ssh;\n@@ -1198,15 +1197,16 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \n \t\targv_array_push(&detect.args, ssh);\n \t\targv_array_push(&detect.args, \"-G\");\n-\t\tpush_ssh_options(&detect.args, &detect.env_array,\n-\t\t\t\t VARIANT_SSH, port, version, flags);\n+\t\tpush_ssh_options(&detect.args, &detect.env_array, VARIANT_SSH,\n+\t\t\t\t port, version_advert, flags);\n \t\targv_array_push(&detect.args, ssh_host);\n \n \t\tvariant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;\n \t}\n \n \targv_array_push(&conn->args, ssh);\n-\tpush_ssh_options(&conn->args, &conn->env_array, variant, port, version, flags);\n+\tpush_ssh_options(&conn->args, &conn->env_array, variant, port,\n+\t\t\t version_advert, flags);\n \targv_array_push(&conn->args, ssh_host);\n }\n \n@@ -1226,16 +1226,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tenum protocol protocol;\n-\tenum protocol_version version = get_protocol_version_config();\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\n@@ -1250,7 +1244,8 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n \t} else if (protocol == PROTO_GIT) {\n-\t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n+\t\tconn = git_connect_git(fd, hostandport, path, prog,\n+\t\t\t\t       &version_advert, flags);\n \t} else {\n \t\tstruct strbuf cmd = STRBUF_INIT;\n \t\tconst char *const *var;\n@@ -1293,12 +1288,14 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n-\t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\n+\t\t\tfill_ssh_args(conn, ssh_host, port, &version_advert,\n+\t\t\t\t      flags);\n \t\t} else {\n \t\t\ttransport_check_allowed(\"file\");\n-\t\t\tif (version > 0) {\n-\t\t\t\targv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t\t\t version);\n+\t\t\tif (strcmp(version_advert.buf, \"version=0\")) {\n+\t\t\t\targv_array_pushf(&conn->env_array,\n+\t\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t\t\t version_advert.buf);\n \t\t\t}\n \t\t}\n \t\targv_array_push(&conn->args, cmd.buf);\ndiff --git a/protocol.c b/protocol.c\nindex 5e636785d1..f788269c47 100644\n--- a/protocol.c\n+++ b/protocol.c\n@@ -2,18 +2,43 @@\n #include \"config.h\"\n #include \"protocol.h\"\n \n+static enum protocol_version *allowed_versions;\n+static int nr_allowed_versions;\n+static int alloc_allowed_versions;\n+static int have_advertised_versions_already = 0;\n+\n+static const char protocol_v0_string[] = \"0\";\n+static const char protocol_v1_string[] = \"1\";\n+static const char protocol_v2_string[] = \"2\";\n+\n static enum protocol_version parse_protocol_version(const char *value)\n {\n-\tif (!strcmp(value, \"0\"))\n+\tif (!strcmp(value, protocol_v0_string))\n \t\treturn protocol_v0;\n-\telse if (!strcmp(value, \"1\"))\n+\telse if (!strcmp(value, protocol_v1_string))\n \t\treturn protocol_v1;\n-\telse if (!strcmp(value, \"2\"))\n+\telse if (!strcmp(value, protocol_v2_string))\n \t\treturn protocol_v2;\n \telse\n \t\treturn protocol_unknown_version;\n }\n \n+/* Return the text representation of a wire protocol version. */\n+static const char *format_protocol_version(enum protocol_version version)\n+{\n+\tswitch (version) {\n+\tcase protocol_v0:\n+\t\treturn protocol_v0_string;\n+\tcase protocol_v1:\n+\t\treturn protocol_v1_string;\n+\tcase protocol_v2:\n+\t\treturn protocol_v2_string;\n+\tcase protocol_unknown_version:\n+\t\tdie(_(\"Unrecognized protocol version\"));\n+\t}\n+\tdie(_(\"Unrecognized protocol_version\"));\n+}\n+\n enum protocol_version get_protocol_version_config(void)\n {\n \tconst char *value;\n@@ -30,6 +55,79 @@ enum protocol_version get_protocol_version_config(void)\n \treturn protocol_v0;\n }\n \n+void register_allowed_protocol_version(enum protocol_version version)\n+{\n+\tif (have_advertised_versions_already)\n+\t\terror(_(\"attempting to register an allowed protocol version after advertisement\"));\n+\n+\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n+\t\t   alloc_allowed_versions);\n+\tallowed_versions[nr_allowed_versions++] = version;\n+}\n+\n+void register_allowed_protocol_versions_from_env(void)\n+{\n+\tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n+\tconst char *version_str;\n+\tstruct string_list version_list = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *version;\n+\n+\tif (!git_protocol)\n+\t\treturn;\n+\n+\tstring_list_split(&version_list, git_protocol, ':', -1);\n+\tfor_each_string_list_item(version, &version_list) {\n+\t\tif (skip_prefix(version->string, \"version=\", &version_str))\n+\t\t\tregister_allowed_protocol_version(\n+\t\t\t\tparse_protocol_version(version_str));\n+\t}\n+\tstring_list_clear(&version_list, 0);\n+}\n+\n+void get_client_protocol_version_advertisement(struct strbuf *advert)\n+{\n+\tint tmp_nr = nr_allowed_versions;\n+\tenum protocol_version *tmp_allowed_versions, config_version;\n+\tstrbuf_reset(advert);\n+\n+\thave_advertised_versions_already = 1;\n+\n+\tconfig_version = get_protocol_version_config();\n+\tif (config_version == protocol_v0) {\n+\t\tstrbuf_addstr(advert, \"version=0\");\n+\t\treturn;\n+\t}\n+\n+\tif (tmp_nr > 0) {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n+\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n+\t\t\t   sizeof(enum protocol_version));\n+\t} else {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n+\t\ttmp_nr = 1;\n+\t\ttmp_allowed_versions[0] = config_version;\n+\t}\n+\n+\tif (tmp_allowed_versions[0] != config_version)\n+\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n+\t\t\tif (tmp_allowed_versions[i] == config_version) {\n+\t\t\t\tenum protocol_version swap =\n+\t\t\t\t\ttmp_allowed_versions[0];\n+\t\t\t\ttmp_allowed_versions[0] =\n+\t\t\t\t\ttmp_allowed_versions[i];\n+\t\t\t\ttmp_allowed_versions[i] = swap;\n+\t\t\t}\n+\n+\tstrbuf_addf(advert, \"version=%s\",\n+\t\t    format_protocol_version(tmp_allowed_versions[0]));\n+\tfor (int i = 1; i < tmp_nr; i++)\n+\t\tstrbuf_addf(advert, \":version=%s\",\n+\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n+\n+}\n+\n enum protocol_version determine_protocol_version_server(void)\n {\n \tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n@@ -38,9 +136,10 @@ enum protocol_version determine_protocol_version_server(void)\n \t/*\n \t * Determine which protocol version the client has requested.  Since\n \t * multiple 'version' keys can be sent by the client, indicating that\n-\t * the client is okay to speak any of them, select the greatest version\n-\t * that the client has requested.  This is due to the assumption that\n-\t * the most recent protocol version will be the most state-of-the-art.\n+\t * the client is okay to speak any of them, select the first\n+\t * recognizable version that the client has requested.  This is due to\n+\t * the assumption that the protocol versions will be listed in\n+\t * preference order.\n \t */\n \tif (git_protocol) {\n \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -53,8 +152,10 @@ enum protocol_version determine_protocol_version_server(void)\n \n \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n \t\t\t\tv = parse_protocol_version(value);\n-\t\t\t\tif (v > version)\n+\t\t\t\tif (v != protocol_unknown_version) {\n \t\t\t\t\tversion = v;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \ndiff --git a/protocol.h b/protocol.h\nindex 2ad35e433c..b67b2259de 100644\n--- a/protocol.h\n+++ b/protocol.h\n@@ -16,6 +16,23 @@ enum protocol_version {\n  */\n extern enum protocol_version get_protocol_version_config(void);\n \n+/*\n+ * Register an allowable protocol version for a given operation. Registration\n+ * must occur before attempting to advertise a version to a server process.\n+ */\n+extern void register_allowed_protocol_version(enum protocol_version version);\n+\n+/*\n+ * Register allowable protocol versions from the GIT_PROTOCOL environment var.\n+ */\n+extern void register_allowed_protocol_versions_from_env(void);\n+\n+/*\n+ * Fill a strbuf with a version advertisement string suitable for use in the\n+ * GIT_PROTOCOL environment variable or similar version negotiation field.\n+ */\n+extern void get_client_protocol_version_advertisement(struct strbuf *advert);\n+\n /*\n  * Used by a server to determine which protocol version should be used based on\n  * a client's request, communicated via the 'GIT_PROTOCOL' environment variable\ndiff --git a/remote-curl.c b/remote-curl.c\nindex fb28309e85..537f8c7aac 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -330,6 +330,20 @@ static int get_protocol_http_header(enum protocol_version version,\n \treturn 0;\n }\n \n+static int get_client_protocol_http_header(const struct strbuf *version_advert,\n+\t\t\t\t\t   struct strbuf *header)\n+{\n+\tif (version_advert->len > 0 &&\n+\t    strcmp(version_advert->buf, \"version=0\")) {\n+\t\tstrbuf_addf(header, GIT_PROTOCOL_HEADER \": %s\",\n+\t\t\t    version_advert->buf);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static struct discovery *discover_refs(const char *service, int for_push)\n {\n \tstruct strbuf exp = STRBUF_INIT;\n@@ -339,11 +353,11 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \tstruct strbuf refs_url = STRBUF_INIT;\n \tstruct strbuf effective_url = STRBUF_INIT;\n \tstruct strbuf protocol_header = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct string_list extra_headers = STRING_LIST_INIT_DUP;\n \tstruct discovery *last = last_discovery;\n \tint http_ret, maybe_smart = 0;\n \tstruct http_get_options http_options;\n-\tenum protocol_version version = get_protocol_version_config();\n \n \tif (last && !strcmp(service, last->service))\n \t\treturn last;\n@@ -360,16 +374,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n \t}\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Add the extra Git-Protocol header */\n-\tif (get_protocol_http_header(version, &protocol_header))\n+\tif (get_client_protocol_http_header(&version_advert, &protocol_header))\n \t\tstring_list_append(&extra_headers, protocol_header.buf);\n \n \tmemset(&http_options, 0, sizeof(http_options));\n@@ -1327,6 +1335,8 @@ int cmd_main(int argc, const char **argv)\n \tstruct strbuf buf = STRBUF_INIT;\n \tint nongit;\n \n+\tregister_allowed_protocol_versions_from_env();\n+\n \tsetup_git_directory_gently(&nongit);\n \tif (argc < 2) {\n \t\terror(\"remote-curl: usage: git remote-curl <remote> [<url>]\");\ndiff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh\nindex 7466aad111..d528e91630 100755\n--- a/t/t5570-git-daemon.sh\n+++ b/t/t5570-git-daemon.sh\n@@ -186,7 +186,7 @@ test_expect_success 'hostname cannot break out of directory' '\n test_expect_success 'daemon log records all attributes' '\n \tcat >expect <<-\\EOF &&\n \tExtended attribute \"host\": localhost\n-\tExtended attribute \"protocol\": version=1\n+\tExtended attribute \"protocol\": version=1:version=2:version=0\n \tEOF\n \t>daemon.log &&\n \tGIT_OVERRIDE_VIRTUAL_HOST=localhost \\\ndiff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh\nindex ba86a44eb1..2e56c79233 100755\n--- a/t/t5700-protocol-v1.sh\n+++ b/t/t5700-protocol-v1.sh\n@@ -26,7 +26,7 @@ test_expect_success 'clone with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"clone< version 1\" log\n '\n@@ -42,7 +42,7 @@ test_expect_success 'fetch with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -56,7 +56,7 @@ test_expect_success 'pull with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'push with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"push> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"push> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"push< version 1\" log\n '\ndiff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh\nindex 3beeed4546..78c17c25e4 100755\n--- a/t/t5702-protocol-v2.sh\n+++ b/t/t5702-protocol-v2.sh\n@@ -24,7 +24,7 @@ test_expect_success 'list refs with git:// using protocol v2' '\n \t\tls-remote --symref \"$GIT_DAEMON_URL/parent\" >actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"git> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"git> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"git< version 2\" log &&\n \n@@ -56,7 +56,7 @@ test_expect_success 'clone with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"clone< version 2\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'fetch with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -90,7 +90,7 @@ test_expect_success 'pull with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -476,7 +476,7 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \ttest_when_finished \"rm -f log\" &&\n \t# Till v2 for push is designed, make sure that if a client has\n \t# protocol.version configured to use v2, that the client instead falls\n-\t# back and uses v0.\n+\t# back to previous versions.\n \n \ttest_commit -C http_child three &&\n \n@@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n \ttest_cmp expect actual &&\n \n-\t# Client didnt request to use protocol v2\n-\t! grep \"Git-Protocol: version=2\" log &&\n-\t# Server didnt respond using protocol v2\n-\t! grep \"git< version 2\" log\n+\t# Server responded with version 1\n+\tgrep \"git< version 1\" log\n '\n \n \ndiff --git a/transport-helper.c b/transport-helper.c\nindex 143ca008c8..ac1937f1e1 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -105,6 +105,7 @@ static struct child_process *get_helper(struct transport *transport)\n {\n \tstruct helper_data *data = transport->data;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct child_process *helper;\n \tint duped;\n \tint code;\n@@ -127,6 +128,11 @@ static struct child_process *get_helper(struct transport *transport)\n \t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n \t\t\t\t GIT_DIR_ENVIRONMENT, get_git_dir());\n \n+\tget_client_protocol_version_advertisement(&version_advert);\n+\tif (version_advert.len > 0)\n+\t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n+\t\t\t\t GIT_PROTOCOL_ENVIRONMENT, version_advert.buf);\n+\n \tcode = start_command(helper);\n \tif (code < 0 && errno == ENOENT)\n \t\tdie(_(\"unable to find remote helper for '%s'\"), data->name);\n-- \n2.19.0.605.g01d371f741-goog\n\n"},{"id":"360359","messageId":"CAGZ79kbuVRAceWF5cb4JAk=ss_4sEKthwxMG2wM+gLWbUdcTVQ@mail.gmail.com","threadId":"49473","inReplyTo":"70986cec32880db16568d85c351b33e9a8e16f1c.1539305180.git.steadmon@google.com","subject":"Re: [PATCH v2 1/1] protocol: advertise multiple supported versions","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-10-12T22:30:52Z","receivedAt":"2018-10-12T22:31:08Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"On Thu, Oct 11, 2018 at 6:02 PM <steadmon@google.com> wrote:\n>\n> From: Josh Steadmon <steadmon@google.com>\n>\n> Currently the client advertises that it supports the wire protocol\n> version set in the protocol.version config. However, not all services\n> support the same set of protocol versions. When connecting to\n> git-receive-pack, the client automatically downgrades to v0 if\n> config.protocol is set to v2, but this check is not performed for other\n> services.\n>\n> This patch creates a protocol version registry. Individual commands\n> register all the protocol versions they support prior to communicating\n> with a server. Versions should be listed in preference order; the\n> version specified in protocol.version will automatically be moved to the\n> front of the registry.\n>\n> The protocol version registry is passed to remote helpers via the\n> GIT_PROTOCOL environment variable.\n>\n> Clients now advertise the full list of registered versions. Servers\n> select the first recognized version from this advertisement.\n\nI like this patch from the users point of view (i.e. inside fetch/push etc),\nand I need to through the details in connect/protocol as that seems\nto be a lot of new code, but hides the complexity very well.\n\n> +       register_allowed_protocol_version(protocol_v2);\n> +       register_allowed_protocol_version(protocol_v1);\n> +       register_allowed_protocol_version(protocol_v0);\n\nWould it make sense to have a\n\n    register_allowed_protocol_versions(protocol_v2, protocol_v1,\nprotocol_v0, NULL);\n\nsimilar to argv_array_pushl  or would that be overengineered?\n\n> @@ -1085,10 +1085,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n>                     target_host, 0);\n>\n>         /* If using a new version put that stuff here after a second null byte */\n> -       if (version > 0) {\n> +       if (strcmp(version_advert->buf, \"version=0\")) {\n>                 strbuf_addch(&request, '\\0');\n> -               strbuf_addf(&request, \"version=%d%c\",\n> -                           version, '\\0');\n> +               strbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n\nIt's a bit unfortunate to pass around the string, but reading through\nthe following\nlines seems like it is easiest.\n\n\n> @@ -1226,16 +1226,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n>  {\n>         char *hostandport, *path;\n>         struct child_process *conn;\n> +       struct strbuf version_advert = STRBUF_INIT;\n>         enum protocol protocol;\n> -       enum protocol_version version = get_protocol_version_config();\n>\n> -       /*\n> -        * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n> -        * to perform a push, then fallback to v0 since the client doesn't know\n> -        * how to push yet using v2.\n> -        */\n> -       if (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n> -               version = protocol_v0;\n> +       get_client_protocol_version_advertisement(&version_advert);\n\nNice to have this special casing gone!\n\n> diff --git a/protocol.c b/protocol.c\n> index 5e636785d1..f788269c47 100644\n> +\n> +static const char protocol_v0_string[] = \"0\";\n> +static const char protocol_v1_string[] = \"1\";\n> +static const char protocol_v2_string[] = \"2\";\n> +\n...\n> +/* Return the text representation of a wire protocol version. */\n> +static const char *format_protocol_version(enum protocol_version version)\n> +{\n> +       switch (version) {\n> +       case protocol_v0:\n> +               return protocol_v0_string;\n> +       case protocol_v1:\n> +               return protocol_v1_string;\n> +       case protocol_v2:\n> +               return protocol_v2_string;\n> +       case protocol_unknown_version:\n> +               die(_(\"Unrecognized protocol version\"));\n> +       }\n> +       die(_(\"Unrecognized protocol_version\"));\n> +}\n\nUp to now we have the textual representation that can easily\nbe constructed from protocol_version by using e.g.\n    sprintf(buf, \"%d\", version);\nwhich is why I initially thought this could be worded way\nshorter, but I guess this is more future proof?\n\n\n> +\n>  enum protocol_version get_protocol_version_config(void)\n>  {\n>         const char *value;\n> @@ -30,6 +55,79 @@ enum protocol_version get_protocol_version_config(void)\n>         return protocol_v0;\n>  }\n>\n> +void register_allowed_protocol_version(enum protocol_version version)\n> +{\n> +       if (have_advertised_versions_already)\n> +               error(_(\"attempting to register an allowed protocol version after advertisement\"));\n\nThis would be a\n    BUG(...)\ninstead?\n"},{"id":"360362","messageId":"20181012233017.GD52080@aiede.svl.corp.google.com","threadId":"49473","inReplyTo":"20181010235303.GB12177@google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2018-10-12T23:30:17Z","receivedAt":"2018-10-12T23:32:12Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Hi,\n\nJosh Steadmon wrote:\n\n> So this runs into problems with remote-curl (and possibly other remote\n> helpers):\n>\n> builtin/push.c can declare whatever allowed versions it wants, but those\n> are not carried over when remote-curl is started to actually talk to the\n> remote. What's worse, remote-curl starts its HTTP connection before it\n> knows what command it's actually acting as a helper for.\n>\n> For now, I'm going to try adding an --allowed_versions flag for the\n> remote helpers, but if anyone has a better idea, let me know.\n\nThere are some external remote helpers (see \"git help remote-helpers\"\nfor the documented interface), so alas, they can't take new flags.\n\nThat said, you can add a new remote helper capability and then\ncommunicate on stdin, or in a pinch you can use the existing \"option\"\ncapability.  Remote helpers are also allowed to query \"git config\" if\nthey want to (either using the config machinery in config.h or by\nrunning \"git config\").\n\nThanks,\nJonathan\n"},{"id":"360363","messageId":"20181012233207.GE52080@aiede.svl.corp.google.com","threadId":"49473","inReplyTo":"20181010235303.GB12177@google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2018-10-12T23:32:07Z","receivedAt":"2018-10-12T23:32:12Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Josh Steadmon wrote:\n\n> For now, I'm going to try adding an --allowed_versions flag for the\n> remote helpers, but if anyone has a better idea, let me know.\n\nI forgot to mention: the stateless-connect remote helper capability is\nstill experimental so you're free to change its interface (e.g. to\nchange the syntax of the stateless-connect command it provides).\n\nThanks again,\nJonathan\n"},{"id":"360365","messageId":"20181012234504.GC12177@google.com","threadId":"49473","inReplyTo":"20181012233207.GE52080@aiede.svl.corp.google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-12T23:45:04Z","receivedAt":"2018-10-12T23:45:13Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.10.12 16:32, Jonathan Nieder wrote:\n> Josh Steadmon wrote:\n> \n> > For now, I'm going to try adding an --allowed_versions flag for the\n> > remote helpers, but if anyone has a better idea, let me know.\n> \n> I forgot to mention: the stateless-connect remote helper capability is\n> still experimental so you're free to change its interface (e.g. to\n> change the syntax of the stateless-connect command it provides).\n\nFor v2 of this patch, I ended up using GIT_PROTOCOL to pass the version\nstring to the remote helpers.\n"},{"id":"360366","messageId":"20181012235333.GF52080@aiede.svl.corp.google.com","threadId":"49473","inReplyTo":"20181012234504.GC12177@google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2018-10-12T23:53:33Z","receivedAt":"2018-10-12T23:57:04Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Josh Steadmon wrote:\n> On 2018.10.12 16:32, Jonathan Nieder wrote:\n>> Josh Steadmon wrote:\n\n>>> For now, I'm going to try adding an --allowed_versions flag for the\n>>> remote helpers, but if anyone has a better idea, let me know.\n>>\n>> I forgot to mention: the stateless-connect remote helper capability is\n>> still experimental so you're free to change its interface (e.g. to\n>> change the syntax of the stateless-connect command it provides).\n>\n> For v2 of this patch, I ended up using GIT_PROTOCOL to pass the version\n> string to the remote helpers.\n\nMakes sense.  Thanks. :)\n"},{"id":"360381","messageId":"xmqq4ldq9t2r.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"20181012235333.GF52080@aiede.svl.corp.google.com","subject":"Re: [PATCH 1/1] protocol: limit max protocol version per service","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-10-13T07:58:04Z","receivedAt":"2018-10-13T07:58:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonathan Nieder <jrnieder@gmail.com> writes:\n\n> Josh Steadmon wrote:\n>> On 2018.10.12 16:32, Jonathan Nieder wrote:\n>>> Josh Steadmon wrote:\n>\n>>>> For now, I'm going to try adding an --allowed_versions flag for the\n>>>> remote helpers, but if anyone has a better idea, let me know.\n>>>\n>>> I forgot to mention: the stateless-connect remote helper capability is\n>>> still experimental so you're free to change its interface (e.g. to\n>>> change the syntax of the stateless-connect command it provides).\n>>\n>> For v2 of this patch, I ended up using GIT_PROTOCOL to pass the version\n>> string to the remote helpers.\n>\n> Makes sense.  Thanks. :)\n\nYeah, it does.\n"},{"id":"361226","messageId":"20181022225559.GD233961@google.com","threadId":"49473","inReplyTo":"CAGZ79kbuVRAceWF5cb4JAk=ss_4sEKthwxMG2wM+gLWbUdcTVQ@mail.gmail.com","subject":"Re: [PATCH v2 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-10-22T22:55:59Z","receivedAt":"2018-10-22T22:56:08Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.10.12 15:30, Stefan Beller wrote:\n> On Thu, Oct 11, 2018 at 6:02 PM <steadmon@google.com> wrote:\n> >\n> > From: Josh Steadmon <steadmon@google.com>\n> >\n> > Currently the client advertises that it supports the wire protocol\n> > version set in the protocol.version config. However, not all services\n> > support the same set of protocol versions. When connecting to\n> > git-receive-pack, the client automatically downgrades to v0 if\n> > config.protocol is set to v2, but this check is not performed for other\n> > services.\n> >\n> > This patch creates a protocol version registry. Individual commands\n> > register all the protocol versions they support prior to communicating\n> > with a server. Versions should be listed in preference order; the\n> > version specified in protocol.version will automatically be moved to the\n> > front of the registry.\n> >\n> > The protocol version registry is passed to remote helpers via the\n> > GIT_PROTOCOL environment variable.\n> >\n> > Clients now advertise the full list of registered versions. Servers\n> > select the first recognized version from this advertisement.\n> \n> I like this patch from the users point of view (i.e. inside fetch/push etc),\n> and I need to through the details in connect/protocol as that seems\n> to be a lot of new code, but hides the complexity very well.\n> \n> > +       register_allowed_protocol_version(protocol_v2);\n> > +       register_allowed_protocol_version(protocol_v1);\n> > +       register_allowed_protocol_version(protocol_v0);\n> \n> Would it make sense to have a\n> \n>     register_allowed_protocol_versions(protocol_v2, protocol_v1,\n> protocol_v0, NULL);\n> \n> similar to argv_array_pushl  or would that be overengineered?\n\nHmm, well it wouldn't be quite as clean as the argv_* versions, since we\ncan't take the pointer of an enum value, so we don't have a good\nstop-value for the va_list. I suppose we could use\nprotocol_unknown_version, but that seems unintuitive to me. We could\nalso pass in an explicit argument count, but... ugh.\n\nAm I missing some other way to do this cleanly? I'll admit I'm not very\nfamiliar with va_lists.\n\n> > @@ -1085,10 +1085,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n> >                     target_host, 0);\n> >\n> >         /* If using a new version put that stuff here after a second null byte */\n> > -       if (version > 0) {\n> > +       if (strcmp(version_advert->buf, \"version=0\")) {\n> >                 strbuf_addch(&request, '\\0');\n> > -               strbuf_addf(&request, \"version=%d%c\",\n> > -                           version, '\\0');\n> > +               strbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n> \n> It's a bit unfortunate to pass around the string, but reading through\n> the following\n> lines seems like it is easiest.\n> \n> \n> > @@ -1226,16 +1226,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n> >  {\n> >         char *hostandport, *path;\n> >         struct child_process *conn;\n> > +       struct strbuf version_advert = STRBUF_INIT;\n> >         enum protocol protocol;\n> > -       enum protocol_version version = get_protocol_version_config();\n> >\n> > -       /*\n> > -        * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n> > -        * to perform a push, then fallback to v0 since the client doesn't know\n> > -        * how to push yet using v2.\n> > -        */\n> > -       if (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n> > -               version = protocol_v0;\n> > +       get_client_protocol_version_advertisement(&version_advert);\n> \n> Nice to have this special casing gone!\n> \n> > diff --git a/protocol.c b/protocol.c\n> > index 5e636785d1..f788269c47 100644\n> > +\n> > +static const char protocol_v0_string[] = \"0\";\n> > +static const char protocol_v1_string[] = \"1\";\n> > +static const char protocol_v2_string[] = \"2\";\n> > +\n> ...\n> > +/* Return the text representation of a wire protocol version. */\n> > +static const char *format_protocol_version(enum protocol_version version)\n> > +{\n> > +       switch (version) {\n> > +       case protocol_v0:\n> > +               return protocol_v0_string;\n> > +       case protocol_v1:\n> > +               return protocol_v1_string;\n> > +       case protocol_v2:\n> > +               return protocol_v2_string;\n> > +       case protocol_unknown_version:\n> > +               die(_(\"Unrecognized protocol version\"));\n> > +       }\n> > +       die(_(\"Unrecognized protocol_version\"));\n> > +}\n> \n> Up to now we have the textual representation that can easily\n> be constructed from protocol_version by using e.g.\n>     sprintf(buf, \"%d\", version);\n> which is why I initially thought this could be worded way\n> shorter, but I guess this is more future proof?\n\nYeah, my understanding is that we don't want to assume that version\nidentifiers will always be simple integers. We could get away with\nsprintf() for now, but I figured I didn't want to cause future pain\nif/when the version identifiers become complex.\n\n> > +\n> >  enum protocol_version get_protocol_version_config(void)\n> >  {\n> >         const char *value;\n> > @@ -30,6 +55,79 @@ enum protocol_version get_protocol_version_config(void)\n> >         return protocol_v0;\n> >  }\n> >\n> > +void register_allowed_protocol_version(enum protocol_version version)\n> > +{\n> > +       if (have_advertised_versions_already)\n> > +               error(_(\"attempting to register an allowed protocol version after advertisement\"));\n> \n> This would be a\n>     BUG(...)\n> instead?\n\nAck, will fix in the next version.\n"},{"id":"361237","messageId":"CAGZ79kbB6F9Bqx7d=9zAKSt=3Yxpm+nj=jgUA=A2P6X8v0ywMg@mail.gmail.com","threadId":"49473","inReplyTo":"20181022225559.GD233961@google.com","subject":"Re: [PATCH v2 1/1] protocol: advertise multiple supported versions","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-10-23T00:37:36Z","receivedAt":"2018-10-23T00:37:51Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"> > similar to argv_array_pushl  or would that be overengineered?\n\n> Am I missing some other way to do this cleanly? I'll admit I'm not very\n> familiar with va_lists.\n\nAh, you're right. By not passing pointers (and I am unfamiliar\nwith va_lists, too), this was a moot suggestion.\n>\n> Yeah, my understanding is that we don't want to assume that version\n> identifiers will always be simple integers. We could get away with\n> sprintf() for now, but I figured I didn't want to cause future pain\n> if/when the version identifiers become complex.\n\nMakes sense.\n\nStefan\n"},{"id":"363086","messageId":"cover.1542059029.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1539305180.git.steadmon@google.com","subject":"[PATCH v3 0/1] Advertise multiple supported proto versions","fromName":"","fromEmail":"steadmon@google.com","sentAt":"2018-11-12T21:49:04Z","receivedAt":"2018-11-12T21:49:09Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"This is a minor iteration on v2, to change an error message to a BUG.\n\nJosh Steadmon (1):\n  protocol: advertise multiple supported versions\n\n builtin/archive.c      |   3 ++\n builtin/clone.c        |   4 ++\n builtin/fetch-pack.c   |   4 ++\n builtin/fetch.c        |   5 ++\n builtin/ls-remote.c    |   5 ++\n builtin/pull.c         |   5 ++\n builtin/push.c         |   4 ++\n builtin/send-pack.c    |   3 ++\n connect.c              |  47 ++++++++---------\n protocol.c             | 112 ++++++++++++++++++++++++++++++++++++++---\n protocol.h             |  17 +++++++\n remote-curl.c          |  28 +++++++----\n t/t5570-git-daemon.sh  |   2 +-\n t/t5700-protocol-v1.sh |   8 +--\n t/t5702-protocol-v2.sh |  16 +++---\n transport-helper.c     |   6 +++\n 16 files changed, 214 insertions(+), 55 deletions(-)\n\nRange-diff against v2:\n1:  70986cec32 ! 1:  b9968e3fb0 protocol: advertise multiple supported versions\n    @@ -358,7 +358,7 @@\n     +void register_allowed_protocol_version(enum protocol_version version)\n     +{\n     +\tif (have_advertised_versions_already)\n    -+\t\terror(_(\"attempting to register an allowed protocol version after advertisement\"));\n    ++\t\tBUG(_(\"attempting to register an allowed protocol version after advertisement\"));\n     +\n     +\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n     +\t\t   alloc_allowed_versions);\n    @@ -395,7 +395,6 @@\n     +\tconfig_version = get_protocol_version_config();\n     +\tif (config_version == protocol_v0) {\n     +\t\tstrbuf_addstr(advert, \"version=0\");\n    -+\t\ttrace_printf(\"Client advert string: %s\", advert->buf);\n     +\t\treturn;\n     +\t}\n     +\n    @@ -424,8 +423,6 @@\n     +\tfor (int i = 1; i < tmp_nr; i++)\n     +\t\tstrbuf_addf(advert, \":version=%s\",\n     +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n    -+\n    -+\ttrace_printf(\"Client advert string: %s\", advert->buf);\n     +}\n     +\n      enum protocol_version determine_protocol_version_server(void)\n-- \n2.19.1.930.g4563a0d9d0-goog\n\n"},{"id":"363087","messageId":"b9968e3fb005a00e9f78627467b4fc86a6259977.1542059029.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1542059029.git.steadmon@google.com","subject":"[PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"","fromEmail":"steadmon@google.com","sentAt":"2018-11-12T21:49:05Z","receivedAt":"2018-11-12T21:49:13Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Currently the client advertises that it supports the wire protocol\nversion set in the protocol.version config. However, not all services\nsupport the same set of protocol versions. When connecting to\ngit-receive-pack, the client automatically downgrades to v0 if\nconfig.protocol is set to v2, but this check is not performed for other\nservices.\n\nThis patch creates a protocol version registry. Individual operations\nregister all the protocol versions they support prior to communicating\nwith a server. Versions should be listed in preference order; the\nversion specified in protocol.version will automatically be moved to the\nfront of the registry.\n\nThe protocol version registry is passed to remote helpers via the\nGIT_PROTOCOL environment variable.\n\nClients now advertise the full list of registered versions. Servers\nselect the first recognized version from this advertisement.\n\nSigned-off-by: Josh Steadmon <steadmon@google.com>\n---\n builtin/archive.c      |   3 ++\n builtin/clone.c        |   4 ++\n builtin/fetch-pack.c   |   4 ++\n builtin/fetch.c        |   5 ++\n builtin/ls-remote.c    |   5 ++\n builtin/pull.c         |   5 ++\n builtin/push.c         |   4 ++\n builtin/send-pack.c    |   3 ++\n connect.c              |  47 ++++++++---------\n protocol.c             | 112 ++++++++++++++++++++++++++++++++++++++---\n protocol.h             |  17 +++++++\n remote-curl.c          |  28 +++++++----\n t/t5570-git-daemon.sh  |   2 +-\n t/t5700-protocol-v1.sh |   8 +--\n t/t5702-protocol-v2.sh |  16 +++---\n transport-helper.c     |   6 +++\n 16 files changed, 214 insertions(+), 55 deletions(-)\n\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex e74f675390..8adb9f381b 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -8,6 +8,7 @@\n #include \"transport.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n \n static void create_output_file(const char *output_file)\n@@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, local_opts, NULL,\n \t\t\t     PARSE_OPT_KEEP_ALL);\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd2c3ef090..1651a950b6 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -900,6 +900,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tstruct argv_array ref_prefixes = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tfetch_if_missing = 0;\n \n \tpacket_trace_identity(\"clone\");\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex 1a1bc63566..cba935e4d3 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -57,6 +57,10 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)\n \n \tfetch_if_missing = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch-pack\");\n \n \tmemset(&args, 0, sizeof(args));\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 61bec5d213..2a20cf8bfd 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -21,6 +21,7 @@\n #include \"argv-array.h\"\n #include \"utf8.h\"\n #include \"packfile.h\"\n+#include \"protocol.h\"\n #include \"list-objects-filter-options.h\"\n \n static const char * const builtin_fetch_usage[] = {\n@@ -1476,6 +1477,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tint prune_tags_ok = 1;\n \tstruct argv_array argv_gc_auto = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch\");\n \n \tfetch_if_missing = 0;\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 1a25df7ee1..ea685e8bb9 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -1,5 +1,6 @@\n #include \"builtin.h\"\n #include \"cache.h\"\n+#include \"protocol.h\"\n #include \"transport.h\"\n #include \"ref-filter.h\"\n #include \"remote.h\"\n@@ -80,6 +81,10 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)\n \n \tmemset(&ref_array, 0, sizeof(ref_array));\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, ls_remote_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tdest = argv[0];\ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex 681c127a07..cb64146834 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"builtin.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"exec-cmd.h\"\n #include \"run-command.h\"\n #include \"sha1-array.h\"\n@@ -849,6 +850,10 @@ int cmd_pull(int argc, const char **argv, const char *prefix)\n \tstruct object_id rebase_fork_point;\n \tint autostash;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tif (!getenv(\"GIT_REFLOG_ACTION\"))\n \t\tset_reflog_message(argc, argv);\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex d09a42062c..10d8abe829 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -10,6 +10,7 @@\n #include \"remote.h\"\n #include \"transport.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"submodule.h\"\n #include \"submodule-config.h\"\n #include \"send-pack.h\"\n@@ -587,6 +588,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"push\");\n \tgit_config(git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8e3c7490f7..f48bd1306b 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -184,6 +184,9 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tgit_config(send_pack_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);\n \tif (argc > 0) {\ndiff --git a/connect.c b/connect.c\nindex 94547e5056..a170ba1e90 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n  */\n static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t\t\t\t     const char *path, const char *prog,\n-\t\t\t\t\t     enum protocol_version version,\n+\t\t\t\t\t     const struct strbuf *version_advert,\n \t\t\t\t\t     int flags)\n {\n \tstruct child_process *conn;\n@@ -1085,10 +1085,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t    target_host, 0);\n \n \t/* If using a new version put that stuff here after a second null byte */\n-\tif (version > 0) {\n+\tif (strcmp(version_advert->buf, \"version=0\")) {\n \t\tstrbuf_addch(&request, '\\0');\n-\t\tstrbuf_addf(&request, \"version=%d%c\",\n-\t\t\t    version, '\\0');\n+\t\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n \t}\n \n \tpacket_write(fd[1], request.buf, request.len);\n@@ -1104,14 +1103,14 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n  */\n static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \t\t\t     enum ssh_variant variant, const char *port,\n-\t\t\t     enum protocol_version version, int flags)\n+\t\t\t     const struct strbuf *version_advert, int flags)\n {\n \tif (variant == VARIANT_SSH &&\n-\t    version > 0) {\n+\t    strcmp(version_advert->buf, \"version=0\")) {\n \t\targv_array_push(args, \"-o\");\n \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n-\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t version);\n+\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t version_advert->buf);\n \t}\n \n \tif (flags & CONNECT_IPV4) {\n@@ -1164,7 +1163,7 @@ static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \n /* Prepare a child_process for use by Git's SSH-tunneled transport. */\n static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n-\t\t\t  const char *port, enum protocol_version version,\n+\t\t\t  const char *port, const struct strbuf *version_advert,\n \t\t\t  int flags)\n {\n \tconst char *ssh;\n@@ -1198,15 +1197,16 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \n \t\targv_array_push(&detect.args, ssh);\n \t\targv_array_push(&detect.args, \"-G\");\n-\t\tpush_ssh_options(&detect.args, &detect.env_array,\n-\t\t\t\t VARIANT_SSH, port, version, flags);\n+\t\tpush_ssh_options(&detect.args, &detect.env_array, VARIANT_SSH,\n+\t\t\t\t port, version_advert, flags);\n \t\targv_array_push(&detect.args, ssh_host);\n \n \t\tvariant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;\n \t}\n \n \targv_array_push(&conn->args, ssh);\n-\tpush_ssh_options(&conn->args, &conn->env_array, variant, port, version, flags);\n+\tpush_ssh_options(&conn->args, &conn->env_array, variant, port,\n+\t\t\t version_advert, flags);\n \targv_array_push(&conn->args, ssh_host);\n }\n \n@@ -1226,16 +1226,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tenum protocol protocol;\n-\tenum protocol_version version = get_protocol_version_config();\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\n@@ -1250,7 +1244,8 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n \t} else if (protocol == PROTO_GIT) {\n-\t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n+\t\tconn = git_connect_git(fd, hostandport, path, prog,\n+\t\t\t\t       &version_advert, flags);\n \t} else {\n \t\tstruct strbuf cmd = STRBUF_INIT;\n \t\tconst char *const *var;\n@@ -1293,12 +1288,14 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n-\t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\n+\t\t\tfill_ssh_args(conn, ssh_host, port, &version_advert,\n+\t\t\t\t      flags);\n \t\t} else {\n \t\t\ttransport_check_allowed(\"file\");\n-\t\t\tif (version > 0) {\n-\t\t\t\targv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t\t\t version);\n+\t\t\tif (strcmp(version_advert.buf, \"version=0\")) {\n+\t\t\t\targv_array_pushf(&conn->env_array,\n+\t\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t\t\t version_advert.buf);\n \t\t\t}\n \t\t}\n \t\targv_array_push(&conn->args, cmd.buf);\ndiff --git a/protocol.c b/protocol.c\nindex 5e636785d1..54d2ab991b 100644\n--- a/protocol.c\n+++ b/protocol.c\n@@ -2,18 +2,43 @@\n #include \"config.h\"\n #include \"protocol.h\"\n \n+static enum protocol_version *allowed_versions;\n+static int nr_allowed_versions;\n+static int alloc_allowed_versions;\n+static int have_advertised_versions_already = 0;\n+\n+static const char protocol_v0_string[] = \"0\";\n+static const char protocol_v1_string[] = \"1\";\n+static const char protocol_v2_string[] = \"2\";\n+\n static enum protocol_version parse_protocol_version(const char *value)\n {\n-\tif (!strcmp(value, \"0\"))\n+\tif (!strcmp(value, protocol_v0_string))\n \t\treturn protocol_v0;\n-\telse if (!strcmp(value, \"1\"))\n+\telse if (!strcmp(value, protocol_v1_string))\n \t\treturn protocol_v1;\n-\telse if (!strcmp(value, \"2\"))\n+\telse if (!strcmp(value, protocol_v2_string))\n \t\treturn protocol_v2;\n \telse\n \t\treturn protocol_unknown_version;\n }\n \n+/* Return the text representation of a wire protocol version. */\n+static const char *format_protocol_version(enum protocol_version version)\n+{\n+\tswitch (version) {\n+\tcase protocol_v0:\n+\t\treturn protocol_v0_string;\n+\tcase protocol_v1:\n+\t\treturn protocol_v1_string;\n+\tcase protocol_v2:\n+\t\treturn protocol_v2_string;\n+\tcase protocol_unknown_version:\n+\t\tdie(_(\"Unrecognized protocol version\"));\n+\t}\n+\tdie(_(\"Unrecognized protocol_version\"));\n+}\n+\n enum protocol_version get_protocol_version_config(void)\n {\n \tconst char *value;\n@@ -30,6 +55,76 @@ enum protocol_version get_protocol_version_config(void)\n \treturn protocol_v0;\n }\n \n+void register_allowed_protocol_version(enum protocol_version version)\n+{\n+\tif (have_advertised_versions_already)\n+\t\tBUG(_(\"attempting to register an allowed protocol version after advertisement\"));\n+\n+\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n+\t\t   alloc_allowed_versions);\n+\tallowed_versions[nr_allowed_versions++] = version;\n+}\n+\n+void register_allowed_protocol_versions_from_env(void)\n+{\n+\tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n+\tconst char *version_str;\n+\tstruct string_list version_list = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *version;\n+\n+\tif (!git_protocol)\n+\t\treturn;\n+\n+\tstring_list_split(&version_list, git_protocol, ':', -1);\n+\tfor_each_string_list_item(version, &version_list) {\n+\t\tif (skip_prefix(version->string, \"version=\", &version_str))\n+\t\t\tregister_allowed_protocol_version(\n+\t\t\t\tparse_protocol_version(version_str));\n+\t}\n+\tstring_list_clear(&version_list, 0);\n+}\n+\n+void get_client_protocol_version_advertisement(struct strbuf *advert)\n+{\n+\tint tmp_nr = nr_allowed_versions;\n+\tenum protocol_version *tmp_allowed_versions, config_version;\n+\tstrbuf_reset(advert);\n+\n+\thave_advertised_versions_already = 1;\n+\n+\tconfig_version = get_protocol_version_config();\n+\tif (config_version == protocol_v0) {\n+\t\tstrbuf_addstr(advert, \"version=0\");\n+\t\treturn;\n+\t}\n+\n+\tif (tmp_nr > 0) {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n+\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n+\t\t\t   sizeof(enum protocol_version));\n+\t} else {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n+\t\ttmp_nr = 1;\n+\t\ttmp_allowed_versions[0] = config_version;\n+\t}\n+\n+\tif (tmp_allowed_versions[0] != config_version)\n+\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n+\t\t\tif (tmp_allowed_versions[i] == config_version) {\n+\t\t\t\tenum protocol_version swap =\n+\t\t\t\t\ttmp_allowed_versions[0];\n+\t\t\t\ttmp_allowed_versions[0] =\n+\t\t\t\t\ttmp_allowed_versions[i];\n+\t\t\t\ttmp_allowed_versions[i] = swap;\n+\t\t\t}\n+\n+\tstrbuf_addf(advert, \"version=%s\",\n+\t\t    format_protocol_version(tmp_allowed_versions[0]));\n+\tfor (int i = 1; i < tmp_nr; i++)\n+\t\tstrbuf_addf(advert, \":version=%s\",\n+\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n+}\n+\n enum protocol_version determine_protocol_version_server(void)\n {\n \tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n@@ -38,9 +133,10 @@ enum protocol_version determine_protocol_version_server(void)\n \t/*\n \t * Determine which protocol version the client has requested.  Since\n \t * multiple 'version' keys can be sent by the client, indicating that\n-\t * the client is okay to speak any of them, select the greatest version\n-\t * that the client has requested.  This is due to the assumption that\n-\t * the most recent protocol version will be the most state-of-the-art.\n+\t * the client is okay to speak any of them, select the first\n+\t * recognizable version that the client has requested.  This is due to\n+\t * the assumption that the protocol versions will be listed in\n+\t * preference order.\n \t */\n \tif (git_protocol) {\n \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -53,8 +149,10 @@ enum protocol_version determine_protocol_version_server(void)\n \n \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n \t\t\t\tv = parse_protocol_version(value);\n-\t\t\t\tif (v > version)\n+\t\t\t\tif (v != protocol_unknown_version) {\n \t\t\t\t\tversion = v;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \ndiff --git a/protocol.h b/protocol.h\nindex 2ad35e433c..b67b2259de 100644\n--- a/protocol.h\n+++ b/protocol.h\n@@ -16,6 +16,23 @@ enum protocol_version {\n  */\n extern enum protocol_version get_protocol_version_config(void);\n \n+/*\n+ * Register an allowable protocol version for a given operation. Registration\n+ * must occur before attempting to advertise a version to a server process.\n+ */\n+extern void register_allowed_protocol_version(enum protocol_version version);\n+\n+/*\n+ * Register allowable protocol versions from the GIT_PROTOCOL environment var.\n+ */\n+extern void register_allowed_protocol_versions_from_env(void);\n+\n+/*\n+ * Fill a strbuf with a version advertisement string suitable for use in the\n+ * GIT_PROTOCOL environment variable or similar version negotiation field.\n+ */\n+extern void get_client_protocol_version_advertisement(struct strbuf *advert);\n+\n /*\n  * Used by a server to determine which protocol version should be used based on\n  * a client's request, communicated via the 'GIT_PROTOCOL' environment variable\ndiff --git a/remote-curl.c b/remote-curl.c\nindex fb28309e85..537f8c7aac 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -330,6 +330,20 @@ static int get_protocol_http_header(enum protocol_version version,\n \treturn 0;\n }\n \n+static int get_client_protocol_http_header(const struct strbuf *version_advert,\n+\t\t\t\t\t   struct strbuf *header)\n+{\n+\tif (version_advert->len > 0 &&\n+\t    strcmp(version_advert->buf, \"version=0\")) {\n+\t\tstrbuf_addf(header, GIT_PROTOCOL_HEADER \": %s\",\n+\t\t\t    version_advert->buf);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static struct discovery *discover_refs(const char *service, int for_push)\n {\n \tstruct strbuf exp = STRBUF_INIT;\n@@ -339,11 +353,11 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \tstruct strbuf refs_url = STRBUF_INIT;\n \tstruct strbuf effective_url = STRBUF_INIT;\n \tstruct strbuf protocol_header = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct string_list extra_headers = STRING_LIST_INIT_DUP;\n \tstruct discovery *last = last_discovery;\n \tint http_ret, maybe_smart = 0;\n \tstruct http_get_options http_options;\n-\tenum protocol_version version = get_protocol_version_config();\n \n \tif (last && !strcmp(service, last->service))\n \t\treturn last;\n@@ -360,16 +374,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n \t}\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Add the extra Git-Protocol header */\n-\tif (get_protocol_http_header(version, &protocol_header))\n+\tif (get_client_protocol_http_header(&version_advert, &protocol_header))\n \t\tstring_list_append(&extra_headers, protocol_header.buf);\n \n \tmemset(&http_options, 0, sizeof(http_options));\n@@ -1327,6 +1335,8 @@ int cmd_main(int argc, const char **argv)\n \tstruct strbuf buf = STRBUF_INIT;\n \tint nongit;\n \n+\tregister_allowed_protocol_versions_from_env();\n+\n \tsetup_git_directory_gently(&nongit);\n \tif (argc < 2) {\n \t\terror(\"remote-curl: usage: git remote-curl <remote> [<url>]\");\ndiff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh\nindex 7466aad111..d528e91630 100755\n--- a/t/t5570-git-daemon.sh\n+++ b/t/t5570-git-daemon.sh\n@@ -186,7 +186,7 @@ test_expect_success 'hostname cannot break out of directory' '\n test_expect_success 'daemon log records all attributes' '\n \tcat >expect <<-\\EOF &&\n \tExtended attribute \"host\": localhost\n-\tExtended attribute \"protocol\": version=1\n+\tExtended attribute \"protocol\": version=1:version=2:version=0\n \tEOF\n \t>daemon.log &&\n \tGIT_OVERRIDE_VIRTUAL_HOST=localhost \\\ndiff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh\nindex ba86a44eb1..2e56c79233 100755\n--- a/t/t5700-protocol-v1.sh\n+++ b/t/t5700-protocol-v1.sh\n@@ -26,7 +26,7 @@ test_expect_success 'clone with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"clone< version 1\" log\n '\n@@ -42,7 +42,7 @@ test_expect_success 'fetch with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -56,7 +56,7 @@ test_expect_success 'pull with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'push with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"push> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"push> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"push< version 1\" log\n '\ndiff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh\nindex 3beeed4546..78c17c25e4 100755\n--- a/t/t5702-protocol-v2.sh\n+++ b/t/t5702-protocol-v2.sh\n@@ -24,7 +24,7 @@ test_expect_success 'list refs with git:// using protocol v2' '\n \t\tls-remote --symref \"$GIT_DAEMON_URL/parent\" >actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"git> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"git> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"git< version 2\" log &&\n \n@@ -56,7 +56,7 @@ test_expect_success 'clone with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"clone< version 2\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'fetch with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -90,7 +90,7 @@ test_expect_success 'pull with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -476,7 +476,7 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \ttest_when_finished \"rm -f log\" &&\n \t# Till v2 for push is designed, make sure that if a client has\n \t# protocol.version configured to use v2, that the client instead falls\n-\t# back and uses v0.\n+\t# back to previous versions.\n \n \ttest_commit -C http_child three &&\n \n@@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n \ttest_cmp expect actual &&\n \n-\t# Client didnt request to use protocol v2\n-\t! grep \"Git-Protocol: version=2\" log &&\n-\t# Server didnt respond using protocol v2\n-\t! grep \"git< version 2\" log\n+\t# Server responded with version 1\n+\tgrep \"git< version 1\" log\n '\n \n \ndiff --git a/transport-helper.c b/transport-helper.c\nindex 143ca008c8..ac1937f1e1 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -105,6 +105,7 @@ static struct child_process *get_helper(struct transport *transport)\n {\n \tstruct helper_data *data = transport->data;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct child_process *helper;\n \tint duped;\n \tint code;\n@@ -127,6 +128,11 @@ static struct child_process *get_helper(struct transport *transport)\n \t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n \t\t\t\t GIT_DIR_ENVIRONMENT, get_git_dir());\n \n+\tget_client_protocol_version_advertisement(&version_advert);\n+\tif (version_advert.len > 0)\n+\t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n+\t\t\t\t GIT_PROTOCOL_ENVIRONMENT, version_advert.buf);\n+\n \tcode = start_command(helper);\n \tif (code < 0 && errno == ENOENT)\n \t\tdie(_(\"unable to find remote helper for '%s'\"), data->name);\n-- \n2.19.1.930.g4563a0d9d0-goog\n\n"},{"id":"363091","messageId":"CAGZ79kaPjw3UbYY=XL0gyKp2VxghwaUOJJoDhiO5sQpT8OWerQ@mail.gmail.com","threadId":"49473","inReplyTo":"b9968e3fb005a00e9f78627467b4fc86a6259977.1542059029.git.steadmon@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-11-12T22:33:57Z","receivedAt":"2018-11-12T22:34:12Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"On Mon, Nov 12, 2018 at 1:49 PM <steadmon@google.com> wrote:\n>\n> Currently the client advertises that it supports the wire protocol\n> version set in the protocol.version config. However, not all services\n> support the same set of protocol versions. When connecting to\n> git-receive-pack, the client automatically downgrades to v0 if\n> config.protocol is set to v2, but this check is not performed for other\n> services.\n>\n> This patch creates a protocol version registry. Individual operations\n> register all the protocol versions they support prior to communicating\n> with a server. Versions should be listed in preference order; the\n> version specified in protocol.version will automatically be moved to the\n> front of the registry.\n>\n> The protocol version registry is passed to remote helpers via the\n> GIT_PROTOCOL environment variable.\n>\n> Clients now advertise the full list of registered versions. Servers\n> select the first recognized version from this advertisement.\n>\n> Signed-off-by: Josh Steadmon <steadmon@google.com>\n\nThanks for resending this patch!\n\n+cc Jonathan Tan, who works a lot on the protocol side of things.\n\n> +void register_allowed_protocol_version(enum protocol_version version)\n> +{\n> +       if (have_advertised_versions_already)\n> +               BUG(_(\"attempting to register an allowed protocol version after advertisement\"));\n\nIf this is a real BUG (due to wrong program flow) instead of bad user input,\nwe would not want to burden the translators with this message.\n\nIf it is a message that the user is intended to see upon bad input\nwe'd rather go with die(_(\"translatable text here\"));\n\n\n> diff --git a/protocol.h b/protocol.h\n> index 2ad35e433c..b67b2259de 100644\n> --- a/protocol.h\n> +++ b/protocol.h\n> @@ -16,6 +16,23 @@ enum protocol_version {\n>   */\n>  extern enum protocol_version get_protocol_version_config(void);\n>\n> +/*\n> + * Register an allowable protocol version for a given operation. Registration\n> + * must occur before attempting to advertise a version to a server process.\n> + */\n> +extern void register_allowed_protocol_version(enum protocol_version version);\n\nWe keep extern here as to imitate the file-local style, although\nDocumentation/CodingGuidelines would prefer to not have the extern keywords.\nOkay.\n\nBonus points for converting the file to omit all extern keywords in a\nresend. :-)\n(I think there is no other series currently in flight touching this header file,\nso it is safe to convert it \"while at it\", `git log --grep \"while at\nit\"` is shorter\nthan expected.)\n\nAll that said, it's only nits that I contributed to this code review;\nthe code/design\nlooks good to me, and if I were maintainer I'd include it as-is, as it\nfixes a long(ish)\nstanding protocol error.\n\nThanks,\nStefan\n"},{"id":"363128","messageId":"xmqqy39xwtfs.fsf_-_@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"CAGZ79kaPjw3UbYY=XL0gyKp2VxghwaUOJJoDhiO5sQpT8OWerQ@mail.gmail.com","subject":"Re* [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-13T03:24:39Z","receivedAt":"2018-11-13T03:24:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Stefan Beller <sbeller@google.com> writes:\n\n>> +       if (have_advertised_versions_already)\n>> +               BUG(_(\"attempting to register an allowed protocol version after advertisement\"));\n>\n> If this is a real BUG (due to wrong program flow) instead of bad user input,\n> we would not want to burden the translators with this message.\n>\n> If it is a message that the user is intended to see upon bad input\n> we'd rather go with die(_(\"translatable text here\"));\n\nYeah, good suggestion.  \n\nPerhaps we should spell it out in docstrings for BUG/die with the\nabove rationale.  A weatherbaloon patch follows.\n\n git-compat-util.h | 22 +++++++++++++++++++++-\n 1 file changed, 21 insertions(+), 1 deletion(-)\n\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex 96a3f86d8e..a1cf68cbbc 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -420,7 +420,16 @@ static inline char *git_find_last_dir_sep(const char *path)\n \n struct strbuf;\n \n-/* General helper functions */\n+/*\n+ * General helper functions\n+ *\n+ * Note that these are to produce end-user facing messages, and most\n+ * of them should be marked for translation (the exception is\n+ * messages generated to be sent over the wire---as we currently do not\n+ * have a mechanism to notice and honor locale settings used on the\n+ * other end, leave them untranslated.  We should *not* send messages\n+ * that are localized for our end).\n+ */\n extern void vreportf(const char *prefix, const char *err, va_list params);\n extern NORETURN void usage(const char *err);\n extern NORETURN void usagef(const char *err, ...) __attribute__((format (printf, 1, 2)));\n@@ -1142,6 +1151,17 @@ static inline int regexec_buf(const regex_t *preg, const char *buf, size_t size,\n /* usage.c: only to be used for testing BUG() implementation (see test-tool) */\n extern int BUG_exit_code;\n \n+/*\n+ * BUG(fmt, ...) is to be used when the problem of reaching that point\n+ * in code can only be caused by a program error.  To abort with a\n+ * message to report impossible-to-continue condition due to external\n+ * factors like end-user input, environment settings, data it was fed\n+ * over the wire, use die(_(fmt),...).\n+ * \n+ * Note that the message from BUG() should not be marked for\n+ * translation while the message from die() is in general end-user\n+ * facing and should be marked for translation.\n+ */\n #ifdef HAVE_VARIADIC_MACROS\n __attribute__((format (printf, 3, 4))) NORETURN\n void BUG_fl(const char *file, int line, const char *fmt, ...);\n"},{"id":"363129","messageId":"xmqqr2fpwrqg.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"b9968e3fb005a00e9f78627467b4fc86a6259977.1542059029.git.steadmon@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-13T04:01:27Z","receivedAt":"2018-11-13T04:02:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"steadmon@google.com writes:\n\n> Currently the client advertises that it supports the wire protocol\n> version set in the protocol.version config. However, not all services\n> support the same set of protocol versions. When connecting to\n> git-receive-pack, the client automatically downgrades to v0 if\n> config.protocol is set to v2, but this check is not performed for other\n> services.\n\n\"downgrades to v0 even if ... is set to v2\" you mean?  Otherwise it\nis unclear why asking for v2 leads to using v0.\n\n> This patch creates a protocol version registry. Individual operations\n> register all the protocol versions they support prior to communicating\n> with a server. Versions should be listed in preference order; the\n> version specified in protocol.version will automatically be moved to the\n> front of the registry.\n>\n> The protocol version registry is passed to remote helpers via the\n> GIT_PROTOCOL environment variable.\n>\n> Clients now advertise the full list of registered versions. Servers\n> select the first recognized version from this advertisement.\n\nMakes sense.\n\n> +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> +{\n> +\tint tmp_nr = nr_allowed_versions;\n> +\tenum protocol_version *tmp_allowed_versions, config_version;\n> +\tstrbuf_reset(advert);\n> +\n> +\thave_advertised_versions_already = 1;\n> +\n> +\tconfig_version = get_protocol_version_config();\n> +\tif (config_version == protocol_v0) {\n> +\t\tstrbuf_addstr(advert, \"version=0\");\n> +\t\treturn;\n> +\t}\n> +\n> +\tif (tmp_nr > 0) {\n> +\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> +\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> +\t\t\t   sizeof(enum protocol_version));\n> +\t} else {\n> +\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n> +\t\ttmp_nr = 1;\n> +\t\ttmp_allowed_versions[0] = config_version;\n> +\t}\n> +\n> +\tif (tmp_allowed_versions[0] != config_version)\n> +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n> +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n> +\t\t\t\tenum protocol_version swap =\n> +\t\t\t\t\ttmp_allowed_versions[0];\n> +\t\t\t\ttmp_allowed_versions[0] =\n> +\t\t\t\t\ttmp_allowed_versions[i];\n> +\t\t\t\ttmp_allowed_versions[i] = swap;\n> +\t\t\t}\n> +\n> +\tstrbuf_addf(advert, \"version=%s\",\n> +\t\t    format_protocol_version(tmp_allowed_versions[0]));\n> +\tfor (int i = 1; i < tmp_nr; i++)\n> +\t\tstrbuf_addf(advert, \":version=%s\",\n> +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n> +}\n> +\n\nSo the idea is that the protocols the other end can talk come in\nadvert in their preferred order, and we take an intersection of them\nand our \"allowed-versions\", but the preference is further skewed\nwith the \"swap\" thing if we have our own preference specified via\nconfig?\n\nI am wondering if the code added by this patch outside this\nfunction, with if (strcmp(client_ad.buf, \"version=0\") sprinkled all\nover the place, works sensibly when the other side says \"I prefer\nversion=0 but I do not mind talking version=1\".\n\nIsn't tmp_allowed_versions[] leaking when we return from this\nfunction?\n"},{"id":"363152","messageId":"xmqqd0r9umlz.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"b9968e3fb005a00e9f78627467b4fc86a6259977.1542059029.git.steadmon@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-13T13:35:04Z","receivedAt":"2018-11-13T13:35:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"steadmon@google.com writes:\n\n> +\tif (tmp_allowed_versions[0] != config_version)\n> +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n> +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n> +\t\t\t\tenum protocol_version swap =\n> +\t\t\t\t\ttmp_allowed_versions[0];\n> +\t\t\t\ttmp_allowed_versions[0] =\n> +\t\t\t\t\ttmp_allowed_versions[i];\n> +\t\t\t\ttmp_allowed_versions[i] = swap;\n> +\t\t\t}\n\nHere is what coccicheck suggests.\n\ndiff -u -p a/protocol.c b/protocol.c\n--- a/protocol.c\n+++ b/protocol.c\n@@ -111,11 +111,8 @@ void get_client_protocol_version_adverti\n \tif (tmp_allowed_versions[0] != config_version)\n \t\tfor (int i = 1; i < nr_allowed_versions; i++)\n \t\t\tif (tmp_allowed_versions[i] == config_version) {\n-\t\t\t\tenum protocol_version swap =\n-\t\t\t\t\ttmp_allowed_versions[0];\n-\t\t\t\ttmp_allowed_versions[0] =\n-\t\t\t\t\ttmp_allowed_versions[i];\n-\t\t\t\ttmp_allowed_versions[i] = swap;\n+\t\t\t\tSWAP(tmp_allowed_versions[0],\n+\t\t\t\t     tmp_allowed_versions[i]);\n \t\t\t}\n \n \tstrbuf_addf(advert, \"version=%s\",\n"},{"id":"363188","messageId":"20181113182802.GG30222@szeder.dev","threadId":"49473","inReplyTo":"b9968e3fb005a00e9f78627467b4fc86a6259977.1542059029.git.steadmon@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2018-11-13T18:28:02Z","receivedAt":"2018-11-13T18:28:10Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Nov 12, 2018 at 01:49:05PM -0800, steadmon@google.com wrote:\n\n> diff --git a/protocol.c b/protocol.c\n> index 5e636785d1..54d2ab991b 100644\n> --- a/protocol.c\n> +++ b/protocol.c\n\n> +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> +{\n> +\tint tmp_nr = nr_allowed_versions;\n> +\tenum protocol_version *tmp_allowed_versions, config_version;\n> +\tstrbuf_reset(advert);\n> +\n> +\thave_advertised_versions_already = 1;\n> +\n> +\tconfig_version = get_protocol_version_config();\n> +\tif (config_version == protocol_v0) {\n> +\t\tstrbuf_addstr(advert, \"version=0\");\n> +\t\treturn;\n> +\t}\n> +\n> +\tif (tmp_nr > 0) {\n> +\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> +\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> +\t\t\t   sizeof(enum protocol_version));\n> +\t} else {\n> +\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n> +\t\ttmp_nr = 1;\n> +\t\ttmp_allowed_versions[0] = config_version;\n> +\t}\n> +\n> +\tif (tmp_allowed_versions[0] != config_version)\n> +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n\nWe don't do C99 yet, thus the declaration of a loop variable like this\nis not allowed and triggers compiler errors.\n\n> +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n> +\t\t\t\tenum protocol_version swap =\n> +\t\t\t\t\ttmp_allowed_versions[0];\n> +\t\t\t\ttmp_allowed_versions[0] =\n> +\t\t\t\t\ttmp_allowed_versions[i];\n> +\t\t\t\ttmp_allowed_versions[i] = swap;\n> +\t\t\t}\n> +\n> +\tstrbuf_addf(advert, \"version=%s\",\n> +\t\t    format_protocol_version(tmp_allowed_versions[0]));\n> +\tfor (int i = 1; i < tmp_nr; i++)\n\nLikewise.\n\n> +\t\tstrbuf_addf(advert, \":version=%s\",\n> +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n> +}\n"},{"id":"363201","messageId":"CAGZ79kYv_a8-6dppo3XcNb8wc0RuTSJeOj71Q+3FmEZguBBUiA@mail.gmail.com","threadId":"49473","inReplyTo":"xmqqy39xwtfs.fsf_-_@gitster-ct.c.googlers.com","subject":"Re: Re* [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2018-11-13T19:21:55Z","receivedAt":"2018-11-13T19:22:11Z","isPatch":true,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"On Mon, Nov 12, 2018 at 7:24 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Stefan Beller <sbeller@google.com> writes:\n>\n> >> +       if (have_advertised_versions_already)\n> >> +               BUG(_(\"attempting to register an allowed protocol version after advertisement\"));\n> >\n> > If this is a real BUG (due to wrong program flow) instead of bad user input,\n> > we would not want to burden the translators with this message.\n> >\n> > If it is a message that the user is intended to see upon bad input\n> > we'd rather go with die(_(\"translatable text here\"));\n>\n> Yeah, good suggestion.\n>\n> Perhaps we should spell it out in docstrings for BUG/die with the\n> above rationale.  A weatherbaloon patch follows.\n\n\"Nobody reads documentation any more, we have too much of it.\" [1]\n[1] c.f. https://quoteinvestigator.com/2014/08/29/too-crowded/\n\nI would have hoped to have a .cocci patch in the bad style category,\ni.e. disallowing the _() function inside the context of BUG().\n\nThat said, I like the patch below. Thanks for writing it.\n\n>  git-compat-util.h | 22 +++++++++++++++++++++-\n>  1 file changed, 21 insertions(+), 1 deletion(-)\n>\n> diff --git a/git-compat-util.h b/git-compat-util.h\n> index 96a3f86d8e..a1cf68cbbc 100644\n> --- a/git-compat-util.h\n> +++ b/git-compat-util.h\n> @@ -420,7 +420,16 @@ static inline char *git_find_last_dir_sep(const char *path)\n>\n>  struct strbuf;\n>\n> -/* General helper functions */\n> +/*\n> + * General helper functions\n> + *\n> + * Note that these are to produce end-user facing messages, and most\n> + * of them should be marked for translation (the exception is\n> + * messages generated to be sent over the wire---as we currently do not\n> + * have a mechanism to notice and honor locale settings used on the\n> + * other end, leave them untranslated.  We should *not* send messages\n> + * that are localized for our end).\n> + */\n>  extern void vreportf(const char *prefix, const char *err, va_list params);\n>  extern NORETURN void usage(const char *err);\n>  extern NORETURN void usagef(const char *err, ...) __attribute__((format (printf, 1, 2)));\n> @@ -1142,6 +1151,17 @@ static inline int regexec_buf(const regex_t *preg, const char *buf, size_t size,\n>  /* usage.c: only to be used for testing BUG() implementation (see test-tool) */\n>  extern int BUG_exit_code;\n>\n> +/*\n> + * BUG(fmt, ...) is to be used when the problem of reaching that point\n> + * in code can only be caused by a program error.  To abort with a\n> + * message to report impossible-to-continue condition due to external\n> + * factors like end-user input, environment settings, data it was fed\n> + * over the wire, use die(_(fmt),...).\n> + *\n> + * Note that the message from BUG() should not be marked for\n> + * translation while the message from die() is in general end-user\n> + * facing and should be marked for translation.\n> + */\n>  #ifdef HAVE_VARIADIC_MACROS\n>  __attribute__((format (printf, 3, 4))) NORETURN\n>  void BUG_fl(const char *file, int line, const char *fmt, ...);\n"},{"id":"363228","messageId":"20181113225320.GG126896@google.com","threadId":"49473","inReplyTo":"xmqqr2fpwrqg.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-13T22:53:20Z","receivedAt":"2018-11-13T22:53:30Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.11.13 13:01, Junio C Hamano wrote:\n> steadmon@google.com writes:\n> \n> > Currently the client advertises that it supports the wire protocol\n> > version set in the protocol.version config. However, not all services\n> > support the same set of protocol versions. When connecting to\n> > git-receive-pack, the client automatically downgrades to v0 if\n> > config.protocol is set to v2, but this check is not performed for other\n> > services.\n> \n> \"downgrades to v0 even if ... is set to v2\" you mean?  Otherwise it\n> is unclear why asking for v2 leads to using v0.\n\nThe downgrade on push happens only when the the configured version is\nv2. If v1 is requested, no downgrade is triggered. I'll clarify the\ncommit message in the next version.\n\n> > This patch creates a protocol version registry. Individual operations\n> > register all the protocol versions they support prior to communicating\n> > with a server. Versions should be listed in preference order; the\n> > version specified in protocol.version will automatically be moved to the\n> > front of the registry.\n> >\n> > The protocol version registry is passed to remote helpers via the\n> > GIT_PROTOCOL environment variable.\n> >\n> > Clients now advertise the full list of registered versions. Servers\n> > select the first recognized version from this advertisement.\n> \n> Makes sense.\n> \n> > +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> > +{\n> > +\tint tmp_nr = nr_allowed_versions;\n> > +\tenum protocol_version *tmp_allowed_versions, config_version;\n> > +\tstrbuf_reset(advert);\n> > +\n> > +\thave_advertised_versions_already = 1;\n> > +\n> > +\tconfig_version = get_protocol_version_config();\n> > +\tif (config_version == protocol_v0) {\n> > +\t\tstrbuf_addstr(advert, \"version=0\");\n> > +\t\treturn;\n> > +\t}\n> > +\n> > +\tif (tmp_nr > 0) {\n> > +\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> > +\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> > +\t\t\t   sizeof(enum protocol_version));\n> > +\t} else {\n> > +\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n> > +\t\ttmp_nr = 1;\n> > +\t\ttmp_allowed_versions[0] = config_version;\n> > +\t}\n> > +\n> > +\tif (tmp_allowed_versions[0] != config_version)\n> > +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n> > +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n> > +\t\t\t\tenum protocol_version swap =\n> > +\t\t\t\t\ttmp_allowed_versions[0];\n> > +\t\t\t\ttmp_allowed_versions[0] =\n> > +\t\t\t\t\ttmp_allowed_versions[i];\n> > +\t\t\t\ttmp_allowed_versions[i] = swap;\n> > +\t\t\t}\n> > +\n> > +\tstrbuf_addf(advert, \"version=%s\",\n> > +\t\t    format_protocol_version(tmp_allowed_versions[0]));\n> > +\tfor (int i = 1; i < tmp_nr; i++)\n> > +\t\tstrbuf_addf(advert, \":version=%s\",\n> > +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n> > +}\n> > +\n> \n> So the idea is that the protocols the other end can talk come in\n> advert in their preferred order, and we take an intersection of them\n> and our \"allowed-versions\", but the preference is further skewed\n> with the \"swap\" thing if we have our own preference specified via\n> config?\n\nWe currently don't intersect with our own allowed list, we just accept\nthe first version that we recognize. This introduces its own version\nnegotiation bug; if we add v2 push in the future, a new client\ntalking to an old server would try to use v2 even though the server may\nnot have the corresponding v2 push implementation. I'll fix this in the\nnext version.\n\nIn any case, the ordering of the server's allowed versions won't matter;\nwe'll pick the the first version in the client's list which is also\nallowed on the server.\n\n> I am wondering if the code added by this patch outside this\n> function, with if (strcmp(client_ad.buf, \"version=0\") sprinkled all\n> over the place, works sensibly when the other side says \"I prefer\n> version=0 but I do not mind talking version=1\".\n\nDepends on what you mean by \"sensibly\" :). In the current case, if the\nclient prefers v0, it will always end up using v0. After the fixes\ndescribed above, it will always use v0 unless the server no longer\nsupports v0. Is that what you would expect?\n\n> Isn't tmp_allowed_versions[] leaking when we return from this\n> function?\n\nYes, sorry about that. Will fix.\n"},{"id":"363229","messageId":"20181113230347.GH126896@google.com","threadId":"49473","inReplyTo":"20181113182802.GG30222@szeder.dev","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-13T23:03:47Z","receivedAt":"2018-11-13T23:03:56Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.11.13 19:28, SZEDER Gábor wrote:\n> On Mon, Nov 12, 2018 at 01:49:05PM -0800, steadmon@google.com wrote:\n> \n> > diff --git a/protocol.c b/protocol.c\n> > index 5e636785d1..54d2ab991b 100644\n> > --- a/protocol.c\n> > +++ b/protocol.c\n> \n> > +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> > +{\n> > +\tint tmp_nr = nr_allowed_versions;\n> > +\tenum protocol_version *tmp_allowed_versions, config_version;\n> > +\tstrbuf_reset(advert);\n> > +\n> > +\thave_advertised_versions_already = 1;\n> > +\n> > +\tconfig_version = get_protocol_version_config();\n> > +\tif (config_version == protocol_v0) {\n> > +\t\tstrbuf_addstr(advert, \"version=0\");\n> > +\t\treturn;\n> > +\t}\n> > +\n> > +\tif (tmp_nr > 0) {\n> > +\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> > +\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> > +\t\t\t   sizeof(enum protocol_version));\n> > +\t} else {\n> > +\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n> > +\t\ttmp_nr = 1;\n> > +\t\ttmp_allowed_versions[0] = config_version;\n> > +\t}\n> > +\n> > +\tif (tmp_allowed_versions[0] != config_version)\n> > +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n> \n> We don't do C99 yet, thus the declaration of a loop variable like this\n> is not allowed and triggers compiler errors.\n> \n> > +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n> > +\t\t\t\tenum protocol_version swap =\n> > +\t\t\t\t\ttmp_allowed_versions[0];\n> > +\t\t\t\ttmp_allowed_versions[0] =\n> > +\t\t\t\t\ttmp_allowed_versions[i];\n> > +\t\t\t\ttmp_allowed_versions[i] = swap;\n> > +\t\t\t}\n> > +\n> > +\tstrbuf_addf(advert, \"version=%s\",\n> > +\t\t    format_protocol_version(tmp_allowed_versions[0]));\n> > +\tfor (int i = 1; i < tmp_nr; i++)\n> \n> Likewise.\n> \n> > +\t\tstrbuf_addf(advert, \":version=%s\",\n> > +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n> > +}\n\nSorry about that. Will fix in v4. Out of curiousity, do you have a\nconfig.mak snippet that will make these into errors? I played around\nwith adding combinations of -ansi, -std=c89, and -pedantic to CFLAGS,\nbut I couldn't get anything that detect the problem without also\nbreaking on other parts of the build.\n"},{"id":"363275","messageId":"20181114004745.GH30222@szeder.dev","threadId":"49473","inReplyTo":"20181113230347.GH126896@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2018-11-14T00:47:45Z","receivedAt":"2018-11-14T00:47:51Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Tue, Nov 13, 2018 at 03:03:47PM -0800, Josh Steadmon wrote:\n> > > +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n> > \n> > We don't do C99 yet, thus the declaration of a loop variable like this\n> > is not allowed and triggers compiler errors.\n\n> Sorry about that. Will fix in v4. Out of curiousity, do you have a\n> config.mak snippet that will make these into errors? I played around\n> with adding combinations of -ansi, -std=c89, and -pedantic to CFLAGS,\n> but I couldn't get anything that detect the problem without also\n> breaking on other parts of the build.\n\nUnfortunately, I don't have such an universal CFLAGS.\n\nWith gcc-4.8 the default CFLAGS is sufficient:\n\n  $ make V=1 CC=gcc-4.8 protocol.o\n  gcc-4.8 -o protocol.o -c -MF ./.depend/protocol.o.d -MQ protocol.o -MMD -MP  -g -O2 -Wall -I. -DHAVE_SYSINFO -DGIT_HOST_CPU=\"\\\"x86_64\\\"\" -DHAVE_ALLOCA_H -DUSE_CURL_FOR_IMAP_SEND -DSHA1_DC -DSHA1DC_NO_STANDARD_INCLUDES -DSHA1DC_INIT_SAFE_HASH_DEFAULT=0 -DSHA1DC_CUSTOM_INCLUDE_SHA1_C=\"\\\"cache.h\\\"\" -DSHA1DC_CUSTOM_INCLUDE_UBC_CHECK_C=\"\\\"git-compat-util.h\\\"\"  -DHAVE_PATHS_H -DHAVE_DEV_TTY -DHAVE_CLOCK_GETTIME -DHAVE_CLOCK_MONOTONIC -DHAVE_GETDELIM '-DPROCFS_EXECUTABLE_PATH=\"/proc/self/exe\"'  -DFREAD_READS_DIRECTORIES -DNO_STRLCPY -DSHELL_PATH='\"/bin/sh\"' -DPAGER_ENV='\"LESS=FRX LV=-c\"'  protocol.c\n  protocol.c: In function ‘get_client_protocol_version_advertisement’:\n  protocol.c:112:3: error: ‘for’ loop initial declarations are only allowed in C99 mode\n     for (int i = 1; i < nr_allowed_versions; i++)\n     ^\n  < ... snip ... >\n\nI couldn't get this error with any newer GCC or Clang, and using\noptions like -std=c89 trigger many other errors as well, just like you\nmentioned.\n\n\n"},{"id":"363283","messageId":"cover.1542162201.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1542059029.git.steadmon@google.com","subject":"[PATCH v4 0/1] Advertise multiple supported proto versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-14T02:30:09Z","receivedAt":"2018-11-14T02:30:16Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Fix several bugs identified in v3, clarify commit message, and clean up\nextern keyword in protocol.h.\n\nJosh Steadmon (1):\n  protocol: advertise multiple supported versions\n\n builtin/archive.c        |   3 +\n builtin/clone.c          |   4 ++\n builtin/fetch-pack.c     |   4 ++\n builtin/fetch.c          |   5 ++\n builtin/ls-remote.c      |   5 ++\n builtin/pull.c           |   5 ++\n builtin/push.c           |   4 ++\n builtin/receive-pack.c   |   3 +\n builtin/send-pack.c      |   3 +\n builtin/upload-archive.c |   3 +\n builtin/upload-pack.c    |   4 ++\n connect.c                |  47 +++++++--------\n protocol.c               | 122 ++++++++++++++++++++++++++++++++++++---\n protocol.h               |  23 +++++++-\n remote-curl.c            |  28 ++++++---\n t/t5570-git-daemon.sh    |   2 +-\n t/t5700-protocol-v1.sh   |   8 +--\n t/t5702-protocol-v2.sh   |  16 +++--\n transport-helper.c       |   6 ++\n 19 files changed, 237 insertions(+), 58 deletions(-)\n\nRange-diff against v3:\n1:  b9968e3fb0 ! 1:  3c023991c0 protocol: advertise multiple supported versions\n    @@ -4,22 +4,25 @@\n     \n         Currently the client advertises that it supports the wire protocol\n         version set in the protocol.version config. However, not all services\n    -    support the same set of protocol versions. When connecting to\n    -    git-receive-pack, the client automatically downgrades to v0 if\n    -    config.protocol is set to v2, but this check is not performed for other\n    -    services.\n    +    support the same set of protocol versions. For example, git-receive-pack\n    +    supports v1 and v0, but not v2. If a client connects to git-receive-pack\n    +    and requests v2, it will instead be downgraded to v0. Other services,\n    +    such as git-upload-archive, do not do any version negotiation checks.\n     \n    -    This patch creates a protocol version registry. Individual operations\n    -    register all the protocol versions they support prior to communicating\n    -    with a server. Versions should be listed in preference order; the\n    -    version specified in protocol.version will automatically be moved to the\n    -    front of the registry.\n    +    This patch creates a protocol version registry. Individual client and\n    +    server programs register all the protocol versions they support prior to\n    +    communicating with a remote instance. Versions should be listed in\n    +    preference order; the version specified in protocol.version will\n    +    automatically be moved to the front of the registry.\n     \n         The protocol version registry is passed to remote helpers via the\n         GIT_PROTOCOL environment variable.\n     \n         Clients now advertise the full list of registered versions. Servers\n    -    select the first recognized version from this advertisement.\n    +    select the first allowed version from this advertisement.\n    +\n    +    While we're at it, remove unnecessary externs from function declarations\n    +    in protocol.h.\n     \n         Signed-off-by: Josh Steadmon <steadmon@google.com>\n    @@ -165,6 +168,20 @@\n      \tgit_config(git_push_config, &flags);\n      \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\n     \n    + diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n    + --- a/builtin/receive-pack.c\n    + +++ b/builtin/receive-pack.c\n    +@@\n    + \n    + \tpacket_trace_identity(\"receive-pack\");\n    + \n    ++\tregister_allowed_protocol_version(protocol_v1);\n    ++\tregister_allowed_protocol_version(protocol_v0);\n    ++\n    + \targc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);\n    + \n    + \tif (argc > 1)\n    +\n      diff --git a/builtin/send-pack.c b/builtin/send-pack.c\n      --- a/builtin/send-pack.c\n      +++ b/builtin/send-pack.c\n    @@ -179,6 +196,42 @@\n      \targc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);\n      \tif (argc > 0) {\n     \n    + diff --git a/builtin/upload-archive.c b/builtin/upload-archive.c\n    + --- a/builtin/upload-archive.c\n    + +++ b/builtin/upload-archive.c\n    +@@\n    + #include \"builtin.h\"\n    + #include \"archive.h\"\n    + #include \"pkt-line.h\"\n    ++#include \"protocol.h\"\n    + #include \"sideband.h\"\n    + #include \"run-command.h\"\n    + #include \"argv-array.h\"\n    +@@\n    + \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n    + \t\tusage(upload_archive_usage);\n    + \n    ++\tregister_allowed_protocol_version(protocol_v0);\n    ++\n    + \t/*\n    + \t * Set up sideband subprocess.\n    + \t *\n    +\n    + diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c\n    + --- a/builtin/upload-pack.c\n    + +++ b/builtin/upload-pack.c\n    +@@\n    + \tpacket_trace_identity(\"upload-pack\");\n    + \tread_replace_refs = 0;\n    + \n    ++\tregister_allowed_protocol_version(protocol_v2);\n    ++\tregister_allowed_protocol_version(protocol_v1);\n    ++\tregister_allowed_protocol_version(protocol_v0);\n    ++\n    + \targc = parse_options(argc, argv, NULL, options, upload_pack_usage, 0);\n    + \n    + \tif (argc != 1)\n    +\n      diff --git a/connect.c b/connect.c\n      --- a/connect.c\n      +++ b/connect.c\n    @@ -311,7 +364,7 @@\n     +static enum protocol_version *allowed_versions;\n     +static int nr_allowed_versions;\n     +static int alloc_allowed_versions;\n    -+static int have_advertised_versions_already = 0;\n    ++static int version_registration_locked = 0;\n     +\n     +static const char protocol_v0_string[] = \"0\";\n     +static const char protocol_v1_string[] = \"1\";\n    @@ -357,8 +410,8 @@\n      \n     +void register_allowed_protocol_version(enum protocol_version version)\n     +{\n    -+\tif (have_advertised_versions_already)\n    -+\t\tBUG(_(\"attempting to register an allowed protocol version after advertisement\"));\n    ++\tif (version_registration_locked)\n    ++\t\tBUG(\"late attempt to register an allowed protocol version\");\n     +\n     +\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n     +\t\t   alloc_allowed_versions);\n    @@ -384,13 +437,23 @@\n     +\tstring_list_clear(&version_list, 0);\n     +}\n     +\n    ++static int is_allowed_protocol_version(enum protocol_version version)\n    ++{\n    ++\tint i;\n    ++\tversion_registration_locked = 1;\n    ++\tfor (i = 0; i < nr_allowed_versions; i++)\n    ++\t\tif (version == allowed_versions[i])\n    ++\t\t\treturn 1;\n    ++\treturn 0;\n    ++}\n    ++\n     +void get_client_protocol_version_advertisement(struct strbuf *advert)\n     +{\n    -+\tint tmp_nr = nr_allowed_versions;\n    ++\tint i, tmp_nr = nr_allowed_versions;\n     +\tenum protocol_version *tmp_allowed_versions, config_version;\n     +\tstrbuf_reset(advert);\n     +\n    -+\thave_advertised_versions_already = 1;\n    ++\tversion_registration_locked = 1;\n     +\n     +\tconfig_version = get_protocol_version_config();\n     +\tif (config_version == protocol_v0) {\n    @@ -409,20 +472,19 @@\n     +\t}\n     +\n     +\tif (tmp_allowed_versions[0] != config_version)\n    -+\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n    ++\t\tfor (i = 1; i < nr_allowed_versions; i++)\n     +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n    -+\t\t\t\tenum protocol_version swap =\n    -+\t\t\t\t\ttmp_allowed_versions[0];\n    -+\t\t\t\ttmp_allowed_versions[0] =\n    -+\t\t\t\t\ttmp_allowed_versions[i];\n    -+\t\t\t\ttmp_allowed_versions[i] = swap;\n    ++\t\t\t\tSWAP(tmp_allowed_versions[0],\n    ++\t\t\t\t     tmp_allowed_versions[i]);\n     +\t\t\t}\n     +\n     +\tstrbuf_addf(advert, \"version=%s\",\n     +\t\t    format_protocol_version(tmp_allowed_versions[0]));\n    -+\tfor (int i = 1; i < tmp_nr; i++)\n    ++\tfor (i = 1; i < tmp_nr; i++)\n     +\t\tstrbuf_addf(advert, \":version=%s\",\n     +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n    ++\n    ++\tfree(tmp_allowed_versions);\n     +}\n     +\n      enum protocol_version determine_protocol_version_server(void)\n    @@ -447,7 +509,8 @@\n      \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n      \t\t\t\tv = parse_protocol_version(value);\n     -\t\t\t\tif (v > version)\n    -+\t\t\t\tif (v != protocol_unknown_version) {\n    ++\t\t\t\tif (v != protocol_unknown_version &&\n    ++\t\t\t\t    is_allowed_protocol_version(v)) {\n      \t\t\t\t\tversion = v;\n     +\t\t\t\t\tbreak;\n     +\t\t\t\t}\n    @@ -459,29 +522,46 @@\n      --- a/protocol.h\n      +++ b/protocol.h\n     @@\n    +  * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n    +  * returned.\n       */\n    - extern enum protocol_version get_protocol_version_config(void);\n    - \n    +-extern enum protocol_version get_protocol_version_config(void);\n    ++enum protocol_version get_protocol_version_config(void);\n    ++\n     +/*\n     + * Register an allowable protocol version for a given operation. Registration\n     + * must occur before attempting to advertise a version to a server process.\n     + */\n    -+extern void register_allowed_protocol_version(enum protocol_version version);\n    ++void register_allowed_protocol_version(enum protocol_version version);\n     +\n     +/*\n     + * Register allowable protocol versions from the GIT_PROTOCOL environment var.\n     + */\n    -+extern void register_allowed_protocol_versions_from_env(void);\n    ++void register_allowed_protocol_versions_from_env(void);\n     +\n     +/*\n     + * Fill a strbuf with a version advertisement string suitable for use in the\n     + * GIT_PROTOCOL environment variable or similar version negotiation field.\n     + */\n    -+extern void get_client_protocol_version_advertisement(struct strbuf *advert);\n    -+\n    ++void get_client_protocol_version_advertisement(struct strbuf *advert);\n    + \n      /*\n       * Used by a server to determine which protocol version should be used based on\n    -  * a client's request, communicated via the 'GIT_PROTOCOL' environment variable\n    +@@\n    +  * request a particular protocol version, a default of 'protocol_v0' will be\n    +  * used.\n    +  */\n    +-extern enum protocol_version determine_protocol_version_server(void);\n    ++enum protocol_version determine_protocol_version_server(void);\n    + \n    + /*\n    +  * Used by a client to determine which protocol version the server is speaking\n    +  * based on the server's initial response.\n    +  */\n    +-extern enum protocol_version determine_protocol_version_client(const char *server_response);\n    ++enum protocol_version determine_protocol_version_client(const char *server_response);\n    + \n    + #endif /* PROTOCOL_H */\n     \n      diff --git a/remote-curl.c b/remote-curl.c\n      --- a/remote-curl.c\n-- \n2.19.1.930.g4563a0d9d0-goog\n\n"},{"id":"363284","messageId":"3c023991c024d4460b5f5a85544c3f6a4b65b107.1542162201.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1542162201.git.steadmon@google.com","subject":"[PATCH v4 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-14T02:30:10Z","receivedAt":"2018-11-14T02:31:08Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Currently the client advertises that it supports the wire protocol\nversion set in the protocol.version config. However, not all services\nsupport the same set of protocol versions. For example, git-receive-pack\nsupports v1 and v0, but not v2. If a client connects to git-receive-pack\nand requests v2, it will instead be downgraded to v0. Other services,\nsuch as git-upload-archive, do not do any version negotiation checks.\n\nThis patch creates a protocol version registry. Individual client and\nserver programs register all the protocol versions they support prior to\ncommunicating with a remote instance. Versions should be listed in\npreference order; the version specified in protocol.version will\nautomatically be moved to the front of the registry.\n\nThe protocol version registry is passed to remote helpers via the\nGIT_PROTOCOL environment variable.\n\nClients now advertise the full list of registered versions. Servers\nselect the first allowed version from this advertisement.\n\nWhile we're at it, remove unnecessary externs from function declarations\nin protocol.h.\n\nSigned-off-by: Josh Steadmon <steadmon@google.com>\n---\n builtin/archive.c        |   3 +\n builtin/clone.c          |   4 ++\n builtin/fetch-pack.c     |   4 ++\n builtin/fetch.c          |   5 ++\n builtin/ls-remote.c      |   5 ++\n builtin/pull.c           |   5 ++\n builtin/push.c           |   4 ++\n builtin/receive-pack.c   |   3 +\n builtin/send-pack.c      |   3 +\n builtin/upload-archive.c |   3 +\n builtin/upload-pack.c    |   4 ++\n connect.c                |  47 +++++++--------\n protocol.c               | 122 ++++++++++++++++++++++++++++++++++++---\n protocol.h               |  23 +++++++-\n remote-curl.c            |  28 ++++++---\n t/t5570-git-daemon.sh    |   2 +-\n t/t5700-protocol-v1.sh   |   8 +--\n t/t5702-protocol-v2.sh   |  16 +++--\n transport-helper.c       |   6 ++\n 19 files changed, 237 insertions(+), 58 deletions(-)\n\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex e74f675390..8adb9f381b 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -8,6 +8,7 @@\n #include \"transport.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n \n static void create_output_file(const char *output_file)\n@@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, local_opts, NULL,\n \t\t\t     PARSE_OPT_KEEP_ALL);\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd2c3ef090..1651a950b6 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -900,6 +900,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tstruct argv_array ref_prefixes = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tfetch_if_missing = 0;\n \n \tpacket_trace_identity(\"clone\");\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex 1a1bc63566..cba935e4d3 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -57,6 +57,10 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)\n \n \tfetch_if_missing = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch-pack\");\n \n \tmemset(&args, 0, sizeof(args));\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 61bec5d213..2a20cf8bfd 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -21,6 +21,7 @@\n #include \"argv-array.h\"\n #include \"utf8.h\"\n #include \"packfile.h\"\n+#include \"protocol.h\"\n #include \"list-objects-filter-options.h\"\n \n static const char * const builtin_fetch_usage[] = {\n@@ -1476,6 +1477,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tint prune_tags_ok = 1;\n \tstruct argv_array argv_gc_auto = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch\");\n \n \tfetch_if_missing = 0;\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 1a25df7ee1..ea685e8bb9 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -1,5 +1,6 @@\n #include \"builtin.h\"\n #include \"cache.h\"\n+#include \"protocol.h\"\n #include \"transport.h\"\n #include \"ref-filter.h\"\n #include \"remote.h\"\n@@ -80,6 +81,10 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)\n \n \tmemset(&ref_array, 0, sizeof(ref_array));\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, ls_remote_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tdest = argv[0];\ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex 681c127a07..cb64146834 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"builtin.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"exec-cmd.h\"\n #include \"run-command.h\"\n #include \"sha1-array.h\"\n@@ -849,6 +850,10 @@ int cmd_pull(int argc, const char **argv, const char *prefix)\n \tstruct object_id rebase_fork_point;\n \tint autostash;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tif (!getenv(\"GIT_REFLOG_ACTION\"))\n \t\tset_reflog_message(argc, argv);\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex d09a42062c..10d8abe829 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -10,6 +10,7 @@\n #include \"remote.h\"\n #include \"transport.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"submodule.h\"\n #include \"submodule-config.h\"\n #include \"send-pack.h\"\n@@ -587,6 +588,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"push\");\n \tgit_config(git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex c17ce94e12..030cb7b7ec 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1929,6 +1929,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n \n \tpacket_trace_identity(\"receive-pack\");\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);\n \n \tif (argc > 1)\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8e3c7490f7..f48bd1306b 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -184,6 +184,9 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tgit_config(send_pack_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);\n \tif (argc > 0) {\ndiff --git a/builtin/upload-archive.c b/builtin/upload-archive.c\nindex 25d9116356..791cbe80a6 100644\n--- a/builtin/upload-archive.c\n+++ b/builtin/upload-archive.c\n@@ -5,6 +5,7 @@\n #include \"builtin.h\"\n #include \"archive.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n #include \"argv-array.h\"\n@@ -80,6 +81,8 @@ int cmd_upload_archive(int argc, const char **argv, const char *prefix)\n \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n \t\tusage(upload_archive_usage);\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \t/*\n \t * Set up sideband subprocess.\n \t *\ndiff --git a/builtin/upload-pack.c b/builtin/upload-pack.c\nindex 42dc4da5a1..293dd45b9e 100644\n--- a/builtin/upload-pack.c\n+++ b/builtin/upload-pack.c\n@@ -33,6 +33,10 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)\n \tpacket_trace_identity(\"upload-pack\");\n \tread_replace_refs = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, NULL, options, upload_pack_usage, 0);\n \n \tif (argc != 1)\ndiff --git a/connect.c b/connect.c\nindex 94547e5056..a170ba1e90 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n  */\n static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t\t\t\t     const char *path, const char *prog,\n-\t\t\t\t\t     enum protocol_version version,\n+\t\t\t\t\t     const struct strbuf *version_advert,\n \t\t\t\t\t     int flags)\n {\n \tstruct child_process *conn;\n@@ -1085,10 +1085,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t    target_host, 0);\n \n \t/* If using a new version put that stuff here after a second null byte */\n-\tif (version > 0) {\n+\tif (strcmp(version_advert->buf, \"version=0\")) {\n \t\tstrbuf_addch(&request, '\\0');\n-\t\tstrbuf_addf(&request, \"version=%d%c\",\n-\t\t\t    version, '\\0');\n+\t\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n \t}\n \n \tpacket_write(fd[1], request.buf, request.len);\n@@ -1104,14 +1103,14 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n  */\n static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \t\t\t     enum ssh_variant variant, const char *port,\n-\t\t\t     enum protocol_version version, int flags)\n+\t\t\t     const struct strbuf *version_advert, int flags)\n {\n \tif (variant == VARIANT_SSH &&\n-\t    version > 0) {\n+\t    strcmp(version_advert->buf, \"version=0\")) {\n \t\targv_array_push(args, \"-o\");\n \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n-\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t version);\n+\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t version_advert->buf);\n \t}\n \n \tif (flags & CONNECT_IPV4) {\n@@ -1164,7 +1163,7 @@ static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \n /* Prepare a child_process for use by Git's SSH-tunneled transport. */\n static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n-\t\t\t  const char *port, enum protocol_version version,\n+\t\t\t  const char *port, const struct strbuf *version_advert,\n \t\t\t  int flags)\n {\n \tconst char *ssh;\n@@ -1198,15 +1197,16 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \n \t\targv_array_push(&detect.args, ssh);\n \t\targv_array_push(&detect.args, \"-G\");\n-\t\tpush_ssh_options(&detect.args, &detect.env_array,\n-\t\t\t\t VARIANT_SSH, port, version, flags);\n+\t\tpush_ssh_options(&detect.args, &detect.env_array, VARIANT_SSH,\n+\t\t\t\t port, version_advert, flags);\n \t\targv_array_push(&detect.args, ssh_host);\n \n \t\tvariant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;\n \t}\n \n \targv_array_push(&conn->args, ssh);\n-\tpush_ssh_options(&conn->args, &conn->env_array, variant, port, version, flags);\n+\tpush_ssh_options(&conn->args, &conn->env_array, variant, port,\n+\t\t\t version_advert, flags);\n \targv_array_push(&conn->args, ssh_host);\n }\n \n@@ -1226,16 +1226,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tenum protocol protocol;\n-\tenum protocol_version version = get_protocol_version_config();\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\n@@ -1250,7 +1244,8 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n \t} else if (protocol == PROTO_GIT) {\n-\t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n+\t\tconn = git_connect_git(fd, hostandport, path, prog,\n+\t\t\t\t       &version_advert, flags);\n \t} else {\n \t\tstruct strbuf cmd = STRBUF_INIT;\n \t\tconst char *const *var;\n@@ -1293,12 +1288,14 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n-\t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\n+\t\t\tfill_ssh_args(conn, ssh_host, port, &version_advert,\n+\t\t\t\t      flags);\n \t\t} else {\n \t\t\ttransport_check_allowed(\"file\");\n-\t\t\tif (version > 0) {\n-\t\t\t\targv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t\t\t version);\n+\t\t\tif (strcmp(version_advert.buf, \"version=0\")) {\n+\t\t\t\targv_array_pushf(&conn->env_array,\n+\t\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t\t\t version_advert.buf);\n \t\t\t}\n \t\t}\n \t\targv_array_push(&conn->args, cmd.buf);\ndiff --git a/protocol.c b/protocol.c\nindex 5e636785d1..5664bd7a05 100644\n--- a/protocol.c\n+++ b/protocol.c\n@@ -2,18 +2,43 @@\n #include \"config.h\"\n #include \"protocol.h\"\n \n+static enum protocol_version *allowed_versions;\n+static int nr_allowed_versions;\n+static int alloc_allowed_versions;\n+static int version_registration_locked = 0;\n+\n+static const char protocol_v0_string[] = \"0\";\n+static const char protocol_v1_string[] = \"1\";\n+static const char protocol_v2_string[] = \"2\";\n+\n static enum protocol_version parse_protocol_version(const char *value)\n {\n-\tif (!strcmp(value, \"0\"))\n+\tif (!strcmp(value, protocol_v0_string))\n \t\treturn protocol_v0;\n-\telse if (!strcmp(value, \"1\"))\n+\telse if (!strcmp(value, protocol_v1_string))\n \t\treturn protocol_v1;\n-\telse if (!strcmp(value, \"2\"))\n+\telse if (!strcmp(value, protocol_v2_string))\n \t\treturn protocol_v2;\n \telse\n \t\treturn protocol_unknown_version;\n }\n \n+/* Return the text representation of a wire protocol version. */\n+static const char *format_protocol_version(enum protocol_version version)\n+{\n+\tswitch (version) {\n+\tcase protocol_v0:\n+\t\treturn protocol_v0_string;\n+\tcase protocol_v1:\n+\t\treturn protocol_v1_string;\n+\tcase protocol_v2:\n+\t\treturn protocol_v2_string;\n+\tcase protocol_unknown_version:\n+\t\tdie(_(\"Unrecognized protocol version\"));\n+\t}\n+\tdie(_(\"Unrecognized protocol_version\"));\n+}\n+\n enum protocol_version get_protocol_version_config(void)\n {\n \tconst char *value;\n@@ -30,6 +55,85 @@ enum protocol_version get_protocol_version_config(void)\n \treturn protocol_v0;\n }\n \n+void register_allowed_protocol_version(enum protocol_version version)\n+{\n+\tif (version_registration_locked)\n+\t\tBUG(\"late attempt to register an allowed protocol version\");\n+\n+\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n+\t\t   alloc_allowed_versions);\n+\tallowed_versions[nr_allowed_versions++] = version;\n+}\n+\n+void register_allowed_protocol_versions_from_env(void)\n+{\n+\tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n+\tconst char *version_str;\n+\tstruct string_list version_list = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *version;\n+\n+\tif (!git_protocol)\n+\t\treturn;\n+\n+\tstring_list_split(&version_list, git_protocol, ':', -1);\n+\tfor_each_string_list_item(version, &version_list) {\n+\t\tif (skip_prefix(version->string, \"version=\", &version_str))\n+\t\t\tregister_allowed_protocol_version(\n+\t\t\t\tparse_protocol_version(version_str));\n+\t}\n+\tstring_list_clear(&version_list, 0);\n+}\n+\n+static int is_allowed_protocol_version(enum protocol_version version)\n+{\n+\tint i;\n+\tversion_registration_locked = 1;\n+\tfor (i = 0; i < nr_allowed_versions; i++)\n+\t\tif (version == allowed_versions[i])\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+void get_client_protocol_version_advertisement(struct strbuf *advert)\n+{\n+\tint i, tmp_nr = nr_allowed_versions;\n+\tenum protocol_version *tmp_allowed_versions, config_version;\n+\tstrbuf_reset(advert);\n+\n+\tversion_registration_locked = 1;\n+\n+\tconfig_version = get_protocol_version_config();\n+\tif (config_version == protocol_v0) {\n+\t\tstrbuf_addstr(advert, \"version=0\");\n+\t\treturn;\n+\t}\n+\n+\tif (tmp_nr > 0) {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n+\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n+\t\t\t   sizeof(enum protocol_version));\n+\t} else {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n+\t\ttmp_nr = 1;\n+\t\ttmp_allowed_versions[0] = config_version;\n+\t}\n+\n+\tif (tmp_allowed_versions[0] != config_version)\n+\t\tfor (i = 1; i < nr_allowed_versions; i++)\n+\t\t\tif (tmp_allowed_versions[i] == config_version) {\n+\t\t\t\tSWAP(tmp_allowed_versions[0],\n+\t\t\t\t     tmp_allowed_versions[i]);\n+\t\t\t}\n+\n+\tstrbuf_addf(advert, \"version=%s\",\n+\t\t    format_protocol_version(tmp_allowed_versions[0]));\n+\tfor (i = 1; i < tmp_nr; i++)\n+\t\tstrbuf_addf(advert, \":version=%s\",\n+\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n+\n+\tfree(tmp_allowed_versions);\n+}\n+\n enum protocol_version determine_protocol_version_server(void)\n {\n \tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n@@ -38,9 +142,10 @@ enum protocol_version determine_protocol_version_server(void)\n \t/*\n \t * Determine which protocol version the client has requested.  Since\n \t * multiple 'version' keys can be sent by the client, indicating that\n-\t * the client is okay to speak any of them, select the greatest version\n-\t * that the client has requested.  This is due to the assumption that\n-\t * the most recent protocol version will be the most state-of-the-art.\n+\t * the client is okay to speak any of them, select the first\n+\t * recognizable version that the client has requested.  This is due to\n+\t * the assumption that the protocol versions will be listed in\n+\t * preference order.\n \t */\n \tif (git_protocol) {\n \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -53,8 +158,11 @@ enum protocol_version determine_protocol_version_server(void)\n \n \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n \t\t\t\tv = parse_protocol_version(value);\n-\t\t\t\tif (v > version)\n+\t\t\t\tif (v != protocol_unknown_version &&\n+\t\t\t\t    is_allowed_protocol_version(v)) {\n \t\t\t\t\tversion = v;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \ndiff --git a/protocol.h b/protocol.h\nindex 2ad35e433c..88330fd0ee 100644\n--- a/protocol.h\n+++ b/protocol.h\n@@ -14,7 +14,24 @@ enum protocol_version {\n  * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n  * returned.\n  */\n-extern enum protocol_version get_protocol_version_config(void);\n+enum protocol_version get_protocol_version_config(void);\n+\n+/*\n+ * Register an allowable protocol version for a given operation. Registration\n+ * must occur before attempting to advertise a version to a server process.\n+ */\n+void register_allowed_protocol_version(enum protocol_version version);\n+\n+/*\n+ * Register allowable protocol versions from the GIT_PROTOCOL environment var.\n+ */\n+void register_allowed_protocol_versions_from_env(void);\n+\n+/*\n+ * Fill a strbuf with a version advertisement string suitable for use in the\n+ * GIT_PROTOCOL environment variable or similar version negotiation field.\n+ */\n+void get_client_protocol_version_advertisement(struct strbuf *advert);\n \n /*\n  * Used by a server to determine which protocol version should be used based on\n@@ -23,12 +40,12 @@ extern enum protocol_version get_protocol_version_config(void);\n  * request a particular protocol version, a default of 'protocol_v0' will be\n  * used.\n  */\n-extern enum protocol_version determine_protocol_version_server(void);\n+enum protocol_version determine_protocol_version_server(void);\n \n /*\n  * Used by a client to determine which protocol version the server is speaking\n  * based on the server's initial response.\n  */\n-extern enum protocol_version determine_protocol_version_client(const char *server_response);\n+enum protocol_version determine_protocol_version_client(const char *server_response);\n \n #endif /* PROTOCOL_H */\ndiff --git a/remote-curl.c b/remote-curl.c\nindex fb28309e85..537f8c7aac 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -330,6 +330,20 @@ static int get_protocol_http_header(enum protocol_version version,\n \treturn 0;\n }\n \n+static int get_client_protocol_http_header(const struct strbuf *version_advert,\n+\t\t\t\t\t   struct strbuf *header)\n+{\n+\tif (version_advert->len > 0 &&\n+\t    strcmp(version_advert->buf, \"version=0\")) {\n+\t\tstrbuf_addf(header, GIT_PROTOCOL_HEADER \": %s\",\n+\t\t\t    version_advert->buf);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static struct discovery *discover_refs(const char *service, int for_push)\n {\n \tstruct strbuf exp = STRBUF_INIT;\n@@ -339,11 +353,11 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \tstruct strbuf refs_url = STRBUF_INIT;\n \tstruct strbuf effective_url = STRBUF_INIT;\n \tstruct strbuf protocol_header = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct string_list extra_headers = STRING_LIST_INIT_DUP;\n \tstruct discovery *last = last_discovery;\n \tint http_ret, maybe_smart = 0;\n \tstruct http_get_options http_options;\n-\tenum protocol_version version = get_protocol_version_config();\n \n \tif (last && !strcmp(service, last->service))\n \t\treturn last;\n@@ -360,16 +374,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n \t}\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Add the extra Git-Protocol header */\n-\tif (get_protocol_http_header(version, &protocol_header))\n+\tif (get_client_protocol_http_header(&version_advert, &protocol_header))\n \t\tstring_list_append(&extra_headers, protocol_header.buf);\n \n \tmemset(&http_options, 0, sizeof(http_options));\n@@ -1327,6 +1335,8 @@ int cmd_main(int argc, const char **argv)\n \tstruct strbuf buf = STRBUF_INIT;\n \tint nongit;\n \n+\tregister_allowed_protocol_versions_from_env();\n+\n \tsetup_git_directory_gently(&nongit);\n \tif (argc < 2) {\n \t\terror(\"remote-curl: usage: git remote-curl <remote> [<url>]\");\ndiff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh\nindex 7466aad111..d528e91630 100755\n--- a/t/t5570-git-daemon.sh\n+++ b/t/t5570-git-daemon.sh\n@@ -186,7 +186,7 @@ test_expect_success 'hostname cannot break out of directory' '\n test_expect_success 'daemon log records all attributes' '\n \tcat >expect <<-\\EOF &&\n \tExtended attribute \"host\": localhost\n-\tExtended attribute \"protocol\": version=1\n+\tExtended attribute \"protocol\": version=1:version=2:version=0\n \tEOF\n \t>daemon.log &&\n \tGIT_OVERRIDE_VIRTUAL_HOST=localhost \\\ndiff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh\nindex ba86a44eb1..2e56c79233 100755\n--- a/t/t5700-protocol-v1.sh\n+++ b/t/t5700-protocol-v1.sh\n@@ -26,7 +26,7 @@ test_expect_success 'clone with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"clone< version 1\" log\n '\n@@ -42,7 +42,7 @@ test_expect_success 'fetch with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -56,7 +56,7 @@ test_expect_success 'pull with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'push with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"push> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"push> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"push< version 1\" log\n '\ndiff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh\nindex 3beeed4546..78c17c25e4 100755\n--- a/t/t5702-protocol-v2.sh\n+++ b/t/t5702-protocol-v2.sh\n@@ -24,7 +24,7 @@ test_expect_success 'list refs with git:// using protocol v2' '\n \t\tls-remote --symref \"$GIT_DAEMON_URL/parent\" >actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"git> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"git> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"git< version 2\" log &&\n \n@@ -56,7 +56,7 @@ test_expect_success 'clone with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"clone< version 2\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'fetch with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -90,7 +90,7 @@ test_expect_success 'pull with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -476,7 +476,7 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \ttest_when_finished \"rm -f log\" &&\n \t# Till v2 for push is designed, make sure that if a client has\n \t# protocol.version configured to use v2, that the client instead falls\n-\t# back and uses v0.\n+\t# back to previous versions.\n \n \ttest_commit -C http_child three &&\n \n@@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n \ttest_cmp expect actual &&\n \n-\t# Client didnt request to use protocol v2\n-\t! grep \"Git-Protocol: version=2\" log &&\n-\t# Server didnt respond using protocol v2\n-\t! grep \"git< version 2\" log\n+\t# Server responded with version 1\n+\tgrep \"git< version 1\" log\n '\n \n \ndiff --git a/transport-helper.c b/transport-helper.c\nindex 143ca008c8..ac1937f1e1 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -105,6 +105,7 @@ static struct child_process *get_helper(struct transport *transport)\n {\n \tstruct helper_data *data = transport->data;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct child_process *helper;\n \tint duped;\n \tint code;\n@@ -127,6 +128,11 @@ static struct child_process *get_helper(struct transport *transport)\n \t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n \t\t\t\t GIT_DIR_ENVIRONMENT, get_git_dir());\n \n+\tget_client_protocol_version_advertisement(&version_advert);\n+\tif (version_advert.len > 0)\n+\t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n+\t\t\t\t GIT_PROTOCOL_ENVIRONMENT, version_advert.buf);\n+\n \tcode = start_command(helper);\n \tif (code < 0 && errno == ENOENT)\n \t\tdie(_(\"unable to find remote helper for '%s'\"), data->name);\n-- \n2.19.1.930.g4563a0d9d0-goog\n\n"},{"id":"363285","messageId":"xmqq5zx0s84b.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"CAGZ79kYv_a8-6dppo3XcNb8wc0RuTSJeOj71Q+3FmEZguBBUiA@mail.gmail.com","subject":"Re: Re* [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-14T02:31:00Z","receivedAt":"2018-11-14T02:31:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Stefan Beller <sbeller@google.com> writes:\n\n> \"Nobody reads documentation any more, we have too much of it.\"\n\nMaybe, but spelling it out and writing it down, you can point at it\n(or hope that other people point at it without making a bad or incorrect\nrephrase).\n\n> I would have hoped to have a .cocci patch in the bad style category,\n> i.e. disallowing the _() function inside the context of BUG().\n>\n> That said, I like the patch below. Thanks for writing it.\n\nThat was a mere weather-baloon, and too early to thank about.\nHopefully people can pick nits and come to a final version that can\nbe applied.\n\nI have a few things in it that I think may be debatable.  I am not\nsure if we should talk about being careful not to send localized\nstrings over the wire in that comment, for example.\n\n\n"},{"id":"363286","messageId":"xmqq1s7os7rz.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"20181113225320.GG126896@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-14T02:38:24Z","receivedAt":"2018-11-14T02:38:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n> On 2018.11.13 13:01, Junio C Hamano wrote:\n>> steadmon@google.com writes:\n>> \n>> > Currently the client advertises that it supports the wire protocol\n>> > version set in the protocol.version config. However, not all services\n>> > support the same set of protocol versions. When connecting to\n>> > git-receive-pack, the client automatically downgrades to v0 if\n>> > config.protocol is set to v2, but this check is not performed for other\n>> > services.\n>> \n>> \"downgrades to v0 even if ... is set to v2\" you mean?  Otherwise it\n>> is unclear why asking for v2 leads to using v0.\n>\n> The downgrade on push happens only when the the configured version is\n> v2. If v1 is requested, no downgrade is triggered. I'll clarify the\n> commit message in the next version.\n\nOK, then it will still be confusing unless \"we downgrade v2 to v0\nbecause ...\"gives the reason.\n\n> In any case, the ordering of the server's allowed versions won't matter;\n> we'll pick the the first version in the client's list which is also\n> allowed on the server.\n\nThat sounds like a very sensible semantics.\n\n>\n>> I am wondering if the code added by this patch outside this\n>> function, with if (strcmp(client_ad.buf, \"version=0\") sprinkled all\n>> over the place, works sensibly when the other side says \"I prefer\n>> version=0 but I do not mind talking version=1\".\n>\n> Depends on what you mean by \"sensibly\" :). In the current case, if the\n> client prefers v0, it will always end up using v0. After the fixes\n> described above, it will always use v0 unless the server no longer\n> supports v0. Is that what you would expect?\n\nYes, that sounds like a sensible behaviour.\n\nWhat I was alludding to was a lot simpler, though.  An advert string\n\"version=0:version=1\" from a client that prefers version 0 won't be\n!strcmp(\"version=0\", advert) but seeing many strcmp() in the patch\nmade me wonder.\n"},{"id":"363287","messageId":"xmqqwopgqsws.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"20181113182802.GG30222@szeder.dev","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-14T02:44:51Z","receivedAt":"2018-11-14T02:45:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n>> +\tif (tmp_allowed_versions[0] != config_version)\n>> +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n>\n> We don't do C99 yet, thus the declaration of a loop variable like this\n> is not allowed and triggers compiler errors.\n\nI thought we did a weather-balloon to see if this bothers people who\nbuild on minority platforms but\n\n\tgit grep 'for (int'\n\nis coming up empty.\n\nWe have been trying designated initializers with weather-balloon\nchanges (both arrays and struct fields) and I somehow thought that\nwe already were trying this out, but apparently that is not the\ncase.  Also trailing comma in the enum definition was what we\ndiscussed earlier but I do not know if we had weather-balloon for it\nor not offhand.  \n\nPerhaps we should write these down in CodingGuidelines.\n"},{"id":"363343","messageId":"xmqqin10m01g.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"cover.1542162201.git.steadmon@google.com","subject":"Re: [PATCH v4 0/1] Advertise multiple supported proto versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-14T10:22:03Z","receivedAt":"2018-11-14T10:22:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n> Fix several bugs identified in v3, clarify commit message, and clean up\n> extern keyword in protocol.h.\n\nIt is good to descirbe the change relative to v3 here, which would\nhelp those who are interested and reviewed v3.\n\nTo help those who missed the boat and v4 is their first encounter\nwith this series, having the description relative to v3 alone and\nnothing else is not very friendly, though.\n\n>     + diff --git a/builtin/upload-archive.c b/builtin/upload-archive.c\n>     + --- a/builtin/upload-archive.c\n>     + +++ b/builtin/upload-archive.c\n>     +@@\n>     + #include \"builtin.h\"\n>     + #include \"archive.h\"\n>     + #include \"pkt-line.h\"\n>     ++#include \"protocol.h\"\n>     + #include \"sideband.h\"\n>     + #include \"run-command.h\"\n>     + #include \"argv-array.h\"\n>     +@@\n>     + \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n>     + \t\tusage(upload_archive_usage);\n>     + \n>     ++\tregister_allowed_protocol_version(protocol_v0);\n>     ++\n>     + \t/*\n>     + \t * Set up sideband subprocess.\n>     + \t *\n\nThis one unfortunately seems to interact rather badly with your\njs/remote-archive-v2 topic which is already in 'next', when this\ntopic is merged to 'pu', and my attempt to mechanically resolve\nconflicts breaks t5000.\n\n\n"},{"id":"363349","messageId":"20181114110134.GI30222@szeder.dev","threadId":"49473","inReplyTo":"xmqqwopgqsws.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2018-11-14T11:01:34Z","receivedAt":"2018-11-14T11:01:40Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Wed, Nov 14, 2018 at 11:44:51AM +0900, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> >> +\tif (tmp_allowed_versions[0] != config_version)\n> >> +\t\tfor (int i = 1; i < nr_allowed_versions; i++)\n> >\n> > We don't do C99 yet, thus the declaration of a loop variable like this\n> > is not allowed and triggers compiler errors.\n> \n> I thought we did a weather-balloon to see if this bothers people who\n> build on minority platforms but\n> \n> \tgit grep 'for (int'\n> \n> is coming up empty.\n> \n> We have been trying designated initializers with weather-balloon\n> changes (both arrays and struct fields) and I somehow thought that\n> we already were trying this out, but apparently that is not the\n> case.\n\nI thought so as well, and I run the exact same 'git grep' command\nbefore replying to Josh :)\n\nThere was a discussion about such a weather-balloon patch [1] (which\nhappened to use an unsigned loop variable, so our grep wouldn't have\nfound it anyway), but it wasn't picked up because it required new\noptions in CFLAGS and there was no standard way to do so [2].\n\n[1] https://public-inbox.org/git/20170719181956.15845-1-sbeller@google.com/\n[2] https://public-inbox.org/git/20170724170813.scceigybl5d3fvdd@sigill.intra.peff.net/\n\n"},{"id":"363393","messageId":"20181114195142.GI126896@google.com","threadId":"49473","inReplyTo":"xmqqin10m01g.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH v4 0/1] Advertise multiple supported proto versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-14T19:51:42Z","receivedAt":"2018-11-14T19:51:53Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.11.14 19:22, Junio C Hamano wrote:\n> Josh Steadmon <steadmon@google.com> writes:\n> \n> > Fix several bugs identified in v3, clarify commit message, and clean up\n> > extern keyword in protocol.h.\n> \n> It is good to descirbe the change relative to v3 here, which would\n> help those who are interested and reviewed v3.\n> \n> To help those who missed the boat and v4 is their first encounter\n> with this series, having the description relative to v3 alone and\n> nothing else is not very friendly, though.\n\nAck. Will keep this in mind for future patches.\n\n> >     + diff --git a/builtin/upload-archive.c b/builtin/upload-archive.c\n> >     + --- a/builtin/upload-archive.c\n> >     + +++ b/builtin/upload-archive.c\n> >     +@@\n> >     + #include \"builtin.h\"\n> >     + #include \"archive.h\"\n> >     + #include \"pkt-line.h\"\n> >     ++#include \"protocol.h\"\n> >     + #include \"sideband.h\"\n> >     + #include \"run-command.h\"\n> >     + #include \"argv-array.h\"\n> >     +@@\n> >     + \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n> >     + \t\tusage(upload_archive_usage);\n> >     + \n> >     ++\tregister_allowed_protocol_version(protocol_v0);\n> >     ++\n> >     + \t/*\n> >     + \t * Set up sideband subprocess.\n> >     + \t *\n> \n> This one unfortunately seems to interact rather badly with your\n> js/remote-archive-v2 topic which is already in 'next', when this\n> topic is merged to 'pu', and my attempt to mechanically resolve\n> conflicts breaks t5000.\n\nHmm, should we drop js/remote-archive-v2 then? Any solution that\nunblocks js/remote-archive-v2 will almost certainly depend on this\npatch.\n"},{"id":"363394","messageId":"20181114195757.GJ126896@google.com","threadId":"49473","inReplyTo":"xmqq1s7os7rz.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-14T19:57:57Z","receivedAt":"2018-11-14T19:58:07Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.11.14 11:38, Junio C Hamano wrote:\n> Josh Steadmon <steadmon@google.com> writes:\n> \n> > On 2018.11.13 13:01, Junio C Hamano wrote:\n> >> I am wondering if the code added by this patch outside this\n> >> function, with if (strcmp(client_ad.buf, \"version=0\") sprinkled all\n> >> over the place, works sensibly when the other side says \"I prefer\n> >> version=0 but I do not mind talking version=1\".\n> >\n> > Depends on what you mean by \"sensibly\" :). In the current case, if the\n> > client prefers v0, it will always end up using v0. After the fixes\n> > described above, it will always use v0 unless the server no longer\n> > supports v0. Is that what you would expect?\n> \n> Yes, that sounds like a sensible behaviour.\n> \n> What I was alludding to was a lot simpler, though.  An advert string\n> \"version=0:version=1\" from a client that prefers version 0 won't be\n> !strcmp(\"version=0\", advert) but seeing many strcmp() in the patch\n> made me wonder.\n\nAh I see. The strcmp()s against \"version=0\" are special cases for where\nit looks like the client does not understand any sort of version\nnegotiation. If we see multiple versions listed in the advert, then the\nrest of the selection logic should do the right thing.\n\nHowever, I think that it might work to remove the special cases. In the\nevent that the client is so old that it doesn't understand any form of\nversion negotiation, it should just ignore the version fields /\nenvironment vars. If you think it's cleaner to remove the special cases,\nlet me know.\n"},{"id":"363458","messageId":"xmqq4lchn3jy.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"20181114195757.GJ126896@google.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-16T02:45:21Z","receivedAt":"2018-11-16T02:45:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n>> What I was alludding to was a lot simpler, though.  An advert string\n>> \"version=0:version=1\" from a client that prefers version 0 won't be\n>> !strcmp(\"version=0\", advert) but seeing many strcmp() in the patch\n>> made me wonder.\n>\n> Ah I see. The strcmp()s against \"version=0\" are special cases for where\n> it looks like the client does not understand any sort of version\n> negotiation. If we see multiple versions listed in the advert, then the\n> rest of the selection logic should do the right thing.\n\nOK, let me try to see if I understand correctly by rephrasing.\n\nIf the client does not say anything about which version it prefers\n(because it only knows about version 0 without even realizing that\nthere is a version negotiation exchange), we substitute the lack of\nproposed versions with string \"version=0\", and the strcmp()s I saw\nand was puzzled by are all about special casing such a client.  But\nwe would end up behaving the same way (at least when judged only by\nexternally visible effects) to a client that is aware of version\nnegotiation and tells us it prefers version 0 (and nothing else)\nwith the selection logic anyway.\n\nDid I get it right?  If so, yeah, I think it makes sense to avoid\ntwo codepaths that ends up doing the same thing by removing the\nspecial case.\n\n> However, I think that it might work to remove the special cases. In the\n> event that the client is so old that it doesn't understand any form of\n> version negotiation, it should just ignore the version fields /\n> environment vars. If you think it's cleaner to remove the special cases,\n> let me know.\n"},{"id":"363497","messageId":"xmqqo9api9la.fsf@gitster-ct.c.googlers.com","threadId":"49473","inReplyTo":"20181114195142.GI126896@google.com","subject":"Re: [PATCH v4 0/1] Advertise multiple supported proto versions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2018-11-16T10:46:09Z","receivedAt":"2018-11-16T10:46:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n>> This one unfortunately seems to interact rather badly with your\n>> js/remote-archive-v2 topic which is already in 'next', when this\n>> topic is merged to 'pu', and my attempt to mechanically resolve\n>> conflicts breaks t5000.\n>\n> Hmm, should we drop js/remote-archive-v2 then? Any solution that\n> unblocks js/remote-archive-v2 will almost certainly depend on this\n> patch.\n\nThat one is already in 'next' for quite some time.  If you are\nretrating it, that's OK.\n\nLet me revert the topic out of 'next' and discard it, and then queue\nthis one in 'pu'.\n\nThanks.\n\n\n\n\n"},{"id":"363530","messageId":"20181116195932.GA9703@google.com","threadId":"49473","inReplyTo":"xmqq4lchn3jy.fsf@gitster-ct.c.googlers.com","subject":"Re: [PATCH v3 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-16T19:59:32Z","receivedAt":"2018-11-16T19:59:43Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.11.16 11:45, Junio C Hamano wrote:\n> Josh Steadmon <steadmon@google.com> writes:\n> \n> >> What I was alludding to was a lot simpler, though.  An advert string\n> >> \"version=0:version=1\" from a client that prefers version 0 won't be\n> >> !strcmp(\"version=0\", advert) but seeing many strcmp() in the patch\n> >> made me wonder.\n> >\n> > Ah I see. The strcmp()s against \"version=0\" are special cases for where\n> > it looks like the client does not understand any sort of version\n> > negotiation. If we see multiple versions listed in the advert, then the\n> > rest of the selection logic should do the right thing.\n> \n> OK, let me try to see if I understand correctly by rephrasing.\n> \n> If the client does not say anything about which version it prefers\n> (because it only knows about version 0 without even realizing that\n> there is a version negotiation exchange), we substitute the lack of\n> proposed versions with string \"version=0\", and the strcmp()s I saw\n> and was puzzled by are all about special casing such a client.  But\n> we would end up behaving the same way (at least when judged only by\n> externally visible effects) to a client that is aware of version\n> negotiation and tells us it prefers version 0 (and nothing else)\n> with the selection logic anyway.\n> \n> Did I get it right?  If so, yeah, I think it makes sense to avoid\n> two codepaths that ends up doing the same thing by removing the\n> special case.\n\nYes, that is correct. The next version will remove the special cases\nhere.\n\n> > However, I think that it might work to remove the special cases. In the\n> > event that the client is so old that it doesn't understand any form of\n> > version negotiation, it should just ignore the version fields /\n> > environment vars. If you think it's cleaner to remove the special cases,\n> > let me know.\n"},{"id":"363537","messageId":"cover.1542407348.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1542162201.git.steadmon@google.com","subject":"[PATCH v5 0/1] Advertise multiple supported proto versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-16T22:34:37Z","receivedAt":"2018-11-16T22:34:44Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Changes from V4: remove special cases around advertising version=0.\n\nAdd a registry of supported wire protocol versions that individual\ncommands can use to declare supported versions before contacting a\nserver. The client will then advertise all supported versions, while the\nserver will choose the first allowed version from the advertised\nlist.\n\nEvery command that acts as a client or server must now register its\nsupported protocol versions.\n\n\nJosh Steadmon (1):\n  protocol: advertise multiple supported versions\n\n builtin/archive.c           |   3 +\n builtin/clone.c             |   4 ++\n builtin/fetch-pack.c        |   4 ++\n builtin/fetch.c             |   5 ++\n builtin/ls-remote.c         |   5 ++\n builtin/pull.c              |   5 ++\n builtin/push.c              |   4 ++\n builtin/receive-pack.c      |   3 +\n builtin/send-pack.c         |   3 +\n builtin/upload-archive.c    |   3 +\n builtin/upload-pack.c       |   4 ++\n connect.c                   |  52 +++++++--------\n protocol.c                  | 122 +++++++++++++++++++++++++++++++++---\n protocol.h                  |  23 ++++++-\n remote-curl.c               |  27 +++++---\n t/t5551-http-fetch-smart.sh |   1 +\n t/t5570-git-daemon.sh       |   2 +-\n t/t5601-clone.sh            |  38 +++++------\n t/t5700-protocol-v1.sh      |   8 +--\n t/t5702-protocol-v2.sh      |  16 +++--\n transport-helper.c          |   6 ++\n 21 files changed, 256 insertions(+), 82 deletions(-)\n\nRange-diff against v4:\n1:  3c023991c0 ! 1:  60f6f2fbd8 protocol: advertise multiple supported versions\n    @@ -21,6 +21,12 @@\n         Clients now advertise the full list of registered versions. Servers\n         select the first allowed version from this advertisement.\n     \n    +    Additionally, remove special cases around advertising version=0.\n    +    Previously we avoided adding version negotiation fields in server\n    +    responses if it looked like the client wanted v0. However, including\n    +    these fields does not change behavior, so it's better to have simpler\n    +    code.\n    +\n         While we're at it, remove unnecessary externs from function declarations\n         in protocol.h.\n     \n    @@ -245,18 +251,21 @@\n      {\n      \tstruct child_process *conn;\n     @@\n    + \t\t    prog, path, 0,\n      \t\t    target_host, 0);\n      \n    - \t/* If using a new version put that stuff here after a second null byte */\n    +-\t/* If using a new version put that stuff here after a second null byte */\n     -\tif (version > 0) {\n    -+\tif (strcmp(version_advert->buf, \"version=0\")) {\n    - \t\tstrbuf_addch(&request, '\\0');\n    +-\t\tstrbuf_addch(&request, '\\0');\n     -\t\tstrbuf_addf(&request, \"version=%d%c\",\n     -\t\t\t    version, '\\0');\n    -+\t\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n    - \t}\n    +-\t}\n    ++\t/* Add version fields after a second null byte */\n    ++\tstrbuf_addch(&request, '\\0');\n    ++\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n      \n      \tpacket_write(fd[1], request.buf, request.len);\n    + \n     @@\n       */\n      static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n    @@ -264,9 +273,9 @@\n     -\t\t\t     enum protocol_version version, int flags)\n     +\t\t\t     const struct strbuf *version_advert, int flags)\n      {\n    - \tif (variant == VARIANT_SSH &&\n    +-\tif (variant == VARIANT_SSH &&\n     -\t    version > 0) {\n    -+\t    strcmp(version_advert->buf, \"version=0\")) {\n    ++\tif (variant == VARIANT_SSH) {\n      \t\targv_array_push(args, \"-o\");\n      \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n     -\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n    @@ -346,13 +355,13 @@\n     -\t\t\tif (version > 0) {\n     -\t\t\t\targv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n     -\t\t\t\t\t\t version);\n    -+\t\t\tif (strcmp(version_advert.buf, \"version=0\")) {\n    -+\t\t\t\targv_array_pushf(&conn->env_array,\n    -+\t\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n    -+\t\t\t\t\t\t version_advert.buf);\n    - \t\t\t}\n    +-\t\t\t}\n    ++\t\t\targv_array_pushf(&conn->env_array,\n    ++\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n    ++\t\t\t\t\t version_advert.buf);\n      \t\t}\n      \t\targv_array_push(&conn->args, cmd.buf);\n    + \n     \n      diff --git a/protocol.c b/protocol.c\n      --- a/protocol.c\n    @@ -573,8 +582,7 @@\n     +static int get_client_protocol_http_header(const struct strbuf *version_advert,\n     +\t\t\t\t\t   struct strbuf *header)\n     +{\n    -+\tif (version_advert->len > 0 &&\n    -+\t    strcmp(version_advert->buf, \"version=0\")) {\n    ++\tif (version_advert->len > 0) {\n     +\t\tstrbuf_addf(header, GIT_PROTOCOL_HEADER \": %s\",\n     +\t\t\t    version_advert->buf);\n     +\n    @@ -629,6 +637,18 @@\n      \tif (argc < 2) {\n      \t\terror(\"remote-curl: usage: git remote-curl <remote> [<url>]\");\n     \n    + diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh\n    + --- a/t/t5551-http-fetch-smart.sh\n    + +++ b/t/t5551-http-fetch-smart.sh\n    +@@\n    + > Accept: */*\n    + > Accept-Encoding: ENCODINGS\n    + > Pragma: no-cache\n    ++> Git-Protocol: version=0\n    + < HTTP/1.1 200 OK\n    + < Pragma: no-cache\n    + < Cache-Control: no-cache, max-age=0, must-revalidate\n    +\n      diff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh\n      --- a/t/t5570-git-daemon.sh\n      +++ b/t/t5570-git-daemon.sh\n    @@ -642,6 +662,161 @@\n      \t>daemon.log &&\n      \tGIT_OVERRIDE_VIRTUAL_HOST=localhost \\\n     \n    + diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\n    + --- a/t/t5601-clone.sh\n    + +++ b/t/t5601-clone.sh\n    +@@\n    + \n    + test_expect_success 'clone myhost:src uses ssh' '\n    + \tgit clone myhost:src ssh-clone &&\n    +-\texpect_ssh myhost src\n    ++\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n    + '\n    + \n    + test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n    +@@\n    + \n    + test_expect_success 'bracketed hostnames are still ssh' '\n    + \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n    +-\texpect_ssh \"-p 123\" myhost src\n    ++\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n    + '\n    + \n    + test_expect_success 'OpenSSH variant passes -4' '\n    + \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n    +-\texpect_ssh \"-4 -p 123\" myhost src\n    ++\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n    + '\n    + \n    + test_expect_success 'variant can be overridden' '\n    +@@\n    + \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n    + \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n    + \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n    +-\texpect_ssh \"-p 123\" myhost src\n    ++\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n    + '\n    + \n    + test_expect_success 'plink is treated specially (as putty)' '\n    +@@\n    + \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n    + \tGIT_SSH_VARIANT=ssh \\\n    + \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n    +-\texpect_ssh \"-p 123\" myhost src\n    ++\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n    + '\n    + \n    + test_expect_success 'ssh.variant overrides plink detection' '\n    + \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n    + \tgit -c ssh.variant=ssh \\\n    + \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n    +-\texpect_ssh \"-p 123\" myhost src\n    ++\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n    + '\n    + \n    + test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n    +@@\n    + }\n    + \n    + test_expect_success !MINGW 'clone c:temp is ssl' '\n    +-\ttest_clone_url c:temp c temp\n    ++\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n    + '\n    + \n    + test_expect_success MINGW 'clone c:temp is dos drive' '\n    +@@\n    + for repo in rep rep/home/project 123\n    + do\n    + \ttest_expect_success \"clone host:$repo\" '\n    +-\t\ttest_clone_url host:$repo host $repo\n    ++\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n    + \t'\n    + done\n    + \n    +@@\n    + for repo in rep rep/home/project 123\n    + do\n    + \ttest_expect_success \"clone [::1]:$repo\" '\n    +-\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n    ++\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n    + \t'\n    + done\n    + #home directory\n    + test_expect_success \"clone host:/~repo\" '\n    +-\ttest_clone_url host:/~repo host \"~repo\"\n    ++\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n    + '\n    + \n    + test_expect_success \"clone [::1]:/~repo\" '\n    +-\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n    ++\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n    + '\n    + \n    + # Corner cases\n    +@@\n    + for tcol in \"\" :\n    + do\n    + \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n    +-\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n    ++\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n    + \t'\n    + \t# from home directory\n    + \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n    +-\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n    ++\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n    + '\n    + done\n    + \n    + # with port number\n    + test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n    +-\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n    ++\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n    + '\n    + \n    + # from home directory with port number\n    + test_expect_success 'clone ssh://host.xz:22/~repo' '\n    +-\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n    ++\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n    + '\n    + \n    + #IPv6\n    +@@\n    + do\n    + \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n    + \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n    +-\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n    ++\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n    + \t\"\n    + done\n    + \n    +@@\n    + do\n    + \teuah=$(echo $tuah | tr -d \"[]\")\n    + \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n    +-\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n    ++\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n    + \t\"\n    + done\n    + \n    +@@\n    + do\n    + \teuah=$(echo $tuah | tr -d \"[]\")\n    + \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n    +-\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n    ++\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n    + \t\"\n    + done\n    + \n    +@@\n    + do\n    + \teuah=$(echo $tuah | tr -d \"[]\")\n    + \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n    +-\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n    ++\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n    + \t\"\n    + done\n    + \n    +\n      diff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh\n      --- a/t/t5700-protocol-v1.sh\n      +++ b/t/t5700-protocol-v1.sh\n-- \n2.19.1.1215.g8438c0b245-goog\n\n"},{"id":"363538","messageId":"60f6f2fbd8ee03b2d461803b9313b7473300eecc.1542407348.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1542407348.git.steadmon@google.com","subject":"[PATCH v5 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-11-16T22:34:38Z","receivedAt":"2018-11-16T22:34:49Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Currently the client advertises that it supports the wire protocol\nversion set in the protocol.version config. However, not all services\nsupport the same set of protocol versions. For example, git-receive-pack\nsupports v1 and v0, but not v2. If a client connects to git-receive-pack\nand requests v2, it will instead be downgraded to v0. Other services,\nsuch as git-upload-archive, do not do any version negotiation checks.\n\nThis patch creates a protocol version registry. Individual client and\nserver programs register all the protocol versions they support prior to\ncommunicating with a remote instance. Versions should be listed in\npreference order; the version specified in protocol.version will\nautomatically be moved to the front of the registry.\n\nThe protocol version registry is passed to remote helpers via the\nGIT_PROTOCOL environment variable.\n\nClients now advertise the full list of registered versions. Servers\nselect the first allowed version from this advertisement.\n\nAdditionally, remove special cases around advertising version=0.\nPreviously we avoided adding version negotiation fields in server\nresponses if it looked like the client wanted v0. However, including\nthese fields does not change behavior, so it's better to have simpler\ncode.\n\nWhile we're at it, remove unnecessary externs from function declarations\nin protocol.h.\n\nSigned-off-by: Josh Steadmon <steadmon@google.com>\n---\n builtin/archive.c           |   3 +\n builtin/clone.c             |   4 ++\n builtin/fetch-pack.c        |   4 ++\n builtin/fetch.c             |   5 ++\n builtin/ls-remote.c         |   5 ++\n builtin/pull.c              |   5 ++\n builtin/push.c              |   4 ++\n builtin/receive-pack.c      |   3 +\n builtin/send-pack.c         |   3 +\n builtin/upload-archive.c    |   3 +\n builtin/upload-pack.c       |   4 ++\n connect.c                   |  52 +++++++--------\n protocol.c                  | 122 +++++++++++++++++++++++++++++++++---\n protocol.h                  |  23 ++++++-\n remote-curl.c               |  27 +++++---\n t/t5551-http-fetch-smart.sh |   1 +\n t/t5570-git-daemon.sh       |   2 +-\n t/t5601-clone.sh            |  38 +++++------\n t/t5700-protocol-v1.sh      |   8 +--\n t/t5702-protocol-v2.sh      |  16 +++--\n transport-helper.c          |   6 ++\n 21 files changed, 256 insertions(+), 82 deletions(-)\n\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex e74f675390..8adb9f381b 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -8,6 +8,7 @@\n #include \"transport.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n \n static void create_output_file(const char *output_file)\n@@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, local_opts, NULL,\n \t\t\t     PARSE_OPT_KEEP_ALL);\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd2c3ef090..1651a950b6 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -900,6 +900,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tstruct argv_array ref_prefixes = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tfetch_if_missing = 0;\n \n \tpacket_trace_identity(\"clone\");\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex 1a1bc63566..cba935e4d3 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -57,6 +57,10 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)\n \n \tfetch_if_missing = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch-pack\");\n \n \tmemset(&args, 0, sizeof(args));\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 61bec5d213..2a20cf8bfd 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -21,6 +21,7 @@\n #include \"argv-array.h\"\n #include \"utf8.h\"\n #include \"packfile.h\"\n+#include \"protocol.h\"\n #include \"list-objects-filter-options.h\"\n \n static const char * const builtin_fetch_usage[] = {\n@@ -1476,6 +1477,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tint prune_tags_ok = 1;\n \tstruct argv_array argv_gc_auto = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch\");\n \n \tfetch_if_missing = 0;\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 1a25df7ee1..ea685e8bb9 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -1,5 +1,6 @@\n #include \"builtin.h\"\n #include \"cache.h\"\n+#include \"protocol.h\"\n #include \"transport.h\"\n #include \"ref-filter.h\"\n #include \"remote.h\"\n@@ -80,6 +81,10 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)\n \n \tmemset(&ref_array, 0, sizeof(ref_array));\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, ls_remote_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tdest = argv[0];\ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex 681c127a07..cb64146834 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"builtin.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"exec-cmd.h\"\n #include \"run-command.h\"\n #include \"sha1-array.h\"\n@@ -849,6 +850,10 @@ int cmd_pull(int argc, const char **argv, const char *prefix)\n \tstruct object_id rebase_fork_point;\n \tint autostash;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tif (!getenv(\"GIT_REFLOG_ACTION\"))\n \t\tset_reflog_message(argc, argv);\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex d09a42062c..10d8abe829 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -10,6 +10,7 @@\n #include \"remote.h\"\n #include \"transport.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"submodule.h\"\n #include \"submodule-config.h\"\n #include \"send-pack.h\"\n@@ -587,6 +588,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"push\");\n \tgit_config(git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex c17ce94e12..030cb7b7ec 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1929,6 +1929,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n \n \tpacket_trace_identity(\"receive-pack\");\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);\n \n \tif (argc > 1)\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8e3c7490f7..f48bd1306b 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -184,6 +184,9 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tgit_config(send_pack_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);\n \tif (argc > 0) {\ndiff --git a/builtin/upload-archive.c b/builtin/upload-archive.c\nindex 25d9116356..791cbe80a6 100644\n--- a/builtin/upload-archive.c\n+++ b/builtin/upload-archive.c\n@@ -5,6 +5,7 @@\n #include \"builtin.h\"\n #include \"archive.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n #include \"argv-array.h\"\n@@ -80,6 +81,8 @@ int cmd_upload_archive(int argc, const char **argv, const char *prefix)\n \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n \t\tusage(upload_archive_usage);\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \t/*\n \t * Set up sideband subprocess.\n \t *\ndiff --git a/builtin/upload-pack.c b/builtin/upload-pack.c\nindex 42dc4da5a1..293dd45b9e 100644\n--- a/builtin/upload-pack.c\n+++ b/builtin/upload-pack.c\n@@ -33,6 +33,10 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)\n \tpacket_trace_identity(\"upload-pack\");\n \tread_replace_refs = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, NULL, options, upload_pack_usage, 0);\n \n \tif (argc != 1)\ndiff --git a/connect.c b/connect.c\nindex 94547e5056..57266b6cec 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n  */\n static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t\t\t\t     const char *path, const char *prog,\n-\t\t\t\t\t     enum protocol_version version,\n+\t\t\t\t\t     const struct strbuf *version_advert,\n \t\t\t\t\t     int flags)\n {\n \tstruct child_process *conn;\n@@ -1084,12 +1084,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t    prog, path, 0,\n \t\t    target_host, 0);\n \n-\t/* If using a new version put that stuff here after a second null byte */\n-\tif (version > 0) {\n-\t\tstrbuf_addch(&request, '\\0');\n-\t\tstrbuf_addf(&request, \"version=%d%c\",\n-\t\t\t    version, '\\0');\n-\t}\n+\t/* Add version fields after a second null byte */\n+\tstrbuf_addch(&request, '\\0');\n+\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n \n \tpacket_write(fd[1], request.buf, request.len);\n \n@@ -1104,14 +1101,13 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n  */\n static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \t\t\t     enum ssh_variant variant, const char *port,\n-\t\t\t     enum protocol_version version, int flags)\n+\t\t\t     const struct strbuf *version_advert, int flags)\n {\n-\tif (variant == VARIANT_SSH &&\n-\t    version > 0) {\n+\tif (variant == VARIANT_SSH) {\n \t\targv_array_push(args, \"-o\");\n \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n-\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t version);\n+\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t version_advert->buf);\n \t}\n \n \tif (flags & CONNECT_IPV4) {\n@@ -1164,7 +1160,7 @@ static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \n /* Prepare a child_process for use by Git's SSH-tunneled transport. */\n static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n-\t\t\t  const char *port, enum protocol_version version,\n+\t\t\t  const char *port, const struct strbuf *version_advert,\n \t\t\t  int flags)\n {\n \tconst char *ssh;\n@@ -1198,15 +1194,16 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \n \t\targv_array_push(&detect.args, ssh);\n \t\targv_array_push(&detect.args, \"-G\");\n-\t\tpush_ssh_options(&detect.args, &detect.env_array,\n-\t\t\t\t VARIANT_SSH, port, version, flags);\n+\t\tpush_ssh_options(&detect.args, &detect.env_array, VARIANT_SSH,\n+\t\t\t\t port, version_advert, flags);\n \t\targv_array_push(&detect.args, ssh_host);\n \n \t\tvariant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;\n \t}\n \n \targv_array_push(&conn->args, ssh);\n-\tpush_ssh_options(&conn->args, &conn->env_array, variant, port, version, flags);\n+\tpush_ssh_options(&conn->args, &conn->env_array, variant, port,\n+\t\t\t version_advert, flags);\n \targv_array_push(&conn->args, ssh_host);\n }\n \n@@ -1226,16 +1223,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tenum protocol protocol;\n-\tenum protocol_version version = get_protocol_version_config();\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\n@@ -1250,7 +1241,8 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n \t} else if (protocol == PROTO_GIT) {\n-\t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n+\t\tconn = git_connect_git(fd, hostandport, path, prog,\n+\t\t\t\t       &version_advert, flags);\n \t} else {\n \t\tstruct strbuf cmd = STRBUF_INIT;\n \t\tconst char *const *var;\n@@ -1293,13 +1285,13 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n-\t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\n+\t\t\tfill_ssh_args(conn, ssh_host, port, &version_advert,\n+\t\t\t\t      flags);\n \t\t} else {\n \t\t\ttransport_check_allowed(\"file\");\n-\t\t\tif (version > 0) {\n-\t\t\t\targv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t\t\t version);\n-\t\t\t}\n+\t\t\targv_array_pushf(&conn->env_array,\n+\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t\t version_advert.buf);\n \t\t}\n \t\targv_array_push(&conn->args, cmd.buf);\n \ndiff --git a/protocol.c b/protocol.c\nindex 5e636785d1..5664bd7a05 100644\n--- a/protocol.c\n+++ b/protocol.c\n@@ -2,18 +2,43 @@\n #include \"config.h\"\n #include \"protocol.h\"\n \n+static enum protocol_version *allowed_versions;\n+static int nr_allowed_versions;\n+static int alloc_allowed_versions;\n+static int version_registration_locked = 0;\n+\n+static const char protocol_v0_string[] = \"0\";\n+static const char protocol_v1_string[] = \"1\";\n+static const char protocol_v2_string[] = \"2\";\n+\n static enum protocol_version parse_protocol_version(const char *value)\n {\n-\tif (!strcmp(value, \"0\"))\n+\tif (!strcmp(value, protocol_v0_string))\n \t\treturn protocol_v0;\n-\telse if (!strcmp(value, \"1\"))\n+\telse if (!strcmp(value, protocol_v1_string))\n \t\treturn protocol_v1;\n-\telse if (!strcmp(value, \"2\"))\n+\telse if (!strcmp(value, protocol_v2_string))\n \t\treturn protocol_v2;\n \telse\n \t\treturn protocol_unknown_version;\n }\n \n+/* Return the text representation of a wire protocol version. */\n+static const char *format_protocol_version(enum protocol_version version)\n+{\n+\tswitch (version) {\n+\tcase protocol_v0:\n+\t\treturn protocol_v0_string;\n+\tcase protocol_v1:\n+\t\treturn protocol_v1_string;\n+\tcase protocol_v2:\n+\t\treturn protocol_v2_string;\n+\tcase protocol_unknown_version:\n+\t\tdie(_(\"Unrecognized protocol version\"));\n+\t}\n+\tdie(_(\"Unrecognized protocol_version\"));\n+}\n+\n enum protocol_version get_protocol_version_config(void)\n {\n \tconst char *value;\n@@ -30,6 +55,85 @@ enum protocol_version get_protocol_version_config(void)\n \treturn protocol_v0;\n }\n \n+void register_allowed_protocol_version(enum protocol_version version)\n+{\n+\tif (version_registration_locked)\n+\t\tBUG(\"late attempt to register an allowed protocol version\");\n+\n+\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n+\t\t   alloc_allowed_versions);\n+\tallowed_versions[nr_allowed_versions++] = version;\n+}\n+\n+void register_allowed_protocol_versions_from_env(void)\n+{\n+\tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n+\tconst char *version_str;\n+\tstruct string_list version_list = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *version;\n+\n+\tif (!git_protocol)\n+\t\treturn;\n+\n+\tstring_list_split(&version_list, git_protocol, ':', -1);\n+\tfor_each_string_list_item(version, &version_list) {\n+\t\tif (skip_prefix(version->string, \"version=\", &version_str))\n+\t\t\tregister_allowed_protocol_version(\n+\t\t\t\tparse_protocol_version(version_str));\n+\t}\n+\tstring_list_clear(&version_list, 0);\n+}\n+\n+static int is_allowed_protocol_version(enum protocol_version version)\n+{\n+\tint i;\n+\tversion_registration_locked = 1;\n+\tfor (i = 0; i < nr_allowed_versions; i++)\n+\t\tif (version == allowed_versions[i])\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+void get_client_protocol_version_advertisement(struct strbuf *advert)\n+{\n+\tint i, tmp_nr = nr_allowed_versions;\n+\tenum protocol_version *tmp_allowed_versions, config_version;\n+\tstrbuf_reset(advert);\n+\n+\tversion_registration_locked = 1;\n+\n+\tconfig_version = get_protocol_version_config();\n+\tif (config_version == protocol_v0) {\n+\t\tstrbuf_addstr(advert, \"version=0\");\n+\t\treturn;\n+\t}\n+\n+\tif (tmp_nr > 0) {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n+\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n+\t\t\t   sizeof(enum protocol_version));\n+\t} else {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n+\t\ttmp_nr = 1;\n+\t\ttmp_allowed_versions[0] = config_version;\n+\t}\n+\n+\tif (tmp_allowed_versions[0] != config_version)\n+\t\tfor (i = 1; i < nr_allowed_versions; i++)\n+\t\t\tif (tmp_allowed_versions[i] == config_version) {\n+\t\t\t\tSWAP(tmp_allowed_versions[0],\n+\t\t\t\t     tmp_allowed_versions[i]);\n+\t\t\t}\n+\n+\tstrbuf_addf(advert, \"version=%s\",\n+\t\t    format_protocol_version(tmp_allowed_versions[0]));\n+\tfor (i = 1; i < tmp_nr; i++)\n+\t\tstrbuf_addf(advert, \":version=%s\",\n+\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n+\n+\tfree(tmp_allowed_versions);\n+}\n+\n enum protocol_version determine_protocol_version_server(void)\n {\n \tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n@@ -38,9 +142,10 @@ enum protocol_version determine_protocol_version_server(void)\n \t/*\n \t * Determine which protocol version the client has requested.  Since\n \t * multiple 'version' keys can be sent by the client, indicating that\n-\t * the client is okay to speak any of them, select the greatest version\n-\t * that the client has requested.  This is due to the assumption that\n-\t * the most recent protocol version will be the most state-of-the-art.\n+\t * the client is okay to speak any of them, select the first\n+\t * recognizable version that the client has requested.  This is due to\n+\t * the assumption that the protocol versions will be listed in\n+\t * preference order.\n \t */\n \tif (git_protocol) {\n \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -53,8 +158,11 @@ enum protocol_version determine_protocol_version_server(void)\n \n \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n \t\t\t\tv = parse_protocol_version(value);\n-\t\t\t\tif (v > version)\n+\t\t\t\tif (v != protocol_unknown_version &&\n+\t\t\t\t    is_allowed_protocol_version(v)) {\n \t\t\t\t\tversion = v;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \ndiff --git a/protocol.h b/protocol.h\nindex 2ad35e433c..88330fd0ee 100644\n--- a/protocol.h\n+++ b/protocol.h\n@@ -14,7 +14,24 @@ enum protocol_version {\n  * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n  * returned.\n  */\n-extern enum protocol_version get_protocol_version_config(void);\n+enum protocol_version get_protocol_version_config(void);\n+\n+/*\n+ * Register an allowable protocol version for a given operation. Registration\n+ * must occur before attempting to advertise a version to a server process.\n+ */\n+void register_allowed_protocol_version(enum protocol_version version);\n+\n+/*\n+ * Register allowable protocol versions from the GIT_PROTOCOL environment var.\n+ */\n+void register_allowed_protocol_versions_from_env(void);\n+\n+/*\n+ * Fill a strbuf with a version advertisement string suitable for use in the\n+ * GIT_PROTOCOL environment variable or similar version negotiation field.\n+ */\n+void get_client_protocol_version_advertisement(struct strbuf *advert);\n \n /*\n  * Used by a server to determine which protocol version should be used based on\n@@ -23,12 +40,12 @@ extern enum protocol_version get_protocol_version_config(void);\n  * request a particular protocol version, a default of 'protocol_v0' will be\n  * used.\n  */\n-extern enum protocol_version determine_protocol_version_server(void);\n+enum protocol_version determine_protocol_version_server(void);\n \n /*\n  * Used by a client to determine which protocol version the server is speaking\n  * based on the server's initial response.\n  */\n-extern enum protocol_version determine_protocol_version_client(const char *server_response);\n+enum protocol_version determine_protocol_version_client(const char *server_response);\n \n #endif /* PROTOCOL_H */\ndiff --git a/remote-curl.c b/remote-curl.c\nindex fb28309e85..6ffefe5169 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -330,6 +330,19 @@ static int get_protocol_http_header(enum protocol_version version,\n \treturn 0;\n }\n \n+static int get_client_protocol_http_header(const struct strbuf *version_advert,\n+\t\t\t\t\t   struct strbuf *header)\n+{\n+\tif (version_advert->len > 0) {\n+\t\tstrbuf_addf(header, GIT_PROTOCOL_HEADER \": %s\",\n+\t\t\t    version_advert->buf);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static struct discovery *discover_refs(const char *service, int for_push)\n {\n \tstruct strbuf exp = STRBUF_INIT;\n@@ -339,11 +352,11 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \tstruct strbuf refs_url = STRBUF_INIT;\n \tstruct strbuf effective_url = STRBUF_INIT;\n \tstruct strbuf protocol_header = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct string_list extra_headers = STRING_LIST_INIT_DUP;\n \tstruct discovery *last = last_discovery;\n \tint http_ret, maybe_smart = 0;\n \tstruct http_get_options http_options;\n-\tenum protocol_version version = get_protocol_version_config();\n \n \tif (last && !strcmp(service, last->service))\n \t\treturn last;\n@@ -360,16 +373,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n \t}\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Add the extra Git-Protocol header */\n-\tif (get_protocol_http_header(version, &protocol_header))\n+\tif (get_client_protocol_http_header(&version_advert, &protocol_header))\n \t\tstring_list_append(&extra_headers, protocol_header.buf);\n \n \tmemset(&http_options, 0, sizeof(http_options));\n@@ -1327,6 +1334,8 @@ int cmd_main(int argc, const char **argv)\n \tstruct strbuf buf = STRBUF_INIT;\n \tint nongit;\n \n+\tregister_allowed_protocol_versions_from_env();\n+\n \tsetup_git_directory_gently(&nongit);\n \tif (argc < 2) {\n \t\terror(\"remote-curl: usage: git remote-curl <remote> [<url>]\");\ndiff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh\nindex 771f36f9ff..343bf3aafa 100755\n--- a/t/t5551-http-fetch-smart.sh\n+++ b/t/t5551-http-fetch-smart.sh\n@@ -28,6 +28,7 @@ cat >exp <<EOF\n > Accept: */*\n > Accept-Encoding: ENCODINGS\n > Pragma: no-cache\n+> Git-Protocol: version=0\n < HTTP/1.1 200 OK\n < Pragma: no-cache\n < Cache-Control: no-cache, max-age=0, must-revalidate\ndiff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh\nindex 7466aad111..d528e91630 100755\n--- a/t/t5570-git-daemon.sh\n+++ b/t/t5570-git-daemon.sh\n@@ -186,7 +186,7 @@ test_expect_success 'hostname cannot break out of directory' '\n test_expect_success 'daemon log records all attributes' '\n \tcat >expect <<-\\EOF &&\n \tExtended attribute \"host\": localhost\n-\tExtended attribute \"protocol\": version=1\n+\tExtended attribute \"protocol\": version=1:version=2:version=0\n \tEOF\n \t>daemon.log &&\n \tGIT_OVERRIDE_VIRTUAL_HOST=localhost \\\ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex ddaa96ac4f..c4dbf1f779 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -346,7 +346,7 @@ expect_ssh () {\n \n test_expect_success 'clone myhost:src uses ssh' '\n \tgit clone myhost:src ssh-clone &&\n-\texpect_ssh myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n '\n \n test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n@@ -357,12 +357,12 @@ test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n \n test_expect_success 'bracketed hostnames are still ssh' '\n \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'OpenSSH variant passes -4' '\n \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n-\texpect_ssh \"-4 -p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n '\n \n test_expect_success 'variant can be overridden' '\n@@ -406,7 +406,7 @@ test_expect_success 'OpenSSH-like uplink is treated as ssh' '\n \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'plink is treated specially (as putty)' '\n@@ -446,14 +446,14 @@ test_expect_success 'GIT_SSH_VARIANT overrides plink detection' '\n \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n \tGIT_SSH_VARIANT=ssh \\\n \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'ssh.variant overrides plink detection' '\n \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n \tgit -c ssh.variant=ssh \\\n \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n@@ -488,7 +488,7 @@ test_clone_url () {\n }\n \n test_expect_success !MINGW 'clone c:temp is ssl' '\n-\ttest_clone_url c:temp c temp\n+\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n '\n \n test_expect_success MINGW 'clone c:temp is dos drive' '\n@@ -499,7 +499,7 @@ test_expect_success MINGW 'clone c:temp is dos drive' '\n for repo in rep rep/home/project 123\n do\n \ttest_expect_success \"clone host:$repo\" '\n-\t\ttest_clone_url host:$repo host $repo\n+\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n \t'\n done\n \n@@ -507,16 +507,16 @@ done\n for repo in rep rep/home/project 123\n do\n \ttest_expect_success \"clone [::1]:$repo\" '\n-\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n+\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n \t'\n done\n #home directory\n test_expect_success \"clone host:/~repo\" '\n-\ttest_clone_url host:/~repo host \"~repo\"\n+\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n '\n \n test_expect_success \"clone [::1]:/~repo\" '\n-\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n+\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n '\n \n # Corner cases\n@@ -532,22 +532,22 @@ done\n for tcol in \"\" :\n do\n \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n-\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n+\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n \t'\n \t# from home directory\n \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n-\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n+\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n '\n done\n \n # with port number\n test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n-\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n+\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n '\n \n # from home directory with port number\n test_expect_success 'clone ssh://host.xz:22/~repo' '\n-\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n+\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n '\n \n #IPv6\n@@ -555,7 +555,7 @@ for tuah in ::1 [::1] [::1]: user@::1 user@[::1] user@[::1]: [user@::1] [user@::\n do\n \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n-\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n+\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n \t\"\n done\n \n@@ -564,7 +564,7 @@ for tuah in ::1 [::1] user@::1 user@[::1] [user@::1]\n do\n \teuah=$(echo $tuah | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n-\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n+\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n \t\"\n done\n \n@@ -573,7 +573,7 @@ for tuah in [::1] user@[::1] [user@::1]\n do\n \teuah=$(echo $tuah | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n-\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n+\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n \t\"\n done\n \n@@ -582,7 +582,7 @@ for tuah in [::1] user@[::1] [user@::1]\n do\n \teuah=$(echo $tuah | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n-\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n+\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n \t\"\n done\n \ndiff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh\nindex ba86a44eb1..2e56c79233 100755\n--- a/t/t5700-protocol-v1.sh\n+++ b/t/t5700-protocol-v1.sh\n@@ -26,7 +26,7 @@ test_expect_success 'clone with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"clone< version 1\" log\n '\n@@ -42,7 +42,7 @@ test_expect_success 'fetch with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -56,7 +56,7 @@ test_expect_success 'pull with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'push with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"push> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"push> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"push< version 1\" log\n '\ndiff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh\nindex 3beeed4546..78c17c25e4 100755\n--- a/t/t5702-protocol-v2.sh\n+++ b/t/t5702-protocol-v2.sh\n@@ -24,7 +24,7 @@ test_expect_success 'list refs with git:// using protocol v2' '\n \t\tls-remote --symref \"$GIT_DAEMON_URL/parent\" >actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"git> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"git> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"git< version 2\" log &&\n \n@@ -56,7 +56,7 @@ test_expect_success 'clone with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"clone< version 2\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'fetch with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -90,7 +90,7 @@ test_expect_success 'pull with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -476,7 +476,7 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \ttest_when_finished \"rm -f log\" &&\n \t# Till v2 for push is designed, make sure that if a client has\n \t# protocol.version configured to use v2, that the client instead falls\n-\t# back and uses v0.\n+\t# back to previous versions.\n \n \ttest_commit -C http_child three &&\n \n@@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n \ttest_cmp expect actual &&\n \n-\t# Client didnt request to use protocol v2\n-\t! grep \"Git-Protocol: version=2\" log &&\n-\t# Server didnt respond using protocol v2\n-\t! grep \"git< version 2\" log\n+\t# Server responded with version 1\n+\tgrep \"git< version 1\" log\n '\n \n \ndiff --git a/transport-helper.c b/transport-helper.c\nindex 143ca008c8..ac1937f1e1 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -105,6 +105,7 @@ static struct child_process *get_helper(struct transport *transport)\n {\n \tstruct helper_data *data = transport->data;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct child_process *helper;\n \tint duped;\n \tint code;\n@@ -127,6 +128,11 @@ static struct child_process *get_helper(struct transport *transport)\n \t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n \t\t\t\t GIT_DIR_ENVIRONMENT, get_git_dir());\n \n+\tget_client_protocol_version_advertisement(&version_advert);\n+\tif (version_advert.len > 0)\n+\t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n+\t\t\t\t GIT_PROTOCOL_ENVIRONMENT, version_advert.buf);\n+\n \tcode = start_command(helper);\n \tif (code < 0 && errno == ENOENT)\n \t\tdie(_(\"unable to find remote helper for '%s'\"), data->name);\n-- \n2.19.1.1215.g8438c0b245-goog\n\n"},{"id":"365370","messageId":"87imzv273e.fsf@evledraar.gmail.com","threadId":"49473","inReplyTo":"60f6f2fbd8ee03b2d461803b9313b7473300eecc.1542407348.git.steadmon@google.com","subject":"Re: [PATCH v5 1/1] protocol: advertise multiple supported versions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-12-14T20:20:37Z","receivedAt":"2018-12-14T20:20:44Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Nov 16 2018, Josh Steadmon wrote:\n\nI started looking at this to address\nhttps://public-inbox.org/git/nycvar.QRO.7.76.6.1812141318520.43@tvgsbejvaqbjf.bet/\nand was about to send a re-roll of my own series, but then...\n\n> Currently the client advertises that it supports the wire protocol\n> version set in the protocol.version config. However, not all services\n> support the same set of protocol versions. For example, git-receive-pack\n> supports v1 and v0, but not v2. If a client connects to git-receive-pack\n> and requests v2, it will instead be downgraded to v0. Other services,\n> such as git-upload-archive, do not do any version negotiation checks.\n>\n> This patch creates a protocol version registry. Individual client and\n> server programs register all the protocol versions they support prior to\n> communicating with a remote instance. Versions should be listed in\n> preference order; the version specified in protocol.version will\n> automatically be moved to the front of the registry.\n>\n> The protocol version registry is passed to remote helpers via the\n> GIT_PROTOCOL environment variable.\n>\n> Clients now advertise the full list of registered versions. Servers\n> select the first allowed version from this advertisement.\n>\n> Additionally, remove special cases around advertising version=0.\n> Previously we avoided adding version negotiation fields in server\n> responses if it looked like the client wanted v0. However, including\n> these fields does not change behavior, so it's better to have simpler\n> code.\n\n...this paragraph is new in your v5, from the cover letter: \"Changes\nfrom V4: remove special cases around advertising version=0\". However as\nseen in the code & tests:\n\n> [...]\n>  static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n>  \t\t\t     enum ssh_variant variant, const char *port,\n> -\t\t\t     enum protocol_version version, int flags)\n> +\t\t\t     const struct strbuf *version_advert, int flags)\n>  {\n> -\tif (variant == VARIANT_SSH &&\n> -\t    version > 0) {\n> +\tif (variant == VARIANT_SSH) {\n>  \t\targv_array_push(args, \"-o\");\n>  \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n> -\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n> -\t\t\t\t version);\n> +\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n> +\t\t\t\t version_advert->buf);\n>  \t}\n> [...]\n> --- a/t/t5601-clone.sh\n> +++ b/t/t5601-clone.sh\n> @@ -346,7 +346,7 @@ expect_ssh () {\n>\n>  test_expect_success 'clone myhost:src uses ssh' '\n>  \tgit clone myhost:src ssh-clone &&\n> -\texpect_ssh myhost src\n> +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n>  '\n>\n>  test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n> @@ -357,12 +357,12 @@ test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n>\n>  test_expect_success 'bracketed hostnames are still ssh' '\n>  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n> -\texpect_ssh \"-p 123\" myhost src\n> +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>  '\n>\n>  test_expect_success 'OpenSSH variant passes -4' '\n>  \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n> -\texpect_ssh \"-4 -p 123\" myhost src\n> +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n>  '\n>\n>  test_expect_success 'variant can be overridden' '\n> @@ -406,7 +406,7 @@ test_expect_success 'OpenSSH-like uplink is treated as ssh' '\n>  \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n>  \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n>  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n> -\texpect_ssh \"-p 123\" myhost src\n> +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>  '\n>\n>  test_expect_success 'plink is treated specially (as putty)' '\n> @@ -446,14 +446,14 @@ test_expect_success 'GIT_SSH_VARIANT overrides plink detection' '\n>  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n>  \tGIT_SSH_VARIANT=ssh \\\n>  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n> -\texpect_ssh \"-p 123\" myhost src\n> +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>  '\n>\n>  test_expect_success 'ssh.variant overrides plink detection' '\n>  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n>  \tgit -c ssh.variant=ssh \\\n>  \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n> -\texpect_ssh \"-p 123\" myhost src\n> +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>  '\n>\n>  test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n> @@ -488,7 +488,7 @@ test_clone_url () {\n>  }\n>\n>  test_expect_success !MINGW 'clone c:temp is ssl' '\n> -\ttest_clone_url c:temp c temp\n> +\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n>  '\n>\n>  test_expect_success MINGW 'clone c:temp is dos drive' '\n> @@ -499,7 +499,7 @@ test_expect_success MINGW 'clone c:temp is dos drive' '\n>  for repo in rep rep/home/project 123\n>  do\n>  \ttest_expect_success \"clone host:$repo\" '\n> -\t\ttest_clone_url host:$repo host $repo\n> +\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n>  \t'\n>  done\n>\n> @@ -507,16 +507,16 @@ done\n>  for repo in rep rep/home/project 123\n>  do\n>  \ttest_expect_success \"clone [::1]:$repo\" '\n> -\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n> +\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n>  \t'\n>  done\n>  #home directory\n>  test_expect_success \"clone host:/~repo\" '\n> -\ttest_clone_url host:/~repo host \"~repo\"\n> +\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n>  '\n>\n>  test_expect_success \"clone [::1]:/~repo\" '\n> -\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n> +\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n>  '\n>\n>  # Corner cases\n> @@ -532,22 +532,22 @@ done\n>  for tcol in \"\" :\n>  do\n>  \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n> -\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n> +\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n>  \t'\n>  \t# from home directory\n>  \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n> -\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n> +\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n>  '\n>  done\n>\n>  # with port number\n>  test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n> -\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n> +\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n>  '\n>\n>  # from home directory with port number\n>  test_expect_success 'clone ssh://host.xz:22/~repo' '\n> -\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n> +\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n>  '\n>\n>  #IPv6\n> @@ -555,7 +555,7 @@ for tuah in ::1 [::1] [::1]: user@::1 user@[::1] user@[::1]: [user@::1] [user@::\n>  do\n>  \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n>  \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n> -\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n> +\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n>  \t\"\n>  done\n>\n> @@ -564,7 +564,7 @@ for tuah in ::1 [::1] user@::1 user@[::1] [user@::1]\n>  do\n>  \teuah=$(echo $tuah | tr -d \"[]\")\n>  \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n> -\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n> +\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n>  \t\"\n>  done\n>\n> @@ -573,7 +573,7 @@ for tuah in [::1] user@[::1] [user@::1]\n>  do\n>  \teuah=$(echo $tuah | tr -d \"[]\")\n>  \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n> -\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n> +\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n>  \t\"\n>  done\n>\n> @@ -582,7 +582,7 @@ for tuah in [::1] user@[::1] [user@::1]\n>  do\n>  \teuah=$(echo $tuah | tr -d \"[]\")\n>  \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n> -\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n> +\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n>  \t\"\n>  done\n\n...so now we're unconditionally going to SendEnv=GIT_PROTOCOL to \"ssh\"\ninvocations. I don't have an issue with this, but given the change in\nthe commit message this seems to have snuck under the radar. You're just\ntalking about always including the version in server responses, nothing\nabout the client always needing SendEnv=GIT_PROTOCOL now even with v0.\n\nIf the server always sends the version now, why don't you need to amend\nthe same t5400-send-pack.sh tests as in my \"tests: mark & fix tests\nbroken under GIT_TEST_PROTOCOL_VERSION=1\"? There's one that spews out\n\"version\" there under my GIT_TEST_PROTOCOL_VERSION=1.\n\nI was worried about this breaking GIT_SSH_COMMAND, but then I see due to\nan interaction with picking \"what ssh implementation?\" we don't pass \"-G\n-o SendEnv=GIT_PROTOCOL\" at all when I have a GIT_SSH_COMMAND, but *do*\npass it to my normal /usr/bin/ssh. Is this intended? Now if I have a\nGIT_SSH_COMMAND that expects to wrap openssh I need to pass \"-c\nssh.variant=ssh\", because \"-c ssh.variant=auto\" will now omit these new\narguments.\n"},{"id":"365385","messageId":"20181214215804.GF37614@google.com","threadId":"49473","inReplyTo":"87imzv273e.fsf@evledraar.gmail.com","subject":"Re: [PATCH v5 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-12-14T21:58:04Z","receivedAt":"2018-12-14T21:58:14Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.12.14 21:20, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Nov 16 2018, Josh Steadmon wrote:\n> \n> I started looking at this to address\n> https://public-inbox.org/git/nycvar.QRO.7.76.6.1812141318520.43@tvgsbejvaqbjf.bet/\n> and was about to send a re-roll of my own series, but then...\n> \n> > Currently the client advertises that it supports the wire protocol\n> > version set in the protocol.version config. However, not all services\n> > support the same set of protocol versions. For example, git-receive-pack\n> > supports v1 and v0, but not v2. If a client connects to git-receive-pack\n> > and requests v2, it will instead be downgraded to v0. Other services,\n> > such as git-upload-archive, do not do any version negotiation checks.\n> >\n> > This patch creates a protocol version registry. Individual client and\n> > server programs register all the protocol versions they support prior to\n> > communicating with a remote instance. Versions should be listed in\n> > preference order; the version specified in protocol.version will\n> > automatically be moved to the front of the registry.\n> >\n> > The protocol version registry is passed to remote helpers via the\n> > GIT_PROTOCOL environment variable.\n> >\n> > Clients now advertise the full list of registered versions. Servers\n> > select the first allowed version from this advertisement.\n> >\n> > Additionally, remove special cases around advertising version=0.\n> > Previously we avoided adding version negotiation fields in server\n> > responses if it looked like the client wanted v0. However, including\n> > these fields does not change behavior, so it's better to have simpler\n> > code.\n> \n> ...this paragraph is new in your v5, from the cover letter: \"Changes\n> from V4: remove special cases around advertising version=0\". However as\n> seen in the code & tests:\n> \n> > [...]\n> >  static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n> >  \t\t\t     enum ssh_variant variant, const char *port,\n> > -\t\t\t     enum protocol_version version, int flags)\n> > +\t\t\t     const struct strbuf *version_advert, int flags)\n> >  {\n> > -\tif (variant == VARIANT_SSH &&\n> > -\t    version > 0) {\n> > +\tif (variant == VARIANT_SSH) {\n> >  \t\targv_array_push(args, \"-o\");\n> >  \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n> > -\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n> > -\t\t\t\t version);\n> > +\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n> > +\t\t\t\t version_advert->buf);\n> >  \t}\n> > [...]\n> > --- a/t/t5601-clone.sh\n> > +++ b/t/t5601-clone.sh\n> > @@ -346,7 +346,7 @@ expect_ssh () {\n> >\n> >  test_expect_success 'clone myhost:src uses ssh' '\n> >  \tgit clone myhost:src ssh-clone &&\n> > -\texpect_ssh myhost src\n> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n> >  '\n> >\n> >  test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n> > @@ -357,12 +357,12 @@ test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n> >\n> >  test_expect_success 'bracketed hostnames are still ssh' '\n> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n> > -\texpect_ssh \"-p 123\" myhost src\n> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >  '\n> >\n> >  test_expect_success 'OpenSSH variant passes -4' '\n> >  \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n> > -\texpect_ssh \"-4 -p 123\" myhost src\n> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n> >  '\n> >\n> >  test_expect_success 'variant can be overridden' '\n> > @@ -406,7 +406,7 @@ test_expect_success 'OpenSSH-like uplink is treated as ssh' '\n> >  \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n> >  \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n> > -\texpect_ssh \"-p 123\" myhost src\n> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >  '\n> >\n> >  test_expect_success 'plink is treated specially (as putty)' '\n> > @@ -446,14 +446,14 @@ test_expect_success 'GIT_SSH_VARIANT overrides plink detection' '\n> >  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n> >  \tGIT_SSH_VARIANT=ssh \\\n> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n> > -\texpect_ssh \"-p 123\" myhost src\n> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >  '\n> >\n> >  test_expect_success 'ssh.variant overrides plink detection' '\n> >  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n> >  \tgit -c ssh.variant=ssh \\\n> >  \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n> > -\texpect_ssh \"-p 123\" myhost src\n> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >  '\n> >\n> >  test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n> > @@ -488,7 +488,7 @@ test_clone_url () {\n> >  }\n> >\n> >  test_expect_success !MINGW 'clone c:temp is ssl' '\n> > -\ttest_clone_url c:temp c temp\n> > +\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n> >  '\n> >\n> >  test_expect_success MINGW 'clone c:temp is dos drive' '\n> > @@ -499,7 +499,7 @@ test_expect_success MINGW 'clone c:temp is dos drive' '\n> >  for repo in rep rep/home/project 123\n> >  do\n> >  \ttest_expect_success \"clone host:$repo\" '\n> > -\t\ttest_clone_url host:$repo host $repo\n> > +\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n> >  \t'\n> >  done\n> >\n> > @@ -507,16 +507,16 @@ done\n> >  for repo in rep rep/home/project 123\n> >  do\n> >  \ttest_expect_success \"clone [::1]:$repo\" '\n> > -\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n> > +\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n> >  \t'\n> >  done\n> >  #home directory\n> >  test_expect_success \"clone host:/~repo\" '\n> > -\ttest_clone_url host:/~repo host \"~repo\"\n> > +\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n> >  '\n> >\n> >  test_expect_success \"clone [::1]:/~repo\" '\n> > -\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n> > +\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n> >  '\n> >\n> >  # Corner cases\n> > @@ -532,22 +532,22 @@ done\n> >  for tcol in \"\" :\n> >  do\n> >  \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n> > -\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n> > +\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n> >  \t'\n> >  \t# from home directory\n> >  \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n> > -\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n> > +\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n> >  '\n> >  done\n> >\n> >  # with port number\n> >  test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n> > -\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n> > +\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n> >  '\n> >\n> >  # from home directory with port number\n> >  test_expect_success 'clone ssh://host.xz:22/~repo' '\n> > -\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n> > +\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n> >  '\n> >\n> >  #IPv6\n> > @@ -555,7 +555,7 @@ for tuah in ::1 [::1] [::1]: user@::1 user@[::1] user@[::1]: [user@::1] [user@::\n> >  do\n> >  \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n> >  \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n> > -\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n> > +\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n> >  \t\"\n> >  done\n> >\n> > @@ -564,7 +564,7 @@ for tuah in ::1 [::1] user@::1 user@[::1] [user@::1]\n> >  do\n> >  \teuah=$(echo $tuah | tr -d \"[]\")\n> >  \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n> > -\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n> > +\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n> >  \t\"\n> >  done\n> >\n> > @@ -573,7 +573,7 @@ for tuah in [::1] user@[::1] [user@::1]\n> >  do\n> >  \teuah=$(echo $tuah | tr -d \"[]\")\n> >  \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n> > -\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n> > +\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n> >  \t\"\n> >  done\n> >\n> > @@ -582,7 +582,7 @@ for tuah in [::1] user@[::1] [user@::1]\n> >  do\n> >  \teuah=$(echo $tuah | tr -d \"[]\")\n> >  \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n> > -\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n> > +\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n> >  \t\"\n> >  done\n> \n> ...so now we're unconditionally going to SendEnv=GIT_PROTOCOL to \"ssh\"\n> invocations. I don't have an issue with this, but given the change in\n> the commit message this seems to have snuck under the radar. You're just\n> talking about always including the version in server responses, nothing\n> about the client always needing SendEnv=GIT_PROTOCOL now even with v0.\n\nAck. I'll make sure that V6 calls this out in the commit message.\n\n\n> If the server always sends the version now, why don't you need to amend\n> the same t5400-send-pack.sh tests as in my \"tests: mark & fix tests\n> broken under GIT_TEST_PROTOCOL_VERSION=1\"? There's one that spews out\n> \"version\" there under my GIT_TEST_PROTOCOL_VERSION=1.\n\nSorry if I'm being dense, but can you point out more specifically what\nis wrong here? I don't see anything in t5400 that would be affected by\nthis; the final test case (\"receive-pack de-dupes .have lines\") is the\nonly one that does any tracing, and it strips out everything that's not\na ref advertisement before checking the output. Sorry if I'm missing\nsomething obvious.\n\n\n> I was worried about this breaking GIT_SSH_COMMAND, but then I see due to\n> an interaction with picking \"what ssh implementation?\" we don't pass \"-G\n> -o SendEnv=GIT_PROTOCOL\" at all when I have a GIT_SSH_COMMAND, but *do*\n> pass it to my normal /usr/bin/ssh. Is this intended? Now if I have a\n> GIT_SSH_COMMAND that expects to wrap openssh I need to pass \"-c\n> ssh.variant=ssh\", because \"-c ssh.variant=auto\" will now omit these new\n> arguments.\n\nI am not an expert on this part of the code, but it seems to be\nintended. Later on in the function, there are BUG()s that state that\nVARIANT_AUTO should not be passed to the push_ssh_options() function.\nAnd this only changes the behavior when version=0. For protocol versions\n1 & 2, VARIANT_AUTO never had SendEnv=GIT_PROTOCOL added to the command\nline. Again, sorry if I'm missing some implication here.\n\n\nThanks,\nJosh\n"},{"id":"365386","messageId":"87h8ff20ol.fsf@evledraar.gmail.com","threadId":"49473","inReplyTo":"20181214215804.GF37614@google.com","subject":"Re: [PATCH v5 1/1] protocol: advertise multiple supported versions","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-12-14T22:39:06Z","receivedAt":"2018-12-14T22:39:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Dec 14 2018, Josh Steadmon wrote:\n\n> On 2018.12.14 21:20, Ævar Arnfjörð Bjarmason wrote:\n>>\n>> On Fri, Nov 16 2018, Josh Steadmon wrote:\n>>\n>> I started looking at this to address\n>> https://public-inbox.org/git/nycvar.QRO.7.76.6.1812141318520.43@tvgsbejvaqbjf.bet/\n>> and was about to send a re-roll of my own series, but then...\n>>\n>> > Currently the client advertises that it supports the wire protocol\n>> > version set in the protocol.version config. However, not all services\n>> > support the same set of protocol versions. For example, git-receive-pack\n>> > supports v1 and v0, but not v2. If a client connects to git-receive-pack\n>> > and requests v2, it will instead be downgraded to v0. Other services,\n>> > such as git-upload-archive, do not do any version negotiation checks.\n>> >\n>> > This patch creates a protocol version registry. Individual client and\n>> > server programs register all the protocol versions they support prior to\n>> > communicating with a remote instance. Versions should be listed in\n>> > preference order; the version specified in protocol.version will\n>> > automatically be moved to the front of the registry.\n>> >\n>> > The protocol version registry is passed to remote helpers via the\n>> > GIT_PROTOCOL environment variable.\n>> >\n>> > Clients now advertise the full list of registered versions. Servers\n>> > select the first allowed version from this advertisement.\n>> >\n>> > Additionally, remove special cases around advertising version=0.\n>> > Previously we avoided adding version negotiation fields in server\n>> > responses if it looked like the client wanted v0. However, including\n>> > these fields does not change behavior, so it's better to have simpler\n>> > code.\n>>\n>> ...this paragraph is new in your v5, from the cover letter: \"Changes\n>> from V4: remove special cases around advertising version=0\". However as\n>> seen in the code & tests:\n>>\n>> > [...]\n>> >  static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n>> >  \t\t\t     enum ssh_variant variant, const char *port,\n>> > -\t\t\t     enum protocol_version version, int flags)\n>> > +\t\t\t     const struct strbuf *version_advert, int flags)\n>> >  {\n>> > -\tif (variant == VARIANT_SSH &&\n>> > -\t    version > 0) {\n>> > +\tif (variant == VARIANT_SSH) {\n>> >  \t\targv_array_push(args, \"-o\");\n>> >  \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n>> > -\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n>> > -\t\t\t\t version);\n>> > +\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n>> > +\t\t\t\t version_advert->buf);\n>> >  \t}\n>> > [...]\n>> > --- a/t/t5601-clone.sh\n>> > +++ b/t/t5601-clone.sh\n>> > @@ -346,7 +346,7 @@ expect_ssh () {\n>> >\n>> >  test_expect_success 'clone myhost:src uses ssh' '\n>> >  \tgit clone myhost:src ssh-clone &&\n>> > -\texpect_ssh myhost src\n>> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n>> >  '\n>> >\n>> >  test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n>> > @@ -357,12 +357,12 @@ test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n>> >\n>> >  test_expect_success 'bracketed hostnames are still ssh' '\n>> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n>> > -\texpect_ssh \"-p 123\" myhost src\n>> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>> >  '\n>> >\n>> >  test_expect_success 'OpenSSH variant passes -4' '\n>> >  \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n>> > -\texpect_ssh \"-4 -p 123\" myhost src\n>> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n>> >  '\n>> >\n>> >  test_expect_success 'variant can be overridden' '\n>> > @@ -406,7 +406,7 @@ test_expect_success 'OpenSSH-like uplink is treated as ssh' '\n>> >  \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n>> >  \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n>> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n>> > -\texpect_ssh \"-p 123\" myhost src\n>> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>> >  '\n>> >\n>> >  test_expect_success 'plink is treated specially (as putty)' '\n>> > @@ -446,14 +446,14 @@ test_expect_success 'GIT_SSH_VARIANT overrides plink detection' '\n>> >  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n>> >  \tGIT_SSH_VARIANT=ssh \\\n>> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n>> > -\texpect_ssh \"-p 123\" myhost src\n>> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>> >  '\n>> >\n>> >  test_expect_success 'ssh.variant overrides plink detection' '\n>> >  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n>> >  \tgit -c ssh.variant=ssh \\\n>> >  \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n>> > -\texpect_ssh \"-p 123\" myhost src\n>> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n>> >  '\n>> >\n>> >  test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n>> > @@ -488,7 +488,7 @@ test_clone_url () {\n>> >  }\n>> >\n>> >  test_expect_success !MINGW 'clone c:temp is ssl' '\n>> > -\ttest_clone_url c:temp c temp\n>> > +\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n>> >  '\n>> >\n>> >  test_expect_success MINGW 'clone c:temp is dos drive' '\n>> > @@ -499,7 +499,7 @@ test_expect_success MINGW 'clone c:temp is dos drive' '\n>> >  for repo in rep rep/home/project 123\n>> >  do\n>> >  \ttest_expect_success \"clone host:$repo\" '\n>> > -\t\ttest_clone_url host:$repo host $repo\n>> > +\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n>> >  \t'\n>> >  done\n>> >\n>> > @@ -507,16 +507,16 @@ done\n>> >  for repo in rep rep/home/project 123\n>> >  do\n>> >  \ttest_expect_success \"clone [::1]:$repo\" '\n>> > -\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n>> > +\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n>> >  \t'\n>> >  done\n>> >  #home directory\n>> >  test_expect_success \"clone host:/~repo\" '\n>> > -\ttest_clone_url host:/~repo host \"~repo\"\n>> > +\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n>> >  '\n>> >\n>> >  test_expect_success \"clone [::1]:/~repo\" '\n>> > -\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n>> > +\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n>> >  '\n>> >\n>> >  # Corner cases\n>> > @@ -532,22 +532,22 @@ done\n>> >  for tcol in \"\" :\n>> >  do\n>> >  \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n>> > -\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n>> > +\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n>> >  \t'\n>> >  \t# from home directory\n>> >  \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n>> > -\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n>> > +\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n>> >  '\n>> >  done\n>> >\n>> >  # with port number\n>> >  test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n>> > -\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n>> > +\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n>> >  '\n>> >\n>> >  # from home directory with port number\n>> >  test_expect_success 'clone ssh://host.xz:22/~repo' '\n>> > -\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n>> > +\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n>> >  '\n>> >\n>> >  #IPv6\n>> > @@ -555,7 +555,7 @@ for tuah in ::1 [::1] [::1]: user@::1 user@[::1] user@[::1]: [user@::1] [user@::\n>> >  do\n>> >  \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n>> >  \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n>> > -\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n>> > +\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n>> >  \t\"\n>> >  done\n>> >\n>> > @@ -564,7 +564,7 @@ for tuah in ::1 [::1] user@::1 user@[::1] [user@::1]\n>> >  do\n>> >  \teuah=$(echo $tuah | tr -d \"[]\")\n>> >  \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n>> > -\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n>> > +\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n>> >  \t\"\n>> >  done\n>> >\n>> > @@ -573,7 +573,7 @@ for tuah in [::1] user@[::1] [user@::1]\n>> >  do\n>> >  \teuah=$(echo $tuah | tr -d \"[]\")\n>> >  \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n>> > -\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n>> > +\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n>> >  \t\"\n>> >  done\n>> >\n>> > @@ -582,7 +582,7 @@ for tuah in [::1] user@[::1] [user@::1]\n>> >  do\n>> >  \teuah=$(echo $tuah | tr -d \"[]\")\n>> >  \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n>> > -\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n>> > +\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n>> >  \t\"\n>> >  done\n>>\n>> ...so now we're unconditionally going to SendEnv=GIT_PROTOCOL to \"ssh\"\n>> invocations. I don't have an issue with this, but given the change in\n>> the commit message this seems to have snuck under the radar. You're just\n>> talking about always including the version in server responses, nothing\n>> about the client always needing SendEnv=GIT_PROTOCOL now even with v0.\n>\n> Ack. I'll make sure that V6 calls this out in the commit message.\n>\n>\n>> If the server always sends the version now, why don't you need to amend\n>> the same t5400-send-pack.sh tests as in my \"tests: mark & fix tests\n>> broken under GIT_TEST_PROTOCOL_VERSION=1\"? There's one that spews out\n>> \"version\" there under my GIT_TEST_PROTOCOL_VERSION=1.\n>\n> Sorry if I'm being dense, but can you point out more specifically what\n> is wrong here? I don't see anything in t5400 that would be affected by\n> this; the final test case (\"receive-pack de-dupes .have lines\") is the\n> only one that does any tracing, and it strips out everything that's not\n> a ref advertisement before checking the output. Sorry if I'm missing\n> something obvious.\n\nI think I'm just misunderstanding this bit:\n\n    Additionally, remove special cases around advertising version=0.\n    Previously we avoided adding version negotiation fields in server\n    responses if it looked like the client wanted v0. However, including\n    these fields does not change behavior, so it's better to have\n    simpler code.\n\nI meant that if you pick the series I have up to \"tests: add a special\nsetup that sets protocol.version\", which is at c6f33984fc in a WIP\nbranch in github.com/avar/git.git and run:\n\n    $ GIT_TEST_PROTOCOL_VERSION=1 ./t5400-send-pack.sh -v -i -x\n\nIt'll fail with:\n\n    + test_cmp expect refs\n    + diff -u expect refs\n    --- expect      2018-12-14 22:26:52.485411992 +0000\n    +++ refs        2018-12-14 22:26:52.713412148 +0000\n    @@ -1,3 +1,4 @@\n    +version 1\n     5285e1710002a06a379056b0d21357a999f3c642 refs/heads/master\n     5285e1710002a06a379056b0d21357a999f3c642 refs/remotes/origin/HEAD\n     5285e1710002a06a379056b0d21357a999f3c642 refs/remotes/origin/master\n    error: last command exited with $?=1\n    not ok 16 - receive-pack de-dupes .have lines\n\nAnd I read your commit message to mean \"v0 clients also support v1\nresponses with the version in them, so let's just always send it\". But I\nthink this is my confusion (but I still don't know what it *really*\nmeans).\n\n>> I was worried about this breaking GIT_SSH_COMMAND, but then I see due to\n>> an interaction with picking \"what ssh implementation?\" we don't pass \"-G\n>> -o SendEnv=GIT_PROTOCOL\" at all when I have a GIT_SSH_COMMAND, but *do*\n>> pass it to my normal /usr/bin/ssh. Is this intended? Now if I have a\n>> GIT_SSH_COMMAND that expects to wrap openssh I need to pass \"-c\n>> ssh.variant=ssh\", because \"-c ssh.variant=auto\" will now omit these new\n>> arguments.\n>\n> I am not an expert on this part of the code, but it seems to be\n> intended. Later on in the function, there are BUG()s that state that\n> VARIANT_AUTO should not be passed to the push_ssh_options() function.\n> And this only changes the behavior when version=0. For protocol versions\n> 1 & 2, VARIANT_AUTO never had SendEnv=GIT_PROTOCOL added to the command\n> line. Again, sorry if I'm missing some implication here.\n\nI'm wondering if we need to worry about some new odd interactions\nbetween client/servers when using GIT_SSH* wrappers, maybe not, but\ne.g. as opposed to 0da0e49ba1 (\"ssh: 'auto' variant to select between\n'ssh' and 'simple'\", 2017-11-20) which noted a similar change had been\ntested at Google internally (and I read it as Google using GIT_SSH* on\nworkstations) your commit doesn't make any mention of this case being\ntested / considered.\n\nSo do we need to worry about some new interaction here? Maybe not. Just\nsomething for people more experienced with the ssh integration to chime\nin on.\n"},{"id":"365557","messageId":"20181218230514.GH37614@google.com","threadId":"49473","inReplyTo":"87h8ff20ol.fsf@evledraar.gmail.com","subject":"Re: [PATCH v5 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-12-18T23:05:14Z","receivedAt":"2018-12-18T23:05:23Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2018.12.14 23:39, Ævar Arnfjörð Bjarmason wrote:\n> \n> On Fri, Dec 14 2018, Josh Steadmon wrote:\n> \n> > On 2018.12.14 21:20, Ævar Arnfjörð Bjarmason wrote:\n> >>\n> >> On Fri, Nov 16 2018, Josh Steadmon wrote:\n> >>\n> >> I started looking at this to address\n> >> https://public-inbox.org/git/nycvar.QRO.7.76.6.1812141318520.43@tvgsbejvaqbjf.bet/\n> >> and was about to send a re-roll of my own series, but then...\n> >>\n> >> > Currently the client advertises that it supports the wire protocol\n> >> > version set in the protocol.version config. However, not all services\n> >> > support the same set of protocol versions. For example, git-receive-pack\n> >> > supports v1 and v0, but not v2. If a client connects to git-receive-pack\n> >> > and requests v2, it will instead be downgraded to v0. Other services,\n> >> > such as git-upload-archive, do not do any version negotiation checks.\n> >> >\n> >> > This patch creates a protocol version registry. Individual client and\n> >> > server programs register all the protocol versions they support prior to\n> >> > communicating with a remote instance. Versions should be listed in\n> >> > preference order; the version specified in protocol.version will\n> >> > automatically be moved to the front of the registry.\n> >> >\n> >> > The protocol version registry is passed to remote helpers via the\n> >> > GIT_PROTOCOL environment variable.\n> >> >\n> >> > Clients now advertise the full list of registered versions. Servers\n> >> > select the first allowed version from this advertisement.\n> >> >\n> >> > Additionally, remove special cases around advertising version=0.\n> >> > Previously we avoided adding version negotiation fields in server\n> >> > responses if it looked like the client wanted v0. However, including\n> >> > these fields does not change behavior, so it's better to have simpler\n> >> > code.\n> >>\n> >> ...this paragraph is new in your v5, from the cover letter: \"Changes\n> >> from V4: remove special cases around advertising version=0\". However as\n> >> seen in the code & tests:\n> >>\n> >> > [...]\n> >> >  static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n> >> >  \t\t\t     enum ssh_variant variant, const char *port,\n> >> > -\t\t\t     enum protocol_version version, int flags)\n> >> > +\t\t\t     const struct strbuf *version_advert, int flags)\n> >> >  {\n> >> > -\tif (variant == VARIANT_SSH &&\n> >> > -\t    version > 0) {\n> >> > +\tif (variant == VARIANT_SSH) {\n> >> >  \t\targv_array_push(args, \"-o\");\n> >> >  \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n> >> > -\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n> >> > -\t\t\t\t version);\n> >> > +\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n> >> > +\t\t\t\t version_advert->buf);\n> >> >  \t}\n> >> > [...]\n> >> > --- a/t/t5601-clone.sh\n> >> > +++ b/t/t5601-clone.sh\n> >> > @@ -346,7 +346,7 @@ expect_ssh () {\n> >> >\n> >> >  test_expect_success 'clone myhost:src uses ssh' '\n> >> >  \tgit clone myhost:src ssh-clone &&\n> >> > -\texpect_ssh myhost src\n> >> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n> >> >  '\n> >> >\n> >> >  test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n> >> > @@ -357,12 +357,12 @@ test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n> >> >\n> >> >  test_expect_success 'bracketed hostnames are still ssh' '\n> >> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n> >> > -\texpect_ssh \"-p 123\" myhost src\n> >> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >> >  '\n> >> >\n> >> >  test_expect_success 'OpenSSH variant passes -4' '\n> >> >  \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n> >> > -\texpect_ssh \"-4 -p 123\" myhost src\n> >> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n> >> >  '\n> >> >\n> >> >  test_expect_success 'variant can be overridden' '\n> >> > @@ -406,7 +406,7 @@ test_expect_success 'OpenSSH-like uplink is treated as ssh' '\n> >> >  \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n> >> >  \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n> >> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n> >> > -\texpect_ssh \"-p 123\" myhost src\n> >> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >> >  '\n> >> >\n> >> >  test_expect_success 'plink is treated specially (as putty)' '\n> >> > @@ -446,14 +446,14 @@ test_expect_success 'GIT_SSH_VARIANT overrides plink detection' '\n> >> >  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n> >> >  \tGIT_SSH_VARIANT=ssh \\\n> >> >  \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n> >> > -\texpect_ssh \"-p 123\" myhost src\n> >> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >> >  '\n> >> >\n> >> >  test_expect_success 'ssh.variant overrides plink detection' '\n> >> >  \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n> >> >  \tgit -c ssh.variant=ssh \\\n> >> >  \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n> >> > -\texpect_ssh \"-p 123\" myhost src\n> >> > +\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n> >> >  '\n> >> >\n> >> >  test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n> >> > @@ -488,7 +488,7 @@ test_clone_url () {\n> >> >  }\n> >> >\n> >> >  test_expect_success !MINGW 'clone c:temp is ssl' '\n> >> > -\ttest_clone_url c:temp c temp\n> >> > +\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n> >> >  '\n> >> >\n> >> >  test_expect_success MINGW 'clone c:temp is dos drive' '\n> >> > @@ -499,7 +499,7 @@ test_expect_success MINGW 'clone c:temp is dos drive' '\n> >> >  for repo in rep rep/home/project 123\n> >> >  do\n> >> >  \ttest_expect_success \"clone host:$repo\" '\n> >> > -\t\ttest_clone_url host:$repo host $repo\n> >> > +\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n> >> >  \t'\n> >> >  done\n> >> >\n> >> > @@ -507,16 +507,16 @@ done\n> >> >  for repo in rep rep/home/project 123\n> >> >  do\n> >> >  \ttest_expect_success \"clone [::1]:$repo\" '\n> >> > -\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n> >> > +\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n> >> >  \t'\n> >> >  done\n> >> >  #home directory\n> >> >  test_expect_success \"clone host:/~repo\" '\n> >> > -\ttest_clone_url host:/~repo host \"~repo\"\n> >> > +\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n> >> >  '\n> >> >\n> >> >  test_expect_success \"clone [::1]:/~repo\" '\n> >> > -\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n> >> > +\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n> >> >  '\n> >> >\n> >> >  # Corner cases\n> >> > @@ -532,22 +532,22 @@ done\n> >> >  for tcol in \"\" :\n> >> >  do\n> >> >  \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n> >> > -\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n> >> > +\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n> >> >  \t'\n> >> >  \t# from home directory\n> >> >  \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n> >> > -\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n> >> > +\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n> >> >  '\n> >> >  done\n> >> >\n> >> >  # with port number\n> >> >  test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n> >> > -\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n> >> > +\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n> >> >  '\n> >> >\n> >> >  # from home directory with port number\n> >> >  test_expect_success 'clone ssh://host.xz:22/~repo' '\n> >> > -\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n> >> > +\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n> >> >  '\n> >> >\n> >> >  #IPv6\n> >> > @@ -555,7 +555,7 @@ for tuah in ::1 [::1] [::1]: user@::1 user@[::1] user@[::1]: [user@::1] [user@::\n> >> >  do\n> >> >  \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n> >> >  \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n> >> > -\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n> >> > +\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n> >> >  \t\"\n> >> >  done\n> >> >\n> >> > @@ -564,7 +564,7 @@ for tuah in ::1 [::1] user@::1 user@[::1] [user@::1]\n> >> >  do\n> >> >  \teuah=$(echo $tuah | tr -d \"[]\")\n> >> >  \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n> >> > -\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n> >> > +\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n> >> >  \t\"\n> >> >  done\n> >> >\n> >> > @@ -573,7 +573,7 @@ for tuah in [::1] user@[::1] [user@::1]\n> >> >  do\n> >> >  \teuah=$(echo $tuah | tr -d \"[]\")\n> >> >  \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n> >> > -\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n> >> > +\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n> >> >  \t\"\n> >> >  done\n> >> >\n> >> > @@ -582,7 +582,7 @@ for tuah in [::1] user@[::1] [user@::1]\n> >> >  do\n> >> >  \teuah=$(echo $tuah | tr -d \"[]\")\n> >> >  \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n> >> > -\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n> >> > +\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n> >> >  \t\"\n> >> >  done\n> >>\n> >> ...so now we're unconditionally going to SendEnv=GIT_PROTOCOL to \"ssh\"\n> >> invocations. I don't have an issue with this, but given the change in\n> >> the commit message this seems to have snuck under the radar. You're just\n> >> talking about always including the version in server responses, nothing\n> >> about the client always needing SendEnv=GIT_PROTOCOL now even with v0.\n> >\n> > Ack. I'll make sure that V6 calls this out in the commit message.\n> >\n> >\n> >> If the server always sends the version now, why don't you need to amend\n> >> the same t5400-send-pack.sh tests as in my \"tests: mark & fix tests\n> >> broken under GIT_TEST_PROTOCOL_VERSION=1\"? There's one that spews out\n> >> \"version\" there under my GIT_TEST_PROTOCOL_VERSION=1.\n> >\n> > Sorry if I'm being dense, but can you point out more specifically what\n> > is wrong here? I don't see anything in t5400 that would be affected by\n> > this; the final test case (\"receive-pack de-dupes .have lines\") is the\n> > only one that does any tracing, and it strips out everything that's not\n> > a ref advertisement before checking the output. Sorry if I'm missing\n> > something obvious.\n> \n> I think I'm just misunderstanding this bit:\n> \n>     Additionally, remove special cases around advertising version=0.\n>     Previously we avoided adding version negotiation fields in server\n>     responses if it looked like the client wanted v0. However, including\n>     these fields does not change behavior, so it's better to have\n>     simpler code.\n\nAh yes, that is a bad description, which I will fix in V6. It should\nreplace \"server responses\" to instead be \"client requests\". Sorry for\nthe confusion, that was a really silly mistake for me to make.\n\nBasically, in the current master, clients will not add a version\nadvertisement to their request if their only supported version is 0.\nWith this patch, they will always include a version advertisement.\n\n> I meant that if you pick the series I have up to \"tests: add a special\n> setup that sets protocol.version\", which is at c6f33984fc in a WIP\n> branch in github.com/avar/git.git and run:\n> \n>     $ GIT_TEST_PROTOCOL_VERSION=1 ./t5400-send-pack.sh -v -i -x\n> \n> It'll fail with:\n> \n>     + test_cmp expect refs\n>     + diff -u expect refs\n>     --- expect      2018-12-14 22:26:52.485411992 +0000\n>     +++ refs        2018-12-14 22:26:52.713412148 +0000\n>     @@ -1,3 +1,4 @@\n>     +version 1\n>      5285e1710002a06a379056b0d21357a999f3c642 refs/heads/master\n>      5285e1710002a06a379056b0d21357a999f3c642 refs/remotes/origin/HEAD\n>      5285e1710002a06a379056b0d21357a999f3c642 refs/remotes/origin/master\n>     error: last command exited with $?=1\n>     not ok 16 - receive-pack de-dupes .have lines\n\nI believe the reason that my series doesn't fail on this test while\nyours does with GIT_TEST_PROTOCOL_VERSION=1 is because the \"fork\" repo\nin the test is a clone from the local filesystem. So in my case the\nversion negotiation differences are not visible in the trace output of\n\"git push\" (since the advert should be in an environment variable in\nthis case).\n\n> And I read your commit message to mean \"v0 clients also support v1\n> responses with the version in them, so let's just always send it\". But I\n> think this is my confusion (but I still don't know what it *really*\n> means).\n\nYeah again, a dumb mistake in the description on my part. We are only\nchanging whether the client sends a version advertisement. If everything\nelse is equal, there should be no behavior changes apart from the\nversion advertisement.\n\n> >> I was worried about this breaking GIT_SSH_COMMAND, but then I see due to\n> >> an interaction with picking \"what ssh implementation?\" we don't pass \"-G\n> >> -o SendEnv=GIT_PROTOCOL\" at all when I have a GIT_SSH_COMMAND, but *do*\n> >> pass it to my normal /usr/bin/ssh. Is this intended? Now if I have a\n> >> GIT_SSH_COMMAND that expects to wrap openssh I need to pass \"-c\n> >> ssh.variant=ssh\", because \"-c ssh.variant=auto\" will now omit these new\n> >> arguments.\n> >\n> > I am not an expert on this part of the code, but it seems to be\n> > intended. Later on in the function, there are BUG()s that state that\n> > VARIANT_AUTO should not be passed to the push_ssh_options() function.\n> > And this only changes the behavior when version=0. For protocol versions\n> > 1 & 2, VARIANT_AUTO never had SendEnv=GIT_PROTOCOL added to the command\n> > line. Again, sorry if I'm missing some implication here.\n> \n> I'm wondering if we need to worry about some new odd interactions\n> between client/servers when using GIT_SSH* wrappers, maybe not, but\n> e.g. as opposed to 0da0e49ba1 (\"ssh: 'auto' variant to select between\n> 'ssh' and 'simple'\", 2017-11-20) which noted a similar change had been\n> tested at Google internally (and I read it as Google using GIT_SSH* on\n> workstations) your commit doesn't make any mention of this case being\n> tested / considered.\n> \n> So do we need to worry about some new interaction here? Maybe not. Just\n> something for people more experienced with the ssh integration to chime\n> in on.\n\nAfter taking a look at 0da0e49ba1 (thanks for the find) and reading\nfill_ssh_args(), it seems to me that this is the intended behavior.\nssh.variant=auto means that we will run an initial \"$ssh_command -G\" to\nsee if the wrapper seems to handle OpenSSH arguments; if so we will\nswitch the variant to VARIANT_SSH and run again with the proper options\n(including SendEnv=GIT_PROTOCOL).\n"},{"id":"365709","messageId":"cover.1545342797.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1538516853.git.steadmon@google.com","subject":"[PATCH v6 0/1] Advertise multiple supported proto versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-12-20T21:58:07Z","receivedAt":"2018-12-20T21:58:13Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Add a registry of supported wire protocol versions that individual\ncommands can use to declare supported versions before contacting a\nserver. The client will then advertise all supported versions, while the\nserver will choose the first allowed version from the advertised\nlist.\n\nEvery command that acts as a client or server must now register its\nsupported protocol versions.\n\nChanges since V4: remove special cases around advertising version=0.\n\nChanges since V5: no code changes, but corrected and clarified commit\n  message regarding changes around advertising version=0.\n\nJosh Steadmon (1):\n  protocol: advertise multiple supported versions\n\n builtin/archive.c           |   3 +\n builtin/clone.c             |   4 ++\n builtin/fetch-pack.c        |   4 ++\n builtin/fetch.c             |   5 ++\n builtin/ls-remote.c         |   5 ++\n builtin/pull.c              |   5 ++\n builtin/push.c              |   4 ++\n builtin/receive-pack.c      |   3 +\n builtin/send-pack.c         |   3 +\n builtin/upload-archive.c    |   3 +\n builtin/upload-pack.c       |   4 ++\n connect.c                   |  52 +++++++--------\n protocol.c                  | 122 +++++++++++++++++++++++++++++++++---\n protocol.h                  |  23 ++++++-\n remote-curl.c               |  27 +++++---\n t/t5551-http-fetch-smart.sh |   1 +\n t/t5570-git-daemon.sh       |   2 +-\n t/t5601-clone.sh            |  38 +++++------\n t/t5700-protocol-v1.sh      |   8 +--\n t/t5702-protocol-v2.sh      |  16 +++--\n transport-helper.c          |   6 ++\n 21 files changed, 256 insertions(+), 82 deletions(-)\n\nRange-diff against v5:\n1:  60f6f2fbd8 ! 1:  10039ca163 protocol: advertise multiple supported versions\n    @@ -22,10 +22,12 @@\n         select the first allowed version from this advertisement.\n     \n         Additionally, remove special cases around advertising version=0.\n    -    Previously we avoided adding version negotiation fields in server\n    -    responses if it looked like the client wanted v0. However, including\n    -    these fields does not change behavior, so it's better to have simpler\n    -    code.\n    +    Previously we avoided adding version advertisements to the client's\n    +    initial connection request if the client wanted version=0. However,\n    +    including these advertisements does not change the version negotiation\n    +    behavior, so it's better to have simpler code. As a side effect, this\n    +    means that client operations over SSH will always include a\n    +    \"SendEnv=GIT_PROTOCOL\" option on the SSH command line.\n     \n         While we're at it, remove unnecessary externs from function declarations\n         in protocol.h.\n-- \n2.20.1.415.g653613c723-goog\n\n"},{"id":"365710","messageId":"10039ca163a9ca26595d3bbfcf70bc2ca02666f9.1545342797.git.steadmon@google.com","threadId":"49473","inReplyTo":"cover.1545342797.git.steadmon@google.com","subject":"[PATCH v6 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2018-12-20T21:58:08Z","receivedAt":"2018-12-20T21:58:16Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"Currently the client advertises that it supports the wire protocol\nversion set in the protocol.version config. However, not all services\nsupport the same set of protocol versions. For example, git-receive-pack\nsupports v1 and v0, but not v2. If a client connects to git-receive-pack\nand requests v2, it will instead be downgraded to v0. Other services,\nsuch as git-upload-archive, do not do any version negotiation checks.\n\nThis patch creates a protocol version registry. Individual client and\nserver programs register all the protocol versions they support prior to\ncommunicating with a remote instance. Versions should be listed in\npreference order; the version specified in protocol.version will\nautomatically be moved to the front of the registry.\n\nThe protocol version registry is passed to remote helpers via the\nGIT_PROTOCOL environment variable.\n\nClients now advertise the full list of registered versions. Servers\nselect the first allowed version from this advertisement.\n\nAdditionally, remove special cases around advertising version=0.\nPreviously we avoided adding version advertisements to the client's\ninitial connection request if the client wanted version=0. However,\nincluding these advertisements does not change the version negotiation\nbehavior, so it's better to have simpler code. As a side effect, this\nmeans that client operations over SSH will always include a\n\"SendEnv=GIT_PROTOCOL\" option on the SSH command line.\n\nWhile we're at it, remove unnecessary externs from function declarations\nin protocol.h.\n\nSigned-off-by: Josh Steadmon <steadmon@google.com>\n---\n builtin/archive.c           |   3 +\n builtin/clone.c             |   4 ++\n builtin/fetch-pack.c        |   4 ++\n builtin/fetch.c             |   5 ++\n builtin/ls-remote.c         |   5 ++\n builtin/pull.c              |   5 ++\n builtin/push.c              |   4 ++\n builtin/receive-pack.c      |   3 +\n builtin/send-pack.c         |   3 +\n builtin/upload-archive.c    |   3 +\n builtin/upload-pack.c       |   4 ++\n connect.c                   |  52 +++++++--------\n protocol.c                  | 122 +++++++++++++++++++++++++++++++++---\n protocol.h                  |  23 ++++++-\n remote-curl.c               |  27 +++++---\n t/t5551-http-fetch-smart.sh |   1 +\n t/t5570-git-daemon.sh       |   2 +-\n t/t5601-clone.sh            |  38 +++++------\n t/t5700-protocol-v1.sh      |   8 +--\n t/t5702-protocol-v2.sh      |  16 +++--\n transport-helper.c          |   6 ++\n 21 files changed, 256 insertions(+), 82 deletions(-)\n\ndiff --git a/builtin/archive.c b/builtin/archive.c\nindex e74f675390..8adb9f381b 100644\n--- a/builtin/archive.c\n+++ b/builtin/archive.c\n@@ -8,6 +8,7 @@\n #include \"transport.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n \n static void create_output_file(const char *output_file)\n@@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, local_opts, NULL,\n \t\t\t     PARSE_OPT_KEEP_ALL);\n \ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fd2c3ef090..1651a950b6 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -900,6 +900,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tstruct refspec rs = REFSPEC_INIT_FETCH;\n \tstruct argv_array ref_prefixes = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tfetch_if_missing = 0;\n \n \tpacket_trace_identity(\"clone\");\ndiff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c\nindex 1a1bc63566..cba935e4d3 100644\n--- a/builtin/fetch-pack.c\n+++ b/builtin/fetch-pack.c\n@@ -57,6 +57,10 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)\n \n \tfetch_if_missing = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch-pack\");\n \n \tmemset(&args, 0, sizeof(args));\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 61bec5d213..2a20cf8bfd 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -21,6 +21,7 @@\n #include \"argv-array.h\"\n #include \"utf8.h\"\n #include \"packfile.h\"\n+#include \"protocol.h\"\n #include \"list-objects-filter-options.h\"\n \n static const char * const builtin_fetch_usage[] = {\n@@ -1476,6 +1477,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)\n \tint prune_tags_ok = 1;\n \tstruct argv_array argv_gc_auto = ARGV_ARRAY_INIT;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"fetch\");\n \n \tfetch_if_missing = 0;\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 1a25df7ee1..ea685e8bb9 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -1,5 +1,6 @@\n #include \"builtin.h\"\n #include \"cache.h\"\n+#include \"protocol.h\"\n #include \"transport.h\"\n #include \"ref-filter.h\"\n #include \"remote.h\"\n@@ -80,6 +81,10 @@ int cmd_ls_remote(int argc, const char **argv, const char *prefix)\n \n \tmemset(&ref_array, 0, sizeof(ref_array));\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, ls_remote_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tdest = argv[0];\ndiff --git a/builtin/pull.c b/builtin/pull.c\nindex 681c127a07..cb64146834 100644\n--- a/builtin/pull.c\n+++ b/builtin/pull.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"builtin.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"exec-cmd.h\"\n #include \"run-command.h\"\n #include \"sha1-array.h\"\n@@ -849,6 +850,10 @@ int cmd_pull(int argc, const char **argv, const char *prefix)\n \tstruct object_id rebase_fork_point;\n \tint autostash;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tif (!getenv(\"GIT_REFLOG_ACTION\"))\n \t\tset_reflog_message(argc, argv);\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex d09a42062c..10d8abe829 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -10,6 +10,7 @@\n #include \"remote.h\"\n #include \"transport.h\"\n #include \"parse-options.h\"\n+#include \"protocol.h\"\n #include \"submodule.h\"\n #include \"submodule-config.h\"\n #include \"send-pack.h\"\n@@ -587,6 +588,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tpacket_trace_identity(\"push\");\n \tgit_config(git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex c17ce94e12..030cb7b7ec 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1929,6 +1929,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)\n \n \tpacket_trace_identity(\"receive-pack\");\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0);\n \n \tif (argc > 1)\ndiff --git a/builtin/send-pack.c b/builtin/send-pack.c\nindex 8e3c7490f7..f48bd1306b 100644\n--- a/builtin/send-pack.c\n+++ b/builtin/send-pack.c\n@@ -184,6 +184,9 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)\n \t\tOPT_END()\n \t};\n \n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \tgit_config(send_pack_config, NULL);\n \targc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);\n \tif (argc > 0) {\ndiff --git a/builtin/upload-archive.c b/builtin/upload-archive.c\nindex 25d9116356..791cbe80a6 100644\n--- a/builtin/upload-archive.c\n+++ b/builtin/upload-archive.c\n@@ -5,6 +5,7 @@\n #include \"builtin.h\"\n #include \"archive.h\"\n #include \"pkt-line.h\"\n+#include \"protocol.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n #include \"argv-array.h\"\n@@ -80,6 +81,8 @@ int cmd_upload_archive(int argc, const char **argv, const char *prefix)\n \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n \t\tusage(upload_archive_usage);\n \n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \t/*\n \t * Set up sideband subprocess.\n \t *\ndiff --git a/builtin/upload-pack.c b/builtin/upload-pack.c\nindex 42dc4da5a1..293dd45b9e 100644\n--- a/builtin/upload-pack.c\n+++ b/builtin/upload-pack.c\n@@ -33,6 +33,10 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)\n \tpacket_trace_identity(\"upload-pack\");\n \tread_replace_refs = 0;\n \n+\tregister_allowed_protocol_version(protocol_v2);\n+\tregister_allowed_protocol_version(protocol_v1);\n+\tregister_allowed_protocol_version(protocol_v0);\n+\n \targc = parse_options(argc, argv, NULL, options, upload_pack_usage, 0);\n \n \tif (argc != 1)\ndiff --git a/connect.c b/connect.c\nindex 94547e5056..57266b6cec 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n  */\n static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t\t\t\t     const char *path, const char *prog,\n-\t\t\t\t\t     enum protocol_version version,\n+\t\t\t\t\t     const struct strbuf *version_advert,\n \t\t\t\t\t     int flags)\n {\n \tstruct child_process *conn;\n@@ -1084,12 +1084,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n \t\t    prog, path, 0,\n \t\t    target_host, 0);\n \n-\t/* If using a new version put that stuff here after a second null byte */\n-\tif (version > 0) {\n-\t\tstrbuf_addch(&request, '\\0');\n-\t\tstrbuf_addf(&request, \"version=%d%c\",\n-\t\t\t    version, '\\0');\n-\t}\n+\t/* Add version fields after a second null byte */\n+\tstrbuf_addch(&request, '\\0');\n+\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n \n \tpacket_write(fd[1], request.buf, request.len);\n \n@@ -1104,14 +1101,13 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n  */\n static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \t\t\t     enum ssh_variant variant, const char *port,\n-\t\t\t     enum protocol_version version, int flags)\n+\t\t\t     const struct strbuf *version_advert, int flags)\n {\n-\tif (variant == VARIANT_SSH &&\n-\t    version > 0) {\n+\tif (variant == VARIANT_SSH) {\n \t\targv_array_push(args, \"-o\");\n \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n-\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t version);\n+\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t version_advert->buf);\n \t}\n \n \tif (flags & CONNECT_IPV4) {\n@@ -1164,7 +1160,7 @@ static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n \n /* Prepare a child_process for use by Git's SSH-tunneled transport. */\n static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n-\t\t\t  const char *port, enum protocol_version version,\n+\t\t\t  const char *port, const struct strbuf *version_advert,\n \t\t\t  int flags)\n {\n \tconst char *ssh;\n@@ -1198,15 +1194,16 @@ static void fill_ssh_args(struct child_process *conn, const char *ssh_host,\n \n \t\targv_array_push(&detect.args, ssh);\n \t\targv_array_push(&detect.args, \"-G\");\n-\t\tpush_ssh_options(&detect.args, &detect.env_array,\n-\t\t\t\t VARIANT_SSH, port, version, flags);\n+\t\tpush_ssh_options(&detect.args, &detect.env_array, VARIANT_SSH,\n+\t\t\t\t port, version_advert, flags);\n \t\targv_array_push(&detect.args, ssh_host);\n \n \t\tvariant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;\n \t}\n \n \targv_array_push(&conn->args, ssh);\n-\tpush_ssh_options(&conn->args, &conn->env_array, variant, port, version, flags);\n+\tpush_ssh_options(&conn->args, &conn->env_array, variant, port,\n+\t\t\t version_advert, flags);\n \targv_array_push(&conn->args, ssh_host);\n }\n \n@@ -1226,16 +1223,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tenum protocol protocol;\n-\tenum protocol_version version = get_protocol_version_config();\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Without this we cannot rely on waitpid() to tell\n \t * what happened to our children.\n@@ -1250,7 +1241,8 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n \t} else if (protocol == PROTO_GIT) {\n-\t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n+\t\tconn = git_connect_git(fd, hostandport, path, prog,\n+\t\t\t\t       &version_advert, flags);\n \t} else {\n \t\tstruct strbuf cmd = STRBUF_INIT;\n \t\tconst char *const *var;\n@@ -1293,13 +1285,13 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t\t\t\tstrbuf_release(&cmd);\n \t\t\t\treturn NULL;\n \t\t\t}\n-\t\t\tfill_ssh_args(conn, ssh_host, port, version, flags);\n+\t\t\tfill_ssh_args(conn, ssh_host, port, &version_advert,\n+\t\t\t\t      flags);\n \t\t} else {\n \t\t\ttransport_check_allowed(\"file\");\n-\t\t\tif (version > 0) {\n-\t\t\t\targv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n-\t\t\t\t\t\t version);\n-\t\t\t}\n+\t\t\targv_array_pushf(&conn->env_array,\n+\t\t\t\t\t GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n+\t\t\t\t\t version_advert.buf);\n \t\t}\n \t\targv_array_push(&conn->args, cmd.buf);\n \ndiff --git a/protocol.c b/protocol.c\nindex 5e636785d1..5664bd7a05 100644\n--- a/protocol.c\n+++ b/protocol.c\n@@ -2,18 +2,43 @@\n #include \"config.h\"\n #include \"protocol.h\"\n \n+static enum protocol_version *allowed_versions;\n+static int nr_allowed_versions;\n+static int alloc_allowed_versions;\n+static int version_registration_locked = 0;\n+\n+static const char protocol_v0_string[] = \"0\";\n+static const char protocol_v1_string[] = \"1\";\n+static const char protocol_v2_string[] = \"2\";\n+\n static enum protocol_version parse_protocol_version(const char *value)\n {\n-\tif (!strcmp(value, \"0\"))\n+\tif (!strcmp(value, protocol_v0_string))\n \t\treturn protocol_v0;\n-\telse if (!strcmp(value, \"1\"))\n+\telse if (!strcmp(value, protocol_v1_string))\n \t\treturn protocol_v1;\n-\telse if (!strcmp(value, \"2\"))\n+\telse if (!strcmp(value, protocol_v2_string))\n \t\treturn protocol_v2;\n \telse\n \t\treturn protocol_unknown_version;\n }\n \n+/* Return the text representation of a wire protocol version. */\n+static const char *format_protocol_version(enum protocol_version version)\n+{\n+\tswitch (version) {\n+\tcase protocol_v0:\n+\t\treturn protocol_v0_string;\n+\tcase protocol_v1:\n+\t\treturn protocol_v1_string;\n+\tcase protocol_v2:\n+\t\treturn protocol_v2_string;\n+\tcase protocol_unknown_version:\n+\t\tdie(_(\"Unrecognized protocol version\"));\n+\t}\n+\tdie(_(\"Unrecognized protocol_version\"));\n+}\n+\n enum protocol_version get_protocol_version_config(void)\n {\n \tconst char *value;\n@@ -30,6 +55,85 @@ enum protocol_version get_protocol_version_config(void)\n \treturn protocol_v0;\n }\n \n+void register_allowed_protocol_version(enum protocol_version version)\n+{\n+\tif (version_registration_locked)\n+\t\tBUG(\"late attempt to register an allowed protocol version\");\n+\n+\tALLOC_GROW(allowed_versions, nr_allowed_versions + 1,\n+\t\t   alloc_allowed_versions);\n+\tallowed_versions[nr_allowed_versions++] = version;\n+}\n+\n+void register_allowed_protocol_versions_from_env(void)\n+{\n+\tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n+\tconst char *version_str;\n+\tstruct string_list version_list = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *version;\n+\n+\tif (!git_protocol)\n+\t\treturn;\n+\n+\tstring_list_split(&version_list, git_protocol, ':', -1);\n+\tfor_each_string_list_item(version, &version_list) {\n+\t\tif (skip_prefix(version->string, \"version=\", &version_str))\n+\t\t\tregister_allowed_protocol_version(\n+\t\t\t\tparse_protocol_version(version_str));\n+\t}\n+\tstring_list_clear(&version_list, 0);\n+}\n+\n+static int is_allowed_protocol_version(enum protocol_version version)\n+{\n+\tint i;\n+\tversion_registration_locked = 1;\n+\tfor (i = 0; i < nr_allowed_versions; i++)\n+\t\tif (version == allowed_versions[i])\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+void get_client_protocol_version_advertisement(struct strbuf *advert)\n+{\n+\tint i, tmp_nr = nr_allowed_versions;\n+\tenum protocol_version *tmp_allowed_versions, config_version;\n+\tstrbuf_reset(advert);\n+\n+\tversion_registration_locked = 1;\n+\n+\tconfig_version = get_protocol_version_config();\n+\tif (config_version == protocol_v0) {\n+\t\tstrbuf_addstr(advert, \"version=0\");\n+\t\treturn;\n+\t}\n+\n+\tif (tmp_nr > 0) {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n+\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n+\t\t\t   sizeof(enum protocol_version));\n+\t} else {\n+\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n+\t\ttmp_nr = 1;\n+\t\ttmp_allowed_versions[0] = config_version;\n+\t}\n+\n+\tif (tmp_allowed_versions[0] != config_version)\n+\t\tfor (i = 1; i < nr_allowed_versions; i++)\n+\t\t\tif (tmp_allowed_versions[i] == config_version) {\n+\t\t\t\tSWAP(tmp_allowed_versions[0],\n+\t\t\t\t     tmp_allowed_versions[i]);\n+\t\t\t}\n+\n+\tstrbuf_addf(advert, \"version=%s\",\n+\t\t    format_protocol_version(tmp_allowed_versions[0]));\n+\tfor (i = 1; i < tmp_nr; i++)\n+\t\tstrbuf_addf(advert, \":version=%s\",\n+\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n+\n+\tfree(tmp_allowed_versions);\n+}\n+\n enum protocol_version determine_protocol_version_server(void)\n {\n \tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n@@ -38,9 +142,10 @@ enum protocol_version determine_protocol_version_server(void)\n \t/*\n \t * Determine which protocol version the client has requested.  Since\n \t * multiple 'version' keys can be sent by the client, indicating that\n-\t * the client is okay to speak any of them, select the greatest version\n-\t * that the client has requested.  This is due to the assumption that\n-\t * the most recent protocol version will be the most state-of-the-art.\n+\t * the client is okay to speak any of them, select the first\n+\t * recognizable version that the client has requested.  This is due to\n+\t * the assumption that the protocol versions will be listed in\n+\t * preference order.\n \t */\n \tif (git_protocol) {\n \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -53,8 +158,11 @@ enum protocol_version determine_protocol_version_server(void)\n \n \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n \t\t\t\tv = parse_protocol_version(value);\n-\t\t\t\tif (v > version)\n+\t\t\t\tif (v != protocol_unknown_version &&\n+\t\t\t\t    is_allowed_protocol_version(v)) {\n \t\t\t\t\tversion = v;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \ndiff --git a/protocol.h b/protocol.h\nindex 2ad35e433c..88330fd0ee 100644\n--- a/protocol.h\n+++ b/protocol.h\n@@ -14,7 +14,24 @@ enum protocol_version {\n  * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n  * returned.\n  */\n-extern enum protocol_version get_protocol_version_config(void);\n+enum protocol_version get_protocol_version_config(void);\n+\n+/*\n+ * Register an allowable protocol version for a given operation. Registration\n+ * must occur before attempting to advertise a version to a server process.\n+ */\n+void register_allowed_protocol_version(enum protocol_version version);\n+\n+/*\n+ * Register allowable protocol versions from the GIT_PROTOCOL environment var.\n+ */\n+void register_allowed_protocol_versions_from_env(void);\n+\n+/*\n+ * Fill a strbuf with a version advertisement string suitable for use in the\n+ * GIT_PROTOCOL environment variable or similar version negotiation field.\n+ */\n+void get_client_protocol_version_advertisement(struct strbuf *advert);\n \n /*\n  * Used by a server to determine which protocol version should be used based on\n@@ -23,12 +40,12 @@ extern enum protocol_version get_protocol_version_config(void);\n  * request a particular protocol version, a default of 'protocol_v0' will be\n  * used.\n  */\n-extern enum protocol_version determine_protocol_version_server(void);\n+enum protocol_version determine_protocol_version_server(void);\n \n /*\n  * Used by a client to determine which protocol version the server is speaking\n  * based on the server's initial response.\n  */\n-extern enum protocol_version determine_protocol_version_client(const char *server_response);\n+enum protocol_version determine_protocol_version_client(const char *server_response);\n \n #endif /* PROTOCOL_H */\ndiff --git a/remote-curl.c b/remote-curl.c\nindex fb28309e85..6ffefe5169 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -330,6 +330,19 @@ static int get_protocol_http_header(enum protocol_version version,\n \treturn 0;\n }\n \n+static int get_client_protocol_http_header(const struct strbuf *version_advert,\n+\t\t\t\t\t   struct strbuf *header)\n+{\n+\tif (version_advert->len > 0) {\n+\t\tstrbuf_addf(header, GIT_PROTOCOL_HEADER \": %s\",\n+\t\t\t    version_advert->buf);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static struct discovery *discover_refs(const char *service, int for_push)\n {\n \tstruct strbuf exp = STRBUF_INIT;\n@@ -339,11 +352,11 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \tstruct strbuf refs_url = STRBUF_INIT;\n \tstruct strbuf effective_url = STRBUF_INIT;\n \tstruct strbuf protocol_header = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct string_list extra_headers = STRING_LIST_INIT_DUP;\n \tstruct discovery *last = last_discovery;\n \tint http_ret, maybe_smart = 0;\n \tstruct http_get_options http_options;\n-\tenum protocol_version version = get_protocol_version_config();\n \n \tif (last && !strcmp(service, last->service))\n \t\treturn last;\n@@ -360,16 +373,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n \t}\n \n-\t/*\n-\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n-\t * to perform a push, then fallback to v0 since the client doesn't know\n-\t * how to push yet using v2.\n-\t */\n-\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n-\t\tversion = protocol_v0;\n+\tget_client_protocol_version_advertisement(&version_advert);\n \n \t/* Add the extra Git-Protocol header */\n-\tif (get_protocol_http_header(version, &protocol_header))\n+\tif (get_client_protocol_http_header(&version_advert, &protocol_header))\n \t\tstring_list_append(&extra_headers, protocol_header.buf);\n \n \tmemset(&http_options, 0, sizeof(http_options));\n@@ -1327,6 +1334,8 @@ int cmd_main(int argc, const char **argv)\n \tstruct strbuf buf = STRBUF_INIT;\n \tint nongit;\n \n+\tregister_allowed_protocol_versions_from_env();\n+\n \tsetup_git_directory_gently(&nongit);\n \tif (argc < 2) {\n \t\terror(\"remote-curl: usage: git remote-curl <remote> [<url>]\");\ndiff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh\nindex 771f36f9ff..343bf3aafa 100755\n--- a/t/t5551-http-fetch-smart.sh\n+++ b/t/t5551-http-fetch-smart.sh\n@@ -28,6 +28,7 @@ cat >exp <<EOF\n > Accept: */*\n > Accept-Encoding: ENCODINGS\n > Pragma: no-cache\n+> Git-Protocol: version=0\n < HTTP/1.1 200 OK\n < Pragma: no-cache\n < Cache-Control: no-cache, max-age=0, must-revalidate\ndiff --git a/t/t5570-git-daemon.sh b/t/t5570-git-daemon.sh\nindex 7466aad111..d528e91630 100755\n--- a/t/t5570-git-daemon.sh\n+++ b/t/t5570-git-daemon.sh\n@@ -186,7 +186,7 @@ test_expect_success 'hostname cannot break out of directory' '\n test_expect_success 'daemon log records all attributes' '\n \tcat >expect <<-\\EOF &&\n \tExtended attribute \"host\": localhost\n-\tExtended attribute \"protocol\": version=1\n+\tExtended attribute \"protocol\": version=1:version=2:version=0\n \tEOF\n \t>daemon.log &&\n \tGIT_OVERRIDE_VIRTUAL_HOST=localhost \\\ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex ddaa96ac4f..c4dbf1f779 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -346,7 +346,7 @@ expect_ssh () {\n \n test_expect_success 'clone myhost:src uses ssh' '\n \tgit clone myhost:src ssh-clone &&\n-\texpect_ssh myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL\" myhost src\n '\n \n test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n@@ -357,12 +357,12 @@ test_expect_success !MINGW,!CYGWIN 'clone local path foo:bar' '\n \n test_expect_success 'bracketed hostnames are still ssh' '\n \tgit clone \"[myhost:123]:src\" ssh-bracket-clone &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'OpenSSH variant passes -4' '\n \tgit clone -4 \"[myhost:123]:src\" ssh-ipv4-clone &&\n-\texpect_ssh \"-4 -p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -4 -p 123\" myhost src\n '\n \n test_expect_success 'variant can be overridden' '\n@@ -406,7 +406,7 @@ test_expect_success 'OpenSSH-like uplink is treated as ssh' '\n \tGIT_SSH=\"$TRASH_DIRECTORY/uplink\" &&\n \ttest_when_finished \"GIT_SSH=\\\"\\$TRASH_DIRECTORY/ssh\\$X\\\"\" &&\n \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-sshlike-uplink &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'plink is treated specially (as putty)' '\n@@ -446,14 +446,14 @@ test_expect_success 'GIT_SSH_VARIANT overrides plink detection' '\n \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n \tGIT_SSH_VARIANT=ssh \\\n \tgit clone \"[myhost:123]:src\" ssh-bracket-clone-variant-1 &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'ssh.variant overrides plink detection' '\n \tcopy_ssh_wrapper_as \"$TRASH_DIRECTORY/plink\" &&\n \tgit -c ssh.variant=ssh \\\n \t\tclone \"[myhost:123]:src\" ssh-bracket-clone-variant-2 &&\n-\texpect_ssh \"-p 123\" myhost src\n+\texpect_ssh \"-o SendEnv=GIT_PROTOCOL -p 123\" myhost src\n '\n \n test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '\n@@ -488,7 +488,7 @@ test_clone_url () {\n }\n \n test_expect_success !MINGW 'clone c:temp is ssl' '\n-\ttest_clone_url c:temp c temp\n+\ttest_clone_url c:temp \"-o SendEnv=GIT_PROTOCOL\" c temp\n '\n \n test_expect_success MINGW 'clone c:temp is dos drive' '\n@@ -499,7 +499,7 @@ test_expect_success MINGW 'clone c:temp is dos drive' '\n for repo in rep rep/home/project 123\n do\n \ttest_expect_success \"clone host:$repo\" '\n-\t\ttest_clone_url host:$repo host $repo\n+\t\ttest_clone_url host:$repo \"-o SendEnv=GIT_PROTOCOL\" host $repo\n \t'\n done\n \n@@ -507,16 +507,16 @@ done\n for repo in rep rep/home/project 123\n do\n \ttest_expect_success \"clone [::1]:$repo\" '\n-\t\ttest_clone_url [::1]:$repo ::1 \"$repo\"\n+\t\ttest_clone_url [::1]:$repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"$repo\"\n \t'\n done\n #home directory\n test_expect_success \"clone host:/~repo\" '\n-\ttest_clone_url host:/~repo host \"~repo\"\n+\ttest_clone_url host:/~repo \"-o SendEnv=GIT_PROTOCOL\" host \"~repo\"\n '\n \n test_expect_success \"clone [::1]:/~repo\" '\n-\ttest_clone_url [::1]:/~repo ::1 \"~repo\"\n+\ttest_clone_url [::1]:/~repo \"-o SendEnv=GIT_PROTOCOL\" ::1 \"~repo\"\n '\n \n # Corner cases\n@@ -532,22 +532,22 @@ done\n for tcol in \"\" :\n do\n \ttest_expect_success \"clone ssh://host.xz$tcol/home/user/repo\" '\n-\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" host.xz /home/user/repo\n+\t\ttest_clone_url \"ssh://host.xz$tcol/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz /home/user/repo\n \t'\n \t# from home directory\n \ttest_expect_success \"clone ssh://host.xz$tcol/~repo\" '\n-\ttest_clone_url \"ssh://host.xz$tcol/~repo\" host.xz \"~repo\"\n+\ttest_clone_url \"ssh://host.xz$tcol/~repo\" \"-o SendEnv=GIT_PROTOCOL\" host.xz \"~repo\"\n '\n done\n \n # with port number\n test_expect_success 'clone ssh://host.xz:22/home/user/repo' '\n-\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-p 22 host.xz\" \"/home/user/repo\"\n+\ttest_clone_url \"ssh://host.xz:22/home/user/repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"/home/user/repo\"\n '\n \n # from home directory with port number\n test_expect_success 'clone ssh://host.xz:22/~repo' '\n-\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-p 22 host.xz\" \"~repo\"\n+\ttest_clone_url \"ssh://host.xz:22/~repo\" \"-o SendEnv=GIT_PROTOCOL -p 22 host.xz\" \"~repo\"\n '\n \n #IPv6\n@@ -555,7 +555,7 @@ for tuah in ::1 [::1] [::1]: user@::1 user@[::1] user@[::1]: [user@::1] [user@::\n do\n \tehost=$(echo $tuah | sed -e \"s/1]:/1]/\" | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah/home/user/repo\" \"\n-\t  test_clone_url ssh://$tuah/home/user/repo $ehost /home/user/repo\n+\t  test_clone_url ssh://$tuah/home/user/repo '-o SendEnv=GIT_PROTOCOL' $ehost /home/user/repo\n \t\"\n done\n \n@@ -564,7 +564,7 @@ for tuah in ::1 [::1] user@::1 user@[::1] [user@::1]\n do\n \teuah=$(echo $tuah | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah/~repo\" \"\n-\t  test_clone_url ssh://$tuah/~repo $euah '~repo'\n+\t  test_clone_url ssh://$tuah/~repo '-o SendEnv=GIT_PROTOCOL' $euah '~repo'\n \t\"\n done\n \n@@ -573,7 +573,7 @@ for tuah in [::1] user@[::1] [user@::1]\n do\n \teuah=$(echo $tuah | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah:22/home/user/repo\" \"\n-\t  test_clone_url ssh://$tuah:22/home/user/repo '-p 22' $euah /home/user/repo\n+\t  test_clone_url ssh://$tuah:22/home/user/repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah /home/user/repo\n \t\"\n done\n \n@@ -582,7 +582,7 @@ for tuah in [::1] user@[::1] [user@::1]\n do\n \teuah=$(echo $tuah | tr -d \"[]\")\n \ttest_expect_success \"clone ssh://$tuah:22/~repo\" \"\n-\t  test_clone_url ssh://$tuah:22/~repo '-p 22' $euah '~repo'\n+\t  test_clone_url ssh://$tuah:22/~repo '-o SendEnv=GIT_PROTOCOL -p 22' $euah '~repo'\n \t\"\n done\n \ndiff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh\nindex ba86a44eb1..2e56c79233 100755\n--- a/t/t5700-protocol-v1.sh\n+++ b/t/t5700-protocol-v1.sh\n@@ -26,7 +26,7 @@ test_expect_success 'clone with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"clone< version 1\" log\n '\n@@ -42,7 +42,7 @@ test_expect_success 'fetch with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -56,7 +56,7 @@ test_expect_success 'pull with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"fetch< version 1\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'push with git:// using protocol v1' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v1\n-\tgrep \"push> .*\\\\\\0\\\\\\0version=1\\\\\\0$\" log &&\n+\tgrep \"push> .*\\\\\\0\\\\\\0version=1.*\\\\\\0$\" log &&\n \t# Server responded using protocol v1\n \tgrep \"push< version 1\" log\n '\ndiff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh\nindex 3beeed4546..78c17c25e4 100755\n--- a/t/t5702-protocol-v2.sh\n+++ b/t/t5702-protocol-v2.sh\n@@ -24,7 +24,7 @@ test_expect_success 'list refs with git:// using protocol v2' '\n \t\tls-remote --symref \"$GIT_DAEMON_URL/parent\" >actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"git> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"git> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"git< version 2\" log &&\n \n@@ -56,7 +56,7 @@ test_expect_success 'clone with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"clone> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"clone> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"clone< version 2\" log\n '\n@@ -74,7 +74,7 @@ test_expect_success 'fetch with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -90,7 +90,7 @@ test_expect_success 'pull with git:// using protocol v2' '\n \ttest_cmp expect actual &&\n \n \t# Client requested to use protocol v2\n-\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2\\\\\\0$\" log &&\n+\tgrep \"fetch> .*\\\\\\0\\\\\\0version=2.*\\\\\\0$\" log &&\n \t# Server responded using protocol v2\n \tgrep \"fetch< version 2\" log\n '\n@@ -476,7 +476,7 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \ttest_when_finished \"rm -f log\" &&\n \t# Till v2 for push is designed, make sure that if a client has\n \t# protocol.version configured to use v2, that the client instead falls\n-\t# back and uses v0.\n+\t# back to previous versions.\n \n \ttest_commit -C http_child three &&\n \n@@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n \tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n \ttest_cmp expect actual &&\n \n-\t# Client didnt request to use protocol v2\n-\t! grep \"Git-Protocol: version=2\" log &&\n-\t# Server didnt respond using protocol v2\n-\t! grep \"git< version 2\" log\n+\t# Server responded with version 1\n+\tgrep \"git< version 1\" log\n '\n \n \ndiff --git a/transport-helper.c b/transport-helper.c\nindex 143ca008c8..ac1937f1e1 100644\n--- a/transport-helper.c\n+++ b/transport-helper.c\n@@ -105,6 +105,7 @@ static struct child_process *get_helper(struct transport *transport)\n {\n \tstruct helper_data *data = transport->data;\n \tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf version_advert = STRBUF_INIT;\n \tstruct child_process *helper;\n \tint duped;\n \tint code;\n@@ -127,6 +128,11 @@ static struct child_process *get_helper(struct transport *transport)\n \t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n \t\t\t\t GIT_DIR_ENVIRONMENT, get_git_dir());\n \n+\tget_client_protocol_version_advertisement(&version_advert);\n+\tif (version_advert.len > 0)\n+\t\targv_array_pushf(&helper->env_array, \"%s=%s\",\n+\t\t\t\t GIT_PROTOCOL_ENVIRONMENT, version_advert.buf);\n+\n \tcode = start_command(helper);\n \tif (code < 0 && errno == ENOENT)\n \t\tdie(_(\"unable to find remote helper for '%s'\"), data->name);\n-- \n2.20.1.415.g653613c723-goog\n\n"},{"id":"368537","messageId":"20190205194226.172769-1-jonathantanmy@google.com","threadId":"49473","inReplyTo":"10039ca163a9ca26595d3bbfcf70bc2ca02666f9.1545342797.git.steadmon@google.com","subject":"Re: [PATCH v6 1/1] protocol: advertise multiple supported versions","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2019-02-05T19:42:26Z","receivedAt":"2019-02-05T19:42:32Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"Thanks. Overall, one major point: I think that the declaration of\nsupported versions need to be transport-scoped (search this email for\n\"transport-scoped\" for context). And one medium point: it might be\nbetter for protocol.version to be the preferred and maximum version, not\nonly the preferred version. I have other minor points, which you can\nread below.\n\n> Currently the client advertises that it supports the wire protocol\n> version set in the protocol.version config. However, not all services\n> support the same set of protocol versions. For example, git-receive-pack\n> supports v1 and v0, but not v2. \n\nThat's true, except <see next paragraph>.\n\n> If a client connects to git-receive-pack\n> and requests v2, it will instead be downgraded to v0.\n\nThis is a bit misleading. The client never requests v2 - connect.c has\nan override that changes it to v0 (as can be seen from the diff of this\npatch) before the request takes place. If we were to create a custom\nclient that requests v2, the server would die with \"support for protocol\nv2 not implemented yet\". (But maybe this is a bug - the server should\ndowngrade instead.)\n\n> Other services,\n> such as git-upload-archive, do not do any version negotiation checks.\n> \n> This patch creates a protocol version registry. Individual client and\n> server programs register all the protocol versions they support prior to\n> communicating with a remote instance. Versions should be listed in\n> preference order; the version specified in protocol.version will\n> automatically be moved to the front of the registry.\n\nIt took me a while to understand what's going on, so let me try to\nsummarize:\n\n - The main issue is that send-pack doesn't support v2, so it would be\n   incorrect for the client to advertise v2; this is currently fine\n   because of the override in connect.c. A side issue is that the client\n   advertises v2 for other things (e.g. archive), which currently works\n   only because the server ignores them.\n\n - To solve both these issues, each command declares which protocols it\n   supports.\n\n - Because we now have such a list, it is not too difficult to pass the\n   entire list to the server, so we should do so. (Servers already\n   support multiple versions.)\n\n> The protocol version registry is passed to remote helpers via the\n> GIT_PROTOCOL environment variable.\n\nThis is unfortunate to me, but I can't think of a better way to do it -\nwe do need to communicate the allowed version list somehow. A transport\noption cannot be ignored by an old remote helper, but an envvar can.\n\nBackwards/forwards compatibility notes:\n\n - If an old Git uses a new remote helper, no GIT_PROTOCOL is passed so\n   the remote helper uses v0 regardless of what's configured in the\n   repo. This should be fine.\n - If a new Git uses an old remote helper, what's configured gets used\n   (unless it's for send-pack, in which case a version override occurs),\n   regardless of which versions Git declares that it supports. I can\n   envision some situations in which this would trip us up, but we are\n   already in this exact situation anyway.\n\n> Additionally, remove special cases around advertising version=0.\n> Previously we avoided adding version advertisements to the client's\n> initial connection request if the client wanted version=0. However,\n> including these advertisements does not change the version negotiation\n> behavior, so it's better to have simpler code. As a side effect, this\n> means that client operations over SSH will always include a\n> \"SendEnv=GIT_PROTOCOL\" option on the SSH command line.\n\nHTTP too, I think?\n\nAlso, this means that the client will send different information to the\nserver now, but it seems fine (as far as I know, clients already send\nGit version information anyway).\n\nOverall, I would write the commit message this way:\n\n    A Git client can communicate with a Git server through various\n    commands, and for each command, in various protocol versions. Each\n    repo can define protocol.version as 0, 1, or 2 for the protocol\n    version that it prefers. But this integer is handled differently by\n    each command: if communicating with \"upload-pack\", the integer is\n    passed verbatim to be interpreted by the server; if communicating\n    with \"receive-pack\", the integer is hardcoded to be overridden to 0\n    or 1; and if communicating with anything else (e.g. \"archive\"), the\n    integer is passed verbatim but ignored by the server.\n\n    Handle each command the same way by having each client command\n    declare which versions it supports, and whenever a command is\n    invoked, send its list of versions to the server (instead of one\n    single version, as is done currently). The protocol.version is now\n    used as a hint to which version the client prefers; if it is present\n    in the list, it is moved to the front, otherwise the list is sent\n    unchanged.\n\n    The server already supports multiple versions, but currently chooses\n    the greatest. Make the server choose the first supported version\n    instead.\n\n    Because remote helpers now need this list of versions, pass the list\n    of versions as the GIT_PROTOCOL environment variable. (New remote\n    helpers will make use of this, and old remote helpers will use the\n    repo configuration.)\n\n    <special case about advertising version=0>\n\n    <unnecessary externs>\n\n> diff --git a/builtin/archive.c b/builtin/archive.c\n> index e74f675390..8adb9f381b 100644\n> --- a/builtin/archive.c\n> +++ b/builtin/archive.c\n> @@ -8,6 +8,7 @@\n>  #include \"transport.h\"\n>  #include \"parse-options.h\"\n>  #include \"pkt-line.h\"\n> +#include \"protocol.h\"\n>  #include \"sideband.h\"\n>  \n>  static void create_output_file(const char *output_file)\n> @@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n>  \t\tOPT_END()\n>  \t};\n>  \n> +\tregister_allowed_protocol_version(protocol_v0);\n> +\n>  \targc = parse_options(argc, argv, prefix, local_opts, NULL,\n>  \t\t\t     PARSE_OPT_KEEP_ALL);\n>  \n\nWhat happens if a command never declares any version? It might be best\nto explicitly allow such a case, and treat it as a single list of\nprotocol_v0.\n\n> diff --git a/connect.c b/connect.c\n> index 94547e5056..57266b6cec 100644\n> --- a/connect.c\n> +++ b/connect.c\n> @@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n>   */\n>  static struct child_process *git_connect_git(int fd[2], char *hostandport,\n>  \t\t\t\t\t     const char *path, const char *prog,\n> -\t\t\t\t\t     enum protocol_version version,\n> +\t\t\t\t\t     const struct strbuf *version_advert,\n>  \t\t\t\t\t     int flags)\n>  {\n>  \tstruct child_process *conn;\n> @@ -1084,12 +1084,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n>  \t\t    prog, path, 0,\n>  \t\t    target_host, 0);\n>  \n> -\t/* If using a new version put that stuff here after a second null byte */\n> -\tif (version > 0) {\n> -\t\tstrbuf_addch(&request, '\\0');\n> -\t\tstrbuf_addf(&request, \"version=%d%c\",\n> -\t\t\t    version, '\\0');\n> -\t}\n> +\t/* Add version fields after a second null byte */\n> +\tstrbuf_addch(&request, '\\0');\n> +\tstrbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n>  \n>  \tpacket_write(fd[1], request.buf, request.len);\n\nAs expected, the version information for git:// is now written\ndifferently.\n\n> @@ -1104,14 +1101,13 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n>   */\n>  static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n>  \t\t\t     enum ssh_variant variant, const char *port,\n> -\t\t\t     enum protocol_version version, int flags)\n> +\t\t\t     const struct strbuf *version_advert, int flags)\n>  {\n> -\tif (variant == VARIANT_SSH &&\n> -\t    version > 0) {\n> +\tif (variant == VARIANT_SSH) {\n>  \t\targv_array_push(args, \"-o\");\n>  \t\targv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n> -\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n> -\t\t\t\t version);\n> +\t\targv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n> +\t\t\t\t version_advert->buf);\n>  \t}\n>  \n>  \tif (flags & CONNECT_IPV4) {\n\nLikewise for ssh://.\n\n[snip other ssh code]\n\n> @@ -1226,16 +1223,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n>  {\n>  \tchar *hostandport, *path;\n>  \tstruct child_process *conn;\n> +\tstruct strbuf version_advert = STRBUF_INIT;\n>  \tenum protocol protocol;\n> -\tenum protocol_version version = get_protocol_version_config();\n>  \n> -\t/*\n> -\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n> -\t * to perform a push, then fallback to v0 since the client doesn't know\n> -\t * how to push yet using v2.\n> -\t */\n> -\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n> -\t\tversion = protocol_v0;\n> +\tget_client_protocol_version_advertisement(&version_advert);\n>  \n>  \t/* Without this we cannot rely on waitpid() to tell\n>  \t * what happened to our children.\n\nIt's great that this hardcoded override is now removed. I see below (in\nanother file) that the same occurs for http://.\n\n> +void register_allowed_protocol_versions_from_env(void)\n> +{\n> +\tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n> +\tconst char *version_str;\n> +\tstruct string_list version_list = STRING_LIST_INIT_DUP;\n> +\tstruct string_list_item *version;\n> +\n> +\tif (!git_protocol)\n> +\t\treturn;\n> +\n> +\tstring_list_split(&version_list, git_protocol, ':', -1);\n> +\tfor_each_string_list_item(version, &version_list) {\n> +\t\tif (skip_prefix(version->string, \"version=\", &version_str))\n> +\t\t\tregister_allowed_protocol_version(\n> +\t\t\t\tparse_protocol_version(version_str));\n\nProbably worth BUG() if does not start with \"version=\"?\n\n> +\t}\n> +\tstring_list_clear(&version_list, 0);\n> +}\n\n[snip]\n\n> +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> +{\n> +\tint i, tmp_nr = nr_allowed_versions;\n> +\tenum protocol_version *tmp_allowed_versions, config_version;\n> +\tstrbuf_reset(advert);\n> +\n> +\tversion_registration_locked = 1;\n> +\n> +\tconfig_version = get_protocol_version_config();\n> +\tif (config_version == protocol_v0) {\n> +\t\tstrbuf_addstr(advert, \"version=0\");\n> +\t\treturn;\n> +\t}\n\nWhy is protocol v0 special-cased like this?\n\n> +\tif (tmp_nr > 0) {\n> +\t\tALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> +\t\tcopy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> +\t\t\t   sizeof(enum protocol_version));\n> +\t} else {\n> +\t\tALLOC_ARRAY(tmp_allowed_versions, 1);\n> +\t\ttmp_nr = 1;\n> +\t\ttmp_allowed_versions[0] = config_version;\n\nIn this \"else\" block, wouldn't this mean that if we forget to declare\nany versions, we might send an unsupported version to the server? I\ndidn't check this, but it might happen if we invoked\ntransport_fetch_refs() without going through any of the builtins (which\nmight happen in the case of a lazy fetch in a partial clone).\n\n> +\t}\n> +\n> +\tif (tmp_allowed_versions[0] != config_version)\n> +\t\tfor (i = 1; i < nr_allowed_versions; i++)\n> +\t\t\tif (tmp_allowed_versions[i] == config_version) {\n> +\t\t\t\tSWAP(tmp_allowed_versions[0],\n> +\t\t\t\t     tmp_allowed_versions[i]);\n> +\t\t\t}\n\nIt doesn't seem right to just directly swap the config_version with the\nfront.\n\nAlso, I think this is more complicated that it needs to be. You could\njust search for the config version; if found, loop backwards starting\nfrom the index, shuffling to the right, then put the config version in\nelement 0.\n\n> +\n> +\tstrbuf_addf(advert, \"version=%s\",\n> +\t\t    format_protocol_version(tmp_allowed_versions[0]));\n> +\tfor (i = 1; i < tmp_nr; i++)\n> +\t\tstrbuf_addf(advert, \":version=%s\",\n> +\t\t\t    format_protocol_version(tmp_allowed_versions[i]));\n> +\n> +\tfree(tmp_allowed_versions);\n> +}\n\n[snip]\n\n>  \tconst char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n> @@ -38,9 +142,10 @@ enum protocol_version determine_protocol_version_server(void)\n>  \t/*\n>  \t * Determine which protocol version the client has requested.  Since\n>  \t * multiple 'version' keys can be sent by the client, indicating that\n> -\t * the client is okay to speak any of them, select the greatest version\n> -\t * that the client has requested.  This is due to the assumption that\n> -\t * the most recent protocol version will be the most state-of-the-art.\n> +\t * the client is okay to speak any of them, select the first\n> +\t * recognizable version that the client has requested.  This is due to\n> +\t * the assumption that the protocol versions will be listed in\n> +\t * preference order.\n>  \t */\n>  \tif (git_protocol) {\n>  \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> @@ -53,8 +158,11 @@ enum protocol_version determine_protocol_version_server(void)\n>  \n>  \t\t\tif (skip_prefix(item->string, \"version=\", &value)) {\n>  \t\t\t\tv = parse_protocol_version(value);\n> -\t\t\t\tif (v > version)\n> +\t\t\t\tif (v != protocol_unknown_version &&\n> +\t\t\t\t    is_allowed_protocol_version(v)) {\n>  \t\t\t\t\tversion = v;\n> +\t\t\t\t\tbreak;\n> +\t\t\t\t}\n>  \t\t\t}\n>  \t\t}\n\nHere we see that the server now takes the first version, as discussed.\n\n> diff --git a/protocol.h b/protocol.h\n> index 2ad35e433c..88330fd0ee 100644\n> --- a/protocol.h\n> +++ b/protocol.h\n> @@ -14,7 +14,24 @@ enum protocol_version {\n>   * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n>   * returned.\n>   */\n> -extern enum protocol_version get_protocol_version_config(void);\n> +enum protocol_version get_protocol_version_config(void);\n> +\n> +/*\n> + * Register an allowable protocol version for a given operation. Registration\n> + * must occur before attempting to advertise a version to a server process.\n> + */\n> +void register_allowed_protocol_version(enum protocol_version version);\n\nI think the allowed protocol versions have to be transport-scoped. A\nsingle builtin command may invoke multiple remote commands (as will be\nthe case if a lazy fetch in a partial clone is ever required).\n\n> @@ -360,16 +373,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n>  \t\tstrbuf_addf(&refs_url, \"service=%s\", service);\n>  \t}\n>  \n> -\t/*\n> -\t * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n> -\t * to perform a push, then fallback to v0 since the client doesn't know\n> -\t * how to push yet using v2.\n> -\t */\n> -\tif (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n> -\t\tversion = protocol_v0;\n> +\tget_client_protocol_version_advertisement(&version_advert);\n>  \n>  \t/* Add the extra Git-Protocol header */\n> -\tif (get_protocol_http_header(version, &protocol_header))\n> +\tif (get_client_protocol_http_header(&version_advert, &protocol_header))\n>  \t\tstring_list_append(&extra_headers, protocol_header.buf);\n>  \n>  \tmemset(&http_options, 0, sizeof(http_options));\n\nGreat to see that this hardcoded override is also removed for http://.\n\n> @@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n>  \tgit -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n>  \ttest_cmp expect actual &&\n>  \n> -\t# Client didnt request to use protocol v2\n> -\t! grep \"Git-Protocol: version=2\" log &&\n> -\t# Server didnt respond using protocol v2\n> -\t! grep \"git< version 2\" log\n> +\t# Server responded with version 1\n> +\tgrep \"git< version 1\" log\n>  '\n\nChange the test title - the client indeed would request 2, but since 1\nis in front, the server will use version 1.\n"},{"id":"368759","messageId":"CANq=j3u-zdb_FvNJGPCmygNMScseav63GhVvBX3NcVS4f7TejA@mail.gmail.com","threadId":"49473","inReplyTo":"20190205194226.172769-1-jonathantanmy@google.com","subject":"Re: [PATCH v6 1/1] protocol: advertise multiple supported versions","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2019-02-07T23:58:38Z","receivedAt":"2019-02-07T23:58:48Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2019.02.05 11:42, Jonathan Tan wrote:\n> Thanks. Overall, one major point: I think that the declaration of\n> supported versions need to be transport-scoped (search this email for\n> \"transport-scoped\" for context). And one medium point: it might be\n> better for protocol.version to be the preferred and maximum version, not\n> only the preferred version. I have other minor points, which you can\n> read below.\n\nThanks for the review, and apologies for the delayed reply. I have some\nquestions below about making the version advertisement transport scoped.\nI think you're right about making the config version the maximum,\nalthough I believe we've also avoided making the protocol versions\nstrictly orderable, so it may not be feasible if we want to keep that\nfeature.\n\n> > Currently the client advertises that it supports the wire protocol\n> > version set in the protocol.version config. However, not all services\n> > support the same set of protocol versions. For example, git-receive-pack\n> > supports v1 and v0, but not v2.\n>\n> That's true, except <see next paragraph>.\n>\n> > If a client connects to git-receive-pack\n> > and requests v2, it will instead be downgraded to v0.\n>\n> This is a bit misleading. The client never requests v2 - connect.c has\n> an override that changes it to v0 (as can be seen from the diff of this\n> patch) before the request takes place. If we were to create a custom\n> client that requests v2, the server would die with \"support for protocol\n> v2 not implemented yet\". (But maybe this is a bug - the server should\n> downgrade instead.)\n\nAck, will fix the description in the next version.\n\n\n> > Other services,\n> > such as git-upload-archive, do not do any version negotiation checks.\n> >\n> > This patch creates a protocol version registry. Individual client and\n> > server programs register all the protocol versions they support prior to\n> > communicating with a remote instance. Versions should be listed in\n> > preference order; the version specified in protocol.version will\n> > automatically be moved to the front of the registry.\n>\n> It took me a while to understand what's going on, so let me try to\n> summarize:\n>\n>  - The main issue is that send-pack doesn't support v2, so it would be\n>    incorrect for the client to advertise v2; this is currently fine\n>    because of the override in connect.c. A side issue is that the client\n>    advertises v2 for other things (e.g. archive), which currently works\n>    only because the server ignores them.\n>\n>  - To solve both these issues, each command declares which protocols it\n>    supports.\n>\n>  - Because we now have such a list, it is not too difficult to pass the\n>    entire list to the server, so we should do so. (Servers already\n>    support multiple versions.)\n>\n> > The protocol version registry is passed to remote helpers via the\n> > GIT_PROTOCOL environment variable.\n>\n> This is unfortunate to me, but I can't think of a better way to do it -\n> we do need to communicate the allowed version list somehow. A transport\n> option cannot be ignored by an old remote helper, but an envvar can.\n>\n> Backwards/forwards compatibility notes:\n>\n>  - If an old Git uses a new remote helper, no GIT_PROTOCOL is passed so\n>    the remote helper uses v0 regardless of what's configured in the\n>    repo. This should be fine.\n>  - If a new Git uses an old remote helper, what's configured gets used\n>    (unless it's for send-pack, in which case a version override occurs),\n>    regardless of which versions Git declares that it supports. I can\n>    envision some situations in which this would trip us up, but we are\n>    already in this exact situation anyway.\n>\n> > Additionally, remove special cases around advertising version=0.\n> > Previously we avoided adding version advertisements to the client's\n> > initial connection request if the client wanted version=0. However,\n> > including these advertisements does not change the version negotiation\n> > behavior, so it's better to have simpler code. As a side effect, this\n> > means that client operations over SSH will always include a\n> > \"SendEnv=GIT_PROTOCOL\" option on the SSH command line.\n>\n> HTTP too, I think?\n\nCorrect.\n\n\n> Also, this means that the client will send different information to the\n> server now, but it seems fine (as far as I know, clients already send\n> Git version information anyway).\n>\n> Overall, I would write the commit message this way:\n>\n>     A Git client can communicate with a Git server through various\n>     commands, and for each command, in various protocol versions. Each\n>     repo can define protocol.version as 0, 1, or 2 for the protocol\n>     version that it prefers. But this integer is handled differently by\n>     each command: if communicating with \"upload-pack\", the integer is\n>     passed verbatim to be interpreted by the server; if communicating\n>     with \"receive-pack\", the integer is hardcoded to be overridden to 0\n>     or 1; and if communicating with anything else (e.g. \"archive\"), the\n>     integer is passed verbatim but ignored by the server.\n>\n>     Handle each command the same way by having each client command\n>     declare which versions it supports, and whenever a command is\n>     invoked, send its list of versions to the server (instead of one\n>     single version, as is done currently). The protocol.version is now\n>     used as a hint to which version the client prefers; if it is present\n>     in the list, it is moved to the front, otherwise the list is sent\n>     unchanged.\n>\n>     The server already supports multiple versions, but currently chooses\n>     the greatest. Make the server choose the first supported version\n>     instead.\n>\n>     Because remote helpers now need this list of versions, pass the list\n>     of versions as the GIT_PROTOCOL environment variable. (New remote\n>     helpers will make use of this, and old remote helpers will use the\n>     repo configuration.)\n>\n>     <special case about advertising version=0>\n>\n>     <unnecessary externs>\n\nAck, will adjust the wording in the next version. Thanks for the\nsuggestion.\n\n\n> > diff --git a/builtin/archive.c b/builtin/archive.c\n> > index e74f675390..8adb9f381b 100644\n> > --- a/builtin/archive.c\n> > +++ b/builtin/archive.c\n> > @@ -8,6 +8,7 @@\n> >  #include \"transport.h\"\n> >  #include \"parse-options.h\"\n> >  #include \"pkt-line.h\"\n> > +#include \"protocol.h\"\n> >  #include \"sideband.h\"\n> >\n> >  static void create_output_file(const char *output_file)\n> > @@ -94,6 +95,8 @@ int cmd_archive(int argc, const char **argv, const char *prefix)\n> >             OPT_END()\n> >     };\n> >\n> > +   register_allowed_protocol_version(protocol_v0);\n> > +\n> >     argc = parse_options(argc, argv, prefix, local_opts, NULL,\n> >                          PARSE_OPT_KEEP_ALL);\n> >\n>\n> What happens if a command never declares any version? It might be best\n> to explicitly allow such a case, and treat it as a single list of\n> protocol_v0.\n\nAddressed below.\n\n\n> > diff --git a/connect.c b/connect.c\n> > index 94547e5056..57266b6cec 100644\n> > --- a/connect.c\n> > +++ b/connect.c\n> > @@ -1046,7 +1046,7 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,\n> >   */\n> >  static struct child_process *git_connect_git(int fd[2], char *hostandport,\n> >                                          const char *path, const char *prog,\n> > -                                        enum protocol_version version,\n> > +                                        const struct strbuf *version_advert,\n> >                                          int flags)\n> >  {\n> >     struct child_process *conn;\n> > @@ -1084,12 +1084,9 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n> >                 prog, path, 0,\n> >                 target_host, 0);\n> >\n> > -   /* If using a new version put that stuff here after a second null byte */\n> > -   if (version > 0) {\n> > -           strbuf_addch(&request, '\\0');\n> > -           strbuf_addf(&request, \"version=%d%c\",\n> > -                       version, '\\0');\n> > -   }\n> > +   /* Add version fields after a second null byte */\n> > +   strbuf_addch(&request, '\\0');\n> > +   strbuf_addf(&request, \"%s%c\", version_advert->buf, '\\0');\n> >\n> >     packet_write(fd[1], request.buf, request.len);\n>\n> As expected, the version information for git:// is now written\n> differently.\n>\n> > @@ -1104,14 +1101,13 @@ static struct child_process *git_connect_git(int fd[2], char *hostandport,\n> >   */\n> >  static void push_ssh_options(struct argv_array *args, struct argv_array *env,\n> >                          enum ssh_variant variant, const char *port,\n> > -                        enum protocol_version version, int flags)\n> > +                        const struct strbuf *version_advert, int flags)\n> >  {\n> > -   if (variant == VARIANT_SSH &&\n> > -       version > 0) {\n> > +   if (variant == VARIANT_SSH) {\n> >             argv_array_push(args, \"-o\");\n> >             argv_array_push(args, \"SendEnv=\" GIT_PROTOCOL_ENVIRONMENT);\n> > -           argv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=version=%d\",\n> > -                            version);\n> > +           argv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT \"=%s\",\n> > +                            version_advert->buf);\n> >     }\n> >\n> >     if (flags & CONNECT_IPV4) {\n>\n> Likewise for ssh://.\n>\n> [snip other ssh code]\n>\n> > @@ -1226,16 +1223,10 @@ struct child_process *git_connect(int fd[2], const char *url,\n> >  {\n> >     char *hostandport, *path;\n> >     struct child_process *conn;\n> > +   struct strbuf version_advert = STRBUF_INIT;\n> >     enum protocol protocol;\n> > -   enum protocol_version version = get_protocol_version_config();\n> >\n> > -   /*\n> > -    * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n> > -    * to perform a push, then fallback to v0 since the client doesn't know\n> > -    * how to push yet using v2.\n> > -    */\n> > -   if (version == protocol_v2 && !strcmp(\"git-receive-pack\", prog))\n> > -           version = protocol_v0;\n> > +   get_client_protocol_version_advertisement(&version_advert);\n> >\n> >     /* Without this we cannot rely on waitpid() to tell\n> >      * what happened to our children.\n>\n> It's great that this hardcoded override is now removed. I see below (in\n> another file) that the same occurs for http://.\n>\n> > +void register_allowed_protocol_versions_from_env(void)\n> > +{\n> > +   const char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n> > +   const char *version_str;\n> > +   struct string_list version_list = STRING_LIST_INIT_DUP;\n> > +   struct string_list_item *version;\n> > +\n> > +   if (!git_protocol)\n> > +           return;\n> > +\n> > +   string_list_split(&version_list, git_protocol, ':', -1);\n> > +   for_each_string_list_item(version, &version_list) {\n> > +           if (skip_prefix(version->string, \"version=\", &version_str))\n> > +                   register_allowed_protocol_version(\n> > +                           parse_protocol_version(version_str));\n>\n> Probably worth BUG() if does not start with \"version=\"?\n\nAgreed.\n\n\n> > +   }\n> > +   string_list_clear(&version_list, 0);\n> > +}\n>\n> [snip]\n>\n> > +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> > +{\n> > +   int i, tmp_nr = nr_allowed_versions;\n> > +   enum protocol_version *tmp_allowed_versions, config_version;\n> > +   strbuf_reset(advert);\n> > +\n> > +   version_registration_locked = 1;\n> > +\n> > +   config_version = get_protocol_version_config();\n> > +   if (config_version == protocol_v0) {\n> > +           strbuf_addstr(advert, \"version=0\");\n> > +           return;\n> > +   }\n>\n> Why is protocol v0 special-cased like this?\n\nTo semi-preserve the existing behavior that no version negotiation would\nhappen when the config specifies version 0. Now we'll send out a version\nadvertisement where we wouldn't have before, but we only advertise v0 so\nthat no negotiation can happen.\n\n\n> > +   if (tmp_nr > 0) {\n> > +           ALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> > +           copy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> > +                      sizeof(enum protocol_version));\n> > +   } else {\n> > +           ALLOC_ARRAY(tmp_allowed_versions, 1);\n> > +           tmp_nr = 1;\n> > +           tmp_allowed_versions[0] = config_version;\n>\n> In this \"else\" block, wouldn't this mean that if we forget to declare\n> any versions, we might send an unsupported version to the server? I\n> didn't check this, but it might happen if we invoked\n> transport_fetch_refs() without going through any of the builtins (which\n> might happen in the case of a lazy fetch in a partial clone).\n\nHmm yeah. I changed the else{} here to die if we haven't registered any\nversions prior to creating the advertisement string. t0410 case 9 and\nt5702 case 20 both fail, and both are lazy fetches in partial clones.\n\nIn this case, defaulting to the version in the config works, because\nfetching is valid for all protocol versions. But if we ever add some\nversion X where fetching hasn't been implemented this would break.\n\nShould we change the else{} case here to be a BUG() and then fix\nfetch-object to register its supported versions?\n\n\n> > +   }\n> > +\n> > +   if (tmp_allowed_versions[0] != config_version)\n> > +           for (i = 1; i < nr_allowed_versions; i++)\n> > +                   if (tmp_allowed_versions[i] == config_version) {\n> > +                           SWAP(tmp_allowed_versions[0],\n> > +                                tmp_allowed_versions[i]);\n> > +                   }\n>\n> It doesn't seem right to just directly swap the config_version with the\n> front.\n>\n> Also, I think this is more complicated that it needs to be. You could\n> just search for the config version; if found, loop backwards starting\n> from the index, shuffling to the right, then put the config version in\n> element 0.\n\nAck. Will fix.\n\n\n> > +\n> > +   strbuf_addf(advert, \"version=%s\",\n> > +               format_protocol_version(tmp_allowed_versions[0]));\n> > +   for (i = 1; i < tmp_nr; i++)\n> > +           strbuf_addf(advert, \":version=%s\",\n> > +                       format_protocol_version(tmp_allowed_versions[i]));\n> > +\n> > +   free(tmp_allowed_versions);\n> > +}\n>\n> [snip]\n>\n> >     const char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT);\n> > @@ -38,9 +142,10 @@ enum protocol_version determine_protocol_version_server(void)\n> >     /*\n> >      * Determine which protocol version the client has requested.  Since\n> >      * multiple 'version' keys can be sent by the client, indicating that\n> > -    * the client is okay to speak any of them, select the greatest version\n> > -    * that the client has requested.  This is due to the assumption that\n> > -    * the most recent protocol version will be the most state-of-the-art.\n> > +    * the client is okay to speak any of them, select the first\n> > +    * recognizable version that the client has requested.  This is due to\n> > +    * the assumption that the protocol versions will be listed in\n> > +    * preference order.\n> >      */\n> >     if (git_protocol) {\n> >             struct string_list list = STRING_LIST_INIT_DUP;\n> > @@ -53,8 +158,11 @@ enum protocol_version determine_protocol_version_server(void)\n> > \n> >                     if (skip_prefix(item->string, \"version=\", &value)) {\n> >                             v = parse_protocol_version(value);\n> > -                           if (v > version)\n> > +                           if (v != protocol_unknown_version &&\n> > +                               is_allowed_protocol_version(v)) {\n> >                                     version = v;\n> > +                                   break;\n> > +                           }\n> >                     }\n> >             }\n>\n> Here we see that the server now takes the first version, as discussed.\n>\n> > diff --git a/protocol.h b/protocol.h\n> > index 2ad35e433c..88330fd0ee 100644\n> > --- a/protocol.h\n> > +++ b/protocol.h\n> > @@ -14,7 +14,24 @@ enum protocol_version {\n> >   * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n> >   * returned.\n> >   */\n> > -extern enum protocol_version get_protocol_version_config(void);\n> > +enum protocol_version get_protocol_version_config(void);\n> > +\n> > +/*\n> > + * Register an allowable protocol version for a given operation. Registration\n> > + * must occur before attempting to advertise a version to a server process.\n> > + */\n> > +void register_allowed_protocol_version(enum protocol_version version);\n>\n> I think the allowed protocol versions have to be transport-scoped. A\n> single builtin command may invoke multiple remote commands (as will be\n> the case if a lazy fetch in a partial clone is ever required).\n\nWouldn't it need to be per-transport per-command then? For example, if\nwe ever added a hypothetical git-fetch-then-rebase-then-push builtin, we\ncouldn't use the same version advertisement for the fetch and the push,\neven if they're still using the same transport. Or would we have to use\nseparate transport objects for the fetch and the push in such a\nsituation?\n\nIf we do move the version list into the transport, what would be the\nright way to register the allowed versions? Maybe we would make each\nvtable entry declare its versions?\n\n> > @@ -360,16 +373,10 @@ static struct discovery *discover_refs(const char *service, int for_push)\n> >             strbuf_addf(&refs_url, \"service=%s\", service);\n> >     }\n> > \n> > -   /*\n> > -    * NEEDSWORK: If we are trying to use protocol v2 and we are planning\n> > -    * to perform a push, then fallback to v0 since the client doesn't know\n> > -    * how to push yet using v2.\n> > -    */\n> > -   if (version == protocol_v2 && !strcmp(\"git-receive-pack\", service))\n> > -           version = protocol_v0;\n> > +   get_client_protocol_version_advertisement(&version_advert);\n> > \n> >     /* Add the extra Git-Protocol header */\n> > -   if (get_protocol_http_header(version, &protocol_header))\n> > +   if (get_client_protocol_http_header(&version_advert, &protocol_header))\n> >             string_list_append(&extra_headers, protocol_header.buf);\n> > \n> >     memset(&http_options, 0, sizeof(http_options));\n>\n> Great to see that this hardcoded override is also removed for http://.\n>\n> > @@ -489,10 +489,8 @@ test_expect_success 'push with http:// and a config of v2 does not request v2' '\n> >     git -C \"$HTTPD_DOCUMENT_ROOT_PATH/http_parent\" log -1 --format=%s client_branch >expect &&\n> >     test_cmp expect actual &&\n> > \n> > -   # Client didnt request to use protocol v2\n> > -   ! grep \"Git-Protocol: version=2\" log &&\n> > -   # Server didnt respond using protocol v2\n> > -   ! grep \"git< version 2\" log\n> > +   # Server responded with version 1\n> > +   grep \"git< version 1\" log\n> >  '\n>\n> Change the test title - the client indeed would request 2, but since 1\n> is in front, the server will use version 1.\n\nWill do.\n"},{"id":"369022","messageId":"20190211185323.75409-1-jonathantanmy@google.com","threadId":"49473","inReplyTo":"CANq=j3u-zdb_FvNJGPCmygNMScseav63GhVvBX3NcVS4f7TejA@mail.gmail.com","subject":"Re: [PATCH v6 1/1] protocol: advertise multiple supported versions","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2019-02-11T18:53:23Z","receivedAt":"2019-02-11T18:53:30Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"> > > +void get_client_protocol_version_advertisement(struct strbuf *advert)\n> > > +{\n> > > +   int i, tmp_nr = nr_allowed_versions;\n> > > +   enum protocol_version *tmp_allowed_versions, config_version;\n> > > +   strbuf_reset(advert);\n> > > +\n> > > +   version_registration_locked = 1;\n> > > +\n> > > +   config_version = get_protocol_version_config();\n> > > +   if (config_version == protocol_v0) {\n> > > +           strbuf_addstr(advert, \"version=0\");\n> > > +           return;\n> > > +   }\n> >\n> > Why is protocol v0 special-cased like this?\n> \n> To semi-preserve the existing behavior that no version negotiation would\n> happen when the config specifies version 0. Now we'll send out a version\n> advertisement where we wouldn't have before, but we only advertise v0 so\n> that no negotiation can happen.\n\nAh, I see.\n\nThis might be an argument for deciding that the protocol versions are\nstrictly orderable. I don't recall any discussions around this, but it\nmakes sense to me that someone specifying version 1 would be OK with\nversion 0 but not version 2, and someone specifying version 2 would be\nOK with any of 0, 1, and 2. So I'm OK with making it effectively\nstrictly orderable by introducing the concept of a maximum.\n\nAnd if we have this concept of a maximum, then we can have the\nno-negotiation-if-v0-configured behavior without any special cases.\n\n> > > +   if (tmp_nr > 0) {\n> > > +           ALLOC_ARRAY(tmp_allowed_versions, tmp_nr);\n> > > +           copy_array(tmp_allowed_versions, allowed_versions, tmp_nr,\n> > > +                      sizeof(enum protocol_version));\n> > > +   } else {\n> > > +           ALLOC_ARRAY(tmp_allowed_versions, 1);\n> > > +           tmp_nr = 1;\n> > > +           tmp_allowed_versions[0] = config_version;\n> >\n> > In this \"else\" block, wouldn't this mean that if we forget to declare\n> > any versions, we might send an unsupported version to the server? I\n> > didn't check this, but it might happen if we invoked\n> > transport_fetch_refs() without going through any of the builtins (which\n> > might happen in the case of a lazy fetch in a partial clone).\n> \n> Hmm yeah. I changed the else{} here to die if we haven't registered any\n> versions prior to creating the advertisement string. t0410 case 9 and\n> t5702 case 20 both fail, and both are lazy fetches in partial clones.\n> \n> In this case, defaulting to the version in the config works, because\n> fetching is valid for all protocol versions. But if we ever add some\n> version X where fetching hasn't been implemented this would break.\n> \n> Should we change the else{} case here to be a BUG() and then fix\n> fetch-object to register its supported versions?\n\nYes, I think so.\n\n> > > diff --git a/protocol.h b/protocol.h\n> > > index 2ad35e433c..88330fd0ee 100644\n> > > --- a/protocol.h\n> > > +++ b/protocol.h\n> > > @@ -14,7 +14,24 @@ enum protocol_version {\n> > >   * 'protocol.version' config.  If unconfigured, a value of 'protocol_v0' is\n> > >   * returned.\n> > >   */\n> > > -extern enum protocol_version get_protocol_version_config(void);\n> > > +enum protocol_version get_protocol_version_config(void);\n> > > +\n> > > +/*\n> > > + * Register an allowable protocol version for a given operation. Registration\n> > > + * must occur before attempting to advertise a version to a server process.\n> > > + */\n> > > +void register_allowed_protocol_version(enum protocol_version version);\n> >\n> > I think the allowed protocol versions have to be transport-scoped. A\n> > single builtin command may invoke multiple remote commands (as will be\n> > the case if a lazy fetch in a partial clone is ever required).\n> \n> Wouldn't it need to be per-transport per-command then? For example, if\n> we ever added a hypothetical git-fetch-then-rebase-then-push builtin, we\n> couldn't use the same version advertisement for the fetch and the push,\n> even if they're still using the same transport. Or would we have to use\n> separate transport objects for the fetch and the push in such a\n> situation?\n\nIf we make each implementation of the vtable function read the config\nand compute its own list of allowed protocols (is this what you mean by\n\"make each vtable entry declare its versions\" below?), then we still\nleave open the possibility of using the same transport object for fetch\nand push.\n\n> If we do move the version list into the transport, what would be the\n> right way to register the allowed versions? Maybe we would make each\n> vtable entry declare its versions?\n\nThis makes sense to me. I think that the implementations of\ntransport_vtable.connect might need to take in a list of versions, but\nfetch and push can probably read the config by themselves so the API\ndoes not need to change.\n"}]}