~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/gpg.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2011 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2009, 2011, 2012, 2013, 2016 Canonical Ltd
2
2
#   Authors: Robert Collins <robert.collins@canonical.com>
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
17
17
 
18
18
"""GPG signing and checking logic."""
19
19
 
 
20
from __future__ import absolute_import
 
21
 
20
22
import os
21
23
import sys
22
24
from StringIO import StringIO
27
29
import subprocess
28
30
 
29
31
from bzrlib import (
 
32
    config,
30
33
    errors,
31
34
    trace,
32
35
    ui,
37
40
    )
38
41
""")
39
42
 
 
43
from bzrlib.symbol_versioning import (
 
44
    deprecated_in,
 
45
    deprecated_method,
 
46
    )
 
47
 
40
48
#verification results
41
49
SIGNATURE_VALID = 0
42
50
SIGNATURE_KEY_MISSING = 1
45
53
SIGNATURE_EXPIRED = 4
46
54
 
47
55
 
 
56
def bulk_verify_signatures(repository, revids, strategy,
 
57
        process_events_callback=None):
 
58
    """Do verifications on a set of revisions
 
59
 
 
60
    :param repository: repository object
 
61
    :param revids: list of revision ids to verify
 
62
    :param strategy: GPG strategy to use
 
63
    :param process_events_callback: method to call for GUI frontends that
 
64
        want to keep their UI refreshed
 
65
 
 
66
    :return: count dictionary of results of each type,
 
67
             result list for each revision,
 
68
             boolean True if all results are verified successfully
 
69
    """
 
70
    count = {SIGNATURE_VALID: 0,
 
71
             SIGNATURE_KEY_MISSING: 0,
 
72
             SIGNATURE_NOT_VALID: 0,
 
73
             SIGNATURE_NOT_SIGNED: 0,
 
74
             SIGNATURE_EXPIRED: 0}
 
75
    result = []
 
76
    all_verifiable = True
 
77
    total = len(revids)
 
78
    pb = ui.ui_factory.nested_progress_bar()
 
79
    try:
 
80
        for i, (rev_id, verification_result, uid) in enumerate(
 
81
                repository.verify_revision_signatures(
 
82
                    revids, strategy)):
 
83
            pb.update("verifying signatures", i, total)
 
84
            result.append([rev_id, verification_result, uid])
 
85
            count[verification_result] += 1
 
86
            if verification_result != SIGNATURE_VALID:
 
87
                all_verifiable = False
 
88
            if process_events_callback is not None:
 
89
                process_events_callback()
 
90
    finally:
 
91
        pb.finished()
 
92
    return (count, result, all_verifiable)
 
93
 
 
94
 
48
95
class DisabledGPGStrategy(object):
49
96
    """A GPG Strategy that makes everything fail."""
50
97
 
95
142
                else:
96
143
                    self.acceptable_keys.append(pattern)
97
144
 
 
145
    @deprecated_method(deprecated_in((2, 6, 0)))
98
146
    def do_verifications(self, revisions, repository):
99
 
        count = {SIGNATURE_VALID: 0,
100
 
                 SIGNATURE_KEY_MISSING: 0,
101
 
                 SIGNATURE_NOT_VALID: 0,
102
 
                 SIGNATURE_NOT_SIGNED: 0,
103
 
                 SIGNATURE_EXPIRED: 0}
104
 
        result = []
105
 
        all_verifiable = True
106
 
        for rev_id in revisions:
107
 
            verification_result, uid =\
108
 
                                repository.verify_revision(rev_id,self)
109
 
            result.append([rev_id, verification_result, uid])
110
 
            count[verification_result] += 1
111
 
            if verification_result != SIGNATURE_VALID:
112
 
                all_verifiable = False
113
 
        return (count, result, all_verifiable)
 
147
        return bulk_verify_signatures(repository, revisions, self)
114
148
 
 
149
    @deprecated_method(deprecated_in((2, 6, 0)))
115
150
    def valid_commits_message(self, count):
116
 
        return gettext(u"{0} commits with valid signatures").format(
117
 
                                        count[SIGNATURE_VALID])            
 
151
        return valid_commits_message(count)
118
152
 
 
153
    @deprecated_method(deprecated_in((2, 6, 0)))
119
154
    def unknown_key_message(self, count):
120
 
        return ngettext(u"{0} commit with unknown key",
121
 
                             u"{0} commits with unknown keys",
122
 
                             count[SIGNATURE_KEY_MISSING]).format(
123
 
                                        count[SIGNATURE_KEY_MISSING])
 
155
        return unknown_key_message(count)
124
156
 
 
157
    @deprecated_method(deprecated_in((2, 6, 0)))
125
158
    def commit_not_valid_message(self, count):
126
 
        return ngettext(u"{0} commit not valid",
127
 
                             u"{0} commits not valid",
128
 
                             count[SIGNATURE_NOT_VALID]).format(
129
 
                                            count[SIGNATURE_NOT_VALID])
 
159
        return commit_not_valid_message(count)
130
160
 
 
161
    @deprecated_method(deprecated_in((2, 6, 0)))
131
162
    def commit_not_signed_message(self, count):
132
 
        return ngettext(u"{0} commit not signed",
133
 
                             u"{0} commits not signed",
134
 
                             count[SIGNATURE_NOT_SIGNED]).format(
135
 
                                        count[SIGNATURE_NOT_SIGNED])
 
163
        return commit_not_signed_message(count)
136
164
 
 
165
    @deprecated_method(deprecated_in((2, 6, 0)))
137
166
    def expired_commit_message(self, count):
138
 
        return ngettext(u"{0} commit with key now expired",
139
 
                        u"{0} commits with key now expired",
140
 
                        count[SIGNATURE_EXPIRED]).format(
141
 
                                        count[SIGNATURE_EXPIRED])
 
167
        return expired_commit_message(count)
142
168
 
143
169
 
144
170
def _set_gpg_tty():
159
185
 
160
186
    acceptable_keys = None
161
187
 
 
188
    def __init__(self, config_stack):
 
189
        self._config_stack = config_stack
 
190
        try:
 
191
            import gpgme
 
192
            self.context = gpgme.Context()
 
193
        except ImportError, error:
 
194
            pass # can't use verify()
 
195
 
162
196
    @staticmethod
163
197
    def verify_signatures_available():
164
198
        """
