##// END OF EJS Templates
py3: handle keyword arguments in hgext/gpg.py...
Pulkit Goyal -
r34979:de1f0457 default
parent child Browse files
Show More
@@ -1,333 +1,335 b''
1 1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''commands to sign and verify changesets'''
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import binascii
11 11 import os
12 12 import tempfile
13 13
14 14 from mercurial.i18n import _
15 15 from mercurial import (
16 16 cmdutil,
17 17 error,
18 18 match,
19 19 node as hgnode,
20 20 pycompat,
21 21 registrar,
22 22 util,
23 23 )
24 24
25 25 cmdtable = {}
26 26 command = registrar.command(cmdtable)
27 27 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
28 28 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
29 29 # be specifying the version(s) of Mercurial they are tested with, or
30 30 # leave the attribute unspecified.
31 31 testedwith = 'ships-with-hg-core'
32 32
33 33 configtable = {}
34 34 configitem = registrar.configitem(configtable)
35 35
36 36 configitem('gpg', 'cmd',
37 37 default='gpg',
38 38 )
39 39 configitem('gpg', 'key',
40 40 default=None,
41 41 )
42 42 configitem('gpg', '.*',
43 43 default=None,
44 44 generic=True,
45 45 )
46 46
47 47 class gpg(object):
48 48 def __init__(self, path, key=None):
49 49 self.path = path
50 50 self.key = (key and " --local-user \"%s\"" % key) or ""
51 51
52 52 def sign(self, data):
53 53 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
54 54 return util.filter(data, gpgcmd)
55 55
56 56 def verify(self, data, sig):
57 57 """ returns of the good and bad signatures"""
58 58 sigfile = datafile = None
59 59 try:
60 60 # create temporary files
61 61 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
62 62 fp = os.fdopen(fd, pycompat.sysstr('wb'))
63 63 fp.write(sig)
64 64 fp.close()
65 65 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
66 66 fp = os.fdopen(fd, pycompat.sysstr('wb'))
67 67 fp.write(data)
68 68 fp.close()
69 69 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
70 70 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
71 71 ret = util.filter("", gpgcmd)
72 72 finally:
73 73 for f in (sigfile, datafile):
74 74 try:
75 75 if f:
76 76 os.unlink(f)
77 77 except OSError:
78 78 pass
79 79 keys = []
80 80 key, fingerprint = None, None
81 81 for l in ret.splitlines():
82 82 # see DETAILS in the gnupg documentation
83 83 # filter the logger output
84 84 if not l.startswith("[GNUPG:]"):
85 85 continue
86 86 l = l[9:]
87 87 if l.startswith("VALIDSIG"):
88 88 # fingerprint of the primary key
89 89 fingerprint = l.split()[10]
90 90 elif l.startswith("ERRSIG"):
91 91 key = l.split(" ", 3)[:2]
92 92 key.append("")
93 93 fingerprint = None
94 94 elif (l.startswith("GOODSIG") or
95 95 l.startswith("EXPSIG") or
96 96 l.startswith("EXPKEYSIG") or
97 97 l.startswith("BADSIG")):
98 98 if key is not None:
99 99 keys.append(key + [fingerprint])
100 100 key = l.split(" ", 2)
101 101 fingerprint = None
102 102 if key is not None:
103 103 keys.append(key + [fingerprint])
104 104 return keys
105 105
106 106 def newgpg(ui, **opts):
107 107 """create a new gpg instance"""
108 108 gpgpath = ui.config("gpg", "cmd")
109 gpgkey = opts.get('key')
109 gpgkey = opts.get(r'key')
110 110 if not gpgkey:
111 111 gpgkey = ui.config("gpg", "key")
112 112 return gpg(gpgpath, gpgkey)
113 113
114 114 def sigwalk(repo):
115 115 """
116 116 walk over every sigs, yields a couple
117 117 ((node, version, sig), (filename, linenumber))
118 118 """
119 119 def parsefile(fileiter, context):
120 120 ln = 1
121 121 for l in fileiter:
122 122 if not l:
123 123 continue
124 124 yield (l.split(" ", 2), (context, ln))
125 125 ln += 1
126 126
127 127 # read the heads
128 128 fl = repo.file(".hgsigs")
129 129 for r in reversed(fl.heads()):
130 130 fn = ".hgsigs|%s" % hgnode.short(r)
131 131 for item in parsefile(fl.read(r).splitlines(), fn):
132 132 yield item
133 133 try:
134 134 # read local signatures
135 135 fn = "localsigs"
136 136 for item in parsefile(repo.vfs(fn), fn):
137 137 yield item
138 138 except IOError:
139 139 pass
140 140
141 141 def getkeys(ui, repo, mygpg, sigdata, context):
142 142 """get the keys who signed a data"""
143 143 fn, ln = context
144 144 node, version, sig = sigdata
145 145 prefix = "%s:%d" % (fn, ln)
146 146 node = hgnode.bin(node)
147 147
148 148 data = node2txt(repo, node, version)
149 149 sig = binascii.a2b_base64(sig)
150 150 keys = mygpg.verify(data, sig)
151 151
152 152 validkeys = []
153 153 # warn for expired key and/or sigs
154 154 for key in keys:
155 155 if key[0] == "ERRSIG":
156 156 ui.write(_("%s Unknown key ID \"%s\"\n")
157 157 % (prefix, shortkey(ui, key[1][:15])))
158 158 continue
159 159 if key[0] == "BADSIG":
160 160 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
161 161 continue
162 162 if key[0] == "EXPSIG":
163 163 ui.write(_("%s Note: Signature has expired"
164 164 " (signed by: \"%s\")\n") % (prefix, key[2]))
165 165 elif key[0] == "EXPKEYSIG":
166 166 ui.write(_("%s Note: This key has expired"
167 167 " (signed by: \"%s\")\n") % (prefix, key[2]))
168 168 validkeys.append((key[1], key[2], key[3]))
169 169 return validkeys
170 170
171 171 @command("sigs", [], _('hg sigs'))
172 172 def sigs(ui, repo):
173 173 """list signed changesets"""
174 174 mygpg = newgpg(ui)
175 175 revs = {}
176 176
177 177 for data, context in sigwalk(repo):
178 178 node, version, sig = data
179 179 fn, ln = context
180 180 try:
181 181 n = repo.lookup(node)
182 182 except KeyError:
183 183 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
184 184 continue
185 185 r = repo.changelog.rev(n)
186 186 keys = getkeys(ui, repo, mygpg, data, context)
187 187 if not keys:
188 188 continue
189 189 revs.setdefault(r, [])
190 190 revs[r].extend(keys)
191 191 for rev in sorted(revs, reverse=True):
192 192 for k in revs[rev]:
193 193 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
194 194 ui.write("%-30s %s\n" % (keystr(ui, k), r))
195 195
196 196 @command("sigcheck", [], _('hg sigcheck REV'))
197 197 def sigcheck(ui, repo, rev):
198 198 """verify all the signatures there may be for a particular revision"""
199 199 mygpg = newgpg(ui)
200 200 rev = repo.lookup(rev)
201 201 hexrev = hgnode.hex(rev)
202 202 keys = []
203 203
204 204 for data, context in sigwalk(repo):
205 205 node, version, sig = data
206 206 if node == hexrev:
207 207 k = getkeys(ui, repo, mygpg, data, context)
208 208 if k:
209 209 keys.extend(k)
210 210
211 211 if not keys:
212 212 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
213 213 return
214 214
215 215 # print summary
216 216 ui.write(_("%s is signed by:\n") % hgnode.short(rev))
217 217 for key in keys:
218 218 ui.write(" %s\n" % keystr(ui, key))
219 219
220 220 def keystr(ui, key):
221 221 """associate a string to a key (username, comment)"""
222 222 keyid, user, fingerprint = key
223 223 comment = ui.config("gpg", fingerprint)
224 224 if comment:
225 225 return "%s (%s)" % (user, comment)
226 226 else:
227 227 return user
228 228
229 229 @command("sign",
230 230 [('l', 'local', None, _('make the signature local')),
231 231 ('f', 'force', None, _('sign even if the sigfile is modified')),
232 232 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
233 233 ('k', 'key', '',
234 234 _('the key id to sign with'), _('ID')),
235 235 ('m', 'message', '',
236 236 _('use text as commit message'), _('TEXT')),
237 237 ('e', 'edit', False, _('invoke editor on commit messages')),
238 238 ] + cmdutil.commitopts2,
239 239 _('hg sign [OPTION]... [REV]...'))
240 240 def sign(ui, repo, *revs, **opts):
241 241 """add a signature for the current or given revision
242 242
243 243 If no revision is given, the parent of the working directory is used,
244 244 or tip if no revision is checked out.
245 245
246 246 The ``gpg.cmd`` config setting can be used to specify the command
247 247 to run. A default key can be specified with ``gpg.key``.
248 248
249 249 See :hg:`help dates` for a list of formats valid for -d/--date.
250 250 """
251 251 with repo.wlock():
252 252 return _dosign(ui, repo, *revs, **opts)
253 253
254 254 def _dosign(ui, repo, *revs, **opts):
255 255 mygpg = newgpg(ui, **opts)
256 opts = pycompat.byteskwargs(opts)
256 257 sigver = "0"
257 258 sigmessage = ""
258 259
259 260 date = opts.get('date')
260 261 if date:
261 262 opts['date'] = util.parsedate(date)
262 263
263 264 if revs:
264 265 nodes = [repo.lookup(n) for n in revs]
265 266 else:
266 267 nodes = [node for node in repo.dirstate.parents()
267 268 if node != hgnode.nullid]
268 269 if len(nodes) > 1:
269 270 raise error.Abort(_('uncommitted merge - please provide a '
270 271 'specific revision'))
271 272 if not nodes:
272 273 nodes = [repo.changelog.tip()]
273 274
274 275 for n in nodes:
275 276 hexnode = hgnode.hex(n)
276 277 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
277 278 hgnode.short(n)))
278 279 # build data
279 280 data = node2txt(repo, n, sigver)
280 281 sig = mygpg.sign(data)
281 282 if not sig:
282 283 raise error.Abort(_("error while signing"))
283 284 sig = binascii.b2a_base64(sig)
284 285 sig = sig.replace("\n", "")
285 286 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
286 287
287 288 # write it
288 289 if opts['local']:
289 290 repo.vfs.append("localsigs", sigmessage)
290 291 return
291 292
292 293 if not opts["force"]:
293 294 msigs = match.exact(repo.root, '', ['.hgsigs'])
294 295 if any(repo.status(match=msigs, unknown=True, ignored=True)):
295 296 raise error.Abort(_("working copy of .hgsigs is changed "),
296 297 hint=_("please commit .hgsigs manually"))
297 298
298 299 sigsfile = repo.wvfs(".hgsigs", "ab")
299 300 sigsfile.write(sigmessage)
300 301 sigsfile.close()
301 302
302 303 if '.hgsigs' not in repo.dirstate:
303 304 repo[None].add([".hgsigs"])
304 305
305 306 if opts["no_commit"]:
306 307 return
307 308
308 309 message = opts['message']
309 310 if not message:
310 311 # we don't translate commit messages
311 312 message = "\n".join(["Added signature for changeset %s"
312 313 % hgnode.short(n)
313 314 for n in nodes])
314 315 try:
315 editor = cmdutil.getcommiteditor(editform='gpg.sign', **opts)
316 editor = cmdutil.getcommiteditor(editform='gpg.sign',
317 **pycompat.strkwargs(opts))
316 318 repo.commit(message, opts['user'], opts['date'], match=msigs,
317 319 editor=editor)
318 320 except ValueError as inst:
319 321 raise error.Abort(str(inst))
320 322
321 323 def shortkey(ui, key):
322 324 if len(key) != 16:
323 325 ui.debug("key ID \"%s\" format error\n" % key)
324 326 return key
325 327
326 328 return key[-8:]
327 329
328 330 def node2txt(repo, node, ver):
329 331 """map a manifest into some text"""
330 332 if ver == "0":
331 333 return "%s\n" % hgnode.hex(node)
332 334 else:
333 335 raise error.Abort(_("unknown signature version"))
General Comments 0
You need to be logged in to leave comments. Login now