Skip to content

Commit e584f32

Browse files
committed
Adapted config parsing to match official spec for comments and multiline
1 parent ef1ef4d commit e584f32

1 file changed

Lines changed: 48 additions & 11 deletions

File tree

git/config.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
264264
# A suitable alternative would be the BlockingLockFile
265265
t_lock = LockFile
266266
re_comment = re.compile(r'^\s*[#;]')
267+
comment_chars = '#;'
268+
escape_char = '\\' # single backslash
267269

268270
#} END configuration
269271

@@ -388,6 +390,41 @@ def optionxform(self, optionstr: str) -> str:
388390
"""Do not transform options in any way when writing"""
389391
return optionstr
390392

393+
def _is_value_continued(self, val: str) -> bool:
394+
"""Check if the option value will be continued on the next line.
395+
396+
Note: Git only supports multi-line option values by
397+
ending a line with '\'. Continuing a line with opened
398+
quotes is not a thing in Git configs.
399+
"""
400+
escaped = False
401+
for char in val:
402+
if escaped:
403+
# consume escape and skip char
404+
escaped = False
405+
continue
406+
if char == self.escape_char:
407+
escaped = True
408+
# Returns true is last char was unescaped escape char
409+
return escaped
410+
411+
def _find_comment(self, optval: str) -> int:
412+
"""Find the beginning of a potential comment tailing a option value"""
413+
escaped = False
414+
quote_open = False
415+
for pos, char in enumerate(optval):
416+
if escaped:
417+
# consume escape and skip char
418+
escaped = False
419+
continue
420+
if char == self.escape_char:
421+
escaped = True
422+
if char == '"' and not escaped:
423+
quote_open = not quote_open
424+
if char in self.comment_chars and not quote_open:
425+
return pos
426+
return -1 # not found
427+
391428
def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None:
392429
"""A direct copy of the py2.4 version of the super class's _read method
393430
to assure it uses ordered dicts. Had to change one line to make it work.
@@ -447,20 +484,21 @@ def string_decode(v: str) -> str:
447484
elif not is_multi_line:
448485
mo = self.OPTCRE.match(line)
449486
if mo:
450-
# We might just have handled the last line, which could contain a quotation we want to remove
487+
# Remove end-of-line comments if not inside double quotes
451488
optname, vi, optval = mo.group('option', 'vi', 'value')
452-
if vi in ('=', ':') and ';' in optval and not optval.strip().startswith('"'):
453-
pos = optval.find(';')
454-
if pos != -1 and optval[pos - 1].isspace():
455-
optval = optval[:pos]
489+
if vi in ('=', ':'):
490+
comment_pos = self._find_comment(optval)
491+
if comment_pos != -1:
492+
# removed end-of-line comment
493+
optval = optval[:comment_pos]
456494
optval = optval.strip()
457495
if optval == '""':
458496
optval = ''
459497
# end handle empty string
460498
optname = self.optionxform(optname.rstrip())
461-
if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
499+
if len(optval) > 1 and self._is_value_continued(optval):
462500
is_multi_line = True
463-
optval = string_decode(optval[1:])
501+
optval = string_decode(optval[:-1])
464502
# end handle multi-line
465503
# preserves multiple values for duplicate optnames
466504
cursect.add(optname, optval)
@@ -472,11 +510,10 @@ def string_decode(v: str) -> str:
472510
e.append(lineno, repr(line))
473511
continue
474512
else:
475-
line = line.rstrip()
476-
if line.endswith('"'):
477-
is_multi_line = False
513+
if self._is_value_continued(line):
478514
line = line[:-1]
479-
# end handle quotations
515+
else:
516+
is_multi_line = False
480517
optval = cursect.getlast(optname)
481518
cursect.setlast(optname, optval + string_decode(line))
482519
# END parse section or option

0 commit comments

Comments
 (0)