view extensions/kwarchive.py @ 440:72df885a1012 default tip trunk

===== signature for changeset e1ae0c15acfc
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 30 May 2026 13:16:08 +0200
parents 111aa1d44ffd
children
line wrap: on
line source

# -*- coding: utf-8 -*-
# @(#) $HGheader$
#      $HGnodeid$
# :-
# :Copyright: © 2017-2026 Franz Glasner <fzglas.hg@dom66.de>
# :License:   This software may be used and distributed according to the
#             terms of the GNU General Public License version 2 or any
#             later version.
#             The license is incorporated herein by reference.
# :-
#
"""like :hg:`archive` but with keyword expansion within selected files

Configuration is done in a versioned ``.hgkwarchive`` configuration
file. This file uses the same syntax as all other Mercurial
configuration files.

The ``[patterns]`` section specifies which files should have the
keywords expanded and possibly the style of expansion. The keys are
Mercurial file name patterns as described in :hg:`patterns`.

Example::

   #
   # Determine which files are considered 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

The ``[keywords]`` section specifies which keywords are expanded. Aliases
can be defined also. By default the pre-defined set of RCC/CVS/SVN-like
keywords is expanded. A non-empty ``[keyword]`` section defines an
explicit white-list of expanded keywords and/or aliases.

Example::

    #
    # 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' (this is in Python's format syntax).
    #
    MyKeyword = {JustDate}
    #
    # `MyCustomSubstKeyword' is a substitution keyword.
    #
    MyCustomSubstKeyword = This is my replacement content

A non-existing ``.hgkwarchive`` file deactivates keyword expansion as does
an empty ``[patterns]`` section.

.. note:: Because the keyword expansion is defined in a *versioned* file
          no templating is supported here. This could lead to remote
          code execution secnarios because Mercurial templates can execute
          a big bunch of Python functions.

(rev |VCSRevision|)

"""

from __future__ import absolute_import


__revision__ = "|VCSRevision|"
__date__ = "|VCSJustDate|"
__author__ = "Franz Glasner"


import os
import inspect
import itertools

from mercurial.i18n import _
from mercurial import (archival, config, cmdutil, error, match, subrepo,
                       pycompat, scmutil, templatefilters, templatekw,
                       util, node, demandimport)
with demandimport.deactivated():
    try:
        from mercurial import registrar
    except ImportError:
        registrar = None
    # check for new template function API with `(context, mapping)`
    try:
        from mercurial import templatefuncs
    except ImportError:
        context_mapping_api = False
    else:
        # >= 4.6
        context_mapping_api = True
    # some date specific util functions moved to new mercurial.utils.dateutil
    try:
        from mercurial.utils import dateutil as _dateutil
    except ImportError:
        _dateutil = util

    # some URL specific util functions moved to new mercurial.utils.urlutil
    try:
        from mercurial.utils import urlutil as _urlutil
    except ImportError:
        _urlutil = util


testedwith = b"4.3.1 4.3.2 4.4.1 4.4.2 4.5.2 4.6.1 4.8.1 4.9 5.0.1 5.1.2 5.2.1 5.5 5.6 5.9.1 6.1.4 6.9.5"

SHORTENED_HG_SCHEMES = {
    b"ssh": b"hg+ssh",
    b"http": b"hg+http",
    b"https": b"hg+https",
    b"file": b"file",
}

KWARCHIVE_CONFIG = b".hgkwarchive"

cmdtable = {}

if registrar and hasattr(registrar, "command"):
    command = registrar.command(cmdtable)
else:
    command = cmdutil.command(cmdtable)


def getversion():
    """Provide the version information for verbose :hg:`version` output.

    Read the :file:`VERSION` from the parent of the :file:`extensions`
    directory.

    """
    import re
    import os
    try:
        fn = __file__
    except NameError:
        return b"<unknown>"
    else:
        try:
            # this is pure Python standard functionality: no util.posixfile
            verdata = open(os.path.join(os.path.dirname(fn),
                                        "../VERSION"),
                           "rb").read()
            return re.search(b"^(.*)", verdata,).group(1)
        except OSError:
            return b"<not found>"


