Skip to content

Commit 71af3aa

Browse files
rawsun007claude
andcommitted
fix: strip inline config comments the way git does
A `#` or `;` outside quotes starts a comment in git, with or without a space before it and whether or not the value is quoted. The parser only cut a `;` that was preceded by whitespace in an unquoted value, so `name = Alice # work` read back with the comment attached, and `k = "quoted" # after` was mistaken for an unterminated multi-line quote and returned `quoted" # after`. `parse_value` already implements git's rule, but it only ran when a backslash continuation had been joined. Cut the comment before the quote-structure branches, using the same quote- and escape-aware scan that `is_line_continuation` uses, and let `parse_value` handle every unquoted value rather than only continued ones. That last part corrects one expectation in test_backslash_line_continuation: `k = val\\` ends in an even number of backslashes, so it is an escaped backslash rather than a continuation and the escape resolves. `git config --get a.k` prints `val\`, where the test expected `val\\`. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent cf43820 commit 71af3aa

2 files changed

Lines changed: 47 additions & 10 deletions

File tree

git/config.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,24 @@ def is_line_continuation(value: str) -> bool:
511511
return False
512512
return escaped
513513

514+
def strip_inline_comment(value: str) -> str:
515+
"""Cut an unquoted ``#`` or ``;`` comment, as git's ``parse_value`` does.
516+
517+
Quoting and backslash escapes are honoured, so a ``#`` inside a quoted
518+
value is literal and an unterminated quote swallows the rest of the line.
519+
"""
520+
quoted = escaped = False
521+
for index, char in enumerate(value):
522+
if escaped:
523+
escaped = False
524+
elif char == "\\":
525+
escaped = True
526+
elif char == '"':
527+
quoted = not quoted
528+
elif char in "#;" and not quoted:
529+
return value[:index]
530+
return value
531+
514532
def parse_value(value: str) -> str:
515533
parsed: List[str] = []
516534
whitespace: List[str] = []
@@ -575,11 +593,7 @@ def parse_value(value: str) -> str:
575593
optname, vi, optval = mo.group("option", "vi", "value")
576594
optname = self.optionxform(optname.rstrip())
577595

578-
if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
579-
pos = optval.find(";")
580-
if pos != -1 and optval[pos - 1].isspace():
581-
optval = optval[:pos]
582-
optval = optval.strip()
596+
optval = strip_inline_comment(optval).strip()
583597

584598
if len(optval) < 2 or optval[0] != '"':
585599
# Does not open quoting.
@@ -589,7 +603,6 @@ def parse_value(value: str) -> str:
589603
# next line is appended before the complete value is
590604
# parsed. An even number means the last backslash is
591605
# escaped and the value ends there.
592-
continued = False
593606
while True:
594607
if not is_line_continuation(optval):
595608
break
@@ -603,9 +616,7 @@ def parse_value(value: str) -> str:
603616
while joined.endswith("\n") or joined.endswith("\r"):
604617
joined = joined[:-1]
605618
optval = optval[:-1] + joined
606-
continued = True
607-
if continued:
608-
optval = parse_value(optval)
619+
optval = parse_value(optval)
609620
elif optval[-1] != '"':
610621
# Opens quoting and does not close: appears to start multi-line quoting.
611622
is_multi_line = True

test/test_config.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,29 @@ def test_multi_line_config(self):
239239
)
240240
self.assertEqual(len(config.sections()), 23)
241241

242+
def test_inline_comments_are_stripped_like_git(self):
243+
"""A `#` or `;` outside quotes starts a comment, with or without a space
244+
before it, and whether or not the value is quoted. Expectations are what
245+
`git config -f <file> --get a.k` prints on git 2.50.1."""
246+
cases = [
247+
(b"[a]\n\tk = value # comment\n", "value"),
248+
(b"[a]\n\tk = value ; comment\n", "value"),
249+
(b"[a]\n\tk = value#nospace\n", "value"),
250+
(b"[a]\n\tk = value;nospace\n", "value"),
251+
(b"[a]\n\tk = a # b ; c\n", "a"),
252+
(b'[a]\n\tk = "quoted" # after\n', "quoted"),
253+
# A comment character inside quotes is literal.
254+
(b'[a]\n\tk = "has # inside"\n', "has # inside"),
255+
(b'[a]\n\tk = "has ; inside"\n', "has ; inside"),
256+
]
257+
for content, expected in cases:
258+
config_file = io.BytesIO(content)
259+
config_file.name = "inline_comment.config"
260+
config = GitConfigParser(config_file)
261+
config.read()
262+
with self.subTest(content=content):
263+
self.assertEqual(config.get_value("a", "k"), expected)
264+
242265
def test_backslash_line_continuation(self):
243266
"""An unquoted value ending in a backslash continues on the next line,
244267
exactly as git config parses it: the final backslash and the newline
@@ -250,7 +273,10 @@ def test_backslash_line_continuation(self):
250273
(b"[a]\n\tk = one\\\n two ; ignored\n", "one two"),
251274
(b'[a]\n\tk = one\\\n "two"\n', "one two"),
252275
(b"[a]\n\tk = one\\\n two\\tthree\n", "one two\tthree"),
253-
(b"[a]\n\tk = val\\\\\n next\n", "val\\\\"),
276+
# An even number of trailing backslashes is an escaped backslash, not a
277+
# continuation, so the value ends here and the escape resolves:
278+
# `git config --get a.k` prints `val\\`.
279+
(b"[a]\n\tk = val\\\\\n next\n", "val\\"),
254280
(b"[a]\n\tk = end\\\n", "end"),
255281
(b"[alias]\n\tco = checkout \\\n\t\t-v\n", "checkout \t\t-v"),
256282
]

0 commit comments

Comments
 (0)