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