Skip to content

Commit 2cbb058

Browse files
committed
Merge branch 'bc/wildcard-credential'
A configuration element used for credential subsystem can now use wildcard pattern to specify for which set of URLs the entry applies. * bc/wildcard-credential: credential: allow wildcard patterns when matching config credential: use the last matching username in the config t0300: add tests for some additional cases t1300: add test for urlmatch with multiple wildcards mailmap: add an additional email address for brian m. carlson
2 parents 25063e2 + 46fd7b3 commit 2cbb058

10 files changed

+228
-23
lines changed

.mailmap

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Brandon Casey <[email protected]> <[email protected]>
3131
3232
brian m. carlson <[email protected]>
3333
34+
3435
3536
3637
Cheng Renquan <[email protected]>

Documentation/gitcredentials.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ context would not match:
131131
because the hostnames differ. Nor would it match `foo.example.com`; Git
132132
compares hostnames exactly, without considering whether two hosts are part of
133133
the same domain. Likewise, a config entry for `http://example.com` would not
134-
match: Git compares the protocols exactly.
134+
match: Git compares the protocols exactly. However, you may use wildcards in
135+
the domain name and other pattern matching techniques as with the `http.<url>.*`
136+
options.
135137

136138
If the "pattern" URL does include a path component, then this too must match
137139
exactly: the context `https://example.com/bar/baz.git` will match a config

credential.c

+55-20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "url.h"
77
#include "prompt.h"
88
#include "sigchain.h"
9+
#include "urlmatch.h"
910

