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