hgext/gpg.py
changeset 43076 2372284d9457
parent 42235 ade02721d3fa
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    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'