{"thread":{"id":"52846","subject":"[PATCH v2 0/5] Wildcard matching for credentials","startedAt":"2020-02-20T02:24:39Z","lastAt":"2020-02-21T06:10:10Z","messageCount":8,"participants":["brian m. carlson","Junio C Hamano","Jeff King"],"isPatch":true,"patchVersion":2,"patchTotal":5},"messages":[{"id":"392151","messageId":"20200220022413.258026-1-sandals@crustytoothpaste.net","threadId":"52846","inReplyTo":null,"subject":"[PATCH v2 0/5] Wildcard matching for credentials","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-02-20T02:24:08Z","receivedAt":"2020-02-20T02:24:39Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"This series introduces wildcard matching (that is, urlmatch support) for\ncredential config options, just like for the http options.  This is\nhelpful in corporate environments where custom credentials should be\nused across a wide variety of subdomains.\n\nChanges from v1:\n* Add a variety of additional tests in patch 3.\n* Switch to using the last matching config option for\n  credential.username like we do everywhere else.\n* Use all matching config keys, as we did before.\n* Skip calling git_default_config.\n* Fix percent-encoding handling and add a function to handle that.\n\nbrian m. carlson (5):\n  mailmap: add an additional email address for brian m. carlson\n  t1300: add test for urlmatch with multiple wildcards\n  t0300: add tests for some additional cases\n  credential: use the last matching username in the config\n  credential: allow wildcard patterns when matching config\n\n .mailmap                         |   1 +\n Documentation/gitcredentials.txt |   4 +-\n credential.c                     |  75 +++++++++++++-----\n credential.h                     |   3 +-\n strbuf.c                         |  15 ++++\n strbuf.h                         |   6 ++\n t/t0300-credentials.sh           | 128 +++++++++++++++++++++++++++++++\n t/t1300-config.sh                |   6 ++\n urlmatch.c                       |   4 +-\n urlmatch.h                       |   9 +++\n 10 files changed, 228 insertions(+), 23 deletions(-)\n\n"},{"id":"392152","messageId":"20200220022413.258026-2-sandals@crustytoothpaste.net","threadId":"52846","inReplyTo":"20200220022413.258026-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 1/5] mailmap: add an additional email address for brian m. carlson","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-02-20T02:24:09Z","receivedAt":"2020-02-20T02:24:39Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"From: brian m. carlson <bk2204@github.com>\n\nTo more accurately track the provenance of contributions, brian uses a\nwork email address for commits created at work. Add this email address\nto .mailmap so that contributions are properly attributed to the same\nperson.\n\nSigned-off-by: brian m. carlson <bk2204@github.com>\n---\n .mailmap | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/.mailmap b/.mailmap\nindex 73fd92e192..bde7aba756 100644\n--- a/.mailmap\n+++ b/.mailmap\n@@ -31,6 +31,7 @@ Brandon Casey <drafnel@gmail.com> <casey@nrlssc.navy.mil>\n Brandon Williams <bwilliams.eng@gmail.com> <bmwill@google.com>\n brian m. carlson <sandals@crustytoothpaste.net>\n brian m. carlson <sandals@crustytoothpaste.net> <sandals@crustytoothpaste.ath.cx>\n+brian m. carlson <sandals@crustytoothpaste.net> <bk2204@github.com>\n Bryan Larsen <bryan@larsen.st> <bryan.larsen@gmail.com>\n Bryan Larsen <bryan@larsen.st> <bryanlarsen@yahoo.com>\n Cheng Renquan <crquan@gmail.com>\n"},{"id":"392153","messageId":"20200220022413.258026-3-sandals@crustytoothpaste.net","threadId":"52846","inReplyTo":"20200220022413.258026-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 2/5] t1300: add test for urlmatch with multiple wildcards","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-02-20T02:24:10Z","receivedAt":"2020-02-20T02:24:40Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"From: brian m. carlson <bk2204@github.com>\n\nOur urlmatch code handles multiple wildcards, but we don't currently\nhave a test that checks this code path. Add a test that we handle this\ncase correctly to avoid any regressions.\n\nSigned-off-by: brian m. carlson <bk2204@github.com>\n---\n t/t1300-config.sh | 6 ++++++\n 1 file changed, 6 insertions(+)\n\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 5464c46c18..97ebfe1f9d 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -1408,6 +1408,8 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \t\tcookieFile = /tmp/wildcard.txt\n \t[http \"https://*.example.com/wildcardwithsubdomain\"]\n \t\tcookieFile = /tmp/wildcardwithsubdomain.txt\n+\t[http \"https://*.example.*\"]\n+\t\tcookieFile = /tmp/multiwildcard.txt\n \t[http \"https://trailing.example.com\"]\n \t\tcookieFile = /tmp/trailing.txt\n \t[http \"https://user@*.example.com/\"]\n@@ -1454,6 +1456,10 @@ test_expect_success 'urlmatch favors more specific URLs' '\n \n \techo http.cookiefile /tmp/sub.txt >expect &&\n \tgit config --get-urlmatch HTTP https://user@sub.example.com >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo http.cookiefile /tmp/multiwildcard.txt >expect &&\n+\tgit config --get-urlmatch HTTP https://wildcard.example.org >actual &&\n \ttest_cmp expect actual\n '\n \n"},{"id":"392154","messageId":"20200220022413.258026-5-sandals@crustytoothpaste.net","threadId":"52846","inReplyTo":"20200220022413.258026-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 4/5] credential: use the last matching username in the config","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-02-20T02:24:12Z","receivedAt":"2020-02-20T02:24:40Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"From: brian m. carlson <bk2204@github.com>\n\nEverywhere else in the codebase, we use the rule that the last matching\nconfiguration option is the one that takes effect.  This is helpful\nbecause it allows more specific configuration settings (e.g., per-repo\nconfiguration) to override less specific settings (e.g., per-user\nconfiguration).\n\nHowever, in the credential code, we didn't honor this setting, and\ninstead picked the first setting we had, and stuck with it.  This was\nlikely to ensure we picked the value from the URL, which we want to\nhonor over the configuration.\n\nIt's possible to do both, though, so let's check if the value is the one\nwe've gotten over our protocol connection, which if present will have\ncome from the URL, and keep it if so.  Otherwise, let's overwrite the\nvalue with the latest version we've got from the configuration, so we\nkeep the last configuration value.\n\nSigned-off-by: brian m. carlson <bk2204@github.com>\n---\n credential.c           | 9 ++++++++-\n credential.h           | 3 ++-\n t/t0300-credentials.sh | 2 +-\n 3 files changed, 11 insertions(+), 3 deletions(-)\n\ndiff --git a/credential.c b/credential.c\nindex 62be651b03..5701e32792 100644\n--- a/credential.c\n+++ b/credential.c\n@@ -71,8 +71,10 @@ static int credential_config_callback(const char *var, const char *value,\n \t\telse\n \t\t\tstring_list_clear(&c->helpers, 0);\n \t} else if (!strcmp(key, \"username\")) {\n-\t\tif (!c->username)\n+\t\tif (!c->username_from_proto) {\n+\t\t\tfree(c->username);\n \t\t\tc->username = xstrdup(value);\n+\t\t}\n \t}\n \telse if (!strcmp(key, \"usehttppath\"))\n \t\tc->use_http_path = git_config_bool(var, value);\n@@ -163,6 +165,7 @@ int credential_read(struct credential *c, FILE *fp)\n \t\tif (!strcmp(key, \"username\")) {\n \t\t\tfree(c->username);\n \t\t\tc->username = xstrdup(value);\n+\t\t\tc->username_from_proto = 1;\n \t\t} else if (!strcmp(key, \"password\")) {\n \t\t\tfree(c->password);\n \t\t\tc->password = xstrdup(value);\n@@ -349,10 +352,14 @@ void credential_from_url(struct credential *c, const char *url)\n \telse if (!colon || at <= colon) {\n \t\t/* Case (2) */\n \t\tc->username = url_decode_mem(cp, at - cp);\n+\t\tif (c->username && *c->username)\n+\t\t\tc->username_from_proto = 1;\n \t\thost = at + 1;\n \t} else {\n \t\t/* Case (3) */\n \t\tc->username = url_decode_mem(cp, colon - cp);\n+\t\tif (c->username && *c->username)\n+\t\t\tc->username_from_proto = 1;\n \t\tc->password = url_decode_mem(colon + 1, at - (colon + 1));\n \t\thost = at + 1;\n \t}\ndiff --git a/credential.h b/credential.h\nindex a5a3ee9bb8..fec7815dd0 100644\n--- a/credential.h\n+++ b/credential.h\n@@ -118,7 +118,8 @@ struct credential {\n \tunsigned approved:1,\n \t\t configured:1,\n \t\t quit:1,\n-\t\t use_http_path:1;\n+\t\t use_http_path:1,\n+\t\t username_from_proto:1;\n \n \tchar *username;\n \tchar *password;\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 4593a0cd3d..8f87599056 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -344,7 +344,7 @@ test_expect_success 'honors username from URL over helper (components)' '\n \tEOF\n '\n \n-test_expect_failure 'last matching username wins' '\n+test_expect_success 'last matching username wins' '\n \ttest_config credential.https://example.com/path.git.username bob &&\n \ttest_config credential.https://example.com.username alice &&\n \ttest_config credential.https://example.com.helper \"verbatim \\\"\\\" bar\" &&\n"},{"id":"392155","messageId":"20200220022413.258026-4-sandals@crustytoothpaste.net","threadId":"52846","inReplyTo":"20200220022413.258026-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 3/5] t0300: add tests for some additional cases","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-02-20T02:24:11Z","receivedAt":"2020-02-20T02:24:42Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"From: brian m. carlson <bk2204@github.com>\n\nThere are some tricky cases in our credential helpers that we don't have\ntest cases for.  To help prevent regressions, let's add some for these\ncases:\n\n* If there are multiple configured credential helpers, one without a\n  path and one with a path, we want to invoke both of them.\n* If there are percent-encoded values in the URL, we handle them\n  properly.\n* And finally, if there is a username in the remote URL, we want to\n  honor that over what the configuration tells us.\n\nFinally, there's an additional case that we'd like to test for as well,\nbut that currently fails.  In all other situations in our configuration,\nwe pick the last configuration setting that's provided.  However, we\nfail to do that for credential.username, where we pick the first setting\ninstead.  Let's add a failing test that we have the consistent behavior\nhere, since that's the documented, expected behavior.\n\nSigned-off-by: brian m. carlson <bk2204@github.com>\n---\n t/t0300-credentials.sh | 108 +++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 108 insertions(+)\n\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 82eaaea0f4..4593a0cd3d 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -240,6 +240,57 @@ test_expect_success 'do not match configured credential' '\n \tEOF\n '\n \n+test_expect_success 'match multiple configured helpers' '\n+\ttest_config credential.helper \"verbatim \\\"\\\" \\\"\\\"\" &&\n+\ttest_config credential.https://example.com.helper \"$HELPER\" &&\n+\tcheck fill <<-\\EOF\n+\tprotocol=https\n+\thost=example.com\n+\tpath=repo.git\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=foo\n+\tpassword=bar\n+\t--\n+\tverbatim: get\n+\tverbatim: protocol=https\n+\tverbatim: host=example.com\n+\tEOF\n+'\n+\n+test_expect_success 'match multiple configured helpers with URLs' '\n+\ttest_config credential.https://example.com/repo.git.helper \"verbatim \\\"\\\" \\\"\\\"\" &&\n+\ttest_config credential.https://example.com.helper \"$HELPER\" &&\n+\tcheck fill <<-\\EOF\n+\tprotocol=https\n+\thost=example.com\n+\tpath=repo.git\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=foo\n+\tpassword=bar\n+\t--\n+\tverbatim: get\n+\tverbatim: protocol=https\n+\tverbatim: host=example.com\n+\tEOF\n+'\n+\n+test_expect_success 'match percent-encoded values' '\n+\ttest_config credential.https://example.com/%2566.git.helper \"$HELPER\" &&\n+\tcheck fill <<-\\EOF\n+\turl=https://example.com/%2566.git\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=foo\n+\tpassword=bar\n+\t--\n+\tEOF\n+'\n+\n test_expect_success 'pull username from config' '\n \ttest_config credential.https://example.com.username foo &&\n \tcheck fill <<-\\EOF\n@@ -255,6 +306,63 @@ test_expect_success 'pull username from config' '\n \tEOF\n '\n \n+test_expect_success 'honors username from URL over helper (URL)' '\n+\ttest_config credential.https://example.com.username bob &&\n+\ttest_config credential.https://example.com.helper \"verbatim \\\"\\\" bar\" &&\n+\tcheck fill <<-\\EOF\n+\turl=https://alice@example.com\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=alice\n+\tpassword=bar\n+\t--\n+\tverbatim: get\n+\tverbatim: protocol=https\n+\tverbatim: host=example.com\n+\tverbatim: username=alice\n+\tEOF\n+'\n+\n+test_expect_success 'honors username from URL over helper (components)' '\n+\ttest_config credential.https://example.com.username bob &&\n+\ttest_config credential.https://example.com.helper \"verbatim \\\"\\\" bar\" &&\n+\tcheck fill <<-\\EOF\n+\tprotocol=https\n+\thost=example.com\n+\tusername=alice\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=alice\n+\tpassword=bar\n+\t--\n+\tverbatim: get\n+\tverbatim: protocol=https\n+\tverbatim: host=example.com\n+\tverbatim: username=alice\n+\tEOF\n+'\n+\n+test_expect_failure 'last matching username wins' '\n+\ttest_config credential.https://example.com/path.git.username bob &&\n+\ttest_config credential.https://example.com.username alice &&\n+\ttest_config credential.https://example.com.helper \"verbatim \\\"\\\" bar\" &&\n+\tcheck fill <<-\\EOF\n+\turl=https://example.com/path.git\n+\t--\n+\tprotocol=https\n+\thost=example.com\n+\tusername=alice\n+\tpassword=bar\n+\t--\n+\tverbatim: get\n+\tverbatim: protocol=https\n+\tverbatim: host=example.com\n+\tverbatim: username=alice\n+\tEOF\n+'\n+\n test_expect_success 'http paths can be part of context' '\n \tcheck fill \"verbatim foo bar\" <<-\\EOF &&\n \tprotocol=https\n"},{"id":"392156","messageId":"20200220022413.258026-6-sandals@crustytoothpaste.net","threadId":"52846","inReplyTo":"20200220022413.258026-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 5/5] credential: allow wildcard patterns when matching config","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-02-20T02:24:13Z","receivedAt":"2020-02-20T02:24:44Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"From: brian m. carlson <bk2204@github.com>\n\nIn some cases, a user will want to use a specific credential helper for\na wildcard pattern, such as https://*.corp.example.com.  We have code\nthat handles this already with the urlmatch code, so let's use that\ninstead of our custom code.\n\nSince the urlmatch code is a superset of our current matching in terms\nof capabilities, there shouldn't be any cases of things that matched\npreviously that don't match now.  However, in addition to wildcard\nmatching, we now use partial path matching, which can cause slightly\ndifferent behavior in the case that a helper applies to the prefix\n(considering path components) of the remote URL.  While different, this\nis probably the behavior people were wanting anyway.\n\nSince we're using the urlmatch code, we need to encode the components\nwe've gotten into a URL to match, so add a function to percent-encode\ndata and format the URL with it.  We now also no longer need to the\ncustom code to match URLs, so let's remove it.\n\nAdditionally, the urlmatch code always looks for the best match, whereas\nwe want all matches for credential helpers to preserve existing\nbehavior.  Let's add an optional field, select_fn, that lets us control\nwhich items we want (in this case, all of them) and default it to the\nbest-match code that already exists for other users.\n\nSigned-off-by: brian m. carlson <bk2204@github.com>\n---\n Documentation/gitcredentials.txt |  4 +-\n credential.c                     | 66 +++++++++++++++++++++++---------\n strbuf.c                         | 15 ++++++++\n strbuf.h                         |  6 +++\n t/t0300-credentials.sh           | 20 ++++++++++\n urlmatch.c                       |  4 +-\n urlmatch.h                       |  9 +++++\n 7 files changed, 103 insertions(+), 21 deletions(-)\n\ndiff --git a/Documentation/gitcredentials.txt b/Documentation/gitcredentials.txt\nindex 5b9d14fb1f..1814d2d23c 100644\n--- a/Documentation/gitcredentials.txt\n+++ b/Documentation/gitcredentials.txt\n@@ -131,7 +131,9 @@ context would not match:\n because the hostnames differ. Nor would it match `foo.example.com`; Git\n compares hostnames exactly, without considering whether two hosts are part of\n the same domain. Likewise, a config entry for `http://example.com` would not\n-match: Git compares the protocols exactly.\n+match: Git compares the protocols exactly.  However, you may use wildcards in\n+the domain name and other pattern matching techniques as with the `http.<url>.*`\n+options.\n \n If the \"pattern\" URL does include a path component, then this too must match\n exactly: the context `https://example.com/bar/baz.git` will match a config\ndiff --git a/credential.c b/credential.c\nindex 5701e32792..77dfde44e3 100644\n--- a/credential.c\n+++ b/credential.c\n@@ -6,6 +6,7 @@\n #include \"url.h\"\n #include \"prompt.h\"\n #include \"sigchain.h\"\n+#include \"urlmatch.h\"\n \n void credential_init(struct credential *c)\n {\n@@ -40,7 +41,7 @@ static int credential_config_callback(const char *var, const char *value,\n \t\t\t\t      void *data)\n {\n \tstruct credential *c = data;\n-\tconst char *key, *dot;\n+\tconst char *key;\n \n \tif (!skip_prefix(var, \"credential.\", &key))\n \t\treturn 0;\n@@ -48,23 +49,6 @@ static int credential_config_callback(const char *var, const char *value,\n \tif (!value)\n \t\treturn config_error_nonbool(var);\n \n-\tdot = strrchr(key, '.');\n-\tif (dot) {\n-\t\tstruct credential want = CREDENTIAL_INIT;\n-\t\tchar *url = xmemdupz(key, dot - key);\n-\t\tint matched;\n-\n-\t\tcredential_from_url(&want, url);\n-\t\tmatched = credential_match(&want, c);\n-\n-\t\tcredential_clear(&want);\n-\t\tfree(url);\n-\n-\t\tif (!matched)\n-\t\t\treturn 0;\n-\t\tkey = dot + 1;\n-\t}\n-\n \tif (!strcmp(key, \"helper\")) {\n \t\tif (*value)\n \t\t\tstring_list_append(&c->helpers, value);\n@@ -89,11 +73,38 @@ static int proto_is_http(const char *s)\n \treturn !strcmp(s, \"https\") || !strcmp(s, \"http\");\n }\n \n+static void credential_describe(struct credential *c, struct strbuf *out);\n+static void credential_format(struct credential *c, struct strbuf *out);\n+\n+static int select_all(const struct urlmatch_item *a,\n+\t\t      const struct urlmatch_item *b)\n+{\n+\treturn 0;\n+}\n+\n static void credential_apply_config(struct credential *c)\n {\n+\tchar *normalized_url;\n+\tstruct urlmatch_config config = { STRING_LIST_INIT_DUP };\n+\tstruct strbuf url = STRBUF_INIT;\n+\n \tif (c->configured)\n \t\treturn;\n-\tgit_config(credential_config_callback, c);\n+\n+\tconfig.section = \"credential\";\n+\tconfig.key = NULL;\n+\tconfig.collect_fn = credential_config_callback;\n+\tconfig.cascade_fn = NULL;\n+\tconfig.select_fn = select_all;\n+\tconfig.cb = c;\n+\n+\tcredential_format(c, &url);\n+\tnormalized_url = url_normalize(url.buf, &config.url);\n+\n+\tgit_config(urlmatch_config_entry, &config);\n+\tfree(normalized_url);\n+\tstrbuf_release(&url);\n+\n \tc->configured = 1;\n \n \tif (!c->use_http_path && proto_is_http(c->protocol)) {\n@@ -114,6 +125,23 @@ static void credential_describe(struct credential *c, struct strbuf *out)\n \t\tstrbuf_addf(out, \"/%s\", c->path);\n }\n \n+static void credential_format(struct credential *c, struct strbuf *out)\n+{\n+\tif (!c->protocol)\n+\t\treturn;\n+\tstrbuf_addf(out, \"%s://\", c->protocol);\n+\tif (c->username && *c->username) {\n+\t\tstrbuf_add_percentencode(out, c->username);\n+\t\tstrbuf_addch(out, '@');\n+\t}\n+\tif (c->host)\n+\t\tstrbuf_addstr(out, c->host);\n+\tif (c->path) {\n+\t\tstrbuf_addch(out, '/');\n+\t\tstrbuf_add_percentencode(out, c->path);\n+\t}\n+}\n+\n static char *credential_ask_one(const char *what, struct credential *c,\n \t\t\t\tint flags)\n {\ndiff --git a/strbuf.c b/strbuf.c\nindex f19da55b07..bb0065ccaf 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -479,6 +479,21 @@ void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src)\n \t}\n }\n \n+#define URL_UNSAFE_CHARS \" <>\\\"%{}|\\\\^`:/?#[]@!$&'()*+,;=\"\n+\n+void strbuf_add_percentencode(struct strbuf *dst, const char *src)\n+{\n+\tsize_t i, len = strlen(src);\n+\n+\tfor (i = 0; i < len; i++) {\n+\t\tunsigned char ch = src[i];\n+\t\tif (ch <= 0x1F || ch >= 0x7F || strchr(URL_UNSAFE_CHARS, ch))\n+\t\t\tstrbuf_addf(dst, \"%%%02X\", (unsigned char)ch);\n+\t\telse\n+\t\t\tstrbuf_addch(dst, ch);\n+\t}\n+}\n+\n size_t strbuf_fread(struct strbuf *sb, size_t size, FILE *f)\n {\n \tsize_t res;\ndiff --git a/strbuf.h b/strbuf.h\nindex aae7ac3a82..ce8e49c0b2 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -378,6 +378,12 @@ size_t strbuf_expand_dict_cb(struct strbuf *sb,\n  */\n void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src);\n \n+/**\n+ * Append the contents of a string to a strbuf, percent-encoding any characters\n+ * that are needed to be encoded for a URL.\n+ */\n+void strbuf_add_percentencode(struct strbuf *dst, const char *src);\n+\n /**\n  * Append the given byte size as a human-readable string (i.e. 12.23 KiB,\n  * 3.50 MiB).\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 8f87599056..39f097ea9e 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -397,6 +397,26 @@ test_expect_success 'http paths can be part of context' '\n \tEOF\n '\n \n+test_expect_success 'context uses urlmatch' '\n+\ttest_config \"credential.https://*.org.useHttpPath\" true &&\n+\tcheck fill \"verbatim foo bar\" <<-\\EOF\n+\tprotocol=https\n+\thost=example.org\n+\tpath=foo.git\n+\t--\n+\tprotocol=https\n+\thost=example.org\n+\tpath=foo.git\n+\tusername=foo\n+\tpassword=bar\n+\t--\n+\tverbatim: get\n+\tverbatim: protocol=https\n+\tverbatim: host=example.org\n+\tverbatim: path=foo.git\n+\tEOF\n+'\n+\n test_expect_success 'helpers can abort the process' '\n \ttest_must_fail git \\\n \t\t-c credential.helper=\"!f() { echo quit=1; }; f\" \\\ndiff --git a/urlmatch.c b/urlmatch.c\nindex 3e42bd7504..29272a5c4f 100644\n--- a/urlmatch.c\n+++ b/urlmatch.c\n@@ -557,6 +557,8 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb)\n \tconst char *key, *dot;\n \tstruct strbuf synthkey = STRBUF_INIT;\n \tint retval;\n+\tint (*select_fn)(const struct urlmatch_item *a, const struct urlmatch_item *b) =\n+\t\tcollect->select_fn ? collect->select_fn : cmp_matches;\n \n \tif (!skip_prefix(var, collect->section, &key) || *(key++) != '.') {\n \t\tif (collect->cascade_fn)\n@@ -587,7 +589,7 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb)\n \tif (!item->util) {\n \t\titem->util = xcalloc(1, sizeof(matched));\n \t} else {\n-\t\tif (cmp_matches(&matched, item->util) < 0)\n+\t\tif (select_fn(&matched, item->util) < 0)\n \t\t\t /*\n \t\t\t  * Our match is worse than the old one,\n \t\t\t  * we cannot use it.\ndiff --git a/urlmatch.h b/urlmatch.h\nindex eed5f29235..2407520731 100644\n--- a/urlmatch.h\n+++ b/urlmatch.h\n@@ -50,6 +50,15 @@ struct urlmatch_config {\n \tvoid *cb;\n \tint (*collect_fn)(const char *var, const char *value, void *cb);\n \tint (*cascade_fn)(const char *var, const char *value, void *cb);\n+\t/*\n+\t * Compare the two matches, the one just discovered and the existing\n+\t * best match and return a negative value if the found item is to be\n+\t * rejected or a non-negative value if it is to be accepted.  If this\n+\t * field is set to NULL, use the default comparison technique, which\n+\t * checks to ses if found is better (according to the urlmatch\n+\t * specificity rules) than existing.\n+\t */\n+\tint (*select_fn)(const struct urlmatch_item *found, const struct urlmatch_item *existing);\n };\n \n int urlmatch_config_entry(const char *var, const char *value, void *cb);\n"},{"id":"392203","messageId":"xmqq5zg19cse.fsf@gitster-ct.c.googlers.com","threadId":"52846","inReplyTo":"20200220022413.258026-6-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 5/5] credential: allow wildcard patterns when matching config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-02-20T21:20:01Z","receivedAt":"2020-02-20T21:20:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> From: brian m. carlson <bk2204@github.com>\n>\n> In some cases, a user will want to use a specific credential helper for\n> a wildcard pattern, such as https://*.corp.example.com.  We have code\n> that handles this already with the urlmatch code, so let's use that\n> instead of our custom code.\n\nQuite nice.  Will queue the whole series.\n\nThanks.\n"},{"id":"392259","messageId":"20200221061007.GG1280313@coredump.intra.peff.net","threadId":"52846","inReplyTo":"20200220022413.258026-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 0/5] Wildcard matching for credentials","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2020-02-21T06:10:07Z","receivedAt":"2020-02-21T06:10:10Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Feb 20, 2020 at 02:24:08AM +0000, brian m. carlson wrote:\n\n> This series introduces wildcard matching (that is, urlmatch support) for\n> credential config options, just like for the http options.  This is\n> helpful in corporate environments where custom credentials should be\n> used across a wide variety of subdomains.\n> \n> Changes from v1:\n> * Add a variety of additional tests in patch 3.\n> * Switch to using the last matching config option for\n>   credential.username like we do everywhere else.\n> * Use all matching config keys, as we did before.\n> * Skip calling git_default_config.\n> * Fix percent-encoding handling and add a function to handle that.\n\nThanks, this addresses all of my concerns, and I'm really happy to see\nall the new tests covering various bits from our discussion. Really\ncleanly done.\n\n-Peff\n"}]}