From 72aec0c79fb395689e40f9228df93e2a39cf8fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 13 Jan 2019 14:08:31 +0000 Subject: [PATCH] Add support for creating signed tags. --- NEWS | 3 +++ bin/dulwich | 12 +++++++----- dulwich/objects.py | 25 ++++++++++++++++++++++--- dulwich/porcelain.py | 10 ++++++++-- dulwich/tests/test_objects.py | 3 +++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/NEWS b/NEWS index 187e5fdd3..b37f5545f 100644 --- 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. diff --git a/bin/dulwich b/bin/dulwich index 9acba566f..eeaee124b 100755 --- a/bin/dulwich +++ b/bin/dulwich @@ -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): diff --git a/dulwich/objects.py b/dulwich/objects.py index 8e2241621..a56fdec90 100644 --- a/dulwich/objects.py +++ b/dulwich/objects.py @@ -67,6 +67,8 @@ 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 @@ def __init__(self): 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 @@ def _serialize(self): 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 @@ def _deserialize(self, chunks): (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 @@ def _set_object(self, value): "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'])): diff --git a/dulwich/porcelain.py b/dulwich/porcelain.py index 187b9863d..228a664f4 100644 --- a/dulwich/porcelain.py +++ b/dulwich/porcelain.py @@ -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: diff --git a/dulwich/tests/test_objects.py b/dulwich/tests/test_objects.py index 441f64166..c6556fac2 100644 --- a/dulwich/tests/test_objects.py +++ b/dulwich/tests/test_objects.py @@ -204,6 +204,9 @@ def test_read_tag_from_file(self): 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'