1011
void credential_init(struct credential *c)
1112
{
@@ -40,39 +41,24 @@ static int credential_config_callback(const char *var, const char *value,
4041
void *data)
4142
{
4243
struct credential *c = data;
43-
const char *key, *dot;
44+
const char *key;
4445

4546
if (!skip_prefix(var, "credential.", &key))
4647
return 0;
4748

4849
if (!value)
4950
return config_error_nonbool(var);
5051

51-
dot = strrchr(key, '.');
52-
if (dot) {
53-
struct credential want = CREDENTIAL_INIT;
54-
char *url = xmemdupz(key, dot - key);
55-
int matched;
56-
57-
credential_from_url(&want, url);
58-
matched = credential_match(&want, c);
59-
60-
credential_clear(&want);
61-
free(url);
62-
63-
if (!matched)
64-
return 0;
65-
key = dot + 1;
66-
}
67-
6852
if (!strcmp(key, "helper")) {
6953
if (*value)
7054
string_list_append(&c->helpers, value);
7155
else
7256
string_list_clear(&c->helpers, 0);
7357
} else if (!strcmp(key, "username")) {
74-
if (!c->username)
58+
if (!c->username_from_proto) {
59+
free(c->username);
7560
c->username = xstrdup(value);
61+
}
7662
}
7763
else if (!strcmp(key, "usehttppath"))
7864
c->use_http_path = git_config_bool(var, value);
@@ -87,11 +73,38 @@ static int proto_is_http(const char *s)
8773
return !strcmp(s, "https") || !strcmp(s, "http");
8874
}
8975

76+
static void credential_describe(struct credential *c, struct strbuf *out);
77+
static void credential_format(struct credential *c, struct strbuf *out);
78+
79+
static int select_all(const struct urlmatch_item *a,
80+
const struct urlmatch_item *b)
81+
{
82+
return 0;
83+
}
84+
9085
static void credential_apply_config(struct credential *c)
9186
{
87+
char *normalized_url;
88+
struct urlmatch_config config = { STRING_LIST_INIT_DUP };
89+
struct strbuf url = STRBUF_INIT;
90+
9291
if (c->configured)
9392
return;
94-
git_config(credential_config_callback, c);
93+
94+
config.section = "credential";
95+
config.key = NULL;
96+
config.collect_fn = credential_config_callback;
97+
config.cascade_fn = NULL;
98+
config.select_fn = select_all;
99+
config.cb = c;
100+
101+
credential_format(c, &url);
102+
normalized_url = url_normalize(url.buf, &config.url);
103+
104+
git_config(urlmatch_config_entry, &config);
105+
free(normalized_url);
106+
strbuf_release(&url);
107+
95108
c->configured = 1;
96109

97110
if (!c->use_http_path && proto_is_http(c->protocol)) {
@@ -112,6 +125,23 @@ static void credential_describe(struct credential *c, struct strbuf *out)
112125
strbuf_addf(out, "/%s", c->path);
113126
}
114127

128+
static void credential_format(struct credential *c, struct strbuf *out)
129+
{
130+
if (!c->protocol)
131+
return;
132+
strbuf_addf(out, "%s://", c->protocol);
133+
if (c->username && *c->username) {
134+
strbuf_add_percentencode(out, c->username);
135+
strbuf_addch(out, '@');
136+
}
137+
if (c->host)
138+
strbuf_addstr(out, c->host);
139+
if (c->path) {
140+
strbuf_addch(out, '/');
141+
strbuf_add_percentencode(out, c->path);
142+
}
143+
}
144+
115145
static char *credential_ask_one(const char *what, struct credential *c,
116146
int flags)
117147
{
@@ -163,6 +193,7 @@ int credential_read(struct credential *c, FILE *fp)
163193
if (!strcmp(key, "username")) {
164194
free(c->username);
165195
c->username = xstrdup(value);
196+
c->username_from_proto = 1;
166197
} else if (!strcmp(key, "password")) {
167198
free(c->password);
168199
c->password = xstrdup(value);
@@ -349,10 +380,14 @@ void credential_from_url(struct credential *c, const char *url)
349380
else if (!colon || at <= colon) {
350381
/* Case (2) */
351382
c->username = url_decode_mem(cp, at - cp);
383+
if (c->username && *c->username)
384+
c->username_from_proto = 1;
352385
host = at + 1;
353386
} else {
354387
/* Case (3) */
355388
c->username = url_decode_mem(cp, colon - cp);
389+
if (c->username && *c->username)
390+
c->username_from_proto = 1;
356391
c->password = url_decode_mem(colon + 1, at - (colon + 1));
357392
host = at + 1;
358393
}

credential.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ struct credential {
118118
unsigned approved:1,
119119
configured:1,
120120
quit:1,
121-
use_http_path:1;
121+
use_http_path:1,
122+
username_from_proto:1;
122123

123124
char *username;
124125
char *password;

strbuf.c

+15
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,21 @@ void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src)
479479
}
480480
}
481481

482+
#define URL_UNSAFE_CHARS " <>\"%{}|\\^`:/?#[]@!$&'()*+,;="
483+
484+
void strbuf_add_percentencode(struct strbuf *dst, const char *src)
485+
{
486+
size_t i, len = strlen(src);
487+
488+
for (i = 0; i < len; i++) {
489+
unsigned char ch = src[i];
490+
if (ch <= 0x1F || ch >= 0x7F || strchr(URL_UNSAFE_CHARS, ch))
491+
strbuf_addf(dst, "%%%02X", (unsigned char)ch);
492+
else
493+
strbuf_addch(dst, ch);
494+
}
495+
}
496+
482497
size_t strbuf_fread(struct strbuf *sb, size_t size, FILE *f)
483498
{
484499
size_t res;

strbuf.h

+6
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,12 @@ size_t strbuf_expand_dict_cb(struct strbuf *sb,
378378
*/
379379
void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src);
380380

381+
/**
382+
* Append the contents of a string to a strbuf, percent-encoding any characters
383+
* that are needed to be encoded for a URL.
384+
*/
385+
void strbuf_add_percentencode(struct strbuf *dst, const char *src);
386+
381387
/**
382388
* Append the given byte size as a human-readable string (i.e. 12.23 KiB,
383389
* 3.50 MiB).

t/t0300-credentials.sh

+128
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,57 @@ test_expect_success 'do not match configured credential' '
240240
EOF
241241
'
242242

243+
test_expect_success 'match multiple configured helpers' '
244+
test_config credential.helper "verbatim \"\" \"\"" &&
245+
test_config credential.https://example.com.helper "$HELPER" &&
246+
check fill <<-\EOF
247+
protocol=https
248+
host=example.com
249+
path=repo.git
250+
--
251+
protocol=https
252+
host=example.com
253+
username=foo
254+
password=bar
255+
--
256+
verbatim: get
257+
verbatim: protocol=https
258+
verbatim: host=example.com
259+
EOF
260+
'
261+
262+
test_expect_success 'match multiple configured helpers with URLs' '
263+
test_config credential.https://example.com/repo.git.helper "verbatim \"\" \"\"" &&
264+
test_config credential.https://example.com.helper "$HELPER" &&
265+
check fill <<-\EOF
266+
protocol=https
267+
host=example.com
268+
path=repo.git
269+
--
270+
protocol=https
271+
host=example.com
272+
username=foo
273+
password=bar
274+
--
275+
verbatim: get
276+
verbatim: protocol=https
277+
verbatim: host=example.com
278+
EOF
279+
'
280+
281+
test_expect_success 'match percent-encoded values' '
282+
test_config credential.https://example.com/%2566.git.helper "$HELPER" &&
283+
check fill <<-\EOF
284+
url=https://example.com/%2566.git
285+
--
286+
protocol=https
287+
host=example.com
288+
username=foo
289+
password=bar
290+
--
291+
EOF
292+
'
293+
243294
test_expect_success 'pull username from config' '
244295
test_config credential.https://example.com.username foo &&
245296
check fill <<-\EOF
@@ -255,6 +306,63 @@ test_expect_success 'pull username from config' '
255306
EOF
256307
'
257308

309+
test_expect_success 'honors username from URL over helper (URL)' '
310+
test_config credential.https://example.com.username bob &&
311+
test_config credential.https://example.com.helper "verbatim \"\" bar" &&
312+
check fill <<-\EOF
313+
url=https://[email protected]
314+
--
315+
protocol=https
316+
host=example.com
317+
username=alice
318+
password=bar
319+
--
320+
verbatim: get
321+
verbatim: protocol=https
322+
verbatim: host=example.com
323+
verbatim: username=alice
324+
EOF
325+
'
326+
327+
test_expect_success 'honors username from URL over helper (components)' '
328+
test_config credential.https://example.com.username bob &&
329+
test_config credential.https://example.com.helper "verbatim \"\" bar" &&
330+
check fill <<-\EOF
331+
protocol=https
332+
host=example.com
333+
username=alice
334+
--
335+
protocol=https
336+
host=example.com
337+
username=alice
338+
password=bar
339+
--
340+
verbatim: get
341+
verbatim: protocol=https
342+
verbatim: host=example.com
343+
verbatim: username=alice
344+
EOF
345+
'
346+
347+
test_expect_success 'last matching username wins' '
348+
test_config credential.https://example.com/path.git.username bob &&
349+
test_config credential.https://example.com.username alice &&
350+
test_config credential.https://example.com.helper "verbatim \"\" bar" &&
351+
check fill <<-\EOF
352+
url=https://example.com/path.git
353+
--
354+
protocol=https
355+
host=example.com
356+
username=alice
357+
password=bar
358+
--
359+
verbatim: get
360+
verbatim: protocol=https
361+
verbatim: host=example.com
362+
verbatim: username=alice
363+
EOF
364+
'
365+
258366
test_expect_success 'http paths can be part of context' '
259367
check fill "verbatim foo bar" <<-\EOF &&
260368
protocol=https
@@ -289,6 +397,26 @@ test_expect_success 'http paths can be part of context' '
289397
EOF
290398
'
291399

400+
test_expect_success 'context uses urlmatch' '
401+
test_config "credential.https://*.org.useHttpPath" true &&
402+
check fill "verbatim foo bar" <<-\EOF
403+
protocol=https
404+
host=example.org
405+
path=foo.git
406+
--
407+
protocol=https
408+
host=example.org
409+
path=foo.git
410+
username=foo
411+
password=bar
412+
--
413+
verbatim: get
414+
verbatim: protocol=https
415+
verbatim: host=example.org
416+
verbatim: path=foo.git
417+
EOF
418+
'
419+
292420
test_expect_success 'helpers can abort the process' '
293421
test_must_fail git \
294422
-c credential.helper="!f() { echo quit=1; }; f" \

t/t1300-config.sh

+6
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,8 @@ test_expect_success 'urlmatch favors more specific URLs' '
14081408
cookieFile = /tmp/wildcard.txt
14091409
[http "https://*.example.com/wildcardwithsubdomain"]
14101410
cookieFile = /tmp/wildcardwithsubdomain.txt
1411+
[http "https://*.example.*"]
1412+
cookieFile = /tmp/multiwildcard.txt
14111413
[http "https://trailing.example.com"]
14121414
cookieFile = /tmp/trailing.txt
14131415
[http "https://user@*.example.com/"]
@@ -1454,6 +1456,10 @@ test_expect_success 'urlmatch favors more specific URLs' '
14541456
14551457
echo http.cookiefile /tmp/sub.txt >expect &&
14561458
git config --get-urlmatch HTTP https://[email protected] >actual &&
1459+
test_cmp expect actual &&
1460+
1461+
echo http.cookiefile /tmp/multiwildcard.txt >expect &&
1462+
git config --get-urlmatch HTTP https://wildcard.example.org >actual &&
14571463
test_cmp expect actual
14581464
'
14591465

urlmatch.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb)
557557
const char *key, *dot;
558558
struct strbuf synthkey = STRBUF_INIT;
559559
int retval;
560+
int (*select_fn)(const struct urlmatch_item *a, const struct urlmatch_item *b) =
561+
collect->select_fn ? collect->select_fn : cmp_matches;
560562

561563
if (!skip_prefix(var, collect->section, &key) || *(key++) != '.') {
562564
if (collect->cascade_fn)
@@ -587,7 +589,7 @@ int urlmatch_config_entry(const char *var, const char *value, void *cb)
587589
if (!item->util) {
588590
item->util = xcalloc(1, sizeof(matched));
589591
} else {
590-
if (cmp_matches(&matched, item->util) < 0)
592+
if (select_fn(&matched, item->util) < 0)
591593
/*
592594
* Our match is worse than the old one,
593595
* we cannot use it.

0 commit comments

Comments
 (0)