Message ID | d832f3dedf5bde4cd9389ddab734703ff2dbd5a1.1608104755.git.ps@pks.im (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | config: allow specifying config entries via env | expand |
Hi Patrick/Peff On 16/12/2020 07:57, Patrick Steinhardt wrote: > From: Jeff King <peff@peff.net> > > When we stuff config options into GIT_CONFIG_PARAMETERS, we shell-quote > each one as a single unit, like: > > 'section.one=value1' 'section.two=value2' > > On the reading side, we de-quote to get the individual strings, and then > parse them by splitting on the first "=" we find. This format is > ambiguous, because an "=" may appear in a subsection. So the config > represented in a file by both: > > [section "subsection=with=equals"] > key = value > > and: > > [section] > subsection = with=equals.key=value > > ends up in this flattened format like: > > 'section.subsection=with=equals.key=value' > > and we can't tell which was desired. We have traditionally resolved this > by taking the first "=" we see starting from the left, meaning that we > allowed arbitrary content in the value, but not in the subsection. I was just wondering what happens if a subsection name contains a single quote - can we handle that now and how is it affected by this change? Best Wishes Phillip > Let's make our environment format a bit more robust by separately > quoting the key and value. That turns those examples into: > > 'section.subsection=with=equals.key'='value' > > and: > > 'section.subsection'='with=equals.key=value' > > respectively, and we can tell the difference between them. We can detect > which format is in use for any given element of the list based on the > presence of the unquoted "=". That means we can continue to allow the > old format to work to support any callers which manually used the old > format, and we can even intermingle the two formats. The old format > wasn't documented, and nobody was supposed to be using it. But it's > likely that such callers exist in the wild, so it's nice if we can avoid > breaking them. Likewise, it may be possible to trigger an older version > of "git -c" that runs a script that calls into a newer version of "git > -c"; that new version would see the intermingled format. > > This does create one complication, which is that the obvious format in > the new scheme for > > [section] > some-bool > > is: > > 'section.some-bool' > > with no equals. We'd mistake that for an old-style variable. And it even > has the same meaning in the old style, but: > > [section "with=equals"] > some-bool > > does not. It would be: > > 'section.with=equals=some-bool' > > which we'd take to mean: > > [section] > with = equals=some-bool > > in the old, ambiguous style. Likewise, we can't use: > > 'section.some-bool'='' > > because that's ambiguous with an actual empty string. Instead, we'll > again use the shell-quoting to give us a hint, and use: > > 'section.some-bool'= > > to show that we have no value. > > Note that this commit just expands the reading side. We'll start writing > the new format via "git -c" in a future patch. In the meantime, the > existing "git -c" tests will make sure we didn't break reading the old > format. But we'll also add some explicit coverage of the two formats to > make sure we continue to handle the old one after we move the writing > side over. > > And one final note: since we're now using the shell-quoting as a > semantically meaningful hint, this closes the door to us ever allowing > arbitrary shell quoting, like: > > 'a'shell'would'be'ok'with'this'.key=value > > But we have never supported that (only what sq_quote() would produce), > and we are probably better off keeping things simple, robust, and > backwards-compatible, than trying to make it easier for humans. We'll > continue not to advertise the format of the variable to users, and > instead keep "git -c" as the recommended mechanism for setting config > (even if we are trying to be kind not to break users who may be relying > on the current undocumented format). > > Signed-off-by: Jeff King <peff@peff.net> > --- > config.c | 69 +++++++++++++++++++++++++++++++++++------------ > t/t1300-config.sh | 52 +++++++++++++++++++++++++++++++++++ > 2 files changed, 104 insertions(+), 17 deletions(-) > > diff --git a/config.c b/config.c > index 53ed048689..60a7261807 100644 > --- a/config.c > +++ b/config.c > @@ -541,14 +541,62 @@ int git_config_parse_parameter(const char *text, > 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); > int ret = 0; > char *envw; > - const char **argv = NULL; > - int nr = 0, alloc = 0; > - int i; > struct config_source source; > > if (!env) > @@ -561,21 +609,8 @@ int git_config_from_parameters(config_fn_t fn, void *data) > > /* sq_dequote will write over it */ > envw = xstrdup(env); > + ret = parse_config_env_list(envw, fn, data); > > - if (sq_dequote_to_argv(envw, &argv, &nr, &alloc) < 0) { > - ret = error(_("bogus format in %s"), CONFIG_DATA_ENVIRONMENT); > - goto out; > - } > - > - for (i = 0; i < nr; i++) { > - if (git_config_parse_parameter(argv[i], fn, data) < 0) { > - ret = -1; > - goto out; > - } > - } > - > -out: > - free(argv); > free(envw); > cf = source.prev; > return ret; > diff --git a/t/t1300-config.sh b/t/t1300-config.sh > index 36a60879f6..35a1a6e8b1 100755 > --- a/t/t1300-config.sh > +++ b/t/t1300-config.sh > @@ -1294,6 +1294,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 >
diff --git a/config.c b/config.c index 53ed048689..60a7261807 100644 --- a/config.c +++ b/config.c @@ -541,14 +541,62 @@ int git_config_parse_parameter(const char *text, 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); int ret = 0; char *envw; - const char **argv = NULL; - int nr = 0, alloc = 0; - int i; struct config_source source; if (!env) @@ -561,21 +609,8 @@ int git_config_from_parameters(config_fn_t fn, void *data) /* sq_dequote will write over it */ envw = xstrdup(env); + ret = parse_config_env_list(envw, fn, data); - if (sq_dequote_to_argv(envw, &argv, &nr, &alloc) < 0) { - ret = error(_("bogus format in %s"), CONFIG_DATA_ENVIRONMENT); - goto out; - } - - for (i = 0; i < nr; i++) { - if (git_config_parse_parameter(argv[i], fn, data) < 0) { - ret = -1; - goto out; - } - } - -out: - free(argv); free(envw); cf = source.prev; return ret; diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 36a60879f6..35a1a6e8b1 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1294,6 +1294,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