Mercurial > hgrepos > DevTools > mercurial-extensions
view extensions/kwarchive.py @ 125:fb7e1e4e4d2c
Provide some SCCS markers also to be able to search with "what(1)" also
| author | Franz Glasner <hg@dom66.de> |
|---|---|
| date | Fri, 17 Aug 2018 09:07:02 +0200 |
| parents | 46b7d34ef0b2 |
| children | 0a6681db2772 |
line wrap: on
line source
# -*- coding: utf-8 -*- # @(#) $HGheader$ # $HGnodeid$ # """like :hg:`archive` but with keyword expansion within selected files The patterns of files to to expanded are configured in an versioned ``.hgkwarchive`` configuration file found in the root of the working directory. The ``.hgkwarchive`` file uses the same syntax as all other Mercurial configuration files. The ``[patterns]`` section specifies which files should have the keywords expanded. Example:: # # Determine which files are consideres for keyword expansion in # which style # [patterns] # # Expand keywords in every python file except those matching "x*" # using the default RCS style expansion format # **.py = x* = NO # This file has reStructuredText style keyword expansion "|VCS<kw>|" only path:VERSION = reST # This file has reStructuredText style and RCS style keyword expansion path:README = reST, RCS # # Determine which keywords are expanded. # A missing and/or empty section means that all predefined keywords # are expanded. # # Consisting of key-value pairs with of the form # # Alias = PreDefinedKeyword # # An empty `PreDefinedKeyword' means the identity. # [keywords] # `Revision' and possibly the `VCSRevision' keyword is expanded Revision = # # Additionally: `MyKeyword' is expanded with the contents of the # pre-defined `JustDate' # MyKeyword = JustDate A non-existing ``.hgkwarchive`` file deactivates keyword expansion as does an empty ``[patterns]`` section. """ from __future__ import absolute_import __revision__ = "$Revision$" __author__ = "Franz Glasner" import os import itertools import inspect from mercurial.i18n import _ from mercurial import (archival, config, cmdutil, error, match, pycompat, scmutil, templatefilters, util, node) testedwith = "4.3.1 4.3.2 4.4.1 4.4.2 4.5.2 4.6.1" SHORTENED_HG_SCHEMES = { "ssh": "hg+ssh", "http": "hg+http", "https": "hg+https", "file": "file", } cmdtable = {} command = cmdutil.command(cmdtable) @command( 'kwarchive', [ ('', 'no-decode', None, _('do not pass files through decoders')), ('p', 'prefix', '', _('directory prefix for files in archive'), _('PREFIX')), ('r', 'rev', '', _('revision to distribute'), _('REV')), ('t', 'type', '', _('type of distribution to create'), _('TYPE')), ('', 'path', 'default', _('the canonical repository to use'), _('PATH')), ('', "kwconfig", '', _('an alternate pattern configuration configuration file'), _('PATTERNCONFIG')), ('', "no-shorten-path", None, _("don't shorten the path urls")), ('', "path-filter", "short", _("determine how the path will be printed")), ('', "user-filter", "user", _("the part of the user to be printed"), _("USERFILTER")) ] + cmdutil.subrepoopts + cmdutil.walkopts, _('[OPTION]... DEST')) def kwarchive(ui, repo, dest, **opts): '''create an unversioned archive of a repository revision with some keywords expanded By default, the revision used is the parent of the working directory; use -r/--rev to specify a different revision. The archive type is automatically detected based on file extension (to override, use -t/--type). .. container:: verbose Examples: - create a zip file containing the 1.0 release:: hg archive -r 1.0 project-1.0.zip - create a tarball excluding .hg files:: hg archive project.tar.gz -X ".hg*" Valid types are: :``files``: a directory full of files (default) :``tar``: tar archive, uncompressed :``tbz2``: tar archive, compressed using bzip2 :``tgz``: tar archive, compressed using gzip :``uzip``: zip archive, uncompressed :``zip``: zip archive, compressed using deflate The exact name of the destination archive or directory is given using a format string; see :hg:`help export` for details. Each member added to an archive file has a directory prefix prepended. Use -p/--prefix to specify a format string for the prefix. The default is the basename of the archive, with suffixes removed. Use --path to specify named path information from :hg:`paths` as the canonical repository location. Use ``.`` for the current repository root root. If no path is given then ``default`` is assumed. Full path URLs are printed somewhat shortened by default. To use them as-is use the --no-shorten-path option. Allowed --user-filter values are: :``user``: the short representation of a user name or email address :``person``: the name before an email address as per RFC 5322 :``email``: the email address :``full``: the complete user information w/o filtering :``none``: an alias for ``full`` The --path-filter values detemine how the path will be printed: :``full``: the path will be printed as is (including passwords et al.) :``nopwd``: like ``full`` but without passwords :``short``: shorten the path somehow: no user/password information and only a short server name :``last``: shorten the path to the last component Returns 0 on success. ''' opts = pycompat.byteskwargs(opts) ctx = scmutil.revsingle(repo, opts.get('rev')) if not ctx: raise error.Abort(_('no working directory: please specify a revision')) node = ctx.node() dest = makefilename_compat(ctx, dest) if os.path.realpath(dest) == repo.root: raise error.Abort(_('repository root cannot be destination')) kind = opts.get('type') or archival.guesskind(dest) or 'files' prefix = opts.get('prefix') if dest == '-': if kind == 'files': raise error.Abort(_('cannot archive plain files to stdout')) dest = makefileobj_compat(ctx, dest) if not prefix: prefix = os.path.basename(repo.root) + '-%h' prefix = makefilename_compat(ctx, prefix) matchfn = scmutil.match(ctx, [], opts) # # Monkey patch archival's archivers classes so that an archiver's "addfile()" # expands keywords # for ac in ("fileit", "tarit", "zipit",): patch_archiver_class( ac, make_keyword_filter( ui, repo, ctx, ac, archival.tidyprefix(dest, kind, prefix), hgpath=opts.get("path"), path_filter=get_checked_path_filter(opts), user_filter=get_checked_user_filter(opts), kwconfig=opts.get("kwconfig"))) archival.archive(repo, dest, node, kind, not opts.get('no_decode'), matchfn, prefix, subrepos=opts.get('subrepos')) # XXX FIXME: Should the original methods be restored here? # XXX FIXME: Should we automatically amend ".hg_archival.txt" with a # path item? But this means shat we should know the complete # configuration ("ui.archivemeta") and it's name and some # output match filters (see archival.archive()). @command( 'kwprint', [ ('r', 'rev', '', _('revision to distribute'), _('REV')), ('', 'path', 'default', _('the canonical repository to use'), _('PATH')), ('', "no-shorten-path", None, _("don't shorten the path urls")), ('', "path-filter", "short", _("determine how the path will be printed")), ('', "user-filter", "user", _("the part of the user to be printed"), _("USERFILTER")), ('', "reST", None, _("output in reST substitution definition syntax")), ('', "no-file", None, _("don't show file-dependent keywords")), ] + cmdutil.subrepoopts + cmdutil.walkopts, _('[OPTION]...')) def kwprint(ui, repo, **opts): '''print the file-independent keywords and for an example file-dependent keywords By default, the revision used is the parent of the working directory; use -r/--rev to specify a different revision. Use --path to specify named path information from :hg:`paths` as the canonical repository location. Use ``.`` for the current repository root root. If no path is given then ``default`` is assumed. Full path URLs are printed somewhat shortened by default. To use them as-is use the --no-shorten-path option. Allowed --user-filter values are: :``user``: the short representation of a user name or email address :``person``: the name before an email address as per RFC 5322 :``email``: the email address :``full``: the complete user information w/o filtering :``none``: an alias for ``full`` The --path-filter values detemine how the path will be printed: :``full``: the path will be printed as is (including passwords et al.) :``nopwd``: like ``full`` but without passwords :``short``: shorten the path somehow: no user/password information and only a short server name :``last``: shorten the path to the last component Use --reST to output the keyword in a format suitable for including in reStructuredText (reST) documents by using it's substitution feature. All revision keyword names have a ``VCS`` prefix. Use --no-file to suppress the output of file-dependent keywords with an example file. Returns 0 on success. ''' opts = pycompat.byteskwargs(opts) ctx = scmutil.revsingle(repo, opts.get('rev')) if not ctx: raise error.Abort(_('no working directory: please specify a revision')) node = ctx.node() prefix = makefilename_compat(ctx, "") keywords = make_node_keywords( ui, repo, ctx, prefix, hgpath=opts.get("path"), path_filter=get_checked_path_filter(opts), user_filter=get_checked_user_filter(opts)) # make file-dependent keywords for an example file file_keywords = make_file_keywords( keywords, "dir1/dir2/test.file", "a4dd6f4b22e11fec41158eec187630c24a43120a") for key in sorted(keywords.keys()): if opts.get("reST"): ui.write(".. |VCS%s| replace:: %s\n" % (key, keywords[key])) else: ui.write("$%s: %s $\n" % (key, keywords[key])) if not opts.get("no_file"): ui.write("\n") for key in sorted(file_keywords.keys()): if opts.get("reST"): ui.write(".. |VCS%s| replace:: %s\n" % (key, file_keywords[key])) else: ui.write("$%s: %s $\n" % (key, file_keywords[key])) def patch_archiver_class(archivername, filter): """Patch an archiver class and return the original unbound method""" archiver_class = getattr(archival, archivername) orig_addfile = getattr(archiver_class, "addfile") def new_addfile(self, name, mode, isline, data): return orig_addfile(self, name, mode, isline, filter(name, data)) setattr(archiver_class, "addfile", new_addfile) return orig_addfile def make_keyword_filter(ui, repo, ctx, archive_class, prefix, hgpath="default", path_filter="short", user_filter="user", kwconfig=""): keywords = make_node_keywords(ui, repo, ctx, prefix, hgpath=hgpath, path_filter=path_filter, user_filter=user_filter) kwconfigdata = kwconfigname = None if kwconfig: kwconfigdata = open(kwconfig, "rb").read() kwconfigname = kwconfig try: if kwconfigdata is None: # .hgkwarchive kwconfigdata = ctx[".hgkwarchive"].data() kwconfigname = ".kwarchive" except (IOError, LookupError): def _filter(name, data): return data else: # # Parse the data in ".hgkwarchive" and generate a # Mercurial matcher # cfg = config.config() cfg.parse(kwconfigname, kwconfigdata) include = [] exclude = [] patterns = [] patterns_rcs = [] patterns_rst = [] if cfg.items("patterns"): for pattern, styles in cfg.items("patterns"): styles = [s.strip() for s in styles.upper().split(",")] if "YES" in styles or "INCLUDE" in styles: include.append(pattern) elif "NO" in styles or "EXCLUDE" in styles: exclude.append(pattern) else: patterns.append(pattern) if "REST" in styles or "RST" in styles \ or "RCS" in styles: if "RCS" in styles: patterns_rcs.append(pattern) if "REST" in styles or "RST" in styles: patterns_rst.append(pattern) else: # default to RCS if no keyword style is given patterns_rcs.append(pattern) matcher = match.match(repo.root, '', patterns=patterns, include=include, exclude=exclude) else: matcher = match.never(repo.root, '') # # An empty patterns_rcs does not mean that match_rcs is always # true. # if not patterns_rcs: matcher_rcs = match.never(repo.root, '') else: matcher_rcs = match.match(repo.root, '', patterns=patterns_rcs, include=[], exclude=[]) # # An empty patterns_rst does not mean that match_rst is always # true. # if not patterns_rst: matcher_rst = match.never(repo.root, '') else: matcher_rst = match.match(repo.root, '', patterns=patterns_rst, include=[], exclude=[]) # # This is the mapping from all the (white-listed) pre-defined # keywords to the keywords to be expanded in files (with aliases). # keyword_aliases = {} for alias, keyword in cfg.items("keywords"): keyword_aliases.setdefault(keyword or alias, []).append(alias) # # Get the manifest to be able to determine a file's NodeId # manifest = ctx.manifest() def _filter(name, data): real_name = name if archive_class != "fileit": if prefix: assert name.startswith(prefix) real_name = name[len(prefix):] if not matcher(real_name): return data try: nodeid = node.hex(manifest[real_name]) except LookupError: nodeid = None # file specific keywords file_keywords = make_file_keywords(keywords, real_name, nodeid) # This prevents unwanted keyword expansion here _MARKER_RCS = '$' _MARKER_RST = '|' for kw, value in itertools.chain(keywords.items(), file_keywords.items()): # an empty database means: all keywords allowed, no aliases if keyword_aliases: kwds = keyword_aliases.get(kw, []) else: kwds = [kw] for kw in kwds: if matcher_rcs(real_name): filekw = "%s%s%s" % (_MARKER_RCS, kw, _MARKER_RCS) filevalue = "%s%s: %s %s" \ % (_MARKER_RCS, kw, value, _MARKER_RCS) data = data.replace(filekw, filevalue) if matcher_rst(real_name): filekw = "%sVCS%s%s" % (_MARKER_RST, kw, _MARKER_RST) filevalue = value data = data.replace(filekw, filevalue) return data return _filter def make_node_keywords(ui, repo, ctx, prefix, hgpath="default", path_filter="short", user_filter="user"): """Make all the node-specific (i.e. file-path independent) keywords """ if hgpath: if hgpath == '.': path_uri = repo.root else: try: if path_filter == "full": path_uri = bytes(util.url(ui.paths[hgpath].loc)) elif path_filter == "nopwd": path_uri = util.hidepassword(ui.paths[hgpath].loc) elif path_filter == "short": path_url = util.url(ui.paths[hgpath].loc) path_url.scheme = SHORTENED_HG_SCHEMES.get(path_url.scheme, "hg") path_url.user = None path_url.passwd = None path_url.host = stripped_hostname(path_url.host) path_url.port = None path_uri = bytes(path_url) elif path_filter == "last": path_url = str(util.url(ui.paths[hgpath].loc)).split("/") path_uri = bytes(b".../"+path_url[-1]) else: raise error.Abort("path-filter `%s' not implemented" % path_filter) except LookupError: raise error.Abort(_('repository %s not found') % hgpath) else: path_uri = repo.root if path_uri.startswith(b"\\\\"): # Make an URL from a Windows UNC path path_uri = b"file:///" + path_uri.replace(b'\\', b'/') elif len(path_uri) >= 2 and (b'A' <= path_uri[0].upper() <= b'Z') \ and path_uri[1] == b':': # make an URL from a Windows path with drive letter path_uri = b"file:///" + path_uri.replace(b'\\', b'/') elif path_uri.startswith(b'/'): # an absolute POSIX path path_uri = b"file://" + path_uri keywords = { "HGpath": path_uri, "HGrevision": ctx.hex(), "Revision": templatefilters.short(ctx.hex()), "Date": templatefilters.isodatesec(ctx.date()), "JustDate": templatefilters.shortdate(ctx.date()), # compatibility alias for `JustDate' "HGshortdate": templatefilters.shortdate(ctx.date()), } if user_filter == "user": keywords["Author"] = templatefilters.emailuser(ctx.user()) elif user_filter == "person": keywords["Author"] = templatefilters.utf8( templatefilters.person(ctx.user()).replace(' ', '+')) elif user_filter == "email": keywords["Author"] = templatefilters.email(ctx.user()) elif user_filter in ("full", "none",): # # "none" is retained for compatibility reasons and now an # alias for "full". # But make it **one** word because that is meant in the RCS spec. # keywords["Author"] = templatefilters.utf8(ctx.user().replace(' ', '+')) else: raise error.Abort(_("unknown user filter")) return keywords def make_file_keywords(keywords, real_name, nodeid): return { "HGsource": keywords["HGpath"] + '/' + real_name, "Source": real_name, "File": templatefilters.basename(real_name), "Header": "%s %s %s %s" % (real_name, keywords["Revision"], keywords["Date"], keywords["Author"]), "HGid": "%s %s %s %s" % (keywords["HGpath"] + '/' + real_name, keywords["Revision"], keywords["Date"], keywords["Author"]), "HGheader": "%s %s %s %s" % (keywords["HGpath"] + '/' + real_name, keywords["HGrevision"], keywords["Date"], keywords["Author"]), "Id": "%s %s %s %s" % (templatefilters.basename(real_name), keywords["Revision"], keywords["Date"], keywords["Author"]), "HGnodeid": nodeid or "", } def stripped_hostname(hostname): """Return `hostname` without any domain port information""" if not hostname: return hostname idx = hostname.find('.') if idx < 0: return hostname return hostname[:idx] def get_checked_user_filter(opts): uf = opts.get("user_filter") # "none" is retained for compatibility reasons and now an alias for "full" if uf not in ("person", "user", "email", "full", "none"): raise error.Abort( _("user filter must be any of `user', `person' or `email'")) return uf def get_checked_path_filter(opts): pf = opts.get("path_filter") if pf not in ("full", "nopwd", "short", "last"): raise error.Abort( _("path filter must be any of `full', `nopwd', `short' or `last'")) return pf if "ctx" in inspect.getargspec(cmdutil.makefilename).args: # Mercurial >= 4.6 makefilename_compat = cmdutil.makefilename makefileobj_compat = cmdutil.makefileobj else: # Mercurial < 4.6 def makefilename_compat(ctx, pat, **props): return cmdutil.makefilename(ctx.repo(), pat, ctx.node(), **props) def makefileobj_compat(ctx, pat, **props): return cmdutil.makefileobj(ctx.repo(), pat, ctx.node(), **props)
