view mercurial/help.py @ 46582:b0a3ca02d17a

copies-rust: implement PartialEqual manually Now that we know that each (dest, rev) pair has at most a unique CopySource, we can simplify comparison a lot. This "simple" step buy a good share of the previous slowdown back in some case: Repo Case Source-Rev Dest-Rev # of revisions old time new time Difference Factor time per rev --------------------------------------------------------------------------------------------------------------------------------------------------------------- mozilla-try x00000_revs_x00000_added_x000_copies 9b2a99adc05e 8e29777b48e6 : 382065 revs, 43.304637 s, 34.443661 s, -8.860976 s, × 0.7954, 90 µs/rev Full benchmark: Repo Case Source-Rev Dest-Rev # of revisions old time new time Difference Factor time per rev --------------------------------------------------------------------------------------------------------------------------------------------------------------- mercurial x_revs_x_added_0_copies ad6b123de1c7 39cfcef4f463 : 1 revs, 0.000043 s, 0.000043 s, +0.000000 s, × 1.0000, 43 µs/rev mercurial x_revs_x_added_x_copies 2b1c78674230 0c1d10351869 : 6 revs, 0.000114 s, 0.000117 s, +0.000003 s, × 1.0263, 19 µs/rev mercurial x000_revs_x000_added_x_copies 81f8ff2a9bf2 dd3267698d84 : 1032 revs, 0.004937 s, 0.004892 s, -0.000045 s, × 0.9909, 4 µs/rev pypy x_revs_x_added_0_copies aed021ee8ae8 099ed31b181b : 9 revs, 0.000339 s, 0.000196 s, -0.000143 s, × 0.5782, 21 µs/rev pypy x_revs_x000_added_0_copies 4aa4e1f8e19a 359343b9ac0e : 1 revs, 0.000049 s, 0.000050 s, +0.000001 s, × 1.0204, 50 µs/rev pypy x_revs_x_added_x_copies ac52eb7bbbb0 72e022663155 : 7 revs, 0.000202 s, 0.000117 s, -0.000085 s, × 0.5792, 16 µs/rev pypy x_revs_x00_added_x_copies c3b14617fbd7 ace7255d9a26 : 1 revs, 0.000409 s, 0.6f1f4a s, -0.000087 s, × 0.7873, 322 µs/rev pypy x_revs_x000_added_x000_copies df6f7a526b60 a83dc6a2d56f : 6 revs, 0.011984 s, 0.011949 s, -0.000035 s, × 0.9971, 1991 µs/rev pypy x000_revs_xx00_added_0_copies 89a76aede314 2f22446ff07e : 4785 revs, 0.050820 s, 0.050802 s, -0.000018 s, × 0.9996, 10 µs/rev pypy x000_revs_x000_added_x_copies 8a3b5bfd266e 2c68e87c3efe : 6780 revs, 0.087953 s, 0.088090 s, +0.000137 s, × 1.0016, 12 µs/rev pypy x000_revs_x000_added_x000_copies 89a76aede314 7b3dda341c84 : 5441 revs, 0.062902 s, 0.062079 s, -0.000823 s, × 0.9869, 11 µs/rev pypy x0000_revs_x_added_0_copies d1defd0dc478 c9cb1334cc78 : 43645 revs, 0.679234 s, 0.635337 s, -0.043897 s, × 0.9354, 14 µs/rev pypy x0000_revs_xx000_added_0_copies bf2c629d0071 4ffed77c095c : 2 revs, 0.013095 s, 0.013262 s, +0.000167 s, × 1.0128, 6631 µs/rev pypy x0000_revs_xx000_added_x000_copies 08ea3258278e d9fa043f30c0 : 11316 revs, 0.120910 s, 0.120085 s, -0.000825 s, × 0.9932, 10 µs/rev netbeans x_revs_x_added_0_copies fb0955ffcbcd a01e9239f9e7 : 2 revs, 0.000087 s, 0.000085 s, -0.000002 s, × 0.9770, 42 µs/rev netbeans x_revs_x000_added_0_copies 6f360122949f 20eb231cc7d0 : 2 revs, 0.000107 s, 0.000110 s, +0.000003 s, × 1.0280, 55 µs/rev netbeans x_revs_x_added_x_copies 1ada3faf6fb6 5a39d12eecf4 : 3 revs, 0.000186 s, 0.000177 s, -0.000009 s, × 0.9516, 59 µs/rev netbeans x_revs_x00_added_x_copies 35be93ba1e2c 9eec5e90c05f : 9 revs, 0.000754 s, 0.000743 s, -0.000011 s, × 0.9854, 82 µs/rev netbeans x000_revs_xx00_added_0_copies eac3045b4fdd 51d4ae7f1290 : 1421 revs, 0.010443 s, 0.010168 s, -0.000275 s, × 0.9737, 7 µs/rev netbeans x000_revs_x000_added_x_copies e2063d266acd 6081d72689dc : 1533 revs, 0.015697 s, 0.015946 s, +0.000249 s, × 1.0159, 10 µs/rev netbeans x000_revs_x000_added_x000_copies ff453e9fee32 411350406ec2 : 5750 revs, 0.063528 s, 0.062712 s, -0.000816 s, × 0.9872, 10 µs/rev netbeans x0000_revs_xx000_added_x000_copies 588c2d1ced70 1aad62e59ddd : 66949 revs, 0.545515 s, 0.523832 s, -0.021683 s, × 0.9603, 7 µs/rev mozilla-central x_revs_x_added_0_copies 3697f962bb7b 7015fcdd43a2 : 2 revs, 0.000089 s, 0.000090 s, +0.000001 s, × 1.0112, 45 µs/rev mozilla-central x_revs_x000_added_0_copies dd390860c6c9 40d0c5bed75d : 8 revs, 0.000265 s, 0.000264 s, -0.000001 s, × 0.9962, 33 µs/rev mozilla-central x_revs_x_added_x_copies 8d198483ae3b 14207ffc2b2f : 9 revs, 0.000381 s, 0.000187 s, -0.000194 s, × 0.4908, 20 µs/rev mozilla-central x_revs_x00_added_x_copies 98cbc58cc6bc 446a150332c3 : 7 revs, 0.000672 s, 0.000665 s, -0.000007 s, × 0.9896, 95 µs/rev mozilla-central x_revs_x000_added_x000_copies 3c684b4b8f68 0a5e72d1b479 : 3 revs, 0.003497 s, 0.003556 s, +0.000059 s, × 1.0169, 1185 µs/rev mozilla-central x_revs_x0000_added_x0000_copies effb563bb7e5 c07a39dc4e80 : 6 revs, 0.073204 s, 0.071345 s, -0.001859 s, × 0.9746, 11890 µs/rev mozilla-central x000_revs_xx00_added_0_copies 6100d773079a 04a55431795e : 1593 revs, 0.006482 s, 0.006551 s, +0.000069 s, × 1.0106, 4 µs/rev mozilla-central x000_revs_x000_added_x_copies 9f17a6fc04f9 2d37b966abed : 41 revs, 0.005066 s, 0.005078 s, +0.000012 s, × 1.0024, 123 µs/rev mozilla-central x000_revs_x000_added_x000_copies 7c97034feb78 4407bd0c6330 : 7839 revs, 0.065707 s, 0.065823 s, +0.000116 s, × 1.0018, 8 µs/rev mozilla-central x0000_revs_xx000_added_0_copies 9eec5917337d 67118cc6dcad : 615 revs, 0.026800 s, 0.027050 s, +0.000250 s, × 1.0093, 43 µs/rev mozilla-central x0000_revs_xx000_added_x000_copies f78c615a656c 96a38b690156 : 30263 revs, 0.203856 s, 0.202443 s, -0.001413 s, × 0.9931, 6 µs/rev mozilla-central x00000_revs_x0000_added_x0000_copies 6832ae71433c 4c222a1d9a00 : 153721 revs, 1.293394 s, 1.261583 s, -0.031811 s, × 0.9754, 8 µs/rev mozilla-central x00000_revs_x00000_added_x000_copies 76caed42cf7c 1daa622bbe42 : 204976 revs, 1.698239 s, 1.643869 s, -0.054370 s, × 0.9680, 8 µs/rev mozilla-try x_revs_x_added_0_copies aaf6dde0deb8 9790f499805a : 2 revs, 0.000875 s, 0.000868 s, -0.000007 s, × 0.9920, 434 µs/rev mozilla-try x_revs_x000_added_0_copies d8d0222927b4 5bb8ce8c7450 : 2 revs, 0.000891 s, 0.000887 s, -0.000004 s, × 0.9955, 443 µs/rev mozilla-try x_revs_x_added_x_copies 092fcca11bdb 936255a0384a : 4 revs, 0.000292 s, 0.000168 s, -0.000124 s, × 0.5753, 42 µs/rev mozilla-try x_revs_x00_added_x_copies b53d2fadbdb5 017afae788ec : 2 revs, 0.003939 s, 0.001160 s, -0.002779 s, × 0.2945, 580 µs/rev mozilla-try x_revs_x000_added_x000_copies 20408ad61ce5 6f0ee96e21ad : 1 revs, 0.033027 s, 0.033016 s, -0.000011 s, × 0.9997, 33016 µs/rev mozilla-try x_revs_x0000_added_x0000_copies effb563bb7e5 c07a39dc4e80 : 6 revs, 0.073703 s, 0.073312 s, -0.39ae31 s, × 0.9947, 12218 µs/rev mozilla-try x000_revs_xx00_added_0_copies 6100d773079a 04a55431795e : 1593 revs, 0.006469 s, 0.006485 s, +0.000016 s, × 1.0025, 4 µs/rev mozilla-try x000_revs_x000_added_x_copies 9f17a6fc04f9 2d37b966abed : 41 revs, 0.005278 s, 0.005494 s, +0.000216 s, × 1.0409, 134 µs/rev mozilla-try x000_revs_x000_added_x000_copies 1346fd0130e4 4c65cbdabc1f : 6657 revs, 0.064995 s, 0.064879 s, -0.000116 s, × 0.9982, 9 µs/rev mozilla-try x0000_revs_x_added_0_copies 63519bfd42ee a36a2a865d92 : 40314 revs, 0.301041 s, 0.301469 s, +0.000428 s, × 1.0014, 7 µs/rev mozilla-try x0000_revs_x_added_x_copies 9fe69ff0762d bcabf2a78927 : 38690 revs, 0.285575 s, 0.297113 s, +0.011538 s, × 1.0404, 7 µs/rev mozilla-try x0000_revs_xx000_added_x_copies 156f6e2674f2 4d0f2c178e66 : 8598 revs, 0.085597 s, 0.085890 s, +0.000293 s, × 1.0034, 9 µs/rev mozilla-try x0000_revs_xx000_added_0_copies 9eec5917337d 67118cc6dcad : 615 revs, 0.027118 s, 0.027718 s, +0.000600 s, × 1.0221, 45 µs/rev mozilla-try x0000_revs_xx000_added_x000_copies 89294cd501d9 7ccb2fc7ccb5 : 97052 revs, 2.119204 s, 2.048949 s, -0.070255 s, × 0.9668, 21 µs/rev mozilla-try x0000_revs_x0000_added_x0000_copies e928c65095ed e951f4ad123a : 52031 revs, 0.701479 s, 0.685924 s, -0.015555 s, × 0.9778, 13 µs/rev mozilla-try x00000_revs_x_added_0_copies 6a320851d377 1ebb79acd503 : 363753 revs, 4.482399 s, 4.482891 s, +0.000492 s, × 1.0001, 12 µs/rev mozilla-try x00000_revs_x00000_added_0_copies dc8a3ca7010e d16fde900c9c : 34414 revs, 0.574082 s, 0.577633 s, +0.003551 s, × 1.0062, 16 µs/rev mozilla-try x00000_revs_x_added_x_copies 5173c4b6f97c 95d83ee7242d : 362229 revs, 4.480366 s, 4.397816 s, -0.082550 s, × 0.9816, 12 µs/rev mozilla-try x00000_revs_x000_added_x_copies 9126823d0e9c ca82787bb23c : 359344 revs, 4.369070 s, 4.370538 s, +0.001468 s, × 1.0003, 12 µs/rev mozilla-try x00000_revs_x0000_added_x0000_copies 8d3fafa80d4b eb884023b810 : 192665 revs, 1.592506 s, 1.570439 s, -0.022067 s, × 0.9861, 8 µs/rev mozilla-try x00000_revs_x00000_added_x0000_copies 1b661134e2ca 1ae03d022d6d : 228985 revs, 87.824489 s, 88.388512 s, +0.564023 s, × 1.0064, 386 µs/rev mozilla-try x00000_revs_x00000_added_x000_copies 9b2a99adc05e 8e29777b48e6 : 382065 revs, 43.304637 s, 34.443661 s, -8.860976 s, × 0.7954, 90 µs/rev private : 459513 revs, 33.853687 s, 27.370148 s, -6.483539 s, × 0.8085, 59 µs/rev Differential Revision: https://phab.mercurial-scm.org/D9653
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Wed, 16 Dec 2020 11:11:05 +0100
parents d481f30ea8e3
children d4ba4d51f85f
line wrap: on
line source

