##// END OF EJS Templates
gpg: use match.exact rather than files for commit
Matt Mackall -
r8701:02a12835 default
parent child Browse files
Show More
@@ -1,282 +1,283 b''
1 # GnuPG signing extension for Mercurial
1 # GnuPG signing extension for Mercurial
2 #
2 #
3 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
3 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 import os, tempfile, binascii
8 import os, tempfile, binascii
9 from mercurial import util, commands
9 from mercurial import util, commands, match
10 from mercurial import node as hgnode
10 from mercurial import node as hgnode
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 class gpg:
13 class gpg:
14 def __init__(self, path, key=None):
14 def __init__(self, path, key=None):
15 self.path = path
15 self.path = path
16 self.key = (key and " --local-user \"%s\"" % key) or ""
16 self.key = (key and " --local-user \"%s\"" % key) or ""
17
17
18 def sign(self, data):
18 def sign(self, data):
19 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
19 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
20 return util.filter(data, gpgcmd)
20 return util.filter(data, gpgcmd)
21
21
22 def verify(self, data, sig):
22 def verify(self, data, sig):
23 """ returns of the good and bad signatures"""
23 """ returns of the good and bad signatures"""
24 sigfile = datafile = None
24 sigfile = datafile = None
25 try:
25 try:
26 # create temporary files
26 # create temporary files
27 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
27 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
28 fp = os.fdopen(fd, 'wb')
28 fp = os.fdopen(fd, 'wb')
29 fp.write(sig)
29 fp.write(sig)
30 fp.close()
30 fp.close()
31 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
31 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
32 fp = os.fdopen(fd, 'wb')
32 fp = os.fdopen(fd, 'wb')
33 fp.write(data)
33 fp.write(data)
34 fp.close()
34 fp.close()
35 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
35 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
36 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
36 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
37 ret = util.filter("", gpgcmd)
37 ret = util.filter("", gpgcmd)
38 finally:
38 finally:
39 for f in (sigfile, datafile):
39 for f in (sigfile, datafile):
40 try:
40 try:
41 if f: os.unlink(f)
41 if f: os.unlink(f)
42 except: pass
42 except: pass
43 keys = []
43 keys = []
44 key, fingerprint = None, None
44 key, fingerprint = None, None
45 err = ""
45 err = ""
46 for l in ret.splitlines():
46 for l in ret.splitlines():
47 # see DETAILS in the gnupg documentation
47 # see DETAILS in the gnupg documentation
48 # filter the logger output
48 # filter the logger output
49 if not l.startswith("[GNUPG:]"):
49 if not l.startswith("[GNUPG:]"):
50 continue
50 continue
51 l = l[9:]
51 l = l[9:]
52 if l.startswith("ERRSIG"):
52 if l.startswith("ERRSIG"):
53 err = _("error while verifying signature")
53 err = _("error while verifying signature")
54 break
54 break
55 elif l.startswith("VALIDSIG"):
55 elif l.startswith("VALIDSIG"):
56 # fingerprint of the primary key
56 # fingerprint of the primary key
57 fingerprint = l.split()[10]
57 fingerprint = l.split()[10]
58 elif (l.startswith("GOODSIG") or
58 elif (l.startswith("GOODSIG") or
59 l.startswith("EXPSIG") or
59 l.startswith("EXPSIG") or
60 l.startswith("EXPKEYSIG") or
60 l.startswith("EXPKEYSIG") or
61 l.startswith("BADSIG")):
61 l.startswith("BADSIG")):
62 if key is not None:
62 if key is not None:
63 keys.append(key + [fingerprint])
63 keys.append(key + [fingerprint])
64 key = l.split(" ", 2)
64 key = l.split(" ", 2)
65 fingerprint = None
65 fingerprint = None
66 if err:
66 if err:
67 return err, []
67 return err, []
68 if key is not None:
68 if key is not None:
69 keys.append(key + [fingerprint])
69 keys.append(key + [fingerprint])
70 return err, keys
70 return err, keys
71
71
72 def newgpg(ui, **opts):
72 def newgpg(ui, **opts):
73 """create a new gpg instance"""
73 """create a new gpg instance"""
74 gpgpath = ui.config("gpg", "cmd", "gpg")
74 gpgpath = ui.config("gpg", "cmd", "gpg")
75 gpgkey = opts.get('key')
75 gpgkey = opts.get('key')
76 if not gpgkey:
76 if not gpgkey:
77 gpgkey = ui.config("gpg", "key", None)
77 gpgkey = ui.config("gpg", "key", None)
78 return gpg(gpgpath, gpgkey)
78 return gpg(gpgpath, gpgkey)
79
79
80 def sigwalk(repo):
80 def sigwalk(repo):
81 """
81 """
82 walk over every sigs, yields a couple
82 walk over every sigs, yields a couple
83 ((node, version, sig), (filename, linenumber))
83 ((node, version, sig), (filename, linenumber))
84 """
84 """
85 def parsefile(fileiter, context):
85 def parsefile(fileiter, context):
86 ln = 1
86 ln = 1
87 for l in fileiter:
87 for l in fileiter:
88 if not l:
88 if not l:
89 continue
89 continue
90 yield (l.split(" ", 2), (context, ln))
90 yield (l.split(" ", 2), (context, ln))
91 ln +=1
91 ln +=1
92
92
93 # read the heads
93 # read the heads
94 fl = repo.file(".hgsigs")
94 fl = repo.file(".hgsigs")
95 for r in reversed(fl.heads()):
95 for r in reversed(fl.heads()):
96 fn = ".hgsigs|%s" % hgnode.short(r)
96 fn = ".hgsigs|%s" % hgnode.short(r)
97 for item in parsefile(fl.read(r).splitlines(), fn):
97 for item in parsefile(fl.read(r).splitlines(), fn):
98 yield item
98 yield item
99 try:
99 try:
100 # read local signatures
100 # read local signatures
101 fn = "localsigs"
101 fn = "localsigs"
102 for item in parsefile(repo.opener(fn), fn):
102 for item in parsefile(repo.opener(fn), fn):
103 yield item
103 yield item
104 except IOError:
104 except IOError:
105 pass
105 pass
106
106
107 def getkeys(ui, repo, mygpg, sigdata, context):
107 def getkeys(ui, repo, mygpg, sigdata, context):
108 """get the keys who signed a data"""
108 """get the keys who signed a data"""
109 fn, ln = context
109 fn, ln = context
110 node, version, sig = sigdata
110 node, version, sig = sigdata
111 prefix = "%s:%d" % (fn, ln)
111 prefix = "%s:%d" % (fn, ln)
112 node = hgnode.bin(node)
112 node = hgnode.bin(node)
113
113
114 data = node2txt(repo, node, version)
114 data = node2txt(repo, node, version)
115 sig = binascii.a2b_base64(sig)
115 sig = binascii.a2b_base64(sig)
116 err, keys = mygpg.verify(data, sig)
116 err, keys = mygpg.verify(data, sig)
117 if err:
117 if err:
118 ui.warn("%s:%d %s\n" % (fn, ln , err))
118 ui.warn("%s:%d %s\n" % (fn, ln , err))
119 return None
119 return None
120
120
121 validkeys = []
121 validkeys = []
122 # warn for expired key and/or sigs
122 # warn for expired key and/or sigs
123 for key in keys:
123 for key in keys:
124 if key[0] == "BADSIG":
124 if key[0] == "BADSIG":
125 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
125 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
126 continue
126 continue
127 if key[0] == "EXPSIG":
127 if key[0] == "EXPSIG":
128 ui.write(_("%s Note: Signature has expired"
128 ui.write(_("%s Note: Signature has expired"
129 " (signed by: \"%s\")\n") % (prefix, key[2]))
129 " (signed by: \"%s\")\n") % (prefix, key[2]))
130 elif key[0] == "EXPKEYSIG":
130 elif key[0] == "EXPKEYSIG":
131 ui.write(_("%s Note: This key has expired"
131 ui.write(_("%s Note: This key has expired"
132 " (signed by: \"%s\")\n") % (prefix, key[2]))
132 " (signed by: \"%s\")\n") % (prefix, key[2]))
133 validkeys.append((key[1], key[2], key[3]))
133 validkeys.append((key[1], key[2], key[3]))
134 return validkeys
134 return validkeys
135
135
136 def sigs(ui, repo):
136 def sigs(ui, repo):
137 """list signed changesets"""
137 """list signed changesets"""
138 mygpg = newgpg(ui)
138 mygpg = newgpg(ui)
139 revs = {}
139 revs = {}
140
140
141 for data, context in sigwalk(repo):
141 for data, context in sigwalk(repo):
142 node, version, sig = data
142 node, version, sig = data
143 fn, ln = context
143 fn, ln = context
144 try:
144 try:
145 n = repo.lookup(node)
145 n = repo.lookup(node)
146 except KeyError:
146 except KeyError:
147 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
147 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
148 continue
148 continue
149 r = repo.changelog.rev(n)
149 r = repo.changelog.rev(n)
150 keys = getkeys(ui, repo, mygpg, data, context)
150 keys = getkeys(ui, repo, mygpg, data, context)
151 if not keys:
151 if not keys:
152 continue
152 continue
153 revs.setdefault(r, [])
153 revs.setdefault(r, [])
154 revs[r].extend(keys)
154 revs[r].extend(keys)
155 for rev in sorted(revs, reverse=True):
155 for rev in sorted(revs, reverse=True):
156 for k in revs[rev]:
156 for k in revs[rev]:
157 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
157 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
158 ui.write("%-30s %s\n" % (keystr(ui, k), r))
158 ui.write("%-30s %s\n" % (keystr(ui, k), r))
159
159
160 def check(ui, repo, rev):
160 def check(ui, repo, rev):
161 """verify all the signatures there may be for a particular revision"""
161 """verify all the signatures there may be for a particular revision"""
162 mygpg = newgpg(ui)
162 mygpg = newgpg(ui)
163 rev = repo.lookup(rev)
163 rev = repo.lookup(rev)
164 hexrev = hgnode.hex(rev)
164 hexrev = hgnode.hex(rev)
165 keys = []
165 keys = []
166
166
167 for data, context in sigwalk(repo):
167 for data, context in sigwalk(repo):
168 node, version, sig = data
168 node, version, sig = data
169 if node == hexrev:
169 if node == hexrev:
170 k = getkeys(ui, repo, mygpg, data, context)
170 k = getkeys(ui, repo, mygpg, data, context)
171 if k:
171 if k:
172 keys.extend(k)
172 keys.extend(k)
173
173
174 if not keys:
174 if not keys:
175 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
175 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
176 return
176 return
177
177
178 # print summary
178 # print summary
179 ui.write("%s is signed by:\n" % hgnode.short(rev))
179 ui.write("%s is signed by:\n" % hgnode.short(rev))
180 for key in keys:
180 for key in keys:
181 ui.write(" %s\n" % keystr(ui, key))
181 ui.write(" %s\n" % keystr(ui, key))
182
182
183 def keystr(ui, key):
183 def keystr(ui, key):
184 """associate a string to a key (username, comment)"""
184 """associate a string to a key (username, comment)"""
185 keyid, user, fingerprint = key
185 keyid, user, fingerprint = key
186 comment = ui.config("gpg", fingerprint, None)
186 comment = ui.config("gpg", fingerprint, None)
187 if comment:
187 if comment:
188 return "%s (%s)" % (user, comment)
188 return "%s (%s)" % (user, comment)
189 else:
189 else:
190 return user
190 return user
191
191
192 def sign(ui, repo, *revs, **opts):
192 def sign(ui, repo, *revs, **opts):
193 """add a signature for the current or given revision
193 """add a signature for the current or given revision
194
194
195 If no revision is given, the parent of the working directory is used,
195 If no revision is given, the parent of the working directory is used,
196 or tip if no revision is checked out.
196 or tip if no revision is checked out.
197
197
198 See 'hg help dates' for a list of formats valid for -d/--date.
198 See 'hg help dates' for a list of formats valid for -d/--date.
199 """
199 """
200
200
201 mygpg = newgpg(ui, **opts)
201 mygpg = newgpg(ui, **opts)
202 sigver = "0"
202 sigver = "0"
203 sigmessage = ""
203 sigmessage = ""
204
204
205 date = opts.get('date')
205 date = opts.get('date')
206 if date:
206 if date:
207 opts['date'] = util.parsedate(date)
207 opts['date'] = util.parsedate(date)
208
208
209 if revs:
209 if revs:
210 nodes = [repo.lookup(n) for n in revs]
210 nodes = [repo.lookup(n) for n in revs]
211 else:
211 else:
212 nodes = [node for node in repo.dirstate.parents()
212 nodes = [node for node in repo.dirstate.parents()
213 if node != hgnode.nullid]
213 if node != hgnode.nullid]
214 if len(nodes) > 1:
214 if len(nodes) > 1:
215 raise util.Abort(_('uncommitted merge - please provide a '
215 raise util.Abort(_('uncommitted merge - please provide a '
216 'specific revision'))
216 'specific revision'))
217 if not nodes:
217 if not nodes:
218 nodes = [repo.changelog.tip()]
218 nodes = [repo.changelog.tip()]
219
219
220 for n in nodes:
220 for n in nodes:
221 hexnode = hgnode.hex(n)
221 hexnode = hgnode.hex(n)
222 ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
222 ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
223 hgnode.short(n)))
223 hgnode.short(n)))
224 # build data
224 # build data
225 data = node2txt(repo, n, sigver)
225 data = node2txt(repo, n, sigver)
226 sig = mygpg.sign(data)
226 sig = mygpg.sign(data)
227 if not sig:
227 if not sig:
228 raise util.Abort(_("Error while signing"))
228 raise util.Abort(_("Error while signing"))
229 sig = binascii.b2a_base64(sig)
229 sig = binascii.b2a_base64(sig)
230 sig = sig.replace("\n", "")
230 sig = sig.replace("\n", "")
231 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
231 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
232
232
233 # write it
233 # write it
234 if opts['local']:
234 if opts['local']:
235 repo.opener("localsigs", "ab").write(sigmessage)
235 repo.opener("localsigs", "ab").write(sigmessage)
236 return
236 return
237
237
238 for x in repo.status(unknown=True)[:5]:
238 for x in repo.status(unknown=True)[:5]:
239 if ".hgsigs" in x and not opts["force"]:
239 if ".hgsigs" in x and not opts["force"]:
240 raise util.Abort(_("working copy of .hgsigs is changed "
240 raise util.Abort(_("working copy of .hgsigs is changed "
241 "(please commit .hgsigs manually "
241 "(please commit .hgsigs manually "
242 "or use --force)"))
242 "or use --force)"))
243
243
244 repo.wfile(".hgsigs", "ab").write(sigmessage)
244 repo.wfile(".hgsigs", "ab").write(sigmessage)
245
245
246 if '.hgsigs' not in repo.dirstate:
246 if '.hgsigs' not in repo.dirstate:
247 repo.add([".hgsigs"])
247 repo.add([".hgsigs"])
248
248
249 if opts["no_commit"]:
249 if opts["no_commit"]:
250 return
250 return
251
251
252 message = opts['message']
252 message = opts['message']
253 if not message:
253 if not message:
254 message = "\n".join([_("Added signature for changeset %s")
254 message = "\n".join([_("Added signature for changeset %s")
255 % hgnode.short(n)
255 % hgnode.short(n)
256 for n in nodes])
256 for n in nodes])
257 try:
257 try:
258 repo.commit([".hgsigs"], message, opts['user'], opts['date'])
258 m = match.exact(['.hgsigs'])
259 repo.commit(None, message, opts['user'], opts['date'], match=m)
259 except ValueError, inst:
260 except ValueError, inst:
260 raise util.Abort(str(inst))
261 raise util.Abort(str(inst))
261
262
262 def node2txt(repo, node, ver):
263 def node2txt(repo, node, ver):
263 """map a manifest into some text"""
264 """map a manifest into some text"""
264 if ver == "0":
265 if ver == "0":
265 return "%s\n" % hgnode.hex(node)
266 return "%s\n" % hgnode.hex(node)
266 else:
267 else:
267 raise util.Abort(_("unknown signature version"))
268 raise util.Abort(_("unknown signature version"))
268
269
269 cmdtable = {
270 cmdtable = {
270 "sign":
271 "sign":
271 (sign,
272 (sign,
272 [('l', 'local', None, _('make the signature local')),
273 [('l', 'local', None, _('make the signature local')),
273 ('f', 'force', None, _('sign even if the sigfile is modified')),
274 ('f', 'force', None, _('sign even if the sigfile is modified')),
274 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
275 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
275 ('k', 'key', '', _('the key id to sign with')),
276 ('k', 'key', '', _('the key id to sign with')),
276 ('m', 'message', '', _('commit message')),
277 ('m', 'message', '', _('commit message')),
277 ] + commands.commitopts2,
278 ] + commands.commitopts2,
278 _('hg sign [OPTION]... [REVISION]...')),
279 _('hg sign [OPTION]... [REVISION]...')),
279 "sigcheck": (check, [], _('hg sigcheck REVISION')),
280 "sigcheck": (check, [], _('hg sigcheck REVISION')),
280 "sigs": (sigs, [], _('hg sigs')),
281 "sigs": (sigs, [], _('hg sigs')),
281 }
282 }
282
283
General Comments 0
You need to be logged in to leave comments. Login now