~bzr-pqm/bzr/bzr.dev

5971.1.36 by Jonathan Riddell
update copyright
1
# Copyright (C) 2005, 2011 Canonical Ltd
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
2
#   Authors: Robert Collins <robert.collins@canonical.com>
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
4183.7.1 by Sabin Iacob
update FSF mailing address
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
17
18
"""GPG signing and checking logic."""
19
1996.3.1 by John Arbash Meinel
Demandloading builtins.py drops our load time from 350ms to 291ms
20
import os
21
import sys
5971.1.4 by Jonathan Riddell
tidy up repository and gpg.py
22
from StringIO import StringIO
1996.3.1 by John Arbash Meinel
Demandloading builtins.py drops our load time from 350ms to 291ms
23
24
from bzrlib.lazy_import import lazy_import
25
lazy_import(globals(), """
1442.1.58 by Robert Collins
gpg signing of content
26
import errno
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
27
import subprocess
28
1912.3.2 by John Arbash Meinel
Adding some logging, because on my machine TTY is not exported by default.
29
from bzrlib import (
30
    errors,
31
    trace,
1551.8.11 by Aaron Bentley
Clear terminal before signing
32
    ui,
1912.3.2 by John Arbash Meinel
Adding some logging, because on my machine TTY is not exported by default.
33
    )
1996.3.1 by John Arbash Meinel
Demandloading builtins.py drops our load time from 350ms to 291ms
34
""")
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
35
5971.1.79 by Jonathan Riddell
gpg.py uses i18n but bzrlib.i18n is not ready for use, so add a stub class for now
36
class i18n:
37
    """this class is ready to use bzrlib.i18n but bzrlib.i18n is not ready to
38
    use so here is a stub until it is"""
39
    @staticmethod
40
    def gettext(string):
41
        return string
42
        
43
    @staticmethod
44
    def ngettext(single, plural, number):
45
        if number == 1:
46
            return single
47
        else:
48
            return plural
49
5971.1.4 by Jonathan Riddell
tidy up repository and gpg.py
50
#verification results
5971.1.1 by Jonathan Riddell
add a verify command
51
SIGNATURE_VALID = 0
52
SIGNATURE_KEY_MISSING = 1
53
SIGNATURE_NOT_VALID = 2
54
SIGNATURE_NOT_SIGNED = 3
55
56
1442.1.62 by Robert Collins
Allow creation of testaments from uncommitted data, and use that to get signatures before committing revisions.
57
class DisabledGPGStrategy(object):
58
    """A GPG Strategy that makes everything fail."""
59
5971.1.60 by Jonathan Riddell
move checking for gpgme availability into gpg.py
60
    @staticmethod
61
    def verify_signatures_available():
62
        return True
63
1442.1.62 by Robert Collins
Allow creation of testaments from uncommitted data, and use that to get signatures before committing revisions.
64
    def __init__(self, ignored):
65
        """Real strategies take a configuration."""
66
67
    def sign(self, content):
68
        raise errors.SigningFailed('Signing is disabled.')
69
5971.1.31 by Jonathan Riddell
and update tests
70
    def verify(self, content, testament):