# help.py - help data for mercurial
#
# Copyright 2006 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

from __future__ import absolute_import

import itertools
import re
import textwrap

from .i18n import (
    _,
    gettext,
)
from .pycompat import getattr
from . import (
    cmdutil,
    encoding,
    error,
    extensions,
    fancyopts,
    filemerge,
    fileset,
    minirst,
    pycompat,
    registrar,
    revset,
    templatefilters,
    templatefuncs,
    templatekw,
    ui as uimod,
    util,
)
from .hgweb import webcommands
from .utils import (
    compression,
    resourceutil,
)

_exclkeywords = {
    b"(ADVANCED)",
    b"(DEPRECATED)",
    b"(EXPERIMENTAL)",
    # i18n: "(ADVANCED)" is a keyword, must be translated consistently
    _(b"(ADVANCED)"),
    # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
    _(b"(DEPRECATED)"),
    # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
    _(b"(EXPERIMENTAL)"),
}

# The order in which command categories will be displayed.
# Extensions with custom categories should insert them into this list
# after/before the appropriate item, rather than replacing the list or
# assuming absolute positions.
CATEGORY_ORDER = [
    registrar.command.CATEGORY_REPO_CREATION,
    registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
    registrar.command.CATEGORY_COMMITTING,
    registrar.command.CATEGORY_CHANGE_MANAGEMENT,
    registrar.command.CATEGORY_CHANGE_ORGANIZATION,
    registrar.command.CATEGORY_FILE_CONTENTS,
    registrar.command.CATEGORY_CHANGE_NAVIGATION,
    registrar.command.CATEGORY_WORKING_DIRECTORY,
    registrar.command.CATEGORY_IMPORT_EXPORT,
    registrar.command.CATEGORY_MAINTENANCE,
    registrar.command.CATEGORY_HELP,
    registrar.command.CATEGORY_MISC,
    registrar.command.CATEGORY_NONE,
]

