Skip to content

Commit ae03abd

Browse files
committed
test(config): port home include and inline header regressions
Cover the configuration regressions from PRs #2305 and #2307 against actual `git config` output. The current configuration facade already delegates these operations to Git, so the valid cases pass without a runtime change. Use scripted fixtures for a real home-relative conditional include and the complete inline-header sample. Preserve native `HOME` and `USERPROFILE` separators so the same test exercises the Windows path case on Windows. Check both matching and nonmatching repositories and file/stream readers. Also check adjacent headers, an escaped quote before `]`, and rejection of unbalanced quotes, missing/stray brackets, malformed keys and sections. Git accepts multiple section headers on one line but omits empty sections from `git config list`. Reflect that output and the facade's documented empty-section limitation instead of restoring a separate Python parser. Document the existing `GIX-12` fallback without claiming native coverage. Validation on macOS, Git 2.54.0, Python 3.12 and GixPython 0.2.0: - All 11 added cases passed against the unchanged runtime. - `test/test_config.py`: 62 passed with each of the CLI and Gix backends. - `ruff check`, `ruff format --check`, `bash -n` and `git diff --check` passed. Windows and Linux execution was not available for this local check.
1 parent b4bd52d commit ae03abd

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,19 @@ former Python parser's repair of that input. General config files and
10901090
mutations still use the CLI on both backends under GIX-12; these observations
10911091
do not demonstrate a native Gix mismatch or eliminate any CLI calls.
10921092

1093+
The October 24 ports of PRs #2305 and #2307 also compare home-relative
1094+
`includeIf` conditions and inline section headers with Git 2.54.0. The
1095+
conditional-include fixture uses a real repository and platform-native
1096+
`HOME`/`USERPROFILE` paths, retaining the Windows regression case; the local
1097+
run covered macOS only. Git accepts options after headers, quoted `]` in
1098+
subsections, and multiple headers on one line. It rejects unbalanced quoted
1099+
headers, missing closing brackets, stray brackets, and malformed options or
1100+
sections. `git config list` omits empty sections, so the facade's enumeration
1101+
also omits the original PR's empty `e` and `f` sections; their inspection is
1102+
still unsupported. These regressions pass through the existing GIX-12 CLI
1103+
fallback on both backends with GixPython 0.2.0. They establish no native
1104+
compatibility bug or reduction in CLI calls.
1105+
10931106
## Other remaining operations
10941107

10951108
| Operation family | Why it still uses Git |

‎test/test_config.py‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,83 @@ def test_config_includes_and_repository_conditions():
8484
]
8585

8686

