Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions git/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,24 @@ def is_line_continuation(value: str) -> bool:
return False
return escaped

def strip_inline_comment(value: str) -> Tuple[str, bool]:
"""Cut an unquoted ``#`` or ``;`` comment and report whether a quote is open.

Quoting and backslash escapes are honoured, so a ``#`` inside a quoted
value is literal and an unterminated quote swallows the rest of the line.
"""
quoted = escaped = False
for index, char in enumerate(value):
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
quoted = not quoted
elif char in "#;" and not quoted:
return value[:index], False
return value, quoted

def parse_value(value: str) -> str:
parsed: List[str] = []
whitespace: List[str] = []
Expand Down Expand Up @@ -575,10 +593,7 @@ def parse_value(value: str) -> str:
optname, vi, optval = mo.group("option", "vi", "value")
optname = self.optionxform(optname.rstrip())

if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'):
pos = optval.find(";")
if pos != -1 and optval[pos - 1].isspace():
optval = optval[:pos]
optval, quote_open = strip_inline_comment(optval)
optval = optval.strip()

if len(optval) < 2 or optval[0] != '"':
Expand Down Expand Up @@ -606,12 +621,12 @@ def parse_value(value: str) -> str:
continued = True
if continued:
optval = parse_value(optval)
elif optval[-1] != '"':
elif quote_open:
# Opens quoting and does not close: appears to start multi-line quoting.
is_multi_line = True
optval = string_decode(optval[1:])
elif re.search(r'(?:^|[^\\])(?:\\\\)*"', optval[1:-1]):
# Preserve malformed values containing unescaped quotes.
# Preserve values containing additional unescaped quotes.
pass
else:
# Opens and closes quoting.
Expand Down
45 changes: 45 additions & 0 deletions test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,51 @@ def test_multi_line_config(self):
)
self.assertEqual(len(config.sections()), 23)

def test_inline_comments_are_stripped_like_git(self):
"""A `#` or `;` outside quotes starts a comment, with or without a space
before it, and whether or not the value is quoted. Expectations are what
`git config -f <file> --get a.k` prints on git 2.50.1."""
cases = [
(b"[a]\n\tk = value # comment\n", "value"),
(b"[a]\n\tk = value ; comment\n", "value"),
(b"[a]\n\tk = value#nospace\n", "value"),
(b"[a]\n\tk = value;nospace\n", "value"),
(b"[a]\n\tk = a # b ; c\n", "a"),
(b'[a]\n\tk = "quoted" # after\n', "quoted"),
# A comment character inside quotes is literal.
(b'[a]\n\tk = "has # inside"\n', "has # inside"),
(b'[a]\n\tk = "has ; inside"\n', "has ; inside"),
]
for content, expected in cases:
config_file = io.BytesIO(content)
config_file.name = "inline_comment.config"
config = GitConfigParser(config_file)
config.read()
with self.subTest(content=content):
self.assertEqual(config.get_value("a", "k"), expected)

@with_rw_directory
def test_inline_comments_preserve_balanced_quotes_and_following_settings(self, rw_dir):
config_path = osp.join(rw_dir, "config")
values = (b'"foo"bar', b'"foo\\"bar"baz', b'"foo#;bar"baz')
for value in values:
for comment in (b' # "note"', b' ; "note"'):
with self.subTest(value=value, comment=comment):
with open(config_path, "wb") as config_file:
config_file.write(b"[a]\n\tk = " + value + comment + b"\n\tx = keep\n[b]\n\ty = stay\n")

with GitConfigParser(config_path, read_only=False) as config:
self.assertEqual(config.get_value("a", "k"), value.decode(defenc))
self.assertEqual(config.get_value("a", "x"), "keep")
self.assertEqual(config.get_value("b", "y"), "stay")
config.set_value("other", "value", "updated")

with GitConfigParser(config_path) as config:
self.assertEqual(config.get_value("a", "k"), value.decode(defenc))
self.assertEqual(config.get_value("a", "x"), "keep")
self.assertEqual(config.get_value("b", "y"), "stay")
self.assertEqual(config.get_value("other", "value"), "updated")

def test_backslash_line_continuation(self):
"""An unquoted value ending in a backslash continues on the next line,
exactly as git config parses it: the final backslash and the newline
Expand Down
Loading