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
This commit is contained in:
Junio C Hamano 2015-05-11 14:23:38 -07:00
commit 17c7f4d8e4
3 changed files with 210 additions and 29 deletions

View File

@ -31,10 +31,41 @@ OPTIONS
--file=<path>::
Use `<path>` to store credentials. The file will have its
Use `<path>` to lookup and store credentials. The file will have its
filesystem permissions set to prevent other users on the system
from reading it, but will not be encrypted or otherwise
protected. Defaults to `~/.git-credentials`.
protected. If not specified, credentials will be searched for from
`~/.git-credentials` and `$XDG_CONFIG_HOME/git/credentials`, and
credentials will be written to `~/.git-credentials` if it exists, or
`$XDG_CONFIG_HOME/git/credentials` if it exists and the former does
not. See also <<FILES>>.
[[FILES]]
FILES
-----
If not set explicitly with '--file', there are two files where
git-credential-store will search for credentials in order of precedence:
~/.git-credentials::
User-specific credentials file.
$XDG_CONFIG_HOME/git/credentials::
Second user-specific credentials file. If '$XDG_CONFIG_HOME' is not set
or empty, `$HOME/.config/git/credentials` will be used. Any credentials
stored in this file will not be used if `~/.git-credentials` has a
matching credential as well. It is a good idea not to create this file
if you sometimes use older versions of Git that do not support it.
For credential lookups, the files are read in the order given above, with the
first matching credential found taking precedence over credentials found in
files further down the list.
Credential storage will by default write to the first existing file in the
list. If none of these files exist, `~/.git-credentials` will be created and
written to.
When erasing credentials, matching credentials will be erased from all files.
EXAMPLES
--------

View File

