Skip to content

Commit 847e1c5

Browse files
authored
Merge pull request #2245 from rawsun007/inline-comment-stripping
fix: strip inline config comments the way git does
2 parents cf43820 + 258a884 commit 847e1c5

2 files changed

Lines changed: 66 additions & 6 deletions

File tree

git/config.py

Lines changed: 21 additions & 6 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) -> Tuple[str, bool]:
515+
"""Cut an unquoted ``#`` or ``;`` comment and report whether a quote is open.
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], False
530+
return value, quoted
531+
514532
def parse_value(value: str) -> str:
515533
parsed: List[str] = []
516534
whitespace: List[str] = []
@@ -575,10 +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]
596+
optval, quote_open = strip_inline_comment(optval)
582597
optval = optval.strip()
583598

584599
if len(optval) < 2 or optval[0] != '"':
@@ -606,12 +621,12 @@ def parse_value(value: str) -> str:
606621
continued = True
607622
if continued:
608623
optval = parse_value(optval)
609-
elif optval[-1] != '"':
624+
elif quote_open:
610625
# Opens quoting and does not close: appears to start multi-line quoting.
611626
is_multi_line = True
612627
optval = string_decode(optval[1:])
613628
elif re.search(r'(?:^|[^\\])(?:\\\\)*"', optval[1:-1]):
614-
# Preserve malformed values containing unescaped quotes.
629+
# Preserve values containing additional unescaped quotes.
615630
pass
616631
else:
617632
# Opens and closes quoting.

test/test_config.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,51 @@ 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+
265+
@with_rw_directory
266+
def test_inline_comments_preserve_balanced_quotes_and_following_settings(self, rw_dir):
267+
config_path = osp.join(rw_dir, "config")
268+
values = (b'"foo"bar', b'"foo\\"bar"baz', b'"foo#;bar"baz')
269+
for value in values:
270+
for comment in (b' # "note"', b' ; "note"'):
271+
with self.subTest(value=value, comment=comment):
272+
with open(config_path, "wb") as config_file:
273+
config_file.write(b"[a]\n\tk = " + value + comment + b"\n\tx = keep\n[b]\n\ty = stay\n")
274+
275+
with GitConfigParser(config_path, read_only=False) as config:
276+
self.assertEqual(config.get_value("a", "k"), value.decode(defenc))
277+
self.assertEqual(config.get_value("a", "x"), "keep")
278+
self.assertEqual(config.get_value("b", "y"), "stay")
279+
config.set_value("other", "value", "updated")
280+
281+
with GitConfigParser(config_path) as config:
282+
self.assertEqual(config.get_value("a", "k"), value.decode(defenc))
283+
self.assertEqual(config.get_value("a", "x"), "keep")
284+
self.assertEqual(config.get_value("b", "y"), "stay")
285+
self.assertEqual(config.get_value("other", "value"), "updated")
286+
242287
def test_backslash_line_continuation(self):
243288
"""An unquoted value ending in a backslash continues on the next line,
244289
exactly as git config parses it: the final backslash and the newline

0 commit comments

Comments
 (0)