Skip to content

Commit 17c7f4d

Browse files
committed
Merge branch 'pt/credential-xdg'
Tweak the sample "store" backend of the credential helper to honor XDG configuration file locations when specified. * pt/credential-xdg: t0302: "unreadable" test needs POSIXPERM t0302: test credential-store support for XDG_CONFIG_HOME git-credential-store: support XDG_CONFIG_HOME git-credential-store: support multiple credential files
2 parents 8440f74 + efee598 commit 17c7f4d

File tree

3 files changed

+210
-29
lines changed

3 files changed

+210
-29
lines changed

Documentation/git-credential-store.txt

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,41 @@ OPTIONS
3131

3232
--file=<path>::
3333

34-
Use `<path>` to store credentials. The file will have its
34+
Use `<path>` to lookup and store credentials. The file will have its
3535
filesystem permissions set to prevent other users on the system
3636
from reading it, but will not be encrypted or otherwise
37-
protected. Defaults to `~/.git-credentials`.
37+
protected. If not specified, credentials will be searched for from
38+
`~/.git-credentials` and `$XDG_CONFIG_HOME/git/credentials`, and
39+
credentials will be written to `~/.git-credentials` if it exists, or
40+
`$XDG_CONFIG_HOME/git/credentials` if it exists and the former does
41+
not. See also <<FILES>>.
42+
43+
[[FILES]]
44+
FILES
45+
-----
46+
47+
If not set explicitly with '--file', there are two files where
48+
git-credential-store will search for credentials in order of precedence:
49+
50+
~/.git-credentials::
51+
User-specific credentials file.
52+
53+
$XDG_CONFIG_HOME/git/credentials::
54+
Second user-specific credentials file. If '$XDG_CONFIG_HOME' is not set
55+
or empty, `$HOME/.config/git/credentials` will be used. Any credentials
56+
stored in this file will not be used if `~/.git-credentials` has a
57+
matching credential as well. It is a good idea not to create this file
58+
if you sometimes use older versions of Git that do not support it.
59+
60+
For credential lookups, the files are read in the order given above, with the
61+
first matching credential found taking precedence over credentials found in
62+
files further down the list.
63+
64+
Credential storage will by default write to the first existing file in the
65+
list. If none of these files exist, `~/.git-credentials` will be created and
66+
written to.
67+
68+
When erasing credentials, matching credentials will be erased from all files.
3869

3970
EXAMPLES
4071
--------

credential-store.c

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,28 @@
66

77
static struct lock_file credential_lock;
88