87+
@pytest.mark.parametrize("directory, matches", [("target", True), ("other", False)])
88+
def test_conditional_includes_expand_home_in_git_dir_pattern(monkeypatch, directory, matches):
89+
path = read_only_fixture("config.sh", "home-includes")
90+
home = path / "home"
91+
# Native Windows separators must reach Git's home expansion unchanged.
92+
monkeypatch.setenv("HOME", str(home))
93+
monkeypatch.setenv("USERPROFILE", str(home))
94+
with Repo(home / directory / "repo") as repo:
95+
expected = b"includeif.gitdir:~/target/.path\n./included\0"
96+
if matches:
97+
expected += b"values.source\nincluded\0"
98+
assert (
99+
repo.git.config("list", "--null", "--includes", "--file", str(path / "config"), stdout_as_string=False)
100+
== expected
101+
)
102+
with GitConfigParser(path / "config", repo=repo) as config:
103+
assert config.get('includeIf "gitdir:~/target/"', "path") == "./included"
104+
assert config.has_section("values") is matches
105+
if matches:
106+
assert config.get_value("values", "source") == "included"
107+
with GitConfigParser(path / "config", repo=repo, merge_includes=False) as config:
108+
assert config.sections() == ['includeif "gitdir:~/target/"']
109+
110+
111+
@pytest.mark.parametrize("stream", [False, True])
112+
def test_option_after_section_header_is_read_like_git(stream):
113+
path = read_only_fixture("config.sh", "inline-headers") / "config"
114+
assert Git().config("list", "--null", "--file", str(path), stdout_as_string=False) == (
115+
b"submodule.lib.path\nlib\0"
116+
b"submodule.lib.url\nhttps://good.example/lib\0"
117+
b"submodule.lib.url\nhttps://evil.example/lib\0"
118+
b"a.x]y.k\nv\0b.flag\0c.k\nv ]\0d.k\nmulti line\0g.k\nv\0"
119+
)
120+
source = BytesIO(path.read_bytes()) if stream else path
121+
with GitConfigParser(source) as config:
122+
assert config.get_value('submodule "lib"', "url") == "https://evil.example/lib"
123+
assert config.get_values('submodule "lib"', "url") == ["https://good.example/lib", "https://evil.example/lib"]
124+
assert config.get_value('a "x]y"', "k") == "v"
125+
assert config.getboolean("b", "flag") is True
126+
assert config.get_value("c", "k") == "v ]"
127+
assert config.get_value("d", "k") == "multi line"
128+
assert config.get_value("g", "k") == "v"
129+
# Git's enumeration omits empty sections; the facade cannot expose them.
130+
for section in ("e", "f"):
131+
with pytest.raises(configparser.NoSectionError):
132+
config.items(section)
133+
assert config.sections() == ['submodule "lib"', 'a "x]y"', "b", "c", "d", "g"]
134+
135+
136+
@pytest.mark.parametrize(
137+
"case, section, key",
138+
[
139+
("multiple", "third", "third.k"),
140+
("escaped-quote", 'a "x\\"]y"', 'a.x"]y.k'),
141+
("unbalanced-quote", None, None),
142+
("missing-bracket", None, None),
143+
("trailing-bracket", None, None),
144+
("invalid-option", None, None),
145+
("invalid-section", None, None),
146+
],
147+
)
148+
def test_inline_section_headers_match_git_validation(case, section, key):
149+
path = read_only_fixture("config.sh", "inline-header-cases", case) / "config"
150+
if section is None:
151+
with pytest.raises(GitCommandError, match="bad config line") as baseline:
152+
Git().config("list", "--file", str(path))
153+
with GitConfigParser(path) as config, pytest.raises(configparser.ParsingError) as error:
154+
config.read()
155+
assert isinstance(error.value.__cause__, GitCommandError)
156+
assert error.value.__cause__.status == baseline.value.status
157+
return
158+
assert Git().config("list", "--null", "--file", str(path)) == key + "\nv\0"
159+
with GitConfigParser(path) as config:
160+
assert config.sections() == [section]
161+
assert config.get_value(section, "k") == "v"
162+
163+
87164
def test_config_multiple_files_and_missing_values():
88165
tmp_path = read_only_fixture("config.sh", "multiple")
89166
one, two = tmp_path / "one", tmp_path / "two"

‎tests/fixtures/config.sh‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,39 @@ includes)
5252
gitdir=$(git -C repo rev-parse --absolute-git-dir)
5353
git config --file config "includeIf.gitdir:$gitdir.path" "$PWD/included"
5454
;;
55+
home-includes)
56+
git init -q home/target/repo
57+
git init -q home/other/repo
58+
printf '[includeIf "gitdir:~/target/"]\npath = ./included\n' > config
59+
printf '[values]\nsource = included\n' > included
60+
;;
61+
inline-headers)
62+
cat > config <<'EOF'
63+
[submodule "lib"]
64+
path = lib
65+
url = https://good.example/lib
66+
[submodule "lib"] url = https://evil.example/lib
67+
[a "x]y"] k = v # comment
68+
[b]flag
69+
[c] k = v ]
70+
[d] k = "multi\
71+
line"
72+
[e] ; comment
73+
[f] [g] k = v
74+
EOF
75+
;;
76+
inline-header-cases)
77+
case "$2" in
78+
multiple) printf '[first] [second] [third] k = v\n' > config ;;
79+
escaped-quote) printf '%s\n' '[a "x\"]y"] k = v' > config ;;
80+
unbalanced-quote) printf '%s\n' '[a "unterminated] k = v' > config ;;
81+
missing-bracket) printf '[a k = v\n' > config ;;
82+
trailing-bracket) printf '[a] ]\n' > config ;;
83+
invalid-option) printf '[a] invalid_key = v\n' > config ;;
84+
invalid-section) printf '[a] [invalid section] k = v\n' > config ;;
85+
*) exit 1 ;;
86+
esac
87+
;;
5588
multiple)
5689
printf '[values]\nnumber = 1\n' > one
5790
printf '[values]\nnumber = 2\n' > two

0 commit comments

Comments
 (0)