Merge branch 'ps/config-env-pairs'

Introduce two new ways to feed configuration variable-value pairs
via environment variables, and tweak the way GIT_CONFIG_PARAMETERS
encodes variable/value pairs to make it more robust.

* ps/config-env-pairs:
  config: allow specifying config entries via envvar pairs
  environment: make `getenv_safe()` a public function
  config: store "git -c" variables using more robust format
  config: parse more robust format in GIT_CONFIG_PARAMETERS
  config: extract function to parse config pairs
  quote: make sq_dequote_step() a public function
  config: add new way to pass config via `--config-env`
  git: add `--super-prefix` to usage string
This commit is contained in:
Junio C Hamano 2021-01-25 14:19:19 -08:00
commit 294e949fa2
11 changed files with 491 additions and 39 deletions

View File

@ -346,6 +346,22 @@ GIT_CONFIG_NOSYSTEM::
See also <<FILES>>.
GIT_CONFIG_COUNT::
GIT_CONFIG_KEY_<n>::
GIT_CONFIG_VALUE_<n>::
If GIT_CONFIG_COUNT is set to a positive number, all environment pairs
GIT_CONFIG_KEY_<n> and GIT_CONFIG_VALUE_<n> up to that number will be
added to the process's runtime configuration. The config pairs are
zero-indexed. Any missing key or value is treated as an error. An empty
GIT_CONFIG_COUNT is treated the same as GIT_CONFIG_COUNT=0, namely no
pairs are processed. These environment variables will override values
in configuration files, but will be overridden by any explicit options
passed via `git -c`.
+
This is useful for cases where you want to spawn multiple git commands
with a common configuration but cannot depend on a configuration file,
for example when writing scripts.
[[EXAMPLES]]
EXAMPLES

View File

@ -13,7 +13,7 @@ SYNOPSIS
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p|--paginate|-P|--no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
[--super-prefix=<path>]
[--super-prefix=<path>] [--config-env <name>=<envvar>]
<command> [<args>]
DESCRIPTION
@ -80,6 +80,28 @@ config file). Including the equals but with an empty value (like `git -c
foo.bar= ...`) sets `foo.bar` to the empty string which `git config
--type=bool` will convert to `false`.
--config-env=<name>=<envvar>::
Like `-c <name>=<value>`, give configuration variable
'<name>' a value, where <envvar> is the name of an
environment variable from which to retrieve the value. Unlike
`-c` there is no shortcut for directly setting the value to an
empty string, instead the environment variable itself must be
set to the empty string. It is an error if the `<envvar>` does not exist
in the environment. `<envvar>` may not contain an equals sign
to avoid ambiguity with `<name>`s which contain one.
+
This is useful for cases where you want to pass transitory
configuration options to git, but are doing so on OS's where
other processes might be able to read your cmdline
(e.g. `/proc/self/cmdline`), but not your environ
(e.g. `/proc/self/environ`). That behavior is the default on
Linux, but may not be on your system.
+
Note that this might add security for variables such as
`http.extraHeader` where the sensitive information is part of
the value, but not e.g. `url.<base>.insteadOf` where the
sensitive information can be part of the key.
--exec-path[=<path>]::
Path to wherever your core Git programs are installed.
This can also be controlled by setting the GIT_EXEC_PATH

View File

@ -472,6 +472,7 @@ static inline enum object_type object_type(unsigned int mode)
#define TEMPLATE_DIR_ENVIRONMENT "GIT_TEMPLATE_DIR"
#define CONFIG_ENVIRONMENT "GIT_CONFIG"
#define CONFIG_DATA_ENVIRONMENT "GIT_CONFIG_PARAMETERS"
#define CONFIG_COUNT_ENVIRONMENT "GIT_CONFIG_COUNT"
#define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH"
#define CEILING_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES"
#define NO_REPLACE_OBJECTS_ENVIRONMENT "GIT_NO_REPLACE_OBJECTS"

209
config.c
View File