173
207
            return False
174
208
 
175
209
    def _command_line(self):
176
 
        
177
 
        return [self._config.gpg_signing_command(), '--clearsign', '-u',
178
 
                                                self._config.gpg_signing_key()]
179
 
 
180
 
    def __init__(self, config):
181
 
        self._config = config
182
 
        try:
183
 
            import gpgme
184
 
            self.context = gpgme.Context()
185
 
        except ImportError, error:
186
 
            pass # can't use verify()
 
210
        key = self._config_stack.get('gpg_signing_key')
 
211
        if key is None or key == 'default':
 
212
            # 'default' or not setting gpg_signing_key at all means we should
 
213
            # use the user email address
 
214
            key = config.extract_email_address(self._config_stack.get('email'))
 
215
        return [self._config_stack.get('gpg_signing_command'), '--clearsign',
 
216
                '-u', key]
187
217
 
188
218
    def sign(self, content):
189
219
        if isinstance(content, unicode):
223
253
 
224
254
    def verify(self, content, testament):
225
255
        """Check content has a valid signature.
226
 
        
 
256
 
227
257
        :param content: the commit signature
228
258
        :param testament: the valid testament string for the commit
229
 
        
 
259
 
230
260
        :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid
231
261
        """
232
262
        try:
236
266
 
237
267
        signature = StringIO(content)
238
268
        plain_output = StringIO()
239
 
        
240
269
        try:
241
270
            result = self.context.verify(signature, None, plain_output)
242
271
        except gpgme.GpgmeError,error:
246
275
        # test_verify_invalid()
247
276
        if len(result) == 0:
248
277
            return SIGNATURE_NOT_VALID, None
249
 
        # User has specified a list of acceptable keys, check our result is in it.
250
 
        # test_verify_unacceptable_key()
 
278
        # User has specified a list of acceptable keys, check our result is in
 
279
        # it.  test_verify_unacceptable_key()
251
280
        fingerprint = result[0].fpr
252
281
        if self.acceptable_keys is not None:
253
 
            if not fingerprint in self.acceptable_keys:                
 
282
            if not fingerprint in self.acceptable_keys:
254
283
                return SIGNATURE_KEY_MISSING, fingerprint[-8:]
255
284
        # Check the signature actually matches the testament.
256
285
        # test_verify_bad_testament()
257
286
        if testament != plain_output.getvalue():
258
 
            return SIGNATURE_NOT_VALID, None 
 
