34 testedwith = 'ships-with-hg-core' |
34 testedwith = 'ships-with-hg-core' |
35 |
35 |
36 configtable = {} |
36 configtable = {} |
37 configitem = registrar.configitem(configtable) |
37 configitem = registrar.configitem(configtable) |
38 |
38 |
39 configitem('gpg', 'cmd', |
39 configitem( |
40 default='gpg', |
40 'gpg', 'cmd', default='gpg', |
41 ) |
41 ) |
42 configitem('gpg', 'key', |
42 configitem( |
43 default=None, |
43 'gpg', 'key', default=None, |
44 ) |
44 ) |
45 configitem('gpg', '.*', |
45 configitem( |
46 default=None, |
46 'gpg', '.*', default=None, generic=True, |
47 generic=True, |
|
48 ) |
47 ) |
49 |
48 |
50 # Custom help category |
49 # Custom help category |
51 _HELP_CATEGORY = 'gpg' |
50 _HELP_CATEGORY = 'gpg' |
52 help.CATEGORY_ORDER.insert( |
51 help.CATEGORY_ORDER.insert( |
53 help.CATEGORY_ORDER.index(registrar.command.CATEGORY_HELP), |
52 help.CATEGORY_ORDER.index(registrar.command.CATEGORY_HELP), _HELP_CATEGORY |
54 _HELP_CATEGORY |
|
55 ) |
53 ) |
56 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'Signing changes (GPG)' |
54 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'Signing changes (GPG)' |
|
55 |
57 |
56 |
58 class gpg(object): |
57 class gpg(object): |
59 def __init__(self, path, key=None): |
58 def __init__(self, path, key=None): |
60 self.path = path |
59 self.path = path |
61 self.key = (key and " --local-user \"%s\"" % key) or "" |
60 self.key = (key and " --local-user \"%s\"" % key) or "" |
75 fp.close() |
74 fp.close() |
76 fd, datafile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".txt") |
75 fd, datafile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".txt") |
77 fp = os.fdopen(fd, r'wb') |
76 fp = os.fdopen(fd, r'wb') |
78 fp.write(data) |
77 fp.write(data) |
79 fp.close() |
78 fp.close() |
80 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify " |
79 gpgcmd = "%s --logger-fd 1 --status-fd 1 --verify " "\"%s\" \"%s\"" % ( |
81 "\"%s\" \"%s\"" % (self.path, sigfile, datafile)) |
80 self.path, |
|
81 sigfile, |
|
82 datafile, |
|
83 ) |
82 ret = procutil.filter("", gpgcmd) |
84 ret = procutil.filter("", gpgcmd) |
83 finally: |
85 finally: |
84 for f in (sigfile, datafile): |
86 for f in (sigfile, datafile): |
85 try: |
87 try: |
86 if f: |
88 if f: |
100 fingerprint = l.split()[10] |
102 fingerprint = l.split()[10] |
101 elif l.startswith("ERRSIG"): |
103 elif l.startswith("ERRSIG"): |
102 key = l.split(" ", 3)[:2] |
104 key = l.split(" ", 3)[:2] |
103 key.append("") |
105 key.append("") |
104 fingerprint = None |
106 fingerprint = None |
105 elif (l.startswith("GOODSIG") or |
107 elif ( |
106 l.startswith("EXPSIG") or |
108 l.startswith("GOODSIG") |
107 l.startswith("EXPKEYSIG") or |
109 or l.startswith("EXPSIG") |
108 l.startswith("BADSIG")): |
110 or l.startswith("EXPKEYSIG") |
|
111 or l.startswith("BADSIG") |
|
112 ): |
109 if key is not None: |
113 if key is not None: |
110 keys.append(key + [fingerprint]) |
114 keys.append(key + [fingerprint]) |
111 key = l.split(" ", 2) |
115 key = l.split(" ", 2) |
112 fingerprint = None |
116 fingerprint = None |
113 if key is not None: |
117 if key is not None: |
114 keys.append(key + [fingerprint]) |
118 keys.append(key + [fingerprint]) |
115 return keys |
119 return keys |
116 |
120 |
|
121 |
117 def newgpg(ui, **opts): |
122 def newgpg(ui, **opts): |
118 """create a new gpg instance""" |
123 """create a new gpg instance""" |
119 gpgpath = ui.config("gpg", "cmd") |
124 gpgpath = ui.config("gpg", "cmd") |
120 gpgkey = opts.get(r'key') |
125 gpgkey = opts.get(r'key') |
121 if not gpgkey: |
126 if not gpgkey: |
122 gpgkey = ui.config("gpg", "key") |
127 gpgkey = ui.config("gpg", "key") |
123 return gpg(gpgpath, gpgkey) |
128 return gpg(gpgpath, gpgkey) |
124 |
129 |
|
130 |
125 def sigwalk(repo): |
131 def sigwalk(repo): |
126 """ |
132 """ |
127 walk over every sigs, yields a couple |
133 walk over every sigs, yields a couple |
128 ((node, version, sig), (filename, linenumber)) |
134 ((node, version, sig), (filename, linenumber)) |
129 """ |
135 """ |
|
136 |
130 def parsefile(fileiter, context): |
137 def parsefile(fileiter, context): |
131 ln = 1 |
138 ln = 1 |
132 for l in fileiter: |
139 for l in fileiter: |
133 if not l: |
140 if not l: |
134 continue |
141 continue |
147 for item in parsefile(repo.vfs(fn), fn): |
154 for item in parsefile(repo.vfs(fn), fn): |
148 yield item |
155 yield item |
149 except IOError: |
156 except IOError: |
150 pass |
157 pass |
151 |
158 |
|
159 |
152 def getkeys(ui, repo, mygpg, sigdata, context): |
160 def getkeys(ui, repo, mygpg, sigdata, context): |
153 """get the keys who signed a data""" |
161 """get the keys who signed a data""" |
154 fn, ln = context |
162 fn, ln = context |
155 node, version, sig = sigdata |
163 node, version, sig = sigdata |
156 prefix = "%s:%d" % (fn, ln) |
164 prefix = "%s:%d" % (fn, ln) |
168 continue |
176 continue |
169 if key[0] == "BADSIG": |
177 if key[0] == "BADSIG": |
170 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2])) |
178 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2])) |
171 continue |
179 continue |
172 if key[0] == "EXPSIG": |
180 if key[0] == "EXPSIG": |
173 ui.write(_("%s Note: Signature has expired" |
181 ui.write( |
174 " (signed by: \"%s\")\n") % (prefix, key[2])) |
182 _("%s Note: Signature has expired" " (signed by: \"%s\")\n") |
|
183 % (prefix, key[2]) |
|
184 ) |
175 elif key[0] == "EXPKEYSIG": |
185 elif key[0] == "EXPKEYSIG": |
176 ui.write(_("%s Note: This key has expired" |
186 ui.write( |
177 " (signed by: \"%s\")\n") % (prefix, key[2])) |
187 _("%s Note: This key has expired" " (signed by: \"%s\")\n") |
|
188 % (prefix, key[2]) |
|
189 ) |
178 validkeys.append((key[1], key[2], key[3])) |
190 validkeys.append((key[1], key[2], key[3])) |
179 return validkeys |
191 return validkeys |
|
192 |
180 |
193 |
181 @command("sigs", [], _('hg sigs'), helpcategory=_HELP_CATEGORY) |
194 @command("sigs", [], _('hg sigs'), helpcategory=_HELP_CATEGORY) |
182 def sigs(ui, repo): |
195 def sigs(ui, repo): |
183 """list signed changesets""" |
196 """list signed changesets""" |
184 mygpg = newgpg(ui) |
197 mygpg = newgpg(ui) |
201 for rev in sorted(revs, reverse=True): |
214 for rev in sorted(revs, reverse=True): |
202 for k in revs[rev]: |
215 for k in revs[rev]: |
203 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev))) |
216 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev))) |
204 ui.write("%-30s %s\n" % (keystr(ui, k), r)) |
217 ui.write("%-30s %s\n" % (keystr(ui, k), r)) |
205 |
218 |
|
219 |
206 @command("sigcheck", [], _('hg sigcheck REV'), helpcategory=_HELP_CATEGORY) |
220 @command("sigcheck", [], _('hg sigcheck REV'), helpcategory=_HELP_CATEGORY) |
207 def sigcheck(ui, repo, rev): |
221 def sigcheck(ui, repo, rev): |
208 """verify all the signatures there may be for a particular revision""" |
222 """verify all the signatures there may be for a particular revision""" |
209 mygpg = newgpg(ui) |
223 mygpg = newgpg(ui) |
210 rev = repo.lookup(rev) |
224 rev = repo.lookup(rev) |
224 |
238 |
225 # print summary |
239 # print summary |
226 ui.write(_("%s is signed by:\n") % hgnode.short(rev)) |
240 ui.write(_("%s is signed by:\n") % hgnode.short(rev)) |
227 for key in keys: |
241 for key in keys: |
228 ui.write(" %s\n" % keystr(ui, key)) |
242 ui.write(" %s\n" % keystr(ui, key)) |
|
243 |
229 |
244 |
230 def keystr(ui, key): |
245 def keystr(ui, key): |
231 """associate a string to a key (username, comment)""" |
246 """associate a string to a key (username, comment)""" |
232 keyid, user, fingerprint = key |
247 keyid, user, fingerprint = key |
233 comment = ui.config("gpg", fingerprint) |
248 comment = ui.config("gpg", fingerprint) |
234 if comment: |
249 if comment: |
235 return "%s (%s)" % (user, comment) |
250 return "%s (%s)" % (user, comment) |
236 else: |
251 else: |
237 return user |
252 return user |
238 |
253 |
239 @command("sign", |
254 |
240 [('l', 'local', None, _('make the signature local')), |
255 @command( |
241 ('f', 'force', None, _('sign even if the sigfile is modified')), |
256 "sign", |
242 ('', 'no-commit', None, _('do not commit the sigfile after signing')), |
257 [ |
243 ('k', 'key', '', |
258 ('l', 'local', None, _('make the signature local')), |
244 _('the key id to sign with'), _('ID')), |
259 ('f', 'force', None, _('sign even if the sigfile is modified')), |
245 ('m', 'message', '', |
260 ('', 'no-commit', None, _('do not commit the sigfile after signing')), |
246 _('use text as commit message'), _('TEXT')), |
261 ('k', 'key', '', _('the key id to sign with'), _('ID')), |
247 ('e', 'edit', False, _('invoke editor on commit messages')), |
262 ('m', 'message', '', _('use text as commit message'), _('TEXT')), |
248 ] + cmdutil.commitopts2, |
263 ('e', 'edit', False, _('invoke editor on commit messages')), |
249 _('hg sign [OPTION]... [REV]...'), |
264 ] |
250 helpcategory=_HELP_CATEGORY) |
265 + cmdutil.commitopts2, |
|
266 _('hg sign [OPTION]... [REV]...'), |
|
267 helpcategory=_HELP_CATEGORY, |
|
268 ) |
251 def sign(ui, repo, *revs, **opts): |
269 def sign(ui, repo, *revs, **opts): |
252 """add a signature for the current or given revision |
270 """add a signature for the current or given revision |
253 |
271 |
254 If no revision is given, the parent of the working directory is used, |
272 If no revision is given, the parent of the working directory is used, |
255 or tip if no revision is checked out. |
273 or tip if no revision is checked out. |
259 |
277 |
260 See :hg:`help dates` for a list of formats valid for -d/--date. |
278 See :hg:`help dates` for a list of formats valid for -d/--date. |
261 """ |
279 """ |
262 with repo.wlock(): |
280 with repo.wlock(): |
263 return _dosign(ui, repo, *revs, **opts) |
281 return _dosign(ui, repo, *revs, **opts) |
|
282 |
264 |
283 |
265 def _dosign(ui, repo, *revs, **opts): |
284 def _dosign(ui, repo, *revs, **opts): |
266 mygpg = newgpg(ui, **opts) |
285 mygpg = newgpg(ui, **opts) |
267 opts = pycompat.byteskwargs(opts) |
286 opts = pycompat.byteskwargs(opts) |
268 sigver = "0" |
287 sigver = "0" |
273 opts['date'] = dateutil.parsedate(date) |
292 opts['date'] = dateutil.parsedate(date) |
274 |
293 |
275 if revs: |
294 if revs: |
276 nodes = [repo.lookup(n) for n in revs] |
295 nodes = [repo.lookup(n) for n in revs] |
277 else: |
296 else: |
278 nodes = [node for node in repo.dirstate.parents() |
297 nodes = [ |
279 if node != hgnode.nullid] |
298 node for node in repo.dirstate.parents() if node != hgnode.nullid |
|
299 ] |
280 if len(nodes) > 1: |
300 if len(nodes) > 1: |
281 raise error.Abort(_('uncommitted merge - please provide a ' |
301 raise error.Abort( |
282 'specific revision')) |
302 _('uncommitted merge - please provide a ' 'specific revision') |
|
303 ) |
283 if not nodes: |
304 if not nodes: |
284 nodes = [repo.changelog.tip()] |
305 nodes = [repo.changelog.tip()] |
285 |
306 |
286 for n in nodes: |
307 for n in nodes: |
287 hexnode = hgnode.hex(n) |
308 hexnode = hgnode.hex(n) |
288 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n), |
309 ui.write( |
289 hgnode.short(n))) |
310 _("signing %d:%s\n") % (repo.changelog.rev(n), hgnode.short(n)) |
|
311 ) |
290 # build data |
312 # build data |
291 data = node2txt(repo, n, sigver) |
313 data = node2txt(repo, n, sigver) |
292 sig = mygpg.sign(data) |
314 sig = mygpg.sign(data) |
293 if not sig: |
315 if not sig: |
294 raise error.Abort(_("error while signing")) |
316 raise error.Abort(_("error while signing")) |
302 return |
324 return |
303 |
325 |
304 if not opts["force"]: |
326 if not opts["force"]: |
305 msigs = match.exact(['.hgsigs']) |
327 msigs = match.exact(['.hgsigs']) |
306 if any(repo.status(match=msigs, unknown=True, ignored=True)): |
328 if any(repo.status(match=msigs, unknown=True, ignored=True)): |
307 raise error.Abort(_("working copy of .hgsigs is changed "), |
329 raise error.Abort( |
308 hint=_("please commit .hgsigs manually")) |
330 _("working copy of .hgsigs is changed "), |
|
331 hint=_("please commit .hgsigs manually"), |
|
332 ) |
309 |
333 |
310 sigsfile = repo.wvfs(".hgsigs", "ab") |
334 sigsfile = repo.wvfs(".hgsigs", "ab") |
311 sigsfile.write(sigmessage) |
335 sigsfile.write(sigmessage) |
312 sigsfile.close() |
336 sigsfile.close() |
313 |
337 |
318 return |
342 return |
319 |
343 |
320 message = opts['message'] |
344 message = opts['message'] |
321 if not message: |
345 if not message: |
322 # we don't translate commit messages |
346 # we don't translate commit messages |
323 message = "\n".join(["Added signature for changeset %s" |
347 message = "\n".join( |
324 % hgnode.short(n) |
348 [ |
325 for n in nodes]) |
349 "Added signature for changeset %s" % hgnode.short(n) |
|
350 for n in nodes |
|
351 ] |
|
352 ) |
326 try: |
353 try: |
327 editor = cmdutil.getcommiteditor(editform='gpg.sign', |
354 editor = cmdutil.getcommiteditor( |
328 **pycompat.strkwargs(opts)) |
355 editform='gpg.sign', **pycompat.strkwargs(opts) |
329 repo.commit(message, opts['user'], opts['date'], match=msigs, |
356 ) |
330 editor=editor) |
357 repo.commit( |
|
358 message, opts['user'], opts['date'], match=msigs, editor=editor |
|
359 ) |
331 except ValueError as inst: |
360 except ValueError as inst: |
332 raise error.Abort(pycompat.bytestr(inst)) |
361 raise error.Abort(pycompat.bytestr(inst)) |
|
362 |
333 |
363 |
334 def node2txt(repo, node, ver): |
364 def node2txt(repo, node, ver): |
335 """map a manifest into some text""" |
365 """map a manifest into some text""" |
336 if ver == "0": |
366 if ver == "0": |
337 return "%s\n" % hgnode.hex(node) |
367 return "%s\n" % hgnode.hex(node) |
338 else: |
368 else: |
339 raise error.Abort(_("unknown signature version")) |
369 raise error.Abort(_("unknown signature version")) |
340 |
370 |
|
371 |
341 def extsetup(ui): |
372 def extsetup(ui): |
342 # Add our category before "Repository maintenance". |
373 # Add our category before "Repository maintenance". |
343 help.CATEGORY_ORDER.insert( |
374 help.CATEGORY_ORDER.insert( |
344 help.CATEGORY_ORDER.index(command.CATEGORY_MAINTENANCE), |
375 help.CATEGORY_ORDER.index(command.CATEGORY_MAINTENANCE), _HELP_CATEGORY |
345 _HELP_CATEGORY) |
376 ) |
346 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'GPG signing' |
377 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'GPG signing' |