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