Repository: python-versioneer/python-versioneer

Issue:
Commits signed with GPG lead to JSONDecodeError
I discovered this bug in [this commit](https://gitlab.com/carmenbianca/schaakmat/tree/7a99f12819e9b4734a8fa48e726cd501cc96f8e8) on my project.

To reproduce in any other project:

- `git commit --alow-empty --gpg-sign=$YOUR_KEYID -m "empty signed commit"`
- `python setup.py sdist`
- `cd dist`
- `pip install $YOUR_TAR_FILE`
- Then open Python and import your project, you will see:
  ```
  >>> import schaakmat
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "/home/carmen/Projects/schaakmat/dist/schaakmat-0.1.0+206.g7a99f12/src/schaakmat/__init__.py", line 53, in <module>
      __version__ = get_versions()['version']
    File "/home/carmen/Projects/schaakmat/dist/schaakmat-0.1.0+206.g7a99f12/src/schaakmat/_version.py", line 21, in get_versions
      return json.loads(version_json)
    File "/usr/lib/python3.5/json/__init__.py", line 319, in loads
      return _default_decoder.decode(s)
    File "/usr/lib/python3.5/json/decoder.py", line 339, in decode
      obj, end = self.raw_decode(s, idx=_w(s, 0).end())
    File "/usr/lib/python3.5/json/decoder.py", line 355, in raw_decode
      obj, end = self.scan_once(s, idx)
  json.decoder.JSONDecodeError: Invalid control character at: line 3 column 59 (char 61)
  ```

The output in the _version.py file:

    # This file was generated by 'versioneer.py' (0.18) from
    # revision-control system data, or from the parent directory name of an
    # unpacked source archive. Distribution tarballs contain a pre-generated copy
    # of this file.

    import json

    version_json = '''
    {
    "date": "gpg:TSignaturemade Sat 03 Jun 2017 12:11:07 CEST\ngpg:                using RSA key 68ECFDA761AB8763CB579CFA76EBA6F6B6EA2EC9\ngpg: Good signature from \"Carmen Bianca Bakker <carmen@carmenbianca.eu>\" [ultimate]\ngpg:                 aka \"Carmen Bianca Bakker <c.b.bakker@carmenbianca.eu>\" [ultimate]\ngpg:                 aka \"Carmen Bianca Bakker <bakk1302@student.nhl.nl>\" [ultimate]\nPrimary key fingerprint: 2A09 F627 39F6 DEC8 CFFC  A216 CD0A 90F1 C5CA 0C92\n     Subkey fingerprint: 68EC FDA7 61AB 8763 CB57  9CFA 76EB A6F6 B6EA 2EC9\n2017-06-03 12:09:47 +0200",
    "dirty": false,
    "error": null,
    "full-revisionid": "7a99f12819e9b4734a8fa48e726cd501cc96f8e8",
    "version": "0.1.0+206.g7a99f12"
    }
    '''  # END VERSION_JSON


    def get_versions():
        return json.loads(version_json)

The 'date' name has a broken value.

I'll tinker a little bit to see if I can fix this and submit a merge request. No promises, though.

Discussion:

Patch:
diff --git a/src/git/from_keywords.py b/src/git/from_keywords.py
index ea7c040..5a46935 100644
--- a/src/git/from_keywords.py
+++ b/src/git/from_keywords.py
@@ -40,6 +40,10 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
         raise NotThisMethod("no keywords at all, weird")
     date = keywords.get("date")
     if date is not None:
+        # Use only the last line.  Previous lines may contain GPG signature
+        # information.
+        date = date.splitlines()[-1]
+
         # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
         # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
         # -like" string, which we must then edit to make compliant), because
diff --git a/src/git/from_vcs.py b/src/git/from_vcs.py
index c7db10d..e007aa4 100644
--- a/src/git/from_vcs.py
+++ b/src/git/from_vcs.py
@@ -100,6 +100,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
     # commit date: see ISO-8601 comment in git_versions_from_keywords()
     date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
                        cwd=root)[0].strip()
+    # Use only the last line.  Previous lines may contain GPG signature
+    # information.
+    date = date.splitlines()[-1]
     pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
 
     return pieces