@ -6,7 +6,7 @@
static struct lock_file credential_lock;
static void parse_credential_file(const char *fn,
static int parse_credential_file(const char *fn,
struct credential *c,
void (*match_cb)(struct credential *),
void (*other_cb)(struct strbuf *))
@ -14,18 +14,20 @@ static void parse_credential_file(const char *fn,
FILE *fh;
struct strbuf line = STRBUF_INIT;
struct credential entry = CREDENTIAL_INIT;
int found_credential = 0;
fh = fopen(fn, "r");
if (!fh) {
if (errno != ENOENT)
if (errno != ENOENT && errno != EACCES)
die_errno("unable to open %s", fn);
return;
return found_credential;
}
while (strbuf_getline(&line, fh, '\n') != EOF) {
credential_from_url(&entry, line.buf);
if (entry.username && entry.password &&
credential_match(c, &entry)) {
found_credential = 1;
if (match_cb) {
match_cb(&entry);
break;
@ -38,6 +40,7 @@ static void parse_credential_file(const char *fn,
credential_clear(&entry);
strbuf_release(&line);
fclose(fh);
return found_credential;
}
static void print_entry(struct credential *c)
@ -64,21 +67,10 @@ static void rewrite_credential_file(const char *fn, struct credential *c,
die_errno("unable to commit credential store");
}
static void store_credential(const char *fn, struct credential *c)
static void store_credential_file(const char *fn, struct credential *c)
{
struct strbuf buf = STRBUF_INIT;
/*
* Sanity check that what we are storing is actually sensible.
* In particular, we can't make a URL without a protocol field.
* Without either a host or pathname (depending on the scheme),
* we have no primary key. And without a username and password,
* we are not actually storing a credential.
*/
if (!c->protocol || !(c->host || c->path) ||
!c->username || !c->password)
return;
strbuf_addf(&buf, "%s://", c->protocol);
strbuf_addstr_urlencode(&buf, c->username, 1);
strbuf_addch(&buf, ':');
@ -95,8 +87,37 @@ static void store_credential(const char *fn, struct credential *c)
strbuf_release(&buf);
}
static void remove_credential(const char *fn, struct credential *c)
static void store_credential(const struct string_list *fns, struct credential *c)
{
struct string_list_item *fn;
/*
* Sanity check that what we are storing is actually sensible.
* In particular, we can't make a URL without a protocol field.
* Without either a host or pathname (depending on the scheme),
* we have no primary key. And without a username and password,
* we are not actually storing a credential.
*/
if (!c->protocol || !(c->host || c->path) || !c->username || !c->password)
return;
for_each_string_list_item(fn, fns)
if (!access(fn->string, F_OK)) {
store_credential_file(fn->string, c);
return;
}
/*
* Write credential to the filename specified by fns->items[0], thus
* creating it
*/
if (fns->nr)
store_credential_file(fns->items[0].string, c);
}
static void remove_credential(const struct string_list *fns, struct credential *c)
{
struct string_list_item *fn;
/*
* Sanity check that we actually have something to match
* against. The input we get is a restrictive pattern,
@ -105,14 +126,20 @@ static void remove_credential(const char *fn, struct credential *c)
* to empty input. So explicitly disallow it, and require that the
* pattern have some actual content to match.
*/
if (c->protocol || c->host || c->path || c->username)
rewrite_credential_file(fn, c, NULL);
if (!c->protocol && !c->host && !c->path && !c->username)
return;
for_each_string_list_item(fn, fns)
if (!access(fn->string, F_OK))
rewrite_credential_file(fn->string, c, NULL);
}
static int lookup_credential(const char *fn, struct credential *c)
static void lookup_credential(const struct string_list *fns, struct credential *c)
{
parse_credential_file(fn, c, print_entry, NULL);
return c->username && c->password;
struct string_list_item *fn;
for_each_string_list_item(fn, fns)
if (parse_credential_file(fn->string, c, print_entry, NULL))
return; /* Found credential */
}
int main(int argc, char **argv)
@ -123,6 +150,7 @@ int main(int argc, char **argv)
};
const char *op;
struct credential c = CREDENTIAL_INIT;
struct string_list fns = STRING_LIST_INIT_DUP;
char *file = NULL;
struct option options[] = {
OPT_STRING(0, "file", &file, "path",
@ -137,22 +165,30 @@ int main(int argc, char **argv)
usage_with_options(usage, options);
op = argv[0];
if (!file)
file = expand_user_path("~/.git-credentials");
if (!file)
if (file) {
string_list_append(&fns, file);
} else {
if ((file = expand_user_path("~/.git-credentials")))
string_list_append_nodup(&fns, file);
home_config_paths(NULL, &file, "credentials");
if (file)
string_list_append_nodup(&fns, file);
}
if (!fns.nr)
die("unable to set up default path; use --file");
if (credential_read(&c, stdin) < 0)
die("unable to read credential");
if (!strcmp(op, "get"))
lookup_credential(file, &c);
lookup_credential(&fns, &c);
else if (!strcmp(op, "erase"))
remove_credential(file, &c);
remove_credential(&fns, &c);
else if (!strcmp(op, "store"))
store_credential(file, &c);
store_credential(&fns, &c);
else
; /* Ignore unknown operation. */
string_list_clear(&fns, 0);
return 0;
}

View File

@ -6,4 +6,118 @@ test_description='credential-store tests'
helper_test store
test_expect_success 'when xdg file does not exist, xdg file not created' '
test_path_is_missing "$HOME/.config/git/credentials" &&
test -s "$HOME/.git-credentials"
'
test_expect_success 'setup xdg file' '
rm -f "$HOME/.git-credentials" &&
mkdir -p "$HOME/.config/git" &&
>"$HOME/.config/git/credentials"
'
helper_test store
test_expect_success 'when xdg file exists, home file not created' '
test -s "$HOME/.config/git/credentials" &&
test_path_is_missing "$HOME/.git-credentials"
'
test_expect_success 'setup custom xdg file' '
rm -f "$HOME/.git-credentials" &&
rm -f "$HOME/.config/git/credentials" &&
mkdir -p "$HOME/xdg/git" &&
>"$HOME/xdg/git/credentials"
'
XDG_CONFIG_HOME="$HOME/xdg"
export XDG_CONFIG_HOME
helper_test store
unset XDG_CONFIG_HOME
test_expect_success 'if custom xdg file exists, home and xdg files not created' '
test_when_finished "rm -f $HOME/xdg/git/credentials" &&
test -s "$HOME/xdg/git/credentials" &&
test_path_is_missing "$HOME/.git-credentials" &&
test_path_is_missing "$HOME/.config/git/credentials"
'
test_expect_success 'get: use home file if both home and xdg files have matches' '
echo "https://home-user:home-pass@example.com" >"$HOME/.git-credentials" &&
mkdir -p "$HOME/.config/git" &&
echo "https://xdg-user:xdg-pass@example.com" >"$HOME/.config/git/credentials" &&
check fill store <<-\EOF
protocol=https
host=example.com
--
protocol=https
host=example.com
username=home-user
password=home-pass
--
EOF
'
test_expect_success 'get: use xdg file if home file has no matches' '
>"$HOME/.git-credentials" &&
mkdir -p "$HOME/.config/git" &&
echo "https://xdg-user:xdg-pass@example.com" >"$HOME/.config/git/credentials" &&
check fill store <<-\EOF
protocol=https
host=example.com
--
protocol=https
host=example.com
username=xdg-user
password=xdg-pass
--
EOF
'
test_expect_success POSIXPERM 'get: use xdg file if home file is unreadable' '
echo "https://home-user:home-pass@example.com" >"$HOME/.git-credentials" &&
chmod -r "$HOME/.git-credentials" &&
mkdir -p "$HOME/.config/git" &&
echo "https://xdg-user:xdg-pass@example.com" >"$HOME/.config/git/credentials" &&
check fill store <<-\EOF
protocol=https
host=example.com
--
protocol=https
host=example.com
username=xdg-user
password=xdg-pass
--
EOF
'
test_expect_success 'store: if both xdg and home files exist, only store in home file' '
>"$HOME/.git-credentials" &&
mkdir -p "$HOME/.config/git" &&
>"$HOME/.config/git/credentials" &&
check approve store <<-\EOF &&
protocol=https
host=example.com
username=store-user
password=store-pass
EOF
echo "https://store-user:store-pass@example.com" >expected &&
test_cmp expected "$HOME/.git-credentials" &&
test_must_be_empty "$HOME/.config/git/credentials"
'
test_expect_success 'erase: erase matching credentials from both xdg and home files' '
echo "https://home-user:home-pass@example.com" >"$HOME/.git-credentials" &&
mkdir -p "$HOME/.config/git" &&
echo "https://xdg-user:xdg-pass@example.com" >"$HOME/.config/git/credentials" &&
check reject store <<-\EOF &&
protocol=https
host=example.com
EOF
test_must_be_empty "$HOME/.git-credentials" &&
test_must_be_empty "$HOME/.config/git/credentials"
'
test_done