@command(
    b"kwarchive",
    [
        (b'', b"no-decode", None, _(b"do not pass files through decoders")),
        (b'p', b"prefix", b'', _(b"directory prefix for files in archive"), _(b"PREFIX")),
        (b'r', b"rev", b'', _(b"revision to distribute"), _(b"REV")),
        (b't', b"type", b'', _(b"type of distribution to create"), _(b"TYPE")),
        (b'', b"path", b"default", _(b"the canonical repository to use"), _(b"PATH")),
        (b'', b"kwconfig", b'', _(b"an alternate pattern configuration configuration file (possibly used for subrepos also)"), _(b"PATTERNCONFIG")),
        (b'', b"no-shorten-path", None, _(b"don't shorten the path urls")),
        (b'', b"path-filter", b"short", _(b"determine how the path will be printed")),
        (b'', b"user-filter", b"user", _(b"the part of the user to be printed"), _(b"USERFILTER"))
    ] + cmdutil.subrepoopts + cmdutil.walkopts,
    _(b"[OPTION]... DEST"),
    inferrepo=True)
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
    :``nouser``:  like ``full`` but without user and password information
    :``short``:   shorten the path somehow: no user/password information and
                  only a short server name
    :``last``:    shorten the path to the last component
    :``repo``:    the repository id will be used as path
    :``reposhort``: the shortened repository id will be used as path

    Returns 0 on success.

    '''

    opts = pycompat.byteskwargs(opts)
    ctx = scmutil.revsingle(repo, opts.get(b"rev"))
    if not ctx:
        raise error.Abort(_(b"no Mercurial revision found: please specify a revision"))
    node = ctx.node()
    dest = makefilename_compat(ctx, dest)
    if os.path.realpath(dest) == repo.root:
        raise error.Abort(_(b"repository root cannot be destination"))

    kind = opts.get(b"type") or archival.guesskind(dest) or b"files"
    prefix = opts.get(b"prefix")

    if dest == b'-':
        if kind == "files":
            raise error.Abort(_(b"cannot archive plain files to stdout"))
        dest = makefileobj_compat(ctx, dest)
        if not prefix:
            prefix = os.path.basename(repo.root) + b'-%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,
                ctx,
                ac,
                archival.tidyprefix(dest, kind, prefix),
                hgpath=opts.get(b"path"),
                path_filter=get_checked_path_filter_option(opts),
                user_filter=get_checked_user_filter_option(opts),
                subrepos=opts.get(b"subrepos"),
                kwconfig=opts.get(b"kwconfig")))

    archival.archive(repo, dest, node, kind, not opts.get(b"no_decode"),
                     matchfn, prefix, subrepos=opts.get(b"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(
    b"kwprint",
    [
        (b'r', b"rev", b'', _(b"revision to distribute"), _(b"REV")),
        (b'', b"path", b"default", _(b"the canonical repository to use"), _(b"PATH")),
        (b'', b"no-shorten-path", None, _(b"don't shorten the path urls")),
        (b'', b"path-filter", b"short", _(b"determine how the path will be printed")),
        (b'', b"user-filter", b"user", _(b"the part of the user to be printed"), _(b"USERFILTER")),
        (b'', b"reST", None, _(b"output in reST substitution definition syntax")),
        (b'', b"no-file", None, _(b"don't show file-dependent keywords")),
    ] + cmdutil.subrepoopts + cmdutil.walkopts,
    _(b"[OPTION]..."),
    inferrepo=True)
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
    :``nouser``:  like ``full`` but without user and password information
    :``short``:   shorten the path somehow: no user/password information and
                  only a short server name
    :``last``:    shorten the path to the last component
    :``repo``:    the repository id will be used as path
    :``reposhort``: the shortened repository id will be used as path

    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(b"rev"))
    if not ctx:
        raise error.Abort(_(b"no Mercurial revision found: please specify a revision"))
    node = ctx.node()

    prefix = makefilename_compat(ctx, b'')

    keywords = make_node_keywords(
        ui, ctx,
        hgpath=opts.get(b"path"),
        path_filter=get_checked_path_filter_option(opts),
        user_filter=get_checked_user_filter_option(opts))
    # make file-dependent keywords for an example file
    if not opts.get(b"no_file"):
        file_keywords = make_file_keywords(
            keywords,
            b"dir1/dir2/test.file",
            b"a4dd6f4b22e11fec41158eec187630c24a43120a")
    else:
        file_keywords = None
    _kwprint_keywords(ui, keywords, file_keywords,
                      opts.get(b"reST"),
                      opts.get(b"no_file"))
    if opts.get(b"subrepos"):
        _kwprint_subrepos(ctx, ui,
                          opts.get(b"reST"),
                          opts.get(b"no_file"),
                          path_filter=get_checked_path_filter_option(opts),
                          user_filter=get_checked_user_filter_option(opts))


def _kwprint_keywords(ui, keywords, file_keywords, rest, no_file):
    """Print all the prepared keywords into the output"""
    for key in sorted(keywords.keys()):
        if rest:
            if keywords[key]:
                ui.write(b".. |VCS%s| replace:: %s\n" % (key, keywords[key]))
            else:
                #
                # empty replacements are not allowed:
                # write a non-breaking space instead
                #
                ui.write(b".. |VCS%s| unicode:: 0xA0\n" % key)
        else:
            ui.write(b"$%s: %s $\n" % (key, keywords[key]))
    if not no_file:
        ui.write(b"\n")
        for key in sorted(file_keywords.keys()):
            if rest:
                ui.write(b".. |VCS%s| replace:: %s\n" % (key, file_keywords[key]))
            else:
                ui.write(b"$%s: %s $\n" % (key, file_keywords[key]))


def _kwprint_subrepos(ctx, ui, rest, no_file, path_filter, user_filter):
    """For all subrepos in `ctx` do keyword expansion resursively"""
    for subpath in sorted(ctx.substate):
        substate = ctx.substate[subpath]
        # skip on non-Mercurial subrepos
        if substate[2] != b"hg":
            continue
        subrep = ctx.workingsub(subpath)
        subctx = subrep._getctx()
        assert subctx.repo() == subrep._repo
        keywords = make_node_keywords(ui, subctx,
                                      hgpath=None,
                                      path_filter=path_filter,
                                      user_filter=user_filter,
                                      hglocation=substate[0])
        if not no_file:
            file_keywords = make_file_keywords(
                keywords,
                b"dir3/dir4/test-s.file",
                b"ffffffff22e11fec41158eec187630c24a43120a")
        else:
            file_keywords = None
        ui.write(b"\n\n")
        _kwprint_keywords(ui, keywords, file_keywords, rest, no_file)
        # Recursively check for other subrepos
        _kwprint_subrepos(subctx, ui, rest, no_file, path_filter, user_filter)


def _test_subrepos(ctx, ui):
    ui.write("STATE: " + repr(subrepo.state(ctx, ui)) + "\n")
    for subpath in sorted(ctx.substate):
        ui.write("SUBPATH: " + subpath + "\n");
        subrep = ctx.workingsub(subpath)
        ui.write(repr(subrep) + ": subrelpath=" + subrepo.subrelpath(subrep)
                 + " reporelpath=" + subrepo.reporelpath(subrep._repo)
                 + '\n')
        # Yes(!)
        assert subrepo.subrelpath(subrep) == subrepo.reporelpath(subrep._repo)
        assert subrepo.subrelpath(subrep).endswith(subpath)
        ui.write("   " + repr(subrep._getctx()) + '\n')
        _test_subrepos(subrep._getctx(), ui)


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, islink, data):
        return orig_addfile(self, name, mode, islink, filter(name, data))

    setattr(archiver_class, "addfile", new_addfile)
    return orig_addfile


def make_keyword_filter(ui, ctx, archive_class, prefix,
                        hgpath=b"default",
                        path_filter=b"short",
                        user_filter=b"user",
                        subrepos=False,
                        kwconfig=b""):
    assert isinstance(archive_class, str), "must be a native string"
    filterdata_by_subrepos = {
        b'': _make_repo_filterdata(ui, ctx, hgpath, None, kwconfig,
                                   path_filter, user_filter),
    }
    if subrepos:
        _amend_filterdata_by_subrepos(filterdata_by_subrepos,
                                      ui, ctx,
                                      kwconfig,
                                      path_filter,
                                      user_filter)
    subrepo_paths = list(filterdata_by_subrepos.keys())
    subrepo_paths.sort(key=len, reverse=True)

    def _filter(name, data):
        real_name = rel_name_in_subrepo = name
        if archive_class != "fileit":
            if prefix:
                assert name.startswith(prefix)
                real_name = rel_name_in_subrepo = name[len(prefix):]
        # find the filterdata configuration corresponding to current subrepo
        for s in subrepo_paths:
            if real_name.startswith(s):
                filterdata = filterdata_by_subrepos[s]
                rel_name_in_subrepo = real_name[len(s):]
                break
        else:
            raise ValueError("invalid subrepo filter data")
        if filterdata is None:
            return data
        matcher, matcher_rcs, matcher_rst, manifest, \
            keyword_substitutions, keywords  = filterdata

        if not matcher(rel_name_in_subrepo):
            #ui.write("NOT MATCHER for " + real_name + " (name: " + name + ") \n")
            return data
        try:
            nodeid = node.hex(manifest[rel_name_in_subrepo])
        except LookupError:
            nodeid = None
        # file specific keywords
        file_keywords = make_file_keywords(
            keywords, rel_name_in_subrepo, nodeid)
        _predef_keywords = keywords.copy()
        _predef_keywords.update(file_keywords)
        # This prevents unwanted keyword expansion here
        _MARKER_RCS = b'$'
        _MARKER_RST = b'|'
        for kw, value in itertools.chain(keyword_substitutions.items(), keywords.items(), file_keywords.items()):
            #
            # Non-empty keyword_substitutions are an implicit whitelist and
            # the value are Python format templates when expanding.
            #
            if keyword_substitutions:
                if kw in keyword_substitutions:
                    kwds = [kw]
                    if value:
                        if not pycompat.ispy3:
                            value = value.format(**_predef_keywords)
                        else:
                            # Assume UTF-8 strings in config files
                            _predef_keywords_u = dict((k.decode("utf-8"), v.decode("utf-8")) for k, v in _predef_keywords.items())
                            value = value.decode("utf-8").format(**_predef_keywords_u).encode("utf-8")
                    else:
                        value = _predef_keywords[kw]
                else:
                    # not whitelisted -> ignore
                    kwds = []
            else:
                # Empty keyword_substitutions mean: expand built-in keywords
                kwds = [kw]
            for kw in kwds:
                if matcher_rcs(rel_name_in_subrepo):
                    filekw = b"%s%s%s" % (_MARKER_RCS, kw, _MARKER_RCS)
                    filevalue = b"%s%s: %s %s" \
                                % (_MARKER_RCS, kw, value, _MARKER_RCS)
                    data = data.replace(filekw, filevalue)
                if matcher_rst(rel_name_in_subrepo):
                    filekw = b"%sVCS%s%s" % (_MARKER_RST, kw, _MARKER_RST)
                    filevalue = b"%s" % value   # always convert to a string
                    data = data.replace(filekw, filevalue)
        return data

    return _filter


def _make_repo_filterdata(ui, ctx, hgpath, hglocation, kwconfig,
                          path_filter, user_filter):
    keywords = make_node_keywords(ui, ctx,
                                  hgpath=hgpath,
                                  path_filter=path_filter,
                                  user_filter=user_filter,
                                  hglocation=hglocation)
    kwconfigdata = kwconfigname = None
    if kwconfig:
        kwconfigdata = util.posixfile(kwconfig, "rb").read()
        kwconfigname = kwconfig
    try:
        if kwconfigdata is None:
            # use versioned .hgkwarchive
            kwconfigdata = ctx[KWARCHIVE_CONFIG].data()
            kwconfigname = KWARCHIVE_CONFIG
    except (IOError, LookupError):
        # sigil for no keyword expansion configured -> filter is pass-through
        return None
    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(b"patterns"):
            for pattern, styles in cfg.items(b"patterns"):
                styles = [s.strip() for s in styles.upper().split(b',')]
                if b"YES" in styles or b"INCLUDE" in styles:
                    include.append(pattern)
                elif b"NO" in styles or b"EXCLUDE" in styles:
                    exclude.append(pattern)
                else:
                    patterns.append(pattern)
                if b"REST" in styles or b"RST" in styles \
                        or b"RCS" in styles:
                    if b"RCS" in styles:
                        patterns_rcs.append(pattern)
                    if b"REST" in styles or b"RST" in styles:
                        patterns_rst.append(pattern)
                else:
                    # default to RCS if no keyword style is given
                    patterns_rcs.append(pattern)
            matcher = match.match(ctx.repo().root, b'', patterns=patterns,
                                  include=include, exclude=exclude)
        else:
            matcher = _matchmod_never(ctx.repo().root, b'')

        #
        # An empty patterns_rcs does not mean that match_rcs is always
        # true.
        #
        if not patterns_rcs:
            matcher_rcs = _matchmod_never(ctx.repo().root, b'')
        else:
            matcher_rcs = match.match(ctx.repo().root, b'',
                                      patterns=patterns_rcs,
                                      include=[], exclude=[])
        #
        # An empty patterns_rst does not mean that match_rst is always
        # true.
        #
        if not patterns_rst:
            matcher_rst = _matchmod_never(ctx.repo().root, b'')
        else:
            matcher_rst = match.match(ctx.repo().root, b'',
                                      patterns=patterns_rst,
                                      include=[], exclude=[])

        #
        # This are the settings of the [keywords] section in .hgkwarchive.
        # An empty section means: all default keywords are enabled.
        # Otherwise only the given keywords are enabled with their expanded
        # values on the right side. An empty right side means: use the default
        # expansion.
        #
        keyword_substitutions = {}

        for alias, value in cfg.items(b"keywords"):
            keyword_substitutions[alias] = value

        #
        # Get the manifest to be able to determine a file's NodeId
        #
        manifest = ctx.manifest()

        return (matcher, matcher_rcs, matcher_rst,
                manifest,
                keyword_substitutions, keywords,)


def _amend_filterdata_by_subrepos(filterdata_by_subrepos,
                                  ui,
                                  ctx,
                                  kwconfig,
                                  path_filter, user_filter):

    for subpath in sorted(ctx.substate):
        substate = ctx.substate[subpath]
        # skip on non-Mercurial subrepos
        if substate[2] != b"hg":
            continue
        subrep = ctx.workingsub(subpath)
        subctx = subrep._getctx()

        # Really amend
        filterdata_by_subrepos[subrepo.subrelpath(subrep) + b'/'] = \
            _make_repo_filterdata(ui, subctx, None, substate[0], kwconfig,
                                  path_filter, user_filter)

        # Recursively check for other subrepos
        _amend_filterdata_by_subrepos(filterdata_by_subrepos,
                                      ui, subctx,
                                      kwconfig,
                                      path_filter, user_filter)


def make_node_keywords(ui, ctx,
                       hgpath=b"default",
                       path_filter=b"short",
                       user_filter=b"user",
                       hglocation=None):
    """Make all the node-specific (i.e. file-path independent) keywords

    """
    if path_filter == b"repo":
        path_uri = b"repo:" + ctx.repo()[ctx.repo().lookup(b'0')].hex()
    elif path_filter == b"reposhort":
        path_uri = b"repo:" + templatefilters.short(
            ctx.repo()[ctx.repo().lookup(b'0')].hex())
    else:
        if (hglocation is not None) and hgpath:
            raise ValueError("either `hgpath' or `hglocation` can be set")
        if (hglocation is not None) or (hgpath and hgpath != b"."):
            if hglocation is None:
                #
                # Since Mercurial 5.8 ui.paths[n] yields a list of
                # locations
                #
                locations = ui.paths[hgpath]
                if isinstance(locations, list):
                    # for now a quick check of assumptions
                    assert len(locations) == 1
                    hglocation = locations[0].loc
                else:
                    hglocation = locations.loc
            try:
                if path_filter == b"full":
                    path_uri = bytes(_urlutil.url(hglocation))
                elif path_filter == b"nopwd":
                    path_uri = _urlutil.hidepassword(hglocation)
                elif path_filter == b"nouser":
                    path_url = _urlutil.url(hglocation)
                    path_url.user = None
                    path_url.passwd = None
                    path_uri = bytes(path_url)
                elif path_filter == b"short":
                    path_url = _urlutil.url(hglocation)
                    path_url.scheme = SHORTENED_HG_SCHEMES.get(path_url.scheme,
                                                               b"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 == b"last":
                    path_url = str(_urlutil.url(hglocation)).split('/')
                    path_uri = pycompat.sysbytes(".../"+path_url[-1])
                else:
                    raise error.Abort(b"path-filter `%s' not implemented"
                                      % path_filter)
            except LookupError:
                raise error.Abort(
                    _(b"remote repository named `%s' not found") % hgpath)
        else:
            path_uri = ctx.repo().root
            if path_filter == b"last":
                m = max(path_uri.rfind(b"/"), path_uri.rfind(b"\\"))
                if m >= 0:
                    path_uri = b".../" + path_uri[m+1:]
        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 ((pycompat.ispy3 and ((b'A' <= pycompat.bytechr(path_uri[0]) <= b'Z') or (b'a' <= pycompat.bytechr(path_uri[0]) <= b'z'))) or ((not pycompat.ispy3 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
    if context_mapping_api:
        mapping = {
            b'repo': ctx.repo(),
            b'ctx': ctx,
            b'cache': {}
        }
        latesttags = templatekw.getlatesttags(_FakeRenderContext(), mapping)
    else:
        latesttags = templatekw.getlatesttags(ctx.repo(), ctx, {})
    keywords = {
        b"HGrepoid": ctx.repo()[ctx.repo().lookup(b'0')].hex(),   # repo id
        b"HGshortrepoid": templatefilters.short(ctx.repo()[ctx.repo().lookup(b'0')].hex()),    # short form of the repo id
        b"HGpath": path_uri,     # XXX FIXME: Should Archive an alias of this
        b"HGbranch": ctx.branch(),
        b"HGtags": b' '.join([tag for tag in ctx.tags() if tag != b"tip"]),
        b"HGlatesttags": b" ".join(latesttags[2]),
        b"HGlatesttagdistance": pycompat.sysbytes(str(latesttags[1])),
        b"HGlatesttagdate": templatefilters.isodatesec(_dateutil.makedate(latesttags[0])),
        b"HGlatesttagjustdate": templatefilters.shortdate(_dateutil.makedate(latesttags[0])),
        b"HGbookmarks": b' '.join([bm if not bm.startswith(b'*') else bm[1:]
                                  for bm in ctx.bookmarks() if bm != b"@"]),
        b"State": ctx.phasestr(),
        b"HGrevision": ctx.hex(),
        b"Revision": templatefilters.short(ctx.hex()),
        b"Date": templatefilters.isodatesec(ctx.date()),
        b"JustDate": templatefilters.shortdate(ctx.date()),
        # compatibility alias for `JustDate'
        b"HGshortdate": templatefilters.shortdate(ctx.date()),
    }
    keywords[b"Date2"] = keywords[b"Date"].replace(b"-", b"/", 2)
    keywords[b"JustDate2"] = keywords[b"JustDate"].replace(b"-", b"/", 2)
    if user_filter == b"user":
        keywords[b"Author"] = templatefilters.emailuser(ctx.user())
    elif user_filter == b"person":
        keywords[b"Author"] = templatefilters.utf8(
            templatefilters.person(ctx.user()).replace(b' ', b'+'))
    elif user_filter == b"email":
        keywords[b"Author"] = templatefilters.email(ctx.user())
    elif user_filter in (b"full", b"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[b"Author"] = templatefilters.utf8(ctx.user().replace(b' ', b'+'))
    else:
        raise error.Abort(_(b"unknown user filter"))

    return keywords


def make_file_keywords(keywords, rel_name, nodeid):
    return {
        b"HGsource": keywords[b"HGpath"] + b'/' + rel_name,
        b"Source": rel_name,
        b"File": templatefilters.basename(rel_name),
        b"Header": b"%s %s %s %s %s" % (rel_name, keywords[b"Revision"], keywords[b"Date"], keywords[b"Author"], keywords[b"State"]),
        b"Header2": b"%s %s %s %s %s" % (rel_name, keywords[b"Revision"], keywords[b"Date2"], keywords[b"Author"], keywords[b"State"]),
        b"HGid": b"%s %s %s %s %s" % (keywords[b"HGpath"] + b'/' + rel_name, keywords[b"Revision"], keywords[b"Date"], keywords[b"Author"], keywords[b"State"]),
        b"HGid2": b"%s %s %s %s %s" % (keywords[b"HGpath"] + b'/' + rel_name, keywords[b"Revision"], keywords[b"Date2"], keywords[b"Author"], keywords[b"State"]),
        b"HGheader": b"%s %s %s %s %s" % (keywords[b"HGpath"] + b'/' + rel_name, keywords[b"HGrevision"], keywords[b"Date"], keywords[b"Author"], keywords[b"State"]),
        b"HGheader2": b"%s %s %s %s %s" % (keywords[b"HGpath"] + b'/' + rel_name, keywords[b"HGrevision"], keywords[b"Date2"], keywords[b"Author"], keywords[b"State"]),
        b"Id": b"%s %s %s %s %s" % (templatefilters.basename(rel_name), keywords[b"Revision"], keywords[b"Date"], keywords[b"Author"], keywords[b"State"]),
        b"Id2": b"%s %s %s %s %s" % (templatefilters.basename(rel_name), keywords[b"Revision"], keywords[b"Date2"], keywords[b"Author"], keywords[b"State"]),
        b"HGnodeid": nodeid or b'',
    }


def stripped_hostname(hostname):
    """Return `hostname` without any domain port information"""
    if not hostname:
        return hostname
    idx = hostname.find(b'.')
    if idx < 0:
        return hostname
    return hostname[:idx]


def get_checked_user_filter_option(opts):
    uf = opts.get(b"user_filter")
    # "none" is retained for compatibility reasons and now an alias for "full"
    if uf not in (b"person", b"user", b"email", b"full", b"none"):
        raise error.Abort(
            _(b"user filter must be any of `user', `person', `email' or `none'"))
    return uf


def get_checked_path_filter_option(opts):
    pf = opts.get(b"path_filter")
    if pf not in (b"full", b"nopwd", b"nouser", b"short", b"last",
                  b"repo", b"reposhort"):
        raise error.Abort(
            _(b"path filter must be any of `full', `nopwd', `nouser`, `short', `last', `repo' or `reposhort'"))
    return pf


if hasattr(inspect, "getfullargspec"):
    # PY3
    _has_makefilename_ctx = "ctx" in inspect.getfullargspec(cmdutil.makefilename).args
    _has_matchmod_with_root = "root" in inspect.getfullargspec(match.exact).args
else:
    _has_makefilename_ctx = "ctx" in inspect.getargspec(cmdutil.makefilename).args
    _has_matchmod_with_root = "root" in inspect.getargspec(match.exact).args

if _has_makefilename_ctx:

    # 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)

if _has_matchmod_with_root:

    # Mercurial < 5

    _matchmod_never = match.never

else:

    def _matchmod_never(root, cwd, badfn=None):
        return match.never(badfn=badfn)


if context_mapping_api:

    class _FakeRenderContext(object):

        @staticmethod
        def resource(mapping, name):
            return mapping[name]