5971.1.33 by Jonathan Riddell
rename errors.VerifyFailed to errors.SignatureVerificationFailed
71
        raise errors.SignatureVerificationFailed('Signature verification is \
72
disabled.')
5971.1.6 by Jonathan Riddell
fix methods for dummy gpg strategies
73
5971.1.69 by Jonathan Riddell
move some code from cmd_verify to gpg.set_acceptable_keys
74
    def set_acceptable_keys(self, command_line_input):
5971.1.14 by Jonathan Riddell
add test for set_acceptable_keys, accept non-trusted keys if specified as acceptable, import dummy key in tests so it works outside my machine
75
        pass
76
1442.1.62 by Robert Collins
Allow creation of testaments from uncommitted data, and use that to get signatures before committing revisions.
77
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
78
class LoopbackGPGStrategy(object):
1442.1.62 by Robert Collins
Allow creation of testaments from uncommitted data, and use that to get signatures before committing revisions.
79
    """A GPG Strategy that acts like 'cat' - data is just passed through."""
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
80
5971.1.60 by Jonathan Riddell
move checking for gpgme availability into gpg.py
81
    @staticmethod
82
    def verify_signatures_available():
83
        return True
84
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
85
    def __init__(self, ignored):
86
        """Real strategies take a configuration."""
87
88
    def sign(self, content):
1551.12.15 by Aaron Bentley
add header/trailer to fake clearsigned texts
89
        return ("-----BEGIN PSEUDO-SIGNED CONTENT-----\n" + content +
1551.12.52 by Aaron Bentley
speling fix
90
                "-----END PSEUDO-SIGNED CONTENT-----\n")
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
91
5971.1.31 by Jonathan Riddell
and update tests
92
    def verify(self, content, testament):
5971.1.22 by Jonathan Riddell
fix tests
93
        return SIGNATURE_VALID, None
5971.1.5 by Jonathan Riddell
catch errors from gpgme, implement verify in dummy gpg strategies
94
5971.1.69 by Jonathan Riddell
move some code from cmd_verify to gpg.set_acceptable_keys
95
    def set_acceptable_keys(self, command_line_input):
96
        if command_line_input is not None:
97
            patterns = command_line_input.split(",")
98
            self.acceptable_keys = []
99
            for pattern in patterns:
100
                if pattern == "unknown":
101
                    pass
102
                else:
103
                    self.acceptable_keys.append(pattern)
5971.1.14 by Jonathan Riddell
add test for set_acceptable_keys, accept non-trusted keys if specified as acceptable, import dummy key in tests so it works outside my machine
104
5971.1.70 by Jonathan Riddell
move code which does verifications of revisions from cmd_verify_signatures to gpg.do_verifications
105
    def do_verifications(self, revisions, repository):
106
        count = {SIGNATURE_VALID: 0,
107
                 SIGNATURE_KEY_MISSING: 0,
108
                 SIGNATURE_NOT_VALID: 0,
109
                 SIGNATURE_NOT_SIGNED: 0}
110
        result = []
111
        all_verifiable = True
112
        for rev_id in revisions:
113
            verification_result, uid =\
114
                                repository.verify_revision(rev_id,self)
115
            result.append([rev_id, verification_result, uid])
116
            count[verification_result] += 1
117
            if verification_result != SIGNATURE_VALID:
118
                all_verifiable = False
119
        return (count, result, all_verifiable)
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
120
121
    def valid_commits_message(self, count):
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
122
        return i18n.gettext("{0} commits with valid signatures").format(
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
123
                                        count[SIGNATURE_VALID])            
124
125
    def unknown_key_message(self, count):
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
126
        return i18n.ngettext("{0} commit with unknown key",
127
                             "{0} commits with unknown keys",
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
128
                             count[SIGNATURE_KEY_MISSING]).format(
129
                                        count[SIGNATURE_KEY_MISSING])
130
131
    def commit_not_valid_message(self, count):
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
132
        return i18n.ngettext("{0} commit not valid",
133
                             "{0} commits not valid",
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
134
                             count[SIGNATURE_NOT_VALID]).format(
135
                                            count[SIGNATURE_NOT_VALID])
136
137
    def commit_not_signed_message(self, count):
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
138
        return i18n.ngettext("{0} commit not signed",
139
                             "{0} commits not signed",
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
140
                             count[SIGNATURE_NOT_SIGNED]).format(
141
                                        count[SIGNATURE_NOT_SIGNED])
142
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
143
1912.3.1 by John Arbash Meinel
updating gpg.py to set GPG_TTY in the environment.
144
def _set_gpg_tty():
145
    tty = os.environ.get('TTY')
146
    if tty is not None:
147
        os.environ['GPG_TTY'] = tty
1912.3.2 by John Arbash Meinel
Adding some logging, because on my machine TTY is not exported by default.
148
        trace.mutter('setting GPG_TTY=%s', tty)
149
    else:
150
        # This is not quite worthy of a warning, because some people
151
        # don't need GPG_TTY to be set. But it is worthy of a big mark
152
        # in ~/.bzr.log, so that people can debug it if it happens to them
153
        trace.mutter('** Env var TTY empty, cannot set GPG_TTY.'
154
                     '  Is TTY exported?')
1912.3.1 by John Arbash Meinel
updating gpg.py to set GPG_TTY in the environment.
155
156
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
157
class GPGStrategy(object):
158
    """GPG Signing and checking facilities."""
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
159
5971.1.11 by Jonathan Riddell
add set_acceptable_keys() so user can specify which gpg keys can be used for verification
160
    acceptable_keys = None
161
5971.1.60 by Jonathan Riddell
move checking for gpgme availability into gpg.py
162
    @staticmethod
163
    def verify_signatures_available():
164
        try:
165
            import gpgme
166
            return True
167
        except ImportError, error:
168
            return False
169
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
170
    def _command_line(self):
1185.78.4 by John Arbash Meinel
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.
171
        return [self._config.gpg_signing_command(), '--clearsign']
1442.1.57 by Robert Collins
check that we get the right command line from the default gpg strategy.
172
173
    def __init__(self, config):
174
        self._config = config
5971.1.61 by Jonathan Riddell
make gpgme context global to class
175
        try:
176
            import gpgme
177
            self.context = gpgme.Context()
178
        except ImportError, error:
179
            pass # can't use verify()
1442.1.58 by Robert Collins
gpg signing of content
180
181
    def sign(self, content):
2273.1.1 by John Arbash Meinel
``GPGStrategy.sign()`` will now raise ``BzrBadParameterUnicode`` if
182
        if isinstance(content, unicode):
183
            raise errors.BzrBadParameterUnicode('content')
1551.8.11 by Aaron Bentley
Clear terminal before signing
184
        ui.ui_factory.clear_term()
1963.1.8 by John Arbash Meinel
Don't use preexec_fn on win32
185
186
        preexec_fn = _set_gpg_tty
187
        if sys.platform == 'win32':
188
            # Win32 doesn't support preexec_fn, but wouldn't support TTY anyway.
189
            preexec_fn = None
1442.1.58 by Robert Collins
gpg signing of content
190
        try:
1185.78.4 by John Arbash Meinel
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.
191
            process = subprocess.Popen(self._command_line(),
192
                                       stdin=subprocess.PIPE,
1912.3.1 by John Arbash Meinel
updating gpg.py to set GPG_TTY in the environment.
193
                                       stdout=subprocess.PIPE,
1963.1.8 by John Arbash Meinel
Don't use preexec_fn on win32
194
                                       preexec_fn=preexec_fn)
1442.1.58 by Robert Collins
gpg signing of content
195
            try:
1185.78.4 by John Arbash Meinel
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.
196
                result = process.communicate(content)[0]
1442.1.58 by Robert Collins
gpg signing of content
197
                if process.returncode is None:
198
                    process.wait()
199
                if process.returncode != 0:
1185.78.4 by John Arbash Meinel
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.
200
                    raise errors.SigningFailed(self._command_line())
1442.1.58 by Robert Collins
gpg signing of content
201
                return result
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
202
            except OSError, e:
1442.1.58 by Robert Collins
gpg signing of content
203
                if e.errno == errno.EPIPE:
1185.78.4 by John Arbash Meinel
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.
204
                    raise errors.SigningFailed(self._command_line())
1442.1.59 by Robert Collins
Add re-sign command to generate a digital signature on a single revision.
205
                else:
206
                    raise
1442.1.58 by Robert Collins
gpg signing of content
207
        except ValueError:
208
            # bad subprocess parameters, should never happen.
209
            raise
210
        except OSError, e:
211
            if e.errno == errno.ENOENT:
212
                # gpg is not installed
1185.78.4 by John Arbash Meinel
Reverting gpg changes, should not be mainline, see gpg_uses_tempfile plugin.
213
                raise errors.SigningFailed(self._command_line())
1442.1.58 by Robert Collins
gpg signing of content
214
            else:
215
                raise
5971.1.1 by Jonathan Riddell
add a verify command
216
5971.1.30 by Jonathan Riddell
check the testament actually matches the commit when validating
217
    def verify(self, content, testament):
5971.1.7 by Jonathan Riddell
add method docs
218
        """Check content has a valid signature.
219
        
220
        :param content: the commit signature
5971.1.30 by Jonathan Riddell
check the testament actually matches the commit when validating
221
        :param testament: the valid testament string for the commit
5971.1.7 by Jonathan Riddell
add method docs
222
        
5971.1.18 by Jonathan Riddell
add email to verbose output
223
        :return: SIGNATURE_VALID or a failed SIGNATURE_ value, key uid if valid
5971.1.7 by Jonathan Riddell
add method docs
224
        """
5971.1.4 by Jonathan Riddell
tidy up repository and gpg.py
225
        try:
226
            import gpgme
5971.1.41 by Jonathan Riddell
fix calling GpgmeNotInstalled
227
        except ImportError, error:
228
            raise errors.GpgmeNotInstalled(error)
5971.1.4 by Jonathan Riddell
tidy up repository and gpg.py
229
5971.1.1 by Jonathan Riddell
add a verify command
230
        signature = StringIO(content)
5971.1.4 by Jonathan Riddell
tidy up repository and gpg.py
231
        plain_output = StringIO()
5971.1.1 by Jonathan Riddell
add a verify command
232
        
5971.1.5 by Jonathan Riddell
catch errors from gpgme, implement verify in dummy gpg strategies
233
        try:
5971.1.61 by Jonathan Riddell
make gpgme context global to class
234
            result = self.context.verify(signature, None, plain_output)
5971.1.5 by Jonathan Riddell
catch errors from gpgme, implement verify in dummy gpg strategies
235
        except gpgme.GpgmeError,error:
5971.1.33 by Jonathan Riddell
rename errors.VerifyFailed to errors.SignatureVerificationFailed
236
            raise errors.SignatureVerificationFailed(error[2])
5971.1.1 by Jonathan Riddell
add a verify command
237
5971.1.9 by Jonathan Riddell
add some tests
238
        if len(result) == 0:
5971.1.17 by Jonathan Riddell
add verbose option
239
            return SIGNATURE_NOT_VALID, None
5971.1.13 by Jonathan Riddell
return missing if not in acceptable keys
240
        fingerprint = result[0].fpr
241
        if self.acceptable_keys is not None:
242
            if not fingerprint in self.acceptable_keys:
5971.1.27 by Jonathan Riddell
verbose info for unknown keys
243
                return SIGNATURE_KEY_MISSING, fingerprint[-8:]
5971.1.30 by Jonathan Riddell
check the testament actually matches the commit when validating
244
        if testament != plain_output.getvalue():
245
            return SIGNATURE_NOT_VALID, None
5971.1.1 by Jonathan Riddell
add a verify command
246
        if result[0].summary & gpgme.SIGSUM_VALID:
5971.1.61 by Jonathan Riddell
make gpgme context global to class
247
            key = self.context.get_key(fingerprint)
5971.1.17 by Jonathan Riddell
add verbose option
248
            name = key.uids[0].name
5971.1.18 by Jonathan Riddell
add email to verbose output
249
            email = key.uids[0].email
250
            return SIGNATURE_VALID, name + " <" + email + ">"
5971.1.1 by Jonathan Riddell
add a verify command
251
        if result[0].summary & gpgme.SIGSUM_RED:
5971.1.17 by Jonathan Riddell
add verbose option
252
            return SIGNATURE_NOT_VALID, None
5971.1.1 by Jonathan Riddell
add a verify command
253
        if result[0].summary & gpgme.SIGSUM_KEY_MISSING:
5971.1.27 by Jonathan Riddell
verbose info for unknown keys
254
            return SIGNATURE_KEY_MISSING, fingerprint[-8:]
5971.1.14 by Jonathan Riddell
add test for set_acceptable_keys, accept non-trusted keys if specified as acceptable, import dummy key in tests so it works outside my machine
255
        #summary isn't set if sig is valid but key is untrusted
256
        if result[0].summary == 0 and self.acceptable_keys is not None:
257
            if fingerprint in self.acceptable_keys:
5971.1.17 by Jonathan Riddell
add verbose option
258
                return SIGNATURE_VALID, None
5971.1.14 by Jonathan Riddell
add test for set_acceptable_keys, accept non-trusted keys if specified as acceptable, import dummy key in tests so it works outside my machine
259
        else:
5971.1.17 by Jonathan Riddell
add verbose option
260
            return SIGNATURE_KEY_MISSING, None
5971.1.42 by Jonathan Riddell
fix string formatting
261
        raise errors.SignatureVerificationFailed("Unknown GnuPG key "\
262
                                                 "verification result")
5971.1.11 by Jonathan Riddell
add set_acceptable_keys() so user can specify which gpg keys can be used for verification
263
5971.1.69 by Jonathan Riddell
move some code from cmd_verify to gpg.set_acceptable_keys
264
    def set_acceptable_keys(self, command_line_input):
265
        """sets the acceptable keys for verifying with this GPGStrategy
266
        
267
        :param command_line_input: comma separated list of patterns from
268
                                command line
269
        :return: nothing
270
        """
271
        key_patterns = None
272
        acceptable_keys_config = self._config.acceptable_keys()
273
        try:
274
            if isinstance(acceptable_keys_config, unicode):
275
                acceptable_keys_config = str(acceptable_keys_config)
276
        except UnicodeEncodeError:
277
            raise errors.BzrCommandError('Only ASCII permitted in option names')
278
279
        if acceptable_keys_config is not None:
280
            key_patterns = acceptable_keys_config
281
        if command_line_input is not None: #command line overrides config
282
            key_patterns = command_line_input
283
        if key_patterns is not None:
284
            patterns = key_patterns.split(",")
285
286
            self.acceptable_keys = []
287
            for pattern in patterns:
288
                result = self.context.keylist(pattern)
289
                found_key = False
290
                for key in result:
291
                    found_key = True
292
                    self.acceptable_keys.append(key.subkeys[0].fpr)
293
                    trace.mutter("Added acceptable key: " + key.subkeys[0].fpr)
294
                if not found_key:
295
                    trace.note(i18n.gettext(
296
                            "No GnuPG key results for pattern: {}"
297
                                ).format(pattern))
5971.1.70 by Jonathan Riddell
move code which does verifications of revisions from cmd_verify_signatures to gpg.do_verifications
298
299
    def do_verifications(self, revisions, repository):
300
        """do verifications on a set of revisions
301
        
302
        :param revisions: list of revision ids to verify
303
        :param repository: repository object
304
        
305
        :return: count dictionary of results of each type,
306
                 result list for each revision,
307
                 boolean True if all results are verified successfully
308
        """
309
        count = {SIGNATURE_VALID: 0,
310
                 SIGNATURE_KEY_MISSING: 0,
311
                 SIGNATURE_NOT_VALID: 0,
312
                 SIGNATURE_NOT_SIGNED: 0}
313
        result = []
314
        all_verifiable = True
315
        for rev_id in revisions:
316
            verification_result, uid =\
317
                                repository.verify_revision(rev_id,self)
318
            result.append([rev_id, verification_result, uid])
319
            count[verification_result] += 1
320
            if verification_result != SIGNATURE_VALID:
321
                all_verifiable = False
322
        return (count, result, all_verifiable)
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
323
324
    def verbose_valid_message(self, result):
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
325
        """takes a verify result and returns list of signed commits strings"""
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
326
        signers = {}
327
        for rev_id, validity, uid in result:
328
            if validity == SIGNATURE_VALID:
329
                signers.setdefault(uid, 0)
330
                signers[uid] += 1
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
331
        result = []
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
332
        for uid, number in signers.items():
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
333
             result.append( i18n.ngettext("{0} signed {1} commit", 
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
334
                             "{0} signed {1} commits",
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
335
                             number).format(uid, number) )
336
        return result
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
337
338
339
    def verbose_not_valid_message(self, result, repo):
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
340
        """takes a verify result and returns list of not valid commit info"""
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
341
        signers = {}
342
        for rev_id, validity, empty in result:
343
            if validity == SIGNATURE_NOT_VALID:
344
                revision = repo.get_revision(rev_id)
345
                authors = ', '.join(revision.get_apparent_authors())
346
                signers.setdefault(authors, 0)
347
                signers[authors] += 1
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
348
        result = []
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
349
        for authors, number in signers.items():
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
350
            result.append( i18n.ngettext("{0} commit by author {1}", 
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
351
                                 "{0} commits by author {1}",
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
352
                                 number).format(number, authors) )
353
        return result
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
354
355
    def verbose_not_signed_message(self, result, repo):
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
356
        """takes a verify result and returns list of not signed commit info"""
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
357
        signers = {}
358
        for rev_id, validity, empty in result:
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
359
            if validity == SIGNATURE_NOT_SIGNED:
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
360
                revision = repo.get_revision(rev_id)
361
                authors = ', '.join(revision.get_apparent_authors())
362
                signers.setdefault(authors, 0)
363
                signers[authors] += 1
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
364
        result = []
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
365
        for authors, number in signers.items():
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
366
            result.append( i18n.ngettext("{0} commit by author {1}", 
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
367
                                 "{0} commits by author {1}",
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
368
                                 number).format(number, authors) )
369
        return result
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
370
371
    def verbose_missing_key_message(self, result):
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
372
        """takes a verify result and returns list of missing key info"""
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
373
        signers = {}
374
        for rev_id, validity, fingerprint in result:
375
            if validity == SIGNATURE_KEY_MISSING:
376
                signers.setdefault(fingerprint, 0)
377
                signers[fingerprint] += 1
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
378
        result = []
5971.1.71 by Jonathan Riddell
move some message code into gpg.py
379
        for fingerprint, number in signers.items():
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
380
            result.append( i18n.ngettext("Unknown key {0} signed {1} commit", 
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
381
                                 "Unknown key {0} signed {1} commits",
5971.1.75 by Jonathan Riddell
fix verbose messages, now return a list
382
                                 number).format(fingerprint, number) )
383
        return result
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
384
385
    def valid_commits_message(self, count):
5971.1.73 by Jonathan Riddell
document methods
386
        """returns message for number of commits"""
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
387
        return i18n.gettext("{0} commits with valid signatures").format(
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
388
                                        count[SIGNATURE_VALID])
389
390
    def unknown_key_message(self, count):
5971.1.73 by Jonathan Riddell
document methods
391
        """returns message for number of commits"""
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
392
        return i18n.ngettext("{0} commit with unknown key",
393
                             "{0} commits with unknown keys",
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
394
                             count[SIGNATURE_KEY_MISSING]).format(
395
                                        count[SIGNATURE_KEY_MISSING])
396
397
    def commit_not_valid_message(self, count):
5971.1.73 by Jonathan Riddell
document methods
398
        """returns message for number of commits"""
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
399
        return i18n.ngettext("{0} commit not valid",
400
                             "{0} commits not valid",
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
401
                             count[SIGNATURE_NOT_VALID]).format(
402
                                            count[SIGNATURE_NOT_VALID])
403
404
    def commit_not_signed_message(self, count):
5971.1.73 by Jonathan Riddell
document methods
405
        """returns message for number of commits"""
5971.1.74 by Jonathan Riddell
formatting changes necessary for qbzr
406
        return i18n.ngettext("{0} commit not signed",
407
                             "{0} commits not signed",
5971.1.72 by Jonathan Riddell
move all messages into gpg.py so they can be reused by other UIs
408
                             count[SIGNATURE_NOT_SIGNED]).format(
409
                                        count[SIGNATURE_NOT_SIGNED])