# Human-readable category names. These are translated.
# Extensions with custom categories should add their names here.
CATEGORY_NAMES = {
    registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
    registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
    registrar.command.CATEGORY_COMMITTING: b'Change creation',
    registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
    registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
    registrar.command.CATEGORY_CHANGE_ORGANIZATION: b'Change organization',
    registrar.command.CATEGORY_WORKING_DIRECTORY: b'Working directory management',
    registrar.command.CATEGORY_FILE_CONTENTS: b'File content management',
    registrar.command.CATEGORY_IMPORT_EXPORT: b'Change import/export',
    registrar.command.CATEGORY_MAINTENANCE: b'Repository maintenance',
    registrar.command.CATEGORY_HELP: b'Help',
    registrar.command.CATEGORY_MISC: b'Miscellaneous commands',
    registrar.command.CATEGORY_NONE: b'Uncategorized commands',
}

# Topic categories.
TOPIC_CATEGORY_IDS = b'ids'
TOPIC_CATEGORY_OUTPUT = b'output'
TOPIC_CATEGORY_CONFIG = b'config'
TOPIC_CATEGORY_CONCEPTS = b'concepts'
TOPIC_CATEGORY_MISC = b'misc'
TOPIC_CATEGORY_NONE = b'none'

# The order in which topic categories will be displayed.
# Extensions with custom categories should insert them into this list
# after/before the appropriate item, rather than replacing the list or
# assuming absolute positions.
TOPIC_CATEGORY_ORDER = [
    TOPIC_CATEGORY_IDS,
    TOPIC_CATEGORY_OUTPUT,
    TOPIC_CATEGORY_CONFIG,
    TOPIC_CATEGORY_CONCEPTS,
    TOPIC_CATEGORY_MISC,
    TOPIC_CATEGORY_NONE,
]

# Human-readable topic category names. These are translated.
TOPIC_CATEGORY_NAMES = {
    TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
    TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
    TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
    TOPIC_CATEGORY_CONCEPTS: b'Concepts',
    TOPIC_CATEGORY_MISC: b'Miscellaneous',
    TOPIC_CATEGORY_NONE: b'Uncategorized topics',
}


def listexts(header, exts, indent=1, showdeprecated=False):
    '''return a text listing of the given extensions'''
    rst = []
    if exts:
        for name, desc in sorted(pycompat.iteritems(exts)):
            if not showdeprecated and any(w in desc for w in _exclkeywords):
                continue
            rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
    if rst:
        rst.insert(0, b'\n%s\n\n' % header)
    return rst


def extshelp(ui):
    rst = loaddoc(b'extensions')(ui).splitlines(True)
    rst.extend(
        listexts(
            _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
        )
    )
    rst.extend(
        listexts(
            _(b'disabled extensions:'),
            extensions.disabled(),
            showdeprecated=ui.verbose,
        )
    )
    doc = b''.join(rst)
    return doc


