From 83b7fd87714c4b70553c56987756ff319ccc7ec6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 3 Apr 2018 18:28:00 +0200 Subject: git_config_set: fix off-by-two Currently, we are slightly overzealous When removing an entry from a config file of this form: [abc]a [xyz] key = value When calling `git config --unset abc.a` on this file, it leaves this (invalid) config behind: [ [xyz] key = value The reason is that we try to search for the beginning of the line (or for the end of the preceding section header on the same line) that defines abc.a, but as an optimization, we subtract 2 from the offset pointing just after the definition before we call find_beginning_of_line(). That function, however, *also* performs that optimization and promptly fails to find the section header correctly. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index e617c2018d..4c8571ab33 100644 --- a/config.c +++ b/config.c @@ -2624,7 +2624,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, } else copy_end = find_beginning_of_line( contents, contents_sz, - store.offset[i]-2, &new_line); + store.offset[i], &new_line); if (copy_end > 0 && contents[copy_end-1] != '\n') new_line = 1; -- cgit v1.3 From efbaca1b69b2d08429889259489b73c8eea16a1d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 3 Apr 2018 18:28:06 +0200 Subject: t1300: rename it to reflect that `repo-config` was deprecated Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1300-config.sh | 1591 ++++++++++++++++++++++++++++++++++++++++++++++++ t/t1300-repo-config.sh | 1591 ------------------------------------------------ 2 files changed, 1591 insertions(+), 1591 deletions(-) create mode 100755 t/t1300-config.sh delete mode 100755 t/t1300-repo-config.sh diff --git a/t/t1300-config.sh b/t/t1300-config.sh new file mode 100755 index 0000000000..cbeb9bebee --- /dev/null +++ b/t/t1300-config.sh @@ -0,0 +1,1591 @@ +#!/bin/sh +# +# Copyright (c) 2005 Johannes Schindelin +# + +test_description='Test git config in different settings' + +. ./test-lib.sh + +test_expect_success 'clear default config' ' + rm -f .git/config +' + +cat > expect << EOF +[core] + penguin = little blue +EOF +test_expect_success 'initial' ' + git config core.penguin "little blue" && + test_cmp expect .git/config +' + +cat > expect << EOF +[core] + penguin = little blue + Movie = BadPhysics +EOF +test_expect_success 'mixed case' ' + git config Core.Movie BadPhysics && + test_cmp expect .git/config +' + +cat > expect << EOF +[core] + penguin = little blue + Movie = BadPhysics +[Cores] + WhatEver = Second +EOF +test_expect_success 'similar section' ' + git config Cores.WhatEver Second && + test_cmp expect .git/config +' + +cat > expect << EOF +[core] + penguin = little blue + Movie = BadPhysics + UPPERCASE = true +[Cores] + WhatEver = Second +EOF +test_expect_success 'uppercase section' ' + git config CORE.UPPERCASE true && + test_cmp expect .git/config +' + +test_expect_success 'replace with non-match' ' + git config core.penguin kingpin !blue +' + +test_expect_success 'replace with non-match (actually matching)' ' + git config core.penguin "very blue" !kingpin +' + +cat > expect << EOF +[core] + penguin = very blue + Movie = BadPhysics + UPPERCASE = true + penguin = kingpin +[Cores] + WhatEver = Second +EOF + +test_expect_success 'non-match result' 'test_cmp expect .git/config' + +test_expect_success 'find mixed-case key by canonical name' ' + echo Second >expect && + git config cores.whatever >actual && + test_cmp expect actual +' + +test_expect_success 'find mixed-case key by non-canonical name' ' + echo Second >expect && + git config CoReS.WhAtEvEr >actual && + test_cmp expect actual +' + +test_expect_success 'subsections are not canonicalized by git-config' ' + cat >>.git/config <<-\EOF && + [section.SubSection] + key = one + [section "SubSection"] + key = two + EOF + echo one >expect && + git config section.subsection.key >actual && + test_cmp expect actual && + echo two >expect && + git config section.SubSection.key >actual && + test_cmp expect actual +' + +cat > .git/config <<\EOF +[alpha] +bar = foo +[beta] +baz = multiple \ +lines +EOF + +test_expect_success 'unset with cont. lines' ' + git config --unset beta.baz +' + +cat > expect <<\EOF +[alpha] +bar = foo +[beta] +EOF + +test_expect_success 'unset with cont. lines is correct' 'test_cmp expect .git/config' + +cat > .git/config << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha ="beta" # last silly comment +haha = hello + haha = bello +[nextSection] noNewline = ouch +EOF + +cp .git/config .git/config2 + +test_expect_success 'multiple unset' ' + git config --unset-all beta.haha +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] noNewline = ouch +EOF + +test_expect_success 'multiple unset is correct' ' + test_cmp expect .git/config +' + +cp .git/config2 .git/config + +test_expect_success '--replace-all missing value' ' + test_must_fail git config --replace-all beta.haha && + test_cmp .git/config2 .git/config +' + +rm .git/config2 + +test_expect_success '--replace-all' ' + git config --replace-all beta.haha gamma +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha = gamma +[nextSection] noNewline = ouch +EOF + +test_expect_success 'all replaced' ' + test_cmp expect .git/config +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha = alpha +[nextSection] noNewline = ouch +EOF +test_expect_success 'really mean test' ' + git config beta.haha alpha && + test_cmp expect .git/config +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha = alpha +[nextSection] + nonewline = wow +EOF +test_expect_success 'really really mean test' ' + git config nextsection.nonewline wow && + test_cmp expect .git/config +' + +test_expect_success 'get value' ' + echo alpha >expect && + git config beta.haha >actual && + test_cmp expect actual +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + nonewline = wow +EOF +test_expect_success 'unset' ' + git config --unset beta.haha && + test_cmp expect .git/config +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + nonewline = wow + NoNewLine = wow2 for me +EOF +test_expect_success 'multivar' ' + git config nextsection.NoNewLine "wow2 for me" "for me$" && + test_cmp expect .git/config +' + +test_expect_success 'non-match' ' + git config --get nextsection.nonewline !for +' + +test_expect_success 'non-match value' ' + echo wow >expect && + git config --get nextsection.nonewline !for >actual && + test_cmp expect actual +' + +test_expect_success 'multi-valued get returns final one' ' + echo "wow2 for me" >expect && + git config --get nextsection.nonewline >actual && + test_cmp expect actual +' + +test_expect_success 'multi-valued get-all returns all' ' + cat >expect <<-\EOF && + wow + wow2 for me + EOF + git config --get-all nextsection.nonewline >actual && + test_cmp expect actual +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + nonewline = wow3 + NoNewLine = wow2 for me +EOF +test_expect_success 'multivar replace' ' + git config nextsection.nonewline "wow3" "wow$" && + test_cmp expect .git/config +' + +test_expect_success 'ambiguous unset' ' + test_must_fail git config --unset nextsection.nonewline +' + +test_expect_success 'invalid unset' ' + test_must_fail git config --unset somesection.nonewline +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + NoNewLine = wow2 for me +EOF + +test_expect_success 'multivar unset' ' + git config --unset nextsection.nonewline "wow3$" && + test_cmp expect .git/config +' + +test_expect_success 'invalid key' 'test_must_fail git config inval.2key blabla' + +test_expect_success 'correct key' 'git config 123456.a123 987' + +test_expect_success 'hierarchical section' ' + git config Version.1.2.3eX.Alpha beta +' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + NoNewLine = wow2 for me +[123456] + a123 = 987 +[Version "1.2.3eX"] + Alpha = beta +EOF + +test_expect_success 'hierarchical section value' ' + test_cmp expect .git/config +' + +cat > expect << EOF +beta.noindent=sillyValue +nextsection.nonewline=wow2 for me +123456.a123=987 +version.1.2.3eX.alpha=beta +EOF + +test_expect_success 'working --list' ' + git config --list > output && + test_cmp expect output +' +cat > expect << EOF +EOF + +test_expect_success '--list without repo produces empty output' ' + git --git-dir=nonexistent config --list >output && + test_cmp expect output +' + +cat > expect << EOF +beta.noindent +nextsection.nonewline +123456.a123 +version.1.2.3eX.alpha +EOF + +test_expect_success '--name-only --list' ' + git config --name-only --list >output && + test_cmp expect output +' + +cat > expect << EOF +beta.noindent sillyValue +nextsection.nonewline wow2 for me +EOF + +test_expect_success '--get-regexp' ' + git config --get-regexp in >output && + test_cmp expect output +' + +cat > expect << EOF +beta.noindent +nextsection.nonewline +EOF + +test_expect_success '--name-only --get-regexp' ' + git config --name-only --get-regexp in >output && + test_cmp expect output +' + +cat > expect << EOF +wow2 for me +wow4 for you +EOF + +test_expect_success '--add' ' + git config --add nextsection.nonewline "wow4 for you" && + git config --get-all nextsection.nonewline > output && + test_cmp expect output +' + +cat > .git/config << EOF +[novalue] + variable +[emptyvalue] + variable = +EOF + +test_expect_success 'get variable with no value' ' + git config --get novalue.variable ^$ +' + +test_expect_success 'get variable with empty value' ' + git config --get emptyvalue.variable ^$ +' + +echo novalue.variable > expect + +test_expect_success 'get-regexp variable with no value' ' + git config --get-regexp novalue > output && + test_cmp expect output +' + +echo 'novalue.variable true' > expect + +test_expect_success 'get-regexp --bool variable with no value' ' + git config --bool --get-regexp novalue > output && + test_cmp expect output +' + +echo 'emptyvalue.variable ' > expect + +test_expect_success 'get-regexp variable with empty value' ' + git config --get-regexp emptyvalue > output && + test_cmp expect output +' + +echo true > expect + +test_expect_success 'get bool variable with no value' ' + git config --bool novalue.variable > output && + test_cmp expect output +' + +echo false > expect + +test_expect_success 'get bool variable with empty value' ' + git config --bool emptyvalue.variable > output && + test_cmp expect output +' + +test_expect_success 'no arguments, but no crash' ' + test_must_fail git config >output 2>&1 && + test_i18ngrep usage output +' + +cat > .git/config << EOF +[a.b] + c = d +EOF + +cat > expect << EOF +[a.b] + c = d +[a] + x = y +EOF + +test_expect_success 'new section is partial match of another' ' + git config a.x y && + test_cmp expect .git/config +' + +cat > expect << EOF +[a.b] + c = d +[a] + x = y + b = c +[b] + x = y +EOF + +test_expect_success 'new variable inserts into proper section' ' + git config b.x y && + git config a.b c && + test_cmp expect .git/config +' + +test_expect_success 'alternative --file (non-existing file should fail)' ' + test_must_fail git config --file non-existing-config -l +' + +cat > other-config << EOF +[ein] + bahn = strasse +EOF + +cat > expect << EOF +ein.bahn=strasse +EOF + +test_expect_success 'alternative GIT_CONFIG' ' + GIT_CONFIG=other-config git config --list >output && + test_cmp expect output +' + +test_expect_success 'alternative GIT_CONFIG (--file)' ' + git config --file other-config --list >output && + test_cmp expect output +' + +test_expect_success 'alternative GIT_CONFIG (--file=-)' ' + git config --file - --list output && + test_cmp expect output +' + +test_expect_success 'setting a value in stdin is an error' ' + test_must_fail git config --file - some.value foo +' + +test_expect_success 'editing stdin is an error' ' + test_must_fail git config --file - --edit +' + +test_expect_success 'refer config from subdirectory' ' + mkdir x && + ( + cd x && + echo strasse >expect && + git config --get --file ../other-config ein.bahn >actual && + test_cmp expect actual + ) + +' + +test_expect_success 'refer config from subdirectory via --file' ' + ( + cd x && + git config --file=../other-config --get ein.bahn >actual && + test_cmp expect actual + ) +' + +cat > expect << EOF +[ein] + bahn = strasse +[anwohner] + park = ausweis +EOF + +test_expect_success '--set in alternative file' ' + git config --file=other-config anwohner.park ausweis && + test_cmp expect other-config +' + +cat > .git/config << EOF +# Hallo + #Bello +[branch "eins"] + x = 1 +[branch.eins] + y = 1 + [branch "1 234 blabl/a"] +weird +EOF + +test_expect_success 'rename section' ' + git config --rename-section branch.eins branch.zwei +' + +cat > expect << EOF +# Hallo + #Bello +[branch "zwei"] + x = 1 +[branch "zwei"] + y = 1 + [branch "1 234 blabl/a"] +weird +EOF + +test_expect_success 'rename succeeded' ' + test_cmp expect .git/config +' + +test_expect_success 'rename non-existing section' ' + test_must_fail git config --rename-section \ + branch."world domination" branch.drei +' + +test_expect_success 'rename succeeded' ' + test_cmp expect .git/config +' + +test_expect_success 'rename another section' ' + git config --rename-section branch."1 234 blabl/a" branch.drei +' + +cat > expect << EOF +# Hallo + #Bello +[branch "zwei"] + x = 1 +[branch "zwei"] + y = 1 +[branch "drei"] +weird +EOF + +test_expect_success 'rename succeeded' ' + test_cmp expect .git/config +' + +cat >> .git/config << EOF +[branch "vier"] z = 1 +EOF + +test_expect_success 'rename a section with a var on the same line' ' + git config --rename-section branch.vier branch.zwei +' + +cat > expect << EOF +# Hallo + #Bello +[branch "zwei"] + x = 1 +[branch "zwei"] + y = 1 +[branch "drei"] +weird +[branch "zwei"] + z = 1 +EOF + +test_expect_success 'rename succeeded' ' + test_cmp expect .git/config +' + +test_expect_success 'renaming empty section name is rejected' ' + test_must_fail git config --rename-section branch.zwei "" +' + +test_expect_success 'renaming to bogus section is rejected' ' + test_must_fail git config --rename-section branch.zwei "bogus name" +' + +cat >> .git/config << EOF + [branch "zwei"] a = 1 [branch "vier"] +EOF + +test_expect_success 'remove section' ' + git config --remove-section branch.zwei +' + +cat > expect << EOF +# Hallo + #Bello +[branch "drei"] +weird +EOF + +test_expect_success 'section was removed properly' ' + test_cmp expect .git/config +' + +cat > expect << EOF +[gitcvs] + enabled = true + dbname = %Ggitcvs2.%a.%m.sqlite +[gitcvs "ext"] + dbname = %Ggitcvs1.%a.%m.sqlite +EOF + +test_expect_success 'section ending' ' + rm -f .git/config && + git config gitcvs.enabled true && + git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite && + git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite && + test_cmp expect .git/config + +' + +test_expect_success numbers ' + git config kilo.gram 1k && + git config mega.ton 1m && + echo 1024 >expect && + echo 1048576 >>expect && + git config --int --get kilo.gram >actual && + git config --int --get mega.ton >>actual && + test_cmp expect actual +' + +test_expect_success '--int is at least 64 bits' ' + git config giga.watts 121g && + echo 129922760704 >expect && + git config --int --get giga.watts >actual && + test_cmp expect actual +' + +test_expect_success 'invalid unit' ' + git config aninvalid.unit "1auto" && + echo 1auto >expect && + git config aninvalid.unit >actual && + test_cmp expect actual && + test_must_fail git config --int --get aninvalid.unit 2>actual && + test_i18ngrep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual +' + +test_expect_success 'line number is reported correctly' ' + printf "[bool]\n\tvar\n" >invalid && + test_must_fail git config -f invalid --path bool.var 2>actual && + test_i18ngrep "line 2" actual +' + +test_expect_success 'invalid stdin config' ' + echo "[broken" | test_must_fail git config --list --file - >output 2>&1 && + test_i18ngrep "bad config line 1 in standard input" output +' + +cat > expect << EOF +true +false +true +false +true +false +true +false +EOF + +test_expect_success bool ' + + git config bool.true1 01 && + git config bool.true2 -1 && + git config bool.true3 YeS && + git config bool.true4 true && + git config bool.false1 000 && + git config bool.false2 "" && + git config bool.false3 nO && + git config bool.false4 FALSE && + rm -f result && + for i in 1 2 3 4 + do + git config --bool --get bool.true$i >>result + git config --bool --get bool.false$i >>result + done && + test_cmp expect result' + +test_expect_success 'invalid bool (--get)' ' + + git config bool.nobool foobar && + test_must_fail git config --bool --get bool.nobool' + +test_expect_success 'invalid bool (set)' ' + + test_must_fail git config --bool bool.nobool foobar' + +cat > expect <<\EOF +[bool] + true1 = true + true2 = true + true3 = true + true4 = true + false1 = false + false2 = false + false3 = false + false4 = false +EOF + +test_expect_success 'set --bool' ' + + rm -f .git/config && + git config --bool bool.true1 01 && + git config --bool bool.true2 -1 && + git config --bool bool.true3 YeS && + git config --bool bool.true4 true && + git config --bool bool.false1 000 && + git config --bool bool.false2 "" && + git config --bool bool.false3 nO && + git config --bool bool.false4 FALSE && + test_cmp expect .git/config' + +cat > expect <<\EOF +[int] + val1 = 1 + val2 = -1 + val3 = 5242880 +EOF + +test_expect_success 'set --int' ' + + rm -f .git/config && + git config --int int.val1 01 && + git config --int int.val2 -1 && + git config --int int.val3 5m && + test_cmp expect .git/config +' + +test_expect_success 'get --bool-or-int' ' + cat >.git/config <<-\EOF && + [bool] + true1 + true2 = true + false = false + [int] + int1 = 0 + int2 = 1 + int3 = -1 + EOF + cat >expect <<-\EOF && + true + true + false + 0 + 1 + -1 + EOF + { + git config --bool-or-int bool.true1 && + git config --bool-or-int bool.true2 && + git config --bool-or-int bool.false && + git config --bool-or-int int.int1 && + git config --bool-or-int int.int2 && + git config --bool-or-int int.int3 + } >actual && + test_cmp expect actual +' + +cat >expect <<\EOF +[bool] + true1 = true + false1 = false + true2 = true + false2 = false +[int] + int1 = 0 + int2 = 1 + int3 = -1 +EOF + +test_expect_success 'set --bool-or-int' ' + rm -f .git/config && + git config --bool-or-int bool.true1 true && + git config --bool-or-int bool.false1 false && + git config --bool-or-int bool.true2 yes && + git config --bool-or-int bool.false2 no && + git config --bool-or-int int.int1 0 && + git config --bool-or-int int.int2 1 && + git config --bool-or-int int.int3 -1 && + test_cmp expect .git/config +' + +cat >expect <<\EOF +[path] + home = ~/ + normal = /dev/null + trailingtilde = foo~ +EOF + +test_expect_success !MINGW 'set --path' ' + rm -f .git/config && + git config --path path.home "~/" && + git config --path path.normal "/dev/null" && + git config --path path.trailingtilde "foo~" && + test_cmp expect .git/config' + +if test_have_prereq !MINGW && test "${HOME+set}" +then + test_set_prereq HOMEVAR +fi + +cat >expect < result && + git config --get --path path.normal >> result && + git config --get --path path.trailingtilde >> result && + test_cmp expect result +' + +cat >expect <<\EOF +/dev/null +foo~ +EOF + +test_expect_success !MINGW 'get --path copes with unset $HOME' ' + ( + unset HOME; + test_must_fail git config --get --path path.home \ + >result 2>msg && + git config --get --path path.normal >>result && + git config --get --path path.trailingtilde >>result + ) && + test_i18ngrep "[Ff]ailed to expand.*~/" msg && + test_cmp expect result +' + +test_expect_success 'get --path barfs on boolean variable' ' + echo "[path]bool" >.git/config && + test_must_fail git config --get --path path.bool +' + +test_expect_success 'get --expiry-date' ' + rel="3.weeks.5.days.00:00" && + rel_out="$rel ->" && + cat >.git/config <<-\EOF && + [date] + valid1 = "3.weeks.5.days 00:00" + valid2 = "Fri Jun 4 15:46:55 2010" + valid3 = "2017/11/11 11:11:11PM" + valid4 = "2017/11/10 09:08:07 PM" + valid5 = "never" + invalid1 = "abc" + EOF + cat >expect <<-EOF && + $(test-date timestamp $rel) + 1275666415 + 1510441871 + 1510348087 + 0 + EOF + { + echo "$rel_out $(git config --expiry-date date.valid1)" + git config --expiry-date date.valid2 && + git config --expiry-date date.valid3 && + git config --expiry-date date.valid4 && + git config --expiry-date date.valid5 + } >actual && + test_cmp expect actual && + test_must_fail git config --expiry-date date.invalid1 +' + +cat > expect << EOF +[quote] + leading = " test" + ending = "test " + semicolon = "test;test" + hash = "test#test" +EOF +test_expect_success 'quoting' ' + rm -f .git/config && + git config quote.leading " test" && + git config quote.ending "test " && + git config quote.semicolon "test;test" && + git config quote.hash "test#test" && + test_cmp expect .git/config +' + +test_expect_success 'key with newline' ' + test_must_fail git config "key.with +newline" 123' + +test_expect_success 'value with newline' 'git config key.sub value.with\\\ +newline' + +cat > .git/config <<\EOF +[section] + ; comment \ + continued = cont\ +inued + noncont = not continued ; \ + quotecont = "cont;\ +inued" +EOF + +cat > expect <<\EOF +section.continued=continued +section.noncont=not continued +section.quotecont=cont;inued +EOF + +test_expect_success 'value continued on next line' ' + git config --list > result && + test_cmp result expect +' + +cat > .git/config <<\EOF +[section "sub=section"] + val1 = foo=bar + val2 = foo\nbar + val3 = \n\n + val4 = + val5 +EOF + +cat > expect <<\EOF +section.sub=section.val1 +foo=barQsection.sub=section.val2 +foo +barQsection.sub=section.val3 + + +Qsection.sub=section.val4 +Qsection.sub=section.val5Q +EOF +test_expect_success '--null --list' ' + git config --null --list >result.raw && + nul_to_q result && + echo >>result && + test_cmp expect result +' + +test_expect_success '--null --get-regexp' ' + git config --null --get-regexp "val[0-9]" >result.raw && + nul_to_q result && + echo >>result && + test_cmp expect result +' + +test_expect_success 'inner whitespace kept verbatim' ' + git config section.val "foo bar" && + echo "foo bar" >expect && + git config section.val >actual && + test_cmp expect actual +' + +test_expect_success SYMLINKS 'symlinked configuration' ' + ln -s notyet myconfig && + git config --file=myconfig test.frotz nitfol && + test -h myconfig && + test -f notyet && + test "z$(git config --file=notyet test.frotz)" = znitfol && + git config --file=myconfig test.xyzzy rezrov && + test -h myconfig && + test -f notyet && + cat >expect <<-\EOF && + nitfol + rezrov + EOF + { + git config --file=notyet test.frotz && + git config --file=notyet test.xyzzy + } >actual && + test_cmp expect actual +' + +test_expect_success 'nonexistent configuration' ' + test_must_fail git config --file=doesnotexist --list && + test_must_fail git config --file=doesnotexist test.xyzzy +' + +test_expect_success SYMLINKS 'symlink to nonexistent configuration' ' + ln -s doesnotexist linktonada && + ln -s linktonada linktolinktonada && + test_must_fail git config --file=linktonada --list && + test_must_fail git config --file=linktolinktonada --list +' + +test_expect_success 'check split_cmdline return' " + git config alias.split-cmdline-fix 'echo \"' && + test_must_fail git split-cmdline-fix && + echo foo > foo && + git add foo && + git commit -m 'initial commit' && + git config branch.master.mergeoptions 'echo \"' && + test_must_fail git merge master +" + +test_expect_success 'git -c "key=value" support' ' + cat >expect <<-\EOF && + value + value + true + EOF + { + git -c core.name=value config core.name && + git -c foo.CamelCase=value config foo.camelcase && + git -c foo.flag config --bool foo.flag + } >actual && + test_cmp expect actual && + test_must_fail git -c name=value config core.name +' + +# We just need a type-specifier here that cares about the +# distinction internally between a NULL boolean and a real +# string (because most of git's internal parsers do care). +# Using "--path" works, but we do not otherwise care about +# its semantics. +test_expect_success 'git -c can represent empty string' ' + echo >expect && + git -c foo.empty= config --path foo.empty >actual && + test_cmp expect actual +' + +test_expect_success 'key sanity-checking' ' + test_must_fail git config foo=bar && + test_must_fail git config foo=.bar && + test_must_fail git config foo.ba=r && + test_must_fail git config foo.1bar && + test_must_fail git config foo."ba + z".bar && + test_must_fail git config . false && + test_must_fail git config .foo false && + test_must_fail git config foo. false && + test_must_fail git config .foo. false && + git config foo.bar true && + git config foo."ba =z".bar false +' + +test_expect_success 'git -c works with aliases of builtins' ' + git config alias.checkconfig "-c foo.check=bar config foo.check" && + echo bar >expect && + git checkconfig >actual && + test_cmp expect actual +' + +test_expect_success 'aliases can be CamelCased' ' + test_config alias.CamelCased "rev-parse HEAD" && + git CamelCased >out && + git rev-parse HEAD >expect && + test_cmp expect out +' + +test_expect_success 'git -c does not split values on equals' ' + echo "value with = in it" >expect && + git -c core.foo="value with = in it" config core.foo >actual && + test_cmp expect actual +' + +test_expect_success 'git -c dies on bogus config' ' + test_must_fail git -c core.bare=foo rev-parse +' + +test_expect_success 'git -c complains about empty key' ' + test_must_fail git -c "=foo" rev-parse +' + +test_expect_success 'git -c complains about empty key and value' ' + test_must_fail git -c "" rev-parse +' + +test_expect_success 'multiple git -c appends config' ' + test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" && + cat >expect <<-\EOF && + x.one 1 + x.two 2 + EOF + git -c x.one=1 x >actual && + test_cmp expect actual +' + +test_expect_success 'last one wins: two level vars' ' + + # sec.var and sec.VAR are the same variable, as the first + # and the last level of a configuration variable name is + # case insensitive. + + echo VAL >expect && + + git -c sec.var=val -c sec.VAR=VAL config --get sec.var >actual && + test_cmp expect actual && + git -c SEC.var=val -c sec.var=VAL config --get sec.var >actual && + test_cmp expect actual && + + git -c sec.var=val -c sec.VAR=VAL config --get SEC.var >actual && + test_cmp expect actual && + git -c SEC.var=val -c sec.var=VAL config --get sec.VAR >actual && + test_cmp expect actual +' + +test_expect_success 'last one wins: three level vars' ' + + # v.a.r and v.A.r are not the same variable, as the middle + # level of a three-level configuration variable name is + # case sensitive. + + echo val >expect && + git -c v.a.r=val -c v.A.r=VAL config --get v.a.r >actual && + test_cmp expect actual && + git -c v.a.r=val -c v.A.r=VAL config --get V.a.R >actual && + test_cmp expect actual && + + # v.a.r and V.a.R are the same variable, as the first + # and the last level of a configuration variable name is + # case insensitive. + + echo VAL >expect && + git -c v.a.r=val -c v.a.R=VAL config --get v.a.r >actual && + test_cmp expect actual && + git -c v.a.r=val -c V.a.r=VAL config --get v.a.r >actual && + test_cmp expect actual && + git -c v.a.r=val -c v.a.R=VAL config --get V.a.R >actual && + test_cmp expect actual && + git -c v.a.r=val -c V.a.r=VAL config --get V.a.R >actual && + test_cmp expect actual +' + +for VAR in a .a a. a.0b a."b c". a."b c".0d +do + test_expect_success "git -c $VAR=VAL rejects invalid '$VAR'" ' + test_must_fail git -c "$VAR=VAL" config -l + ' +done + +for VAR in a.b a."b c".d +do + test_expect_success "git -c $VAR=VAL works with valid '$VAR'" ' + echo VAL >expect && + git -c "$VAR=VAL" config --get "$VAR" >actual && + test_cmp expect actual + ' +done + +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 --edit works' ' + git config -f tmp test.value no && + echo test.value=yes >expect && + GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit && + git config -f tmp --list >actual && + test_cmp expect actual +' + +test_expect_success 'git config --edit respects core.editor' ' + git config -f tmp test.value no && + echo test.value=yes >expect && + test_config core.editor "echo [test]value=yes >" && + git config -f tmp --edit && + git config -f tmp --list >actual && + test_cmp expect actual +' + +# malformed configuration files +test_expect_success 'barf on syntax error' ' + cat >.git/config <<-\EOF && + # broken section line + [section] + key garbage + EOF + test_must_fail git config --get section.key >actual 2>error && + test_i18ngrep " line 3 " error +' + +test_expect_success 'barf on incomplete section header' ' + cat >.git/config <<-\EOF && + # broken section line + [section + key = value + EOF + test_must_fail git config --get section.key >actual 2>error && + test_i18ngrep " line 2 " error +' + +test_expect_success 'barf on incomplete string' ' + cat >.git/config <<-\EOF && + # broken section line + [section] + key = "value string + EOF + test_must_fail git config --get section.key >actual 2>error && + test_i18ngrep " line 3 " error +' + +test_expect_success 'urlmatch' ' + cat >.git/config <<-\EOF && + [http] + sslVerify + [http "https://weak.example.com"] + sslVerify = false + cookieFile = /tmp/cookie.txt + EOF + + test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual && + test_must_be_empty actual && + + echo true >expect && + git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual && + test_cmp expect actual && + + echo false >expect && + git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual && + test_cmp expect actual && + + { + echo http.cookiefile /tmp/cookie.txt && + echo http.sslverify false + } >expect && + git config --get-urlmatch HTTP https://weak.example.com >actual && + test_cmp expect actual +' + +test_expect_success 'urlmatch favors more specific URLs' ' + cat >.git/config <<-\EOF && + [http "https://example.com/"] + cookieFile = /tmp/root.txt + [http "https://example.com/subdirectory"] + cookieFile = /tmp/subdirectory.txt + [http "https://user@example.com/"] + cookieFile = /tmp/user.txt + [http "https://averylonguser@example.com/"] + cookieFile = /tmp/averylonguser.txt + [http "https://preceding.example.com"] + cookieFile = /tmp/preceding.txt + [http "https://*.example.com"] + cookieFile = /tmp/wildcard.txt + [http "https://*.example.com/wildcardwithsubdomain"] + cookieFile = /tmp/wildcardwithsubdomain.txt + [http "https://trailing.example.com"] + cookieFile = /tmp/trailing.txt + [http "https://user@*.example.com/"] + cookieFile = /tmp/wildcardwithuser.txt + [http "https://sub.example.com/"] + cookieFile = /tmp/sub.txt + EOF + + echo http.cookiefile /tmp/root.txt >expect && + git config --get-urlmatch HTTP https://example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/subdirectory.txt >expect && + git config --get-urlmatch HTTP https://example.com/subdirectory >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/subdirectory.txt >expect && + git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/user.txt >expect && + git config --get-urlmatch HTTP https://user@example.com/ >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/subdirectory.txt >expect && + git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/preceding.txt >expect && + git config --get-urlmatch HTTP https://preceding.example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/wildcard.txt >expect && + git config --get-urlmatch HTTP https://wildcard.example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/sub.txt >expect && + git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/trailing.txt >expect && + git config --get-urlmatch HTTP https://trailing.example.com >actual && + test_cmp expect actual && + + echo http.cookiefile /tmp/sub.txt >expect && + git config --get-urlmatch HTTP https://user@sub.example.com >actual && + test_cmp expect actual +' + +test_expect_success 'urlmatch with wildcard' ' + cat >.git/config <<-\EOF && + [http] + sslVerify + [http "https://*.example.com"] + sslVerify = false + cookieFile = /tmp/cookie.txt + EOF + + test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual && + test_must_be_empty actual && + + echo true >expect && + git config --bool --get-urlmatch http.SSLverify https://example.com >actual && + test_cmp expect actual && + + echo true >expect && + git config --bool --get-urlmatch http.SSLverify https://good-example.com >actual && + test_cmp expect actual && + + echo true >expect && + git config --bool --get-urlmatch http.sslverify https://deep.nested.example.com >actual && + test_cmp expect actual && + + echo false >expect && + git config --bool --get-urlmatch http.sslverify https://good.example.com >actual && + test_cmp expect actual && + + { + echo http.cookiefile /tmp/cookie.txt && + echo http.sslverify false + } >expect && + git config --get-urlmatch HTTP https://good.example.com >actual && + test_cmp expect actual && + + echo http.sslverify >expect && + git config --get-urlmatch HTTP https://more.example.com.au >actual && + test_cmp expect actual +' + +# good section hygiene +test_expect_failure 'unsetting the last key in a section removes header' ' + cat >.git/config <<-\EOF && + # some generic comment on the configuration file itself + # a comment specific to this "section" section. + [section] + # some intervening lines + # that should also be dropped + + key = value + # please be careful when you update the above variable + EOF + + cat >expect <<-\EOF && + # some generic comment on the configuration file itself + EOF + + git config --unset section.key && + test_cmp expect .git/config +' + +test_expect_failure 'adding a key into an empty section reuses header' ' + cat >.git/config <<-\EOF && + [section] + EOF + + q_to_tab >expect <<-\EOF && + [section] + Qkey = value + EOF + + git config section.key value && + test_cmp expect .git/config +' + +test_expect_success POSIXPERM,PERL 'preserves existing permissions' ' + chmod 0600 .git/config && + git config imap.pass Hunter2 && + perl -e \ + "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" && + git config --rename-section imap pop && + perl -e \ + "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600" +' + +! test_have_prereq MINGW || +HOME="$(pwd)" # convert to Windows path + +test_expect_success 'set up --show-origin tests' ' + INCLUDE_DIR="$HOME/include" && + mkdir -p "$INCLUDE_DIR" && + cat >"$INCLUDE_DIR"/absolute.include <<-\EOF && + [user] + absolute = include + EOF + cat >"$INCLUDE_DIR"/relative.include <<-\EOF && + [user] + relative = include + EOF + cat >"$HOME"/.gitconfig <<-EOF && + [user] + global = true + override = global + [include] + path = "$INCLUDE_DIR/absolute.include" + EOF + cat >.git/config <<-\EOF + [user] + local = true + override = local + [include] + path = ../include/relative.include + EOF +' + +test_expect_success '--show-origin with --list' ' + cat >expect <<-EOF && + file:$HOME/.gitconfig user.global=true + file:$HOME/.gitconfig user.override=global + file:$HOME/.gitconfig include.path=$INCLUDE_DIR/absolute.include + file:$INCLUDE_DIR/absolute.include user.absolute=include + file:.git/config user.local=true + 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.cmdline=true + EOF + git -c user.cmdline=true config --list --show-origin >output && + test_cmp expect output +' + +test_expect_success '--show-origin with --list --null' ' + cat >expect <<-EOF && + file:$HOME/.gitconfigQuser.global + trueQfile:$HOME/.gitconfigQuser.override + globalQfile:$HOME/.gitconfigQinclude.path + $INCLUDE_DIR/absolute.includeQfile:$INCLUDE_DIR/absolute.includeQuser.absolute + includeQfile:.git/configQuser.local + trueQfile:.git/configQuser.override + localQfile:.git/configQinclude.path + ../include/relative.includeQfile:.git/../include/relative.includeQuser.relative + includeQcommand line:Quser.cmdline + trueQ + EOF + git -c user.cmdline=true config --null --list --show-origin >output.raw && + nul_to_q output && + # The here-doc above adds a newline that the --null output would not + # include. Add it here to make the two comparable. + echo >>output && + test_cmp expect output +' + +test_expect_success '--show-origin with single file' ' + cat >expect <<-\EOF && + file:.git/config user.local=true + file:.git/config user.override=local + file:.git/config include.path=../include/relative.include + EOF + git config --local --list --show-origin >output && + test_cmp expect output +' + +test_expect_success '--show-origin with --get-regexp' ' + cat >expect <<-EOF && + file:$HOME/.gitconfig user.global true + file:.git/config user.local true + EOF + git config --show-origin --get-regexp "user\.[g|l].*" >output && + test_cmp expect output +' + +test_expect_success '--show-origin getting a single key' ' + cat >expect <<-\EOF && + file:.git/config local + EOF + git config --show-origin user.override >output && + test_cmp expect output +' + +test_expect_success 'set up custom config file' ' + CUSTOM_CONFIG_FILE="file\" (dq) and spaces.conf" && + cat >"$CUSTOM_CONFIG_FILE" <<-\EOF + [user] + custom = true + EOF +' + +test_expect_success !MINGW '--show-origin escape special file name characters' ' + cat >expect <<-\EOF && + file:"file\" (dq) and spaces.conf" user.custom=true + EOF + git config --file "$CUSTOM_CONFIG_FILE" --show-origin --list >output && + test_cmp expect output +' + +test_expect_success '--show-origin stdin' ' + cat >expect <<-\EOF && + standard input: user.custom=true + EOF + git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output && + test_cmp expect output +' + +test_expect_success '--show-origin stdin with file include' ' + cat >"$INCLUDE_DIR"/stdin.include <<-EOF && + [user] + stdin = include + EOF + cat >expect <<-EOF && + file:$INCLUDE_DIR/stdin.include include + EOF + echo "[include]path=\"$INCLUDE_DIR\"/stdin.include" \ + | git config --show-origin --includes --file - user.stdin >output && + test_cmp expect output +' + +test_expect_success !MINGW '--show-origin blob' ' + cat >expect <<-\EOF && + blob:a9d9f9e555b5c6f07cbe09d3f06fe3df11e09c08 user.custom=true + EOF + blob=$(git hash-object -w "$CUSTOM_CONFIG_FILE") && + git config --blob=$blob --show-origin --list >output && + test_cmp expect output +' + +test_expect_success !MINGW '--show-origin blob ref' ' + cat >expect <<-\EOF && + blob:"master:file\" (dq) and spaces.conf" user.custom=true + EOF + git add "$CUSTOM_CONFIG_FILE" && + git commit -m "new config file" && + git config --blob=master:"$CUSTOM_CONFIG_FILE" --show-origin --list >output && + test_cmp expect output +' + +test_expect_success '--local requires a repo' ' + # we expect 128 to ensure that we do not simply + # fail to find anything and return code "1" + test_expect_code 128 nongit git config --local foo.bar +' + +test_done diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh deleted file mode 100755 index cbeb9bebee..0000000000 --- a/t/t1300-repo-config.sh +++ /dev/null @@ -1,1591 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2005 Johannes Schindelin -# - -test_description='Test git config in different settings' - -. ./test-lib.sh - -test_expect_success 'clear default config' ' - rm -f .git/config -' - -cat > expect << EOF -[core] - penguin = little blue -EOF -test_expect_success 'initial' ' - git config core.penguin "little blue" && - test_cmp expect .git/config -' - -cat > expect << EOF -[core] - penguin = little blue - Movie = BadPhysics -EOF -test_expect_success 'mixed case' ' - git config Core.Movie BadPhysics && - test_cmp expect .git/config -' - -cat > expect << EOF -[core] - penguin = little blue - Movie = BadPhysics -[Cores] - WhatEver = Second -EOF -test_expect_success 'similar section' ' - git config Cores.WhatEver Second && - test_cmp expect .git/config -' - -cat > expect << EOF -[core] - penguin = little blue - Movie = BadPhysics - UPPERCASE = true -[Cores] - WhatEver = Second -EOF -test_expect_success 'uppercase section' ' - git config CORE.UPPERCASE true && - test_cmp expect .git/config -' - -test_expect_success 'replace with non-match' ' - git config core.penguin kingpin !blue -' - -test_expect_success 'replace with non-match (actually matching)' ' - git config core.penguin "very blue" !kingpin -' - -cat > expect << EOF -[core] - penguin = very blue - Movie = BadPhysics - UPPERCASE = true - penguin = kingpin -[Cores] - WhatEver = Second -EOF - -test_expect_success 'non-match result' 'test_cmp expect .git/config' - -test_expect_success 'find mixed-case key by canonical name' ' - echo Second >expect && - git config cores.whatever >actual && - test_cmp expect actual -' - -test_expect_success 'find mixed-case key by non-canonical name' ' - echo Second >expect && - git config CoReS.WhAtEvEr >actual && - test_cmp expect actual -' - -test_expect_success 'subsections are not canonicalized by git-config' ' - cat >>.git/config <<-\EOF && - [section.SubSection] - key = one - [section "SubSection"] - key = two - EOF - echo one >expect && - git config section.subsection.key >actual && - test_cmp expect actual && - echo two >expect && - git config section.SubSection.key >actual && - test_cmp expect actual -' - -cat > .git/config <<\EOF -[alpha] -bar = foo -[beta] -baz = multiple \ -lines -EOF - -test_expect_success 'unset with cont. lines' ' - git config --unset beta.baz -' - -cat > expect <<\EOF -[alpha] -bar = foo -[beta] -EOF - -test_expect_success 'unset with cont. lines is correct' 'test_cmp expect .git/config' - -cat > .git/config << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment - haha ="beta" # last silly comment -haha = hello - haha = bello -[nextSection] noNewline = ouch -EOF - -cp .git/config .git/config2 - -test_expect_success 'multiple unset' ' - git config --unset-all beta.haha -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment -[nextSection] noNewline = ouch -EOF - -test_expect_success 'multiple unset is correct' ' - test_cmp expect .git/config -' - -cp .git/config2 .git/config - -test_expect_success '--replace-all missing value' ' - test_must_fail git config --replace-all beta.haha && - test_cmp .git/config2 .git/config -' - -rm .git/config2 - -test_expect_success '--replace-all' ' - git config --replace-all beta.haha gamma -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment - haha = gamma -[nextSection] noNewline = ouch -EOF - -test_expect_success 'all replaced' ' - test_cmp expect .git/config -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment - haha = alpha -[nextSection] noNewline = ouch -EOF -test_expect_success 'really mean test' ' - git config beta.haha alpha && - test_cmp expect .git/config -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment - haha = alpha -[nextSection] - nonewline = wow -EOF -test_expect_success 'really really mean test' ' - git config nextsection.nonewline wow && - test_cmp expect .git/config -' - -test_expect_success 'get value' ' - echo alpha >expect && - git config beta.haha >actual && - test_cmp expect actual -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment -[nextSection] - nonewline = wow -EOF -test_expect_success 'unset' ' - git config --unset beta.haha && - test_cmp expect .git/config -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment -[nextSection] - nonewline = wow - NoNewLine = wow2 for me -EOF -test_expect_success 'multivar' ' - git config nextsection.NoNewLine "wow2 for me" "for me$" && - test_cmp expect .git/config -' - -test_expect_success 'non-match' ' - git config --get nextsection.nonewline !for -' - -test_expect_success 'non-match value' ' - echo wow >expect && - git config --get nextsection.nonewline !for >actual && - test_cmp expect actual -' - -test_expect_success 'multi-valued get returns final one' ' - echo "wow2 for me" >expect && - git config --get nextsection.nonewline >actual && - test_cmp expect actual -' - -test_expect_success 'multi-valued get-all returns all' ' - cat >expect <<-\EOF && - wow - wow2 for me - EOF - git config --get-all nextsection.nonewline >actual && - test_cmp expect actual -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment -[nextSection] - nonewline = wow3 - NoNewLine = wow2 for me -EOF -test_expect_success 'multivar replace' ' - git config nextsection.nonewline "wow3" "wow$" && - test_cmp expect .git/config -' - -test_expect_success 'ambiguous unset' ' - test_must_fail git config --unset nextsection.nonewline -' - -test_expect_success 'invalid unset' ' - test_must_fail git config --unset somesection.nonewline -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment -[nextSection] - NoNewLine = wow2 for me -EOF - -test_expect_success 'multivar unset' ' - git config --unset nextsection.nonewline "wow3$" && - test_cmp expect .git/config -' - -test_expect_success 'invalid key' 'test_must_fail git config inval.2key blabla' - -test_expect_success 'correct key' 'git config 123456.a123 987' - -test_expect_success 'hierarchical section' ' - git config Version.1.2.3eX.Alpha beta -' - -cat > expect << EOF -[beta] ; silly comment # another comment -noIndent= sillyValue ; 'nother silly comment - -# empty line - ; comment -[nextSection] - NoNewLine = wow2 for me -[123456] - a123 = 987 -[Version "1.2.3eX"] - Alpha = beta -EOF - -test_expect_success 'hierarchical section value' ' - test_cmp expect .git/config -' - -cat > expect << EOF -beta.noindent=sillyValue -nextsection.nonewline=wow2 for me -123456.a123=987 -version.1.2.3eX.alpha=beta -EOF - -test_expect_success 'working --list' ' - git config --list > output && - test_cmp expect output -' -cat > expect << EOF -EOF - -test_expect_success '--list without repo produces empty output' ' - git --git-dir=nonexistent config --list >output && - test_cmp expect output -' - -cat > expect << EOF -beta.noindent -nextsection.nonewline -123456.a123 -version.1.2.3eX.alpha -EOF - -test_expect_success '--name-only --list' ' - git config --name-only --list >output && - test_cmp expect output -' - -cat > expect << EOF -beta.noindent sillyValue -nextsection.nonewline wow2 for me -EOF - -test_expect_success '--get-regexp' ' - git config --get-regexp in >output && - test_cmp expect output -' - -cat > expect << EOF -beta.noindent -nextsection.nonewline -EOF - -test_expect_success '--name-only --get-regexp' ' - git config --name-only --get-regexp in >output && - test_cmp expect output -' - -cat > expect << EOF -wow2 for me -wow4 for you -EOF - -test_expect_success '--add' ' - git config --add nextsection.nonewline "wow4 for you" && - git config --get-all nextsection.nonewline > output && - test_cmp expect output -' - -cat > .git/config << EOF -[novalue] - variable -[emptyvalue] - variable = -EOF - -test_expect_success 'get variable with no value' ' - git config --get novalue.variable ^$ -' - -test_expect_success 'get variable with empty value' ' - git config --get emptyvalue.variable ^$ -' - -echo novalue.variable > expect - -test_expect_success 'get-regexp variable with no value' ' - git config --get-regexp novalue > output && - test_cmp expect output -' - -echo 'novalue.variable true' > expect - -test_expect_success 'get-regexp --bool variable with no value' ' - git config --bool --get-regexp novalue > output && - test_cmp expect output -' - -echo 'emptyvalue.variable ' > expect - -test_expect_success 'get-regexp variable with empty value' ' - git config --get-regexp emptyvalue > output && - test_cmp expect output -' - -echo true > expect - -test_expect_success 'get bool variable with no value' ' - git config --bool novalue.variable > output && - test_cmp expect output -' - -echo false > expect - -test_expect_success 'get bool variable with empty value' ' - git config --bool emptyvalue.variable > output && - test_cmp expect output -' - -test_expect_success 'no arguments, but no crash' ' - test_must_fail git config >output 2>&1 && - test_i18ngrep usage output -' - -cat > .git/config << EOF -[a.b] - c = d -EOF - -cat > expect << EOF -[a.b] - c = d -[a] - x = y -EOF - -test_expect_success 'new section is partial match of another' ' - git config a.x y && - test_cmp expect .git/config -' - -cat > expect << EOF -[a.b] - c = d -[a] - x = y - b = c -[b] - x = y -EOF - -test_expect_success 'new variable inserts into proper section' ' - git config b.x y && - git config a.b c && - test_cmp expect .git/config -' - -test_expect_success 'alternative --file (non-existing file should fail)' ' - test_must_fail git config --file non-existing-config -l -' - -cat > other-config << EOF -[ein] - bahn = strasse -EOF - -cat > expect << EOF -ein.bahn=strasse -EOF - -test_expect_success 'alternative GIT_CONFIG' ' - GIT_CONFIG=other-config git config --list >output && - test_cmp expect output -' - -test_expect_success 'alternative GIT_CONFIG (--file)' ' - git config --file other-config --list >output && - test_cmp expect output -' - -test_expect_success 'alternative GIT_CONFIG (--file=-)' ' - git config --file - --list output && - test_cmp expect output -' - -test_expect_success 'setting a value in stdin is an error' ' - test_must_fail git config --file - some.value foo -' - -test_expect_success 'editing stdin is an error' ' - test_must_fail git config --file - --edit -' - -test_expect_success 'refer config from subdirectory' ' - mkdir x && - ( - cd x && - echo strasse >expect && - git config --get --file ../other-config ein.bahn >actual && - test_cmp expect actual - ) - -' - -test_expect_success 'refer config from subdirectory via --file' ' - ( - cd x && - git config --file=../other-config --get ein.bahn >actual && - test_cmp expect actual - ) -' - -cat > expect << EOF -[ein] - bahn = strasse -[anwohner] - park = ausweis -EOF - -test_expect_success '--set in alternative file' ' - git config --file=other-config anwohner.park ausweis && - test_cmp expect other-config -' - -cat > .git/config << EOF -# Hallo - #Bello -[branch "eins"] - x = 1 -[branch.eins] - y = 1 - [branch "1 234 blabl/a"] -weird -EOF - -test_expect_success 'rename section' ' - git config --rename-section branch.eins branch.zwei -' - -cat > expect << EOF -# Hallo - #Bello -[branch "zwei"] - x = 1 -[branch "zwei"] - y = 1 - [branch "1 234 blabl/a"] -weird -EOF - -test_expect_success 'rename succeeded' ' - test_cmp expect .git/config -' - -test_expect_success 'rename non-existing section' ' - test_must_fail git config --rename-section \ - branch."world domination" branch.drei -' - -test_expect_success 'rename succeeded' ' - test_cmp expect .git/config -' - -test_expect_success 'rename another section' ' - git config --rename-section branch."1 234 blabl/a" branch.drei -' - -cat > expect << EOF -# Hallo - #Bello -[branch "zwei"] - x = 1 -[branch "zwei"] - y = 1 -[branch "drei"] -weird -EOF - -test_expect_success 'rename succeeded' ' - test_cmp expect .git/config -' - -cat >> .git/config << EOF -[branch "vier"] z = 1 -EOF - -test_expect_success 'rename a section with a var on the same line' ' - git config --rename-section branch.vier branch.zwei -' - -cat > expect << EOF -# Hallo - #Bello -[branch "zwei"] - x = 1 -[branch "zwei"] - y = 1 -[branch "drei"] -weird -[branch "zwei"] - z = 1 -EOF - -test_expect_success 'rename succeeded' ' - test_cmp expect .git/config -' - -test_expect_success 'renaming empty section name is rejected' ' - test_must_fail git config --rename-section branch.zwei "" -' - -test_expect_success 'renaming to bogus section is rejected' ' - test_must_fail git config --rename-section branch.zwei "bogus name" -' - -cat >> .git/config << EOF - [branch "zwei"] a = 1 [branch "vier"] -EOF - -test_expect_success 'remove section' ' - git config --remove-section branch.zwei -' - -cat > expect << EOF -# Hallo - #Bello -[branch "drei"] -weird -EOF - -test_expect_success 'section was removed properly' ' - test_cmp expect .git/config -' - -cat > expect << EOF -[gitcvs] - enabled = true - dbname = %Ggitcvs2.%a.%m.sqlite -[gitcvs "ext"] - dbname = %Ggitcvs1.%a.%m.sqlite -EOF - -test_expect_success 'section ending' ' - rm -f .git/config && - git config gitcvs.enabled true && - git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite && - git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite && - test_cmp expect .git/config - -' - -test_expect_success numbers ' - git config kilo.gram 1k && - git config mega.ton 1m && - echo 1024 >expect && - echo 1048576 >>expect && - git config --int --get kilo.gram >actual && - git config --int --get mega.ton >>actual && - test_cmp expect actual -' - -test_expect_success '--int is at least 64 bits' ' - git config giga.watts 121g && - echo 129922760704 >expect && - git config --int --get giga.watts >actual && - test_cmp expect actual -' - -test_expect_success 'invalid unit' ' - git config aninvalid.unit "1auto" && - echo 1auto >expect && - git config aninvalid.unit >actual && - test_cmp expect actual && - test_must_fail git config --int --get aninvalid.unit 2>actual && - test_i18ngrep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual -' - -test_expect_success 'line number is reported correctly' ' - printf "[bool]\n\tvar\n" >invalid && - test_must_fail git config -f invalid --path bool.var 2>actual && - test_i18ngrep "line 2" actual -' - -test_expect_success 'invalid stdin config' ' - echo "[broken" | test_must_fail git config --list --file - >output 2>&1 && - test_i18ngrep "bad config line 1 in standard input" output -' - -cat > expect << EOF -true -false -true -false -true -false -true -false -EOF - -test_expect_success bool ' - - git config bool.true1 01 && - git config bool.true2 -1 && - git config bool.true3 YeS && - git config bool.true4 true && - git config bool.false1 000 && - git config bool.false2 "" && - git config bool.false3 nO && - git config bool.false4 FALSE && - rm -f result && - for i in 1 2 3 4 - do - git config --bool --get bool.true$i >>result - git config --bool --get bool.false$i >>result - done && - test_cmp expect result' - -test_expect_success 'invalid bool (--get)' ' - - git config bool.nobool foobar && - test_must_fail git config --bool --get bool.nobool' - -test_expect_success 'invalid bool (set)' ' - - test_must_fail git config --bool bool.nobool foobar' - -cat > expect <<\EOF -[bool] - true1 = true - true2 = true - true3 = true - true4 = true - false1 = false - false2 = false - false3 = false - false4 = false -EOF - -test_expect_success 'set --bool' ' - - rm -f .git/config && - git config --bool bool.true1 01 && - git config --bool bool.true2 -1 && - git config --bool bool.true3 YeS && - git config --bool bool.true4 true && - git config --bool bool.false1 000 && - git config --bool bool.false2 "" && - git config --bool bool.false3 nO && - git config --bool bool.false4 FALSE && - test_cmp expect .git/config' - -cat > expect <<\EOF -[int] - val1 = 1 - val2 = -1 - val3 = 5242880 -EOF - -test_expect_success 'set --int' ' - - rm -f .git/config && - git config --int int.val1 01 && - git config --int int.val2 -1 && - git config --int int.val3 5m && - test_cmp expect .git/config -' - -test_expect_success 'get --bool-or-int' ' - cat >.git/config <<-\EOF && - [bool] - true1 - true2 = true - false = false - [int] - int1 = 0 - int2 = 1 - int3 = -1 - EOF - cat >expect <<-\EOF && - true - true - false - 0 - 1 - -1 - EOF - { - git config --bool-or-int bool.true1 && - git config --bool-or-int bool.true2 && - git config --bool-or-int bool.false && - git config --bool-or-int int.int1 && - git config --bool-or-int int.int2 && - git config --bool-or-int int.int3 - } >actual && - test_cmp expect actual -' - -cat >expect <<\EOF -[bool] - true1 = true - false1 = false - true2 = true - false2 = false -[int] - int1 = 0 - int2 = 1 - int3 = -1 -EOF - -test_expect_success 'set --bool-or-int' ' - rm -f .git/config && - git config --bool-or-int bool.true1 true && - git config --bool-or-int bool.false1 false && - git config --bool-or-int bool.true2 yes && - git config --bool-or-int bool.false2 no && - git config --bool-or-int int.int1 0 && - git config --bool-or-int int.int2 1 && - git config --bool-or-int int.int3 -1 && - test_cmp expect .git/config -' - -cat >expect <<\EOF -[path] - home = ~/ - normal = /dev/null - trailingtilde = foo~ -EOF - -test_expect_success !MINGW 'set --path' ' - rm -f .git/config && - git config --path path.home "~/" && - git config --path path.normal "/dev/null" && - git config --path path.trailingtilde "foo~" && - test_cmp expect .git/config' - -if test_have_prereq !MINGW && test "${HOME+set}" -then - test_set_prereq HOMEVAR -fi - -cat >expect < result && - git config --get --path path.normal >> result && - git config --get --path path.trailingtilde >> result && - test_cmp expect result -' - -cat >expect <<\EOF -/dev/null -foo~ -EOF - -test_expect_success !MINGW 'get --path copes with unset $HOME' ' - ( - unset HOME; - test_must_fail git config --get --path path.home \ - >result 2>msg && - git config --get --path path.normal >>result && - git config --get --path path.trailingtilde >>result - ) && - test_i18ngrep "[Ff]ailed to expand.*~/" msg && - test_cmp expect result -' - -test_expect_success 'get --path barfs on boolean variable' ' - echo "[path]bool" >.git/config && - test_must_fail git config --get --path path.bool -' - -test_expect_success 'get --expiry-date' ' - rel="3.weeks.5.days.00:00" && - rel_out="$rel ->" && - cat >.git/config <<-\EOF && - [date] - valid1 = "3.weeks.5.days 00:00" - valid2 = "Fri Jun 4 15:46:55 2010" - valid3 = "2017/11/11 11:11:11PM" - valid4 = "2017/11/10 09:08:07 PM" - valid5 = "never" - invalid1 = "abc" - EOF - cat >expect <<-EOF && - $(test-date timestamp $rel) - 1275666415 - 1510441871 - 1510348087 - 0 - EOF - { - echo "$rel_out $(git config --expiry-date date.valid1)" - git config --expiry-date date.valid2 && - git config --expiry-date date.valid3 && - git config --expiry-date date.valid4 && - git config --expiry-date date.valid5 - } >actual && - test_cmp expect actual && - test_must_fail git config --expiry-date date.invalid1 -' - -cat > expect << EOF -[quote] - leading = " test" - ending = "test " - semicolon = "test;test" - hash = "test#test" -EOF -test_expect_success 'quoting' ' - rm -f .git/config && - git config quote.leading " test" && - git config quote.ending "test " && - git config quote.semicolon "test;test" && - git config quote.hash "test#test" && - test_cmp expect .git/config -' - -test_expect_success 'key with newline' ' - test_must_fail git config "key.with -newline" 123' - -test_expect_success 'value with newline' 'git config key.sub value.with\\\ -newline' - -cat > .git/config <<\EOF -[section] - ; comment \ - continued = cont\ -inued - noncont = not continued ; \ - quotecont = "cont;\ -inued" -EOF - -cat > expect <<\EOF -section.continued=continued -section.noncont=not continued -section.quotecont=cont;inued -EOF - -test_expect_success 'value continued on next line' ' - git config --list > result && - test_cmp result expect -' - -cat > .git/config <<\EOF -[section "sub=section"] - val1 = foo=bar - val2 = foo\nbar - val3 = \n\n - val4 = - val5 -EOF - -cat > expect <<\EOF -section.sub=section.val1 -foo=barQsection.sub=section.val2 -foo -barQsection.sub=section.val3 - - -Qsection.sub=section.val4 -Qsection.sub=section.val5Q -EOF -test_expect_success '--null --list' ' - git config --null --list >result.raw && - nul_to_q result && - echo >>result && - test_cmp expect result -' - -test_expect_success '--null --get-regexp' ' - git config --null --get-regexp "val[0-9]" >result.raw && - nul_to_q result && - echo >>result && - test_cmp expect result -' - -test_expect_success 'inner whitespace kept verbatim' ' - git config section.val "foo bar" && - echo "foo bar" >expect && - git config section.val >actual && - test_cmp expect actual -' - -test_expect_success SYMLINKS 'symlinked configuration' ' - ln -s notyet myconfig && - git config --file=myconfig test.frotz nitfol && - test -h myconfig && - test -f notyet && - test "z$(git config --file=notyet test.frotz)" = znitfol && - git config --file=myconfig test.xyzzy rezrov && - test -h myconfig && - test -f notyet && - cat >expect <<-\EOF && - nitfol - rezrov - EOF - { - git config --file=notyet test.frotz && - git config --file=notyet test.xyzzy - } >actual && - test_cmp expect actual -' - -test_expect_success 'nonexistent configuration' ' - test_must_fail git config --file=doesnotexist --list && - test_must_fail git config --file=doesnotexist test.xyzzy -' - -test_expect_success SYMLINKS 'symlink to nonexistent configuration' ' - ln -s doesnotexist linktonada && - ln -s linktonada linktolinktonada && - test_must_fail git config --file=linktonada --list && - test_must_fail git config --file=linktolinktonada --list -' - -test_expect_success 'check split_cmdline return' " - git config alias.split-cmdline-fix 'echo \"' && - test_must_fail git split-cmdline-fix && - echo foo > foo && - git add foo && - git commit -m 'initial commit' && - git config branch.master.mergeoptions 'echo \"' && - test_must_fail git merge master -" - -test_expect_success 'git -c "key=value" support' ' - cat >expect <<-\EOF && - value - value - true - EOF - { - git -c core.name=value config core.name && - git -c foo.CamelCase=value config foo.camelcase && - git -c foo.flag config --bool foo.flag - } >actual && - test_cmp expect actual && - test_must_fail git -c name=value config core.name -' - -# We just need a type-specifier here that cares about the -# distinction internally between a NULL boolean and a real -# string (because most of git's internal parsers do care). -# Using "--path" works, but we do not otherwise care about -# its semantics. -test_expect_success 'git -c can represent empty string' ' - echo >expect && - git -c foo.empty= config --path foo.empty >actual && - test_cmp expect actual -' - -test_expect_success 'key sanity-checking' ' - test_must_fail git config foo=bar && - test_must_fail git config foo=.bar && - test_must_fail git config foo.ba=r && - test_must_fail git config foo.1bar && - test_must_fail git config foo."ba - z".bar && - test_must_fail git config . false && - test_must_fail git config .foo false && - test_must_fail git config foo. false && - test_must_fail git config .foo. false && - git config foo.bar true && - git config foo."ba =z".bar false -' - -test_expect_success 'git -c works with aliases of builtins' ' - git config alias.checkconfig "-c foo.check=bar config foo.check" && - echo bar >expect && - git checkconfig >actual && - test_cmp expect actual -' - -test_expect_success 'aliases can be CamelCased' ' - test_config alias.CamelCased "rev-parse HEAD" && - git CamelCased >out && - git rev-parse HEAD >expect && - test_cmp expect out -' - -test_expect_success 'git -c does not split values on equals' ' - echo "value with = in it" >expect && - git -c core.foo="value with = in it" config core.foo >actual && - test_cmp expect actual -' - -test_expect_success 'git -c dies on bogus config' ' - test_must_fail git -c core.bare=foo rev-parse -' - -test_expect_success 'git -c complains about empty key' ' - test_must_fail git -c "=foo" rev-parse -' - -test_expect_success 'git -c complains about empty key and value' ' - test_must_fail git -c "" rev-parse -' - -test_expect_success 'multiple git -c appends config' ' - test_config alias.x "!git -c x.two=2 config --get-regexp ^x\.*" && - cat >expect <<-\EOF && - x.one 1 - x.two 2 - EOF - git -c x.one=1 x >actual && - test_cmp expect actual -' - -test_expect_success 'last one wins: two level vars' ' - - # sec.var and sec.VAR are the same variable, as the first - # and the last level of a configuration variable name is - # case insensitive. - - echo VAL >expect && - - git -c sec.var=val -c sec.VAR=VAL config --get sec.var >actual && - test_cmp expect actual && - git -c SEC.var=val -c sec.var=VAL config --get sec.var >actual && - test_cmp expect actual && - - git -c sec.var=val -c sec.VAR=VAL config --get SEC.var >actual && - test_cmp expect actual && - git -c SEC.var=val -c sec.var=VAL config --get sec.VAR >actual && - test_cmp expect actual -' - -test_expect_success 'last one wins: three level vars' ' - - # v.a.r and v.A.r are not the same variable, as the middle - # level of a three-level configuration variable name is - # case sensitive. - - echo val >expect && - git -c v.a.r=val -c v.A.r=VAL config --get v.a.r >actual && - test_cmp expect actual && - git -c v.a.r=val -c v.A.r=VAL config --get V.a.R >actual && - test_cmp expect actual && - - # v.a.r and V.a.R are the same variable, as the first - # and the last level of a configuration variable name is - # case insensitive. - - echo VAL >expect && - git -c v.a.r=val -c v.a.R=VAL config --get v.a.r >actual && - test_cmp expect actual && - git -c v.a.r=val -c V.a.r=VAL config --get v.a.r >actual && - test_cmp expect actual && - git -c v.a.r=val -c v.a.R=VAL config --get V.a.R >actual && - test_cmp expect actual && - git -c v.a.r=val -c V.a.r=VAL config --get V.a.R >actual && - test_cmp expect actual -' - -for VAR in a .a a. a.0b a."b c". a."b c".0d -do - test_expect_success "git -c $VAR=VAL rejects invalid '$VAR'" ' - test_must_fail git -c "$VAR=VAL" config -l - ' -done - -for VAR in a.b a."b c".d -do - test_expect_success "git -c $VAR=VAL works with valid '$VAR'" ' - echo VAL >expect && - git -c "$VAR=VAL" config --get "$VAR" >actual && - test_cmp expect actual - ' -done - -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 --edit works' ' - git config -f tmp test.value no && - echo test.value=yes >expect && - GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit && - git config -f tmp --list >actual && - test_cmp expect actual -' - -test_expect_success 'git config --edit respects core.editor' ' - git config -f tmp test.value no && - echo test.value=yes >expect && - test_config core.editor "echo [test]value=yes >" && - git config -f tmp --edit && - git config -f tmp --list >actual && - test_cmp expect actual -' - -# malformed configuration files -test_expect_success 'barf on syntax error' ' - cat >.git/config <<-\EOF && - # broken section line - [section] - key garbage - EOF - test_must_fail git config --get section.key >actual 2>error && - test_i18ngrep " line 3 " error -' - -test_expect_success 'barf on incomplete section header' ' - cat >.git/config <<-\EOF && - # broken section line - [section - key = value - EOF - test_must_fail git config --get section.key >actual 2>error && - test_i18ngrep " line 2 " error -' - -test_expect_success 'barf on incomplete string' ' - cat >.git/config <<-\EOF && - # broken section line - [section] - key = "value string - EOF - test_must_fail git config --get section.key >actual 2>error && - test_i18ngrep " line 3 " error -' - -test_expect_success 'urlmatch' ' - cat >.git/config <<-\EOF && - [http] - sslVerify - [http "https://weak.example.com"] - sslVerify = false - cookieFile = /tmp/cookie.txt - EOF - - test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual && - test_must_be_empty actual && - - echo true >expect && - git config --bool --get-urlmatch http.SSLverify https://good.example.com >actual && - test_cmp expect actual && - - echo false >expect && - git config --bool --get-urlmatch http.sslverify https://weak.example.com >actual && - test_cmp expect actual && - - { - echo http.cookiefile /tmp/cookie.txt && - echo http.sslverify false - } >expect && - git config --get-urlmatch HTTP https://weak.example.com >actual && - test_cmp expect actual -' - -test_expect_success 'urlmatch favors more specific URLs' ' - cat >.git/config <<-\EOF && - [http "https://example.com/"] - cookieFile = /tmp/root.txt - [http "https://example.com/subdirectory"] - cookieFile = /tmp/subdirectory.txt - [http "https://user@example.com/"] - cookieFile = /tmp/user.txt - [http "https://averylonguser@example.com/"] - cookieFile = /tmp/averylonguser.txt - [http "https://preceding.example.com"] - cookieFile = /tmp/preceding.txt - [http "https://*.example.com"] - cookieFile = /tmp/wildcard.txt - [http "https://*.example.com/wildcardwithsubdomain"] - cookieFile = /tmp/wildcardwithsubdomain.txt - [http "https://trailing.example.com"] - cookieFile = /tmp/trailing.txt - [http "https://user@*.example.com/"] - cookieFile = /tmp/wildcardwithuser.txt - [http "https://sub.example.com/"] - cookieFile = /tmp/sub.txt - EOF - - echo http.cookiefile /tmp/root.txt >expect && - git config --get-urlmatch HTTP https://example.com >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/subdirectory.txt >expect && - git config --get-urlmatch HTTP https://example.com/subdirectory >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/subdirectory.txt >expect && - git config --get-urlmatch HTTP https://example.com/subdirectory/nested >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/user.txt >expect && - git config --get-urlmatch HTTP https://user@example.com/ >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/subdirectory.txt >expect && - git config --get-urlmatch HTTP https://averylonguser@example.com/subdirectory >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/preceding.txt >expect && - git config --get-urlmatch HTTP https://preceding.example.com >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/wildcard.txt >expect && - git config --get-urlmatch HTTP https://wildcard.example.com >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/sub.txt >expect && - git config --get-urlmatch HTTP https://sub.example.com/wildcardwithsubdomain >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/trailing.txt >expect && - git config --get-urlmatch HTTP https://trailing.example.com >actual && - test_cmp expect actual && - - echo http.cookiefile /tmp/sub.txt >expect && - git config --get-urlmatch HTTP https://user@sub.example.com >actual && - test_cmp expect actual -' - -test_expect_success 'urlmatch with wildcard' ' - cat >.git/config <<-\EOF && - [http] - sslVerify - [http "https://*.example.com"] - sslVerify = false - cookieFile = /tmp/cookie.txt - EOF - - test_expect_code 1 git config --bool --get-urlmatch doesnt.exist https://good.example.com >actual && - test_must_be_empty actual && - - echo true >expect && - git config --bool --get-urlmatch http.SSLverify https://example.com >actual && - test_cmp expect actual && - - echo true >expect && - git config --bool --get-urlmatch http.SSLverify https://good-example.com >actual && - test_cmp expect actual && - - echo true >expect && - git config --bool --get-urlmatch http.sslverify https://deep.nested.example.com >actual && - test_cmp expect actual && - - echo false >expect && - git config --bool --get-urlmatch http.sslverify https://good.example.com >actual && - test_cmp expect actual && - - { - echo http.cookiefile /tmp/cookie.txt && - echo http.sslverify false - } >expect && - git config --get-urlmatch HTTP https://good.example.com >actual && - test_cmp expect actual && - - echo http.sslverify >expect && - git config --get-urlmatch HTTP https://more.example.com.au >actual && - test_cmp expect actual -' - -# good section hygiene -test_expect_failure 'unsetting the last key in a section removes header' ' - cat >.git/config <<-\EOF && - # some generic comment on the configuration file itself - # a comment specific to this "section" section. - [section] - # some intervening lines - # that should also be dropped - - key = value - # please be careful when you update the above variable - EOF - - cat >expect <<-\EOF && - # some generic comment on the configuration file itself - EOF - - git config --unset section.key && - test_cmp expect .git/config -' - -test_expect_failure 'adding a key into an empty section reuses header' ' - cat >.git/config <<-\EOF && - [section] - EOF - - q_to_tab >expect <<-\EOF && - [section] - Qkey = value - EOF - - git config section.key value && - test_cmp expect .git/config -' - -test_expect_success POSIXPERM,PERL 'preserves existing permissions' ' - chmod 0600 .git/config && - git config imap.pass Hunter2 && - perl -e \ - "die q(badset) if ((stat(q(.git/config)))[2] & 07777) != 0600" && - git config --rename-section imap pop && - perl -e \ - "die q(badrename) if ((stat(q(.git/config)))[2] & 07777) != 0600" -' - -! test_have_prereq MINGW || -HOME="$(pwd)" # convert to Windows path - -test_expect_success 'set up --show-origin tests' ' - INCLUDE_DIR="$HOME/include" && - mkdir -p "$INCLUDE_DIR" && - cat >"$INCLUDE_DIR"/absolute.include <<-\EOF && - [user] - absolute = include - EOF - cat >"$INCLUDE_DIR"/relative.include <<-\EOF && - [user] - relative = include - EOF - cat >"$HOME"/.gitconfig <<-EOF && - [user] - global = true - override = global - [include] - path = "$INCLUDE_DIR/absolute.include" - EOF - cat >.git/config <<-\EOF - [user] - local = true - override = local - [include] - path = ../include/relative.include - EOF -' - -test_expect_success '--show-origin with --list' ' - cat >expect <<-EOF && - file:$HOME/.gitconfig user.global=true - file:$HOME/.gitconfig user.override=global - file:$HOME/.gitconfig include.path=$INCLUDE_DIR/absolute.include - file:$INCLUDE_DIR/absolute.include user.absolute=include - file:.git/config user.local=true - 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.cmdline=true - EOF - git -c user.cmdline=true config --list --show-origin >output && - test_cmp expect output -' - -test_expect_success '--show-origin with --list --null' ' - cat >expect <<-EOF && - file:$HOME/.gitconfigQuser.global - trueQfile:$HOME/.gitconfigQuser.override - globalQfile:$HOME/.gitconfigQinclude.path - $INCLUDE_DIR/absolute.includeQfile:$INCLUDE_DIR/absolute.includeQuser.absolute - includeQfile:.git/configQuser.local - trueQfile:.git/configQuser.override - localQfile:.git/configQinclude.path - ../include/relative.includeQfile:.git/../include/relative.includeQuser.relative - includeQcommand line:Quser.cmdline - trueQ - EOF - git -c user.cmdline=true config --null --list --show-origin >output.raw && - nul_to_q output && - # The here-doc above adds a newline that the --null output would not - # include. Add it here to make the two comparable. - echo >>output && - test_cmp expect output -' - -test_expect_success '--show-origin with single file' ' - cat >expect <<-\EOF && - file:.git/config user.local=true - file:.git/config user.override=local - file:.git/config include.path=../include/relative.include - EOF - git config --local --list --show-origin >output && - test_cmp expect output -' - -test_expect_success '--show-origin with --get-regexp' ' - cat >expect <<-EOF && - file:$HOME/.gitconfig user.global true - file:.git/config user.local true - EOF - git config --show-origin --get-regexp "user\.[g|l].*" >output && - test_cmp expect output -' - -test_expect_success '--show-origin getting a single key' ' - cat >expect <<-\EOF && - file:.git/config local - EOF - git config --show-origin user.override >output && - test_cmp expect output -' - -test_expect_success 'set up custom config file' ' - CUSTOM_CONFIG_FILE="file\" (dq) and spaces.conf" && - cat >"$CUSTOM_CONFIG_FILE" <<-\EOF - [user] - custom = true - EOF -' - -test_expect_success !MINGW '--show-origin escape special file name characters' ' - cat >expect <<-\EOF && - file:"file\" (dq) and spaces.conf" user.custom=true - EOF - git config --file "$CUSTOM_CONFIG_FILE" --show-origin --list >output && - test_cmp expect output -' - -test_expect_success '--show-origin stdin' ' - cat >expect <<-\EOF && - standard input: user.custom=true - EOF - git config --file - --show-origin --list <"$CUSTOM_CONFIG_FILE" >output && - test_cmp expect output -' - -test_expect_success '--show-origin stdin with file include' ' - cat >"$INCLUDE_DIR"/stdin.include <<-EOF && - [user] - stdin = include - EOF - cat >expect <<-EOF && - file:$INCLUDE_DIR/stdin.include include - EOF - echo "[include]path=\"$INCLUDE_DIR\"/stdin.include" \ - | git config --show-origin --includes --file - user.stdin >output && - test_cmp expect output -' - -test_expect_success !MINGW '--show-origin blob' ' - cat >expect <<-\EOF && - blob:a9d9f9e555b5c6f07cbe09d3f06fe3df11e09c08 user.custom=true - EOF - blob=$(git hash-object -w "$CUSTOM_CONFIG_FILE") && - git config --blob=$blob --show-origin --list >output && - test_cmp expect output -' - -test_expect_success !MINGW '--show-origin blob ref' ' - cat >expect <<-\EOF && - blob:"master:file\" (dq) and spaces.conf" user.custom=true - EOF - git add "$CUSTOM_CONFIG_FILE" && - git commit -m "new config file" && - git config --blob=master:"$CUSTOM_CONFIG_FILE" --show-origin --list >output && - test_cmp expect output -' - -test_expect_success '--local requires a repo' ' - # we expect 128 to ensure that we do not simply - # fail to find anything and return code "1" - test_expect_code 128 nongit git config --local foo.bar -' - -test_done -- cgit v1.3 From e9313952bf85bcc8b602f582b2e71660d670f97b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 3 Apr 2018 18:28:10 +0200 Subject: t1300: demonstrate that --replace-all can "invent" newlines Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1300-config.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/t/t1300-config.sh b/t/t1300-config.sh index cbeb9bebee..cef816325b 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1588,4 +1588,25 @@ test_expect_success '--local requires a repo' ' test_expect_code 128 nongit git config --local foo.bar ' +test_expect_failure '--replace-all does not invent newlines' ' + q_to_tab >.git/config <<-\EOF && + [abc]key + QkeepSection + [xyz] + Qkey = 1 + [abc] + Qkey = a + EOF + q_to_tab >expect <<-\EOF && + [abc] + QkeepSection + [xyz] + Qkey = 1 + [abc] + Qkey = b + EOF + git config --replace-all abc.key b && + test_cmp .git/config expect +' + test_done -- cgit v1.3 From 46fc89ce74b46e88764c796b3ab20d5ab90a5e96 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 3 Apr 2018 18:28:14 +0200 Subject: config --replace-all: avoid extra line breaks When replacing multiple config entries at once, we did not re-set the flag that indicates whether we need to insert a new-line before the new entry. As a consequence, an extra new-line was inserted under certain circumstances. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 1 + t/t1300-config.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index 4c8571ab33..c55d6a564e 100644 --- a/config.c +++ b/config.c @@ -2617,6 +2617,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, store.seen = 1; for (i = 0, copy_begin = 0; i < store.seen; i++) { + new_line = 0; if (store.offset[i] == 0) { store.offset[i] = copy_end = contents_sz; } else if (store.state != KEY_SEEN) { diff --git a/t/t1300-config.sh b/t/t1300-config.sh index cef816325b..8f37ffadb1 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1588,7 +1588,7 @@ test_expect_success '--local requires a repo' ' test_expect_code 128 nongit git config --local foo.bar ' -test_expect_failure '--replace-all does not invent newlines' ' +test_expect_success '--replace-all does not invent newlines' ' q_to_tab >.git/config <<-\EOF && [abc]key QkeepSection -- cgit v1.3 From 85bf5d61e717f79f7ac68d15e336e54293035405 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 3 Apr 2018 18:28:18 +0200 Subject: t1300: avoid relying on a bug The test case 'unset with cont. lines' relied on a bug that is about to be fixed: it tests *explicitly* that removing the last entry from a config section leaves an *empty* section behind. Let's fix this test case not to rely on that behavior, simply by preventing the section from becoming empty. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1300-config.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 8f37ffadb1..05c011ee03 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -108,6 +108,7 @@ bar = foo [beta] baz = multiple \ lines +foo = bar EOF test_expect_success 'unset with cont. lines' ' @@ -118,6 +119,7 @@ cat > expect <<\EOF [alpha] bar = foo [beta] +foo = bar EOF test_expect_success 'unset with cont. lines is correct' 'test_cmp expect .git/config' -- cgit v1.3 From dde154b5bd84e6258b496498901eb24b4914ec6b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 3 Apr 2018 18:28:22 +0200 Subject: t1300: remove unreasonable expectation from TODO In https://public-inbox.org/git/7vvc8alzat.fsf@alter.siamese.dyndns.org/ a reasonable patch was made quite a bit less so by changing a test case demonstrating a bug to a test case that demonstrates that we ask for too much: the test case 'unsetting the last key in a section removes header' now expects a future bug fix to be able to determine whether a free-form comment above a section header refers to said section or not. Rather than shooting for the stars (and not even getting off the ground), let's start shooting for something obtainable and be reasonably confident that we *can* get it. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1300-config.sh | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 05c011ee03..3ab83fff8f 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1390,7 +1390,7 @@ test_expect_success 'urlmatch with wildcard' ' ' # good section hygiene -test_expect_failure 'unsetting the last key in a section removes header' ' +test_expect_failure '--unset last key removes section (except if commented)' ' cat >.git/config <<-\EOF && # some generic comment on the configuration file itself # a comment specific to this "section" section. @@ -1404,6 +1404,25 @@ test_expect_failure 'unsetting the last key in a section removes header' ' cat >expect <<-\EOF && # some generic comment on the configuration file itself + # a comment specific to this "section" section. + [section] + # some intervening lines + # that should also be dropped + + # please be careful when you update the above variable + EOF + + git config --unset section.key && + test_cmp expect .git/config && + + cat >.git/config <<-\EOF && + [section] + key = value + [next-section] + EOF + + cat >expect <<-\EOF && + [next-section] EOF git config --unset section.key && -- cgit v1.3 From 422e8ef26d35a7e54d5b7b990669f4a525cb8828 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:31:57 +0200 Subject: t1300: add a few more hairy examples of sections becoming empty During the review of the first iteration of the patch series to remove sections that become empty upon --unset or --unset-all, Jeff King identified a couple of problematic cases with the backtracking approach that was still used then to "look backwards for the section header": https://public-inbox.org/git/20180329213229.GG2939@sigill.intra.peff.net/ This patch adds a couple of concocted examples designed to fool a backtracking parser. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1300-config.sh | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 3ab83fff8f..a59c07fcb7 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1426,7 +1426,50 @@ test_expect_failure '--unset last key removes section (except if commented)' ' EOF git config --unset section.key && - test_cmp expect .git/config + test_cmp expect .git/config && + + q_to_tab >.git/config <<-\EOF && + [one] + Qkey = "multiline \ + QQ# with comment" + [two] + key = true + EOF + git config --unset two.key && + ! grep two .git/config && + + q_to_tab >.git/config <<-\EOF && + [one] + Qkey = "multiline \ + QQ# with comment" + [one] + key = true + EOF + git config --unset-all one.key && + test_line_count = 0 .git/config && + + q_to_tab >.git/config <<-\EOF && + [one] + Qkey = true + Q# a comment not at the start + [two] + Qkey = true + EOF + git config --unset two.key && + grep two .git/config && + + q_to_tab >.git/config <<-\EOF && + [one] + Qkey = not [two "subsection"] + [two "subsection"] + [two "subsection"] + Qkey = true + [TWO "subsection"] + [one] + EOF + git config --unset two.subsection.key && + test "not [two subsection]" = "$(git config one.key)" && + test_line_count = 3 .git/config ' test_expect_failure 'adding a key into an empty section reuses header' ' -- cgit v1.3 From b73bdc34c06358359750a896983a2ea85b694ca0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:02 +0200 Subject: t1300: `--unset-all` can leave an empty section behind (bug) We already have a test demonstrating that removing the last entry from a config section fails to remove the section header of the now-empty section. The same can happen, of course, if we remove the last entries in one fell swoop. This is *also* a bug, and should be fixed at the same time. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1300-config.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/t1300-config.sh b/t/t1300-config.sh index a59c07fcb7..8a3cd2c114 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1472,6 +1472,17 @@ test_expect_failure '--unset last key removes section (except if commented)' ' test_line_count = 3 .git/config ' +test_expect_failure '--unset-all removes section if empty & uncommented' ' + cat >.git/config <<-\EOF && + [section] + key = value1 + key = value2 + EOF + + git config --unset-all section.key && + test_line_count = 0 .git/config +' + test_expect_failure 'adding a key into an empty section reuses header' ' cat >.git/config <<-\EOF && [section] -- cgit v1.3 From 8032cc4462b8af268fe457c2a0431473334dfd18 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:05 +0200 Subject: config: introduce an optional event stream while parsing This extends our config parser so that it can optionally produce an event stream via callback function, where it reports e.g. when a comment was parsed, or a section header, etc. This parser will be used subsequently to handle the scenarios better where removing config entries would make sections empty, or where a new entry could be added to an already-existing, empty section. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- config.h | 25 ++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/config.c b/config.c index c55d6a564e..d53ed88281 100644 --- a/config.c +++ b/config.c @@ -653,7 +653,45 @@ static int get_base_var(struct strbuf *name) } } -static int git_parse_source(config_fn_t fn, void *data) +struct parse_event_data { + enum config_event_t previous_type; + size_t previous_offset; + const struct config_options *opts; +}; + +static int do_event(enum config_event_t type, struct parse_event_data *data) +{ + size_t offset; + + if (!data->opts || !data->opts->event_fn) + return 0; + + if (type == CONFIG_EVENT_WHITESPACE && + data->previous_type == type) + return 0; + + offset = cf->do_ftell(cf); + /* + * At EOF, the parser always "inserts" an extra '\n', therefore + * the end offset of the event is the current file position, otherwise + * we will already have advanced to the next event. + */ + if (type != CONFIG_EVENT_EOF) + offset--; + + if (data->previous_type != CONFIG_EVENT_EOF && + data->opts->event_fn(data->previous_type, data->previous_offset, + offset, data->opts->event_fn_data) < 0) + return -1; + + data->previous_type = type; + data->previous_offset = offset; + + return 0; +} + +static int git_parse_source(config_fn_t fn, void *data, + const struct config_options *opts) { int comment = 0; int baselen = 0; @@ -664,8 +702,15 @@ static int git_parse_source(config_fn_t fn, void *data) /* U+FEFF Byte Order Mark in UTF8 */ const char *bomptr = utf8_bom; + /* For the parser event callback */ + struct parse_event_data event_data = { + CONFIG_EVENT_EOF, 0, opts + }; + for (;;) { - int c = get_next_char(); + int c; + + c = get_next_char(); if (bomptr && *bomptr) { /* We are at the file beginning; skip UTF8-encoded BOM * if present. Sane editors won't put this in on their @@ -682,18 +727,33 @@ static int git_parse_source(config_fn_t fn, void *data) } } if (c == '\n') { - if (cf->eof) + if (cf->eof) { + if (do_event(CONFIG_EVENT_EOF, &event_data) < 0) + return -1; return 0; + } + if (do_event(CONFIG_EVENT_WHITESPACE, &event_data) < 0) + return -1; comment = 0; continue; } - if (comment || isspace(c)) + if (comment) continue; + if (isspace(c)) { + if (do_event(CONFIG_EVENT_WHITESPACE, &event_data) < 0) + return -1; + continue; + } if (c == '#' || c == ';') { + if (do_event(CONFIG_EVENT_COMMENT, &event_data) < 0) + return -1; comment = 1; continue; } if (c == '[') { + if (do_event(CONFIG_EVENT_SECTION, &event_data) < 0) + return -1; + /* Reset prior to determining a new stem */ strbuf_reset(var); if (get_base_var(var) < 0 || var->len < 1) @@ -704,6 +764,10 @@ static int git_parse_source(config_fn_t fn, void *data) } if (!isalpha(c)) break; + + if (do_event(CONFIG_EVENT_ENTRY, &event_data) < 0) + return -1; + /* * Truncate the var name back to the section header * stem prior to grabbing the suffix part of the name @@ -715,6 +779,9 @@ static int git_parse_source(config_fn_t fn, void *data) break; } + if (do_event(CONFIG_EVENT_ERROR, &event_data) < 0) + return -1; + switch (cf->origin_type) { case CONFIG_ORIGIN_BLOB: error_msg = xstrfmt(_("bad config line %d in blob %s"), @@ -1390,7 +1457,8 @@ int git_default_config(const char *var, const char *value, void *dummy) * fgetc, ungetc, ftell of top need to be initialized before calling * this function. */ -static int do_config_from(struct config_source *top, config_fn_t fn, void *data) +static int do_config_from(struct config_source *top, config_fn_t fn, void *data, + const struct config_options *opts) { int ret; @@ -1402,7 +1470,7 @@ static int do_config_from(struct config_source *top, config_fn_t fn, void *data) strbuf_init(&top->var, 1024); cf = top; - ret = git_parse_source(fn, data); + ret = git_parse_source(fn, data, opts); /* pop config-file parsing state stack */ strbuf_release(&top->value); @@ -1415,7 +1483,7 @@ static int do_config_from(struct config_source *top, config_fn_t fn, void *data) static int do_config_from_file(config_fn_t fn, const enum config_origin_type origin_type, const char *name, const char *path, FILE *f, - void *data) + void *data, const struct config_options *opts) { struct config_source top; @@ -1428,15 +1496,18 @@ static int do_config_from_file(config_fn_t fn, top.do_ungetc = config_file_ungetc; top.do_ftell = config_file_ftell; - return do_config_from(&top, fn, data); + return do_config_from(&top, fn, data, opts); } static int git_config_from_stdin(config_fn_t fn, void *data) { - return do_config_from_file(fn, CONFIG_ORIGIN_STDIN, "", NULL, stdin, data); + return do_config_from_file(fn, CONFIG_ORIGIN_STDIN, "", NULL, stdin, + data, NULL); } -int git_config_from_file(config_fn_t fn, const char *filename, void *data) +int git_config_from_file_with_options(config_fn_t fn, const char *filename, + void *data, + const struct config_options *opts) { int ret = -1; FILE *f; @@ -1444,13 +1515,19 @@ int git_config_from_file(config_fn_t fn, const char *filename, void *data) f = fopen_or_warn(filename, "r"); if (f) { flockfile(f); - ret = do_config_from_file(fn, CONFIG_ORIGIN_FILE, filename, filename, f, data); + ret = do_config_from_file(fn, CONFIG_ORIGIN_FILE, filename, + filename, f, data, opts); funlockfile(f); fclose(f); } return ret; } +int git_config_from_file(config_fn_t fn, const char *filename, void *data) +{ + return git_config_from_file_with_options(fn, filename, data, NULL); +} + int git_config_from_mem(config_fn_t fn, const enum config_origin_type origin_type, const char *name, const char *buf, size_t len, void *data) { @@ -1467,7 +1544,7 @@ int git_config_from_mem(config_fn_t fn, const enum config_origin_type origin_typ top.do_ungetc = config_buf_ungetc; top.do_ftell = config_buf_ftell; - return do_config_from(&top, fn, data); + return do_config_from(&top, fn, data, NULL); } int git_config_from_blob_oid(config_fn_t fn, diff --git a/config.h b/config.h index ef70a9cac1..5a2394daae 100644 --- a/config.h +++ b/config.h @@ -28,15 +28,40 @@ enum config_origin_type { CONFIG_ORIGIN_CMDLINE }; +enum config_event_t { + CONFIG_EVENT_SECTION, + CONFIG_EVENT_ENTRY, + CONFIG_EVENT_WHITESPACE, + CONFIG_EVENT_COMMENT, + CONFIG_EVENT_EOF, + CONFIG_EVENT_ERROR +}; + +/* + * The parser event function (if not NULL) is called with the event type and + * the begin/end offsets of the parsed elements. + * + * Note: for CONFIG_EVENT_ENTRY (i.e. config variables), the trailing newline + * character is considered part of the element. + */ +typedef int (*config_parser_event_fn_t)(enum config_event_t type, + size_t begin_offset, size_t end_offset, + void *event_fn_data); + struct config_options { unsigned int respect_includes : 1; const char *commondir; const char *git_dir; + config_parser_event_fn_t event_fn; + void *event_fn_data; }; typedef int (*config_fn_t)(const char *, const char *, void *); extern int git_default_config(const char *, const char *, void *); extern int git_config_from_file(config_fn_t fn, const char *, void *); +extern int git_config_from_file_with_options(config_fn_t fn, const char *, + void *, + const struct config_options *); extern int git_config_from_mem(config_fn_t fn, const enum config_origin_type, const char *name, const char *buf, size_t len, void *data); extern int git_config_from_blob_oid(config_fn_t fn, const char *name, -- cgit v1.3 From fee8572c6ddf6afcfeba023067fea36b835a9df4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:09 +0200 Subject: config: avoid using the global variable `store` It is much easier to reason about, when the config code to set/unset variables or to remove/rename sections does not rely on a global (or file-local) variable. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 119 +++++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/config.c b/config.c index d53ed88281..6aee5d3d7c 100644 --- a/config.c +++ b/config.c @@ -2288,7 +2288,7 @@ void git_die_config(const char *key, const char *err, ...) * Find all the stuff for git_config_set() below. */ -static struct { +struct config_store_data { int baselen; char *key; int do_not_match; @@ -2298,56 +2298,58 @@ static struct { unsigned int offset_alloc; enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state; unsigned int seen; -} store; +}; -static int matches(const char *key, const char *value) +static int matches(const char *key, const char *value, + const struct config_store_data *store) { - if (strcmp(key, store.key)) + if (strcmp(key, store->key)) return 0; /* not ours */ - if (!store.value_regex) + if (!store->value_regex) return 1; /* always matches */ - if (store.value_regex == CONFIG_REGEX_NONE) + if (store->value_regex == CONFIG_REGEX_NONE) return 0; /* never matches */ - return store.do_not_match ^ - (value && !regexec(store.value_regex, value, 0, NULL, 0)); + return store->do_not_match ^ + (value && !regexec(store->value_regex, value, 0, NULL, 0)); } static int store_aux(const char *key, const char *value, void *cb) { const char *ep; size_t section_len; + struct config_store_data *store = cb; - switch (store.state) { + switch (store->state) { case KEY_SEEN: - if (matches(key, value)) { - if (store.seen == 1 && store.multi_replace == 0) { + if (matches(key, value, store)) { + if (store->seen == 1 && store->multi_replace == 0) { warning(_("%s has multiple values"), key); } - ALLOC_GROW(store.offset, store.seen + 1, - store.offset_alloc); + ALLOC_GROW(store->offset, store->seen + 1, + store->offset_alloc); - store.offset[store.seen] = cf->do_ftell(cf); - store.seen++; + store->offset[store->seen] = cf->do_ftell(cf); + store->seen++; } break; case SECTION_SEEN: /* - * What we are looking for is in store.key (both + * What we are looking for is in store->key (both * section and var), and its section part is baselen * long. We found key (again, both section and var). * We would want to know if this key is in the same * section as what we are looking for. We already * know we are in the same section as what should - * hold store.key. + * hold store->key. */ ep = strrchr(key, '.'); section_len = ep - key; - if ((section_len != store.baselen) || - memcmp(key, store.key, section_len+1)) { - store.state = SECTION_END_SEEN; + if ((section_len != store->baselen) || + memcmp(key, store->key, section_len+1)) { + store->state = SECTION_END_SEEN; break; } @@ -2355,26 +2357,27 @@ static int store_aux(const char *key, const char *value, void *cb) * Do not increment matches: this is no match, but we * just made sure we are in the desired section. */ - ALLOC_GROW(store.offset, store.seen + 1, - store.offset_alloc); - store.offset[store.seen] = cf->do_ftell(cf); + ALLOC_GROW(store->offset, store->seen + 1, + store->offset_alloc); + store->offset[store->seen] = cf->do_ftell(cf); /* fallthru */ case SECTION_END_SEEN: case START: - if (matches(key, value)) { - ALLOC_GROW(store.offset, store.seen + 1, - store.offset_alloc); - store.offset[store.seen] = cf->do_ftell(cf); - store.state = KEY_SEEN; - store.seen++; + if (matches(key, value, store)) { + ALLOC_GROW(store->offset, store->seen + 1, + store->offset_alloc); + store->offset[store->seen] = cf->do_ftell(cf); + store->state = KEY_SEEN; + store->seen++; } else { - if (strrchr(key, '.') - key == store.baselen && - !strncmp(key, store.key, store.baselen)) { - store.state = SECTION_SEEN; - ALLOC_GROW(store.offset, - store.seen + 1, - store.offset_alloc); - store.offset[store.seen] = cf->do_ftell(cf); + if (strrchr(key, '.') - key == store->baselen && + !strncmp(key, store->key, store->baselen)) { + store->state = SECTION_SEEN; + ALLOC_GROW(store->offset, + store->seen + 1, + store->offset_alloc); + store->offset[store->seen] = + cf->do_ftell(cf); } } } @@ -2389,31 +2392,33 @@ static int write_error(const char *filename) return 4; } -static struct strbuf store_create_section(const char *key) +static struct strbuf store_create_section(const char *key, + const struct config_store_data *store) { const char *dot; int i; struct strbuf sb = STRBUF_INIT; - dot = memchr(key, '.', store.baselen); + dot = memchr(key, '.', store->baselen); if (dot) { strbuf_addf(&sb, "[%.*s \"", (int)(dot - key), key); - for (i = dot - key + 1; i < store.baselen; i++) { + for (i = dot - key + 1; i < store->baselen; i++) { if (key[i] == '"' || key[i] == '\\') strbuf_addch(&sb, '\\'); strbuf_addch(&sb, key[i]); } strbuf_addstr(&sb, "\"]\n"); } else { - strbuf_addf(&sb, "[%.*s]\n", store.baselen, key); + strbuf_addf(&sb, "[%.*s]\n", store->baselen, key); } return sb; } -static ssize_t write_section(int fd, const char *key) +static ssize_t write_section(int fd, const char *key, + const struct config_store_data *store) { - struct strbuf sb = store_create_section(key); + struct strbuf sb = store_create_section(key, store); ssize_t ret; ret = write_in_full(fd, sb.buf, sb.len); @@ -2422,11 +2427,12 @@ static ssize_t write_section(int fd, const char *key) return ret; } -static ssize_t write_pair(int fd, const char *key, const char *value) +static ssize_t write_pair(int fd, const char *key, const char *value, + const struct config_store_data *store) { int i; ssize_t ret; - int length = strlen(key + store.baselen + 1); + int length = strlen(key + store->baselen + 1); const char *quote = ""; struct strbuf sb = STRBUF_INIT; @@ -2446,7 +2452,7 @@ static ssize_t write_pair(int fd, const char *key, const char *value) quote = "\""; strbuf_addf(&sb, "\t%.*s = %s", - length, key + store.baselen + 1, quote); + length, key + store->baselen + 1, quote); for (i = 0; value[i]; i++) switch (value[i]) { @@ -2556,6 +2562,9 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, char *filename_buf = NULL; char *contents = NULL; size_t contents_sz; + struct config_store_data store; + + memset(&store, 0, sizeof(store)); /* parse-key returns negative; flip the sign to feed exit(3) */ ret = 0 - git_config_parse_key(key, &store.key, &store.baselen); @@ -2598,8 +2607,8 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, } store.key = (char *)key; - if (write_section(fd, key) < 0 || - write_pair(fd, key, value) < 0) + if (write_section(fd, key, &store) < 0 || + write_pair(fd, key, value, &store) < 0) goto write_err_out; } else { struct stat st; @@ -2638,7 +2647,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, * As a side effect, we make sure to transform only a valid * existing config file. */ - if (git_config_from_file(store_aux, config_filename, NULL)) { + if (git_config_from_file(store_aux, config_filename, &store)) { error("invalid config file %s", config_filename); free(store.key); if (store.value_regex != NULL && @@ -2722,10 +2731,10 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, /* write the pair (value == NULL means unset) */ if (value != NULL) { if (store.state == START) { - if (write_section(fd, key) < 0) + if (write_section(fd, key, &store) < 0) goto write_err_out; } - if (write_pair(fd, key, value) < 0) + if (write_pair(fd, key, value, &store) < 0) goto write_err_out; } @@ -2849,7 +2858,8 @@ static int section_name_is_ok(const char *name) /* if new_name == NULL, the section is removed instead */ static int git_config_copy_or_rename_section_in_file(const char *config_filename, - const char *old_name, const char *new_name, int copy) + const char *old_name, + const char *new_name, int copy) { int ret = 0, remove = 0; char *filename_buf = NULL; @@ -2859,6 +2869,9 @@ static int git_config_copy_or_rename_section_in_file(const char *config_filename FILE *config_file = NULL; struct stat st; struct strbuf copystr = STRBUF_INIT; + struct config_store_data store; + + memset(&store, 0, sizeof(store)); if (new_name && !section_name_is_ok(new_name)) { ret = error("invalid section name: %s", new_name); @@ -2928,7 +2941,7 @@ static int git_config_copy_or_rename_section_in_file(const char *config_filename } store.baselen = strlen(new_name); if (!copy) { - if (write_section(out_fd, new_name) < 0) { + if (write_section(out_fd, new_name, &store) < 0) { ret = write_error(get_lock_file_path(&lock)); goto out; } @@ -2949,7 +2962,7 @@ static int git_config_copy_or_rename_section_in_file(const char *config_filename output[0] = '\t'; } } else { - copystr = store_create_section(new_name); + copystr = store_create_section(new_name, &store); } } remove = 0; -- cgit v1.3 From 668b9ade6bcafbed1577468902d27c05c17cf026 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:13 +0200 Subject: config_set_store: rename some fields for consistency The `seen` field is the actual length of the `offset` array, and the `offset_alloc` field records what was allocated (to avoid resizing wherever `seen` has to be incremented). Elsewhere, we use the convention `name` for the array, where `name` is descriptive enough to guess its purpose, `name_nr` for the actual length and `name_alloc` to record the maximum length without needing to resize. Let's make the names of the fields in question consistent with that convention. This will also help with the next steps where we will let the git_config_set() machinery use the config event stream that we just introduced. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 63 +++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/config.c b/config.c index 6aee5d3d7c..9402acefa8 100644 --- a/config.c +++ b/config.c @@ -2294,10 +2294,9 @@ struct config_store_data { int do_not_match; regex_t *value_regex; int multi_replace; - size_t *offset; - unsigned int offset_alloc; + size_t *seen; + unsigned int seen_nr, seen_alloc; enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state; - unsigned int seen; }; static int matches(const char *key, const char *value, @@ -2323,15 +2322,15 @@ static int store_aux(const char *key, const char *value, void *cb) switch (store->state) { case KEY_SEEN: if (matches(key, value, store)) { - if (store->seen == 1 && store->multi_replace == 0) { + if (store->seen_nr == 1 && store->multi_replace == 0) { warning(_("%s has multiple values"), key); } - ALLOC_GROW(store->offset, store->seen + 1, - store->offset_alloc); + ALLOC_GROW(store->seen, store->seen_nr + 1, + store->seen_alloc); - store->offset[store->seen] = cf->do_ftell(cf); - store->seen++; + store->seen[store->seen_nr] = cf->do_ftell(cf); + store->seen_nr++; } break; case SECTION_SEEN: @@ -2357,26 +2356,26 @@ static int store_aux(const char *key, const char *value, void *cb) * Do not increment matches: this is no match, but we * just made sure we are in the desired section. */ - ALLOC_GROW(store->offset, store->seen + 1, - store->offset_alloc); - store->offset[store->seen] = cf->do_ftell(cf); + ALLOC_GROW(store->seen, store->seen_nr + 1, + store->seen_alloc); + store->seen[store->seen_nr] = cf->do_ftell(cf); /* fallthru */ case SECTION_END_SEEN: case START: if (matches(key, value, store)) { - ALLOC_GROW(store->offset, store->seen + 1, - store->offset_alloc); - store->offset[store->seen] = cf->do_ftell(cf); + ALLOC_GROW(store->seen, store->seen_nr + 1, + store->seen_alloc); + store->seen[store->seen_nr] = cf->do_ftell(cf); store->state = KEY_SEEN; - store->seen++; + store->seen_nr++; } else { if (strrchr(key, '.') - key == store->baselen && !strncmp(key, store->key, store->baselen)) { store->state = SECTION_SEEN; - ALLOC_GROW(store->offset, - store->seen + 1, - store->offset_alloc); - store->offset[store->seen] = + ALLOC_GROW(store->seen, + store->seen_nr + 1, + store->seen_alloc); + store->seen[store->seen_nr] = cf->do_ftell(cf); } } @@ -2636,10 +2635,10 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, } } - ALLOC_GROW(store.offset, 1, store.offset_alloc); - store.offset[0] = 0; + ALLOC_GROW(store.seen, 1, store.seen_alloc); + store.seen[0] = 0; store.state = START; - store.seen = 0; + store.seen_nr = 0; /* * After this, store.offset will contain the *end* offset @@ -2667,8 +2666,8 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, } /* if nothing to unset, or too many matches, error out */ - if ((store.seen == 0 && value == NULL) || - (store.seen > 1 && multi_replace == 0)) { + if ((store.seen_nr == 0 && value == NULL) || + (store.seen_nr > 1 && multi_replace == 0)) { ret = CONFIG_NOTHING_SET; goto out_free; } @@ -2699,19 +2698,19 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, goto out_free; } - if (store.seen == 0) - store.seen = 1; + if (store.seen_nr == 0) + store.seen_nr = 1; - for (i = 0, copy_begin = 0; i < store.seen; i++) { + for (i = 0, copy_begin = 0; i < store.seen_nr; i++) { new_line = 0; - if (store.offset[i] == 0) { - store.offset[i] = copy_end = contents_sz; + if (store.seen[i] == 0) { + store.seen[i] = copy_end = contents_sz; } else if (store.state != KEY_SEEN) { - copy_end = store.offset[i]; + copy_end = store.seen[i]; } else copy_end = find_beginning_of_line( contents, contents_sz, - store.offset[i], &new_line); + store.seen[i], &new_line); if (copy_end > 0 && contents[copy_end-1] != '\n') new_line = 1; @@ -2725,7 +2724,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, write_str_in_full(fd, "\n") < 0) goto write_err_out; } - copy_begin = store.offset[i]; + copy_begin = store.seen[i]; } /* write the pair (value == NULL means unset) */ -- cgit v1.3 From 5221c3159f670281ff36b6adbdf568661e930b50 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:17 +0200 Subject: git_config_set: do not use a state machine While a neat theoretical construct, state machines are hard to read. In this instance, it does not even make a whole lot of sense because we are more interested in flags, anyway: has the section been seen? Has the key been seen? Does the current section match the key we are looking for? Besides, the state `SECTION_SEEN` was named in a misleading way: it did not indicate that we saw the section matching the key we are looking for, but it instead indicated that we are *currently* in that section. Let's just replace the state machine logic by clear and obvious flags. This will also make it easier to review the upcoming patches to use the newly-introduced `event_fn` callback of the config parser. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 59 +++++++++++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/config.c b/config.c index 9402acefa8..036bae205c 100644 --- a/config.c +++ b/config.c @@ -2296,7 +2296,7 @@ struct config_store_data { int multi_replace; size_t *seen; unsigned int seen_nr, seen_alloc; - enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state; + unsigned int key_seen:1, section_seen:1, is_keys_section:1; }; static int matches(const char *key, const char *value, @@ -2319,8 +2319,7 @@ static int store_aux(const char *key, const char *value, void *cb) size_t section_len; struct config_store_data *store = cb; - switch (store->state) { - case KEY_SEEN: + if (store->key_seen) { if (matches(key, value, store)) { if (store->seen_nr == 1 && store->multi_replace == 0) { warning(_("%s has multiple values"), key); @@ -2332,8 +2331,8 @@ static int store_aux(const char *key, const char *value, void *cb) store->seen[store->seen_nr] = cf->do_ftell(cf); store->seen_nr++; } - break; - case SECTION_SEEN: + return 0; + } else if (store->is_keys_section) { /* * What we are looking for is in store->key (both * section and var), and its section part is baselen @@ -2348,10 +2347,9 @@ static int store_aux(const char *key, const char *value, void *cb) if ((section_len != store->baselen) || memcmp(key, store->key, section_len+1)) { - store->state = SECTION_END_SEEN; - break; + store->is_keys_section = 0; + return 0; } - /* * Do not increment matches: this is no match, but we * just made sure we are in the desired section. @@ -2359,27 +2357,29 @@ static int store_aux(const char *key, const char *value, void *cb) ALLOC_GROW(store->seen, store->seen_nr + 1, store->seen_alloc); store->seen[store->seen_nr] = cf->do_ftell(cf); - /* fallthru */ - case SECTION_END_SEEN: - case START: - if (matches(key, value, store)) { - ALLOC_GROW(store->seen, store->seen_nr + 1, - store->seen_alloc); - store->seen[store->seen_nr] = cf->do_ftell(cf); - store->state = KEY_SEEN; - store->seen_nr++; - } else { - if (strrchr(key, '.') - key == store->baselen && - !strncmp(key, store->key, store->baselen)) { - store->state = SECTION_SEEN; - ALLOC_GROW(store->seen, - store->seen_nr + 1, - store->seen_alloc); - store->seen[store->seen_nr] = - cf->do_ftell(cf); - } + } + + if (matches(key, value, store)) { + ALLOC_GROW(store->seen, store->seen_nr + 1, + store->seen_alloc); + store->seen[store->seen_nr] = cf->do_ftell(cf); + store->seen_nr++; + store->key_seen = 1; + store->section_seen = 1; + store->is_keys_section = 1; + } else { + if (strrchr(key, '.') - key == store->baselen && + !strncmp(key, store->key, store->baselen)) { + store->section_seen = 1; + store->is_keys_section = 1; + ALLOC_GROW(store->seen, + store->seen_nr + 1, + store->seen_alloc); + store->seen[store->seen_nr] = + cf->do_ftell(cf); } } + return 0; } @@ -2637,7 +2637,6 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, ALLOC_GROW(store.seen, 1, store.seen_alloc); store.seen[0] = 0; - store.state = START; store.seen_nr = 0; /* @@ -2705,7 +2704,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, new_line = 0; if (store.seen[i] == 0) { store.seen[i] = copy_end = contents_sz; - } else if (store.state != KEY_SEEN) { + } else if (!store.key_seen) { copy_end = store.seen[i]; } else copy_end = find_beginning_of_line( @@ -2729,7 +2728,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, /* write the pair (value == NULL means unset) */ if (value != NULL) { - if (store.state == START) { + if (!store.section_seen) { if (write_section(fd, key, &store) < 0) goto write_err_out; } -- cgit v1.3 From 6ae996f2acf3ad780b8d338c81e24143f0b0d304 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:20 +0200 Subject: git_config_set: make use of the config parser's event stream In the recent commit with the title "config: introduce an optional event stream while parsing", we introduced an optional callback to keep track of the config parser's events "comment", "white-space", "section header" and "entry". One motivation for this feature was to make use of it in the code that edits the config. And this commit makes it so. Note: this patch changes the meaning of the `seen` array that records whether we saw the config entry that is to be edited: previously, it contained the end offset of the found entry. Now, we introduce a new array `parsed` that keeps a record of *all* config parser events (with begin/end offsets), and the items in the `seen` array now point into the `parsed` array. There are two reasons why we do it this way: 1. To keep the implementation simple, the config parser's event stream reports the event only after the config callback was called, so we would not receive the begin offset otherwise. 2. In the following patches, we will re-use the `parsed` array to fix two long-standing bugs related to empty sections. Note that this also makes the code more robust with respect to finding the begin offset of the part(s) of the config file to be edited, as we no longer back-track to find the beginning of the line. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 170 ++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 81 insertions(+), 89 deletions(-) diff --git a/config.c b/config.c index 036bae205c..cf94a690ad 100644 --- a/config.c +++ b/config.c @@ -2294,8 +2294,11 @@ struct config_store_data { int do_not_match; regex_t *value_regex; int multi_replace; - size_t *seen; - unsigned int seen_nr, seen_alloc; + struct { + size_t begin, end; + enum config_event_t type; + } *parsed; + unsigned int parsed_nr, parsed_alloc, *seen, seen_nr, seen_alloc; unsigned int key_seen:1, section_seen:1, is_keys_section:1; }; @@ -2313,10 +2316,31 @@ static int matches(const char *key, const char *value, (value && !regexec(store->value_regex, value, 0, NULL, 0)); } +static int store_aux_event(enum config_event_t type, + size_t begin, size_t end, void *data) +{ + struct config_store_data *store = data; + + ALLOC_GROW(store->parsed, store->parsed_nr + 1, store->parsed_alloc); + store->parsed[store->parsed_nr].begin = begin; + store->parsed[store->parsed_nr].end = end; + store->parsed[store->parsed_nr].type = type; + store->parsed_nr++; + + if (type == CONFIG_EVENT_SECTION) { + if (cf->var.len < 2 || cf->var.buf[cf->var.len - 1] != '.') + BUG("Invalid section name '%s'", cf->var.buf); + + /* Is this the section we were looking for? */ + store->is_keys_section = cf->var.len - 1 == store->baselen && + !strncasecmp(cf->var.buf, store->key, store->baselen); + } + + return 0; +} + static int store_aux(const char *key, const char *value, void *cb) { - const char *ep; - size_t section_len; struct config_store_data *store = cb; if (store->key_seen) { @@ -2328,55 +2352,21 @@ static int store_aux(const char *key, const char *value, void *cb) ALLOC_GROW(store->seen, store->seen_nr + 1, store->seen_alloc); - store->seen[store->seen_nr] = cf->do_ftell(cf); + store->seen[store->seen_nr] = store->parsed_nr; store->seen_nr++; } - return 0; } else if (store->is_keys_section) { /* - * What we are looking for is in store->key (both - * section and var), and its section part is baselen - * long. We found key (again, both section and var). - * We would want to know if this key is in the same - * section as what we are looking for. We already - * know we are in the same section as what should - * hold store->key. + * Do not increment matches yet: this may not be a match, but we + * are in the desired section. */ - ep = strrchr(key, '.'); - section_len = ep - key; - - if ((section_len != store->baselen) || - memcmp(key, store->key, section_len+1)) { - store->is_keys_section = 0; - return 0; - } - /* - * Do not increment matches: this is no match, but we - * just made sure we are in the desired section. - */ - ALLOC_GROW(store->seen, store->seen_nr + 1, - store->seen_alloc); - store->seen[store->seen_nr] = cf->do_ftell(cf); - } - - if (matches(key, value, store)) { - ALLOC_GROW(store->seen, store->seen_nr + 1, - store->seen_alloc); - store->seen[store->seen_nr] = cf->do_ftell(cf); - store->seen_nr++; - store->key_seen = 1; + ALLOC_GROW(store->seen, store->seen_nr + 1, store->seen_alloc); + store->seen[store->seen_nr] = store->parsed_nr; store->section_seen = 1; - store->is_keys_section = 1; - } else { - if (strrchr(key, '.') - key == store->baselen && - !strncmp(key, store->key, store->baselen)) { - store->section_seen = 1; - store->is_keys_section = 1; - ALLOC_GROW(store->seen, - store->seen_nr + 1, - store->seen_alloc); - store->seen[store->seen_nr] = - cf->do_ftell(cf); + + if (matches(key, value, store)) { + store->seen_nr++; + store->key_seen = 1; } } @@ -2477,32 +2467,6 @@ static ssize_t write_pair(int fd, const char *key, const char *value, return ret; } -static ssize_t find_beginning_of_line(const char *contents, size_t size, - size_t offset_, int *found_bracket) -{ - size_t equal_offset = size, bracket_offset = size; - ssize_t offset; - -contline: - for (offset = offset_-2; offset > 0 - && contents[offset] != '\n'; offset--) - switch (contents[offset]) { - case '=': equal_offset = offset; break; - case ']': bracket_offset = offset; break; - } - if (offset > 0 && contents[offset-1] == '\\') { - offset_ = offset; - goto contline; - } - if (bracket_offset < equal_offset) { - *found_bracket = 1; - offset = bracket_offset+1; - } else - offset++; - - return offset; -} - int git_config_set_in_file_gently(const char *config_filename, const char *key, const char *value) { @@ -2613,6 +2577,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, struct stat st; size_t copy_begin, copy_end; int i, new_line = 0; + struct config_options opts; if (value_regex == NULL) store.value_regex = NULL; @@ -2635,17 +2600,24 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, } } - ALLOC_GROW(store.seen, 1, store.seen_alloc); - store.seen[0] = 0; - store.seen_nr = 0; + ALLOC_GROW(store.parsed, 1, store.parsed_alloc); + store.parsed[0].end = 0; + + memset(&opts, 0, sizeof(opts)); + opts.event_fn = store_aux_event; + opts.event_fn_data = &store; /* - * After this, store.offset will contain the *end* offset - * of the last match, or remain at 0 if no match was found. + * After this, store.parsed will contain offsets of all the + * parsed elements, and store.seen will contain a list of + * matches, as indices into store.parsed. + * * As a side effect, we make sure to transform only a valid * existing config file. */ - if (git_config_from_file(store_aux, config_filename, &store)) { + if (git_config_from_file_with_options(store_aux, + config_filename, + &store, &opts)) { error("invalid config file %s", config_filename); free(store.key); if (store.value_regex != NULL && @@ -2697,19 +2669,39 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, goto out_free; } - if (store.seen_nr == 0) + if (store.seen_nr == 0) { + if (!store.seen_alloc) { + /* Did not see key nor section */ + ALLOC_GROW(store.seen, 1, store.seen_alloc); + store.seen[0] = store.parsed_nr + - !!store.parsed_nr; + } store.seen_nr = 1; + } for (i = 0, copy_begin = 0; i < store.seen_nr; i++) { + size_t replace_end; + int j = store.seen[i]; + new_line = 0; - if (store.seen[i] == 0) { - store.seen[i] = copy_end = contents_sz; - } else if (!store.key_seen) { - copy_end = store.seen[i]; - } else - copy_end = find_beginning_of_line( - contents, contents_sz, - store.seen[i], &new_line); + if (!store.key_seen) { + replace_end = copy_end = store.parsed[j].end; + } else { + replace_end = store.parsed[j].end; + copy_end = store.parsed[j].begin; + /* + * Swallow preceding white-space on the same + * line. + */ + while (copy_end > 0 ) { + char c = contents[copy_end - 1]; + + if (isspace(c) && c != '\n') + copy_end--; + else + break; + } + } if (copy_end > 0 && contents[copy_end-1] != '\n') new_line = 1; @@ -2723,7 +2715,7 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, write_str_in_full(fd, "\n") < 0) goto write_err_out; } - copy_begin = store.seen[i]; + copy_begin = replace_end; } /* write the pair (value == NULL means unset) */ -- cgit v1.3 From 22aedfccd0cfe7c8f09a990cf4777efc7f27bd2e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:24 +0200 Subject: git config --unset: remove empty sections (in the common case) The original reasoning for not removing section headers upon removal of the last entry went like this: the user could have added comments about the section, or about the entries therein, and if there were other comments there, we would not know whether we should remove them. In particular, a concocted example was presented that looked like this (and was added to t1300): # some generic comment on the configuration file itself # a comment specific to this "section" section. [section] # some intervening lines # that should also be dropped key = value # please be careful when you update the above variable The ideal thing for `git config --unset section.key` in this case would be to leave only the first line behind, because all the other comments are now obsolete. However, this is unfeasible, short of adding a complete Natural Language Processing module to Git, which seems not only a lot of work, but a totally unreasonable feature (for little benefit to most users). Now, the real kicker about this problem is: most users do not edit their config files at all! In their use case, the config looks like this instead: [section] key = value ... and it is totally obvious what should happen if the entry is removed: the entire section should vanish. Let's generalize this observation to this conservative strategy: if we are removing the last entry from a section, and there are no comments inside that section nor surrounding it, then remove the entire section. Otherwise behave as before: leave the now-empty section (including those comments, even ones about the now-deleted entry). We have to be extra careful to handle the case where more than one entry is removed: any subset of them might be the last entries of their respective sections (and if there are no comments in or around that section, the section should be removed, too). Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- t/t1300-config.sh | 4 +-- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/config.c b/config.c index cf94a690ad..69c4188ce8 100644 --- a/config.c +++ b/config.c @@ -2297,6 +2297,7 @@ struct config_store_data { struct { size_t begin, end; enum config_event_t type; + int is_keys_section; } *parsed; unsigned int parsed_nr, parsed_alloc, *seen, seen_nr, seen_alloc; unsigned int key_seen:1, section_seen:1, is_keys_section:1; @@ -2325,17 +2326,20 @@ static int store_aux_event(enum config_event_t type, store->parsed[store->parsed_nr].begin = begin; store->parsed[store->parsed_nr].end = end; store->parsed[store->parsed_nr].type = type; - store->parsed_nr++; if (type == CONFIG_EVENT_SECTION) { if (cf->var.len < 2 || cf->var.buf[cf->var.len - 1] != '.') BUG("Invalid section name '%s'", cf->var.buf); /* Is this the section we were looking for? */ - store->is_keys_section = cf->var.len - 1 == store->baselen && + store->is_keys_section = + store->parsed[store->parsed_nr].is_keys_section = + cf->var.len - 1 == store->baselen && !strncasecmp(cf->var.buf, store->key, store->baselen); } + store->parsed_nr++; + return 0; } @@ -2467,6 +2471,87 @@ static ssize_t write_pair(int fd, const char *key, const char *value, return ret; } +/* + * If we are about to unset the last key(s) in a section, and if there are + * no comments surrounding (or included in) the section, we will want to + * extend begin/end to remove the entire section. + * + * Note: the parameter `seen_ptr` points to the index into the store.seen + * array. * This index may be incremented if a section has more than one + * entry (which all are to be removed). + */ +static void maybe_remove_section(struct config_store_data *store, + const char *contents, + size_t *begin_offset, size_t *end_offset, + int *seen_ptr) +{ + size_t begin; + int i, seen, section_seen = 0; + + /* + * First, ensure that this is the first key, and that there are no + * comments before the entry nor before the section header. + */ + seen = *seen_ptr; + for (i = store->seen[seen]; i > 0; i--) { + enum config_event_t type = store->parsed[i - 1].type; + + if (type == CONFIG_EVENT_COMMENT) + /* There is a comment before this entry or section */ + return; + if (type == CONFIG_EVENT_ENTRY) { + if (!section_seen) + /* This is not the section's first entry. */ + return; + /* We encountered no comment before the section. */ + break; + } + if (type == CONFIG_EVENT_SECTION) { + if (!store->parsed[i - 1].is_keys_section) + break; + section_seen = 1; + } + } + begin = store->parsed[i].begin; + + /* + * Next, make sure that we are removing he last key(s) in the section, + * and that there are no comments that are possibly about the current + * section. + */ + for (i = store->seen[seen] + 1; i < store->parsed_nr; i++) { + enum config_event_t type = store->parsed[i].type; + + if (type == CONFIG_EVENT_COMMENT) + return; + if (type == CONFIG_EVENT_SECTION) { + if (store->parsed[i].is_keys_section) + continue; + break; + } + if (type == CONFIG_EVENT_ENTRY) { + if (++seen < store->seen_nr && + i == store->seen[seen]) + /* We want to remove this entry, too */ + continue; + /* There is another entry in this section. */ + return; + } + } + + /* + * We are really removing the last entry/entries from this section, and + * there are no enclosed or surrounding comments. Remove the entire, + * now-empty section. + */ + *seen_ptr = seen; + *begin_offset = begin; + if (i < store->parsed_nr) + *end_offset = store->parsed[i].begin; + else + *end_offset = store->parsed[store->parsed_nr - 1].end; +} + int git_config_set_in_file_gently(const char *config_filename, const char *key, const char *value) { @@ -2689,6 +2774,10 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, } else { replace_end = store.parsed[j].end; copy_end = store.parsed[j].begin; + if (!value) + maybe_remove_section(&store, contents, + ©_end, + &replace_end, &i); /* * Swallow preceding white-space on the same * line. diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 8a3cd2c114..92aaa53794 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1390,7 +1390,7 @@ test_expect_success 'urlmatch with wildcard' ' ' # good section hygiene -test_expect_failure '--unset last key removes section (except if commented)' ' +test_expect_success '--unset last key removes section (except if commented)' ' cat >.git/config <<-\EOF && # some generic comment on the configuration file itself # a comment specific to this "section" section. @@ -1472,7 +1472,7 @@ test_expect_failure '--unset last key removes section (except if commented)' ' test_line_count = 3 .git/config ' -test_expect_failure '--unset-all removes section if empty & uncommented' ' +test_expect_success '--unset-all removes section if empty & uncommented' ' cat >.git/config <<-\EOF && [section] key = value1 -- cgit v1.3 From c71d8bb38a73abc910a63bf7a81f3869dc9c2f34 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2018 10:32:29 +0200 Subject: git_config_set: reuse empty sections It can happen quite easily that the last setting in a config section is removed, and to avoid confusion when there are comments in the config about that section, we keep a lone section header, i.e. an empty section. Now that we use the `event_fn` callback, it is easy to add support for re-using empty sections, so let's do that. Note: t5512-ls-remote requires that this change is applied *after* the patch "git config --unset: remove empty sections (in the common case)": without that patch, there would be empty `transfer` and `uploadpack` sections ready for reuse, but in the *wrong* order (and sconsequently, t5512's "overrides work between mixed transfer/upload-pack hideRefs" would fail). Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- config.c | 14 +++++++++++++- t/t1300-config.sh | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index 69c4188ce8..d4527beb44 100644 --- a/config.c +++ b/config.c @@ -2336,6 +2336,12 @@ static int store_aux_event(enum config_event_t type, store->parsed[store->parsed_nr].is_keys_section = cf->var.len - 1 == store->baselen && !strncasecmp(cf->var.buf, store->key, store->baselen); + if (store->is_keys_section) { + store->section_seen = 1; + ALLOC_GROW(store->seen, store->seen_nr + 1, + store->seen_alloc); + store->seen[store->seen_nr] = store->parsed_nr; + } } store->parsed_nr++; @@ -2770,7 +2776,13 @@ int git_config_set_multivar_in_file_gently(const char *config_filename, new_line = 0; if (!store.key_seen) { - replace_end = copy_end = store.parsed[j].end; + copy_end = store.parsed[j].end; + /* include '\n' when copying section header */ + if (copy_end > 0 && copy_end < contents_sz && + contents[copy_end - 1] != '\n' && + contents[copy_end] == '\n') + copy_end++; + replace_end = copy_end; } else { replace_end = store.parsed[j].end; copy_end = store.parsed[j].begin; diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 92aaa53794..e43982a9c1 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -1483,7 +1483,7 @@ test_expect_success '--unset-all removes section if empty & uncommented' ' test_line_count = 0 .git/config ' -test_expect_failure 'adding a key into an empty section reuses header' ' +test_expect_success 'adding a key into an empty section reuses header' ' cat >.git/config <<-\EOF && [section] EOF -- cgit v1.3