##// END OF EJS Templates
gpg: use the same log message format as hg tag
Benoit Boissinot -
r5475:3aa5c458 default
parent child Browse files
Show More
@@ -1,279 +1,279 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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 import os, tempfile, binascii
9 9 from mercurial import util, commands
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 fl = repo.file(".hgsigs")
94 94 h = fl.heads()
95 95 h.reverse()
96 96 # read the heads
97 97 for r in h:
98 98 fn = ".hgsigs|%s" % hgnode.short(r)
99 99 for item in parsefile(fl.read(r).splitlines(), fn):
100 100 yield item
101 101 try:
102 102 # read local signatures
103 103 fn = "localsigs"
104 104 for item in parsefile(repo.opener(fn), fn):
105 105 yield item
106 106 except IOError:
107 107 pass
108 108
109 109 def getkeys(ui, repo, mygpg, sigdata, context):
110 110 """get the keys who signed a data"""
111 111 fn, ln = context
112 112 node, version, sig = sigdata
113 113 prefix = "%s:%d" % (fn, ln)
114 114 node = hgnode.bin(node)
115 115
116 116 data = node2txt(repo, node, version)
117 117 sig = binascii.a2b_base64(sig)
118 118 err, keys = mygpg.verify(data, sig)
119 119 if err:
120 120 ui.warn("%s:%d %s\n" % (fn, ln , err))
121 121 return None
122 122
123 123 validkeys = []
124 124 # warn for expired key and/or sigs
125 125 for key in keys:
126 126 if key[0] == "BADSIG":
127 127 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
128 128 continue
129 129 if key[0] == "EXPSIG":
130 130 ui.write(_("%s Note: Signature has expired"
131 131 " (signed by: \"%s\")\n") % (prefix, key[2]))
132 132 elif key[0] == "EXPKEYSIG":
133 133 ui.write(_("%s Note: This key has expired"
134 134 " (signed by: \"%s\")\n") % (prefix, key[2]))
135 135 validkeys.append((key[1], key[2], key[3]))
136 136 return validkeys
137 137
138 138 def sigs(ui, repo):
139 139 """list signed changesets"""
140 140 mygpg = newgpg(ui)
141 141 revs = {}
142 142
143 143 for data, context in sigwalk(repo):
144 144 node, version, sig = data
145 145 fn, ln = context
146 146 try:
147 147 n = repo.lookup(node)
148 148 except KeyError:
149 149 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
150 150 continue
151 151 r = repo.changelog.rev(n)
152 152 keys = getkeys(ui, repo, mygpg, data, context)
153 153 if not keys:
154 154 continue
155 155 revs.setdefault(r, [])
156 156 revs[r].extend(keys)
157 157 nodes = list(revs)
158 158 nodes.reverse()
159 159 for rev in nodes:
160 160 for k in revs[rev]:
161 161 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
162 162 ui.write("%-30s %s\n" % (keystr(ui, k), r))
163 163
164 164 def check(ui, repo, rev):
165 165 """verify all the signatures there may be for a particular revision"""
166 166 mygpg = newgpg(ui)
167 167 rev = repo.lookup(rev)
168 168 hexrev = hgnode.hex(rev)
169 169 keys = []
170 170
171 171 for data, context in sigwalk(repo):
172 172 node, version, sig = data
173 173 if node == hexrev:
174 174 k = getkeys(ui, repo, mygpg, data, context)
175 175 if k:
176 176 keys.extend(k)
177 177
178 178 if not keys:
179 179 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
180 180 return
181 181
182 182 # print summary
183 183 ui.write("%s is signed by:\n" % hgnode.short(rev))
184 184 for key in keys:
185 185 ui.write(" %s\n" % keystr(ui, key))
186 186
187 187 def keystr(ui, key):
188 188 """associate a string to a key (username, comment)"""
189 189 keyid, user, fingerprint = key
190 190 comment = ui.config("gpg", fingerprint, None)
191 191 if comment:
192 192 return "%s (%s)" % (user, comment)
193 193 else:
194 194 return user
195 195
196 196 def sign(ui, repo, *revs, **opts):
197 197 """add a signature for the current or given revision
198 198
199 199 If no revision is given, the parent of the working directory is used,
200 200 or tip if no revision is checked out.
201 201 """
202 202
203 203 mygpg = newgpg(ui, **opts)
204 204 sigver = "0"
205 205 sigmessage = ""
206 206 if revs:
207 207 nodes = [repo.lookup(n) for n in revs]
208 208 else:
209 209 nodes = [node for node in repo.dirstate.parents()
210 210 if node != hgnode.nullid]
211 211 if len(nodes) > 1:
212 212 raise util.Abort(_('uncommitted merge - please provide a '
213 213 'specific revision'))
214 214 if not nodes:
215 215 nodes = [repo.changelog.tip()]
216 216
217 217 for n in nodes:
218 218 hexnode = hgnode.hex(n)
219 219 ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
220 220 hgnode.short(n)))
221 221 # build data
222 222 data = node2txt(repo, n, sigver)
223 223 sig = mygpg.sign(data)
224 224 if not sig:
225 225 raise util.Abort(_("Error while signing"))
226 226 sig = binascii.b2a_base64(sig)
227 227 sig = sig.replace("\n", "")
228 228 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
229 229
230 230 # write it
231 231 if opts['local']:
232 232 repo.opener("localsigs", "ab").write(sigmessage)
233 233 return
234 234
235 235 for x in repo.status()[:5]:
236 236 if ".hgsigs" in x and not opts["force"]:
237 237 raise util.Abort(_("working copy of .hgsigs is changed "
238 238 "(please commit .hgsigs manually "
239 239 "or use --force)"))
240 240
241 241 repo.wfile(".hgsigs", "ab").write(sigmessage)
242 242
243 243 if '.hgsigs' not in repo.dirstate:
244 244 repo.add([".hgsigs"])
245 245
246 246 if opts["no_commit"]:
247 247 return
248 248
249 249 message = opts['message']
250 250 if not message:
251 251 message = "\n".join([_("Added signature for changeset %s")
252 % hgnode.hex(n)
252 % hgnode.short(n)
253 253 for n in nodes])
254 254 try:
255 255 repo.commit([".hgsigs"], message, opts['user'], opts['date'])
256 256 except ValueError, inst:
257 257 raise util.Abort(str(inst))
258 258
259 259 def node2txt(repo, node, ver):
260 260 """map a manifest into some text"""
261 261 if ver == "0":
262 262 return "%s\n" % hgnode.hex(node)
263 263 else:
264 264 raise util.Abort(_("unknown signature version"))
265 265
266 266 cmdtable = {
267 267 "sign":
268 268 (sign,
269 269 [('l', 'local', None, _('make the signature local')),
270 270 ('f', 'force', None, _('sign even if the sigfile is modified')),
271 271 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
272 272 ('k', 'key', '', _('the key id to sign with')),
273 273 ('m', 'message', '', _('commit message')),
274 274 ] + commands.commitopts2,
275 275 _('hg sign [OPTION]... [REVISION]...')),
276 276 "sigcheck": (check, [], _('hg sigcheck REVISION')),
277 277 "sigs": (sigs, [], _('hg sigs')),
278 278 }
279 279
General Comments 0
You need to be logged in to leave comments. Login now