def parsedefaultmarker(text):
    """given a text 'abc (DEFAULT: def.ghi)',
    returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
    if text[-1:] == b')':
        marker = b' (DEFAULT: '
        pos = text.find(marker)
        if pos >= 0:
            item = text[pos + len(marker) : -1]
            return text[:pos], item.split(b'.', 2)


def optrst(header, options, verbose, ui):
    data = []
    multioccur = False
    for option in options:
        if len(option) == 5:
            shortopt, longopt, default, desc, optlabel = option
        else:
            shortopt, longopt, default, desc = option
            optlabel = _(b"VALUE")  # default label

        if not verbose and any(w in desc for w in _exclkeywords):
            continue
        defaultstrsuffix = b''
        if default is None:
            parseresult = parsedefaultmarker(desc)
            if parseresult is not None:
                (desc, (section, name)) = parseresult
                if ui.configbool(section, name):
                    default = True
                    defaultstrsuffix = _(b' from config')
        so = b''
        if shortopt:
            so = b'-' + shortopt
        lo = b'--' + longopt
        if default is True:
            lo = b'--[no-]' + longopt

        if isinstance(default, fancyopts.customopt):
            default = default.getdefaultvalue()
        if default and not callable(default):
            # default is of unknown type, and in Python 2 we abused
            # the %s-shows-repr property to handle integers etc. To
            # match that behavior on Python 3, we do str(default) and
            # then convert it to bytes.
            defaultstr = pycompat.bytestr(default)
            if default is True:
                defaultstr = _(b"on")
            desc += _(b" (default: %s)") % (defaultstr + defaultstrsuffix)

        if isinstance(default, list):
            lo += b" %s [+]" % optlabel
            multioccur = True
        elif (default is not None) and not isinstance(default, bool):
            lo += b" %s" % optlabel

        data.append((so, lo, desc))

    if multioccur:
        header += _(b" ([+] can be repeated)")

    rst = [b'\n%s:\n\n' % header]
    rst.extend(minirst.maketable(data, 1))

    return b''.join(rst)


def indicateomitted(rst, omitted, notomitted=None):
    rst.append(b'\n\n.. container:: omitted\n\n    %s\n\n' % omitted)
    if notomitted:
        rst.append(b'\n\n.. container:: notomitted\n\n    %s\n\n' % notomitted)


def filtercmd(ui, cmd, func, kw, doc):
    if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
        # Debug command, and user is not looking for those.
        return True
    if not ui.verbose:
        if not kw and not doc:
            # Command had no documentation, no point in showing it by default.
            return True
        if getattr(func, 'alias', False) and not getattr(func, 'owndoc', False):
            # Alias didn't have its own documentation.
            return True
        if doc and any(w in doc for w in _exclkeywords):
            # Documentation has excluded keywords.
            return True
    if kw == b"shortlist" and not getattr(func, 'helpbasic', False):
        # We're presenting the short list but the command is not basic.
        return True
    if ui.configbool(b'help', b'hidden-command.%s' % cmd):
        # Configuration explicitly hides the command.
        return True
    return False


def filtertopic(ui, topic):
    return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)


def topicmatch(ui, commands, kw):
    """Return help topics matching kw.

    Returns {'section': [(name, summary), ...], ...} where section is
    one of topics, commands, extensions, or extensioncommands.
    """
    kw = encoding.lower(kw)

    def lowercontains(container):
        return kw in encoding.lower(container)  # translated in helptable

    results = {
        b'topics': [],
        b'commands': [],
        b'extensions': [],
        b'extensioncommands': [],
    }
    for topic in helptable:
        names, header, doc = topic[0:3]
        # Old extensions may use a str as doc.
        if (
            sum(map(lowercontains, names))
            or lowercontains(header)
            or (callable(doc) and lowercontains(doc(ui)))
        ):
            name = names[0]
            if not filtertopic(ui, name):
                results[b'topics'].append((names[0], header))
    for cmd, entry in pycompat.iteritems(commands.table):
        if len(entry) == 3:
            summary = entry[2]
        else:
            summary = b''
        # translate docs *before* searching there
        func = entry[0]
        docs = _(pycompat.getdoc(func)) or b''
        if kw in cmd or lowercontains(summary) or lowercontains(docs):
            doclines = docs.splitlines()
            if doclines:
                summary = doclines[0]
            cmdname = cmdutil.parsealiases(cmd)[0]
            if filtercmd(ui, cmdname, func, kw, docs):
                continue
            results[b'commands'].append((cmdname, summary))
    for name, docs in itertools.chain(
        pycompat.iteritems(extensions.enabled(False)),
        pycompat.iteritems(extensions.disabled()),
    ):
        if not docs:
            continue
        name = name.rpartition(b'.')[-1]
        if lowercontains(name) or lowercontains(docs):
            # extension docs are already translated
            results[b'extensions'].append((name, docs.splitlines()[0]))
        try:
            mod = extensions.load(ui, name, b'')
        except ImportError:
            # debug message would be printed in extensions.load()
            continue
        for cmd, entry in pycompat.iteritems(getattr(mod, 'cmdtable', {})):
            if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
                cmdname = cmdutil.parsealiases(cmd)[0]
                func = entry[0]
                cmddoc = pycompat.getdoc(func)
                if cmddoc:
                    cmddoc = gettext(cmddoc).splitlines()[0]
                else:
                    cmddoc = _(b'(no help text available)')
                if filtercmd(ui, cmdname, func, kw, cmddoc):
                    continue
                results[b'extensioncommands'].append((cmdname, cmddoc))
    return results


def loaddoc(topic, subdir=None):
    """Return a delayed loader for help/topic.txt."""

    def loader(ui):
        package = b'mercurial.helptext'
        if subdir:
            package += b'.' + subdir
        with resourceutil.open_resource(package, topic + b'.txt') as fp:
            doc = gettext(fp.read())
        for rewriter in helphooks.get(topic, []):
            doc = rewriter(ui, topic, doc)
        return doc

    return loader


internalstable = sorted(
    [
        (
            [b'bid-merge'],
            _(b'Bid Merge Algorithm'),
            loaddoc(b'bid-merge', subdir=b'internals'),
        ),
        ([b'bundle2'], _(b'Bundle2'), loaddoc(b'bundle2', subdir=b'internals')),
        ([b'bundles'], _(b'Bundles'), loaddoc(b'bundles', subdir=b'internals')),
        ([b'cbor'], _(b'CBOR'), loaddoc(b'cbor', subdir=b'internals')),
        ([b'censor'], _(b'Censor'), loaddoc(b'censor', subdir=b'internals')),
        (
            [b'changegroups'],
            _(b'Changegroups'),
            loaddoc(b'changegroups', subdir=b'internals'),
        ),
        (
            [b'config'],
            _(b'Config Registrar'),
            loaddoc(b'config', subdir=b'internals'),
        ),
        (
            [b'extensions', b'extension'],
            _(b'Extension API'),
            loaddoc(b'extensions', subdir=b'internals'),
        ),
        (
            [b'mergestate'],
            _(b'Mergestate'),
            loaddoc(b'mergestate', subdir=b'internals'),
        ),
        (
            [b'requirements'],
            _(b'Repository Requirements'),
            loaddoc(b'requirements', subdir=b'internals'),
        ),
        (
            [b'revlogs'],
            _(b'Revision Logs'),
            loaddoc(b'revlogs', subdir=b'internals'),
        ),
        (
            [b'wireprotocol'],
            _(b'Wire Protocol'),
            loaddoc(b'wireprotocol', subdir=b'internals'),
        ),
        (
            [b'wireprotocolrpc'],
            _(b'Wire Protocol RPC'),
            loaddoc(b'wireprotocolrpc', subdir=b'internals'),
        ),
        (
            [b'wireprotocolv2'],
            _(b'Wire Protocol Version 2'),
            loaddoc(b'wireprotocolv2', subdir=b'internals'),
        ),
    ]
)


def internalshelp(ui):
    """Generate the index for the "internals" topic."""
    lines = [
        b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
        b'\n',
    ]
    for names, header, doc in internalstable:
        lines.append(b' :%s: %s\n' % (names[0], header))

    return b''.join(lines)


helptable = sorted(
    [
        (
            [b'bundlespec'],
            _(b"Bundle File Formats"),
            loaddoc(b'bundlespec'),
            TOPIC_CATEGORY_CONCEPTS,
        ),
        (
            [b'color'],
            _(b"Colorizing Outputs"),
            loaddoc(b'color'),
            TOPIC_CATEGORY_OUTPUT,
        ),
        (
            [b"config", b"hgrc"],
            _(b"Configuration Files"),
            loaddoc(b'config'),
            TOPIC_CATEGORY_CONFIG,
        ),
        (
            [b'deprecated'],
            _(b"Deprecated Features"),
            loaddoc(b'deprecated'),
            TOPIC_CATEGORY_MISC,
        ),
        (
            [b"dates"],
            _(b"Date Formats"),
            loaddoc(b'dates'),
            TOPIC_CATEGORY_OUTPUT,
        ),
        (
            [b"flags"],
            _(b"Command-line flags"),
            loaddoc(b'flags'),
            TOPIC_CATEGORY_CONFIG,
        ),
        (
            [b"patterns"],
            _(b"File Name Patterns"),
            loaddoc(b'patterns'),
            TOPIC_CATEGORY_IDS,
        ),
        (
            [b'environment', b'env'],
            _(b'Environment Variables'),
            loaddoc(b'environment'),
            TOPIC_CATEGORY_CONFIG,
        ),
        (
            [
                b'revisions',
                b'revs',
                b'revsets',
                b'revset',
                b'multirevs',
                b'mrevs',
            ],
            _(b'Specifying Revisions'),
            loaddoc(b'revisions'),
            TOPIC_CATEGORY_IDS,
        ),
        (
            [b'filesets', b'fileset'],
            _(b"Specifying File Sets"),
            loaddoc(b'filesets'),
            TOPIC_CATEGORY_IDS,
        ),
        (
            [b'diffs'],
            _(b'Diff Formats'),
            loaddoc(b'diffs'),
            TOPIC_CATEGORY_OUTPUT,
        ),
        (
            [b'merge-tools', b'mergetools', b'mergetool'],
            _(b'Merge Tools'),
            loaddoc(b'merge-tools'),
            TOPIC_CATEGORY_CONFIG,
        ),
        (
            [b'templating', b'templates', b'template', b'style'],
            _(b'Template Usage'),
            loaddoc(b'templates'),
            TOPIC_CATEGORY_OUTPUT,
        ),
        ([b'urls'], _(b'URL Paths'), loaddoc(b'urls'), TOPIC_CATEGORY_IDS),
        (
            [b"extensions"],
            _(b"Using Additional Features"),
            extshelp,
            TOPIC_CATEGORY_CONFIG,
        ),
        (
            [b"subrepos", b"subrepo"],
            _(b"Subrepositories"),
            loaddoc(b'subrepos'),
            TOPIC_CATEGORY_CONCEPTS,
        ),
        (
            [b"hgweb"],
            _(b"Configuring hgweb"),
            loaddoc(b'hgweb'),
            TOPIC_CATEGORY_CONFIG,
        ),
        (
            [b"glossary"],
            _(b"Glossary"),
            loaddoc(b'glossary'),
            TOPIC_CATEGORY_CONCEPTS,
        ),
        (
            [b"hgignore", b"ignore"],
            _(b"Syntax for Mercurial Ignore Files"),
            loaddoc(b'hgignore'),
            TOPIC_CATEGORY_IDS,
        ),
        (
            [b"phases"],
            _(b"Working with Phases"),
            loaddoc(b'phases'),
            TOPIC_CATEGORY_CONCEPTS,
        ),
        (
            [b'scripting'],
            _(b'Using Mercurial from scripts and automation'),
            loaddoc(b'scripting'),
            TOPIC_CATEGORY_MISC,
        ),
        (
            [b'internals'],
            _(b"Technical implementation topics"),
            internalshelp,
            TOPIC_CATEGORY_MISC,
        ),
        (
            [b'pager'],
            _(b"Pager Support"),
            loaddoc(b'pager'),
            TOPIC_CATEGORY_CONFIG,
        ),
    ]
)

# Maps topics with sub-topics to a list of their sub-topics.
subtopics = {
    b'internals': internalstable,
}

# Map topics to lists of callable taking the current topic help and
# returning the updated version
helphooks = {}


def addtopichook(topic, rewriter):
    helphooks.setdefault(topic, []).append(rewriter)


def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
    """Extract docstring from the items key to function mapping, build a
    single documentation block and use it to overwrite the marker in doc.
    """
    entries = []
    for name in sorted(items):
        text = (pycompat.getdoc(items[name]) or b'').rstrip()
        if not text or not ui.verbose and any(w in text for w in _exclkeywords):
            continue
        text = gettext(text)
        if dedent:
            # Abuse latin1 to use textwrap.dedent() on bytes.
            text = textwrap.dedent(text.decode('latin1')).encode('latin1')
        lines = text.splitlines()
        doclines = [(lines[0])]
        for l in lines[1:]:
            # Stop once we find some Python doctest
            if l.strip().startswith(b'>>>'):
                break
            if dedent:
                doclines.append(l.rstrip())
            else:
                doclines.append(b'  ' + l.strip())
        entries.append(b'\n'.join(doclines))
    entries = b'\n\n'.join(entries)
    return doc.replace(marker, entries)


def addtopicsymbols(topic, marker, symbols, dedent=False):
    def add(ui, topic, doc):
        return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)

    addtopichook(topic, add)


addtopicsymbols(
    b'bundlespec',
    b'.. bundlecompressionmarker',
    compression.bundlecompressiontopics(),
)
addtopicsymbols(b'filesets', b'.. predicatesmarker', fileset.symbols)
addtopicsymbols(
    b'merge-tools', b'.. internaltoolsmarker', filemerge.internalsdoc
)
addtopicsymbols(b'revisions', b'.. predicatesmarker', revset.symbols)
addtopicsymbols(b'templates', b'.. keywordsmarker', templatekw.keywords)
addtopicsymbols(b'templates', b'.. filtersmarker', templatefilters.filters)
addtopicsymbols(b'templates', b'.. functionsmarker', templatefuncs.funcs)
addtopicsymbols(
    b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
)


def inserttweakrc(ui, topic, doc):
    marker = b'.. tweakdefaultsmarker'
    repl = uimod.tweakrc

    def sub(m):
        lines = [m.group(1) + s for s in repl.splitlines()]
        return b'\n'.join(lines)

    return re.sub(br'( *)%s' % re.escape(marker), sub, doc)


def _getcategorizedhelpcmds(ui, cmdtable, name, select=None):
    # Category -> list of commands
    cats = {}
    # Command -> short description
    h = {}
    # Command -> string showing synonyms
    syns = {}
    for c, e in pycompat.iteritems(cmdtable):
        fs = cmdutil.parsealiases(c)
        f = fs[0]
        syns[f] = fs
        func = e[0]
        if select and not select(f):
            continue
        doc = pycompat.getdoc(func)
        if filtercmd(ui, f, func, name, doc):
            continue
        doc = gettext(doc)
        if not doc:
            doc = _(b"(no help text available)")
        h[f] = doc.splitlines()[0].rstrip()

        cat = getattr(func, 'helpcategory', None) or (
            registrar.command.CATEGORY_NONE
        )
        cats.setdefault(cat, []).append(f)
    return cats, h, syns


def _getcategorizedhelptopics(ui, topictable):
    # Group commands by category.
    topiccats = {}
    syns = {}
    for topic in topictable:
        names, header, doc = topic[0:3]
        if len(topic) > 3 and topic[3]:
            category = topic[3]
        else:
            category = TOPIC_CATEGORY_NONE

        topicname = names[0]
        syns[topicname] = list(names)
        if not filtertopic(ui, topicname):
            topiccats.setdefault(category, []).append((topicname, header))
    return topiccats, syns


addtopichook(b'config', inserttweakrc)


def help_(
    ui,
    commands,
    name,
    unknowncmd=False,
    full=True,
    subtopic=None,
    fullname=None,
    **opts
):
    """
    Generate the help for 'name' as unformatted restructured text. If
    'name' is None, describe the commands available.
    """

    opts = pycompat.byteskwargs(opts)

    def helpcmd(name, subtopic=None):
        try:
            aliases, entry = cmdutil.findcmd(
                name, commands.table, strict=unknowncmd
            )
        except error.AmbiguousCommand as inst:
            # py3 fix: except vars can't be used outside the scope of the
            # except block, nor can be used inside a lambda. python issue4617
            prefix = inst.prefix
            select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
            rst = helplist(select)
            return rst

        rst = []

        # check if it's an invalid alias and display its error if it is
        if getattr(entry[0], 'badalias', None):
            rst.append(entry[0].badalias + b'\n')
            if entry[0].unknowncmd:
                try:
                    rst.extend(helpextcmd(entry[0].cmdname))
                except error.UnknownCommand:
                    pass
            return rst

        # synopsis
        if len(entry) > 2:
            if entry[2].startswith(b'hg'):
                rst.append(b"%s\n" % entry[2])
            else:
                rst.append(b'hg %s %s\n' % (aliases[0], entry[2]))
        else:
            rst.append(b'hg %s\n' % aliases[0])
        # aliases
        if full and not ui.quiet and len(aliases) > 1:
            rst.append(_(b"\naliases: %s\n") % b', '.join(aliases[1:]))
        rst.append(b'\n')

        # description
        doc = gettext(pycompat.getdoc(entry[0]))
        if not doc:
            doc = _(b"(no help text available)")
        if util.safehasattr(entry[0], b'definition'):  # aliased command
            source = entry[0].source
            if entry[0].definition.startswith(b'!'):  # shell alias
                doc = _(b'shell alias for: %s\n\n%s\n\ndefined by: %s\n') % (
                    entry[0].definition[1:],
                    doc,
                    source,
                )
            else:
                doc = _(b'alias for: hg %s\n\n%s\n\ndefined by: %s\n') % (
                    entry[0].definition,
                    doc,
                    source,
                )
        doc = doc.splitlines(True)
        if ui.quiet or not full:
            rst.append(doc[0])
        else:
            rst.extend(doc)
        rst.append(b'\n')

        # check if this command shadows a non-trivial (multi-line)
        # extension help text
        try:
            mod = extensions.find(name)
            doc = gettext(pycompat.getdoc(mod)) or b''
            if b'\n' in doc.strip():
                msg = _(
                    b"(use 'hg help -e %s' to show help for "
                    b"the %s extension)"
                ) % (name, name)
                rst.append(b'\n%s\n' % msg)
        except KeyError:
            pass

        # options
        if not ui.quiet and entry[1]:
            rst.append(optrst(_(b"options"), entry[1], ui.verbose, ui))

        if ui.verbose:
            rst.append(
                optrst(
                    _(b"global options"), commands.globalopts, ui.verbose, ui
                )
            )

        if not ui.verbose:
            if not full:
                rst.append(_(b"\n(use 'hg %s -h' to show more help)\n") % name)
            elif not ui.quiet:
                rst.append(
                    _(
                        b'\n(some details hidden, use --verbose '
                        b'to show complete help)'
                    )
                )

        return rst

    def helplist(select=None, **opts):
        cats, h, syns = _getcategorizedhelpcmds(
            ui, commands.table, name, select
        )

        rst = []
        if not h:
            if not ui.quiet:
                rst.append(_(b'no commands defined\n'))
            return rst

        # Output top header.
        if not ui.quiet:
            if name == b"shortlist":
                rst.append(_(b'basic commands:\n\n'))
            elif name == b"debug":
                rst.append(_(b'debug commands (internal and unsupported):\n\n'))
            else:
                rst.append(_(b'list of commands:\n'))

        def appendcmds(cmds):
            cmds = sorted(cmds)
            for c in cmds:
                display_cmd = c
                if ui.verbose:
                    display_cmd = b', '.join(syns[c])
                display_cmd = display_cmd.replace(b':', br'\:')
                rst.append(b' :%s: %s\n' % (display_cmd, h[c]))

        if name in (b'shortlist', b'debug'):
            # List without categories.
            appendcmds(h)
        else:
            # Check that all categories have an order.
            missing_order = set(cats.keys()) - set(CATEGORY_ORDER)
            if missing_order:
                ui.develwarn(
                    b'help categories missing from CATEGORY_ORDER: %s'
                    % missing_order
                )

            # List per category.
            for cat in CATEGORY_ORDER:
                catfns = cats.get(cat, [])
                if catfns:
                    if len(cats) > 1:
                        catname = gettext(CATEGORY_NAMES[cat])
                        rst.append(b"\n%s:\n" % catname)
                    rst.append(b"\n")
                    appendcmds(catfns)

        ex = opts.get
        anyopts = ex('keyword') or not (ex('command') or ex('extension'))
        if not name and anyopts:
            exts = listexts(
                _(b'enabled extensions:'),
                extensions.enabled(),
                showdeprecated=ui.verbose,
            )
            if exts:
                rst.append(b'\n')
                rst.extend(exts)

            rst.append(_(b"\nadditional help topics:\n"))
            topiccats, topicsyns = _getcategorizedhelptopics(ui, helptable)

            # Check that all categories have an order.
            missing_order = set(topiccats.keys()) - set(TOPIC_CATEGORY_ORDER)
            if missing_order:
                ui.develwarn(
                    b'help categories missing from TOPIC_CATEGORY_ORDER: %s'
                    % missing_order
                )

            # Output topics per category.
            for cat in TOPIC_CATEGORY_ORDER:
                topics = topiccats.get(cat, [])
                if topics:
                    if len(topiccats) > 1:
                        catname = gettext(TOPIC_CATEGORY_NAMES[cat])
                        rst.append(b"\n%s:\n" % catname)
                    rst.append(b"\n")
                    for t, desc in topics:
                        rst.append(b" :%s: %s\n" % (t, desc))

        if ui.quiet:
            pass
        elif ui.verbose:
            rst.append(
                b'\n%s\n'
                % optrst(
                    _(b"global options"), commands.globalopts, ui.verbose, ui
                )
            )
            if name == b'shortlist':
                rst.append(
                    _(b"\n(use 'hg help' for the full list of commands)\n")
                )
        else:
            if name == b'shortlist':
                rst.append(
                    _(
                        b"\n(use 'hg help' for the full list of commands "
                        b"or 'hg -v' for details)\n"
                    )
                )
            elif name and not full:
                rst.append(
                    _(b"\n(use 'hg help %s' to show the full help text)\n")
                    % name
                )
            elif name and syns and name in syns.keys():
                rst.append(
                    _(
                        b"\n(use 'hg help -v -e %s' to show built-in "
                        b"aliases and global options)\n"
                    )
                    % name
                )
            else:
                rst.append(
                    _(
                        b"\n(use 'hg help -v%s' to show built-in aliases "
                        b"and global options)\n"
                    )
                    % (name and b" " + name or b"")
                )
        return rst

    def helptopic(name, subtopic=None):
        # Look for sub-topic entry first.
        header, doc = None, None
        if subtopic and name in subtopics:
            for names, header, doc in subtopics[name]:
                if subtopic in names:
                    break
            if not any(subtopic in s[0] for s in subtopics[name]):
                raise error.UnknownCommand(name)

        if not header:
            for topic in helptable:
                names, header, doc = topic[0:3]
                if name in names:
                    break
            else:
                raise error.UnknownCommand(name)

        rst = [minirst.section(header)]

        # description
        if not doc:
            rst.append(b"    %s\n" % _(b"(no help text available)"))
        if callable(doc):
            rst += [b"    %s\n" % l for l in doc(ui).splitlines()]

        if not ui.verbose:
            omitted = _(
                b'(some details hidden, use --verbose'
                b' to show complete help)'
            )
            indicateomitted(rst, omitted)

        try:
            cmdutil.findcmd(name, commands.table)
            rst.append(
                _(b"\nuse 'hg help -c %s' to see help for the %s command\n")
                % (name, name)
            )
        except error.UnknownCommand:
            pass
        return rst

    def helpext(name, subtopic=None):
        try:
            mod = extensions.find(name)
            doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
        except KeyError:
            mod = None
            doc = extensions.disabled_help(name)
            if not doc:
                raise error.UnknownCommand(name)

        if b'\n' not in doc:
            head, tail = doc, b""
        else:
            head, tail = doc.split(b'\n', 1)
        rst = [_(b'%s extension - %s\n\n') % (name.rpartition(b'.')[-1], head)]
        if tail:
            rst.extend(tail.splitlines(True))
            rst.append(b'\n')

        if not ui.verbose:
            omitted = _(
                b'(some details hidden, use --verbose'
                b' to show complete help)'
            )
            indicateomitted(rst, omitted)

        if mod:
            try:
                ct = mod.cmdtable
            except AttributeError:
                ct = {}
            modcmds = {c.partition(b'|')[0] for c in ct}
            rst.extend(helplist(modcmds.__contains__))
        else:
            rst.append(
                _(
                    b"(use 'hg help extensions' for information on enabling"
                    b" extensions)\n"
                )
            )
        return rst

    def helpextcmd(name, subtopic=None):
        cmd, ext, doc = extensions.disabledcmd(
            ui, name, ui.configbool(b'ui', b'strict')
        )
        doc = doc.splitlines()[0]

        rst = listexts(
            _(b"'%s' is provided by the following extension:") % cmd,
            {ext: doc},
            indent=4,
            showdeprecated=True,
        )
        rst.append(b'\n')
        rst.append(
            _(
                b"(use 'hg help extensions' for information on enabling "
                b"extensions)\n"
            )
        )
        return rst

    rst = []
    kw = opts.get(b'keyword')
    if kw or name is None and any(opts[o] for o in opts):
        matches = topicmatch(ui, commands, name or b'')
        helpareas = []
        if opts.get(b'extension'):
            helpareas += [(b'extensions', _(b'Extensions'))]
        if opts.get(b'command'):
            helpareas += [(b'commands', _(b'Commands'))]
        if not helpareas:
            helpareas = [
                (b'topics', _(b'Topics')),
                (b'commands', _(b'Commands')),
                (b'extensions', _(b'Extensions')),
                (b'extensioncommands', _(b'Extension Commands')),
            ]
        for t, title in helpareas:
            if matches[t]:
                rst.append(b'%s:\n\n' % title)
                rst.extend(minirst.maketable(sorted(matches[t]), 1))
                rst.append(b'\n')
        if not rst:
            msg = _(b'no matches')
            hint = _(b"try 'hg help' for a list of topics")
            raise error.InputError(msg, hint=hint)
    elif name and name != b'shortlist':
        queries = []
        if unknowncmd:
            queries += [helpextcmd]
        if opts.get(b'extension'):
            queries += [helpext]
        if opts.get(b'command'):
            queries += [helpcmd]
        if not queries:
            queries = (helptopic, helpcmd, helpext, helpextcmd)
        for f in queries:
            try:
                rst = f(name, subtopic)
                break
            except error.UnknownCommand:
                pass
        else:
            if unknowncmd:
                raise error.UnknownCommand(name)
            else:
                if fullname:
                    formatname = fullname
                else:
                    formatname = name
                if subtopic:
                    hintname = subtopic
                else:
                    hintname = name
                msg = _(b'no such help topic: %s') % formatname
                hint = _(b"try 'hg help --keyword %s'") % hintname
                raise error.InputError(msg, hint=hint)
    else:
        # program name
        if not ui.quiet:
            rst = [_(b"Mercurial Distributed SCM\n"), b'\n']
        rst.extend(helplist(None, **pycompat.strkwargs(opts)))

    return b''.join(rst)


def formattedhelp(
    ui, commands, fullname, keep=None, unknowncmd=False, full=True, **opts
):
    """get help for a given topic (as a dotted name) as rendered rst

    Either returns the rendered help text or raises an exception.
    """
    if keep is None:
        keep = []
    else:
        keep = list(keep)  # make a copy so we can mutate this later

    # <fullname> := <name>[.<subtopic][.<section>]
    name = subtopic = section = None
    if fullname is not None:
        nameparts = fullname.split(b'.')
        name = nameparts.pop(0)
        if nameparts and name in subtopics:
            subtopic = nameparts.pop(0)
        if nameparts:
            section = encoding.lower(b'.'.join(nameparts))

    textwidth = ui.configint(b'ui', b'textwidth')
    termwidth = ui.termwidth() - 2
    if textwidth <= 0 or termwidth < textwidth:
        textwidth = termwidth
    text = help_(
        ui,
        commands,
        name,
        fullname=fullname,
        subtopic=subtopic,
        unknowncmd=unknowncmd,
        full=full,
        **opts
    )

    blocks, pruned = minirst.parse(text, keep=keep)
    if b'verbose' in pruned:
        keep.append(b'omitted')
    else:
        keep.append(b'notomitted')
    blocks, pruned = minirst.parse(text, keep=keep)
    if section:
        blocks = minirst.filtersections(blocks, section)

    # We could have been given a weird ".foo" section without a name
    # to look for, or we could have simply failed to found "foo.bar"
    # because bar isn't a section of foo
    if section and not (blocks and name):
        raise error.InputError(_(b"help section not found: %s") % fullname)

    return minirst.formatplain(blocks, textwidth)