9-
static void parse_credential_file(const char *fn,
9+
static int parse_credential_file(const char *fn,
1010
struct credential *c,
1111
void (*match_cb)(struct credential *),
1212
void (*other_cb)(struct strbuf *))
1313
{
1414
FILE *fh;
1515
struct strbuf line = STRBUF_INIT;
1616
struct credential entry = CREDENTIAL_INIT;
17+
int found_credential = 0;
1718

1819
fh = fopen(fn, "r");
1920
if (!fh) {
20-
if (errno != ENOENT)
21+
if (errno != ENOENT && errno != EACCES)
2122
die_errno("unable to open %s", fn);
22-
return;
23+
return found_credential;
2324
}
2425

2526
while (strbuf_getline(&line, fh, '\n') != EOF) {
2627
credential_from_url(&entry, line.buf);
2728
if (entry.username && entry.password &&
2829
credential_match(c, &entry)) {
30+
found_credential = 1;
2931
if (match_cb) {
3032
match_cb(&entry);
3133
break;
@@ -38,6 +40,7 @@ static void parse_credential_file(const char *fn,
3840
credential_clear(&entry);
3941
strbuf_release(&line);
4042
fclose(fh);
43+
return found_credential;
4144
}
4245

4346
static void print_entry(struct credential *c)
@@ -64,21 +67,10 @@ static void rewrite_credential_file(const char *fn, struct credential *c,
6467
die_errno("unable to commit credential store");
6568
}
6669

67-
static void store_credential(const char *fn, struct credential *c)
70+
static void store_credential_file(const char *fn, struct credential *c)
6871
{
6972
struct strbuf buf = STRBUF_INIT;
7073

71-
/*
72-
* Sanity check that what we are storing is actually sensible.
73-
* In particular, we can't make a URL without a protocol field.
74-
* Without either a host or pathname (depending on the scheme),
75-
* we have no primary key. And without a username and password,
76-
* we are not actually storing a credential.
77-
*/
78-
if (!c->protocol || !(c->host || c->path) ||
79-
!c->username || !c->password)
80-
return;
81-
8274
strbuf_addf(&buf, "%s://", c->protocol);
8375
strbuf_addstr_urlencode(&buf, c->username, 1);
8476
strbuf_addch(&buf, ':');
@@ -95,8 +87,37 @@ static void store_credential(const char *fn, struct credential *c)
9587
strbuf_release(&buf);
9688
}
9789

98-
static void remove_credential(const char *fn, struct credential *c)
90+
static void store_credential(const struct string_list *fns, struct credential *c)
91+
{
92+
struct string_list_item *fn;
93+
94+
/*
95+
* Sanity check that what we are storing is actually sensible.
96+
* In particular, we can't make a URL without a protocol field.
97+
* Without either a host or pathname (depending on the scheme),
98+
* we have no primary key. And without a username and password,
99+
* we are not actually storing a credential.
100+
*/
101+
if (!c->protocol || !(c->host || c->path) || !c->username || !c->password)
102+
return;
103+
104+
for_each_string_list_item(fn, fns)
105+
if (!access(fn->string, F_OK)) {
106+
store_credential_file(fn->string, c);
107+
return;
108+
}
109+
/*
110+
* Write credential to the filename specified by fns->items[0], thus
111+
* creating it
112+
*/
113+
if (fns->nr)
114+
store_credential_file(fns->items[0].string, c);
115+
}
116+
117+
static void remove_credential(const struct string_list *fns, struct credential *c)
99118
{
119+
struct string_list_item *fn;
120+
100121
/*
101122
* Sanity check that we actually have something to match
102123
* against. The input we get is a restrictive pattern,
@@ -105,14 +126,20 @@ static void remove_credential(const char *fn, struct credential *c)
105126
* to empty input. So explicitly disallow it, and require that the
106127
* pattern have some actual content to match.
107128
*/
108-
if (c->protocol || c->host || c->path || c->username)
109-
rewrite_credential_file(fn, c, NULL);
129+
if (!c->protocol && !c->host && !c->path && !c->username)
130+
return;
131+
for_each_string_list_item(fn, fns)
132+
if (!access(fn->string, F_OK))
133+
rewrite_credential_file(fn->string, c, NULL);
110134
}
111135

112-
static int lookup_credential(const char *fn, struct credential *c)
136+
static void lookup_credential(const struct string_list *fns, struct credential *c)
113137
{
114-
parse_credential_file(fn, c, print_entry, NULL);
115-
return c->username && c->password;
138+
struct string_list_item *fn;
139+
140+
for_each_string_list_item(fn, fns)
141+
if (parse_credential_file(fn->string, c, print_entry, NULL))
142+
return; /* Found credential */
116143
}
117144

118145
int main(int argc, char **argv)
@@ -123,6 +150,7 @@ int main(int argc, char **argv)
123150
};
124151
const char *op;
125152
struct credential c = CREDENTIAL_INIT;
153+
struct string_list fns = STRING_LIST_INIT_DUP;
126154
char *file = NULL;
127155
struct option options[] = {
128156
OPT_STRING(0, "file", &file, "path",
@@ -137,22 +165,30 @@ int main(int argc, char **argv)
137165
usage_with_options(usage, options);
138166
op = argv[0];
139167

140-
if (!file)
141-
file = expand_user_path("~/.git-credentials");
142-
if (!file)
168+
if (file) {
169+
string_list_append(&fns, file);
170+
} else {
171+
if ((file = expand_user_path("~/.git-credentials")))
172+
string_list_append_nodup(&fns, file);
173+
home_config_paths(NULL, &file, "credentials");
174+
if (file)
175+
string_list_append_nodup(&fns, file);
176+
}
177+
if (!fns.nr)
143178
die("unable to set up default path; use --file");
144179

145180
if (credential_read(&c, stdin) < 0)
146181
die("unable to read credential");
147182

148183
if (!strcmp(op, "get"))
149-
lookup_credential(file, &c);
184+
lookup_credential(&fns, &c);
150185
else if (!strcmp(op, "erase"))
151-
remove_credential(file, &c);
186+
remove_credential(&fns, &c);
152187
else if (!strcmp(op, "store"))
153-
store_credential(file, &c);
188+
store_credential(&fns, &c);
154189
else
155190
; /* Ignore unknown operation. */
156191

192+
string_list_clear(&fns, 0);
157193
return 0;
158194
}

t/t0302-credential-store.sh

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,118 @@ test_description='credential-store tests'
66

77
helper_test store
88

9+
test_expect_success 'when xdg file does not exist, xdg file not created' '
10+
test_path_is_missing "$HOME/.config/git/credentials" &&
11+
test -s "$HOME/.git-credentials"
12+
'
13+
14+
test_expect_success 'setup xdg file' '
15+
rm -f "$HOME/.git-credentials" &&
16+
mkdir -p "$HOME/.config/git" &&
17+
>"$HOME/.config/git/credentials"
18+
'
19+
20+
helper_test store
21+
22+
test_expect_success 'when xdg file exists, home file not created' '
23+
test -s "$HOME/.config/git/credentials" &&
24+
test_path_is_missing "$HOME/.git-credentials"
25+
'
26+
27+
test_expect_success 'setup custom xdg file' '
28+
rm -f "$HOME/.git-credentials" &&
29+
rm -f "$HOME/.config/git/credentials" &&
30+
mkdir -p "$HOME/xdg/git" &&
31+
>"$HOME/xdg/git/credentials"
32+
'
33+
34+
XDG_CONFIG_HOME="$HOME/xdg"
35+
export XDG_CONFIG_HOME
36+
helper_test store
37+
unset XDG_CONFIG_HOME
38+
39+
test_expect_success 'if custom xdg file exists, home and xdg files not created' '
40+
test_when_finished "rm -f $HOME/xdg/git/credentials" &&
41+
test -s "$HOME/xdg/git/credentials" &&
42+
test_path_is_missing "$HOME/.git-credentials" &&
43+
test_path_is_missing "$HOME/.config/git/credentials"
44+
'
45+
46+
test_expect_success 'get: use home file if both home and xdg files have matches' '
47+
echo "https://home-user:[email protected]" >"$HOME/.git-credentials" &&
48+
mkdir -p "$HOME/.config/git" &&
49+
echo "https://xdg-user:[email protected]" >"$HOME/.config/git/credentials" &&
50+
check fill store <<-\EOF
51+
protocol=https
52+
host=example.com
53+
--
54+
protocol=https
55+
host=example.com
56+
username=home-user
57+
password=home-pass
58+
--
59+
EOF
60+
'
61+
62+
test_expect_success 'get: use xdg file if home file has no matches' '
63+
>"$HOME/.git-credentials" &&
64+
mkdir -p "$HOME/.config/git" &&
65+
echo "https://xdg-user:[email protected]" >"$HOME/.config/git/credentials" &&
66+
check fill store <<-\EOF
67+
protocol=https
68+
host=example.com
69+
--
70+
protocol=https
71+
host=example.com
72+
username=xdg-user
73+
password=xdg-pass
74+
--
75+
EOF
76+
'
77+
78+
test_expect_success POSIXPERM 'get: use xdg file if home file is unreadable' '
79+
echo "https://home-user:[email protected]" >"$HOME/.git-credentials" &&
80+
chmod -r "$HOME/.git-credentials" &&
81+
mkdir -p "$HOME/.config/git" &&
82+
echo "https://xdg-user:[email protected]" >"$HOME/.config/git/credentials" &&
83+
check fill store <<-\EOF
84+
protocol=https
85+
host=example.com
86+
--
87+
protocol=https
88+
host=example.com
89+
username=xdg-user
90+
password=xdg-pass
91+
--
92+
EOF
93+
'
94+
95+
test_expect_success 'store: if both xdg and home files exist, only store in home file' '
96+
>"$HOME/.git-credentials" &&
97+
mkdir -p "$HOME/.config/git" &&
98+
>"$HOME/.config/git/credentials" &&
99+
check approve store <<-\EOF &&
100+
protocol=https
101+
host=example.com
102+
username=store-user
103+
password=store-pass
104+
EOF
105+
echo "https://store-user:[email protected]" >expected &&
106+
test_cmp expected "$HOME/.git-credentials" &&
107+
test_must_be_empty "$HOME/.config/git/credentials"
108+
'
109+
110+
111+
test_expect_success 'erase: erase matching credentials from both xdg and home files' '
112+
echo "https://home-user:[email protected]" >"$HOME/.git-credentials" &&
113+
mkdir -p "$HOME/.config/git" &&
114+
echo "https://xdg-user:[email protected]" >"$HOME/.config/git/credentials" &&
115+
check reject store <<-\EOF &&
116+
protocol=https
117+
host=example.com
118+
EOF
119+
test_must_be_empty "$HOME/.git-credentials" &&
120+
test_must_be_empty "$HOME/.config/git/credentials"
121+
'
122+
9123
test_done

0 commit comments

Comments
 (0)