287
            return SIGNATURE_NOT_VALID, None
259
288
        # Yay gpgme set the valid bit.
260
289
        # Can't write a test for this one as you can't set a key to be
261
290
        # trusted using gpgme.
272
301
        # test_verify_unknown_key()
273
302
        if result[0].summary & gpgme.SIGSUM_KEY_MISSING:
274
303
            return SIGNATURE_KEY_MISSING, fingerprint[-8:]
275
 
        # Summary isn't set if sig is valid but key is untrusted
276
 
        # but if user has explicity set the key as acceptable we can validate it.
 
304
        # Summary isn't set if sig is valid but key is untrusted but if user
 
305
        # has explicity set the key as acceptable we can validate it.
277
306
        if result[0].summary == 0 and self.acceptable_keys is not None:
278
307
            if fingerprint in self.acceptable_keys:
279
308
                # test_verify_untrusted_but_accepted()
280
 
                return SIGNATURE_VALID, None 
 
309
                return SIGNATURE_VALID, None
281
310
        # test_verify_valid_but_untrusted()
282
311
        if result[0].summary == 0 and self.acceptable_keys is None:
283
312
            return SIGNATURE_NOT_VALID, None
293
322
                return SIGNATURE_NOT_VALID, None
294
323
        # A signature from a revoked key gets this.
295
324
        # test_verify_revoked_signature()
296
 
        if result[0].summary & gpgme.SIGSUM_SYS_ERROR:
 
325
        if ((result[0].summary & gpgme.SIGSUM_SYS_ERROR
 
326
             or result[0].status.strerror == 'Certificate revoked')):
297
327
            return SIGNATURE_NOT_VALID, None
298
328
        # Other error types such as revoked keys should (I think) be caught by
299
329
        # SIGSUM_RED so anything else means something is buggy.
300
 
        raise errors.SignatureVerificationFailed("Unknown GnuPG key "\
301
 
                                                 "verification result")
 
330
        raise errors.SignatureVerificationFailed(
 
331
            "Unknown GnuPG key verification result")
302
332
 
303
333
    def set_acceptable_keys(self, command_line_input):
304
 
        """sets the acceptable keys for verifying with this GPGStrategy
305
 
        
 
334
        """Set the acceptable keys for verifying with this GPGStrategy.
 
335
 
306
336
        :param command_line_input: comma separated list of patterns from
307
337
                                command line
308
338
        :return: nothing
309
339
        """
310
 
        key_patterns = None
311
 
        acceptable_keys_config = self._config.acceptable_keys()
312
 
        try:
313
 
            if isinstance(acceptable_keys_config, unicode):
314
 
                acceptable_keys_config = str(acceptable_keys_config)
315
 
        except UnicodeEncodeError:
316
 
            #gpg Context.keylist(pattern) does not like unicode
317
 
            raise errors.BzrCommandError(gettext('Only ASCII permitted in option names'))
318
 
 
 
340
        patterns = None
 
341
        acceptable_keys_config = self._config_stack.get('acceptable_keys')
319
342
        if acceptable_keys_config is not None:
320
 
            key_patterns = acceptable_keys_config
321
 
        if command_line_input is not None: #command line overrides config
322
 
            key_patterns = command_line_input
323
 
        if key_patterns is not None:
324
 
            patterns = key_patterns.split(",")
 
343
            patterns = acceptable_keys_config
 
344
        if command_line_input is not None: # command line overrides config
 
345
            patterns = command_line_input.split(',')
325
346
 
 
347
        if patterns:
326
348
            self.acceptable_keys = []
327
349
            for pattern in patterns:
328
350
                result = self.context.keylist(pattern)
336
358
                            "No GnuPG key results for pattern: {0}"
337
359
                                ).format(pattern))
338
360
 
 
361
    @deprecated_method(deprecated_in((2, 6, 0)))
339
362
    def do_verifications(self, revisions, repository,
340
363
                            process_events_callback=None):
341
364
        """do verifications on a set of revisions
342
 
        
 
365
 
343
366
        :param revisions: list of revision ids to verify
344
367
        :param repository: repository object
345
368
        :param process_events_callback: method to call for GUI frontends that
346
 
                                                want to keep their UI refreshed
347
 
        
 
369
            want to keep their UI refreshed
 
370
 
348
371
        :return: count dictionary of results of each type,
349
372
                 result list for each revision,
350
373
                 boolean True if all results are verified successfully
