Add support for creating signed tags.
authorJelmer Vernooij <jelmer@jelmer.uk>
Sun, 13 Jan 2019 14:08:31 +0000 (14:08 +0000)
committerJelmer Vernooij <jelmer@jelmer.uk>
Sun, 13 Jan 2019 14:08:31 +0000 (14:08 +0000)
NEWS
bin/dulwich
dulwich/objects.py
dulwich/porcelain.py
dulwich/tests/test_objects.py

diff --git a/NEWS b/NEWS
index 187e5fdd3dce6183295ed3f5f349577f950cf6ce..b37f5545f744fcb020621886cad3606b8bc6bc5e 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,9 @@
  * Support plain strings as refspec arguments to
    ``dulwich.porcelain.push``. (Jelmer Vernooij)
 
+ * Add support for creating signed tags.
+   (Jelmer Vernooij, #542)
+
  BUG FIXES
 
  *  Handle invalid ref that pretends to be a sub-folder under a valid ref.
index 9acba566fd42036b059cc9ed7e1da9bc68f44701..eeaee124b022f0b2518fdd1f10071ee20c8bf7b8 100755 (executable)
@@ -311,11 +311,13 @@ class cmd_rev_list(Command):
 class cmd_tag(Command):
 
     def run(self, args):
-        opts, args = getopt(args, '', [])
-        if len(args) < 2:
-            print('Usage: dulwich tag NAME')
-            sys.exit(1)
-        porcelain.tag('.', args[0])
+        parser = optparse.OptionParser()
+        parser.add_option("-a", "--annotated", help="Create an annotated tag.", action="store_true")
+        parser.add_option("-s", "--sign", help="Sign the annotated tag.", action="store_true")
+        options, args = parser.parse_args(args)
+        porcelain.tag_create(
+            '.', args[0], annotated=options.annotated,
+            sign=options.sign)
 
 
 class cmd_repack(Command):
index 8e224162104b101b21c4ae697a2444849ed68268..a56fdec90ed8d0bb6529738829341fb96ba1a5a0 100644 (file)
@@ -67,6 +67,8 @@ S_IFGITLINK = 0o160000
 
 MAX_TIME = 9223372036854775807  # (2**63) - 1 - signed long int max
 
+BEGIN_PGP_SIGNATURE = b"-----BEGIN PGP SIGNATURE-----"
+
 
 def S_ISGITLINK(m):
     """Check if a mode indicates a submodule.
@@ -691,7 +693,7 @@ class Tag(ShaFile):
 
     __slots__ = ('_tag_timezone_neg_utc', '_name', '_object_sha',
                  '_object_class', '_tag_time', '_tag_timezone',
-                 '_tagger', '_message')
+                 '_tagger', '_message', '_signature')
 
     def __init__(self):
         super(Tag, self).__init__()
@@ -699,6 +701,7 @@ class Tag(ShaFile):
         self._tag_time = None
         self._tag_timezone = None
         self._tag_timezone_neg_utc = False
+        self._signature = None
 
     @classmethod
     def from_path(cls, filename):
@@ -757,6 +760,8 @@ class Tag(ShaFile):
         if self._message is not None:
             chunks.append(b'\n')  # To close headers
             chunks.append(self._message)
+        if self._signature is not None:
+            chunks.append(self._signature)
         return chunks
 
     def _deserialize(self, chunks):
@@ -781,7 +786,18 @@ class Tag(ShaFile):
                  (self._tag_timezone,
                   self._tag_timezone_neg_utc)) = parse_time_entry(value)
             elif field is None:
-                self._message = value
+                if value is None:
+                    self._message = None
+                    self._signature = None
+                else:
+                    try:
+                        sig_idx = value.index(BEGIN_PGP_SIGNATURE)
+                    except ValueError:
+                        self._message = value
+                        self._signature = None
+                    else:
+                        self._message = value[:sig_idx]
+                        self._signature = value[sig_idx:]
             else:
                 raise ObjectFormatException("Unknown field %s" % field)
 
@@ -810,7 +826,10 @@ class Tag(ShaFile):
             "tag_timezone",
             "The timezone that tag_time is in.")
     message = serializable_property(
-            "message", "The message attached to this tag")
+            "message", "the message attached to this tag")
+
+    signature = serializable_property(
+            "signature", "Optional detached GPG signature")
 
 
 class TreeEntry(namedtuple('TreeEntry', ['path', 'mode', 'sha'])):
index 187b9863d4276729e586f6fe370e9f5cc924a693..228a664f42e5989e19fc8dc9e4e1d4164b9dc13f 100644 (file)
@@ -681,7 +681,8 @@ def tag(*args, **kwargs):
 
 def tag_create(
         repo, tag, author=None, message=None, annotated=False,
-        objectish="HEAD", tag_time=None, tag_timezone=None):
+        objectish="HEAD", tag_time=None, tag_timezone=None,
+        sign=False):
     """Creates a tag in git via dulwich calls:
 
     :param repo: Path to repository
@@ -692,6 +693,7 @@ def tag_create(
     :param objectish: object the tag should point at, defaults to HEAD
     :param tag_time: Optional time for annotated tag
     :param tag_timezone: Optional timezone for annotated tag
+    :param sign: GPG Sign the tag
     """
 
     with open_repo_closing(repo) as r:
@@ -702,7 +704,7 @@ def tag_create(
             tag_obj = Tag()
             if author is None:
                 # TODO(jelmer): Don't use repo private method.
-                author = r._get_user_identity()
+                author = r._get_user_identity(r.get_config_stack())
             tag_obj.tagger = author
             tag_obj.message = message
             tag_obj.name = tag
@@ -716,6 +718,10 @@ def tag_create(
             elif isinstance(tag_timezone, str):
                 tag_timezone = parse_timezone(tag_timezone)
             tag_obj.tag_timezone = tag_timezone
+            if sign:
+                import gpg
+                with gpg.Context(armor=True) as c:
+                    tag_obj.signature, result = c.sign(tag_obj.as_raw_string())
             r.object_store.add_object(tag_obj)
             tag_id = tag_obj.id
         else:
index 441f64166f61bc468fa55d68f17f67135634fcfa..c6556fac21a002b9174f0a470e2786d8fc1b8506 100644 (file)
@@ -204,6 +204,9 @@ class BlobReadTests(TestCase):
         self.assertEqual(
                 t.message,
                 b'This is a signed tag\n'
+                )
+        self.assertEqual(
+                t.signature,
                 b'-----BEGIN PGP SIGNATURE-----\n'
                 b'Version: GnuPG v1.4.9 (GNU/Linux)\n'
                 b'\n'