Skip to content

Commit 075a664

Browse files
author
NK
committed
fix: join backslash line continuations when reading config values
git config joins an unquoted value that ends in a backslash with the next line: the backslash and the newline are removed and the next line is appended verbatim, so 'k = line1\' followed by ' line2' reads back as 'line1 line2'. GitPython only implemented multi-line values for quoted strings; for unquoted values the continuation line was silently dropped, truncating whatever was stored in the config. Read the continuation inside _read and join it, chaining across lines that themselves end in a backslash. An even number of trailing backslashes is an escaped one, so the value ends there; a single backslash right at end-of-file is dropped, matching git. Verified against git itself for every case covered by the new tests.
1 parent 62d1e2f commit 075a664

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

git/config.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,26 @@ def string_decode(v: str) -> str:
513513

514514
if len(optval) < 2 or optval[0] != '"':
515515
# Does not open quoting.
516-
pass
516+
# A value ending in an odd number of backslashes
517+
# continues on the next line, exactly as git does: the
518+
# final backslash and the newline are removed and the
519+
# next line is appended verbatim (leading whitespace
520+
# included). An even number means the last backslash
521+
# is escaped and the value ends there.
522+
while True:
523+
trailing = len(optval) - len(optval.rstrip("\\"))
524+
if trailing % 2 == 0:
525+
break
526+
continuation = fp.readline()
527+
if not continuation:
528+
# Backslash at end of file: git drops it.
529+
optval = optval[:-1]
530+
break
531+
lineno = lineno + 1
532+
joined = continuation.decode(defenc)
533+
while joined.endswith("\n") or joined.endswith("\r"):
534+
joined = joined[:-1]
535+
optval = optval[:-1] + joined
517536
elif optval[-1] != '"':
518537
# Opens quoting and does not close: appears to start multi-line quoting.
519538
is_multi_line = True

test/test_config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,26 @@ def test_multi_line_config(self):
143143
)
144144
self.assertEqual(len(config.sections()), 23)
145145

146+
def test_backslash_line_continuation(self):
147+
"""An unquoted value ending in a backslash continues on the next line,
148+
exactly as git config parses it: the final backslash and the newline
149+
are removed and the next line is appended verbatim."""
150+
cases = [
151+
(b"[a]\n\tk = line1\\\n line2\n", "line1 line2"),
152+
(b"[a]\n\tk = one\\\n two\\\n three\n", "one two three"),
153+
(b"[a]\n\tk = val\\\\\n next\n", "val\\\\"),
154+
(b"[a]\n\tk = end\\\n", "end"),
155+
(b"[alias]\n\tco = checkout \\\n\t\t-v\n", "checkout \t\t-v"),
156+
]
157+
for content, expected in cases:
158+
config_file = io.BytesIO(content)
159+
config_file.name = "backslash_continuation.config"
160+
config = GitConfigParser(config_file)
161+
config.read()
162+
section = "alias" if b"[alias]" in content else "a"
163+
key = "co" if section == "alias" else "k"
164+
self.assertEqual(config.get_value(section, key), expected)
165+
146166
def test_config_value_with_trailing_new_line(self):
147167
config_content = b'[section-header]\nkey:"value\n"'
148168
config_file = io.BytesIO(config_content)

0 commit comments

Comments
 (0)