@@ -365,7 +365,7 @@ def test_backslash_line_continuation(self):
365365 (b"[a]\n \t k = one\\ \n two ; ignored\n " , "one two" ),
366366 (b'[a]\n \t k = one\\ \n "two"\n ' , "one two" ),
367367 (b"[a]\n \t k = one\\ \n two\\ tthree\n " , "one two\t three" ),
368- (b"[a]\n \t k = val\\ \\ \n next\n " , "val\\ \\ " ),
368+ (b"[a]\n \t k = val\\ \\ \n next\n " , "val\\ " ),
369369 (b"[a]\n \t k = end\\ \n " , "end" ),
370370 (b"[alias]\n \t co = checkout \\ \n \t \t -v\n " , "checkout \t \t -v" ),
371371 ]
@@ -378,6 +378,33 @@ def test_backslash_line_continuation(self):
378378 key = "co" if section == "alias" else "k"
379379 self .assertEqual (config .get_value (section , key ), expected )
380380
381+ @with_rw_directory
382+ def test_continued_values_preserve_comments_and_eof_whitespace (self , rw_dir ):
383+ cases = [
384+ (b"one\\ \n two # C:\\ Users\n " , "one two" ),
385+ (b"one\\ \n two ; C:\\ Users\n " , "one two" ),
386+ (b"one\\ \n two\\ tthree # C:\\ Users\n " , "one two\t three" ),
387+ (b'one\\ \n "two # three" # C:\\ Users\n ' , "one two # three" ),
388+ (b"value \\ " , "value " ),
389+ (b"value \\ \n " , "value " ),
390+ (b"value \t \\ \n " , "value \t " ),
391+ (b"one\\ \n two \\ \n " , "one two " ),
392+ (b"C:\\ Users \\ \n " , "C:\\ Users " ),
393+ ]
394+ config_path = osp .join (rw_dir , "config" )
395+ for value , expected in cases :
396+ with self .subTest (value = value ):
397+ with open (config_path , "wb" ) as config_file :
398+ config_file .write (b"[a]\n \t k = " + value )
399+
400+ with GitConfigParser (config_path , read_only = False ) as config :
401+ self .assertEqual (config .get_value ("a" , "k" ), expected )
402+ config .set_value ("other" , "value" , "updated" )
403+
404+ with GitConfigParser (config_path ) as config :
405+ self .assertEqual (config .get_value ("a" , "k" ), expected )
406+ self .assertEqual (config .get_value ("other" , "value" ), "updated" )
407+
381408 def test_get_value_reads_git_boolean_spellings (self ):
382409 # git accepts yes/no and on/off as well as true/false, and getboolean on
383410 # this class already did. get_value returned them as strings, so "no" and
@@ -1209,6 +1236,30 @@ def test_config_with_quotes_containing_escapes(self):
12091236 # Cases without quotes should still parse correctly even after those, too.
12101237 self .assertEqual (cr .get ("custom" , "unquoted" ), "good evening" )
12111238
1239+ def test_config_with_unquoted_escapes (self ):
1240+ """Decode git's escape sequences in unquoted values, but only when every
1241+ backslash in the value is part of a known escape."""
1242+ cr = GitConfigParser (fixture_path ("git_config_unquoted_escapes" ), read_only = True )
1243+
1244+ self .assertEqual (cr .get ("custom" , "hasnewline" ), "first\n second" )
1245+ self .assertEqual (cr .get ("custom" , "hastab" ), "first\t second" )
1246+ self .assertEqual (cr .get ("custom" , "hasbackslash" ), R"foo\bar" )
1247+ self .assertEqual (cr .get ("custom" , "hasquote" ), 'ab"cd' )
1248+ self .assertEqual (cr .get ("custom" , "hastrailingbackslash" ), "word\\ " )
1249+
1250+ # Git rejects an unknown escape rather than decoding it. GitPython keeps
1251+ # such a value literal instead of rejecting it, which is what keeps a
1252+ # Windows path usable, so no backslash in one may be decoded: doing so
1253+ # would turn the \t of \Temp\test into a tab while leaving \U alone.
1254+ self .assertEqual (cr .get ("custom" , "hasunknownescape" ), R"foo\qbar" )
1255+ self .assertEqual (cr .get ("custom" , "haswindowspath" ), R"C:\Users\RUNNER~1\AppData\Local\Temp\test_foo" )
1256+
1257+ # Values without escapes keep parsing as before.
1258+ self .assertEqual (cr .get ("custom" , "ordinary" ), "hello world" )
1259+ self .assertEqual (cr .get ("custom" , "withcolon" ), "host:port" )
1260+ self .assertEqual (cr .get ("custom" , "withequals" ), "key=value" )
1261+ self .assertEqual (cr .get ("custom" , "withhash" ), "value" )
1262+
12121263 def test_get_values_works_without_requiring_any_other_calls_first (self ):
12131264 file_obj = self ._to_memcache (fixture_path ("git_config_multiple" ))
12141265 cr = GitConfigParser (file_obj , read_only = True )
0 commit comments