{"thread":{"id":"61375","subject":"[PATCH 00/13] builtin: implement, document and test url-parse","startedAt":"2024-04-28T22:31:05Z","lastAt":"2026-05-12T09:02:57Z","messageCount":44,"participants":["Matheus Moreira via GitGitGadget","Matheus Afonso Martins Moreira via GitGitGadget","Torsten Bögershausen","Matheus Afonso Martins Moreira","Ghanshyam Thakkar","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":13},"messages":[{"id":"493560","messageId":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":null,"subject":"[PATCH 00/13] builtin: implement, document and test url-parse","fromName":"Matheus Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:48Z","receivedAt":"2024-04-28T22:31:05Z","isPatch":true,"body":"Git commands accept a wide variety of URLs syntaxes, not just standard URLs.\nThis can make parsing git URLs difficult since standard URL parsers cannot\nbe used. Even if an external parser were implemented, it would have to track\ngit's development closely in case support for any new URL schemes are added.\n\nThese patches introduce a new url-parse builtin command that exposes git's\nnative URL parsing algorithms as a plumbing command, allowing other programs\nto then call upon git itself to parse the git URLs and their components.\n\nThis should be quite useful for scripts. For example, a script might want to\nadd remotes to repositories, naming them according to the domain name where\nthe repository is hosted. This new builtin allows it to parse the git URL\nand extract its host name which can then be used as input for other\noperations. This would be difficult to implement otherwise due to git's\nsupport for scp style URLs.\n\nSigned-off-by: Matheus Afonso Martins Moreira matheus@matheusmoreira.com\n\nMatheus Afonso Martins Moreira (13):\n  url: move helper function to URL header and source\n  urlmatch: define url_parse function\n  builtin: create url-parse command\n  url-parse: add URL parsing helper function\n  url-parse: enumerate possible URL components\n  url-parse: define component extraction helper fn\n  url-parse: define string to component converter fn\n  url-parse: define usage and options\n  url-parse: parse options given on the command line\n  url-parse: validate all given git URLs\n  url-parse: output URL components selected by user\n  Documentation: describe the url-parse builtin\n  tests: add tests for the new url-parse builtin\n\n .gitignore                      |   1 +\n Documentation/git-url-parse.txt |  59 ++++++++++\n Makefile                        |   1 +\n builtin.h                       |   1 +\n builtin/url-parse.c             | 132 ++++++++++++++++++++++\n command-list.txt                |   1 +\n connect.c                       |   8 --\n connect.h                       |   1 -\n git.c                           |   1 +\n remote.c                        |   1 +\n t/t9904-url-parse.sh            | 194 ++++++++++++++++++++++++++++++++\n url.c                           |   8 ++\n url.h                           |   2 +\n urlmatch.c                      |  90 +++++++++++++++\n urlmatch.h                      |   1 +\n 15 files changed, 492 insertions(+), 9 deletions(-)\n create mode 100644 Documentation/git-url-parse.txt\n create mode 100644 builtin/url-parse.c\n create mode 100755 t/t9904-url-parse.sh\n\n\nbase-commit: e326e520101dcf43a0499c3adc2df7eca30add2d\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1715%2Fmatheusmoreira%2Furl-parse-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1715/matheusmoreira/url-parse-v1\nPull-Request: https://github.com/git/git/pull/1715\n-- \ngitgitgadget\n"},{"id":"493561","messageId":"42eb0cbf68a4f7ee46041687277d529fe981df6b.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 01/13] url: move helper function to URL header and source","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:49Z","receivedAt":"2024-04-28T22:31:05Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nIt will be used in more places so it should be placed in url.h.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 8 --------\n connect.h | 1 -\n remote.c  | 1 +\n url.c     | 8 ++++++++\n url.h     | 2 ++\n 5 files changed, 11 insertions(+), 9 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex 0d77737a536..0cd9439501b 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -693,14 +693,6 @@ enum protocol {\n \tPROTO_GIT\n };\n \n-int url_is_local_not_ssh(const char *url)\n-{\n-\tconst char *colon = strchr(url, ':');\n-\tconst char *slash = strchr(url, '/');\n-\treturn !colon || (slash && slash < colon) ||\n-\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n-}\n-\n static const char *prot_name(enum protocol protocol)\n {\n \tswitch (protocol) {\ndiff --git a/connect.h b/connect.h\nindex 1645126c17f..8d84f6656b1 100644\n--- a/connect.h\n+++ b/connect.h\n@@ -13,7 +13,6 @@ int git_connection_is_socket(struct child_process *conn);\n int server_supports(const char *feature);\n int parse_feature_request(const char *features, const char *feature);\n const char *server_feature_value(const char *feature, size_t *len_ret);\n-int url_is_local_not_ssh(const char *url);\n \n struct packet_reader;\n enum protocol_version discover_version(struct packet_reader *reader);\ndiff --git a/remote.c b/remote.c\nindex 2b650b813b7..2425dbc4660 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -5,6 +5,7 @@\n #include \"gettext.h\"\n #include \"hex.h\"\n #include \"remote.h\"\n+#include \"url.h\"\n #include \"urlmatch.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\ndiff --git a/url.c b/url.c\nindex 282b12495ae..c36818c3037 100644\n--- a/url.c\n+++ b/url.c\n@@ -119,3 +119,11 @@ void str_end_url_with_slash(const char *url, char **dest)\n \tfree(*dest);\n \t*dest = strbuf_detach(&buf, NULL);\n }\n+\n+int url_is_local_not_ssh(const char *url)\n+{\n+\tconst char *colon = strchr(url, ':');\n+\tconst char *slash = strchr(url, '/');\n+\treturn !colon || (slash && slash < colon) ||\n+\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n+}\ndiff --git a/url.h b/url.h\nindex 2a27c342776..867d3af6691 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,6 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+int url_is_local_not_ssh(const char *url);\n+\n #endif /* URL_H */\n-- \ngitgitgadget\n\n"},{"id":"493562","messageId":"13b81b8aa06cfd63a5fd9d1acbaf21a8b388ff47.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 02/13] urlmatch: define url_parse function","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:50Z","receivedAt":"2024-04-28T22:31:06Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nDefine general parsing function that supports all Git URLs\nincluding scp style URLs such as hostname:~user/repo.\nHas the same interface as the URL normalization function\nand uses the same data structures, facilitating its use.\nIt's adapted from the algorithm used to process URLs in connect.c,\nso it should support the same inputs.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n urlmatch.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n urlmatch.h |  1 +\n 2 files changed, 91 insertions(+)\n\ndiff --git a/urlmatch.c b/urlmatch.c\nindex 1d0254abacb..5a442e31fa2 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -3,6 +3,7 @@\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"urlmatch.h\"\n+#include \"url.h\"\n \n #define URL_ALPHA \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n #define URL_DIGIT \"0123456789\"\n@@ -438,6 +439,95 @@ char *url_normalize(const char *url, struct url_info *out_info)\n \treturn url_normalize_1(url, out_info, 0);\n }\n \n+enum protocol {\n+\tPROTO_UNKNOWN = 0,\n+\tPROTO_LOCAL,\n+\tPROTO_FILE,\n+\tPROTO_SSH,\n+\tPROTO_GIT,\n+};\n+\n+static enum protocol url_get_protocol(const char *name, size_t n)\n+{\n+\tif (!strncmp(name, \"ssh\", n))\n+\t\treturn PROTO_SSH;\n+\tif (!strncmp(name, \"git\", n))\n+\t\treturn PROTO_GIT;\n+\tif (!strncmp(name, \"git+ssh\", n)) /* deprecated - do not use */\n+\t\treturn PROTO_SSH;\n+\tif (!strncmp(name, \"ssh+git\", n)) /* deprecated - do not use */\n+\t\treturn PROTO_SSH;\n+\tif (!strncmp(name, \"file\", n))\n+\t\treturn PROTO_FILE;\n+\treturn PROTO_UNKNOWN;\n+}\n+\n+char *url_parse(const char *url_orig, struct url_info *out_info)\n+{\n+\tstruct strbuf url;\n+\tchar *host, *separator;\n+\tchar *detached, *normalized;\n+\tenum protocol protocol = PROTO_LOCAL;\n+\tstruct url_info local_info;\n+\tstruct url_info *info = out_info? out_info : &local_info;\n+\tbool scp_syntax = false;\n+\n+\tif (is_url(url_orig)) {\n+\t\turl_orig = url_decode(url_orig);\n+\t} else {\n+\t\turl_orig = xstrdup(url_orig);\n+\t}\n+\n+\tstrbuf_init(&url, strlen(url_orig) + sizeof(\"ssh://\"));\n+\tstrbuf_addstr(&url, url_orig);\n+\n+\thost = strstr(url.buf, \"://\");\n+\tif (host) {\n+\t\tprotocol = url_get_protocol(url.buf, host - url.buf);\n+\t\thost += 3;\n+\t} else {\n+\t\tif (!url_is_local_not_ssh(url.buf)) {\n+\t\t\tscp_syntax = true;\n+\t\t\tprotocol = PROTO_SSH;\n+\t\t\tstrbuf_insertstr(&url, 0, \"ssh://\");\n+\t\t\thost = url.buf + 6;\n+\t\t}\n+\t}\n+\n+\t/* path starts after ':' in scp style SSH URLs */\n+\tif (scp_syntax) {\n+\t\tseparator = strchr(host, ':');\n+\t\tif (separator) {\n+\t\t\tif (separator[1] == '/')\n+\t\t\t\tstrbuf_remove(&url, separator - url.buf, 1);\n+\t\t\telse\n+\t\t\t\t*separator = '/';\n+\t\t}\n+\t}\n+\n+\tdetached = strbuf_detach(&url, NULL);\n+\tnormalized = url_normalize(detached, info);\n+\tfree(detached);\n+\n+\tif (!normalized) {\n+\t\treturn NULL;\n+\t}\n+\n+\t/* point path to ~ for URL's like this:\n+\t *\n+\t *     ssh://host.xz/~user/repo\n+\t *     git://host.xz/~user/repo\n+\t *     host.xz:~user/repo\n+\t *\n+\t */\n+\tif (protocol == PROTO_GIT || protocol == PROTO_SSH) {\n+\t\tif (normalized[info->path_off + 1] == '~')\n+\t\t\tinfo->path_off++;\n+\t}\n+\n+\treturn normalized;\n+}\n+\n static size_t url_match_prefix(const char *url,\n \t\t\t       const char *url_prefix,\n \t\t\t       size_t url_prefix_len)\ndiff --git a/urlmatch.h b/urlmatch.h\nindex 5ba85cea139..6b3ce428582 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -35,6 +35,7 @@ struct url_info {\n };\n \n char *url_normalize(const char *, struct url_info *);\n+char *url_parse(const char *, struct url_info *);\n \n struct urlmatch_item {\n \tsize_t hostmatch_len;\n-- \ngitgitgadget\n\n"},{"id":"493563","messageId":"e4781b36d546ee7ab43566c8c7f5f0c68064b249.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 03/13] builtin: create url-parse command","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:51Z","receivedAt":"2024-04-28T22:31:07Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nGit commands can accept a rather wide variety of URLs syntaxes.\nThe range of accepted inputs might expand even more in the future.\nThis makes the parsing of URL components difficult since standard URL\nparsers cannot be used. Extracting the components of a git URL would\nrequire implementing all the schemes that git itself supports, not to\nmention tracking its development continuously in case new URL schemes\nare added.\n\nThe url-parse builtin command is designed to solve this problem\nby exposing git's native URL parsing facilities as a plumbing command.\nOther programs can then call upon git itself to parse the git URLs and\nextract their components. This should be quite useful for scripts.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n .gitignore          |  1 +\n Makefile            |  1 +\n builtin.h           |  1 +\n builtin/url-parse.c | 18 ++++++++++++++++++\n command-list.txt    |  1 +\n git.c               |  1 +\n 6 files changed, 23 insertions(+)\n create mode 100644 builtin/url-parse.c\n\ndiff --git a/.gitignore b/.gitignore\nindex 612c0f6a0ff..4f8dde600a5 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -174,6 +174,7 @@\n /git-update-server-info\n /git-upload-archive\n /git-upload-pack\n+/git-url-parse\n /git-var\n /git-verify-commit\n /git-verify-pack\ndiff --git a/Makefile b/Makefile\nindex 1e31acc72ec..b6054b5c1f4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1326,6 +1326,7 @@ BUILTIN_OBJS += builtin/update-ref.o\n BUILTIN_OBJS += builtin/update-server-info.o\n BUILTIN_OBJS += builtin/upload-archive.o\n BUILTIN_OBJS += builtin/upload-pack.o\n+BUILTIN_OBJS += builtin/url-parse.o\n BUILTIN_OBJS += builtin/var.o\n BUILTIN_OBJS += builtin/verify-commit.o\n BUILTIN_OBJS += builtin/verify-pack.o\ndiff --git a/builtin.h b/builtin.h\nindex 28280636da8..e8858808943 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -240,6 +240,7 @@ int cmd_update_server_info(int argc, const char **argv, const char *prefix);\n int cmd_upload_archive(int argc, const char **argv, const char *prefix);\n int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix);\n int cmd_upload_pack(int argc, const char **argv, const char *prefix);\n+int cmd_url_parse(int argc, const char **argv, const char *prefix);\n int cmd_var(int argc, const char **argv, const char *prefix);\n int cmd_verify_commit(int argc, const char **argv, const char *prefix);\n int cmd_verify_tag(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nnew file mode 100644\nindex 00000000000..994ccec4b2e\n--- /dev/null\n+++ b/builtin/url-parse.c\n@@ -0,0 +1,18 @@\n+/* SPDX-License-Identifier: GPL-2.0-only\n+ *\n+ * url-parse - parses git URLs and extracts their components\n+ *\n+ * Copyright © 2024 Matheus Afonso Martins Moreira\n+ *\n+ * This program is free software; you can redistribute it and/or modify\n+ * it under the terms of the GNU General Public License as published by\n+ * the Free Software Foundation; version 2.\n+ */\n+\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+\n+int cmd_url_parse(int argc, const char **argv, const char *prefix)\n+{\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex c4cd0f352b8..6d89b6c4dc6 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -196,6 +196,7 @@ git-update-ref                          plumbingmanipulators\n git-update-server-info                  synchingrepositories\n git-upload-archive                      synchelpers\n git-upload-pack                         synchelpers\n+git-url-parse                           plumbinginterrogators\n git-var                                 plumbinginterrogators\n git-verify-commit                       ancillaryinterrogators\n git-verify-pack                         plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 654d615a188..7aac812d9d4 100644\n--- a/git.c\n+++ b/git.c\n@@ -625,6 +625,7 @@ static struct cmd_struct commands[] = {\n \t{ \"upload-archive\", cmd_upload_archive, NO_PARSEOPT },\n \t{ \"upload-archive--writer\", cmd_upload_archive_writer, NO_PARSEOPT },\n \t{ \"upload-pack\", cmd_upload_pack },\n+\t{ \"url-parse\", cmd_url_parse, NO_PARSEOPT },\n \t{ \"var\", cmd_var, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"verify-commit\", cmd_verify_commit, RUN_SETUP },\n \t{ \"verify-pack\", cmd_verify_pack },\n-- \ngitgitgadget\n\n"},{"id":"493564","messageId":"1e0895651cb77759e983b365eda0e7546bf7453d.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 04/13] url-parse: add URL parsing helper function","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:52Z","receivedAt":"2024-04-28T22:31:08Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nThis function either successfully parses an URL\nor dies with an error message. Since this is a\nplumbing command, the error message is not translated.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex 994ccec4b2e..933e63aaa0a 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -11,6 +11,16 @@\n \n #include \"builtin.h\"\n #include \"gettext.h\"\n+#include \"urlmatch.h\"\n+\n+static void parse_or_die(const char *url, struct url_info *info)\n+{\n+\tif (url_parse(url, info)) {\n+\t\treturn;\n+\t} else {\n+\t\tdie(\"invalid git URL '%s', %s\", url, info->err);\n+\t}\n+}\n \n int cmd_url_parse(int argc, const char **argv, const char *prefix)\n {\n-- \ngitgitgadget\n\n"},{"id":"493565","messageId":"0bf83ee1228130df6069206190b97f1d0329794e.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 05/13] url-parse: enumerate possible URL components","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:53Z","receivedAt":"2024-04-28T22:31:09Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nCreate an enumeration containing all possible git URL components\nwhich may be selected by the user. The URL_NONE component is used\nwhen the user did not request the parsing of any component.\nIn this case, the command will return successfully if the URL parses.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex 933e63aaa0a..d250338422e 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -13,6 +13,16 @@\n #include \"gettext.h\"\n #include \"urlmatch.h\"\n \n+enum url_component {\n+\tURL_NONE = 0,\n+\tURL_PROTOCOL,\n+\tURL_USER,\n+\tURL_PASSWORD,\n+\tURL_HOST,\n+\tURL_PORT,\n+\tURL_PATH,\n+};\n+\n static void parse_or_die(const char *url, struct url_info *info)\n {\n \tif (url_parse(url, info)) {\n-- \ngitgitgadget\n\n"},{"id":"493566","messageId":"149c476b1ed74897ffbdf711754b9804fa679467.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 06/13] url-parse: define component extraction helper fn","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:54Z","receivedAt":"2024-04-28T22:31:10Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nThe extract function returns a newly allocated string\nwhose contents are the specified git URL component.\nThe string must be freed later.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 36 ++++++++++++++++++++++++++++++++++++\n 1 file changed, 36 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex d250338422e..b8ac46dcdeb 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -32,6 +32,42 @@ static void parse_or_die(const char *url, struct url_info *info)\n \t}\n }\n \n+static char *extract(enum url_component component, struct url_info *info)\n+{\n+\tsize_t offset, length;\n+\n+\tswitch (component) {\n+\tcase URL_PROTOCOL:\n+\t\toffset = 0;\n+\t\tlength = info->scheme_len;\n+\t\tbreak;\n+\tcase URL_USER:\n+\t\toffset = info->user_off;\n+\t\tlength = info->user_len;\n+\t\tbreak;\n+\tcase URL_PASSWORD:\n+\t\toffset = info->passwd_off;\n+\t\tlength = info->passwd_len;\n+\t\tbreak;\n+\tcase URL_HOST:\n+\t\toffset = info->host_off;\n+\t\tlength = info->host_len;\n+\t\tbreak;\n+\tcase URL_PORT:\n+\t\toffset = info->port_off;\n+\t\tlength = info->port_len;\n+\t\tbreak;\n+\tcase URL_PATH:\n+\t\toffset = info->path_off;\n+\t\tlength = info->path_len;\n+\t\tbreak;\n+\tcase URL_NONE:\n+\t\treturn NULL;\n+\t}\n+\n+\treturn xstrndup(info->url + offset, length);\n+}\n+\n int cmd_url_parse(int argc, const char **argv, const char *prefix)\n {\n \treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"493567","messageId":"eb9ef8a17bd123b2611d5df0fab4364fbff4b277.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 07/13] url-parse: define string to component converter fn","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:55Z","receivedAt":"2024-04-28T22:31:11Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nConverts a git URL component name to its corresponding\nenumeration value so that it can be conveniently used\ninternally by the url-parse command.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex b8ac46dcdeb..15923460a78 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -32,6 +32,23 @@ static void parse_or_die(const char *url, struct url_info *info)\n \t}\n }\n \n+static enum url_component get_component_or_die(const char *arg)\n+{\n+\tif (!strcmp(\"path\", arg))\n+\t\treturn URL_PATH;\n+\tif (!strcmp(\"host\", arg))\n+\t\treturn URL_HOST;\n+\tif (!strcmp(\"protocol\", arg))\n+\t\treturn URL_PROTOCOL;\n+\tif (!strcmp(\"user\", arg))\n+\t\treturn URL_USER;\n+\tif (!strcmp(\"password\", arg))\n+\t\treturn URL_PASSWORD;\n+\tif (!strcmp(\"port\", arg))\n+\t\treturn URL_PORT;\n+\tdie(\"invalid git URL component '%s'\", arg);\n+}\n+\n static char *extract(enum url_component component, struct url_info *info)\n {\n \tsize_t offset, length;\n-- \ngitgitgadget\n\n"},{"id":"493568","messageId":"a2acfdbc76c29f59c24c8a8b25076bde121efdf3.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 08/13] url-parse: define usage and options","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:56Z","receivedAt":"2024-04-28T22:31:12Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nCreate the data structures expected by the git option parser.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 14 ++++++++++++++\n 1 file changed, 14 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex 15923460a78..c6095b37ede 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -11,8 +11,22 @@\n \n #include \"builtin.h\"\n #include \"gettext.h\"\n+#include \"parse-options.h\"\n #include \"urlmatch.h\"\n \n+static const char * const builtin_url_parse_usage[] = {\n+\tN_(\"git url-parse [<options>] [--] <url>...\"),\n+\tNULL\n+};\n+\n+static char *component_arg = NULL;\n+\n+static struct option builtin_url_parse_options[] = {\n+\tOPT_STRING('c', \"component\", &component_arg, \"<component>\", \\\n+\t\tN_(\"which URL component to extract\")),\n+\tOPT_END(),\n+};\n+\n enum url_component {\n \tURL_NONE = 0,\n \tURL_PROTOCOL,\n-- \ngitgitgadget\n\n"},{"id":"493569","messageId":"5de00324fb7011549da066068a4d102b9bfcddd2.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 09/13] url-parse: parse options given on the command line","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:57Z","receivedAt":"2024-04-28T22:31:13Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nPrepare to handle input by parsing the command line options\nand removing them from the arguments vector.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex c6095b37ede..03030035b4f 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -101,5 +101,10 @@ static char *extract(enum url_component component, struct url_info *info)\n \n int cmd_url_parse(int argc, const char **argv, const char *prefix)\n {\n+\targc = parse_options(argc, argv, prefix,\n+\t\tbuiltin_url_parse_options,\n+\t\tbuiltin_url_parse_usage,\n+\t\t0);\n+\n \treturn 0;\n }\n-- \ngitgitgadget\n\n"},{"id":"493570","messageId":"15d355a43cbedc16f420066e50e02041c8e1626d.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 10/13] url-parse: validate all given git URLs","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:58Z","receivedAt":"2024-04-28T22:31:14Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nParse all the git URLs given as input on the command line.\nDie if an URL cannot be parsed.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 8 ++++++++\n 1 file changed, 8 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex 03030035b4f..ab996eadf38 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -101,10 +101,18 @@ static char *extract(enum url_component component, struct url_info *info)\n \n int cmd_url_parse(int argc, const char **argv, const char *prefix)\n {\n+\tstruct url_info info;\n+\tint i;\n+\n \targc = parse_options(argc, argv, prefix,\n \t\tbuiltin_url_parse_options,\n \t\tbuiltin_url_parse_usage,\n \t\t0);\n \n+\tfor (i = 0; i < argc; ++i) {\n+\t\tparse_or_die(argv[i], &info);\n+\t\tfree(info.url);\n+\t}\n+\n \treturn 0;\n }\n-- \ngitgitgadget\n\n"},{"id":"493572","messageId":"4e93509c80f944b2d10c6d62853cecb78aa6a464.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 11/13] url-parse: output URL components selected by user","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:30:59Z","receivedAt":"2024-04-28T22:31:15Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nParse the specified git URL component from each of the given git URLs\nand print them to standard output, one per line.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n builtin/url-parse.c | 14 ++++++++++++++\n 1 file changed, 14 insertions(+)\n\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nindex ab996eadf38..6c1a8676bad 100644\n--- a/builtin/url-parse.c\n+++ b/builtin/url-parse.c\n@@ -102,6 +102,8 @@ static char *extract(enum url_component component, struct url_info *info)\n int cmd_url_parse(int argc, const char **argv, const char *prefix)\n {\n \tstruct url_info info;\n+\tenum url_component selected = URL_NONE;\n+\tchar *extracted;\n \tint i;\n \n \targc = parse_options(argc, argv, prefix,\n@@ -109,8 +111,20 @@ int cmd_url_parse(int argc, const char **argv, const char *prefix)\n \t\tbuiltin_url_parse_usage,\n \t\t0);\n \n+\tif (component_arg)\n+\t\tselected = get_component_or_die(component_arg);\n+\n \tfor (i = 0; i < argc; ++i) {\n \t\tparse_or_die(argv[i], &info);\n+\n+\t\tif (selected != URL_NONE) {\n+\t\t\textracted = extract(selected, &info);\n+\t\t\tif (extracted) {\n+\t\t\t\tputs(extracted);\n+\t\t\t\tfree(extracted);\n+\t\t\t}\n+\t\t}\n+\n \t\tfree(info.url);\n \t}\n \n-- \ngitgitgadget\n\n"},{"id":"493571","messageId":"abda074aeef2ffb20d2156b5f24f47745b6f3134.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 12/13] Documentation: describe the url-parse builtin","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:31:00Z","receivedAt":"2024-04-28T22:31:16Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nThe new url-parse builtin validates git URLs\nand optionally extracts their components.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n Documentation/git-url-parse.txt | 59 +++++++++++++++++++++++++++++++++\n 1 file changed, 59 insertions(+)\n create mode 100644 Documentation/git-url-parse.txt\n\ndiff --git a/Documentation/git-url-parse.txt b/Documentation/git-url-parse.txt\nnew file mode 100644\nindex 00000000000..bfbbad6c033\n--- /dev/null\n+++ b/Documentation/git-url-parse.txt\n@@ -0,0 +1,59 @@\n+git-url-parse(1)\n+================\n+\n+NAME\n+----\n+git-url-parse - Parse and extract git URL components\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git url-parse' [<options>] [--] <url>...\n+\n+DESCRIPTION\n+-----------\n+\n+Git supports many ways to specify URLs, some of them non-standard.\n+For example, git supports the scp style [user@]host:[path] format.\n+This command eases interoperability with git URLs by enabling the\n+parsing and extraction of the components of all git URLs.\n+\n+OPTIONS\n+-------\n+\n+-c <arg>::\n+--component <arg>::\n+\tExtract the `<arg>` component from the given git URLs.\n+\t`<arg>` can be one of:\n+\t`protocol`, `user`, `password`, `host`, `port`, `path`.\n+\n+EXAMPLES\n+--------\n+\n+* Print the host name:\n++\n+------------\n+$ git url-parse --component host https://example.com/user/repo\n+example.com\n+------------\n+\n+* Print the path:\n++\n+------------\n+$ git url-parse --component path https://example.com/user/repo\n+/usr/repo\n+$ git url-parse --component path example.com:~user/repo\n+~user/repo\n+$ git url-parse --component path example.com:user/repo\n+/user/repo\n+------------\n+\n+* Validate URLs without outputting anything:\n++\n+------------\n+$ git url-parse https://example.com/user/repo example.com:~user/repo\n+------------\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\n-- \ngitgitgadget\n\n"},{"id":"493573","messageId":"33e128496b3d250b45a27490fa45d40cb659c3b2.1714343461.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH 13/13] tests: add tests for the new url-parse builtin","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-04-28T22:31:01Z","receivedAt":"2024-04-28T22:31:17Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nTest git URL parsing, validation and component extraction\non all documented git URL schemes and syntaxes.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n t/t9904-url-parse.sh | 194 +++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 194 insertions(+)\n create mode 100755 t/t9904-url-parse.sh\n\ndiff --git a/t/t9904-url-parse.sh b/t/t9904-url-parse.sh\nnew file mode 100755\nindex 00000000000..f147f00591c\n--- /dev/null\n+++ b/t/t9904-url-parse.sh\n@@ -0,0 +1,194 @@\n+#!/bin/sh\n+#\n+# Copyright © 2024 Matheus Afonso Martins Moreira\n+#\n+\n+test_description='git url-parse tests'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git url-parse -- ssh syntax' '\n+\tgit url-parse \"ssh://user@example.com:1234/repository/path\" &&\n+\tgit url-parse \"ssh://user@example.com/repository/path\" &&\n+\tgit url-parse \"ssh://example.com:1234/repository/path\" &&\n+\tgit url-parse \"ssh://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- git syntax' '\n+\tgit url-parse \"git://example.com:1234/repository/path\" &&\n+\tgit url-parse \"git://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- http syntax' '\n+\tgit url-parse \"https://example.com:1234/repository/path\" &&\n+\tgit url-parse \"https://example.com/repository/path\" &&\n+\tgit url-parse \"http://example.com:1234/repository/path\" &&\n+\tgit url-parse \"http://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax' '\n+\tgit url-parse \"user@example.com:/repository/path\" &&\n+\tgit url-parse \"example.com:/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - ssh syntax' '\n+\tgit url-parse \"ssh://user@example.com:1234/~user/repository\" &&\n+\tgit url-parse \"ssh://user@example.com/~user/repository\" &&\n+\tgit url-parse \"ssh://example.com:1234/~user/repository\" &&\n+\tgit url-parse \"ssh://example.com/~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - git syntax' '\n+\tgit url-parse \"git://example.com:1234/~user/repository\" &&\n+\tgit url-parse \"git://example.com/~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - scp syntax' '\n+\tgit url-parse \"user@example.com:~user/repository\" &&\n+\tgit url-parse \"example.com:~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- file urls' '\n+\tgit url-parse \"file:///repository/path\" &&\n+\tgit url-parse \"file:///\" &&\n+\tgit url-parse \"file://\"\n+'\n+\n+test_expect_success 'git url-parse -c protocol -- ssh syntax' '\n+\ttest ssh = \"$(git url-parse -c protocol \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c protocol \"ssh://user@example.com/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c protocol \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c protocol \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c protocol -- git syntax' '\n+\ttest git = \"$(git url-parse -c protocol \"git://example.com:1234/repository/path\")\" &&\n+\ttest git = \"$(git url-parse -c protocol \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c protocol -- http syntax' '\n+\ttest https = \"$(git url-parse -c protocol \"https://example.com:1234/repository/path\")\" &&\n+\ttest https = \"$(git url-parse -c protocol \"https://example.com/repository/path\")\" &&\n+\ttest http = \"$(git url-parse -c protocol \"http://example.com:1234/repository/path\")\" &&\n+\ttest http = \"$(git url-parse -c protocol \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c protocol -- scp syntax' '\n+\ttest ssh = \"$(git url-parse -c protocol \"user@example.com:/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c protocol \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- ssh syntax' '\n+\ttest user = \"$(git url-parse -c user \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest user = \"$(git url-parse -c user \"ssh://user@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- git syntax' '\n+\ttest \"\" = \"$(git url-parse -c user \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- http syntax' '\n+\ttest \"\" = \"$(git url-parse -c user \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"https://example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- scp syntax' '\n+\ttest user = \"$(git url-parse -c user \"user@example.com:/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- ssh syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- git syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"git://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- http syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"https://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"https://example.com/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"http://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- scp syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"user@example.com:/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- ssh syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://user@example.com/repository/path\")\" &&\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- git syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- http syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"https://example.com/repository/path\")\" &&\n+\ttest 1234 = \"$(git url-parse -c port \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- scp syntax' '\n+\ttest \"\" = \"$(git url-parse -c port \"user@example.com:/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- ssh syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://user@example.com/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- git syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- http syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"https://example.com/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- scp syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"user@example.com:/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - ssh syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://user@example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://user@example.com/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - git syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com/~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - scp syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"user@example.com:~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"example.com:~user/repository\")\"\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"493709","messageId":"20240429205351.GA27257@tb-raspi4","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/13] builtin: implement, document and test url-parse","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2024-04-29T20:53:51Z","receivedAt":"2024-04-29T20:53:54Z","isPatch":true,"body":"On Sun, Apr 28, 2024 at 10:30:48PM +0000, Matheus Moreira via GitGitGadget wrote:\n> Git commands accept a wide variety of URLs syntaxes, not just standard URLs.\n> This can make parsing git URLs difficult since standard URL parsers cannot\n> be used. Even if an external parser were implemented, it would have to track\n> git's development closely in case support for any new URL schemes are added.\n>\n> These patches introduce a new url-parse builtin command that exposes git's\n> native URL parsing algorithms as a plumbing command, allowing other programs\n> to then call upon git itself to parse the git URLs and their components.\n>\n> This should be quite useful for scripts. For example, a script might want to\n> add remotes to repositories, naming them according to the domain name where\n> the repository is hosted. This new builtin allows it to parse the git URL\n> and extract its host name which can then be used as input for other\n> operations. This would be difficult to implement otherwise due to git's\n> support for scp style URLs.\n>\n\nAll in all, having a URL parser as such is a good thing, thanks for working\non that.\n\nThere are, however, some notes and questions, up for discussion:\n\n- are there any plans to integrate the parser into connect.c and fetch ?\n  Speaking as a person, who manage to break the parsing of URLs once,\n  with the good intention to improve things, I need to learn that\n  test cases are important.\n  Some work can be seen in t5601-clone.sh\n  Especially, when dealing with literal IPv6 addresses, the ones with []\n  and the simplified ssh syntax 'myhost:src' are interesting to test.\n  Git itself strives to be RFC compliant when parsing URLs, but\n  we do not fully guarantee to be \"fully certified\".\n  And some features using the [] syntax to embedd a port number\n  inside the simplified ssh syntax had not been documented,\n  but used in practise, and are now part of the test suite.\n  See \"[myhost:123]:src\" in t5601\n\n- Or is this new tool just a helper, to verify \"good\" URL's,\n  and not accepting our legacy parser quirks ?\n  Then we still should see some IPv6 tests ?\n  Or may be not, as we prefer hostnames these days ?\n\n- One minor comment:\n  in 02/13 we read:\n        +enum protocol {\n        +       PROTO_UNKNOWN = 0,\n        +       PROTO_LOCAL,\n        +       PROTO_FILE,\n        +       PROTO_SSH,\n        +       PROTO_GIT,\n  The RFC 1738 uses the term \"scheme\" here, and using the very generic\n  term \"protocol\" may lead to name clashes later.\n  Would something like \"git_scheme\" or so be better ?\n\n- One minor comment:\n   In 13/13 we read:\n        +       git url-parse \"file:///\" &&\n        +       git url-parse \"file://\"\n\n  I think that the \"///\" version is superflous, it should already\n  be covered by the \"//\" version\n\n"},{"id":"493711","messageId":"e7f49f373b2a3b51785d369e1f504825@gmail.com","threadId":"61375","inReplyTo":"20240429205351.GA27257@tb-raspi4","subject":"Reply to community feedback","fromName":"Matheus Afonso Martins Moreira","fromEmail":"matheus.a.m.moreira@gmail.com","sentAt":"2024-04-29T22:04:40Z","receivedAt":"2024-04-29T22:04:34Z","isPatch":false,"body":"Thank you for your feedback.\n\n> are there any plans to integrate the parser into connect.c and fetch ?\n\nYes.\n\nThat was my intention but I was not confident enough to touch connect.c\nbefore getting feedback from the community, since it's critical code\nand it is my first contribution.\n\nI do want to merge all URL parsing in git into this one function though,\nthereby creating a \"single point of truth\". This is so that if the algorithm\nis modified the changes are visible to the URL parser builtin as well.\n\n> Speaking as a person, who manage to break the parsing of URLs once,\n> with the good intention to improve things, I need to learn that\n> test cases are important.\n\nAbsolutely agree.\n\nWhen adding test cases, I looked at the possibilities enumerated in urls.txt\nand generated test cases based on those. I also looked at the urlmatch.h\ntest cases. However...\n\n> Some work can be seen in t5601-clone.sh\n\n... I did not think to check those.\n\n> Especially, when dealing with literal IPv6 addresses,\n> the ones with [] and the simplified ssh syntax 'myhost:src'\n> are interesting to test.\n\nYou're right about that. I shall prepare an updated v2 patchset\nwith more test cases, and also any other changes/improvements\nrequested by maintainers.\n\n> And some features using the [] syntax to embedd a port number\n> inside the simplified ssh syntax had not been documented,\n> but used in practise, and are now part of the test suite.\n> See \"[myhost:123]:src\" in t5601\n\nIndeed, I did not read anything of the sort when I checked it.\nWould you like me to commit a note to this effect to urls.txt ?\n\n> Or is this new tool just a helper, to verify \"good\" URL's,\n> and not accepting our legacy parser quirks ?\n\nIt is my intention that this builtin be able to accept, parse\nand decompose all types of URLs that git itself can accept.\n\n> Then we still should see some IPv6 tests ?\n\nI will add them!\n\n> Or may be not, as we prefer hostnames these days ?\n\nI would have to defer that choice to someone more experienced\nwith the codebase. Please advise on how to proceed.\n\n> The RFC 1738 uses the term \"scheme\" here, and using the very generic\n> term \"protocol\" may lead to name clashes later.\n> Would something like \"git_scheme\" or so be better ?\n\nScheme does seem like a better word if it's the terminology used by RFCs.\nI can change that in a new version if necessary.\nThat code is based on the existing connect.c parsing code though.\n\n> I think that the \"///\" version is superflous, it should already\n> be covered by the \"//\" version\n\nI thought it was a good idea because of existing precedent:\nmy first approach to creating the test cases was to copy the\nones from t0110-urlmatch-normalization.sh which did have many\ncases such as those. Then as I developed the code I came to\nbelieve that it was not necessary: I call url_normalize\nin the url_parse function and url_normalize is already being\ntested. I think I just forgot to delete those lines.\n\nReading that file over once again, it does have IPv6 address\ntest cases. So I should probably go over it again.\n\nThanks again for the feedback,\n\n  Matheus\n"},{"id":"493735","messageId":"20240430065142.GA1504@tb-raspi4","threadId":"61375","inReplyTo":"e7f49f373b2a3b51785d369e1f504825@gmail.com","subject":"Re: Reply to community feedback","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2024-04-30T06:51:42Z","receivedAt":"2024-04-30T06:51:51Z","isPatch":false,"body":"On Mon, Apr 29, 2024 at 07:04:40PM -0300, Matheus Afonso Martins Moreira wrote:\n\n> Thank you for your feedback.\n>\n> > are there any plans to integrate the parser into connect.c and fetch ?\n>\n> Yes.\n>\n> That was my intention but I was not confident enough to touch connect.c\n> before getting feedback from the community, since it's critical code\n> and it is my first contribution.\n\nWelcome to the Git community.\n\nI wasn't aware of t0110 as a test case...\n\n>\n> I do want to merge all URL parsing in git into this one function though,\n> thereby creating a \"single point of truth\". This is so that if the algorithm\n> is modified the changes are visible to the URL parser builtin as well.\n>\n\nThat is a good thing to do. Be prepared for a longer journey, since we have\nthis legacy stuff to deal with. But I am happy to help with reviews, even\nif that may take some days,\n\n[]\n\n> When adding test cases, I looked at the possibilities enumerated in urls.txt\n> and generated test cases based on those. I also looked at the urlmatch.h\n> test cases. However...\n>\n> > Some work can be seen in t5601-clone.sh\n>\n> ... I did not think to check those.\n>\n> > Especially, when dealing with literal IPv6 addresses,\n> > the ones with [] and the simplified ssh syntax 'myhost:src'\n> > are interesting to test.\n>\n> You're right about that. I shall prepare an updated v2 patchset\n> with more test cases, and also any other changes/improvements\n> requested by maintainers.\n>\n> > And some features using the [] syntax to embedd a port number\n> > inside the simplified ssh syntax had not been documented,\n> > but used in practise, and are now part of the test suite.\n> > See \"[myhost:123]:src\" in t5601\n>\n> Indeed, I did not read anything of the sort when I checked it.\n> Would you like me to commit a note to this effect to urls.txt ?\n\nOn short: please not.\nThis kind of syntax was never ment to be used.\nThe official \"ssh://myhost:123/src\" is recommended.\nWhen IPv6 parsing was added, people discovered that it could be\nused to \"protect\" the ':' from being a seperator between the hostname\nand the path, and can be used to seperate the hostname from the port.\nOnce that was used in real live, it was too late to change it.\nIf we now get a better debug tool, it could mention that this is\na legacy feature, and recommend the longer \"ssh://\" syntax.\n\n>\n> > Or is this new tool just a helper, to verify \"good\" URL's,\n> > and not accepting our legacy parser quirks ?\n>\n> It is my intention that this builtin be able to accept, parse\n> and decompose all types of URLs that git itself can accept.\n>\n> > Then we still should see some IPv6 tests ?\n>\n> I will add them!\n>\n> > Or may be not, as we prefer hostnames these days ?\n>\n> I would have to defer that choice to someone more experienced\n> with the codebase. Please advise on how to proceed.\n\nRe-reading this email conversation,\nI think that we should support (in the future),\nwhat we support today.\nHaving a new parser tool means, that there is a chance to reject\nthose URLs with the note/hint, that they are depracted, and should\nbe replaced by a proper one.\nFrom my point of view this means that all existing test case should pass\neven with the new parser, as a general approach.\nDeprecating things is hard, may take years, and may be done in a seperate\ntask/patch series. Or may be part of this one, in seperate commits.\n\n>\n> > The RFC 1738 uses the term \"scheme\" here, and using the very generic\n> > term \"protocol\" may lead to name clashes later.\n> > Would something like \"git_scheme\" or so be better ?\n>\n> Scheme does seem like a better word if it's the terminology used by RFCs.\n> I can change that in a new version if necessary.\n> That code is based on the existing connect.c parsing code though.\n>\n> > I think that the \"///\" version is superflous, it should already\n> > be covered by the \"//\" version\n>\n> I thought it was a good idea because of existing precedent:\n> my first approach to creating the test cases was to copy the\n> ones from t0110-urlmatch-normalization.sh which did have many\n> cases such as those. Then as I developed the code I came to\n> believe that it was not necessary: I call url_normalize\n> in the url_parse function and url_normalize is already being\n> tested. I think I just forgot to delete those lines.\n>\n> Reading that file over once again, it does have IPv6 address\n> test cases. So I should probably go over it again.\n>\n> Thanks again for the feedback,\n>\n>   Matheus\n>\n"},{"id":"493740","messageId":"55mpyq542lyaxc7zmgyxzsbugwhzy7p4j5plbmnrbapfuncved@6macxutkszws","threadId":"61375","inReplyTo":"abda074aeef2ffb20d2156b5f24f47745b6f3134.1714343461.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 12/13] Documentation: describe the url-parse builtin","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-04-30T07:37:36Z","receivedAt":"2024-04-30T07:37:41Z","isPatch":true,"body":"On Sun, 28 Apr 2024, Matheus Afonso Martins Moreira via GitGitGadget <gitgitgadget@gmail.com> wrote:\n> +* Print the path:\n> ++\n> +------------\n> +$ git url-parse --component path https://example.com/user/repo\n> +/usr/repo\n\ns/usr/user/\n\nThanks.\n"},{"id":"493849","messageId":"rt77kwzaia3w52o3hoeodjilk5gkh6dz5dip6wmn5hia55gp2r@5fznbu2ljdqy","threadId":"61375","inReplyTo":"13b81b8aa06cfd63a5fd9d1acbaf21a8b388ff47.1714343461.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 02/13] urlmatch: define url_parse function","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-05-01T22:18:33Z","receivedAt":"2024-05-01T22:18:36Z","isPatch":true,"body":"On Sun, 28 Apr 2024, Matheus Afonso Martins Moreira via GitGitGadget <gitgitgadget@gmail.com> wrote:\n> From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n> \n> Define general parsing function that supports all Git URLs\n> including scp style URLs such as hostname:~user/repo.\n> Has the same interface as the URL normalization function\n> and uses the same data structures, facilitating its use.\n> It's adapted from the algorithm used to process URLs in connect.c,\n> so it should support the same inputs.\n> \n> Signed-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n> ---\n>  urlmatch.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n>  urlmatch.h |  1 +\n>  2 files changed, 91 insertions(+)\n> \n> diff --git a/urlmatch.c b/urlmatch.c\n> index 1d0254abacb..5a442e31fa2 100644\n> --- a/urlmatch.c\n> +++ b/urlmatch.c\n> @@ -3,6 +3,7 @@\n>  #include \"hex-ll.h\"\n>  #include \"strbuf.h\"\n>  #include \"urlmatch.h\"\n> +#include \"url.h\"\n>  \n>  #define URL_ALPHA \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n>  #define URL_DIGIT \"0123456789\"\n> @@ -438,6 +439,95 @@ char *url_normalize(const char *url, struct url_info *out_info)\n>  \treturn url_normalize_1(url, out_info, 0);\n>  }\n>  \n> +enum protocol {\n> +\tPROTO_UNKNOWN = 0,\n> +\tPROTO_LOCAL,\n> +\tPROTO_FILE,\n> +\tPROTO_SSH,\n> +\tPROTO_GIT,\n> +};\n> +\n> +static enum protocol url_get_protocol(const char *name, size_t n)\n> +{\n> +\tif (!strncmp(name, \"ssh\", n))\n> +\t\treturn PROTO_SSH;\n> +\tif (!strncmp(name, \"git\", n))\n> +\t\treturn PROTO_GIT;\n> +\tif (!strncmp(name, \"git+ssh\", n)) /* deprecated - do not use */\n> +\t\treturn PROTO_SSH;\n> +\tif (!strncmp(name, \"ssh+git\", n)) /* deprecated - do not use */\n> +\t\treturn PROTO_SSH;\n> +\tif (!strncmp(name, \"file\", n))\n> +\t\treturn PROTO_FILE;\n> +\treturn PROTO_UNKNOWN;\n> +}\n> +\n> +char *url_parse(const char *url_orig, struct url_info *out_info)\n> +{\n> +\tstruct strbuf url;\n> +\tchar *host, *separator;\n> +\tchar *detached, *normalized;\n> +\tenum protocol protocol = PROTO_LOCAL;\n> +\tstruct url_info local_info;\n> +\tstruct url_info *info = out_info? out_info : &local_info;\n> +\tbool scp_syntax = false;\n> +\n> +\tif (is_url(url_orig)) {\n> +\t\turl_orig = url_decode(url_orig);\n> +\t} else {\n> +\t\turl_orig = xstrdup(url_orig);\n> +\t}\n> +\n> +\tstrbuf_init(&url, strlen(url_orig) + sizeof(\"ssh://\"));\n> +\tstrbuf_addstr(&url, url_orig);\n> +\n> +\thost = strstr(url.buf, \"://\");\n> +\tif (host) {\n> +\t\tprotocol = url_get_protocol(url.buf, host - url.buf);\n> +\t\thost += 3;\n> +\t} else {\n> +\t\tif (!url_is_local_not_ssh(url.buf)) {\n> +\t\t\tscp_syntax = true;\n> +\t\t\tprotocol = PROTO_SSH;\n> +\t\t\tstrbuf_insertstr(&url, 0, \"ssh://\");\n> +\t\t\thost = url.buf + 6;\n> +\t\t}\n> +\t}\nInteresting. \n\n    `\n    $ ./git url-parse -c protocol file:/test/test\n    ssh\n    `\n\nseems like only having a single slash after the 'protocol:' prints\n'ssh' always (I think this may not even be a valid url). After this 'else'\nblock, the url turns into 'ssh://file/test/test'. Will examine the details\nlater. Not that it's your code's doing, and rather the result of\nurl_is_local_not_ssh(). But just wanted to point this out and ask if this\nshould error out or is this an intended behavior that I can't figure out. \n\nThanks.\n"},{"id":"493857","messageId":"20240502040207.GA26134@tb-raspi4","threadId":"61375","inReplyTo":"rt77kwzaia3w52o3hoeodjilk5gkh6dz5dip6wmn5hia55gp2r@5fznbu2ljdqy","subject":"Re: [PATCH 02/13] urlmatch: define url_parse function","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2024-05-02T04:02:07Z","receivedAt":"2024-05-02T04:02:17Z","isPatch":true,"body":"[]\n> Interesting.\n>\n>     `\n>     $ ./git url-parse -c protocol file:/test/test\n>     ssh\n>     `\n>\n> seems like only having a single slash after the 'protocol:' prints\n> 'ssh' always (I think this may not even be a valid url). After this 'else'\n> block, the url turns into 'ssh://file/test/test'. Will examine the details\n> later. Not that it's your code's doing, and rather the result of\n> url_is_local_not_ssh(). But just wanted to point this out and ask if this\n> should error out or is this an intended behavior that I can't figure out.\n\nssh is the correct answer, try something like\n\n`git clone localhost:/home/myself/project/git.git`\n\nIt is the scp syntax, supported by Git as well.\nFrom `man scp`\n\n    scp copies files between hosts on a network.\n    []\n    The source and target may be specified as a local pathname,\n    a remote host with optional path in the form\n    [user@]host:[path],\n    or a URI in the form scp://[user@]host[:port][/path].\n    Local file names can be made explicit using absolute or relative pathnames\n    to avoid scp treating file names containing ‘:’ as host specifiers.\n\nSo yes, they share similar problems\nwith the ':' that could mean different things when using the short form.\n\n\n"},{"id":"542561","messageId":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.git.git.1714343461.gitgitgadget@gmail.com","subject":"[PATCH v2 0/8] builtin: implement, document and test url-parse","fromName":"Matheus Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:02Z","receivedAt":"2026-05-01T23:15:14Z","isPatch":true,"body":"This series adds git url-parse, a plumbing builtin for inspecting git URLs.\nGit accepts a wider variety of URL forms than any standard parser handles.\nThe supported forms include RFC URLs, file:// URLs, scp-style\n[user@]host:path for SSH, and IPv6 in brackets. Tools wanting to reason\nabout them have historically had to reimplement git's parsing or shell out\nindirectly. With git url-parse, scripts can ask git directly: validate a\nURL, extract a component (scheme, user, host, port, path, password), or\nboth.\n\nThe series consists of eight commits.\n\nThe first four are preparatory. They rename enum protocol to enum url_scheme\nfor RFC alignment, move url_is_local_not_ssh and the scheme-detection\nroutines from connect.c to url.h/url.c, and stop url_get_scheme from dying\non unknown schemes so other parsers can handle unknowns gracefully.\n\nThe fifth commit defines the new parser, url_parse, in urlmatch.c. It is\nadapted from parse_connect_url and uses the same data structures as\nurl_normalize. The parser returns NULL on failure with err populated, and\nexposes URL components as offset/length pairs into the normalized URL\nbuffer.\n\nThe sixth commit adds the user-facing command, with a helpful error when the\ninput looks like a local path rather than a URL.\n\nThe last two commits are documentation (a manpage) and 53 tests covering URL\nform, scp form, IPv6 in URL and scp forms, bracket forms, username\nexpansion, query/fragment stripping, the local-path error, and\nvalidation-only mode.\n\nSeveral choices in this series are judgment calls. Happy to amend or follow\nup on any of them.\n\nThe component name is scheme, not protocol. RFC 1738/3986 calls them\nschemes. The series renames enum protocol to enum url_scheme internally, and\nthe user-facing component name follows the same direction. I considered\naccepting both as aliases but decided against the precedent for a new\ncommand. If you would rather see protocol, or both protocol and scheme, that\nis easy to change.\n\nLocal paths are deliberately not URLs. parse_connect_url accepts bare paths\nlike /abs/path or ./rel as URL_SCHEME_LOCAL. url_parse rejects them, since\nurl_normalize requires a scheme://host form, and silent conversion to\nfile:// has no good answer for relative or tilde forms. The builtin emits a\nhelpful error suggesting the explicit file:// form. If full git clone parity\nis preferred (bare paths accepted via auto-conversion or a new flag), that\ncould be added.\n\nAbsent and empty components are conflated in output. --component user\nhttp://host/ and --component user http://@host/ both produce empty lines.\nThe underlying struct url_info preserves the distinction: *_off == 0 vs\n*_off != 0 with *_len == 0. A future option can expose it without breaking\nchange. Can amend this patch set if necessary.\n\nChanges since v1:\n\n * Bug fix: ~user paths with a query string or fragment were leaking the ?\n   or # into the path output. The ~user-skip logic in url_parse previously\n   ran only for file://. It now runs for git/ssh/scp URLs as well, matching\n   what parse_connect_url does and what users expect.\n\n * Helpful error for local paths instead of the cryptic \"invalid URL scheme\n   name or missing '://' suffix\".\n\n * -c protocol renamed to -c scheme for consistency with the internal rename\n   and the RFC.\n\n * Documented the deliberate divergence from parse_connect_url (local paths\n   and unknown schemes) in the urlmatch commit message.\n\n * Doc and command-list polish: purehelpers category, asciidoc placeholder\n   convention, [synopsis] form.\n\n * Original micro commit style staged buildup of the builtin collapsed to a\n   single self-contained commit. The rest of the series is unchanged in\n   shape.\n\nMatheus Afonso Martins Moreira (8):\n  connect: rename enum protocol to url_scheme\n  url: move url_is_local_not_ssh to url.h\n  url: move scheme detection to URL header/source\n  url: return URL_SCHEME_UNKNOWN instead of dying\n  urlmatch: define url_parse function\n  builtin: create url-parse command\n  doc: describe the url-parse builtin\n  t9904: add tests for the new url-parse builtin\n\n .gitignore                              |   1 +\n Documentation/git-url-parse.adoc        |  80 ++++++\n Documentation/meson.build               |   1 +\n Makefile                                |   1 +\n builtin.h                               |   1 +\n builtin/url-parse.c                     | 132 ++++++++++\n command-list.txt                        |   1 +\n connect.c                               |  78 ++----\n connect.h                               |   1 -\n git.c                                   |   1 +\n meson.build                             |   1 +\n remote.c                                |   1 +\n t/meson.build                           |   1 +\n t/t9904-url-parse.sh                    | 319 ++++++++++++++++++++++++\n t/unit-tests/u-urlmatch-normalization.c |  45 ++++\n url.c                                   |  23 ++\n url.h                                   |  16 ++\n urlmatch.c                              | 127 ++++++++++\n urlmatch.h                              |   1 +\n 19 files changed, 777 insertions(+), 54 deletions(-)\n create mode 100644 Documentation/git-url-parse.adoc\n create mode 100644 builtin/url-parse.c\n create mode 100755 t/t9904-url-parse.sh\n\n\nbase-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1715%2Fmatheusmoreira%2Furl-parse-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1715/matheusmoreira/url-parse-v2\nPull-Request: https://github.com/git/git/pull/1715\n\nRange-diff vs v1:\n\n  -:  ---------- >  1:  38f797362d connect: rename enum protocol to url_scheme\n  1:  42eb0cbf68 !  2:  a4153e1d24 url: move helper function to URL header and source\n     @@ Metadata\n      Author: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n       ## Commit message ##\n     -    url: move helper function to URL header and source\n     +    url: move url_is_local_not_ssh to url.h\n      \n     -    It will be used in more places so it should be placed in url.h.\n     +    Move url_is_local_not_ssh from connect.c/connect.h\n     +    to url.c/url.h so that the new url_parse function\n     +    in urlmatch.c, and any future code that needs to\n     +    distinguish a local path from an scp style SSH URL,\n     +    can reuse the heuristic without depending on connect.c.\n     +\n     +    No behavior change.\n      \n          Signed-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n       ## connect.c ##\n     -@@ connect.c: enum protocol {\n     - \tPROTO_GIT\n     +@@ connect.c: enum url_scheme {\n     + \tURL_SCHEME_GIT\n       };\n       \n      -int url_is_local_not_ssh(const char *url)\n     @@ connect.c: enum protocol {\n      -\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n      -}\n      -\n     - static const char *prot_name(enum protocol protocol)\n     + static const char *url_scheme_name(enum url_scheme scheme)\n       {\n     - \tswitch (protocol) {\n     + \tswitch (scheme) {\n      \n       ## connect.h ##\n      @@ connect.h: int git_connection_is_socket(struct child_process *conn);\n     @@ url.h: char *url_decode_parameter_value(const char **query);\n       \n      +int url_is_local_not_ssh(const char *url);\n      +\n     - #endif /* URL_H */\n     + /*\n     +  * The set of unreserved characters as per STD66 (RFC3986) is\n     +  * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n  -:  ---------- >  3:  e584fb03f3 url: move scheme detection to URL header/source\n  -:  ---------- >  4:  7381704c38 url: return URL_SCHEME_UNKNOWN instead of dying\n  2:  13b81b8aa0 !  5:  89932a70f3 urlmatch: define url_parse function\n     @@ Metadata\n       ## Commit message ##\n          urlmatch: define url_parse function\n      \n     -    Define general parsing function that supports all Git URLs\n     +    Define url_parse, a general parsing function that supports all Git URLs\n          including scp style URLs such as hostname:~user/repo.\n     -    Has the same interface as the URL normalization function\n     -    and uses the same data structures, facilitating its use.\n     -    It's adapted from the algorithm used to process URLs in connect.c,\n     -    so it should support the same inputs.\n     +\n     +    It is adapted from the algorithm in connect.c's parse_connect_url\n     +    and reuses the shared enum url_scheme and url_get_scheme function\n     +    that previous commits made available in url.h. The new parser and\n     +    the connect path agree on scheme classification. url_parse has the\n     +    same interface as url_normalize and uses the same data structures.\n     +\n     +    Both functions accept the same URL forms with one deliberate\n     +    exception. Bare local paths such as \"/abs/path\", \"./rel\"\n     +    or \"repo\" are accepted by parse_connect_url as URL_SCHEME_LOCAL,\n     +    but rejected by url_parse because url_normalize requires a URL\n     +    with a scheme://host form. A consumer that wants to handle both\n     +    URLs and local paths needs to dispatch on url_is_local_not_ssh\n     +    before calling url_parse, just as the connect path does internally.\n     +\n     +    The duplication with parse_connect_url is intentional.\n     +    The two functions have different contracts:\n     +\n     +      - parse_connect_url\n     +\n     +        Calls die() on an unknown scheme\n     +        and returns NUL-terminated host/path\n     +        strings for the connect path\n     +\n     +      - url_parse\n     +\n     +        Returns NULL on failure while populating\n     +        out_info->err, and exposes components\n     +        as offset/length pairs into the normalized\n     +        URL buffer, matching url_normalize.\n     +\n     +    Reconciling both is possible, but not in the scope\n     +    of the current patch set.\n      \n          Signed-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n     + ## t/unit-tests/u-urlmatch-normalization.c ##\n     +@@ t/unit-tests/u-urlmatch-normalization.c: void test_urlmatch_normalization__equivalents(void)\n     + \tcompare_normalized_urls(\"https://@x.y/^/../abc\", \"httpS://@x.y:0443/abc\", 1);\n     + \tcompare_normalized_urls(\"https://@x.y/^/..\", \"httpS://@x.y:0443/\", 1);\n     + }\n     ++\n     ++static void check_parsed_path(const char *url, const char *expected_path)\n     ++{\n     ++\tstruct url_info info;\n     ++\tchar *parsed = url_parse(url, &info);\n     ++\tchar *path;\n     ++\n     ++\tcl_assert(parsed != NULL);\n     ++\tpath = xstrndup(parsed + info.path_off, info.path_len);\n     ++\tcl_assert_equal_s(path, expected_path);\n     ++\tfree(path);\n     ++\tfree(parsed);\n     ++}\n     ++\n     ++void test_urlmatch_normalization__parse_scp(void)\n     ++{\n     ++\tcheck_parsed_path(\"host:path\", \"/path\");\n     ++\tcheck_parsed_path(\"user@host:path\", \"/path\");\n     ++\tcheck_parsed_path(\"host:~user/repo\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"user@host:~user/repo\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"[host]:src\", \"/src\");\n     ++\tcheck_parsed_path(\"[host:123]:src\", \"/src\");\n     ++\tcheck_parsed_path(\"[::1]:repo\", \"/repo\");\n     ++\tcheck_parsed_path(\"user@[::1]:repo\", \"/repo\");\n     ++}\n     ++\n     ++void test_urlmatch_normalization__parse_url_form(void)\n     ++{\n     ++\tcheck_parsed_path(\"ssh://host/repo\", \"/repo\");\n     ++\tcheck_parsed_path(\"ssh://host/~user/repo\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"git://host:9418/repo\", \"/repo\");\n     ++\tcheck_parsed_path(\"git://host/~user/repo\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"ssh://[::1]:1234/repo\", \"/repo\");\n     ++\tcheck_parsed_path(\"http://[2001:db8::1]/repo\", \"/repo\");\n     ++}\n     ++\n     ++void test_urlmatch_normalization__parse_strips_query_and_fragment(void)\n     ++{\n     ++\tcheck_parsed_path(\"ssh://host/~user/repo?q\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"ssh://host/~user/repo#frag\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"git://host/~user/repo?q\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"user@host:~user/repo?q\", \"~user/repo\");\n     ++\tcheck_parsed_path(\"https://host/repo?q\", \"/repo\");\n     ++\tcheck_parsed_path(\"https://host/repo#frag\", \"/repo\");\n     ++}\n     +\n       ## urlmatch.c ##\n      @@\n       #include \"hex-ll.h\"\n     @@ urlmatch.c: char *url_normalize(const char *url, struct url_info *out_info)\n       \treturn url_normalize_1(url, out_info, 0);\n       }\n       \n     -+enum protocol {\n     -+\tPROTO_UNKNOWN = 0,\n     -+\tPROTO_LOCAL,\n     -+\tPROTO_FILE,\n     -+\tPROTO_SSH,\n     -+\tPROTO_GIT,\n     -+};\n     -+\n     -+static enum protocol url_get_protocol(const char *name, size_t n)\n     -+{\n     -+\tif (!strncmp(name, \"ssh\", n))\n     -+\t\treturn PROTO_SSH;\n     -+\tif (!strncmp(name, \"git\", n))\n     -+\t\treturn PROTO_GIT;\n     -+\tif (!strncmp(name, \"git+ssh\", n)) /* deprecated - do not use */\n     -+\t\treturn PROTO_SSH;\n     -+\tif (!strncmp(name, \"ssh+git\", n)) /* deprecated - do not use */\n     -+\t\treturn PROTO_SSH;\n     -+\tif (!strncmp(name, \"file\", n))\n     -+\t\treturn PROTO_FILE;\n     -+\treturn PROTO_UNKNOWN;\n     -+}\n     -+\n      +char *url_parse(const char *url_orig, struct url_info *out_info)\n      +{\n      +\tstruct strbuf url;\n      +\tchar *host, *separator;\n      +\tchar *detached, *normalized;\n     -+\tenum protocol protocol = PROTO_LOCAL;\n     ++\tchar *url_decoded;\n     ++\tenum url_scheme scheme = URL_SCHEME_LOCAL;\n      +\tstruct url_info local_info;\n     -+\tstruct url_info *info = out_info? out_info : &local_info;\n     ++\tstruct url_info *info = out_info ? out_info : &local_info;\n      +\tbool scp_syntax = false;\n      +\n     -+\tif (is_url(url_orig)) {\n     -+\t\turl_orig = url_decode(url_orig);\n     -+\t} else {\n     -+\t\turl_orig = xstrdup(url_orig);\n     -+\t}\n     ++\tif (is_url(url_orig))\n     ++\t\turl_decoded = url_decode(url_orig);\n     ++\telse\n     ++\t\turl_decoded = xstrdup(url_orig);\n      +\n     -+\tstrbuf_init(&url, strlen(url_orig) + sizeof(\"ssh://\"));\n     -+\tstrbuf_addstr(&url, url_orig);\n     ++\tstrbuf_init(&url, strlen(url_decoded) + sizeof(\"ssh://\"));\n     ++\tstrbuf_addstr(&url, url_decoded);\n     ++\tfree(url_decoded);\n      +\n      +\thost = strstr(url.buf, \"://\");\n      +\tif (host) {\n     -+\t\tprotocol = url_get_protocol(url.buf, host - url.buf);\n     ++\t\t/*\n     ++\t\t * Temporarily NUL-terminate the scheme name\n     ++\t\t * so we can pass it to url_get_scheme(),\n     ++\t\t * then restore the ':' so the buffer\n     ++\t\t * is intact for url_normalize() below.\n     ++\t\t */\n     ++\t\tchar saved = *host;\n     ++\t\t*host = '\\0';\n     ++\t\tscheme = url_get_scheme(url.buf);\n     ++\t\t*host = saved;\n      +\t\thost += 3;\n      +\t} else {\n      +\t\tif (!url_is_local_not_ssh(url.buf)) {\n      +\t\t\tscp_syntax = true;\n     -+\t\t\tprotocol = PROTO_SSH;\n     ++\t\t\tscheme = URL_SCHEME_SSH;\n      +\t\t\tstrbuf_insertstr(&url, 0, \"ssh://\");\n     -+\t\t\thost = url.buf + 6;\n     ++\t\t\thost = url.buf + strlen(\"ssh://\");\n      +\t\t}\n      +\t}\n      +\n     -+\t/* path starts after ':' in scp style SSH URLs */\n     ++\t/*\n     ++\t * Path starts after ':' in scp style SSH URLs.\n     ++\t *\n     ++\t * The host portion can begin with an optional \"user@\",\n     ++\t * and the host itself can be wrapped in '[' ']' brackets.\n     ++\t * The bracket form is git's legacy way of supporting:\n     ++\t *\n     ++\t *   - IPv6 literals: [::1]:repo\n     ++\t *   - host:port pairs in the short form: [myhost:123]:src\n     ++\t *   - Plain hostnames that happen to need bracketing: [host]:path\n     ++\t *\n     ++\t * Treat '[' followed by 0 or 1 inner colons as the host:port\n     ++\t * or plain hostname form and strip the brackets so url_normalize\n     ++\t * sees host[:port] natively. Two or more inner colons mark an\n     ++\t * IPv6 literal: keep the brackets for url_normalize to recognize.\n     ++\t *\n     ++\t * The scp path separator is the ':' that follows the host part,\n     ++\t * and we must skip over user@ and any '[...]' before searching.\n     ++\t */\n      +\tif (scp_syntax) {\n     -+\t\tseparator = strchr(host, ':');\n     ++\t\tchar *user_at;\n     ++\t\tchar *host_start;\n     ++\t\tchar *bracket_end;\n     ++\n     ++\t\tuser_at = strchr(host, '@');\n     ++\t\thost_start = user_at ? user_at + 1 : host;\n     ++\n     ++\t\tif (*host_start == '[') {\n     ++\t\t\tchar *p;\n     ++\t\t\tint inner_colons;\n     ++\n     ++\t\t\tbracket_end = strchr(host_start, ']');\n     ++\t\t\tinner_colons = 0;\n     ++\t\t\tfor (p = host_start + 1; bracket_end && p < bracket_end; p++)\n     ++\t\t\t\tif (*p == ':')\n     ++\t\t\t\t\tinner_colons++;\n     ++\n     ++\t\t\tif (bracket_end && inner_colons <= 1) {\n     ++\t\t\t\tsize_t close_off = bracket_end - url.buf;\n     ++\t\t\t\tsize_t open_off = host_start - url.buf;\n     ++\t\t\t\tstrbuf_remove(&url, close_off, 1);\n     ++\t\t\t\tstrbuf_remove(&url, open_off, 1);\n     ++\t\t\t\tseparator = url.buf + close_off - 1;\n     ++\t\t\t} else if (bracket_end) {\n     ++\t\t\t\tseparator = strchr(bracket_end + 1, ':');\n     ++\t\t\t} else {\n     ++\t\t\t\tseparator = strchr(host_start, ':');\n     ++\t\t\t}\n     ++\t\t} else {\n     ++\t\t\tseparator = strchr(host_start, ':');\n     ++\t\t}\n     ++\n      +\t\tif (separator) {\n      +\t\t\tif (separator[1] == '/')\n      +\t\t\t\tstrbuf_remove(&url, separator - url.buf, 1);\n     @@ urlmatch.c: char *url_normalize(const char *url, struct url_info *out_info)\n      +\tnormalized = url_normalize(detached, info);\n      +\tfree(detached);\n      +\n     -+\tif (!normalized) {\n     ++\tif (!normalized)\n      +\t\treturn NULL;\n     -+\t}\n      +\n     -+\t/* point path to ~ for URL's like this:\n     ++\t/*\n     ++\t * Point path to ~ for URLs like this:\n      +\t *\n      +\t *     ssh://host.xz/~user/repo\n      +\t *     git://host.xz/~user/repo\n      +\t *     host.xz:~user/repo\n     -+\t *\n      +\t */\n     -+\tif (protocol == PROTO_GIT || protocol == PROTO_SSH) {\n     -+\t\tif (normalized[info->path_off + 1] == '~')\n     ++\tif (scheme == URL_SCHEME_GIT || scheme == URL_SCHEME_SSH) {\n     ++\t\tif (normalized[info->path_off + 1] == '~') {\n      +\t\t\tinfo->path_off++;\n     ++\t\t\tinfo->path_len--;\n     ++\t\t}\n      +\t}\n      +\n      +\treturn normalized;\n  3:  e4781b36d5 !  6:  886a7d659e builtin: create url-parse command\n     @@ Commit message\n      \n          The url-parse builtin command is designed to solve this problem\n          by exposing git's native URL parsing facilities as a plumbing command.\n     -    Other programs can then call upon git itself to parse the git URLs and\n     -    extract their components. This should be quite useful for scripts.\n     +    Other programs can then call upon git itself to parse the git URLs\n     +    and extract their components. This should be quite useful for scripts.\n      \n          Signed-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n     @@ Makefile: BUILTIN_OBJS += builtin/update-ref.o\n       BUILTIN_OBJS += builtin/verify-pack.o\n      \n       ## builtin.h ##\n     -@@ builtin.h: int cmd_update_server_info(int argc, const char **argv, const char *prefix);\n     - int cmd_upload_archive(int argc, const char **argv, const char *prefix);\n     - int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix);\n     - int cmd_upload_pack(int argc, const char **argv, const char *prefix);\n     -+int cmd_url_parse(int argc, const char **argv, const char *prefix);\n     - int cmd_var(int argc, const char **argv, const char *prefix);\n     - int cmd_verify_commit(int argc, const char **argv, const char *prefix);\n     - int cmd_verify_tag(int argc, const char **argv, const char *prefix);\n     +@@ builtin.h: int cmd_update_server_info(int argc, const char **argv, const char *prefix, stru\n     + int cmd_upload_archive(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_upload_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n     ++int cmd_url_parse(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_var(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_verify_commit(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_verify_tag(int argc, const char **argv, const char *prefix, struct repository *repo);\n      \n       ## builtin/url-parse.c (new) ##\n      @@\n     -+/* SPDX-License-Identifier: GPL-2.0-only\n     -+ *\n     -+ * url-parse - parses git URLs and extracts their components\n     -+ *\n     -+ * Copyright © 2024 Matheus Afonso Martins Moreira\n     -+ *\n     -+ * This program is free software; you can redistribute it and/or modify\n     -+ * it under the terms of the GNU General Public License as published by\n     -+ * the Free Software Foundation; version 2.\n     -+ */\n     -+\n      +#include \"builtin.h\"\n      +#include \"gettext.h\"\n     ++#include \"parse-options.h\"\n     ++#include \"url.h\"\n     ++#include \"urlmatch.h\"\n     ++\n     ++static const char * const builtin_url_parse_usage[] = {\n     ++\tN_(\"git url-parse [-c <component>] [--] <url>...\"),\n     ++\tNULL\n     ++};\n     ++\n     ++static char *component_arg;\n     ++\n     ++static struct option builtin_url_parse_options[] = {\n     ++\tOPT_STRING('c', \"component\", &component_arg, N_(\"component\"),\n     ++\t\tN_(\"which URL component to extract\")),\n     ++\tOPT_END(),\n     ++};\n     ++\n     ++enum url_component {\n     ++\tURL_NONE = 0,\n     ++\tURL_SCHEME,\n     ++\tURL_USER,\n     ++\tURL_PASSWORD,\n     ++\tURL_HOST,\n     ++\tURL_PORT,\n     ++\tURL_PATH,\n     ++};\n     ++\n     ++static void parse_or_die(const char *url, struct url_info *info)\n     ++{\n     ++\tif (url_is_local_not_ssh(url)) {\n     ++\t\tif (*url == '/')\n     ++\t\t\tdie(\"'%s' is not a URL; if you meant a local \"\n     ++\t\t\t    \"repository, use 'file://%s'\", url, url);\n     ++\t\tdie(\"'%s' is not a URL; if you meant a local repository, \"\n     ++\t\t    \"use a 'file://' URL with an absolute path\", url);\n     ++\t}\n     ++\tif (!url_parse(url, info))\n     ++\t\tdie(\"invalid git URL '%s': %s\", url, info->err);\n     ++}\n     ++\n     ++static enum url_component get_component_or_die(const char *arg)\n     ++{\n     ++\tif (!strcmp(\"path\", arg))\n     ++\t\treturn URL_PATH;\n     ++\tif (!strcmp(\"host\", arg))\n     ++\t\treturn URL_HOST;\n     ++\tif (!strcmp(\"scheme\", arg))\n     ++\t\treturn URL_SCHEME;\n     ++\tif (!strcmp(\"user\", arg))\n     ++\t\treturn URL_USER;\n     ++\tif (!strcmp(\"password\", arg))\n     ++\t\treturn URL_PASSWORD;\n     ++\tif (!strcmp(\"port\", arg))\n     ++\t\treturn URL_PORT;\n     ++\tdie(\"invalid git URL component '%s'\", arg);\n     ++}\n     ++\n     ++static char *extract_component(enum url_component component,\n     ++\t\t\t       struct url_info *info)\n     ++{\n     ++\tsize_t offset, length;\n     ++\n     ++\tswitch (component) {\n     ++\tcase URL_SCHEME:\n     ++\t\toffset = 0;\n     ++\t\tlength = info->scheme_len;\n     ++\t\tbreak;\n     ++\tcase URL_USER:\n     ++\t\toffset = info->user_off;\n     ++\t\tlength = info->user_len;\n     ++\t\tbreak;\n     ++\tcase URL_PASSWORD:\n     ++\t\toffset = info->passwd_off;\n     ++\t\tlength = info->passwd_len;\n     ++\t\tbreak;\n     ++\tcase URL_HOST:\n     ++\t\toffset = info->host_off;\n     ++\t\tlength = info->host_len;\n     ++\t\tbreak;\n     ++\tcase URL_PORT:\n     ++\t\toffset = info->port_off;\n     ++\t\tlength = info->port_len;\n     ++\t\tbreak;\n     ++\tcase URL_PATH:\n     ++\t\toffset = info->path_off;\n     ++\t\tlength = info->path_len;\n     ++\t\tbreak;\n     ++\tcase URL_NONE:\n     ++\t\treturn NULL;\n     ++\t}\n     ++\n     ++\treturn xstrndup(info->url + offset, length);\n     ++}\n      +\n     -+int cmd_url_parse(int argc, const char **argv, const char *prefix)\n     ++int cmd_url_parse(int argc,\n     ++\t\t  const char **argv,\n     ++\t\t  const char *prefix,\n     ++\t\t  struct repository *repo UNUSED)\n      +{\n     ++\tstruct url_info info;\n     ++\tenum url_component selected = URL_NONE;\n     ++\tchar *extracted;\n     ++\tint i;\n     ++\n     ++\targc = parse_options(argc, argv, prefix, builtin_url_parse_options,\n     ++\t\t\t     builtin_url_parse_usage, 0);\n     ++\n     ++\tif (argc == 0)\n     ++\t\tusage_with_options(builtin_url_parse_usage,\n     ++\t\t\t\t   builtin_url_parse_options);\n     ++\n     ++\tif (component_arg)\n     ++\t\tselected = get_component_or_die(component_arg);\n     ++\n     ++\tfor (i = 0; i < argc; i++) {\n     ++\t\tparse_or_die(argv[i], &info);\n     ++\n     ++\t\tif (selected != URL_NONE) {\n     ++\t\t\textracted = extract_component(selected, &info);\n     ++\t\t\tif (extracted) {\n     ++\t\t\t\tputs(extracted);\n     ++\t\t\t\tfree(extracted);\n     ++\t\t\t}\n     ++\t\t}\n     ++\n     ++\t\tfree(info.url);\n     ++\t}\n     ++\n      +\treturn 0;\n      +}\n      \n     @@ command-list.txt: git-update-ref                          plumbingmanipulators\n       git-update-server-info                  synchingrepositories\n       git-upload-archive                      synchelpers\n       git-upload-pack                         synchelpers\n     -+git-url-parse                           plumbinginterrogators\n     ++git-url-parse                           purehelpers\n       git-var                                 plumbinginterrogators\n       git-verify-commit                       ancillaryinterrogators\n       git-verify-pack                         plumbinginterrogators\n     @@ git.c: static struct cmd_struct commands[] = {\n       \t{ \"upload-archive\", cmd_upload_archive, NO_PARSEOPT },\n       \t{ \"upload-archive--writer\", cmd_upload_archive_writer, NO_PARSEOPT },\n       \t{ \"upload-pack\", cmd_upload_pack },\n     -+\t{ \"url-parse\", cmd_url_parse, NO_PARSEOPT },\n     ++\t{ \"url-parse\", cmd_url_parse },\n       \t{ \"var\", cmd_var, RUN_SETUP_GENTLY | NO_PARSEOPT },\n       \t{ \"verify-commit\", cmd_verify_commit, RUN_SETUP },\n       \t{ \"verify-pack\", cmd_verify_pack },\n     +\n     + ## meson.build ##\n     +@@ meson.build: builtin_sources = [\n     +   'builtin/update-server-info.c',\n     +   'builtin/upload-archive.c',\n     +   'builtin/upload-pack.c',\n     ++  'builtin/url-parse.c',\n     +   'builtin/var.c',\n     +   'builtin/verify-commit.c',\n     +   'builtin/verify-pack.c',\n  4:  1e0895651c <  -:  ---------- url-parse: add URL parsing helper function\n  5:  0bf83ee122 <  -:  ---------- url-parse: enumerate possible URL components\n  6:  149c476b1e <  -:  ---------- url-parse: define component extraction helper fn\n  7:  eb9ef8a17b <  -:  ---------- url-parse: define string to component converter fn\n  8:  a2acfdbc76 <  -:  ---------- url-parse: define usage and options\n  9:  5de00324fb <  -:  ---------- url-parse: parse options given on the command line\n 10:  15d355a43c <  -:  ---------- url-parse: validate all given git URLs\n 11:  4e93509c80 <  -:  ---------- url-parse: output URL components selected by user\n 12:  abda074aee !  7:  3c44e0f478 Documentation: describe the url-parse builtin\n     @@ Metadata\n      Author: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n       ## Commit message ##\n     -    Documentation: describe the url-parse builtin\n     +    doc: describe the url-parse builtin\n      \n          The new url-parse builtin validates git URLs\n          and optionally extracts their components.\n      \n     +    Helped-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n          Signed-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n     - ## Documentation/git-url-parse.txt (new) ##\n     + ## Documentation/git-url-parse.adoc (new) ##\n      @@\n      +git-url-parse(1)\n      +================\n     @@ Documentation/git-url-parse.txt (new)\n      +\n      +SYNOPSIS\n      +--------\n     -+[verse]\n     -+'git url-parse' [<options>] [--] <url>...\n     ++[synopsis]\n     ++git url-parse [-c <component>] [--] <url>...\n      +\n      +DESCRIPTION\n      +-----------\n     @@ Documentation/git-url-parse.txt (new)\n      +This command eases interoperability with git URLs by enabling the\n      +parsing and extraction of the components of all git URLs.\n      +\n     ++Any syntactically valid URL is parsed, even if the scheme is not one\n     ++git supports for fetching or pushing.\n     ++\n      +OPTIONS\n      +-------\n      +\n     -+-c <arg>::\n     -+--component <arg>::\n     -+\tExtract the `<arg>` component from the given git URLs.\n     -+\t`<arg>` can be one of:\n     -+\t`protocol`, `user`, `password`, `host`, `port`, `path`.\n     ++`-c <component>`::\n     ++`--component <component>`::\n     ++\tExtract the _<component>_ component from the given Git URLs.\n     ++\t_<component>_ can be one of:\n     ++\t`scheme`, `user`, `password`, `host`, `port`, `path`.\n     ++\n     ++OUTPUT\n     ++------\n     ++\n     ++When `--component` is given, the requested component of each URL\n     ++is printed on its own line, in the order the URLs were given. If\n     ++the URL has no such component (for example, a port in a URL that\n     ++does not specify one), an empty line is printed in its place.\n     ++\n     ++When `--component` is not given, no output is produced. The exit\n     ++status is zero if every URL parses successfully and non-zero\n     ++otherwise, allowing the command to be used purely as a validator.\n      +\n      +EXAMPLES\n      +--------\n     @@ Documentation/git-url-parse.txt (new)\n      ++\n      +------------\n      +$ git url-parse --component path https://example.com/user/repo\n     -+/usr/repo\n     ++/user/repo\n      +$ git url-parse --component path example.com:~user/repo\n      +~user/repo\n      +$ git url-parse --component path example.com:user/repo\n     @@ Documentation/git-url-parse.txt (new)\n      +$ git url-parse https://example.com/user/repo example.com:~user/repo\n      +------------\n      +\n     ++SEE ALSO\n     ++--------\n     ++linkgit:git-clone[1],\n     ++linkgit:git-fetch[1],\n     ++linkgit:git-config[1]\n     ++\n      +GIT\n      +---\n      +Part of the linkgit:git[1] suite\n     +\n     + ## Documentation/meson.build ##\n     +@@ Documentation/meson.build: manpages = {\n     +   'git-update-server-info.adoc' : 1,\n     +   'git-upload-archive.adoc' : 1,\n     +   'git-upload-pack.adoc' : 1,\n     ++  'git-url-parse.adoc' : 1,\n     +   'git-var.adoc' : 1,\n     +   'git-verify-commit.adoc' : 1,\n     +   'git-verify-pack.adoc' : 1,\n 13:  33e128496b !  8:  cf2ae409e6 tests: add tests for the new url-parse builtin\n     @@ Metadata\n      Author: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n       ## Commit message ##\n     -    tests: add tests for the new url-parse builtin\n     +    t9904: add tests for the new url-parse builtin\n      \n          Test git URL parsing, validation and component extraction\n          on all documented git URL schemes and syntaxes.\n      \n     +    Add IPv6 host coverage in URL form:\n     +\n     +        ssh://[::1]/path\n     +        ssh://user@[::1]:1234/path\n     +        git://[::1]:9418/path\n     +        http://[2001:db8::1]/path\n     +        https://[2001:db8::1]/path\n     +\n     +    In URL form the brackets are kept in the host component (RFC 3986\n     +    syntax for IPv6 literals).\n     +\n     +    Also exercise the bracketed scp short forms that t5601-clone.sh\n     +    covers via parse_connect_url:\n     +\n     +        [host]:path\n     +        [host:port]:path\n     +        [::1]:repo\n     +        user@[::1]:repo\n     +        user@[host:port]:path\n     +\n     +    In scp form, brackets are kept for IPv6 literals (two or more inner\n     +    colons) and stripped for plain hostnames or host:port pairs.\n     +\n     +    Suggested-by: Torsten Bögershausen <tboegi@web.de>\n          Signed-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n      \n     + ## t/meson.build ##\n     +@@ t/meson.build: integration_tests = [\n     +   't9901-git-web--browse.sh',\n     +   't9902-completion.sh',\n     +   't9903-bash-prompt.sh',\n     ++  't9904-url-parse.sh',\n     + ]\n     + \n     + benchmarks = [\n     +\n       ## t/t9904-url-parse.sh (new) ##\n      @@\n      +#!/bin/sh\n      +#\n     -+# Copyright © 2024 Matheus Afonso Martins Moreira\n     ++# Copyright (c) 2024 Matheus Afonso Martins Moreira\n      +#\n      +\n      +test_description='git url-parse tests'\n     @@ t/t9904-url-parse.sh (new)\n      +\n      +test_expect_success 'git url-parse -- file urls' '\n      +\tgit url-parse \"file:///repository/path\" &&\n     -+\tgit url-parse \"file:///\" &&\n      +\tgit url-parse \"file://\"\n      +'\n      +\n     -+test_expect_success 'git url-parse -c protocol -- ssh syntax' '\n     -+\ttest ssh = \"$(git url-parse -c protocol \"ssh://user@example.com:1234/repository/path\")\" &&\n     -+\ttest ssh = \"$(git url-parse -c protocol \"ssh://user@example.com/repository/path\")\" &&\n     -+\ttest ssh = \"$(git url-parse -c protocol \"ssh://example.com:1234/repository/path\")\" &&\n     -+\ttest ssh = \"$(git url-parse -c protocol \"ssh://example.com/repository/path\")\"\n     ++test_expect_success 'git url-parse -c scheme -- ssh syntax' '\n     ++\ttest ssh = \"$(git url-parse -c scheme \"ssh://user@example.com:1234/repository/path\")\" &&\n     ++\ttest ssh = \"$(git url-parse -c scheme \"ssh://user@example.com/repository/path\")\" &&\n     ++\ttest ssh = \"$(git url-parse -c scheme \"ssh://example.com:1234/repository/path\")\" &&\n     ++\ttest ssh = \"$(git url-parse -c scheme \"ssh://example.com/repository/path\")\"\n      +'\n      +\n     -+test_expect_success 'git url-parse -c protocol -- git syntax' '\n     -+\ttest git = \"$(git url-parse -c protocol \"git://example.com:1234/repository/path\")\" &&\n     -+\ttest git = \"$(git url-parse -c protocol \"git://example.com/repository/path\")\"\n     ++test_expect_success 'git url-parse -c scheme -- git syntax' '\n     ++\ttest git = \"$(git url-parse -c scheme \"git://example.com:1234/repository/path\")\" &&\n     ++\ttest git = \"$(git url-parse -c scheme \"git://example.com/repository/path\")\"\n      +'\n      +\n     -+test_expect_success 'git url-parse -c protocol -- http syntax' '\n     -+\ttest https = \"$(git url-parse -c protocol \"https://example.com:1234/repository/path\")\" &&\n     -+\ttest https = \"$(git url-parse -c protocol \"https://example.com/repository/path\")\" &&\n     -+\ttest http = \"$(git url-parse -c protocol \"http://example.com:1234/repository/path\")\" &&\n     -+\ttest http = \"$(git url-parse -c protocol \"http://example.com/repository/path\")\"\n     ++test_expect_success 'git url-parse -c scheme -- http syntax' '\n     ++\ttest https = \"$(git url-parse -c scheme \"https://example.com:1234/repository/path\")\" &&\n     ++\ttest https = \"$(git url-parse -c scheme \"https://example.com/repository/path\")\" &&\n     ++\ttest http = \"$(git url-parse -c scheme \"http://example.com:1234/repository/path\")\" &&\n     ++\ttest http = \"$(git url-parse -c scheme \"http://example.com/repository/path\")\"\n      +'\n      +\n     -+test_expect_success 'git url-parse -c protocol -- scp syntax' '\n     -+\ttest ssh = \"$(git url-parse -c protocol \"user@example.com:/repository/path\")\" &&\n     -+\ttest ssh = \"$(git url-parse -c protocol \"example.com:/repository/path\")\"\n     ++test_expect_success 'git url-parse -c scheme -- scp syntax' '\n     ++\ttest ssh = \"$(git url-parse -c scheme \"user@example.com:/repository/path\")\" &&\n     ++\ttest ssh = \"$(git url-parse -c scheme \"example.com:/repository/path\")\"\n      +'\n      +\n      +test_expect_success 'git url-parse -c user -- ssh syntax' '\n     @@ t/t9904-url-parse.sh (new)\n      +\ttest \"\" = \"$(git url-parse -c user \"example.com:/repository/path\")\"\n      +'\n      +\n     ++test_expect_success 'git url-parse -c password -- http syntax' '\n     ++\ttest secret = \"$(git url-parse -c password \"https://user:secret@example.com:1234/repository/path\")\" &&\n     ++\ttest secret = \"$(git url-parse -c password \"http://user:secret@example.com/repository/path\")\" &&\n     ++\ttest \"\" = \"$(git url-parse -c password \"https://user@example.com/repository/path\")\" &&\n     ++\ttest \"\" = \"$(git url-parse -c password \"https://example.com/repository/path\")\"\n     ++'\n     ++\n      +test_expect_success 'git url-parse -c host -- ssh syntax' '\n      +\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com:1234/repository/path\")\" &&\n      +\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com/repository/path\")\" &&\n     @@ t/t9904-url-parse.sh (new)\n      +\ttest \"~user/repository\" = \"$(git url-parse -c path \"example.com:~user/repository\")\"\n      +'\n      +\n     ++test_expect_success 'git url-parse -c path -- username expansion strips query and fragment' '\n     ++\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository?query\")\" &&\n     ++\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository#fragment\")\" &&\n     ++\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com/~user/repository?query\")\" &&\n     ++\ttest \"~user/repository\" = \"$(git url-parse -c path \"user@example.com:~user/repository?query\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- ssh syntax with IPv6' '\n     ++\tgit url-parse \"ssh://user@[::1]:1234/repository/path\" &&\n     ++\tgit url-parse \"ssh://user@[::1]/repository/path\" &&\n     ++\tgit url-parse \"ssh://[::1]:1234/repository/path\" &&\n     ++\tgit url-parse \"ssh://[::1]/repository/path\" &&\n     ++\tgit url-parse \"ssh://[2001:db8::1]/repository/path\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- git syntax with IPv6' '\n     ++\tgit url-parse \"git://[::1]:9418/repository/path\" &&\n     ++\tgit url-parse \"git://[::1]/repository/path\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- http syntax with IPv6' '\n     ++\tgit url-parse \"https://[::1]:1234/repository/path\" &&\n     ++\tgit url-parse \"https://[::1]/repository/path\" &&\n     ++\tgit url-parse \"http://[2001:db8::1]/repository/path\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c host -- IPv6 in URL form' '\n     ++\ttest \"[::1]\" = \"$(git url-parse -c host \"ssh://user@[::1]:1234/repository/path\")\" &&\n     ++\ttest \"[::1]\" = \"$(git url-parse -c host \"ssh://[::1]/repository/path\")\" &&\n     ++\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"ssh://[2001:db8::1]/repository/path\")\" &&\n     ++\ttest \"[::1]\" = \"$(git url-parse -c host \"git://[::1]/repository/path\")\" &&\n     ++\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"https://[2001:db8::1]/repository/path\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c port -- IPv6 in URL form' '\n     ++\ttest 1234 = \"$(git url-parse -c port \"ssh://user@[::1]:1234/repository/path\")\" &&\n     ++\ttest \"\" = \"$(git url-parse -c port \"ssh://[::1]/repository/path\")\" &&\n     ++\ttest 9418 = \"$(git url-parse -c port \"git://[::1]:9418/repository/path\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- scp syntax with IPv6' '\n     ++\tgit url-parse \"[::1]:repository/path\" &&\n     ++\tgit url-parse \"user@[::1]:repository/path\" &&\n     ++\tgit url-parse \"[2001:db8::1]:repo\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- scp syntax with bracketed hostname' '\n     ++\tgit url-parse \"[myhost]:src\" &&\n     ++\tgit url-parse \"user@[myhost]:src\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- scp syntax with bracketed host:port' '\n     ++\tgit url-parse \"[myhost:123]:src\" &&\n     ++\tgit url-parse \"user@[myhost:123]:src\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c host -- scp+IPv6' '\n     ++\ttest \"[::1]\" = \"$(git url-parse -c host \"[::1]:repository/path\")\" &&\n     ++\ttest \"[::1]\" = \"$(git url-parse -c host \"user@[::1]:repository/path\")\" &&\n     ++\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"[2001:db8::1]:repo\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c path -- scp+IPv6' '\n     ++\ttest \"/repository/path\" = \"$(git url-parse -c path \"[::1]:/repository/path\")\" &&\n     ++\ttest \"/repository/path\" = \"$(git url-parse -c path \"[::1]:repository/path\")\" &&\n     ++\ttest \"/repo\" = \"$(git url-parse -c path \"[2001:db8::1]:repo\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c host,port,path -- scp [host:port]:src' '\n     ++\ttest myhost = \"$(git url-parse -c host \"[myhost:123]:src\")\" &&\n     ++\ttest 123 = \"$(git url-parse -c port \"[myhost:123]:src\")\" &&\n     ++\ttest \"/src\" = \"$(git url-parse -c path \"[myhost:123]:src\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c host,path -- scp [host]:src' '\n     ++\ttest myhost = \"$(git url-parse -c host \"[myhost]:src\")\" &&\n     ++\ttest \"/src\" = \"$(git url-parse -c path \"[myhost]:src\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -c user -- scp with user@ and brackets' '\n     ++\ttest user = \"$(git url-parse -c user \"user@[::1]:repo\")\" &&\n     ++\ttest user = \"$(git url-parse -c user \"user@[myhost:123]:src\")\" &&\n     ++\ttest user = \"$(git url-parse -c user \"user@[myhost]:src\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse -- scp+IPv6 with username expansion' '\n     ++\ttest \"~user/repo\" = \"$(git url-parse -c path \"[::1]:~user/repo\")\" &&\n     ++\ttest \"~user/repo\" = \"$(git url-parse -c path \"user@[::1]:~user/repo\")\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse fails on invalid URL' '\n     ++\ttest_must_fail git url-parse \"not a url\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse helpful error for absolute local path' '\n     ++\ttest_must_fail git url-parse \"/abs/path\" 2>err &&\n     ++\ttest_grep \"is not a URL\" err &&\n     ++\ttest_grep \"file:///abs/path\" err\n     ++'\n     ++\n     ++test_expect_success 'git url-parse helpful error for relative local path' '\n     ++\ttest_must_fail git url-parse \"./rel\" 2>err &&\n     ++\ttest_grep \"is not a URL\" err &&\n     ++\ttest_grep \"absolute path\" err\n     ++'\n     ++\n     ++test_expect_success 'git url-parse fails on unknown -c component name' '\n     ++\ttest_must_fail git url-parse -c bogus \"https://example.com/repo\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse fails on URL missing host' '\n     ++\ttest_must_fail git url-parse \"https://\"\n     ++'\n     ++\n     ++test_expect_success 'git url-parse with no URL prints usage' '\n     ++\ttest_must_fail git url-parse 2>err &&\n     ++\ttest_grep \"usage:\" err\n     ++'\n     ++\n      +test_done\n\n-- \ngitgitgadget\n"},{"id":"542562","messageId":"38f797362d268a51b979efaa1d435d9f7a3378f6.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 1/8] connect: rename enum protocol to url_scheme","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:03Z","receivedAt":"2026-05-01T23:15:16Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nRFC 1738 names the part of a URL before the colon a \"scheme\".\nconnect.c calls it \"protocol\", which is more generic\nand collides with the unrelated enum protocol_version.\n\nRename:\n\n    enum protocol -> enum url_scheme\n    PROTO_*       -> URL_SCHEME_*\n    prot_name     -> url_scheme_name\n    get_protocol  -> url_get_scheme\n\nThe local variables in parse_connect_url and git_connect\nare renamed accordingly, from protocol to scheme.\n\nNo behavior change. The user-visible diagnostics\nand translated error messages are preserved:\n\n    \"Diag: protocol=...\"\n    \"protocol '%s' is not supported\"\n    \"unknown protocol\"\n\nThis rename also prepares for moving the scheme-detection functions\nto a shared header so that a future plumbing command can parse URLs\nusing the same logic as the connect path.\n\nSuggested-by: Torsten Bögershausen <tboegi@web.de>\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 68 +++++++++++++++++++++++++++----------------------------\n 1 file changed, 34 insertions(+), 34 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex fcd35c5539..46da89905e 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -700,11 +700,11 @@ int server_supports(const char *feature)\n \treturn !!server_feature_value(feature, NULL);\n }\n \n-enum protocol {\n-\tPROTO_LOCAL = 1,\n-\tPROTO_FILE,\n-\tPROTO_SSH,\n-\tPROTO_GIT\n+enum url_scheme {\n+\tURL_SCHEME_LOCAL = 1,\n+\tURL_SCHEME_FILE,\n+\tURL_SCHEME_SSH,\n+\tURL_SCHEME_GIT\n };\n \n int url_is_local_not_ssh(const char *url)\n@@ -715,33 +715,33 @@ int url_is_local_not_ssh(const char *url)\n \t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n }\n \n-static const char *prot_name(enum protocol protocol)\n+static const char *url_scheme_name(enum url_scheme scheme)\n {\n-\tswitch (protocol) {\n-\t\tcase PROTO_LOCAL:\n-\t\tcase PROTO_FILE:\n+\tswitch (scheme) {\n+\t\tcase URL_SCHEME_LOCAL:\n+\t\tcase URL_SCHEME_FILE:\n \t\t\treturn \"file\";\n-\t\tcase PROTO_SSH:\n+\t\tcase URL_SCHEME_SSH:\n \t\t\treturn \"ssh\";\n-\t\tcase PROTO_GIT:\n+\t\tcase URL_SCHEME_GIT:\n \t\t\treturn \"git\";\n \t\tdefault:\n \t\t\treturn \"unknown protocol\";\n \t}\n }\n \n-static enum protocol get_protocol(const char *name)\n+static enum url_scheme url_get_scheme(const char *name)\n {\n \tif (!strcmp(name, \"ssh\"))\n-\t\treturn PROTO_SSH;\n+\t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"git\"))\n-\t\treturn PROTO_GIT;\n+\t\treturn URL_SCHEME_GIT;\n \tif (!strcmp(name, \"git+ssh\")) /* deprecated - do not use */\n-\t\treturn PROTO_SSH;\n+\t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"ssh+git\")) /* deprecated - do not use */\n-\t\treturn PROTO_SSH;\n+\t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"file\"))\n-\t\treturn PROTO_FILE;\n+\t\treturn URL_SCHEME_FILE;\n \tdie(_(\"protocol '%s' is not supported\"), name);\n }\n \n@@ -1083,14 +1083,14 @@ static char *get_port(char *host)\n  * Extract protocol and relevant parts from the specified connection URL.\n  * The caller must free() the returned strings.\n  */\n-static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n-\t\t\t\t       char **ret_path)\n+static enum url_scheme parse_connect_url(const char *url_orig, char **ret_host,\n+\t\t\t\t\t char **ret_path)\n {\n \tchar *url;\n \tchar *host, *path;\n \tchar *end;\n \tint separator = '/';\n-\tenum protocol protocol = PROTO_LOCAL;\n+\tenum url_scheme scheme = URL_SCHEME_LOCAL;\n \n \tif (is_url(url_orig))\n \t\turl = url_decode(url_orig);\n@@ -1100,12 +1100,12 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \thost = strstr(url, \"://\");\n \tif (host) {\n \t\t*host = '\\0';\n-\t\tprotocol = get_protocol(url);\n+\t\tscheme = url_get_scheme(url);\n \t\thost += 3;\n \t} else {\n \t\thost = url;\n \t\tif (!url_is_local_not_ssh(url)) {\n-\t\t\tprotocol = PROTO_SSH;\n+\t\t\tscheme = URL_SCHEME_SSH;\n \t\t\tseparator = ':';\n \t\t}\n \t}\n@@ -1116,13 +1116,13 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \t */\n \tend = host_end(&host, 0);\n \n-\tif (protocol == PROTO_LOCAL)\n+\tif (scheme == URL_SCHEME_LOCAL)\n \t\tpath = end;\n-\telse if (protocol == PROTO_FILE && *host != '/' &&\n+\telse if (scheme == URL_SCHEME_FILE && *host != '/' &&\n \t\t !has_dos_drive_prefix(host) &&\n \t\t offset_1st_component(host - 2) > 1)\n \t\tpath = host - 2; /* include the leading \"//\" */\n-\telse if (protocol == PROTO_FILE && has_dos_drive_prefix(end))\n+\telse if (scheme == URL_SCHEME_FILE && has_dos_drive_prefix(end))\n \t\tpath = end; /* \"file://$(pwd)\" may be \"file://C:/projects/repo\" */\n \telse\n \t\tpath = strchr(end, separator);\n@@ -1138,7 +1138,7 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \tend = path; /* Need to \\0 terminate host here */\n \tif (separator == ':')\n \t\tpath++; /* path starts after ':' */\n-\tif (protocol == PROTO_GIT || protocol == PROTO_SSH) {\n+\tif (scheme == URL_SCHEME_GIT || scheme == URL_SCHEME_SSH) {\n \t\tif (path[1] == '~')\n \t\t\tpath++;\n \t}\n@@ -1149,7 +1149,7 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \t*ret_host = xstrdup(host);\n \t*ret_path = path;\n \tfree(url);\n-\treturn protocol;\n+\treturn scheme;\n }\n \n static const char *get_ssh_command(void)\n@@ -1434,7 +1434,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n-\tenum protocol protocol;\n+\tenum url_scheme scheme;\n \tenum protocol_version version = get_protocol_version_config();\n \n \t/*\n@@ -1451,14 +1451,14 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t */\n \tsignal(SIGCHLD, SIG_DFL);\n \n-\tprotocol = parse_connect_url(url, &hostandport, &path);\n-\tif ((flags & CONNECT_DIAG_URL) && (protocol != PROTO_SSH)) {\n+\tscheme = parse_connect_url(url, &hostandport, &path);\n+\tif ((flags & CONNECT_DIAG_URL) && (scheme != URL_SCHEME_SSH)) {\n \t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\n-\t\tprintf(\"Diag: protocol=%s\\n\", prot_name(protocol));\n+\t\tprintf(\"Diag: protocol=%s\\n\", url_scheme_name(scheme));\n \t\tprintf(\"Diag: hostandport=%s\\n\", hostandport ? hostandport : \"NULL\");\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n-\t} else if (protocol == PROTO_GIT) {\n+\t} else if (scheme == URL_SCHEME_GIT) {\n \t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n \t\tconn->trace2_child_class = \"transport/git\";\n \t} else {\n@@ -1481,7 +1481,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n \n \t\tconn->use_shell = 1;\n \t\tconn->in = conn->out = -1;\n-\t\tif (protocol == PROTO_SSH) {\n+\t\tif (scheme == URL_SCHEME_SSH) {\n \t\t\tchar *ssh_host = hostandport;\n \t\t\tconst char *port = NULL;\n \t\t\ttransport_check_allowed(\"ssh\");\n@@ -1492,7 +1492,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n \n \t\t\tif (flags & CONNECT_DIAG_URL) {\n \t\t\t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\n-\t\t\t\tprintf(\"Diag: protocol=%s\\n\", prot_name(protocol));\n+\t\t\t\tprintf(\"Diag: protocol=%s\\n\", url_scheme_name(scheme));\n \t\t\t\tprintf(\"Diag: userandhost=%s\\n\", ssh_host ? ssh_host : \"NULL\");\n \t\t\t\tprintf(\"Diag: port=%s\\n\", port ? port : \"NONE\");\n \t\t\t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n-- \ngitgitgadget\n\n"},{"id":"542563","messageId":"a4153e1d2470d42d17758df727fc267a7debbc3d.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 2/8] url: move url_is_local_not_ssh to url.h","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:04Z","receivedAt":"2026-05-01T23:15:17Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nMove url_is_local_not_ssh from connect.c/connect.h\nto url.c/url.h so that the new url_parse function\nin urlmatch.c, and any future code that needs to\ndistinguish a local path from an scp style SSH URL,\ncan reuse the heuristic without depending on connect.c.\n\nNo behavior change.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 8 --------\n connect.h | 1 -\n remote.c  | 1 +\n url.c     | 8 ++++++++\n url.h     | 2 ++\n 5 files changed, 11 insertions(+), 9 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex 46da89905e..cb145de30e 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -707,14 +707,6 @@ enum url_scheme {\n \tURL_SCHEME_GIT\n };\n \n-int url_is_local_not_ssh(const char *url)\n-{\n-\tconst char *colon = strchr(url, ':');\n-\tconst char *slash = strchr(url, '/');\n-\treturn !colon || (slash && slash < colon) ||\n-\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n-}\n-\n static const char *url_scheme_name(enum url_scheme scheme)\n {\n \tswitch (scheme) {\ndiff --git a/connect.h b/connect.h\nindex 1645126c17..8d84f6656b 100644\n--- a/connect.h\n+++ b/connect.h\n@@ -13,7 +13,6 @@ int git_connection_is_socket(struct child_process *conn);\n int server_supports(const char *feature);\n int parse_feature_request(const char *features, const char *feature);\n const char *server_feature_value(const char *feature, size_t *len_ret);\n-int url_is_local_not_ssh(const char *url);\n \n struct packet_reader;\n enum protocol_version discover_version(struct packet_reader *reader);\ndiff --git a/remote.c b/remote.c\nindex a664cd166a..24a8118d25 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -8,6 +8,7 @@\n #include \"gettext.h\"\n #include \"hex.h\"\n #include \"remote.h\"\n+#include \"url.h\"\n #include \"urlmatch.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\ndiff --git a/url.c b/url.c\nindex 3ca5987e90..057576042a 100644\n--- a/url.c\n+++ b/url.c\n@@ -132,3 +132,11 @@ void str_end_url_with_slash(const char *url, char **dest)\n \tfree(*dest);\n \t*dest = strbuf_detach(&buf, NULL);\n }\n+\n+int url_is_local_not_ssh(const char *url)\n+{\n+\tconst char *colon = strchr(url, ':');\n+\tconst char *slash = strchr(url, '/');\n+\treturn !colon || (slash && slash < colon) ||\n+\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n+}\ndiff --git a/url.h b/url.h\nindex cd9140e994..39d621312f 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,6 +21,8 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+int url_is_local_not_ssh(const char *url);\n+\n /*\n  * The set of unreserved characters as per STD66 (RFC3986) is\n  * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n-- \ngitgitgadget\n\n"},{"id":"542564","messageId":"e584fb03f3c23fda452b8e53d287f8f835becd48.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 3/8] url: move scheme detection to URL header/source","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:05Z","receivedAt":"2026-05-01T23:15:19Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nMove enum url_scheme and url_get_scheme()\nfrom connect.c to url.h and url.c\nso that other code can identify\na URL's scheme without depending\non connect.c.\n\nNo behavior change. url_get_scheme() still dies\non an unrecognized scheme name, with the same\ntranslated message as before.\n\nscheme_name() stays in connect.c\nbecause it has no other callers.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 22 ----------------------\n url.c     | 16 ++++++++++++++++\n url.h     | 13 +++++++++++++\n 3 files changed, 29 insertions(+), 22 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex cb145de30e..1ac7acc6e8 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -700,13 +700,6 @@ int server_supports(const char *feature)\n \treturn !!server_feature_value(feature, NULL);\n }\n \n-enum url_scheme {\n-\tURL_SCHEME_LOCAL = 1,\n-\tURL_SCHEME_FILE,\n-\tURL_SCHEME_SSH,\n-\tURL_SCHEME_GIT\n-};\n-\n static const char *url_scheme_name(enum url_scheme scheme)\n {\n \tswitch (scheme) {\n@@ -722,21 +715,6 @@ static const char *url_scheme_name(enum url_scheme scheme)\n \t}\n }\n \n-static enum url_scheme url_get_scheme(const char *name)\n-{\n-\tif (!strcmp(name, \"ssh\"))\n-\t\treturn URL_SCHEME_SSH;\n-\tif (!strcmp(name, \"git\"))\n-\t\treturn URL_SCHEME_GIT;\n-\tif (!strcmp(name, \"git+ssh\")) /* deprecated - do not use */\n-\t\treturn URL_SCHEME_SSH;\n-\tif (!strcmp(name, \"ssh+git\")) /* deprecated - do not use */\n-\t\treturn URL_SCHEME_SSH;\n-\tif (!strcmp(name, \"file\"))\n-\t\treturn URL_SCHEME_FILE;\n-\tdie(_(\"protocol '%s' is not supported\"), name);\n-}\n-\n static char *host_end(char **hoststart, int removebrackets)\n {\n \tchar *host = *hoststart;\ndiff --git a/url.c b/url.c\nindex 057576042a..300acf98fe 100644\n--- a/url.c\n+++ b/url.c\n@@ -1,4 +1,5 @@\n #include \"git-compat-util.h\"\n+#include \"gettext.h\"\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"url.h\"\n@@ -140,3 +141,18 @@ int url_is_local_not_ssh(const char *url)\n \treturn !colon || (slash && slash < colon) ||\n \t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n }\n+\n+enum url_scheme url_get_scheme(const char *name)\n+{\n+\tif (!strcmp(name, \"ssh\"))\n+\t\treturn URL_SCHEME_SSH;\n+\tif (!strcmp(name, \"git\"))\n+\t\treturn URL_SCHEME_GIT;\n+\tif (!strcmp(name, \"git+ssh\")) /* deprecated - do not use */\n+\t\treturn URL_SCHEME_SSH;\n+\tif (!strcmp(name, \"ssh+git\")) /* deprecated - do not use */\n+\t\treturn URL_SCHEME_SSH;\n+\tif (!strcmp(name, \"file\"))\n+\t\treturn URL_SCHEME_FILE;\n+\tdie(_(\"protocol '%s' is not supported\"), name);\n+}\ndiff --git a/url.h b/url.h\nindex 39d621312f..24c8cd91d0 100644\n--- a/url.h\n+++ b/url.h\n@@ -23,6 +23,19 @@ void str_end_url_with_slash(const char *url, char **dest);\n \n int url_is_local_not_ssh(const char *url);\n \n+enum url_scheme {\n+\tURL_SCHEME_LOCAL = 1,\n+\tURL_SCHEME_FILE,\n+\tURL_SCHEME_SSH,\n+\tURL_SCHEME_GIT,\n+};\n+\n+/*\n+ * Identify the URL scheme by name. Dies if the name does not match\n+ * any scheme that Git knows about.\n+ */\n+enum url_scheme url_get_scheme(const char *name);\n+\n /*\n  * The set of unreserved characters as per STD66 (RFC3986) is\n  * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n-- \ngitgitgadget\n\n"},{"id":"542565","messageId":"7381704c3809deec3adf1442a920f47840e0f104.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 4/8] url: return URL_SCHEME_UNKNOWN instead of dying","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:06Z","receivedAt":"2026-05-01T23:15:20Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nEnumerate a URL_SCHEME_UNKNOWN result with value 0.\nHave url_get_scheme() return it for unrecognized\nschemes instead of calling die() itself.\nMove the die() call to parse_connect_url()\nwhere url_get_scheme() is used.\n\nThis lets url_get_scheme() be used from contexts\nthat need to identify a URL's scheme without aborting\nthe program. For example, a future plumbing command\nthat validates URLs.\n\nNo external behavior change. parse_connect_url() still dies\nwith the same translated message for unrecognized schemes.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 2 ++\n url.c     | 3 +--\n url.h     | 7 ++++---\n 3 files changed, 7 insertions(+), 5 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex 1ac7acc6e8..73d7a6b8d0 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1071,6 +1071,8 @@ static enum url_scheme parse_connect_url(const char *url_orig, char **ret_host,\n \tif (host) {\n \t\t*host = '\\0';\n \t\tscheme = url_get_scheme(url);\n+\t\tif (scheme == URL_SCHEME_UNKNOWN)\n+\t\t\tdie(_(\"protocol '%s' is not supported\"), url);\n \t\thost += 3;\n \t} else {\n \t\thost = url;\ndiff --git a/url.c b/url.c\nindex 300acf98fe..a59818278f 100644\n--- a/url.c\n+++ b/url.c\n@@ -1,5 +1,4 @@\n #include \"git-compat-util.h\"\n-#include \"gettext.h\"\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"url.h\"\n@@ -154,5 +153,5 @@ enum url_scheme url_get_scheme(const char *name)\n \t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"file\"))\n \t\treturn URL_SCHEME_FILE;\n-\tdie(_(\"protocol '%s' is not supported\"), name);\n+\treturn URL_SCHEME_UNKNOWN;\n }\ndiff --git a/url.h b/url.h\nindex 24c8cd91d0..7289523605 100644\n--- a/url.h\n+++ b/url.h\n@@ -24,15 +24,16 @@ void str_end_url_with_slash(const char *url, char **dest);\n int url_is_local_not_ssh(const char *url);\n \n enum url_scheme {\n-\tURL_SCHEME_LOCAL = 1,\n+\tURL_SCHEME_UNKNOWN = 0,\n+\tURL_SCHEME_LOCAL,\n \tURL_SCHEME_FILE,\n \tURL_SCHEME_SSH,\n \tURL_SCHEME_GIT,\n };\n \n /*\n- * Identify the URL scheme by name. Dies if the name does not match\n- * any scheme that Git knows about.\n+ * Identify the URL scheme by name. Returns URL_SCHEME_UNKNOWN\n+ * if the name does not match any scheme that Git knows about.\n  */\n enum url_scheme url_get_scheme(const char *name);\n \n-- \ngitgitgadget\n\n"},{"id":"542566","messageId":"89932a70f3ace6ff1198628873df702f40f1442a.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 5/8] urlmatch: define url_parse function","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:07Z","receivedAt":"2026-05-01T23:15:22Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nDefine url_parse, a general parsing function that supports all Git URLs\nincluding scp style URLs such as hostname:~user/repo.\n\nIt is adapted from the algorithm in connect.c's parse_connect_url\nand reuses the shared enum url_scheme and url_get_scheme function\nthat previous commits made available in url.h. The new parser and\nthe connect path agree on scheme classification. url_parse has the\nsame interface as url_normalize and uses the same data structures.\n\nBoth functions accept the same URL forms with one deliberate\nexception. Bare local paths such as \"/abs/path\", \"./rel\"\nor \"repo\" are accepted by parse_connect_url as URL_SCHEME_LOCAL,\nbut rejected by url_parse because url_normalize requires a URL\nwith a scheme://host form. A consumer that wants to handle both\nURLs and local paths needs to dispatch on url_is_local_not_ssh\nbefore calling url_parse, just as the connect path does internally.\n\nThe duplication with parse_connect_url is intentional.\nThe two functions have different contracts:\n\n  - parse_connect_url\n\n    Calls die() on an unknown scheme\n    and returns NUL-terminated host/path\n    strings for the connect path\n\n  - url_parse\n\n    Returns NULL on failure while populating\n    out_info->err, and exposes components\n    as offset/length pairs into the normalized\n    URL buffer, matching url_normalize.\n\nReconciling both is possible, but not in the scope\nof the current patch set.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n t/unit-tests/u-urlmatch-normalization.c |  45 +++++++++\n urlmatch.c                              | 127 ++++++++++++++++++++++++\n urlmatch.h                              |   1 +\n 3 files changed, 173 insertions(+)\n\ndiff --git a/t/unit-tests/u-urlmatch-normalization.c b/t/unit-tests/u-urlmatch-normalization.c\nindex 39f6e1ba26..3595d893a2 100644\n--- a/t/unit-tests/u-urlmatch-normalization.c\n+++ b/t/unit-tests/u-urlmatch-normalization.c\n@@ -245,3 +245,48 @@ void test_urlmatch_normalization__equivalents(void)\n \tcompare_normalized_urls(\"https://@x.y/^/../abc\", \"httpS://@x.y:0443/abc\", 1);\n \tcompare_normalized_urls(\"https://@x.y/^/..\", \"httpS://@x.y:0443/\", 1);\n }\n+\n+static void check_parsed_path(const char *url, const char *expected_path)\n+{\n+\tstruct url_info info;\n+\tchar *parsed = url_parse(url, &info);\n+\tchar *path;\n+\n+\tcl_assert(parsed != NULL);\n+\tpath = xstrndup(parsed + info.path_off, info.path_len);\n+\tcl_assert_equal_s(path, expected_path);\n+\tfree(path);\n+\tfree(parsed);\n+}\n+\n+void test_urlmatch_normalization__parse_scp(void)\n+{\n+\tcheck_parsed_path(\"host:path\", \"/path\");\n+\tcheck_parsed_path(\"user@host:path\", \"/path\");\n+\tcheck_parsed_path(\"host:~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"user@host:~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"[host]:src\", \"/src\");\n+\tcheck_parsed_path(\"[host:123]:src\", \"/src\");\n+\tcheck_parsed_path(\"[::1]:repo\", \"/repo\");\n+\tcheck_parsed_path(\"user@[::1]:repo\", \"/repo\");\n+}\n+\n+void test_urlmatch_normalization__parse_url_form(void)\n+{\n+\tcheck_parsed_path(\"ssh://host/repo\", \"/repo\");\n+\tcheck_parsed_path(\"ssh://host/~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"git://host:9418/repo\", \"/repo\");\n+\tcheck_parsed_path(\"git://host/~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"ssh://[::1]:1234/repo\", \"/repo\");\n+\tcheck_parsed_path(\"http://[2001:db8::1]/repo\", \"/repo\");\n+}\n+\n+void test_urlmatch_normalization__parse_strips_query_and_fragment(void)\n+{\n+\tcheck_parsed_path(\"ssh://host/~user/repo?q\", \"~user/repo\");\n+\tcheck_parsed_path(\"ssh://host/~user/repo#frag\", \"~user/repo\");\n+\tcheck_parsed_path(\"git://host/~user/repo?q\", \"~user/repo\");\n+\tcheck_parsed_path(\"user@host:~user/repo?q\", \"~user/repo\");\n+\tcheck_parsed_path(\"https://host/repo?q\", \"/repo\");\n+\tcheck_parsed_path(\"https://host/repo#frag\", \"/repo\");\n+}\ndiff --git a/urlmatch.c b/urlmatch.c\nindex eea8300489..bf8cce6de9 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -5,6 +5,7 @@\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"urlmatch.h\"\n+#include \"url.h\"\n \n #define URL_ALPHA \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n #define URL_DIGIT \"0123456789\"\n@@ -440,6 +441,132 @@ char *url_normalize(const char *url, struct url_info *out_info)\n \treturn url_normalize_1(url, out_info, 0);\n }\n \n+char *url_parse(const char *url_orig, struct url_info *out_info)\n+{\n+\tstruct strbuf url;\n+\tchar *host, *separator;\n+\tchar *detached, *normalized;\n+\tchar *url_decoded;\n+\tenum url_scheme scheme = URL_SCHEME_LOCAL;\n+\tstruct url_info local_info;\n+\tstruct url_info *info = out_info ? out_info : &local_info;\n+\tbool scp_syntax = false;\n+\n+\tif (is_url(url_orig))\n+\t\turl_decoded = url_decode(url_orig);\n+\telse\n+\t\turl_decoded = xstrdup(url_orig);\n+\n+\tstrbuf_init(&url, strlen(url_decoded) + sizeof(\"ssh://\"));\n+\tstrbuf_addstr(&url, url_decoded);\n+\tfree(url_decoded);\n+\n+\thost = strstr(url.buf, \"://\");\n+\tif (host) {\n+\t\t/*\n+\t\t * Temporarily NUL-terminate the scheme name\n+\t\t * so we can pass it to url_get_scheme(),\n+\t\t * then restore the ':' so the buffer\n+\t\t * is intact for url_normalize() below.\n+\t\t */\n+\t\tchar saved = *host;\n+\t\t*host = '\\0';\n+\t\tscheme = url_get_scheme(url.buf);\n+\t\t*host = saved;\n+\t\thost += 3;\n+\t} else {\n+\t\tif (!url_is_local_not_ssh(url.buf)) {\n+\t\t\tscp_syntax = true;\n+\t\t\tscheme = URL_SCHEME_SSH;\n+\t\t\tstrbuf_insertstr(&url, 0, \"ssh://\");\n+\t\t\thost = url.buf + strlen(\"ssh://\");\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Path starts after ':' in scp style SSH URLs.\n+\t *\n+\t * The host portion can begin with an optional \"user@\",\n+\t * and the host itself can be wrapped in '[' ']' brackets.\n+\t * The bracket form is git's legacy way of supporting:\n+\t *\n+\t *   - IPv6 literals: [::1]:repo\n+\t *   - host:port pairs in the short form: [myhost:123]:src\n+\t *   - Plain hostnames that happen to need bracketing: [host]:path\n+\t *\n+\t * Treat '[' followed by 0 or 1 inner colons as the host:port\n+\t * or plain hostname form and strip the brackets so url_normalize\n+\t * sees host[:port] natively. Two or more inner colons mark an\n+\t * IPv6 literal: keep the brackets for url_normalize to recognize.\n+\t *\n+\t * The scp path separator is the ':' that follows the host part,\n+\t * and we must skip over user@ and any '[...]' before searching.\n+\t */\n+\tif (scp_syntax) {\n+\t\tchar *user_at;\n+\t\tchar *host_start;\n+\t\tchar *bracket_end;\n+\n+\t\tuser_at = strchr(host, '@');\n+\t\thost_start = user_at ? user_at + 1 : host;\n+\n+\t\tif (*host_start == '[') {\n+\t\t\tchar *p;\n+\t\t\tint inner_colons;\n+\n+\t\t\tbracket_end = strchr(host_start, ']');\n+\t\t\tinner_colons = 0;\n+\t\t\tfor (p = host_start + 1; bracket_end && p < bracket_end; p++)\n+\t\t\t\tif (*p == ':')\n+\t\t\t\t\tinner_colons++;\n+\n+\t\t\tif (bracket_end && inner_colons <= 1) {\n+\t\t\t\tsize_t close_off = bracket_end - url.buf;\n+\t\t\t\tsize_t open_off = host_start - url.buf;\n+\t\t\t\tstrbuf_remove(&url, close_off, 1);\n+\t\t\t\tstrbuf_remove(&url, open_off, 1);\n+\t\t\t\tseparator = url.buf + close_off - 1;\n+\t\t\t} else if (bracket_end) {\n+\t\t\t\tseparator = strchr(bracket_end + 1, ':');\n+\t\t\t} else {\n+\t\t\t\tseparator = strchr(host_start, ':');\n+\t\t\t}\n+\t\t} else {\n+\t\t\tseparator = strchr(host_start, ':');\n+\t\t}\n+\n+\t\tif (separator) {\n+\t\t\tif (separator[1] == '/')\n+\t\t\t\tstrbuf_remove(&url, separator - url.buf, 1);\n+\t\t\telse\n+\t\t\t\t*separator = '/';\n+\t\t}\n+\t}\n+\n+\tdetached = strbuf_detach(&url, NULL);\n+\tnormalized = url_normalize(detached, info);\n+\tfree(detached);\n+\n+\tif (!normalized)\n+\t\treturn NULL;\n+\n+\t/*\n+\t * Point path to ~ for URLs like this:\n+\t *\n+\t *     ssh://host.xz/~user/repo\n+\t *     git://host.xz/~user/repo\n+\t *     host.xz:~user/repo\n+\t */\n+\tif (scheme == URL_SCHEME_GIT || scheme == URL_SCHEME_SSH) {\n+\t\tif (normalized[info->path_off + 1] == '~') {\n+\t\t\tinfo->path_off++;\n+\t\t\tinfo->path_len--;\n+\t\t}\n+\t}\n+\n+\treturn normalized;\n+}\n+\n static size_t url_match_prefix(const char *url,\n \t\t\t       const char *url_prefix,\n \t\t\t       size_t url_prefix_len)\ndiff --git a/urlmatch.h b/urlmatch.h\nindex 5ba85cea13..6b3ce42858 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -35,6 +35,7 @@ struct url_info {\n };\n \n char *url_normalize(const char *, struct url_info *);\n+char *url_parse(const char *, struct url_info *);\n \n struct urlmatch_item {\n \tsize_t hostmatch_len;\n-- \ngitgitgadget\n\n"},{"id":"542567","messageId":"886a7d659ee3b018290ed24ad16d381525436b81.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 6/8] builtin: create url-parse command","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:08Z","receivedAt":"2026-05-01T23:15:23Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nGit commands can accept a rather wide variety of URLs syntaxes.\nThe range of accepted inputs might expand even more in the future.\nThis makes the parsing of URL components difficult since standard URL\nparsers cannot be used. Extracting the components of a git URL would\nrequire implementing all the schemes that git itself supports, not to\nmention tracking its development continuously in case new URL schemes\nare added.\n\nThe url-parse builtin command is designed to solve this problem\nby exposing git's native URL parsing facilities as a plumbing command.\nOther programs can then call upon git itself to parse the git URLs\nand extract their components. This should be quite useful for scripts.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n .gitignore          |   1 +\n Makefile            |   1 +\n builtin.h           |   1 +\n builtin/url-parse.c | 132 ++++++++++++++++++++++++++++++++++++++++++++\n command-list.txt    |   1 +\n git.c               |   1 +\n meson.build         |   1 +\n 7 files changed, 138 insertions(+)\n create mode 100644 builtin/url-parse.c\n\ndiff --git a/.gitignore b/.gitignore\nindex 24635cf2d6..c5673daa6e 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -182,6 +182,7 @@\n /git-update-server-info\n /git-upload-archive\n /git-upload-pack\n+/git-url-parse\n /git-var\n /git-verify-commit\n /git-verify-pack\ndiff --git a/Makefile b/Makefile\nindex cedc234173..1c757a1aa0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1497,6 +1497,7 @@ BUILTIN_OBJS += builtin/update-ref.o\n BUILTIN_OBJS += builtin/update-server-info.o\n BUILTIN_OBJS += builtin/upload-archive.o\n BUILTIN_OBJS += builtin/upload-pack.o\n+BUILTIN_OBJS += builtin/url-parse.o\n BUILTIN_OBJS += builtin/var.o\n BUILTIN_OBJS += builtin/verify-commit.o\n BUILTIN_OBJS += builtin/verify-pack.o\ndiff --git a/builtin.h b/builtin.h\nindex 235c51f30e..c6f7672991 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -271,6 +271,7 @@ int cmd_update_server_info(int argc, const char **argv, const char *prefix, stru\n int cmd_upload_archive(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_upload_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_url_parse(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_var(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_verify_commit(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_verify_tag(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nnew file mode 100644\nindex 0000000000..6c70c131e1\n--- /dev/null\n+++ b/builtin/url-parse.c\n@@ -0,0 +1,132 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+#include \"url.h\"\n+#include \"urlmatch.h\"\n+\n+static const char * const builtin_url_parse_usage[] = {\n+\tN_(\"git url-parse [-c <component>] [--] <url>...\"),\n+\tNULL\n+};\n+\n+static char *component_arg;\n+\n+static struct option builtin_url_parse_options[] = {\n+\tOPT_STRING('c', \"component\", &component_arg, N_(\"component\"),\n+\t\tN_(\"which URL component to extract\")),\n+\tOPT_END(),\n+};\n+\n+enum url_component {\n+\tURL_NONE = 0,\n+\tURL_SCHEME,\n+\tURL_USER,\n+\tURL_PASSWORD,\n+\tURL_HOST,\n+\tURL_PORT,\n+\tURL_PATH,\n+};\n+\n+static void parse_or_die(const char *url, struct url_info *info)\n+{\n+\tif (url_is_local_not_ssh(url)) {\n+\t\tif (*url == '/')\n+\t\t\tdie(\"'%s' is not a URL; if you meant a local \"\n+\t\t\t    \"repository, use 'file://%s'\", url, url);\n+\t\tdie(\"'%s' is not a URL; if you meant a local repository, \"\n+\t\t    \"use a 'file://' URL with an absolute path\", url);\n+\t}\n+\tif (!url_parse(url, info))\n+\t\tdie(\"invalid git URL '%s': %s\", url, info->err);\n+}\n+\n+static enum url_component get_component_or_die(const char *arg)\n+{\n+\tif (!strcmp(\"path\", arg))\n+\t\treturn URL_PATH;\n+\tif (!strcmp(\"host\", arg))\n+\t\treturn URL_HOST;\n+\tif (!strcmp(\"scheme\", arg))\n+\t\treturn URL_SCHEME;\n+\tif (!strcmp(\"user\", arg))\n+\t\treturn URL_USER;\n+\tif (!strcmp(\"password\", arg))\n+\t\treturn URL_PASSWORD;\n+\tif (!strcmp(\"port\", arg))\n+\t\treturn URL_PORT;\n+\tdie(\"invalid git URL component '%s'\", arg);\n+}\n+\n+static char *extract_component(enum url_component component,\n+\t\t\t       struct url_info *info)\n+{\n+\tsize_t offset, length;\n+\n+\tswitch (component) {\n+\tcase URL_SCHEME:\n+\t\toffset = 0;\n+\t\tlength = info->scheme_len;\n+\t\tbreak;\n+\tcase URL_USER:\n+\t\toffset = info->user_off;\n+\t\tlength = info->user_len;\n+\t\tbreak;\n+\tcase URL_PASSWORD:\n+\t\toffset = info->passwd_off;\n+\t\tlength = info->passwd_len;\n+\t\tbreak;\n+\tcase URL_HOST:\n+\t\toffset = info->host_off;\n+\t\tlength = info->host_len;\n+\t\tbreak;\n+\tcase URL_PORT:\n+\t\toffset = info->port_off;\n+\t\tlength = info->port_len;\n+\t\tbreak;\n+\tcase URL_PATH:\n+\t\toffset = info->path_off;\n+\t\tlength = info->path_len;\n+\t\tbreak;\n+\tcase URL_NONE:\n+\t\treturn NULL;\n+\t}\n+\n+\treturn xstrndup(info->url + offset, length);\n+}\n+\n+int cmd_url_parse(int argc,\n+\t\t  const char **argv,\n+\t\t  const char *prefix,\n+\t\t  struct repository *repo UNUSED)\n+{\n+\tstruct url_info info;\n+\tenum url_component selected = URL_NONE;\n+\tchar *extracted;\n+\tint i;\n+\n+\targc = parse_options(argc, argv, prefix, builtin_url_parse_options,\n+\t\t\t     builtin_url_parse_usage, 0);\n+\n+\tif (argc == 0)\n+\t\tusage_with_options(builtin_url_parse_usage,\n+\t\t\t\t   builtin_url_parse_options);\n+\n+\tif (component_arg)\n+\t\tselected = get_component_or_die(component_arg);\n+\n+\tfor (i = 0; i < argc; i++) {\n+\t\tparse_or_die(argv[i], &info);\n+\n+\t\tif (selected != URL_NONE) {\n+\t\t\textracted = extract_component(selected, &info);\n+\t\t\tif (extracted) {\n+\t\t\t\tputs(extracted);\n+\t\t\t\tfree(extracted);\n+\t\t\t}\n+\t\t}\n+\n+\t\tfree(info.url);\n+\t}\n+\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex f9005cf459..1ede48186f 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -202,6 +202,7 @@ git-update-ref                          plumbingmanipulators\n git-update-server-info                  synchingrepositories\n git-upload-archive                      synchelpers\n git-upload-pack                         synchelpers\n+git-url-parse                           purehelpers\n git-var                                 plumbinginterrogators\n git-verify-commit                       ancillaryinterrogators\n git-verify-pack                         plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 5a40eab8a2..a073eed931 100644\n--- a/git.c\n+++ b/git.c\n@@ -670,6 +670,7 @@ static struct cmd_struct commands[] = {\n \t{ \"upload-archive\", cmd_upload_archive, NO_PARSEOPT },\n \t{ \"upload-archive--writer\", cmd_upload_archive_writer, NO_PARSEOPT },\n \t{ \"upload-pack\", cmd_upload_pack },\n+\t{ \"url-parse\", cmd_url_parse },\n \t{ \"var\", cmd_var, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"verify-commit\", cmd_verify_commit, RUN_SETUP },\n \t{ \"verify-pack\", cmd_verify_pack },\ndiff --git a/meson.build b/meson.build\nindex 11488623bf..dc3cf68ee5 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -686,6 +686,7 @@ builtin_sources = [\n   'builtin/update-server-info.c',\n   'builtin/upload-archive.c',\n   'builtin/upload-pack.c',\n+  'builtin/url-parse.c',\n   'builtin/var.c',\n   'builtin/verify-commit.c',\n   'builtin/verify-pack.c',\n-- \ngitgitgadget\n\n"},{"id":"542568","messageId":"3c44e0f478c40375a63cc08139736aeb07aff26c.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 7/8] doc: describe the url-parse builtin","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:09Z","receivedAt":"2026-05-01T23:15:25Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nThe new url-parse builtin validates git URLs\nand optionally extracts their components.\n\nHelped-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n Documentation/git-url-parse.adoc | 80 ++++++++++++++++++++++++++++++++\n Documentation/meson.build        |  1 +\n 2 files changed, 81 insertions(+)\n create mode 100644 Documentation/git-url-parse.adoc\n\ndiff --git a/Documentation/git-url-parse.adoc b/Documentation/git-url-parse.adoc\nnew file mode 100644\nindex 0000000000..9d0d93da4a\n--- /dev/null\n+++ b/Documentation/git-url-parse.adoc\n@@ -0,0 +1,80 @@\n+git-url-parse(1)\n+================\n+\n+NAME\n+----\n+git-url-parse - Parse and extract git URL components\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git url-parse [-c <component>] [--] <url>...\n+\n+DESCRIPTION\n+-----------\n+\n+Git supports many ways to specify URLs, some of them non-standard.\n+For example, git supports the scp style [user@]host:[path] format.\n+This command eases interoperability with git URLs by enabling the\n+parsing and extraction of the components of all git URLs.\n+\n+Any syntactically valid URL is parsed, even if the scheme is not one\n+git supports for fetching or pushing.\n+\n+OPTIONS\n+-------\n+\n+`-c <component>`::\n+`--component <component>`::\n+\tExtract the _<component>_ component from the given Git URLs.\n+\t_<component>_ can be one of:\n+\t`scheme`, `user`, `password`, `host`, `port`, `path`.\n+\n+OUTPUT\n+------\n+\n+When `--component` is given, the requested component of each URL\n+is printed on its own line, in the order the URLs were given. If\n+the URL has no such component (for example, a port in a URL that\n+does not specify one), an empty line is printed in its place.\n+\n+When `--component` is not given, no output is produced. The exit\n+status is zero if every URL parses successfully and non-zero\n+otherwise, allowing the command to be used purely as a validator.\n+\n+EXAMPLES\n+--------\n+\n+* Print the host name:\n++\n+------------\n+$ git url-parse --component host https://example.com/user/repo\n+example.com\n+------------\n+\n+* Print the path:\n++\n+------------\n+$ git url-parse --component path https://example.com/user/repo\n+/user/repo\n+$ git url-parse --component path example.com:~user/repo\n+~user/repo\n+$ git url-parse --component path example.com:user/repo\n+/user/repo\n+------------\n+\n+* Validate URLs without outputting anything:\n++\n+------------\n+$ git url-parse https://example.com/user/repo example.com:~user/repo\n+------------\n+\n+SEE ALSO\n+--------\n+linkgit:git-clone[1],\n+linkgit:git-fetch[1],\n+linkgit:git-config[1]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex d6365b888b..32c8606a80 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -155,6 +155,7 @@ manpages = {\n   'git-update-server-info.adoc' : 1,\n   'git-upload-archive.adoc' : 1,\n   'git-upload-pack.adoc' : 1,\n+  'git-url-parse.adoc' : 1,\n   'git-var.adoc' : 1,\n   'git-verify-commit.adoc' : 1,\n   'git-verify-pack.adoc' : 1,\n-- \ngitgitgadget\n\n"},{"id":"542569","messageId":"cf2ae409e6eb4aa24c6afdcdca4ce4c4df2ae7b8.1777677310.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v2 8/8] t9904: add tests for the new url-parse builtin","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-01T23:15:10Z","receivedAt":"2026-05-01T23:15:26Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nTest git URL parsing, validation and component extraction\non all documented git URL schemes and syntaxes.\n\nAdd IPv6 host coverage in URL form:\n\n    ssh://[::1]/path\n    ssh://user@[::1]:1234/path\n    git://[::1]:9418/path\n    http://[2001:db8::1]/path\n    https://[2001:db8::1]/path\n\nIn URL form the brackets are kept in the host component (RFC 3986\nsyntax for IPv6 literals).\n\nAlso exercise the bracketed scp short forms that t5601-clone.sh\ncovers via parse_connect_url:\n\n    [host]:path\n    [host:port]:path\n    [::1]:repo\n    user@[::1]:repo\n    user@[host:port]:path\n\nIn scp form, brackets are kept for IPv6 literals (two or more inner\ncolons) and stripped for plain hostnames or host:port pairs.\n\nSuggested-by: Torsten Bögershausen <tboegi@web.de>\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n t/meson.build        |   1 +\n t/t9904-url-parse.sh | 319 +++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 320 insertions(+)\n create mode 100755 t/t9904-url-parse.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 7528e5cda5..41b389a472 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1114,6 +1114,7 @@ integration_tests = [\n   't9901-git-web--browse.sh',\n   't9902-completion.sh',\n   't9903-bash-prompt.sh',\n+  't9904-url-parse.sh',\n ]\n \n benchmarks = [\ndiff --git a/t/t9904-url-parse.sh b/t/t9904-url-parse.sh\nnew file mode 100755\nindex 0000000000..32b3f4a286\n--- /dev/null\n+++ b/t/t9904-url-parse.sh\n@@ -0,0 +1,319 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2024 Matheus Afonso Martins Moreira\n+#\n+\n+test_description='git url-parse tests'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git url-parse -- ssh syntax' '\n+\tgit url-parse \"ssh://user@example.com:1234/repository/path\" &&\n+\tgit url-parse \"ssh://user@example.com/repository/path\" &&\n+\tgit url-parse \"ssh://example.com:1234/repository/path\" &&\n+\tgit url-parse \"ssh://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- git syntax' '\n+\tgit url-parse \"git://example.com:1234/repository/path\" &&\n+\tgit url-parse \"git://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- http syntax' '\n+\tgit url-parse \"https://example.com:1234/repository/path\" &&\n+\tgit url-parse \"https://example.com/repository/path\" &&\n+\tgit url-parse \"http://example.com:1234/repository/path\" &&\n+\tgit url-parse \"http://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax' '\n+\tgit url-parse \"user@example.com:/repository/path\" &&\n+\tgit url-parse \"example.com:/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - ssh syntax' '\n+\tgit url-parse \"ssh://user@example.com:1234/~user/repository\" &&\n+\tgit url-parse \"ssh://user@example.com/~user/repository\" &&\n+\tgit url-parse \"ssh://example.com:1234/~user/repository\" &&\n+\tgit url-parse \"ssh://example.com/~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - git syntax' '\n+\tgit url-parse \"git://example.com:1234/~user/repository\" &&\n+\tgit url-parse \"git://example.com/~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - scp syntax' '\n+\tgit url-parse \"user@example.com:~user/repository\" &&\n+\tgit url-parse \"example.com:~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- file urls' '\n+\tgit url-parse \"file:///repository/path\" &&\n+\tgit url-parse \"file://\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- ssh syntax' '\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://user@example.com/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- git syntax' '\n+\ttest git = \"$(git url-parse -c scheme \"git://example.com:1234/repository/path\")\" &&\n+\ttest git = \"$(git url-parse -c scheme \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- http syntax' '\n+\ttest https = \"$(git url-parse -c scheme \"https://example.com:1234/repository/path\")\" &&\n+\ttest https = \"$(git url-parse -c scheme \"https://example.com/repository/path\")\" &&\n+\ttest http = \"$(git url-parse -c scheme \"http://example.com:1234/repository/path\")\" &&\n+\ttest http = \"$(git url-parse -c scheme \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- scp syntax' '\n+\ttest ssh = \"$(git url-parse -c scheme \"user@example.com:/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- ssh syntax' '\n+\ttest user = \"$(git url-parse -c user \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest user = \"$(git url-parse -c user \"ssh://user@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- git syntax' '\n+\ttest \"\" = \"$(git url-parse -c user \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- http syntax' '\n+\ttest \"\" = \"$(git url-parse -c user \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"https://example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- scp syntax' '\n+\ttest user = \"$(git url-parse -c user \"user@example.com:/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c password -- http syntax' '\n+\ttest secret = \"$(git url-parse -c password \"https://user:secret@example.com:1234/repository/path\")\" &&\n+\ttest secret = \"$(git url-parse -c password \"http://user:secret@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c password \"https://user@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c password \"https://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- ssh syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- git syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"git://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- http syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"https://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"https://example.com/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"http://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- scp syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"user@example.com:/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- ssh syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://user@example.com/repository/path\")\" &&\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- git syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- http syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"https://example.com/repository/path\")\" &&\n+\ttest 1234 = \"$(git url-parse -c port \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- scp syntax' '\n+\ttest \"\" = \"$(git url-parse -c port \"user@example.com:/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- ssh syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://user@example.com/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- git syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- http syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"https://example.com/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- scp syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"user@example.com:/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - ssh syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://user@example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://user@example.com/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - git syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com/~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - scp syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"user@example.com:~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"example.com:~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion strips query and fragment' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository?query\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository#fragment\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com/~user/repository?query\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"user@example.com:~user/repository?query\")\"\n+'\n+\n+test_expect_success 'git url-parse -- ssh syntax with IPv6' '\n+\tgit url-parse \"ssh://user@[::1]:1234/repository/path\" &&\n+\tgit url-parse \"ssh://user@[::1]/repository/path\" &&\n+\tgit url-parse \"ssh://[::1]:1234/repository/path\" &&\n+\tgit url-parse \"ssh://[::1]/repository/path\" &&\n+\tgit url-parse \"ssh://[2001:db8::1]/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- git syntax with IPv6' '\n+\tgit url-parse \"git://[::1]:9418/repository/path\" &&\n+\tgit url-parse \"git://[::1]/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- http syntax with IPv6' '\n+\tgit url-parse \"https://[::1]:1234/repository/path\" &&\n+\tgit url-parse \"https://[::1]/repository/path\" &&\n+\tgit url-parse \"http://[2001:db8::1]/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- IPv6 in URL form' '\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"ssh://user@[::1]:1234/repository/path\")\" &&\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"ssh://[::1]/repository/path\")\" &&\n+\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"ssh://[2001:db8::1]/repository/path\")\" &&\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"git://[::1]/repository/path\")\" &&\n+\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"https://[2001:db8::1]/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- IPv6 in URL form' '\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://user@[::1]:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://[::1]/repository/path\")\" &&\n+\ttest 9418 = \"$(git url-parse -c port \"git://[::1]:9418/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax with IPv6' '\n+\tgit url-parse \"[::1]:repository/path\" &&\n+\tgit url-parse \"user@[::1]:repository/path\" &&\n+\tgit url-parse \"[2001:db8::1]:repo\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax with bracketed hostname' '\n+\tgit url-parse \"[myhost]:src\" &&\n+\tgit url-parse \"user@[myhost]:src\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax with bracketed host:port' '\n+\tgit url-parse \"[myhost:123]:src\" &&\n+\tgit url-parse \"user@[myhost:123]:src\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- scp+IPv6' '\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"[::1]:repository/path\")\" &&\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"user@[::1]:repository/path\")\" &&\n+\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"[2001:db8::1]:repo\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- scp+IPv6' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"[::1]:/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"[::1]:repository/path\")\" &&\n+\ttest \"/repo\" = \"$(git url-parse -c path \"[2001:db8::1]:repo\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host,port,path -- scp [host:port]:src' '\n+\ttest myhost = \"$(git url-parse -c host \"[myhost:123]:src\")\" &&\n+\ttest 123 = \"$(git url-parse -c port \"[myhost:123]:src\")\" &&\n+\ttest \"/src\" = \"$(git url-parse -c path \"[myhost:123]:src\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host,path -- scp [host]:src' '\n+\ttest myhost = \"$(git url-parse -c host \"[myhost]:src\")\" &&\n+\ttest \"/src\" = \"$(git url-parse -c path \"[myhost]:src\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- scp with user@ and brackets' '\n+\ttest user = \"$(git url-parse -c user \"user@[::1]:repo\")\" &&\n+\ttest user = \"$(git url-parse -c user \"user@[myhost:123]:src\")\" &&\n+\ttest user = \"$(git url-parse -c user \"user@[myhost]:src\")\"\n+'\n+\n+test_expect_success 'git url-parse -- scp+IPv6 with username expansion' '\n+\ttest \"~user/repo\" = \"$(git url-parse -c path \"[::1]:~user/repo\")\" &&\n+\ttest \"~user/repo\" = \"$(git url-parse -c path \"user@[::1]:~user/repo\")\"\n+'\n+\n+test_expect_success 'git url-parse fails on invalid URL' '\n+\ttest_must_fail git url-parse \"not a url\"\n+'\n+\n+test_expect_success 'git url-parse helpful error for absolute local path' '\n+\ttest_must_fail git url-parse \"/abs/path\" 2>err &&\n+\ttest_grep \"is not a URL\" err &&\n+\ttest_grep \"file:///abs/path\" err\n+'\n+\n+test_expect_success 'git url-parse helpful error for relative local path' '\n+\ttest_must_fail git url-parse \"./rel\" 2>err &&\n+\ttest_grep \"is not a URL\" err &&\n+\ttest_grep \"absolute path\" err\n+'\n+\n+test_expect_success 'git url-parse fails on unknown -c component name' '\n+\ttest_must_fail git url-parse -c bogus \"https://example.com/repo\"\n+'\n+\n+test_expect_success 'git url-parse fails on URL missing host' '\n+\ttest_must_fail git url-parse \"https://\"\n+'\n+\n+test_expect_success 'git url-parse with no URL prints usage' '\n+\ttest_must_fail git url-parse 2>err &&\n+\ttest_grep \"usage:\" err\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"542570","messageId":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v2.git.git.1777677310.gitgitgadget@gmail.com","subject":"[PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Matheus Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:34Z","receivedAt":"2026-05-02T05:28:45Z","isPatch":true,"body":"This series adds git url-parse, a plumbing builtin for inspecting git URLs.\nGit accepts a wider variety of URL forms than any standard parser handles.\nThe supported forms include RFC URLs, file:// URLs, scp-style\n[user@]host:path for SSH, and IPv6 in brackets. Tools wanting to reason\nabout them have historically had to reimplement git's parsing or shell out\nindirectly. With git url-parse, scripts can ask git directly: validate a\nURL, extract a component (scheme, user, host, port, path, password), or\nboth.\n\nThe series consists of eight commits.\n\nThe first four are preparatory. They rename enum protocol to enum url_scheme\nfor RFC alignment, move url_is_local_not_ssh and the scheme-detection\nroutines from connect.c to url.h/url.c, and stop url_get_scheme from dying\non unknown schemes so other parsers can handle unknowns gracefully.\n\nThe fifth commit defines the new parser, url_parse, in urlmatch.c. It is\nadapted from parse_connect_url and uses the same data structures as\nurl_normalize. The parser returns NULL on failure with err populated, and\nexposes URL components as offset/length pairs into the normalized URL\nbuffer.\n\nThe sixth commit adds the user-facing command, with a helpful error when the\ninput looks like a local path rather than a URL.\n\nThe last two commits are documentation (a manpage) and 53 tests covering URL\nform, scp form, IPv6 in URL and scp forms, bracket forms, username\nexpansion, query/fragment stripping, the local-path error, and\nvalidation-only mode.\n\nSeveral choices in this series are judgment calls. Happy to amend or follow\nup on any of them.\n\nThe component name is scheme, not protocol. RFC 1738/3986 calls them\nschemes. The series renames enum protocol to enum url_scheme internally, and\nthe user-facing component name follows the same direction. I considered\naccepting both as aliases but decided against the precedent for a new\ncommand. If you would rather see protocol, or both protocol and scheme, that\nis easy to change.\n\nLocal paths are deliberately not URLs. parse_connect_url accepts bare paths\nlike /abs/path or ./rel as URL_SCHEME_LOCAL. url_parse rejects them, since\nurl_normalize requires a scheme://host form, and silent conversion to\nfile:// has no good answer for relative or tilde forms. The builtin emits a\nhelpful error suggesting the explicit file:// form. If full git clone parity\nis preferred (bare paths accepted via auto-conversion or a new flag), that\ncould be added.\n\nAbsent and empty components are conflated in output. --component user\nhttp://host/ and --component user http://@host/ both produce empty lines.\nThe underlying struct url_info preserves the distinction: *_off == 0 vs\n*_off != 0 with *_len == 0. A future option can expose it without breaking\nchange. Can amend this patch set if necessary.\n\nChanges since v1:\n\n * Bug fix: ~user paths with a query string or fragment were leaking the ?\n   or # into the path output. The ~user-skip logic in url_parse previously\n   ran only for file://. It now runs for git/ssh/scp URLs as well, matching\n   what parse_connect_url does and what users expect.\n\n * Helpful error for local paths instead of the cryptic \"invalid URL scheme\n   name or missing '://' suffix\".\n\n * -c protocol renamed to -c scheme for consistency with the internal rename\n   and the RFC.\n\n * Documented the deliberate divergence from parse_connect_url (local paths\n   and unknown schemes) in the urlmatch commit message.\n\n * Doc and command-list polish: purehelpers category, asciidoc placeholder\n   convention, [synopsis] form.\n\n * Original micro commit style staged buildup of the builtin collapsed to a\n   single self-contained commit. The rest of the series is unchanged in\n   shape.\n\nChanges since v2:\n\n * Fix Windows CI failure: handle DOS drive prefix in the helpful local-path\n   error. With this, the message for a drive-letter input like C:/repo (or\n   an MSYS-mangled /abs/path that bash rewrites to D:/.../abs/path before\n   git sees it) gets the specific file:///<input> suggestion rather than the\n   generic fallback. No effect on Linux or macOS, since has_dos_drive_prefix\n   is a no-op on non-Windows builds.\n\n * t9904: relax the grep on the absolute-path test from the literal\n   file:///abs/path to the structural file:/// (three slashes). The original\n   assertion depended on the input being preserved verbatim, which MSYS does\n   not do. The relaxed grep verifies the structurally meaningful property\n   (specific URL suggestion was produced, not the generic fallback) and runs\n   cross-platform.\n\nRange-diff against v2:\n\n1: 38f797362d = 1: 38f797362d connect: rename enum protocol to url_scheme 2:\na4153e1d24 = 2: a4153e1d24 url: move url_is_local_not_ssh to url.h 3:\ne584fb03f3 = 3: e584fb03f3 url: move scheme detection to URL header/source\n4: 7381704c38 = 4: 7381704c38 url: return URL_SCHEME_UNKNOWN instead of\ndying 5: 89932a70f3 = 5: 89932a70f3 urlmatch: define url_parse function 6:\n886a7d659e ! 6: af6c71227b builtin: create url-parse command @@\nbuiltin/url-parse.c (new) + if (*url == '/') + die(\"'%s' is not a URL; if\nyou meant a local \" + \"repository, use 'file://%s'\", url, url); ++ if\n(has_dos_drive_prefix(url)) ++ die(\"'%s' is not a URL; if you meant a local\n\" ++ \"repository, use 'file:///%s'\", url, url); + die(\"'%s' is not a URL; if\nyou meant a local repository, \" + \"use a 'file://' URL with an absolute\npath\", url); + } 7: 3c44e0f478 = 7: 2b32cb71a3 doc: describe the url-parse\nbuiltin 8: cf2ae409e6 ! 8: ce41d2ec50 t9904: add tests for the new url-parse\nbuiltin @@ t/t9904-url-parse.sh (new) +test_expect_success 'git url-parse\nhelpful error for absolute local path' ' + test_must_fail git url-parse\n\"/abs/path\" 2>err && + test_grep \"is not a URL\" err && -+ test_grep\n\"file:///abs/path\" err ++ test_grep \"file:///\" err +' + +test_expect_success\n'git url-parse helpful error for relative local path' '\n\nMatheus Afonso Martins Moreira (8):\n  connect: rename enum protocol to url_scheme\n  url: move url_is_local_not_ssh to url.h\n  url: move scheme detection to URL header/source\n  url: return URL_SCHEME_UNKNOWN instead of dying\n  urlmatch: define url_parse function\n  builtin: create url-parse command\n  doc: describe the url-parse builtin\n  t9904: add tests for the new url-parse builtin\n\n .gitignore                              |   1 +\n Documentation/git-url-parse.adoc        |  80 ++++++\n Documentation/meson.build               |   1 +\n Makefile                                |   1 +\n builtin.h                               |   1 +\n builtin/url-parse.c                     | 135 ++++++++++\n command-list.txt                        |   1 +\n connect.c                               |  78 ++----\n connect.h                               |   1 -\n git.c                                   |   1 +\n meson.build                             |   1 +\n remote.c                                |   1 +\n t/meson.build                           |   1 +\n t/t9904-url-parse.sh                    | 319 ++++++++++++++++++++++++\n t/unit-tests/u-urlmatch-normalization.c |  45 ++++\n url.c                                   |  23 ++\n url.h                                   |  16 ++\n urlmatch.c                              | 127 ++++++++++\n urlmatch.h                              |   1 +\n 19 files changed, 780 insertions(+), 54 deletions(-)\n create mode 100644 Documentation/git-url-parse.adoc\n create mode 100644 builtin/url-parse.c\n create mode 100755 t/t9904-url-parse.sh\n\n\nbase-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1715%2Fmatheusmoreira%2Furl-parse-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1715/matheusmoreira/url-parse-v3\nPull-Request: https://github.com/git/git/pull/1715\n\nRange-diff vs v2:\n\n 1:  38f797362d = 1:  38f797362d connect: rename enum protocol to url_scheme\n 2:  a4153e1d24 = 2:  a4153e1d24 url: move url_is_local_not_ssh to url.h\n 3:  e584fb03f3 = 3:  e584fb03f3 url: move scheme detection to URL header/source\n 4:  7381704c38 = 4:  7381704c38 url: return URL_SCHEME_UNKNOWN instead of dying\n 5:  89932a70f3 = 5:  89932a70f3 urlmatch: define url_parse function\n 6:  886a7d659e ! 6:  af6c71227b builtin: create url-parse command\n     @@ builtin/url-parse.c (new)\n      +\t\tif (*url == '/')\n      +\t\t\tdie(\"'%s' is not a URL; if you meant a local \"\n      +\t\t\t    \"repository, use 'file://%s'\", url, url);\n     ++\t\tif (has_dos_drive_prefix(url))\n     ++\t\t\tdie(\"'%s' is not a URL; if you meant a local \"\n     ++\t\t\t    \"repository, use 'file:///%s'\", url, url);\n      +\t\tdie(\"'%s' is not a URL; if you meant a local repository, \"\n      +\t\t    \"use a 'file://' URL with an absolute path\", url);\n      +\t}\n 7:  3c44e0f478 = 7:  2b32cb71a3 doc: describe the url-parse builtin\n 8:  cf2ae409e6 ! 8:  ce41d2ec50 t9904: add tests for the new url-parse builtin\n     @@ t/t9904-url-parse.sh (new)\n      +test_expect_success 'git url-parse helpful error for absolute local path' '\n      +\ttest_must_fail git url-parse \"/abs/path\" 2>err &&\n      +\ttest_grep \"is not a URL\" err &&\n     -+\ttest_grep \"file:///abs/path\" err\n     ++\ttest_grep \"file:///\" err\n      +'\n      +\n      +test_expect_success 'git url-parse helpful error for relative local path' '\n\n-- \ngitgitgadget\n"},{"id":"542571","messageId":"38f797362d268a51b979efaa1d435d9f7a3378f6.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 1/8] connect: rename enum protocol to url_scheme","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:35Z","receivedAt":"2026-05-02T05:28:46Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nRFC 1738 names the part of a URL before the colon a \"scheme\".\nconnect.c calls it \"protocol\", which is more generic\nand collides with the unrelated enum protocol_version.\n\nRename:\n\n    enum protocol -> enum url_scheme\n    PROTO_*       -> URL_SCHEME_*\n    prot_name     -> url_scheme_name\n    get_protocol  -> url_get_scheme\n\nThe local variables in parse_connect_url and git_connect\nare renamed accordingly, from protocol to scheme.\n\nNo behavior change. The user-visible diagnostics\nand translated error messages are preserved:\n\n    \"Diag: protocol=...\"\n    \"protocol '%s' is not supported\"\n    \"unknown protocol\"\n\nThis rename also prepares for moving the scheme-detection functions\nto a shared header so that a future plumbing command can parse URLs\nusing the same logic as the connect path.\n\nSuggested-by: Torsten Bögershausen <tboegi@web.de>\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 68 +++++++++++++++++++++++++++----------------------------\n 1 file changed, 34 insertions(+), 34 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex fcd35c5539..46da89905e 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -700,11 +700,11 @@ int server_supports(const char *feature)\n \treturn !!server_feature_value(feature, NULL);\n }\n \n-enum protocol {\n-\tPROTO_LOCAL = 1,\n-\tPROTO_FILE,\n-\tPROTO_SSH,\n-\tPROTO_GIT\n+enum url_scheme {\n+\tURL_SCHEME_LOCAL = 1,\n+\tURL_SCHEME_FILE,\n+\tURL_SCHEME_SSH,\n+\tURL_SCHEME_GIT\n };\n \n int url_is_local_not_ssh(const char *url)\n@@ -715,33 +715,33 @@ int url_is_local_not_ssh(const char *url)\n \t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n }\n \n-static const char *prot_name(enum protocol protocol)\n+static const char *url_scheme_name(enum url_scheme scheme)\n {\n-\tswitch (protocol) {\n-\t\tcase PROTO_LOCAL:\n-\t\tcase PROTO_FILE:\n+\tswitch (scheme) {\n+\t\tcase URL_SCHEME_LOCAL:\n+\t\tcase URL_SCHEME_FILE:\n \t\t\treturn \"file\";\n-\t\tcase PROTO_SSH:\n+\t\tcase URL_SCHEME_SSH:\n \t\t\treturn \"ssh\";\n-\t\tcase PROTO_GIT:\n+\t\tcase URL_SCHEME_GIT:\n \t\t\treturn \"git\";\n \t\tdefault:\n \t\t\treturn \"unknown protocol\";\n \t}\n }\n \n-static enum protocol get_protocol(const char *name)\n+static enum url_scheme url_get_scheme(const char *name)\n {\n \tif (!strcmp(name, \"ssh\"))\n-\t\treturn PROTO_SSH;\n+\t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"git\"))\n-\t\treturn PROTO_GIT;\n+\t\treturn URL_SCHEME_GIT;\n \tif (!strcmp(name, \"git+ssh\")) /* deprecated - do not use */\n-\t\treturn PROTO_SSH;\n+\t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"ssh+git\")) /* deprecated - do not use */\n-\t\treturn PROTO_SSH;\n+\t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"file\"))\n-\t\treturn PROTO_FILE;\n+\t\treturn URL_SCHEME_FILE;\n \tdie(_(\"protocol '%s' is not supported\"), name);\n }\n \n@@ -1083,14 +1083,14 @@ static char *get_port(char *host)\n  * Extract protocol and relevant parts from the specified connection URL.\n  * The caller must free() the returned strings.\n  */\n-static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n-\t\t\t\t       char **ret_path)\n+static enum url_scheme parse_connect_url(const char *url_orig, char **ret_host,\n+\t\t\t\t\t char **ret_path)\n {\n \tchar *url;\n \tchar *host, *path;\n \tchar *end;\n \tint separator = '/';\n-\tenum protocol protocol = PROTO_LOCAL;\n+\tenum url_scheme scheme = URL_SCHEME_LOCAL;\n \n \tif (is_url(url_orig))\n \t\turl = url_decode(url_orig);\n@@ -1100,12 +1100,12 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \thost = strstr(url, \"://\");\n \tif (host) {\n \t\t*host = '\\0';\n-\t\tprotocol = get_protocol(url);\n+\t\tscheme = url_get_scheme(url);\n \t\thost += 3;\n \t} else {\n \t\thost = url;\n \t\tif (!url_is_local_not_ssh(url)) {\n-\t\t\tprotocol = PROTO_SSH;\n+\t\t\tscheme = URL_SCHEME_SSH;\n \t\t\tseparator = ':';\n \t\t}\n \t}\n@@ -1116,13 +1116,13 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \t */\n \tend = host_end(&host, 0);\n \n-\tif (protocol == PROTO_LOCAL)\n+\tif (scheme == URL_SCHEME_LOCAL)\n \t\tpath = end;\n-\telse if (protocol == PROTO_FILE && *host != '/' &&\n+\telse if (scheme == URL_SCHEME_FILE && *host != '/' &&\n \t\t !has_dos_drive_prefix(host) &&\n \t\t offset_1st_component(host - 2) > 1)\n \t\tpath = host - 2; /* include the leading \"//\" */\n-\telse if (protocol == PROTO_FILE && has_dos_drive_prefix(end))\n+\telse if (scheme == URL_SCHEME_FILE && has_dos_drive_prefix(end))\n \t\tpath = end; /* \"file://$(pwd)\" may be \"file://C:/projects/repo\" */\n \telse\n \t\tpath = strchr(end, separator);\n@@ -1138,7 +1138,7 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \tend = path; /* Need to \\0 terminate host here */\n \tif (separator == ':')\n \t\tpath++; /* path starts after ':' */\n-\tif (protocol == PROTO_GIT || protocol == PROTO_SSH) {\n+\tif (scheme == URL_SCHEME_GIT || scheme == URL_SCHEME_SSH) {\n \t\tif (path[1] == '~')\n \t\t\tpath++;\n \t}\n@@ -1149,7 +1149,7 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,\n \t*ret_host = xstrdup(host);\n \t*ret_path = path;\n \tfree(url);\n-\treturn protocol;\n+\treturn scheme;\n }\n \n static const char *get_ssh_command(void)\n@@ -1434,7 +1434,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n {\n \tchar *hostandport, *path;\n \tstruct child_process *conn;\n-\tenum protocol protocol;\n+\tenum url_scheme scheme;\n \tenum protocol_version version = get_protocol_version_config();\n \n \t/*\n@@ -1451,14 +1451,14 @@ struct child_process *git_connect(int fd[2], const char *url,\n \t */\n \tsignal(SIGCHLD, SIG_DFL);\n \n-\tprotocol = parse_connect_url(url, &hostandport, &path);\n-\tif ((flags & CONNECT_DIAG_URL) && (protocol != PROTO_SSH)) {\n+\tscheme = parse_connect_url(url, &hostandport, &path);\n+\tif ((flags & CONNECT_DIAG_URL) && (scheme != URL_SCHEME_SSH)) {\n \t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\n-\t\tprintf(\"Diag: protocol=%s\\n\", prot_name(protocol));\n+\t\tprintf(\"Diag: protocol=%s\\n\", url_scheme_name(scheme));\n \t\tprintf(\"Diag: hostandport=%s\\n\", hostandport ? hostandport : \"NULL\");\n \t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n \t\tconn = NULL;\n-\t} else if (protocol == PROTO_GIT) {\n+\t} else if (scheme == URL_SCHEME_GIT) {\n \t\tconn = git_connect_git(fd, hostandport, path, prog, version, flags);\n \t\tconn->trace2_child_class = \"transport/git\";\n \t} else {\n@@ -1481,7 +1481,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n \n \t\tconn->use_shell = 1;\n \t\tconn->in = conn->out = -1;\n-\t\tif (protocol == PROTO_SSH) {\n+\t\tif (scheme == URL_SCHEME_SSH) {\n \t\t\tchar *ssh_host = hostandport;\n \t\t\tconst char *port = NULL;\n \t\t\ttransport_check_allowed(\"ssh\");\n@@ -1492,7 +1492,7 @@ struct child_process *git_connect(int fd[2], const char *url,\n \n \t\t\tif (flags & CONNECT_DIAG_URL) {\n \t\t\t\tprintf(\"Diag: url=%s\\n\", url ? url : \"NULL\");\n-\t\t\t\tprintf(\"Diag: protocol=%s\\n\", prot_name(protocol));\n+\t\t\t\tprintf(\"Diag: protocol=%s\\n\", url_scheme_name(scheme));\n \t\t\t\tprintf(\"Diag: userandhost=%s\\n\", ssh_host ? ssh_host : \"NULL\");\n \t\t\t\tprintf(\"Diag: port=%s\\n\", port ? port : \"NONE\");\n \t\t\t\tprintf(\"Diag: path=%s\\n\", path ? path : \"NULL\");\n-- \ngitgitgadget\n\n"},{"id":"542572","messageId":"a4153e1d2470d42d17758df727fc267a7debbc3d.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 2/8] url: move url_is_local_not_ssh to url.h","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:36Z","receivedAt":"2026-05-02T05:28:47Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nMove url_is_local_not_ssh from connect.c/connect.h\nto url.c/url.h so that the new url_parse function\nin urlmatch.c, and any future code that needs to\ndistinguish a local path from an scp style SSH URL,\ncan reuse the heuristic without depending on connect.c.\n\nNo behavior change.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 8 --------\n connect.h | 1 -\n remote.c  | 1 +\n url.c     | 8 ++++++++\n url.h     | 2 ++\n 5 files changed, 11 insertions(+), 9 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex 46da89905e..cb145de30e 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -707,14 +707,6 @@ enum url_scheme {\n \tURL_SCHEME_GIT\n };\n \n-int url_is_local_not_ssh(const char *url)\n-{\n-\tconst char *colon = strchr(url, ':');\n-\tconst char *slash = strchr(url, '/');\n-\treturn !colon || (slash && slash < colon) ||\n-\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n-}\n-\n static const char *url_scheme_name(enum url_scheme scheme)\n {\n \tswitch (scheme) {\ndiff --git a/connect.h b/connect.h\nindex 1645126c17..8d84f6656b 100644\n--- a/connect.h\n+++ b/connect.h\n@@ -13,7 +13,6 @@ int git_connection_is_socket(struct child_process *conn);\n int server_supports(const char *feature);\n int parse_feature_request(const char *features, const char *feature);\n const char *server_feature_value(const char *feature, size_t *len_ret);\n-int url_is_local_not_ssh(const char *url);\n \n struct packet_reader;\n enum protocol_version discover_version(struct packet_reader *reader);\ndiff --git a/remote.c b/remote.c\nindex a664cd166a..24a8118d25 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -8,6 +8,7 @@\n #include \"gettext.h\"\n #include \"hex.h\"\n #include \"remote.h\"\n+#include \"url.h\"\n #include \"urlmatch.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\ndiff --git a/url.c b/url.c\nindex 3ca5987e90..057576042a 100644\n--- a/url.c\n+++ b/url.c\n@@ -132,3 +132,11 @@ void str_end_url_with_slash(const char *url, char **dest)\n \tfree(*dest);\n \t*dest = strbuf_detach(&buf, NULL);\n }\n+\n+int url_is_local_not_ssh(const char *url)\n+{\n+\tconst char *colon = strchr(url, ':');\n+\tconst char *slash = strchr(url, '/');\n+\treturn !colon || (slash && slash < colon) ||\n+\t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n+}\ndiff --git a/url.h b/url.h\nindex cd9140e994..39d621312f 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,6 +21,8 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+int url_is_local_not_ssh(const char *url);\n+\n /*\n  * The set of unreserved characters as per STD66 (RFC3986) is\n  * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n-- \ngitgitgadget\n\n"},{"id":"542573","messageId":"e584fb03f3c23fda452b8e53d287f8f835becd48.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 3/8] url: move scheme detection to URL header/source","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:37Z","receivedAt":"2026-05-02T05:28:49Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nMove enum url_scheme and url_get_scheme()\nfrom connect.c to url.h and url.c\nso that other code can identify\na URL's scheme without depending\non connect.c.\n\nNo behavior change. url_get_scheme() still dies\non an unrecognized scheme name, with the same\ntranslated message as before.\n\nscheme_name() stays in connect.c\nbecause it has no other callers.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 22 ----------------------\n url.c     | 16 ++++++++++++++++\n url.h     | 13 +++++++++++++\n 3 files changed, 29 insertions(+), 22 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex cb145de30e..1ac7acc6e8 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -700,13 +700,6 @@ int server_supports(const char *feature)\n \treturn !!server_feature_value(feature, NULL);\n }\n \n-enum url_scheme {\n-\tURL_SCHEME_LOCAL = 1,\n-\tURL_SCHEME_FILE,\n-\tURL_SCHEME_SSH,\n-\tURL_SCHEME_GIT\n-};\n-\n static const char *url_scheme_name(enum url_scheme scheme)\n {\n \tswitch (scheme) {\n@@ -722,21 +715,6 @@ static const char *url_scheme_name(enum url_scheme scheme)\n \t}\n }\n \n-static enum url_scheme url_get_scheme(const char *name)\n-{\n-\tif (!strcmp(name, \"ssh\"))\n-\t\treturn URL_SCHEME_SSH;\n-\tif (!strcmp(name, \"git\"))\n-\t\treturn URL_SCHEME_GIT;\n-\tif (!strcmp(name, \"git+ssh\")) /* deprecated - do not use */\n-\t\treturn URL_SCHEME_SSH;\n-\tif (!strcmp(name, \"ssh+git\")) /* deprecated - do not use */\n-\t\treturn URL_SCHEME_SSH;\n-\tif (!strcmp(name, \"file\"))\n-\t\treturn URL_SCHEME_FILE;\n-\tdie(_(\"protocol '%s' is not supported\"), name);\n-}\n-\n static char *host_end(char **hoststart, int removebrackets)\n {\n \tchar *host = *hoststart;\ndiff --git a/url.c b/url.c\nindex 057576042a..300acf98fe 100644\n--- a/url.c\n+++ b/url.c\n@@ -1,4 +1,5 @@\n #include \"git-compat-util.h\"\n+#include \"gettext.h\"\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"url.h\"\n@@ -140,3 +141,18 @@ int url_is_local_not_ssh(const char *url)\n \treturn !colon || (slash && slash < colon) ||\n \t\t(has_dos_drive_prefix(url) && is_valid_path(url));\n }\n+\n+enum url_scheme url_get_scheme(const char *name)\n+{\n+\tif (!strcmp(name, \"ssh\"))\n+\t\treturn URL_SCHEME_SSH;\n+\tif (!strcmp(name, \"git\"))\n+\t\treturn URL_SCHEME_GIT;\n+\tif (!strcmp(name, \"git+ssh\")) /* deprecated - do not use */\n+\t\treturn URL_SCHEME_SSH;\n+\tif (!strcmp(name, \"ssh+git\")) /* deprecated - do not use */\n+\t\treturn URL_SCHEME_SSH;\n+\tif (!strcmp(name, \"file\"))\n+\t\treturn URL_SCHEME_FILE;\n+\tdie(_(\"protocol '%s' is not supported\"), name);\n+}\ndiff --git a/url.h b/url.h\nindex 39d621312f..24c8cd91d0 100644\n--- a/url.h\n+++ b/url.h\n@@ -23,6 +23,19 @@ void str_end_url_with_slash(const char *url, char **dest);\n \n int url_is_local_not_ssh(const char *url);\n \n+enum url_scheme {\n+\tURL_SCHEME_LOCAL = 1,\n+\tURL_SCHEME_FILE,\n+\tURL_SCHEME_SSH,\n+\tURL_SCHEME_GIT,\n+};\n+\n+/*\n+ * Identify the URL scheme by name. Dies if the name does not match\n+ * any scheme that Git knows about.\n+ */\n+enum url_scheme url_get_scheme(const char *name);\n+\n /*\n  * The set of unreserved characters as per STD66 (RFC3986) is\n  * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n-- \ngitgitgadget\n\n"},{"id":"542574","messageId":"7381704c3809deec3adf1442a920f47840e0f104.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 4/8] url: return URL_SCHEME_UNKNOWN instead of dying","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:38Z","receivedAt":"2026-05-02T05:28:50Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nEnumerate a URL_SCHEME_UNKNOWN result with value 0.\nHave url_get_scheme() return it for unrecognized\nschemes instead of calling die() itself.\nMove the die() call to parse_connect_url()\nwhere url_get_scheme() is used.\n\nThis lets url_get_scheme() be used from contexts\nthat need to identify a URL's scheme without aborting\nthe program. For example, a future plumbing command\nthat validates URLs.\n\nNo external behavior change. parse_connect_url() still dies\nwith the same translated message for unrecognized schemes.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n connect.c | 2 ++\n url.c     | 3 +--\n url.h     | 7 ++++---\n 3 files changed, 7 insertions(+), 5 deletions(-)\n\ndiff --git a/connect.c b/connect.c\nindex 1ac7acc6e8..73d7a6b8d0 100644\n--- a/connect.c\n+++ b/connect.c\n@@ -1071,6 +1071,8 @@ static enum url_scheme parse_connect_url(const char *url_orig, char **ret_host,\n \tif (host) {\n \t\t*host = '\\0';\n \t\tscheme = url_get_scheme(url);\n+\t\tif (scheme == URL_SCHEME_UNKNOWN)\n+\t\t\tdie(_(\"protocol '%s' is not supported\"), url);\n \t\thost += 3;\n \t} else {\n \t\thost = url;\ndiff --git a/url.c b/url.c\nindex 300acf98fe..a59818278f 100644\n--- a/url.c\n+++ b/url.c\n@@ -1,5 +1,4 @@\n #include \"git-compat-util.h\"\n-#include \"gettext.h\"\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"url.h\"\n@@ -154,5 +153,5 @@ enum url_scheme url_get_scheme(const char *name)\n \t\treturn URL_SCHEME_SSH;\n \tif (!strcmp(name, \"file\"))\n \t\treturn URL_SCHEME_FILE;\n-\tdie(_(\"protocol '%s' is not supported\"), name);\n+\treturn URL_SCHEME_UNKNOWN;\n }\ndiff --git a/url.h b/url.h\nindex 24c8cd91d0..7289523605 100644\n--- a/url.h\n+++ b/url.h\n@@ -24,15 +24,16 @@ void str_end_url_with_slash(const char *url, char **dest);\n int url_is_local_not_ssh(const char *url);\n \n enum url_scheme {\n-\tURL_SCHEME_LOCAL = 1,\n+\tURL_SCHEME_UNKNOWN = 0,\n+\tURL_SCHEME_LOCAL,\n \tURL_SCHEME_FILE,\n \tURL_SCHEME_SSH,\n \tURL_SCHEME_GIT,\n };\n \n /*\n- * Identify the URL scheme by name. Dies if the name does not match\n- * any scheme that Git knows about.\n+ * Identify the URL scheme by name. Returns URL_SCHEME_UNKNOWN\n+ * if the name does not match any scheme that Git knows about.\n  */\n enum url_scheme url_get_scheme(const char *name);\n \n-- \ngitgitgadget\n\n"},{"id":"542575","messageId":"89932a70f3ace6ff1198628873df702f40f1442a.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 5/8] urlmatch: define url_parse function","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:39Z","receivedAt":"2026-05-02T05:28:51Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nDefine url_parse, a general parsing function that supports all Git URLs\nincluding scp style URLs such as hostname:~user/repo.\n\nIt is adapted from the algorithm in connect.c's parse_connect_url\nand reuses the shared enum url_scheme and url_get_scheme function\nthat previous commits made available in url.h. The new parser and\nthe connect path agree on scheme classification. url_parse has the\nsame interface as url_normalize and uses the same data structures.\n\nBoth functions accept the same URL forms with one deliberate\nexception. Bare local paths such as \"/abs/path\", \"./rel\"\nor \"repo\" are accepted by parse_connect_url as URL_SCHEME_LOCAL,\nbut rejected by url_parse because url_normalize requires a URL\nwith a scheme://host form. A consumer that wants to handle both\nURLs and local paths needs to dispatch on url_is_local_not_ssh\nbefore calling url_parse, just as the connect path does internally.\n\nThe duplication with parse_connect_url is intentional.\nThe two functions have different contracts:\n\n  - parse_connect_url\n\n    Calls die() on an unknown scheme\n    and returns NUL-terminated host/path\n    strings for the connect path\n\n  - url_parse\n\n    Returns NULL on failure while populating\n    out_info->err, and exposes components\n    as offset/length pairs into the normalized\n    URL buffer, matching url_normalize.\n\nReconciling both is possible, but not in the scope\nof the current patch set.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n t/unit-tests/u-urlmatch-normalization.c |  45 +++++++++\n urlmatch.c                              | 127 ++++++++++++++++++++++++\n urlmatch.h                              |   1 +\n 3 files changed, 173 insertions(+)\n\ndiff --git a/t/unit-tests/u-urlmatch-normalization.c b/t/unit-tests/u-urlmatch-normalization.c\nindex 39f6e1ba26..3595d893a2 100644\n--- a/t/unit-tests/u-urlmatch-normalization.c\n+++ b/t/unit-tests/u-urlmatch-normalization.c\n@@ -245,3 +245,48 @@ void test_urlmatch_normalization__equivalents(void)\n \tcompare_normalized_urls(\"https://@x.y/^/../abc\", \"httpS://@x.y:0443/abc\", 1);\n \tcompare_normalized_urls(\"https://@x.y/^/..\", \"httpS://@x.y:0443/\", 1);\n }\n+\n+static void check_parsed_path(const char *url, const char *expected_path)\n+{\n+\tstruct url_info info;\n+\tchar *parsed = url_parse(url, &info);\n+\tchar *path;\n+\n+\tcl_assert(parsed != NULL);\n+\tpath = xstrndup(parsed + info.path_off, info.path_len);\n+\tcl_assert_equal_s(path, expected_path);\n+\tfree(path);\n+\tfree(parsed);\n+}\n+\n+void test_urlmatch_normalization__parse_scp(void)\n+{\n+\tcheck_parsed_path(\"host:path\", \"/path\");\n+\tcheck_parsed_path(\"user@host:path\", \"/path\");\n+\tcheck_parsed_path(\"host:~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"user@host:~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"[host]:src\", \"/src\");\n+\tcheck_parsed_path(\"[host:123]:src\", \"/src\");\n+\tcheck_parsed_path(\"[::1]:repo\", \"/repo\");\n+\tcheck_parsed_path(\"user@[::1]:repo\", \"/repo\");\n+}\n+\n+void test_urlmatch_normalization__parse_url_form(void)\n+{\n+\tcheck_parsed_path(\"ssh://host/repo\", \"/repo\");\n+\tcheck_parsed_path(\"ssh://host/~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"git://host:9418/repo\", \"/repo\");\n+\tcheck_parsed_path(\"git://host/~user/repo\", \"~user/repo\");\n+\tcheck_parsed_path(\"ssh://[::1]:1234/repo\", \"/repo\");\n+\tcheck_parsed_path(\"http://[2001:db8::1]/repo\", \"/repo\");\n+}\n+\n+void test_urlmatch_normalization__parse_strips_query_and_fragment(void)\n+{\n+\tcheck_parsed_path(\"ssh://host/~user/repo?q\", \"~user/repo\");\n+\tcheck_parsed_path(\"ssh://host/~user/repo#frag\", \"~user/repo\");\n+\tcheck_parsed_path(\"git://host/~user/repo?q\", \"~user/repo\");\n+\tcheck_parsed_path(\"user@host:~user/repo?q\", \"~user/repo\");\n+\tcheck_parsed_path(\"https://host/repo?q\", \"/repo\");\n+\tcheck_parsed_path(\"https://host/repo#frag\", \"/repo\");\n+}\ndiff --git a/urlmatch.c b/urlmatch.c\nindex eea8300489..bf8cce6de9 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -5,6 +5,7 @@\n #include \"hex-ll.h\"\n #include \"strbuf.h\"\n #include \"urlmatch.h\"\n+#include \"url.h\"\n \n #define URL_ALPHA \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n #define URL_DIGIT \"0123456789\"\n@@ -440,6 +441,132 @@ char *url_normalize(const char *url, struct url_info *out_info)\n \treturn url_normalize_1(url, out_info, 0);\n }\n \n+char *url_parse(const char *url_orig, struct url_info *out_info)\n+{\n+\tstruct strbuf url;\n+\tchar *host, *separator;\n+\tchar *detached, *normalized;\n+\tchar *url_decoded;\n+\tenum url_scheme scheme = URL_SCHEME_LOCAL;\n+\tstruct url_info local_info;\n+\tstruct url_info *info = out_info ? out_info : &local_info;\n+\tbool scp_syntax = false;\n+\n+\tif (is_url(url_orig))\n+\t\turl_decoded = url_decode(url_orig);\n+\telse\n+\t\turl_decoded = xstrdup(url_orig);\n+\n+\tstrbuf_init(&url, strlen(url_decoded) + sizeof(\"ssh://\"));\n+\tstrbuf_addstr(&url, url_decoded);\n+\tfree(url_decoded);\n+\n+\thost = strstr(url.buf, \"://\");\n+\tif (host) {\n+\t\t/*\n+\t\t * Temporarily NUL-terminate the scheme name\n+\t\t * so we can pass it to url_get_scheme(),\n+\t\t * then restore the ':' so the buffer\n+\t\t * is intact for url_normalize() below.\n+\t\t */\n+\t\tchar saved = *host;\n+\t\t*host = '\\0';\n+\t\tscheme = url_get_scheme(url.buf);\n+\t\t*host = saved;\n+\t\thost += 3;\n+\t} else {\n+\t\tif (!url_is_local_not_ssh(url.buf)) {\n+\t\t\tscp_syntax = true;\n+\t\t\tscheme = URL_SCHEME_SSH;\n+\t\t\tstrbuf_insertstr(&url, 0, \"ssh://\");\n+\t\t\thost = url.buf + strlen(\"ssh://\");\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Path starts after ':' in scp style SSH URLs.\n+\t *\n+\t * The host portion can begin with an optional \"user@\",\n+\t * and the host itself can be wrapped in '[' ']' brackets.\n+\t * The bracket form is git's legacy way of supporting:\n+\t *\n+\t *   - IPv6 literals: [::1]:repo\n+\t *   - host:port pairs in the short form: [myhost:123]:src\n+\t *   - Plain hostnames that happen to need bracketing: [host]:path\n+\t *\n+\t * Treat '[' followed by 0 or 1 inner colons as the host:port\n+\t * or plain hostname form and strip the brackets so url_normalize\n+\t * sees host[:port] natively. Two or more inner colons mark an\n+\t * IPv6 literal: keep the brackets for url_normalize to recognize.\n+\t *\n+\t * The scp path separator is the ':' that follows the host part,\n+\t * and we must skip over user@ and any '[...]' before searching.\n+\t */\n+\tif (scp_syntax) {\n+\t\tchar *user_at;\n+\t\tchar *host_start;\n+\t\tchar *bracket_end;\n+\n+\t\tuser_at = strchr(host, '@');\n+\t\thost_start = user_at ? user_at + 1 : host;\n+\n+\t\tif (*host_start == '[') {\n+\t\t\tchar *p;\n+\t\t\tint inner_colons;\n+\n+\t\t\tbracket_end = strchr(host_start, ']');\n+\t\t\tinner_colons = 0;\n+\t\t\tfor (p = host_start + 1; bracket_end && p < bracket_end; p++)\n+\t\t\t\tif (*p == ':')\n+\t\t\t\t\tinner_colons++;\n+\n+\t\t\tif (bracket_end && inner_colons <= 1) {\n+\t\t\t\tsize_t close_off = bracket_end - url.buf;\n+\t\t\t\tsize_t open_off = host_start - url.buf;\n+\t\t\t\tstrbuf_remove(&url, close_off, 1);\n+\t\t\t\tstrbuf_remove(&url, open_off, 1);\n+\t\t\t\tseparator = url.buf + close_off - 1;\n+\t\t\t} else if (bracket_end) {\n+\t\t\t\tseparator = strchr(bracket_end + 1, ':');\n+\t\t\t} else {\n+\t\t\t\tseparator = strchr(host_start, ':');\n+\t\t\t}\n+\t\t} else {\n+\t\t\tseparator = strchr(host_start, ':');\n+\t\t}\n+\n+\t\tif (separator) {\n+\t\t\tif (separator[1] == '/')\n+\t\t\t\tstrbuf_remove(&url, separator - url.buf, 1);\n+\t\t\telse\n+\t\t\t\t*separator = '/';\n+\t\t}\n+\t}\n+\n+\tdetached = strbuf_detach(&url, NULL);\n+\tnormalized = url_normalize(detached, info);\n+\tfree(detached);\n+\n+\tif (!normalized)\n+\t\treturn NULL;\n+\n+\t/*\n+\t * Point path to ~ for URLs like this:\n+\t *\n+\t *     ssh://host.xz/~user/repo\n+\t *     git://host.xz/~user/repo\n+\t *     host.xz:~user/repo\n+\t */\n+\tif (scheme == URL_SCHEME_GIT || scheme == URL_SCHEME_SSH) {\n+\t\tif (normalized[info->path_off + 1] == '~') {\n+\t\t\tinfo->path_off++;\n+\t\t\tinfo->path_len--;\n+\t\t}\n+\t}\n+\n+\treturn normalized;\n+}\n+\n static size_t url_match_prefix(const char *url,\n \t\t\t       const char *url_prefix,\n \t\t\t       size_t url_prefix_len)\ndiff --git a/urlmatch.h b/urlmatch.h\nindex 5ba85cea13..6b3ce42858 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -35,6 +35,7 @@ struct url_info {\n };\n \n char *url_normalize(const char *, struct url_info *);\n+char *url_parse(const char *, struct url_info *);\n \n struct urlmatch_item {\n \tsize_t hostmatch_len;\n-- \ngitgitgadget\n\n"},{"id":"542576","messageId":"af6c71227b795d59916e0383ec053067f77835fb.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 6/8] builtin: create url-parse command","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:40Z","receivedAt":"2026-05-02T05:28:52Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nGit commands can accept a rather wide variety of URLs syntaxes.\nThe range of accepted inputs might expand even more in the future.\nThis makes the parsing of URL components difficult since standard URL\nparsers cannot be used. Extracting the components of a git URL would\nrequire implementing all the schemes that git itself supports, not to\nmention tracking its development continuously in case new URL schemes\nare added.\n\nThe url-parse builtin command is designed to solve this problem\nby exposing git's native URL parsing facilities as a plumbing command.\nOther programs can then call upon git itself to parse the git URLs\nand extract their components. This should be quite useful for scripts.\n\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n .gitignore          |   1 +\n Makefile            |   1 +\n builtin.h           |   1 +\n builtin/url-parse.c | 135 ++++++++++++++++++++++++++++++++++++++++++++\n command-list.txt    |   1 +\n git.c               |   1 +\n meson.build         |   1 +\n 7 files changed, 141 insertions(+)\n create mode 100644 builtin/url-parse.c\n\ndiff --git a/.gitignore b/.gitignore\nindex 24635cf2d6..c5673daa6e 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -182,6 +182,7 @@\n /git-update-server-info\n /git-upload-archive\n /git-upload-pack\n+/git-url-parse\n /git-var\n /git-verify-commit\n /git-verify-pack\ndiff --git a/Makefile b/Makefile\nindex cedc234173..1c757a1aa0 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1497,6 +1497,7 @@ BUILTIN_OBJS += builtin/update-ref.o\n BUILTIN_OBJS += builtin/update-server-info.o\n BUILTIN_OBJS += builtin/upload-archive.o\n BUILTIN_OBJS += builtin/upload-pack.o\n+BUILTIN_OBJS += builtin/url-parse.o\n BUILTIN_OBJS += builtin/var.o\n BUILTIN_OBJS += builtin/verify-commit.o\n BUILTIN_OBJS += builtin/verify-pack.o\ndiff --git a/builtin.h b/builtin.h\nindex 235c51f30e..c6f7672991 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -271,6 +271,7 @@ int cmd_update_server_info(int argc, const char **argv, const char *prefix, stru\n int cmd_upload_archive(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_upload_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_url_parse(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_var(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_verify_commit(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_verify_tag(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/url-parse.c b/builtin/url-parse.c\nnew file mode 100644\nindex 0000000000..7e705538c0\n--- /dev/null\n+++ b/builtin/url-parse.c\n@@ -0,0 +1,135 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+#include \"url.h\"\n+#include \"urlmatch.h\"\n+\n+static const char * const builtin_url_parse_usage[] = {\n+\tN_(\"git url-parse [-c <component>] [--] <url>...\"),\n+\tNULL\n+};\n+\n+static char *component_arg;\n+\n+static struct option builtin_url_parse_options[] = {\n+\tOPT_STRING('c', \"component\", &component_arg, N_(\"component\"),\n+\t\tN_(\"which URL component to extract\")),\n+\tOPT_END(),\n+};\n+\n+enum url_component {\n+\tURL_NONE = 0,\n+\tURL_SCHEME,\n+\tURL_USER,\n+\tURL_PASSWORD,\n+\tURL_HOST,\n+\tURL_PORT,\n+\tURL_PATH,\n+};\n+\n+static void parse_or_die(const char *url, struct url_info *info)\n+{\n+\tif (url_is_local_not_ssh(url)) {\n+\t\tif (*url == '/')\n+\t\t\tdie(\"'%s' is not a URL; if you meant a local \"\n+\t\t\t    \"repository, use 'file://%s'\", url, url);\n+\t\tif (has_dos_drive_prefix(url))\n+\t\t\tdie(\"'%s' is not a URL; if you meant a local \"\n+\t\t\t    \"repository, use 'file:///%s'\", url, url);\n+\t\tdie(\"'%s' is not a URL; if you meant a local repository, \"\n+\t\t    \"use a 'file://' URL with an absolute path\", url);\n+\t}\n+\tif (!url_parse(url, info))\n+\t\tdie(\"invalid git URL '%s': %s\", url, info->err);\n+}\n+\n+static enum url_component get_component_or_die(const char *arg)\n+{\n+\tif (!strcmp(\"path\", arg))\n+\t\treturn URL_PATH;\n+\tif (!strcmp(\"host\", arg))\n+\t\treturn URL_HOST;\n+\tif (!strcmp(\"scheme\", arg))\n+\t\treturn URL_SCHEME;\n+\tif (!strcmp(\"user\", arg))\n+\t\treturn URL_USER;\n+\tif (!strcmp(\"password\", arg))\n+\t\treturn URL_PASSWORD;\n+\tif (!strcmp(\"port\", arg))\n+\t\treturn URL_PORT;\n+\tdie(\"invalid git URL component '%s'\", arg);\n+}\n+\n+static char *extract_component(enum url_component component,\n+\t\t\t       struct url_info *info)\n+{\n+\tsize_t offset, length;\n+\n+\tswitch (component) {\n+\tcase URL_SCHEME:\n+\t\toffset = 0;\n+\t\tlength = info->scheme_len;\n+\t\tbreak;\n+\tcase URL_USER:\n+\t\toffset = info->user_off;\n+\t\tlength = info->user_len;\n+\t\tbreak;\n+\tcase URL_PASSWORD:\n+\t\toffset = info->passwd_off;\n+\t\tlength = info->passwd_len;\n+\t\tbreak;\n+\tcase URL_HOST:\n+\t\toffset = info->host_off;\n+\t\tlength = info->host_len;\n+\t\tbreak;\n+\tcase URL_PORT:\n+\t\toffset = info->port_off;\n+\t\tlength = info->port_len;\n+\t\tbreak;\n+\tcase URL_PATH:\n+\t\toffset = info->path_off;\n+\t\tlength = info->path_len;\n+\t\tbreak;\n+\tcase URL_NONE:\n+\t\treturn NULL;\n+\t}\n+\n+\treturn xstrndup(info->url + offset, length);\n+}\n+\n+int cmd_url_parse(int argc,\n+\t\t  const char **argv,\n+\t\t  const char *prefix,\n+\t\t  struct repository *repo UNUSED)\n+{\n+\tstruct url_info info;\n+\tenum url_component selected = URL_NONE;\n+\tchar *extracted;\n+\tint i;\n+\n+\targc = parse_options(argc, argv, prefix, builtin_url_parse_options,\n+\t\t\t     builtin_url_parse_usage, 0);\n+\n+\tif (argc == 0)\n+\t\tusage_with_options(builtin_url_parse_usage,\n+\t\t\t\t   builtin_url_parse_options);\n+\n+\tif (component_arg)\n+\t\tselected = get_component_or_die(component_arg);\n+\n+\tfor (i = 0; i < argc; i++) {\n+\t\tparse_or_die(argv[i], &info);\n+\n+\t\tif (selected != URL_NONE) {\n+\t\t\textracted = extract_component(selected, &info);\n+\t\t\tif (extracted) {\n+\t\t\t\tputs(extracted);\n+\t\t\t\tfree(extracted);\n+\t\t\t}\n+\t\t}\n+\n+\t\tfree(info.url);\n+\t}\n+\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex f9005cf459..1ede48186f 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -202,6 +202,7 @@ git-update-ref                          plumbingmanipulators\n git-update-server-info                  synchingrepositories\n git-upload-archive                      synchelpers\n git-upload-pack                         synchelpers\n+git-url-parse                           purehelpers\n git-var                                 plumbinginterrogators\n git-verify-commit                       ancillaryinterrogators\n git-verify-pack                         plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 5a40eab8a2..a073eed931 100644\n--- a/git.c\n+++ b/git.c\n@@ -670,6 +670,7 @@ static struct cmd_struct commands[] = {\n \t{ \"upload-archive\", cmd_upload_archive, NO_PARSEOPT },\n \t{ \"upload-archive--writer\", cmd_upload_archive_writer, NO_PARSEOPT },\n \t{ \"upload-pack\", cmd_upload_pack },\n+\t{ \"url-parse\", cmd_url_parse },\n \t{ \"var\", cmd_var, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"verify-commit\", cmd_verify_commit, RUN_SETUP },\n \t{ \"verify-pack\", cmd_verify_pack },\ndiff --git a/meson.build b/meson.build\nindex 11488623bf..dc3cf68ee5 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -686,6 +686,7 @@ builtin_sources = [\n   'builtin/update-server-info.c',\n   'builtin/upload-archive.c',\n   'builtin/upload-pack.c',\n+  'builtin/url-parse.c',\n   'builtin/var.c',\n   'builtin/verify-commit.c',\n   'builtin/verify-pack.c',\n-- \ngitgitgadget\n\n"},{"id":"542577","messageId":"2b32cb71a359b936095f41e7c482e8369ddac786.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 7/8] doc: describe the url-parse builtin","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:41Z","receivedAt":"2026-05-02T05:28:53Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nThe new url-parse builtin validates git URLs\nand optionally extracts their components.\n\nHelped-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n Documentation/git-url-parse.adoc | 80 ++++++++++++++++++++++++++++++++\n Documentation/meson.build        |  1 +\n 2 files changed, 81 insertions(+)\n create mode 100644 Documentation/git-url-parse.adoc\n\ndiff --git a/Documentation/git-url-parse.adoc b/Documentation/git-url-parse.adoc\nnew file mode 100644\nindex 0000000000..9d0d93da4a\n--- /dev/null\n+++ b/Documentation/git-url-parse.adoc\n@@ -0,0 +1,80 @@\n+git-url-parse(1)\n+================\n+\n+NAME\n+----\n+git-url-parse - Parse and extract git URL components\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git url-parse [-c <component>] [--] <url>...\n+\n+DESCRIPTION\n+-----------\n+\n+Git supports many ways to specify URLs, some of them non-standard.\n+For example, git supports the scp style [user@]host:[path] format.\n+This command eases interoperability with git URLs by enabling the\n+parsing and extraction of the components of all git URLs.\n+\n+Any syntactically valid URL is parsed, even if the scheme is not one\n+git supports for fetching or pushing.\n+\n+OPTIONS\n+-------\n+\n+`-c <component>`::\n+`--component <component>`::\n+\tExtract the _<component>_ component from the given Git URLs.\n+\t_<component>_ can be one of:\n+\t`scheme`, `user`, `password`, `host`, `port`, `path`.\n+\n+OUTPUT\n+------\n+\n+When `--component` is given, the requested component of each URL\n+is printed on its own line, in the order the URLs were given. If\n+the URL has no such component (for example, a port in a URL that\n+does not specify one), an empty line is printed in its place.\n+\n+When `--component` is not given, no output is produced. The exit\n+status is zero if every URL parses successfully and non-zero\n+otherwise, allowing the command to be used purely as a validator.\n+\n+EXAMPLES\n+--------\n+\n+* Print the host name:\n++\n+------------\n+$ git url-parse --component host https://example.com/user/repo\n+example.com\n+------------\n+\n+* Print the path:\n++\n+------------\n+$ git url-parse --component path https://example.com/user/repo\n+/user/repo\n+$ git url-parse --component path example.com:~user/repo\n+~user/repo\n+$ git url-parse --component path example.com:user/repo\n+/user/repo\n+------------\n+\n+* Validate URLs without outputting anything:\n++\n+------------\n+$ git url-parse https://example.com/user/repo example.com:~user/repo\n+------------\n+\n+SEE ALSO\n+--------\n+linkgit:git-clone[1],\n+linkgit:git-fetch[1],\n+linkgit:git-config[1]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex d6365b888b..32c8606a80 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -155,6 +155,7 @@ manpages = {\n   'git-update-server-info.adoc' : 1,\n   'git-upload-archive.adoc' : 1,\n   'git-upload-pack.adoc' : 1,\n+  'git-url-parse.adoc' : 1,\n   'git-var.adoc' : 1,\n   'git-verify-commit.adoc' : 1,\n   'git-verify-pack.adoc' : 1,\n-- \ngitgitgadget\n\n"},{"id":"542578","messageId":"ce41d2ec50effb277246c459d328a9d5d720ed3a.1777699722.git.gitgitgadget@gmail.com","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"[PATCH v3 8/8] t9904: add tests for the new url-parse builtin","fromName":"Matheus Afonso Martins Moreira via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-05-02T05:28:42Z","receivedAt":"2026-05-02T05:28:55Z","isPatch":true,"body":"From: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n\nTest git URL parsing, validation and component extraction\non all documented git URL schemes and syntaxes.\n\nAdd IPv6 host coverage in URL form:\n\n    ssh://[::1]/path\n    ssh://user@[::1]:1234/path\n    git://[::1]:9418/path\n    http://[2001:db8::1]/path\n    https://[2001:db8::1]/path\n\nIn URL form the brackets are kept in the host component (RFC 3986\nsyntax for IPv6 literals).\n\nAlso exercise the bracketed scp short forms that t5601-clone.sh\ncovers via parse_connect_url:\n\n    [host]:path\n    [host:port]:path\n    [::1]:repo\n    user@[::1]:repo\n    user@[host:port]:path\n\nIn scp form, brackets are kept for IPv6 literals (two or more inner\ncolons) and stripped for plain hostnames or host:port pairs.\n\nSuggested-by: Torsten Bögershausen <tboegi@web.de>\nSigned-off-by: Matheus Afonso Martins Moreira <matheus@matheusmoreira.com>\n---\n t/meson.build        |   1 +\n t/t9904-url-parse.sh | 319 +++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 320 insertions(+)\n create mode 100755 t/t9904-url-parse.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 7528e5cda5..41b389a472 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -1114,6 +1114,7 @@ integration_tests = [\n   't9901-git-web--browse.sh',\n   't9902-completion.sh',\n   't9903-bash-prompt.sh',\n+  't9904-url-parse.sh',\n ]\n \n benchmarks = [\ndiff --git a/t/t9904-url-parse.sh b/t/t9904-url-parse.sh\nnew file mode 100755\nindex 0000000000..8a369d2040\n--- /dev/null\n+++ b/t/t9904-url-parse.sh\n@@ -0,0 +1,319 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2024 Matheus Afonso Martins Moreira\n+#\n+\n+test_description='git url-parse tests'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git url-parse -- ssh syntax' '\n+\tgit url-parse \"ssh://user@example.com:1234/repository/path\" &&\n+\tgit url-parse \"ssh://user@example.com/repository/path\" &&\n+\tgit url-parse \"ssh://example.com:1234/repository/path\" &&\n+\tgit url-parse \"ssh://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- git syntax' '\n+\tgit url-parse \"git://example.com:1234/repository/path\" &&\n+\tgit url-parse \"git://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- http syntax' '\n+\tgit url-parse \"https://example.com:1234/repository/path\" &&\n+\tgit url-parse \"https://example.com/repository/path\" &&\n+\tgit url-parse \"http://example.com:1234/repository/path\" &&\n+\tgit url-parse \"http://example.com/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax' '\n+\tgit url-parse \"user@example.com:/repository/path\" &&\n+\tgit url-parse \"example.com:/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - ssh syntax' '\n+\tgit url-parse \"ssh://user@example.com:1234/~user/repository\" &&\n+\tgit url-parse \"ssh://user@example.com/~user/repository\" &&\n+\tgit url-parse \"ssh://example.com:1234/~user/repository\" &&\n+\tgit url-parse \"ssh://example.com/~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - git syntax' '\n+\tgit url-parse \"git://example.com:1234/~user/repository\" &&\n+\tgit url-parse \"git://example.com/~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- username expansion - scp syntax' '\n+\tgit url-parse \"user@example.com:~user/repository\" &&\n+\tgit url-parse \"example.com:~user/repository\"\n+'\n+\n+test_expect_success 'git url-parse -- file urls' '\n+\tgit url-parse \"file:///repository/path\" &&\n+\tgit url-parse \"file://\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- ssh syntax' '\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://user@example.com/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- git syntax' '\n+\ttest git = \"$(git url-parse -c scheme \"git://example.com:1234/repository/path\")\" &&\n+\ttest git = \"$(git url-parse -c scheme \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- http syntax' '\n+\ttest https = \"$(git url-parse -c scheme \"https://example.com:1234/repository/path\")\" &&\n+\ttest https = \"$(git url-parse -c scheme \"https://example.com/repository/path\")\" &&\n+\ttest http = \"$(git url-parse -c scheme \"http://example.com:1234/repository/path\")\" &&\n+\ttest http = \"$(git url-parse -c scheme \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c scheme -- scp syntax' '\n+\ttest ssh = \"$(git url-parse -c scheme \"user@example.com:/repository/path\")\" &&\n+\ttest ssh = \"$(git url-parse -c scheme \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- ssh syntax' '\n+\ttest user = \"$(git url-parse -c user \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest user = \"$(git url-parse -c user \"ssh://user@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- git syntax' '\n+\ttest \"\" = \"$(git url-parse -c user \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- http syntax' '\n+\ttest \"\" = \"$(git url-parse -c user \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"https://example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- scp syntax' '\n+\ttest user = \"$(git url-parse -c user \"user@example.com:/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c user \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c password -- http syntax' '\n+\ttest secret = \"$(git url-parse -c password \"https://user:secret@example.com:1234/repository/path\")\" &&\n+\ttest secret = \"$(git url-parse -c password \"http://user:secret@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c password \"https://user@example.com/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c password \"https://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- ssh syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://user@example.com/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- git syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"git://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- http syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"https://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"https://example.com/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"http://example.com:1234/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- scp syntax' '\n+\ttest example.com = \"$(git url-parse -c host \"user@example.com:/repository/path\")\" &&\n+\ttest example.com = \"$(git url-parse -c host \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- ssh syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://user@example.com/repository/path\")\" &&\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- git syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- http syntax' '\n+\ttest 1234 = \"$(git url-parse -c port \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"https://example.com/repository/path\")\" &&\n+\ttest 1234 = \"$(git url-parse -c port \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- scp syntax' '\n+\ttest \"\" = \"$(git url-parse -c port \"user@example.com:/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- ssh syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://user@example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://user@example.com/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"ssh://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- git syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"git://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"git://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- http syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"https://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"https://example.com/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"http://example.com:1234/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"http://example.com/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- scp syntax' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"user@example.com:/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"example.com:/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - ssh syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://user@example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://user@example.com/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - git syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com:1234/~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com/~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion - scp syntax' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"user@example.com:~user/repository\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"example.com:~user/repository\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- username expansion strips query and fragment' '\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository?query\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"ssh://example.com/~user/repository#fragment\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"git://example.com/~user/repository?query\")\" &&\n+\ttest \"~user/repository\" = \"$(git url-parse -c path \"user@example.com:~user/repository?query\")\"\n+'\n+\n+test_expect_success 'git url-parse -- ssh syntax with IPv6' '\n+\tgit url-parse \"ssh://user@[::1]:1234/repository/path\" &&\n+\tgit url-parse \"ssh://user@[::1]/repository/path\" &&\n+\tgit url-parse \"ssh://[::1]:1234/repository/path\" &&\n+\tgit url-parse \"ssh://[::1]/repository/path\" &&\n+\tgit url-parse \"ssh://[2001:db8::1]/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- git syntax with IPv6' '\n+\tgit url-parse \"git://[::1]:9418/repository/path\" &&\n+\tgit url-parse \"git://[::1]/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -- http syntax with IPv6' '\n+\tgit url-parse \"https://[::1]:1234/repository/path\" &&\n+\tgit url-parse \"https://[::1]/repository/path\" &&\n+\tgit url-parse \"http://[2001:db8::1]/repository/path\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- IPv6 in URL form' '\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"ssh://user@[::1]:1234/repository/path\")\" &&\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"ssh://[::1]/repository/path\")\" &&\n+\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"ssh://[2001:db8::1]/repository/path\")\" &&\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"git://[::1]/repository/path\")\" &&\n+\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"https://[2001:db8::1]/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -c port -- IPv6 in URL form' '\n+\ttest 1234 = \"$(git url-parse -c port \"ssh://user@[::1]:1234/repository/path\")\" &&\n+\ttest \"\" = \"$(git url-parse -c port \"ssh://[::1]/repository/path\")\" &&\n+\ttest 9418 = \"$(git url-parse -c port \"git://[::1]:9418/repository/path\")\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax with IPv6' '\n+\tgit url-parse \"[::1]:repository/path\" &&\n+\tgit url-parse \"user@[::1]:repository/path\" &&\n+\tgit url-parse \"[2001:db8::1]:repo\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax with bracketed hostname' '\n+\tgit url-parse \"[myhost]:src\" &&\n+\tgit url-parse \"user@[myhost]:src\"\n+'\n+\n+test_expect_success 'git url-parse -- scp syntax with bracketed host:port' '\n+\tgit url-parse \"[myhost:123]:src\" &&\n+\tgit url-parse \"user@[myhost:123]:src\"\n+'\n+\n+test_expect_success 'git url-parse -c host -- scp+IPv6' '\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"[::1]:repository/path\")\" &&\n+\ttest \"[::1]\" = \"$(git url-parse -c host \"user@[::1]:repository/path\")\" &&\n+\ttest \"[2001:db8::1]\" = \"$(git url-parse -c host \"[2001:db8::1]:repo\")\"\n+'\n+\n+test_expect_success 'git url-parse -c path -- scp+IPv6' '\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"[::1]:/repository/path\")\" &&\n+\ttest \"/repository/path\" = \"$(git url-parse -c path \"[::1]:repository/path\")\" &&\n+\ttest \"/repo\" = \"$(git url-parse -c path \"[2001:db8::1]:repo\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host,port,path -- scp [host:port]:src' '\n+\ttest myhost = \"$(git url-parse -c host \"[myhost:123]:src\")\" &&\n+\ttest 123 = \"$(git url-parse -c port \"[myhost:123]:src\")\" &&\n+\ttest \"/src\" = \"$(git url-parse -c path \"[myhost:123]:src\")\"\n+'\n+\n+test_expect_success 'git url-parse -c host,path -- scp [host]:src' '\n+\ttest myhost = \"$(git url-parse -c host \"[myhost]:src\")\" &&\n+\ttest \"/src\" = \"$(git url-parse -c path \"[myhost]:src\")\"\n+'\n+\n+test_expect_success 'git url-parse -c user -- scp with user@ and brackets' '\n+\ttest user = \"$(git url-parse -c user \"user@[::1]:repo\")\" &&\n+\ttest user = \"$(git url-parse -c user \"user@[myhost:123]:src\")\" &&\n+\ttest user = \"$(git url-parse -c user \"user@[myhost]:src\")\"\n+'\n+\n+test_expect_success 'git url-parse -- scp+IPv6 with username expansion' '\n+\ttest \"~user/repo\" = \"$(git url-parse -c path \"[::1]:~user/repo\")\" &&\n+\ttest \"~user/repo\" = \"$(git url-parse -c path \"user@[::1]:~user/repo\")\"\n+'\n+\n+test_expect_success 'git url-parse fails on invalid URL' '\n+\ttest_must_fail git url-parse \"not a url\"\n+'\n+\n+test_expect_success 'git url-parse helpful error for absolute local path' '\n+\ttest_must_fail git url-parse \"/abs/path\" 2>err &&\n+\ttest_grep \"is not a URL\" err &&\n+\ttest_grep \"file:///\" err\n+'\n+\n+test_expect_success 'git url-parse helpful error for relative local path' '\n+\ttest_must_fail git url-parse \"./rel\" 2>err &&\n+\ttest_grep \"is not a URL\" err &&\n+\ttest_grep \"absolute path\" err\n+'\n+\n+test_expect_success 'git url-parse fails on unknown -c component name' '\n+\ttest_must_fail git url-parse -c bogus \"https://example.com/repo\"\n+'\n+\n+test_expect_success 'git url-parse fails on URL missing host' '\n+\ttest_must_fail git url-parse \"https://\"\n+'\n+\n+test_expect_success 'git url-parse with no URL prints usage' '\n+\ttest_must_fail git url-parse 2>err &&\n+\ttest_grep \"usage:\" err\n+'\n+\n+test_done\n-- \ngitgitgadget\n"},{"id":"542597","messageId":"xmqqik95taoh.fsf@gitster.g","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-03T03:49:02Z","receivedAt":"2026-05-03T03:49:05Z","isPatch":true,"body":"\"Matheus Moreira via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> ... Tools wanting to reason\n> about them have historically had to reimplement git's parsing or shell out\n> indirectly. With git url-parse, scripts can ask git directly: validate a\n> URL, extract a component (scheme, user, host, port, path, password), or\n> both.\n\nNitpick.  With \"git url-parse\", these scripts has to do what they\ntraditionally have always done, i.e., shell out to the command, no?\n\n"},{"id":"542598","messageId":"b7155ff9d03be5b1baa1358fe8a5f434@matheusmoreira.com","threadId":"61375","inReplyTo":"xmqqik95taoh.fsf@gitster.g","subject":"Re: [PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Matheus Afonso Martins Moreira","fromEmail":"matheus@matheusmoreira.com","sentAt":"2026-05-03T04:29:38Z","receivedAt":"2026-05-03T04:29:46Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Nitpick.  With \"git url-parse\", these scripts has to do what they\n> traditionally have always done, i.e., shell out to the command, no?\n\nIt's a good point. I should have worded it better:\n\n  Tools wanting to reason about them have historically had to\n  reimplement git's parsing logic externally. With git url-parse,\n  scripts can delegate URL parsing to git's own parser: validate\n  a URL, extract a component (scheme, user, host, port, path,\n  password), or both.\n\nWhat I meant to say is that git's URL parsing was never exposed\nas a standalone operation, leading external tools to reimplement\nthe logic themselves.\n\nFor example:\n\n    npm/git-url-parse       millions of weekly downloads\n    crates/git-url-parse    half a million downloads\n\nWith this builtin, scripts can rely on git's own parser\ninstead of a third party reimplementation.\n\nReferences:\n    https://www.npmjs.com/package/git-url-parse\n    https://crates.io/crates/git-url-parse\n"},{"id":"542617","messageId":"20260503172838.GA22957@tb-raspi4","threadId":"61375","inReplyTo":"pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2026-05-03T17:28:38Z","receivedAt":"2026-05-03T17:28:50Z","isPatch":true,"body":"> \n> The series consists of eight commits.\n\nReviewers comment: Nicely done.\n\n> Changes since v2:\n> \n>  * Fix Windows CI failure: handle DOS drive prefix in the helpful local-path\n>    error. With this, the message for a drive-letter input like C:/repo (or\n>    an MSYS-mangled /abs/path that bash rewrites to D:/.../abs/path before\n>    git sees it) gets the specific file:///<input> suggestion rather than the\n>    generic fallback. No effect on Linux or macOS, since has_dos_drive_prefix\n>    is a no-op on non-Windows builds.\n> \n>  * t9904: relax the grep on the absolute-path test from the literal\n>    file:///abs/path to the structural file:/// (three slashes). The original\n>    assertion depended on the input being preserved verbatim, which MSYS does\n>    not do. The relaxed grep verifies the structurally meaningful property\n>    (specific URL suggestion was produced, not the generic fallback) and runs\n>    cross-platform.\n\nMore a question to myself, may be, about t9904 (and may be other parts)\nI have in mind that the parser learned to handle\n\nfile://server/share/repo\ncorrectly under Windows.\nI don't know if this needs to be addressed here or in a follow-up commit ?\nThe \\\\server\\share\\repo is an UNC name, which is handled by the\nWindows file system, backslashes towards windows must be used (which we do)\nand '/' may be used outside Git.\n\n\ncommit ebb8d2c90fb0840a0803935804e37e2205505f23\nAuthor: Torsten Bögershausen <tboegi@web.de>\nDate:   Sat Aug 24 15:07:59 2019 -0700\n\n    mingw: support UNC in git clone file://server/share/repo\n    \n    Extend the parser to accept file://server/share/repo in the way that\n    Windows users expect it to be parsed who are used to referring to file\n    shares by UNC paths of the form \\\\server\\share\\folder.\n    \n    [jes: tightened check to avoid handling file://C:/some/path as a UNC\n    path.]\n    \n    This closes https://github.com/git-for-windows/git/issues/1264.\n"},{"id":"542620","messageId":"6c0a1601cd379bcdc87b4fe3b854166a@matheusmoreira.com","threadId":"61375","inReplyTo":"20260503172838.GA22957@tb-raspi4","subject":"Re: [PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Matheus Afonso Martins Moreira","fromEmail":"matheus@matheusmoreira.com","sentAt":"2026-05-03T19:36:46Z","receivedAt":"2026-05-03T19:36:55Z","isPatch":true,"body":"> Reviewers comment: Nicely done.\n\nThank you!\n\n> More a question to myself, may be, about t9904 (and may be other parts)\n> I have in mind that the parser learned to handle\n>\n> file://server/share/repo\n> correctly under Windows.\n> I don't know if this needs to be addressed here or in a follow-up commit ?\n\nI'd be happy to revisit this in a follow-up. It's been a while\nsince I used MSYS but I do remember the fact it rewrites paths\ninternally. I wasn't sure how to handle it properly in the tests.\n\nThe problematic test case is:\n\n    test_must_fail git url-parse \"/abs/path\" 2>err &&\n      test_grep \"is not a URL\" err &&\n      test_grep \"file:///abs/path\" err\n\nMSYS bash rewrites /abs/path to C:/Program Files/Git/abs/path\nbefore git even runs. This edge case caused the error message:\n\n    fatal: 'C:/Program Files/Git/abs/path' is not a URL;\n    if you meant a local repository, use a 'file://' URL\n    with an absolute path\n\nThe test_grep \"is not a URL\" passed but test_grep \"file:///abs/path\"\nfailed because the suggestion did not contain the literal string\n\"file:///abs/path\". The drive letter broke the tool's absolute\npath recognition: it was printing the generic error message.\n\nThe fix was to use has_dos_drive_prefix() to recognize the edge case.\nHowever, that led to the generation of error messages containing paths\nthat I wasn't sure if I could depend on in the test suite, such as:\n\n    file:///C:/Program Files/Git/abs/path\n\nSo I decided to relax the test case just a little:\n\n    test_must_fail git url-parse \"/abs/path\" 2>err &&\n      test_grep \"is not a URL\" err &&\n      test_grep \"file:///\" err\n\nThe \"file:///\" checks that the path was properly recognized\nand that the friendlier error message was printed, all while\navoiding the hard coding of a \"C:/Program Files/Git\" prefix\nthat may or may not vary depending on testing environment.\n\nIn any case, the parser already handles it correctly.\n\nIt decomposes:\n\n    file://server/share/repo\n\nAs:\n\n  - scheme: file\n  - host:   server\n  - path:   /share/repo\n\nWhich is the correct interpretation.\n\nOn Windows, connect.c then takes that data and reconstructs\nthe UNC path \\\\server\\share\\repo for the filesystem.\nSo the UNC reconstruction happens downstream in connect.c,\nnot directly in the url-parse builtin or url_parse logic.\n\n    Matheus\n"},{"id":"543129","messageId":"xmqqjyt9p9pk.fsf@gitster.g","threadId":"61375","inReplyTo":"6c0a1601cd379bcdc87b4fe3b854166a@matheusmoreira.com","subject":"Re: [PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-12T03:50:47Z","receivedAt":"2026-05-12T03:50:50Z","isPatch":true,"body":"Matheus Afonso Martins Moreira <matheus@matheusmoreira.com> writes:\n\n>> Reviewers comment: Nicely done.\n>\n> Thank you!\n>\n>> More a question to myself, may be, about t9904 (and may be other parts)\n>> I have in mind that the parser learned to handle\n>>\n>> file://server/share/repo\n>> correctly under Windows.\n>> I don't know if this needs to be addressed here or in a follow-up commit ?\n>\n> I'd be happy to revisit this in a follow-up. It's been a while\n> since I used MSYS but I do remember the fact it rewrites paths\n> internally. I wasn't sure how to handle it properly in the tests.\n\nSo the only potential thing that is missing from the series is the\nabove, which we are fine to postpone in a follow-up series?  I think\nthat is a good stopping point.  Given that this command is new, it\nis fine that it has known and documented short-comings that will be\nimproved (of course on the other hand, we are not in any urgent need\nfor this new command, so we do not have to ship it half-baked).\n\nIs everybody happy with the patches in the current shape and should\nI mark it for 'next'?\n\nThanks.\n"},{"id":"543161","messageId":"20260512085734.GA26769@tb-raspi4","threadId":"61375","inReplyTo":"xmqqjyt9p9pk.fsf@gitster.g","subject":"Re: [PATCH v3 0/8] builtin: implement, document and test url-parse","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2026-05-12T08:57:34Z","receivedAt":"2026-05-12T09:02:57Z","isPatch":true,"body":"On Tue, May 12, 2026 at 12:50:47PM +0900, Junio C Hamano wrote:\n> Matheus Afonso Martins Moreira <matheus@matheusmoreira.com> writes:\n> \n> >> Reviewers comment: Nicely done.\n> >\n> > Thank you!\n> >\n> >> More a question to myself, may be, about t9904 (and may be other parts)\n> >> I have in mind that the parser learned to handle\n> >>\n> >> file://server/share/repo\n> >> correctly under Windows.\n> >> I don't know if this needs to be addressed here or in a follow-up commit ?\n> >\n> > I'd be happy to revisit this in a follow-up. It's been a while\n> > since I used MSYS but I do remember the fact it rewrites paths\n> > internally. I wasn't sure how to handle it properly in the tests.\n> \n> So the only potential thing that is missing from the series is the\n> above, which we are fine to postpone in a follow-up series?  I think\n> that is a good stopping point.  Given that this command is new, it\n> is fine that it has known and documented short-comings that will be\n> improved (of course on the other hand, we are not in any urgent need\n> for this new command, so we do not have to ship it half-baked).\n> \n> Is everybody happy with the patches in the current shape and should\n> I mark it for 'next'?\n> \n> Thanks.\n> \nI am happy with merging to next.\n"}]}