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