@@ -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