@ -8,6 +8,7 @@
#include "cache.h"
#include "branch.h"
#include "config.h"
#include "environment.h"
#include "repository.h"
#include "lockfile.h"
#include "exec-cmd.h"
@ -332,7 +333,7 @@ int git_config_include(const char *var, const char *value, void *data)
return ret;
}
void git_config_push_parameter(const char *text)
static void git_config_push_split_parameter(const char *key, const char *value)
{
struct strbuf env = STRBUF_INIT;
const char *old = getenv(CONFIG_DATA_ENVIRONMENT);
@ -340,11 +341,74 @@ void git_config_push_parameter(const char *text)
strbuf_addstr(&env, old);
strbuf_addch(&env, ' ');
}
sq_quote_buf(&env, text);
sq_quote_buf(&env, key);
strbuf_addch(&env, '=');
if (value)
sq_quote_buf(&env, value);
setenv(CONFIG_DATA_ENVIRONMENT, env.buf, 1);
strbuf_release(&env);
}
void git_config_push_parameter(const char *text)
{
const char *value;
/*
* When we see:
*
* section.subsection=with=equals.key=value
*
* we cannot tell if it means:
*
* [section "subsection=with=equals"]
* key = value
*
* or:
*
* [section]
* subsection = with=equals.key=value
*
* We parse left-to-right for the first "=", meaning we'll prefer to
* keep the value intact over the subsection. This is historical, but
* also sensible since values are more likely to contain odd or
* untrusted input than a section name.
*
* A missing equals is explicitly allowed (as a bool-only entry).
*/
value = strchr(text, '=');
if (value) {
char *key = xmemdupz(text, value - text);
git_config_push_split_parameter(key, value + 1);
free(key);
} else {
git_config_push_split_parameter(text, NULL);
}
}
void git_config_push_env(const char *spec)
{
char *key;
const char *env_name;
const char *env_value;
env_name = strrchr(spec, '=');
if (!env_name)
die(_("invalid config format: %s"), spec);
key = xmemdupz(spec, env_name - spec);
env_name++;
if (!*env_name)
die(_("missing environment variable name for configuration '%.*s'"),
(int)(env_name - spec - 1), spec);
env_value = getenv(env_name);
if (!env_value)
die(_("missing environment variable '%s' for configuration '%.*s'"),
env_name, (int)(env_name - spec - 1), spec);
git_config_push_split_parameter(key, env_value);
free(key);
}
static inline int iskeychar(int c)
{
return isalnum(c) || c == '-';
@ -437,11 +501,26 @@ int git_config_key_is_valid(const char *key)
return !git_config_parse_key_1(key, NULL, NULL, 1);
}
static int config_parse_pair(const char *key, const char *value,
config_fn_t fn, void *data)
{
char *canonical_name;
int ret;
if (!strlen(key))
return error(_("empty config key"));
if (git_config_parse_key(key, &canonical_name, NULL))
return -1;
ret = (fn(canonical_name, value, data) < 0) ? -1 : 0;
free(canonical_name);
return ret;
}
int git_config_parse_parameter(const char *text,
config_fn_t fn, void *data)
{
const char *value;
char *canonical_name;
struct strbuf **pair;
int ret;
@ -462,51 +541,131 @@ int git_config_parse_parameter(const char *text,
return error(_("bogus config parameter: %s"), text);
}
if (git_config_parse_key(pair[0]->buf, &canonical_name, NULL)) {
ret = -1;
} else {
ret = (fn(canonical_name, value, data) < 0) ? -1 : 0;
free(canonical_name);
}
ret = config_parse_pair(pair[0]->buf, value, fn, data);
strbuf_list_free(pair);
return ret;
}
static int parse_config_env_list(char *env, config_fn_t fn, void *data)
{
char *cur = env;
while (cur && *cur) {
const char *key = sq_dequote_step(cur, &cur);
if (!key)
return error(_("bogus format in %s"),
CONFIG_DATA_ENVIRONMENT);
if (!cur || isspace(*cur)) {
/* old-style 'key=value' */
if (git_config_parse_parameter(key, fn, data) < 0)
return -1;
}
else if (*cur == '=') {
/* new-style 'key'='value' */
const char *value;
cur++;
if (*cur == '\'') {
/* quoted value */
value = sq_dequote_step(cur, &cur);
if (!value || (cur && !isspace(*cur))) {
return error(_("bogus format in %s"),
CONFIG_DATA_ENVIRONMENT);
}
} else if (!*cur || isspace(*cur)) {
/* implicit bool: 'key'= */
value = NULL;
} else {
return error(_("bogus format in %s"),
CONFIG_DATA_ENVIRONMENT);
}
if (config_parse_pair(key, value, fn, data) < 0)
return -1;
}
else {
/* unknown format */
return error(_("bogus format in %s"),
CONFIG_DATA_ENVIRONMENT);
}
if (cur) {
while (isspace(*cur))
cur++;
}
}
return 0;
}
int git_config_from_parameters(config_fn_t fn, void *data)
{
const char *env = getenv(CONFIG_DATA_ENVIRONMENT);
const char *env;
struct strbuf envvar = STRBUF_INIT;
struct strvec to_free = STRVEC_INIT;
int ret = 0;
char *envw;
const char **argv = NULL;
int nr = 0, alloc = 0;
int i;
char *envw = NULL;
struct config_source source;
if (!env)
return 0;
memset(&source, 0, sizeof(source));
source.prev = cf;
source.origin_type = CONFIG_ORIGIN_CMDLINE;
cf = &source;
/* sq_dequote will write over it */
envw = xstrdup(env);
env = getenv(CONFIG_COUNT_ENVIRONMENT);
if (env) {
unsigned long count;
char *endp;
int i;
if (sq_dequote_to_argv(envw, &argv, &nr, &alloc) < 0) {
ret = error(_("bogus format in %s"), CONFIG_DATA_ENVIRONMENT);
goto out;
count = strtoul(env, &endp, 10);
if (*endp) {
ret = error(_("bogus count in %s"), CONFIG_COUNT_ENVIRONMENT);
goto out;
}
if (count > INT_MAX) {
ret = error(_("too many entries in %s"), CONFIG_COUNT_ENVIRONMENT);
goto out;
}
for (i = 0; i < count; i++) {
const char *key, *value;
strbuf_addf(&envvar, "GIT_CONFIG_KEY_%d", i);
key = getenv_safe(&to_free, envvar.buf);
if (!key) {
ret = error(_("missing config key %s"), envvar.buf);
goto out;
}
strbuf_reset(&envvar);
strbuf_addf(&envvar, "GIT_CONFIG_VALUE_%d", i);
value = getenv_safe(&to_free, envvar.buf);
if (!value) {
ret = error(_("missing config value %s"), envvar.buf);
goto out;
}
strbuf_reset(&envvar);
if (config_parse_pair(key, value, fn, data) < 0) {
ret = -1;
goto out;
}
}
}
for (i = 0; i < nr; i++) {
if (git_config_parse_parameter(argv[i], fn, data) < 0) {
env = getenv(CONFIG_DATA_ENVIRONMENT);
if (env) {
/* sq_dequote will write over it */
envw = xstrdup(env);
if (parse_config_env_list(envw, fn, data) < 0) {
ret = -1;
goto out;
}
}
out:
free(argv);
strbuf_release(&envvar);
strvec_clear(&to_free);
free(envw);
cf = source.prev;
return ret;

View File

@ -138,6 +138,7 @@ int git_config_from_mem(config_fn_t fn,
int git_config_from_blob_oid(config_fn_t fn, const char *name,
const struct object_id *oid, void *data);
void git_config_push_parameter(const char *text);
void git_config_push_env(const char *spec);
int git_config_from_parameters(config_fn_t fn, void *data);
void read_early_config(config_fn_t cb, void *data);
void read_very_early_config(config_fn_t cb, void *data);

View File

@ -9,6 +9,7 @@
*/
#include "cache.h"
#include "branch.h"
#include "environment.h"
#include "repository.h"
#include "config.h"
#include "refs.h"
@ -116,6 +117,7 @@ const char * const local_repo_env[] = {
ALTERNATE_DB_ENVIRONMENT,
CONFIG_ENVIRONMENT,
CONFIG_DATA_ENVIRONMENT,
CONFIG_COUNT_ENVIRONMENT,
DB_ENVIRONMENT,
GIT_DIR_ENVIRONMENT,
GIT_WORK_TREE_ENVIRONMENT,
@ -152,11 +154,7 @@ static char *expand_namespace(const char *raw_namespace)
return strbuf_detach(&buf, NULL);
}
/*
* Wrapper of getenv() that returns a strdup value. This value is kept
* in argv to be freed later.
*/
static const char *getenv_safe(struct strvec *argv, const char *name)
const char *getenv_safe(struct strvec *argv, const char *name)
{
const char *value = getenv(name);

12
environment.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef ENVIRONMENT_H
#define ENVIRONMENT_H
#include "strvec.h"
/*
* Wrapper of getenv() that returns a strdup value. This value is kept
* in argv to be freed later.
*/
const char *getenv_safe(struct strvec *argv, const char *name);
#endif

3
git.c
View File

@ -29,6 +29,7 @@ const char git_usage_string[] =
" [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]\n"
" [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]\n"
" [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]\n"
" [--super-prefix=<path>] [--config-env=<name>=<envvar>]\n"
" <command> [<args>]");
const char git_more_info_string[] =
@ -254,6 +255,8 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
git_config_push_parameter((*argv)[1]);
(*argv)++;
(*argc)--;
} else if (skip_prefix(cmd, "--config-env=", &cmd)) {
git_config_push_env(cmd);
} else if (!strcmp(cmd, "--literal-pathspecs")) {
setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "1", 1);
if (envchanged)

15
quote.c
View File

@ -116,7 +116,7 @@ void sq_append_quote_argv_pretty(struct strbuf *dst, const char **argv)
}
}
static char *sq_dequote_step(char *arg, char **next)
char *sq_dequote_step(char *arg, char **next)
{
char *dst = arg;
char *src = arg;
@ -153,11 +153,8 @@ static char *sq_dequote_step(char *arg, char **next)
}
/* Fallthrough */
default:
if (!next || !isspace(*src))
if (!next)
return NULL;
do {
c = *++src;
} while (isspace(c));
*dst = 0;
*next = src;
return arg;
@ -182,6 +179,14 @@ static int sq_dequote_to_argv_internal(char *arg,
char *dequoted = sq_dequote_step(next, &next);
if (!dequoted)
return -1;
if (next) {
char c;
if (!isspace(*next))
return -1;
do {
c = *++next;
} while (isspace(c));
}
if (argv) {
ALLOC_GROW(*argv, *nr + 1, *alloc);
(*argv)[(*nr)++] = dequoted;

18
quote.h
View File

@ -42,12 +42,26 @@ void sq_quote_buf_pretty(struct strbuf *, const char *src);
void sq_quote_argv_pretty(struct strbuf *, const char **argv);
void sq_append_quote_argv_pretty(struct strbuf *dst, const char **argv);
/* This unwraps what sq_quote() produces in place, but returns
/*
* This unwraps what sq_quote() produces in place, but returns
* NULL if the input does not look like what sq_quote would have
* produced.
* produced (the full string must be a single quoted item).
*/
char *sq_dequote(char *);
/*
* Like sq_dequote(), but dequote a single item, and leave "next" pointing to
* the next character. E.g., in the string:
*
* 'one' 'two' 'three'
*
* after the first call, the return value would be the unquoted string "one",
* with "next" pointing to the space between "one" and "two"). The caller is
* responsible for advancing the pointer to the start of the next item before
* calling sq_dequote_step() again.
*/
char *sq_dequote_step(char *src, char **next);
/*
* Same as the above, but can be used to unwrap many arguments in the
* same string separated by space. Like sq_quote, it works in place,

View File

@ -1289,6 +1289,58 @@ test_expect_success 'git -c is not confused by empty environment' '
GIT_CONFIG_PARAMETERS="" git -c x.one=1 config --list
'
test_expect_success 'GIT_CONFIG_PARAMETERS handles old-style entries' '
v="${SQ}key.one=foo${SQ}" &&
v="$v ${SQ}key.two=bar${SQ}" &&
v="$v ${SQ}key.ambiguous=section.whatever=value${SQ}" &&
GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
cat >expect <<-EOF &&
key.one foo
key.two bar
key.ambiguous section.whatever=value
EOF
test_cmp expect actual
'
test_expect_success 'GIT_CONFIG_PARAMETERS handles new-style entries' '
v="${SQ}key.one${SQ}=${SQ}foo${SQ}" &&
v="$v ${SQ}key.two${SQ}=${SQ}bar${SQ}" &&
v="$v ${SQ}key.ambiguous=section.whatever${SQ}=${SQ}value${SQ}" &&
GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
cat >expect <<-EOF &&
key.one foo
key.two bar
key.ambiguous=section.whatever value
EOF
test_cmp expect actual
'
test_expect_success 'old and new-style entries can mix' '
v="${SQ}key.oldone=oldfoo${SQ}" &&
v="$v ${SQ}key.newone${SQ}=${SQ}newfoo${SQ}" &&
v="$v ${SQ}key.oldtwo=oldbar${SQ}" &&
v="$v ${SQ}key.newtwo${SQ}=${SQ}newbar${SQ}" &&
GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
cat >expect <<-EOF &&
key.oldone oldfoo
key.newone newfoo
key.oldtwo oldbar
key.newtwo newbar
EOF
test_cmp expect actual
'
test_expect_success 'old and new bools with ambiguous subsection' '
v="${SQ}key.with=equals.oldbool${SQ}" &&
v="$v ${SQ}key.with=equals.newbool${SQ}=" &&
GIT_CONFIG_PARAMETERS=$v git config --get-regexp "key.*" >actual &&
cat >expect <<-EOF &&
key.with equals.oldbool
key.with=equals.newbool
EOF
test_cmp expect actual
'
test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
cat >expect <<-\EOF &&
env.one one
@ -1311,6 +1363,173 @@ test_expect_success 'detect bogus GIT_CONFIG_PARAMETERS' '
git config --get-regexp "env.*"
'
test_expect_success 'git --config-env=key=envvar support' '
cat >expect <<-\EOF &&
value
value
false
EOF
{
ENVVAR=value git --config-env=core.name=ENVVAR config core.name &&
ENVVAR=value git --config-env=foo.CamelCase=ENVVAR config foo.camelcase &&
ENVVAR= git --config-env=foo.flag=ENVVAR config --bool foo.flag
} >actual &&
test_cmp expect actual
'
test_expect_success 'git --config-env fails with invalid parameters' '
test_must_fail git --config-env=foo.flag config --bool foo.flag 2>error &&
test_i18ngrep "invalid config format: foo.flag" error &&
test_must_fail git --config-env=foo.flag= config --bool foo.flag 2>error &&
test_i18ngrep "missing environment variable name for configuration ${SQ}foo.flag${SQ}" error &&
sane_unset NONEXISTENT &&
test_must_fail git --config-env=foo.flag=NONEXISTENT config --bool foo.flag 2>error &&
test_i18ngrep "missing environment variable ${SQ}NONEXISTENT${SQ} for configuration ${SQ}foo.flag${SQ}" error
'
test_expect_success 'git -c and --config-env work together' '
cat >expect <<-\EOF &&
bar.cmd cmd-value
bar.env env-value
EOF
ENVVAR=env-value git \
-c bar.cmd=cmd-value \
--config-env=bar.env=ENVVAR \
config --get-regexp "^bar.*" >actual &&
test_cmp expect actual
'
test_expect_success 'git -c and --config-env override each other' '
cat >expect <<-\EOF &&
env
cmd
EOF
{
ENVVAR=env git -c bar.bar=cmd --config-env=bar.bar=ENVVAR config bar.bar &&
ENVVAR=env git --config-env=bar.bar=ENVVAR -c bar.bar=cmd config bar.bar
} >actual &&
test_cmp expect actual
'
test_expect_success '--config-env handles keys with equals' '
echo value=with=equals >expect &&
ENVVAR=value=with=equals git \
--config-env=section.subsection=with=equals.key=ENVVAR \
config section.subsection=with=equals.key >actual &&
test_cmp expect actual
'
test_expect_success 'git config handles environment config pairs' '
GIT_CONFIG_COUNT=2 \
GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="foo" \
GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="bar" \
git config --get-regexp "pair.*" >actual &&
cat >expect <<-EOF &&
pair.one foo
pair.two bar
EOF
test_cmp expect actual
'
test_expect_success 'git config ignores pairs without count' '
test_must_fail env GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
git config pair.one 2>error &&
test_must_be_empty error
'
test_expect_success 'git config ignores pairs with zero count' '
test_must_fail env \
GIT_CONFIG_COUNT=0 \
GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
git config pair.one
'
test_expect_success 'git config ignores pairs exceeding count' '
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
GIT_CONFIG_KEY_1="pair.two" GIT_CONFIG_VALUE_1="value" \
git config --get-regexp "pair.*" >actual &&
cat >expect <<-EOF &&
pair.one value
EOF
test_cmp expect actual
'
test_expect_success 'git config ignores pairs with zero count' '
test_must_fail env \
GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
git config pair.one >error &&
test_must_be_empty error
'
test_expect_success 'git config ignores pairs with empty count' '
test_must_fail env \
GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0="pair.one" GIT_CONFIG_VALUE_0="value" \
git config pair.one >error &&
test_must_be_empty error
'
test_expect_success 'git config fails with invalid count' '
test_must_fail env GIT_CONFIG_COUNT=10a git config --list 2>error &&
test_i18ngrep "bogus count" error &&
test_must_fail env GIT_CONFIG_COUNT=9999999999999999 git config --list 2>error &&
test_i18ngrep "too many entries" error
'
test_expect_success 'git config fails with missing config key' '
test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_VALUE_0="value" \
git config --list 2>error &&
test_i18ngrep "missing config key" error
'
test_expect_success 'git config fails with missing config value' '
test_must_fail env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="pair.one" \
git config --list 2>error &&
test_i18ngrep "missing config value" error
'
test_expect_success 'git config fails with invalid config pair key' '
test_must_fail env GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0= GIT_CONFIG_VALUE_0=value \
git config --list &&
test_must_fail env GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0=missing-section GIT_CONFIG_VALUE_0=value \
git config --list
'
test_expect_success 'environment overrides config file' '
test_when_finished "rm -f .git/config" &&
cat >.git/config <<-EOF &&
[pair]
one = value
EOF
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=override \
git config pair.one >actual &&
cat >expect <<-EOF &&
override
EOF
test_cmp expect actual
'
test_expect_success 'GIT_CONFIG_PARAMETERS overrides environment config' '
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
GIT_CONFIG_PARAMETERS="${SQ}pair.one=override${SQ}" \
git config pair.one >actual &&
cat >expect <<-EOF &&
override
EOF
test_cmp expect actual
'
test_expect_success 'command line overrides environment config' '
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=pair.one GIT_CONFIG_VALUE_0=value \
git -c pair.one=override config pair.one >actual &&
cat >expect <<-EOF &&
override
EOF
test_cmp expect actual
'
test_expect_success 'git config --edit works' '
git config -f tmp test.value no &&
echo test.value=yes >expect &&
@ -1656,9 +1875,11 @@ test_expect_success '--show-origin with --list' '
file:.git/config user.override=local
file:.git/config include.path=../include/relative.include
file:.git/../include/relative.include user.relative=include
command line: user.environ=true
command line: user.cmdline=true
EOF
git -c user.cmdline=true config --list --show-origin >output &&
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.environ GIT_CONFIG_VALUE_0=true\
git -c user.cmdline=true config --list --show-origin >output &&
test_cmp expect output
'