351
374
        """
352
 
        count = {SIGNATURE_VALID: 0,
353
 
                 SIGNATURE_KEY_MISSING: 0,
354
 
                 SIGNATURE_NOT_VALID: 0,
355
 
                 SIGNATURE_NOT_SIGNED: 0,
356
 
                 SIGNATURE_EXPIRED: 0}
357
 
        result = []
358
 
        all_verifiable = True
359
 
        for rev_id in revisions:
360
 
            verification_result, uid =\
361
 
                                repository.verify_revision(rev_id,self)
362
 
            result.append([rev_id, verification_result, uid])
363
 
            count[verification_result] += 1
364
 
            if verification_result != SIGNATURE_VALID:
365
 
                all_verifiable = False
366
 
            if process_events_callback is not None:
367
 
                process_events_callback()
368
 
        return (count, result, all_verifiable)
 
375
        return bulk_verify_signatures(repository, revisions, self,
 
376
            process_events_callback)
369
377
 
 
378
    @deprecated_method(deprecated_in((2, 6, 0)))
370
379
    def verbose_valid_message(self, result):
371
380
        """takes a verify result and returns list of signed commits strings"""
372
 
        signers = {}
373
 
        for rev_id, validity, uid in result:
374
 
            if validity == SIGNATURE_VALID:
375
 
                signers.setdefault(uid, 0)
376
 
                signers[uid] += 1
377
 
        result = []
378
 
        for uid, number in signers.items():
379
 
             result.append( ngettext(u"{0} signed {1} commit", 
380
 
                             u"{0} signed {1} commits",
381
 
                             number).format(uid, number) )
382
 
        return result
383
 
 
384
 
 
 
381
        return verbose_valid_message(result)
 
382
 
 
383
    @deprecated_method(deprecated_in((2, 6, 0)))
385
384
    def verbose_not_valid_message(self, result, repo):
386
385
        """takes a verify result and returns list of not valid commit info"""
387
 
        signers = {}
388
 
        for rev_id, validity, empty in result:
389
 
            if validity == SIGNATURE_NOT_VALID:
390
 
                revision = repo.get_revision(rev_id)
391
 
                authors = ', '.join(revision.get_apparent_authors())
392
 
                signers.setdefault(authors, 0)
393
 
                signers[authors] += 1
394
 
        result = []
395
 
        for authors, number in signers.items():
396
 
            result.append( ngettext(u"{0} commit by author {1}", 
397
 
                                 u"{0} commits by author {1}",
398
 
                                 number).format(number, authors) )
399
 
        return result
 
386
        return verbose_not_valid_message(result, repo)
400
387
 
 
388
    @deprecated_method(deprecated_in((2, 6, 0)))
401
389
    def verbose_not_signed_message(self, result, repo):
402
390
        """takes a verify result and returns list of not signed commit info"""
403
 
        signers = {}
404
 
        for rev_id, validity, empty in result:
405
 
            if validity == SIGNATURE_NOT_SIGNED:
406
 
                revision = repo.get_revision(rev_id)
407
 
                authors = ', '.join(revision.get_apparent_authors())
408
 
                signers.setdefault(authors, 0)
409
 
                signers[authors] += 1
410
 
        result = []
411
 
        for authors, number in signers.items():
412
 
            result.append( ngettext(u"{0} commit by author {1}", 
413
 
                                 u"{0} commits by author {1}",
414
 
                                 number).format(number, authors) )
415
 
        return result
 
391
        return verbose_not_valid_message(result, repo)
416
392
 
 
393
    @deprecated_method(deprecated_in((2, 6, 0)))
417
394
    def verbose_missing_key_message(self, result):
418
395
        """takes a verify result and returns list of missing key info"""
419
 
        signers = {}
420
 
        for rev_id, validity, fingerprint in result:
421
 
            if validity == SIGNATURE_KEY_MISSING:
422
 
                signers.setdefault(fingerprint, 0)
423
 
                signers[fingerprint] += 1
424
 
        result = []
425
 
        for fingerprint, number in signers.items():
426
 
            result.append( ngettext(u"Unknown key {0} signed {1} commit", 
427
 
                                 u"Unknown key {0} signed {1} commits",
428
 
                                 number).format(fingerprint, number) )
429
 
        return result
 
396
        return verbose_missing_key_message(result)
430
397
 
 
398
    @deprecated_method(deprecated_in((2, 6, 0)))
431
399
    def verbose_expired_key_message(self, result, repo):
432
400
        """takes a verify result and returns list of expired key info"""
433
 
        signers = {}
434
 
        fingerprint_to_authors = {}
435
 
        for rev_id, validity, fingerprint in result:
436
 
            if validity == SIGNATURE_EXPIRED:
437
 
                revision = repo.get_revision(rev_id)
438
 
                authors = ', '.join(revision.get_apparent_authors())
439
 
                signers.setdefault(fingerprint, 0)
440
 
                signers[fingerprint] += 1
441
 
                fingerprint_to_authors[fingerprint] = authors
442
 
        result = []
443
 
        for fingerprint, number in signers.items():
444
 
            result.append(ngettext(u"{0} commit by author {1} with "\
445
 
                                    "key {2} now expired", 
446
 
                                   u"{0} commits by author {1} with key {2} now "\
447
 
                                    "expired",
448
 
                                    number).format(number,
449
 
                            fingerprint_to_authors[fingerprint], fingerprint) )
450
 
        return result
 
401
        return verbose_expired_key_message(result, repo)
451
402
 
 
403
    @deprecated_method(deprecated_in((2, 6, 0)))
452
404
    def valid_commits_message(self, count):
453
405
        """returns message for number of commits"""
454
 
        return gettext(u"{0} commits with valid signatures").format(
455
 
                                        count[SIGNATURE_VALID])
 
406
        return valid_commits_message(count)
456
407
 
 
408
    @deprecated_method(deprecated_in((2, 6, 0)))
457
409
    def unknown_key_message(self, count):
458
410
        """returns message for number of commits"""
459
 
        return ngettext(u"{0} commit with unknown key",
460
 
                        u"{0} commits with unknown keys",
461
 
                        count[SIGNATURE_KEY_MISSING]).format(
462
 
                                        count[SIGNATURE_KEY_MISSING])
 
411
        return unknown_key_message(count)
463
412
 
 
413
    @deprecated_method(deprecated_in((2, 6, 0)))
464
414
    def commit_not_valid_message(self, count):
465
415
        """returns message for number of commits"""
466
 
        return ngettext(u"{0} commit not valid",
467
 
                        u"{0} commits not valid",
468
 
                        count[SIGNATURE_NOT_VALID]).format(
469
 
                                            count[SIGNATURE_NOT_VALID])
 
416
        return commit_not_valid_message(count)
470
417
 
 
418
    @deprecated_method(deprecated_in((2, 6, 0)))
471
419
    def commit_not_signed_message(self, count):
472
420
        """returns message for number of commits"""
473
 
        return ngettext(u"{0} commit not signed",
474
 
                        u"{0} commits not signed",
475
 
                        count[SIGNATURE_NOT_SIGNED]).format(
476
 
                                        count[SIGNATURE_NOT_SIGNED])
 
421
        return commit_not_signed_message(count)
477
422
 
 
423
    @deprecated_method(deprecated_in((2, 6, 0)))
478
424
    def expired_commit_message(self, count):
479
425
        """returns message for number of commits"""
480
 
        return ngettext(u"{0} commit with key now expired",
481
 
                        u"{0} commits with key now expired",
482
 
                        count[SIGNATURE_EXPIRED]).format(
483
 
                                    count[SIGNATURE_EXPIRED])
 
426
        return expired_commit_message(count)
 
427
 
 
428
 
 
429
def valid_commits_message(count):
 
430
    """returns message for number of commits"""
 
431
    return gettext(u"{0} commits with valid signatures").format(
 
432
                                    count[SIGNATURE_VALID])
 
433
 
 
434
 
 
435
def unknown_key_message(count):
 
436
    """returns message for number of commits"""
 
437
    return ngettext(u"{0} commit with unknown key",
 
438
                    u"{0} commits with unknown keys",
 
439
                    count[SIGNATURE_KEY_MISSING]).format(
 
440
                                    count[SIGNATURE_KEY_MISSING])
 
441
 
 
442
 
 
443
def commit_not_valid_message(count):
 
444
    """returns message for number of commits"""
 
445
    return ngettext(u"{0} commit not valid",
 
446
                    u"{0} commits not valid",
 
447
                    count[SIGNATURE_NOT_VALID]).format(
 
448
                                        count[SIGNATURE_NOT_VALID])
 
449
 
 
450
 
 
451
def commit_not_signed_message(count):
 
452
    """returns message for number of commits"""
 
453
    return ngettext(u"{0} commit not signed",
 
454
                    u"{0} commits not signed",
 
455
                    count[SIGNATURE_NOT_SIGNED]).format(
 
456
                                    count[SIGNATURE_NOT_SIGNED])
 
457
 
 
458
 
 
459
def expired_commit_message(count):
 
460
    """returns message for number of commits"""
 
461
    return ngettext(u"{0} commit with key now expired",
 
462
                    u"{0} commits with key now expired",
 
463
                    count[SIGNATURE_EXPIRED]).format(
 
464
                                count[SIGNATURE_EXPIRED])
 
465
 
 
466
 
 
467
def verbose_expired_key_message(result, repo):
 
468
    """takes a verify result and returns list of expired key info"""
 
469
    signers = {}
 
470
    fingerprint_to_authors = {}
 
471
    for rev_id, validity, fingerprint in result:
 
472
        if validity == SIGNATURE_EXPIRED:
 
473
            revision = repo.get_revision(rev_id)
 
474
            authors = ', '.join(revision.get_apparent_authors())
 
475
            signers.setdefault(fingerprint, 0)
 
476
            signers[fingerprint] += 1
 
477
            fingerprint_to_authors[fingerprint] = authors
 
478
    result = []
 
479
    for fingerprint, number in signers.items():
 
480
        result.append(
 
481
            ngettext(u"{0} commit by author {1} with key {2} now expired",
 
482
                     u"{0} commits by author {1} with key {2} now expired",
 
483
                     number).format(
 
484
                number, fingerprint_to_authors[fingerprint], fingerprint))
 
485
    return result
 
486
 
 
487
 
 
488
def verbose_valid_message(result):
 
489
    """takes a verify result and returns list of signed commits strings"""
 
490
    signers = {}
 
491
    for rev_id, validity, uid in result:
 
492
        if validity == SIGNATURE_VALID:
 
493
            signers.setdefault(uid, 0)
 
494
            signers[uid] += 1
 
495
    result = []
 
496
    for uid, number in signers.items():
 
497
         result.append(ngettext(u"{0} signed {1} commit",
 
498
                                u"{0} signed {1} commits",
 
499
                                number).format(uid, number))
 
500
    return result
 
501
 
 
502
 
 
503
def verbose_not_valid_message(result, repo):
 
504
    """takes a verify result and returns list of not valid commit info"""
 
505
    signers = {}
 
506
    for rev_id, validity, empty in result:
 
507
        if validity == SIGNATURE_NOT_VALID:
 
508
            revision = repo.get_revision(rev_id)
 
509
            authors = ', '.join(revision.get_apparent_authors())
 
510
            signers.setdefault(authors, 0)
 
511
            signers[authors] += 1
 
512
    result = []
 
513
    for authors, number in signers.items():
 
514
        result.append(ngettext(u"{0} commit by author {1}",
 
515
                               u"{0} commits by author {1}",
 
516
                               number).format(number, authors))
 
517
    return result
 
518
 
 
519
 
 
520
def verbose_not_signed_message(result, repo):
 
521
    """takes a verify result and returns list of not signed commit info"""
 
522
    signers = {}
 
523
    for rev_id, validity, empty in result:
 
524
        if validity == SIGNATURE_NOT_SIGNED:
 
525
            revision = repo.get_revision(rev_id)
 
526
            authors = ', '.join(revision.get_apparent_authors())
 
527
            signers.setdefault(authors, 0)
 
528
            signers[authors] += 1
 
529
    result = []
 
530
    for authors, number in signers.items():
 
531
        result.append(ngettext(u"{0} commit by author {1}",
 
532
                               u"{0} commits by author {1}",
 
533
                               number).format(number, authors))
 
534
    return result
 
535
 
 
536
 
 
537
def verbose_missing_key_message(result):
 
538
    """takes a verify result and returns list of missing key info"""
 
539
    signers = {}
 
540
    for rev_id, validity, fingerprint in result:
 
541
        if validity == SIGNATURE_KEY_MISSING:
 
542
            signers.setdefault(fingerprint, 0)
 
543
            signers[fingerprint] += 1
 
544
    result = []
 
545
    for fingerprint, number in signers.items():
 
546
        result.append(ngettext(u"Unknown key {0} signed {1} commit",
 
547
                               u"Unknown key {0} signed {1} commits",
 
548
                               number).format(fingerprint, number))
 
549
    return result