Repository: gitpython-developers/GitPython

Issue:
GitConfigParser misparses quotes in options
My `~/.gitconfig` starts like this:

```
[user]
        name = "Colin Watson"
        email = "cjwatson@debian.org"
```

It's been like that for a long time, so I don't remember whether an old version of `git config` wrote it like that, or whether I put it there by hand.  Anyway, `git` itself has always been fine with this.  The relevant part of git-config(1) says:

> A line that defines a value can be continued to the next line by ending it with a \; the backslash and the end-of-line are stripped. Leading whitespaces after name =, the remainder of the line after the first comment character # or ;, and trailing whitespaces of the line are discarded unless they are enclosed in double quotes. Internal whitespaces within the value are retained verbatim.
> 
> Inside double quotes, double quote " and backslash \ characters must be escaped: use \" for " and \\ for \.
> 
> The following escape sequences (beside \" and \\) are recognized: \n for newline character (NL), \t for horizontal tabulation (HT, TAB) and \b for backspace (BS). Other char escape sequences (including octal escape sequences) are invalid.

But when I create a commit using `IndexFile.commit()`, I get this in the commit metadata, which I'm pretty sure is malformed:

`Author: "Colin Watson" <"cjwatson@debian.org">`

I think `GitConfigParser` should match the parsing rules that `git config` follows.


Discussion:
Thanks a lot for reporting!

The config parser here is based on the INI config parser, which makes it very much non-conforming to the Git specification.
Fixes on top of that can certainly be done though, hoping they won't introduce more problems than they solve.


This is a problem as GitHub won't recognize the quoted version, so commits aren't liked to the authors profile.
i just stumbled upon the same problem and tried my hand at a fix in #2035.



Patch:
diff --git a/git/config.py b/git/config.py
index de3508360..2df99a753 100644
--- a/git/config.py
+++ b/git/config.py
@@ -508,6 +508,8 @@ def string_decode(v: str) -> str:
                     if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
                         is_multi_line = True
                         optval = string_decode(optval[1:])
+                    elif len(optval) > 1 and optval[0] == '"' and optval[-1] == '"':
+                        optval = optval[1:-1].strip()
                     # END handle multi-line
                     # Preserves multiple values for duplicate optnames.
                     cursect.add(optname, optval)


Test patch:
diff --git a/test/fixtures/git_config_with_quotes b/test/fixtures/git_config_with_quotes
new file mode 100644
index 000000000..40e6710d9
--- /dev/null
+++ b/test/fixtures/git_config_with_quotes
@@ -0,0 +1,3 @@
+[user]
+  name = "Cody Veal"
+  email = "cveal05@gmail.com"
diff --git a/test/test_config.py b/test/test_config.py
index 92997422d..886d5b136 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -391,7 +391,7 @@ def test_complex_aliases(self):
         with GitConfigParser(file_obj, read_only=False) as w_config:
             self.assertEqual(
                 w_config.get("alias", "rbi"),
-                '"!g() { git rebase -i origin/${1:-master} ; } ; g"',
+                "!g() { git rebase -i origin/${1:-master} ; } ; g",
             )
         self.assertEqual(
             file_obj.getvalue(),
@@ -406,6 +406,12 @@ def test_empty_config_value(self):
         with self.assertRaises(cp.NoOptionError):
             cr.get_value("color", "ui")
 
+    def test_config_with_quotes(self):
+        cr = GitConfigParser(fixture_path("git_config_with_quotes"), read_only=True)
+
+        self.assertEqual(cr.get("user", "name"), "Cody Veal")
+        self.assertEqual(cr.get("user", "email"), "cveal05@gmail.com")
+
     def test_get_values_works_without_requiring_any_other_calls_first(self):
         file_obj = self._to_memcache(fixture_path("git_config_multiple"))
         cr = GitConfigParser(file_obj, read_only=True)