Skip to content

Commit a15f791

Browse files
Byroncodex
andcommitted
fix: parse joined config values as a whole
Git removes each backslash-newline pair before parsing the resulting logical value. Appending physical lines after processing only the first line left trailing whitespace, comments, quotes, and recognized escapes literal. That divergence returned values unlike git config and could preserve comment text as configuration data. - Accumulate continuation text before parsing it. - Apply whitespace, comment, quote, and escape handling once to the complete value. - Cover each affected syntax form with Git-compatible expectations. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
1 parent 60dd946 commit a15f791

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

git/config.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,32 @@ def is_line_continuation(value: str) -> bool:
480480
return False
481481
return escaped
482482

483+
def parse_value(value: str) -> str:
484+
parsed: List[str] = []
485+
whitespace: List[str] = []
486+
quoted = escaped = False
487+
escapes = {"b": "\b", "n": "\n", "t": "\t", '"': '"', "\\": "\\"}
488+
for char in value:
489+
if escaped:
490+
parsed.append(escapes.get(char, "\\" + char))
491+
escaped = False
492+
continue
493+
if char.isspace() and not quoted:
494+
if parsed:
495+
whitespace.append(char)
496+
continue
497+
if char in "#;" and not quoted:
498+
break
499+
parsed.extend(whitespace)
500+
whitespace.clear()
501+
if char == "\\":
502+
escaped = True
503+
elif char == '"':
504+
quoted = not quoted
505+
else:
506+
parsed.append(char)
507+
return "".join(parsed)
508+
483509
while True:
484510
# We assume to read binary!
485511
line = fp.readline().decode(defenc)
@@ -529,9 +555,10 @@ def is_line_continuation(value: str) -> bool:
529555
# A value ending in an odd number of backslashes
530556
# continues on the next line, exactly as git does: the
531557
# final backslash and the newline are removed and the
532-
# next line is appended verbatim (leading whitespace
533-
# included). An even number means the last backslash
534-
# is escaped and the value ends there.
558+
# next line is appended before the complete value is
559+
# parsed. An even number means the last backslash is
560+
# escaped and the value ends there.
561+
continued = False
535562
while True:
536563
if not is_line_continuation(optval):
537564
break
@@ -545,6 +572,9 @@ def is_line_continuation(value: str) -> bool:
545572
while joined.endswith("\n") or joined.endswith("\r"):
546573
joined = joined[:-1]
547574
optval = optval[:-1] + joined
575+
continued = True
576+
if continued:
577+
optval = parse_value(optval)
548578
elif optval[-1] != '"':
549579
# Opens quoting and does not close: appears to start multi-line quoting.
550580
is_multi_line = True

test/test_config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,14 @@ def test_multi_line_config(self):
146146
def test_backslash_line_continuation(self):
147147
"""An unquoted value ending in a backslash continues on the next line,
148148
exactly as git config parses it: the final backslash and the newline
149-
are removed and the next line is appended verbatim."""
149+
are removed before the complete logical value is parsed."""
150150
cases = [
151151
(b"[a]\n\tk = line1\\\n line2\n", "line1 line2"),
152152
(b"[a]\n\tk = one\\\n two\\\n three\n", "one two three"),
153+
(b"[a]\n\tk = one\\\n two \n", "one two"),
154+
(b"[a]\n\tk = one\\\n two ; ignored\n", "one two"),
155+
(b'[a]\n\tk = one\\\n "two"\n', "one two"),
156+
(b"[a]\n\tk = one\\\n two\\tthree\n", "one two\tthree"),
153157
(b"[a]\n\tk = val\\\\\n next\n", "val\\\\"),
154158
(b"[a]\n\tk = end\\\n", "end"),
155159
(b"[alias]\n\tco = checkout \\\n\t\t-v\n", "checkout \t\t-v"),
@@ -170,6 +174,7 @@ def test_comment_backslash_does_not_continue_value(self, rw_dir):
170174
config_file.write(b"[a]\n\tk = one\\\n two ; ignored \\\n\tx = two\n")
171175

172176
with GitConfigParser(config_path, read_only=False) as config:
177+
self.assertEqual(config.get_value("a", "k"), "one two")
173178
self.assertEqual(config.get_value("a", "x"), "two")
174179
config.set_value("a", "added", "three")
175180

0 commit comments

Comments
 (0)