diff --git a/git/config.py b/git/config.py index f54b4b97e..aa157fcde 100644 --- a/git/config.py +++ b/git/config.py @@ -511,6 +511,24 @@ def is_line_continuation(value: str) -> bool: return False return escaped + def strip_inline_comment(value: str) -> str: + """Cut an unquoted ``#`` or ``;`` comment, as git's ``parse_value`` does. + + 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] + return value + def parse_value(value: str) -> str: parsed: List[str] = [] whitespace: List[str] = [] @@ -575,11 +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 = optval.strip() + optval = strip_inline_comment(optval).strip() if len(optval) < 2 or optval[0] != '"': # Does not open quoting. diff --git a/test/test_config.py b/test/test_config.py index 498b8879f..5f51d6a13 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -239,6 +239,29 @@ 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 --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) + 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