Test patch:
diff --git a/test/git/test_git.py b/test/git/test_git.py
index e5d27c1..cd3df5f 100644
--- a/test/git/test_git.py
+++ b/test/git/test_git.py
@@ -35,6 +35,8 @@ class ParseGitDescribe(unittest.TestCase):
                 if args[0] == "rev-list":
                     return "42\n", 0
                 if args[0] == "show":
+                    if do_error == "show":
+                        return "gpg: signature\n12345\n", 0
                     return "12345\n", 0
                 self.fail("git called in weird way: %s" % (args,))
             return from_vcs.git_pieces_from_vcs(
@@ -50,6 +52,12 @@ class ParseGitDescribe(unittest.TestCase):
                           "long": "longlong",
                           "short": "longlon",
                           "date": "12345"})
+        self.assertEqual(pv("1f", do_error="show"),
+                         {"closest-tag": None, "dirty": False, "error": None,
+                          "distance": 42,
+                          "long": "longlong",
+                          "short": "longlon",
+                          "date": "12345"})
         self.assertEqual(pv("1f-dirty"),
                          {"closest-tag": None, "dirty": True, "error": None,
                           "distance": 42,
@@ -87,9 +95,9 @@ class ParseGitDescribe(unittest.TestCase):
 
 
 class Keywords(unittest.TestCase):
-    def parse(self, refnames, full, prefix=""):
+    def parse(self, refnames, full, prefix="", date=None):
         return from_keywords.git_versions_from_keywords(
-            {"refnames": refnames, "full": full}, prefix, False)
+            {"refnames": refnames, "full": full, "date": date}, prefix, False)
 
     def test_parse(self):
         v = self.parse(" (HEAD, 2.0,master  , otherbranch ) ", " full ")
@@ -97,6 +105,7 @@ class Keywords(unittest.TestCase):
         self.assertEqual(v["full-revisionid"], "full")
         self.assertEqual(v["dirty"], False)
         self.assertEqual(v["error"], None)
+        self.assertEqual(v["date"], None)
 
     def test_prefer_short(self):
         v = self.parse(" (HEAD, 2.0rc1, 2.0, 2.0rc2) ", " full ")
@@ -104,6 +113,7 @@ class Keywords(unittest.TestCase):
         self.assertEqual(v["full-revisionid"], "full")
         self.assertEqual(v["dirty"], False)
         self.assertEqual(v["error"], None)
+        self.assertEqual(v["date"], None)
 
     def test_prefix(self):
         v = self.parse(" (HEAD, projectname-2.0) ", " full ", "projectname-")
@@ -111,6 +121,7 @@ class Keywords(unittest.TestCase):
         self.assertEqual(v["full-revisionid"], "full")
         self.assertEqual(v["dirty"], False)
         self.assertEqual(v["error"], None)
+        self.assertEqual(v["date"], None)
 
     def test_unexpanded(self):
         self.assertRaises(from_keywords.NotThisMethod,
@@ -122,6 +133,7 @@ class Keywords(unittest.TestCase):
         self.assertEqual(v["full-revisionid"], "full")
         self.assertEqual(v["dirty"], False)
         self.assertEqual(v["error"], "no suitable tags")
+        self.assertEqual(v["date"], None)
 
     def test_no_prefix(self):
         v = self.parse("(HEAD, master, 1.23)", "full", "missingprefix-")
@@ -129,6 +141,24 @@ class Keywords(unittest.TestCase):
         self.assertEqual(v["full-revisionid"], "full")
         self.assertEqual(v["dirty"], False)
         self.assertEqual(v["error"], "no suitable tags")
+        self.assertEqual(v["date"], None)
+
+    def test_date(self):
+        date = "2017-07-24 16:03:40 +0200"
+        result = "2017-07-24T16:03:40+0200"
+        v = self.parse(" (HEAD, 2.0,master  , otherbranch ) ", " full ",
+                       date=date)
+        self.assertEqual(v["date"], result)
+
+    def test_date_gpg(self):
+        date = """
+        gpg: Signature information
+        gpg: ...
+        2017-07-24 16:03:40 +0200"""
+        result = "2017-07-24T16:03:40+0200"
+        v = self.parse(" (HEAD, 2.0,master  , otherbranch ) ", " full ",
+                       date=date)
+        self.assertEqual(v["date"], result)
 
 expected_renders = """
 closest-tag: 1.0