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