1# help.py - help data for mercurial
2#
3# Copyright 2006 Olivia Mackall <olivia@selenic.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8from __future__ import absolute_import
9
10import itertools
11import re
12import textwrap
13
14from .i18n import (
15    _,
16    gettext,
17)
18from .pycompat import getattr
19from . import (
20    cmdutil,
21    encoding,
22    error,
23    extensions,
24    fancyopts,
25    filemerge,
26    fileset,
27    minirst,
28    pycompat,
29    registrar,
30    revset,
31    templatefilters,
32    templatefuncs,
33    templatekw,
34    ui as uimod,
35    util,
36)
37from .hgweb import webcommands
38from .utils import (
39    compression,
40    resourceutil,
41)
42
43_exclkeywords = {
44    b"(ADVANCED)",
45    b"(DEPRECATED)",
46    b"(EXPERIMENTAL)",
47    # i18n: "(ADVANCED)" is a keyword, must be translated consistently
48    _(b"(ADVANCED)"),
49    # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
50    _(b"(DEPRECATED)"),
51    # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
52    _(b"(EXPERIMENTAL)"),
53}
54
55# The order in which command categories will be displayed.
56# Extensions with custom categories should insert them into this list
57# after/before the appropriate item, rather than replacing the list or
58# assuming absolute positions.
59CATEGORY_ORDER = [
60    registrar.command.CATEGORY_REPO_CREATION,
61    registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
62    registrar.command.CATEGORY_COMMITTING,
63    registrar.command.CATEGORY_CHANGE_MANAGEMENT,
64    registrar.command.CATEGORY_CHANGE_ORGANIZATION,
65    registrar.command.CATEGORY_FILE_CONTENTS,
66    registrar.command.CATEGORY_CHANGE_NAVIGATION,
67    registrar.command.CATEGORY_WORKING_DIRECTORY,
68    registrar.command.CATEGORY_IMPORT_EXPORT,
69    registrar.command.CATEGORY_MAINTENANCE,
70    registrar.command.CATEGORY_HELP,
71    registrar.command.CATEGORY_MISC,
72    registrar.command.CATEGORY_NONE,
73]
74
75# Human-readable category names. These are translated.
76# Extensions with custom categories should add their names here.
77CATEGORY_NAMES = {
78    registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
79    registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
80    registrar.command.CATEGORY_COMMITTING: b'Change creation',
81    registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
82    registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
83    registrar.command.CATEGORY_CHANGE_ORGANIZATION: b'Change organization',
84    registrar.command.CATEGORY_WORKING_DIRECTORY: b'Working directory management',
85    registrar.command.CATEGORY_FILE_CONTENTS: b'File content management',
86    registrar.command.CATEGORY_IMPORT_EXPORT: b'Change import/export',
87    registrar.command.CATEGORY_MAINTENANCE: b'Repository maintenance',
88    registrar.command.CATEGORY_HELP: b'Help',
89    registrar.command.CATEGORY_MISC: b'Miscellaneous commands',
90    registrar.command.CATEGORY_NONE: b'Uncategorized commands',
91}
92
93# Topic categories.
94TOPIC_CATEGORY_IDS = b'ids'
95TOPIC_CATEGORY_OUTPUT = b'output'
96TOPIC_CATEGORY_CONFIG = b'config'
97TOPIC_CATEGORY_CONCEPTS = b'concepts'
98TOPIC_CATEGORY_MISC = b'misc'
99TOPIC_CATEGORY_NONE = b'none'
100
101# The order in which topic categories will be displayed.
102# Extensions with custom categories should insert them into this list
103# after/before the appropriate item, rather than replacing the list or
104# assuming absolute positions.
105TOPIC_CATEGORY_ORDER = [
106    TOPIC_CATEGORY_IDS,
107    TOPIC_CATEGORY_OUTPUT,
108    TOPIC_CATEGORY_CONFIG,
109    TOPIC_CATEGORY_CONCEPTS,
110    TOPIC_CATEGORY_MISC,
111    TOPIC_CATEGORY_NONE,
112]
113
114# Human-readable topic category names. These are translated.
115TOPIC_CATEGORY_NAMES = {
116    TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
117    TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
118    TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
119    TOPIC_CATEGORY_CONCEPTS: b'Concepts',
120    TOPIC_CATEGORY_MISC: b'Miscellaneous',
121    TOPIC_CATEGORY_NONE: b'Uncategorized topics',
122}
123
124
125def listexts(header, exts, indent=1, showdeprecated=False):
126    '''return a text listing of the given extensions'''
127    rst = []
128    if exts:
129        for name, desc in sorted(pycompat.iteritems(exts)):
130            if not showdeprecated and any(w in desc for w in _exclkeywords):
131                continue
132            rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
133    if rst:
134        rst.insert(0, b'\n%s\n\n' % header)
135    return rst
136
137
138def extshelp(ui):
139    rst = loaddoc(b'extensions')(ui).splitlines(True)
140    rst.extend(
141        listexts(
142            _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
143        )
144    )
145    rst.extend(
146        listexts(
147            _(b'disabled extensions:'),
148            extensions.disabled(),
149            showdeprecated=ui.verbose,
150        )
151    )
152    doc = b''.join(rst)
153    return doc
154
155
156def parsedefaultmarker(text):
157    """given a text 'abc (DEFAULT: def.ghi)',
158    returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
159    if text[-1:] == b')':
160        marker = b' (DEFAULT: '
161        pos = text.find(marker)
162        if pos >= 0:
163            item = text[pos + len(marker) : -1]
164            return text[:pos], item.split(b'.', 2)
165
166
167def optrst(header, options, verbose, ui):
168    data = []
169    multioccur = False
170    for option in options:
171        if len(option) == 5:
172            shortopt, longopt, default, desc, optlabel = option
173        else:
174            shortopt, longopt, default, desc = option
175            optlabel = _(b"VALUE")  # default label
176
177        if not verbose and any(w in desc for w in _exclkeywords):
178            continue
179        defaultstrsuffix = b''
180        if default is None:
181            parseresult = parsedefaultmarker(desc)
182            if parseresult is not None:
183                (desc, (section, name)) = parseresult
184                if ui.configbool(section, name):
185                    default = True
186                    defaultstrsuffix = _(b' from config')
187        so = b''
188        if shortopt:
189            so = b'-' + shortopt
190        lo = b'--' + longopt
191        if default is True:
192            lo = b'--[no-]' + longopt
193
194        if isinstance(default, fancyopts.customopt):
195            default = default.getdefaultvalue()
196        if default and not callable(default):
197            # default is of unknown type, and in Python 2 we abused
198            # the %s-shows-repr property to handle integers etc. To
199            # match that behavior on Python 3, we do str(default) and
200            # then convert it to bytes.
201            defaultstr = pycompat.bytestr(default)
202            if default is True:
203                defaultstr = _(b"on")
204            desc += _(b" (default: %s)") % (defaultstr + defaultstrsuffix)
205
206        if isinstance(default, list):
207            lo += b" %s [+]" % optlabel
208            multioccur = True
209        elif (default is not None) and not isinstance(default, bool):
210            lo += b" %s" % optlabel
211
212        data.append((so, lo, desc))
213
214    if multioccur:
215        header += _(b" ([+] can be repeated)")
216
217    rst = [b'\n%s:\n\n' % header]
218    rst.extend(minirst.maketable(data, 1))
219
220    return b''.join(rst)
221
222
223def indicateomitted(rst, omitted, notomitted=None):
224    rst.append(b'\n\n.. container:: omitted\n\n    %s\n\n' % omitted)
225    if notomitted:
226        rst.append(b'\n\n.. container:: notomitted\n\n    %s\n\n' % notomitted)
227
228
229def filtercmd(ui, cmd, func, kw, doc):
230    if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
231        # Debug command, and user is not looking for those.
232        return True
233    if not ui.verbose:
234        if not kw and not doc:
235            # Command had no documentation, no point in showing it by default.
236            return True
237        if getattr(func, 'alias', False) and not getattr(func, 'owndoc', False):
238            # Alias didn't have its own documentation.
239            return True
240        if doc and any(w in doc for w in _exclkeywords):
241            # Documentation has excluded keywords.
242            return True
243    if kw == b"shortlist" and not getattr(func, 'helpbasic', False):
244        # We're presenting the short list but the command is not basic.
245        return True
246    if ui.configbool(b'help', b'hidden-command.%s' % cmd):
247        # Configuration explicitly hides the command.
248        return True
249    return False
250
251
252def filtertopic(ui, topic):
253    return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
254
255
256def topicmatch(ui, commands, kw):
257    """Return help topics matching kw.
258
259    Returns {'section': [(name, summary), ...], ...} where section is
260    one of topics, commands, extensions, or extensioncommands.
261    """
262    kw = encoding.lower(kw)
263
264    def lowercontains(container):
265        return kw in encoding.lower(container)  # translated in helptable
266
267    results = {
268        b'topics': [],
269        b'commands': [],
270        b'extensions': [],
271        b'extensioncommands': [],
272    }
273    for topic in helptable:
274        names, header, doc = topic[0:3]
275        # Old extensions may use a str as doc.
276        if (
277            sum(map(lowercontains, names))
278            or lowercontains(header)
279            or (callable(doc) and lowercontains(doc(ui)))
280        ):
281            name = names[0]
282            if not filtertopic(ui, name):
283                results[b'topics'].append((names[0], header))
284    for cmd, entry in pycompat.iteritems(commands.table):
285        if len(entry) == 3:
286            summary = entry[2]
287        else:
288            summary = b''
289        # translate docs *before* searching there
290        func = entry[0]
291        docs = _(pycompat.getdoc(func)) or b''
292        if kw in cmd or lowercontains(summary) or lowercontains(docs):
293            doclines = docs.splitlines()
294            if doclines:
295                summary = doclines[0]
296            cmdname = cmdutil.parsealiases(cmd)[0]
297            if filtercmd(ui, cmdname, func, kw, docs):
298                continue
299            results[b'commands'].append((cmdname, summary))
300    for name, docs in itertools.chain(
301        pycompat.iteritems(extensions.enabled(False)),
302        pycompat.iteritems(extensions.disabled()),
303    ):
304        if not docs:
305            continue
306        name = name.rpartition(b'.')[-1]
307        if lowercontains(name) or lowercontains(docs):
308            # extension docs are already translated
309            results[b'extensions'].append((name, docs.splitlines()[0]))
310        try:
311            mod = extensions.load(ui, name, b'')
312        except ImportError:
313            # debug message would be printed in extensions.load()
314            continue
315        for cmd, entry in pycompat.iteritems(getattr(mod, 'cmdtable', {})):
316            if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
317                cmdname = cmdutil.parsealiases(cmd)[0]
318                func = entry[0]
319                cmddoc = pycompat.getdoc(func)
320                if cmddoc:
321                    cmddoc = gettext(cmddoc).splitlines()[0]
322                else:
323                    cmddoc = _(b'(no help text available)')
324                if filtercmd(ui, cmdname, func, kw, cmddoc):
325                    continue
326                results[b'extensioncommands'].append((cmdname, cmddoc))
327    return results
328
329
330def loaddoc(topic, subdir=None):
331    """Return a delayed loader for help/topic.txt."""
332
333    def loader(ui):
334        package = b'mercurial.helptext'
335        if subdir:
336            package += b'.' + subdir
337        with resourceutil.open_resource(package, topic + b'.txt') as fp:
338            doc = gettext(fp.read())
339        for rewriter in helphooks.get(topic, []):
340            doc = rewriter(ui, topic, doc)
341        return doc
342
343    return loader
344
345
346internalstable = sorted(
347    [
348        (
349            [b'bid-merge'],
350            _(b'Bid Merge Algorithm'),
351            loaddoc(b'bid-merge', subdir=b'internals'),
352        ),
353        ([b'bundle2'], _(b'Bundle2'), loaddoc(b'bundle2', subdir=b'internals')),
354        ([b'bundles'], _(b'Bundles'), loaddoc(b'bundles', subdir=b'internals')),
355        ([b'cbor'], _(b'CBOR'), loaddoc(b'cbor', subdir=b'internals')),
356        ([b'censor'], _(b'Censor'), loaddoc(b'censor', subdir=b'internals')),
357        (
358            [b'changegroups'],
359            _(b'Changegroups'),
360            loaddoc(b'changegroups', subdir=b'internals'),
361        ),
362        (
363            [b'config'],
364            _(b'Config Registrar'),
365            loaddoc(b'config', subdir=b'internals'),
366        ),
367        (
368            [b'dirstate-v2'],
369            _(b'dirstate-v2 file format'),
370            loaddoc(b'dirstate-v2', subdir=b'internals'),
371        ),
372        (
373            [b'extensions', b'extension'],
374            _(b'Extension API'),
375            loaddoc(b'extensions', subdir=b'internals'),
376        ),
377        (
378            [b'mergestate'],
379            _(b'Mergestate'),
380            loaddoc(b'mergestate', subdir=b'internals'),
381        ),
382        (
383            [b'requirements'],
384            _(b'Repository Requirements'),
385            loaddoc(b'requirements', subdir=b'internals'),
386        ),
387        (
388            [b'revlogs'],
389            _(b'Revision Logs'),
390            loaddoc(b'revlogs', subdir=b'internals'),
391        ),
392        (
393            [b'wireprotocol'],
394            _(b'Wire Protocol'),
395            loaddoc(b'wireprotocol', subdir=b'internals'),
396        ),
397        (
398            [b'wireprotocolrpc'],
399            _(b'Wire Protocol RPC'),
400            loaddoc(b'wireprotocolrpc', subdir=b'internals'),
401        ),
402        (
403            [b'wireprotocolv2'],
404            _(b'Wire Protocol Version 2'),
405            loaddoc(b'wireprotocolv2', subdir=b'internals'),
406        ),
407    ]
408)
409
410
411def internalshelp(ui):
412    """Generate the index for the "internals" topic."""
413    lines = [
414        b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
415        b'\n',
416    ]
417    for names, header, doc in internalstable:
418        lines.append(b' :%s: %s\n' % (names[0], header))
419
420    return b''.join(lines)
421
422
423helptable = sorted(
424    [
425        (
426            [b'bundlespec'],
427            _(b"Bundle File Formats"),
428            loaddoc(b'bundlespec'),
429            TOPIC_CATEGORY_CONCEPTS,
430        ),
431        (
432            [b'color'],
433            _(b"Colorizing Outputs"),
434            loaddoc(b'color'),
435            TOPIC_CATEGORY_OUTPUT,
436        ),
437        (
438            [b"config", b"hgrc"],
439            _(b"Configuration Files"),
440            loaddoc(b'config'),
441            TOPIC_CATEGORY_CONFIG,
442        ),
443        (
444            [b'deprecated'],
445            _(b"Deprecated Features"),
446            loaddoc(b'deprecated'),
447            TOPIC_CATEGORY_MISC,
448        ),
449        (
450            [b"dates"],
451            _(b"Date Formats"),
452            loaddoc(b'dates'),
453            TOPIC_CATEGORY_OUTPUT,
454        ),
455        (
456            [b"flags"],
457            _(b"Command-line flags"),
458            loaddoc(b'flags'),
459            TOPIC_CATEGORY_CONFIG,
460        ),
461        (
462            [b"patterns"],
463            _(b"File Name Patterns"),
464            loaddoc(b'patterns'),
465            TOPIC_CATEGORY_IDS,
466        ),
467        (
468            [b'environment', b'env'],
469            _(b'Environment Variables'),
470            loaddoc(b'environment'),
471            TOPIC_CATEGORY_CONFIG,
472        ),
473        (
474            [
475                b'revisions',
476                b'revs',
477                b'revsets',
478                b'revset',
479                b'multirevs',
480                b'mrevs',
481            ],
482            _(b'Specifying Revisions'),
483            loaddoc(b'revisions'),
484            TOPIC_CATEGORY_IDS,
485        ),
486        (
487            [b'filesets', b'fileset'],
488            _(b"Specifying File Sets"),
489            loaddoc(b'filesets'),
490            TOPIC_CATEGORY_IDS,
491        ),
492        (
493            [b'diffs'],
494            _(b'Diff Formats'),
495            loaddoc(b'diffs'),
496            TOPIC_CATEGORY_OUTPUT,
497        ),
498        (
499            [b'merge-tools', b'mergetools', b'mergetool'],
500            _(b'Merge Tools'),
501            loaddoc(b'merge-tools'),
502            TOPIC_CATEGORY_CONFIG,
503        ),
504        (
505            [b'templating', b'templates', b'template', b'style'],
506            _(b'Template Usage'),
507            loaddoc(b'templates'),
508            TOPIC_CATEGORY_OUTPUT,
509        ),
510        ([b'urls'], _(b'URL Paths'), loaddoc(b'urls'), TOPIC_CATEGORY_IDS),
511        (
512            [b"extensions"],
513            _(b"Using Additional Features"),
514            extshelp,
515            TOPIC_CATEGORY_CONFIG,
516        ),
517        (
518            [b"subrepos", b"subrepo"],
519            _(b"Subrepositories"),
520            loaddoc(b'subrepos'),
521            TOPIC_CATEGORY_CONCEPTS,
522        ),
523        (
524            [b"hgweb"],
525            _(b"Configuring hgweb"),
526            loaddoc(b'hgweb'),
527            TOPIC_CATEGORY_CONFIG,
528        ),
529        (
530            [b"glossary"],
531            _(b"Glossary"),
532            loaddoc(b'glossary'),
533            TOPIC_CATEGORY_CONCEPTS,
534        ),
535        (
536            [b"hgignore", b"ignore"],
537            _(b"Syntax for Mercurial Ignore Files"),
538            loaddoc(b'hgignore'),
539            TOPIC_CATEGORY_IDS,
540        ),
541        (
542            [b"phases"],
543            _(b"Working with Phases"),
544            loaddoc(b'phases'),
545            TOPIC_CATEGORY_CONCEPTS,
546        ),
547        (
548            [b"evolution"],
549            _(b"Safely rewriting history (EXPERIMENTAL)"),
550            loaddoc(b'evolution'),
551            TOPIC_CATEGORY_CONCEPTS,
552        ),
553        (
554            [b'scripting'],
555            _(b'Using Mercurial from scripts and automation'),
556            loaddoc(b'scripting'),
557            TOPIC_CATEGORY_MISC,
558        ),
559        (
560            [b'internals'],
561            _(b"Technical implementation topics"),
562            internalshelp,
563            TOPIC_CATEGORY_MISC,
564        ),
565        (
566            [b'pager'],
567            _(b"Pager Support"),
568            loaddoc(b'pager'),
569            TOPIC_CATEGORY_CONFIG,
570        ),
571    ]
572)
573
574# Maps topics with sub-topics to a list of their sub-topics.
575subtopics = {
576    b'internals': internalstable,
577}
578
579# Map topics to lists of callable taking the current topic help and
580# returning the updated version
581helphooks = {}
582
583
584def addtopichook(topic, rewriter):
585    helphooks.setdefault(topic, []).append(rewriter)
586
587
588def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
589    """Extract docstring from the items key to function mapping, build a
590    single documentation block and use it to overwrite the marker in doc.
591    """
592    entries = []
593    for name in sorted(items):
594        text = (pycompat.getdoc(items[name]) or b'').rstrip()
595        if not text or not ui.verbose and any(w in text for w in _exclkeywords):
596            continue
597        text = gettext(text)
598        if dedent:
599            # Abuse latin1 to use textwrap.dedent() on bytes.
600            text = textwrap.dedent(text.decode('latin1')).encode('latin1')
601        lines = text.splitlines()
602        doclines = [(lines[0])]
603        for l in lines[1:]:
604            # Stop once we find some Python doctest
605            if l.strip().startswith(b'>>>'):
606                break
607            if dedent:
608                doclines.append(l.rstrip())
609            else:
610                doclines.append(b'  ' + l.strip())
611        entries.append(b'\n'.join(doclines))
612    entries = b'\n\n'.join(entries)
613    return doc.replace(marker, entries)
614
615
616def addtopicsymbols(topic, marker, symbols, dedent=False):
617    def add(ui, topic, doc):
618        return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
619
620    addtopichook(topic, add)
621
622
623addtopicsymbols(
624    b'bundlespec',
625    b'.. bundlecompressionmarker',
626    compression.bundlecompressiontopics(),
627)
628addtopicsymbols(b'filesets', b'.. predicatesmarker', fileset.symbols)
629addtopicsymbols(
630    b'merge-tools', b'.. internaltoolsmarker', filemerge.internalsdoc
631)
632addtopicsymbols(b'revisions', b'.. predicatesmarker', revset.symbols)
633addtopicsymbols(b'templates', b'.. keywordsmarker', templatekw.keywords)
634addtopicsymbols(b'templates', b'.. filtersmarker', templatefilters.filters)
635addtopicsymbols(b'templates', b'.. functionsmarker', templatefuncs.funcs)
636addtopicsymbols(
637    b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
638)
639
640
641def inserttweakrc(ui, topic, doc):
642    marker = b'.. tweakdefaultsmarker'
643    repl = uimod.tweakrc
644
645    def sub(m):
646        lines = [m.group(1) + s for s in repl.splitlines()]
647        return b'\n'.join(lines)
648
649    return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
650
651
652def _getcategorizedhelpcmds(ui, cmdtable, name, select=None):
653    # Category -> list of commands
654    cats = {}
655    # Command -> short description
656    h = {}
657    # Command -> string showing synonyms
658    syns = {}
659    for c, e in pycompat.iteritems(cmdtable):
660        fs = cmdutil.parsealiases(c)
661        f = fs[0]
662        syns[f] = fs
663        func = e[0]
664        if select and not select(f):
665            continue
666        doc = pycompat.getdoc(func)
667        if filtercmd(ui, f, func, name, doc):
668            continue
669        doc = gettext(doc)
670        if not doc:
671            doc = _(b"(no help text available)")
672        h[f] = doc.splitlines()[0].rstrip()
673
674        cat = getattr(func, 'helpcategory', None) or (
675            registrar.command.CATEGORY_NONE
676        )
677        cats.setdefault(cat, []).append(f)
678    return cats, h, syns
679
680
681def _getcategorizedhelptopics(ui, topictable):
682    # Group commands by category.
683    topiccats = {}
684    syns = {}
685    for topic in topictable:
686        names, header, doc = topic[0:3]
687        if len(topic) > 3 and topic[3]:
688            category = topic[3]
689        else:
690            category = TOPIC_CATEGORY_NONE
691
692        topicname = names[0]
693        syns[topicname] = list(names)
694        if not filtertopic(ui, topicname):
695            topiccats.setdefault(category, []).append((topicname, header))
696    return topiccats, syns
697
698
699addtopichook(b'config', inserttweakrc)
700
701
702def help_(
703    ui,
704    commands,
705    name,
706    unknowncmd=False,
707    full=True,
708    subtopic=None,
709    fullname=None,
710    **opts
711):
712    """
713    Generate the help for 'name' as unformatted restructured text. If
714    'name' is None, describe the commands available.
715    """
716
717    opts = pycompat.byteskwargs(opts)
718
719    def helpcmd(name, subtopic=None):
720        try:
721            aliases, entry = cmdutil.findcmd(
722                name, commands.table, strict=unknowncmd
723            )
724        except error.AmbiguousCommand as inst:
725            # py3 fix: except vars can't be used outside the scope of the
726            # except block, nor can be used inside a lambda. python issue4617
727            prefix = inst.prefix
728            select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
729            rst = helplist(select)
730            return rst
731
732        rst = []
733
734        # check if it's an invalid alias and display its error if it is
735        if getattr(entry[0], 'badalias', None):
736            rst.append(entry[0].badalias + b'\n')
737            if entry[0].unknowncmd:
738                try:
739                    rst.extend(helpextcmd(entry[0].cmdname))
740                except error.UnknownCommand:
741                    pass
742            return rst
743
744        # synopsis
745        if len(entry) > 2:
746            if entry[2].startswith(b'hg'):
747                rst.append(b"%s\n" % entry[2])
748            else:
749                rst.append(b'hg %s %s\n' % (aliases[0], entry[2]))
750        else:
751            rst.append(b'hg %s\n' % aliases[0])
752        # aliases
753        if full and not ui.quiet and len(aliases) > 1:
754            rst.append(_(b"\naliases: %s\n") % b', '.join(aliases[1:]))
755        rst.append(b'\n')
756
757        # description
758        doc = gettext(pycompat.getdoc(entry[0]))
759        if not doc:
760            doc = _(b"(no help text available)")
761        if util.safehasattr(entry[0], b'definition'):  # aliased command
762            source = entry[0].source
763            if entry[0].definition.startswith(b'!'):  # shell alias
764                doc = _(b'shell alias for: %s\n\n%s\n\ndefined by: %s\n') % (
765                    entry[0].definition[1:],
766                    doc,
767                    source,
768                )
769            else:
770                doc = _(b'alias for: hg %s\n\n%s\n\ndefined by: %s\n') % (
771                    entry[0].definition,
772                    doc,
773                    source,
774                )
775        doc = doc.splitlines(True)
776        if ui.quiet or not full:
777            rst.append(doc[0])
778        else:
779            rst.extend(doc)
780        rst.append(b'\n')
781
782        # check if this command shadows a non-trivial (multi-line)
783        # extension help text
784        try:
785            mod = extensions.find(name)
786            doc = gettext(pycompat.getdoc(mod)) or b''
787            if b'\n' in doc.strip():
788                msg = _(
789                    b"(use 'hg help -e %s' to show help for "
790                    b"the %s extension)"
791                ) % (name, name)
792                rst.append(b'\n%s\n' % msg)
793        except KeyError:
794            pass
795
796        # options
797        if not ui.quiet and entry[1]:
798            rst.append(optrst(_(b"options"), entry[1], ui.verbose, ui))
799
800        if ui.verbose:
801            rst.append(
802                optrst(
803                    _(b"global options"), commands.globalopts, ui.verbose, ui
804                )
805            )
806
807        if not ui.verbose:
808            if not full:
809                rst.append(_(b"\n(use 'hg %s -h' to show more help)\n") % name)
810            elif not ui.quiet:
811                rst.append(
812                    _(
813                        b'\n(some details hidden, use --verbose '
814                        b'to show complete help)'
815                    )
816                )
817
818        return rst
819
820    def helplist(select=None, **opts):
821        cats, h, syns = _getcategorizedhelpcmds(
822            ui, commands.table, name, select
823        )
824
825        rst = []
826        if not h:
827            if not ui.quiet:
828                rst.append(_(b'no commands defined\n'))
829            return rst
830
831        # Output top header.
832        if not ui.quiet:
833            if name == b"shortlist":
834                rst.append(_(b'basic commands:\n\n'))
835            elif name == b"debug":
836                rst.append(_(b'debug commands (internal and unsupported):\n\n'))
837            else:
838                rst.append(_(b'list of commands:\n'))
839
840        def appendcmds(cmds):
841            cmds = sorted(cmds)
842            for c in cmds:
843                display_cmd = c
844                if ui.verbose:
845                    display_cmd = b', '.join(syns[c])
846                display_cmd = display_cmd.replace(b':', br'\:')
847                rst.append(b' :%s: %s\n' % (display_cmd, h[c]))
848
849        if name in (b'shortlist', b'debug'):
850            # List without categories.
851            appendcmds(h)
852        else:
853            # Check that all categories have an order.
854            missing_order = set(cats.keys()) - set(CATEGORY_ORDER)
855            if missing_order:
856                ui.develwarn(
857                    b'help categories missing from CATEGORY_ORDER: %s'
858                    % missing_order
859                )
860
861            # List per category.
862            for cat in CATEGORY_ORDER:
863                catfns = cats.get(cat, [])
864                if catfns:
865                    if len(cats) > 1:
866                        catname = gettext(CATEGORY_NAMES[cat])
867                        rst.append(b"\n%s:\n" % catname)
868                    rst.append(b"\n")
869                    appendcmds(catfns)
870
871        ex = opts.get
872        anyopts = ex('keyword') or not (ex('command') or ex('extension'))
873        if not name and anyopts:
874            exts = listexts(
875                _(b'enabled extensions:'),
876                extensions.enabled(),
877                showdeprecated=ui.verbose,
878            )
879            if exts:
880                rst.append(b'\n')
881                rst.extend(exts)
882
883            rst.append(_(b"\nadditional help topics:\n"))
884            topiccats, topicsyns = _getcategorizedhelptopics(ui, helptable)
885
886            # Check that all categories have an order.
887            missing_order = set(topiccats.keys()) - set(TOPIC_CATEGORY_ORDER)
888            if missing_order:
889                ui.develwarn(
890                    b'help categories missing from TOPIC_CATEGORY_ORDER: %s'
891                    % missing_order
892                )
893
894            # Output topics per category.
895            for cat in TOPIC_CATEGORY_ORDER:
896                topics = topiccats.get(cat, [])
897                if topics:
898                    if len(topiccats) > 1:
899                        catname = gettext(TOPIC_CATEGORY_NAMES[cat])
900                        rst.append(b"\n%s:\n" % catname)
901                    rst.append(b"\n")
902                    for t, desc in topics:
903                        rst.append(b" :%s: %s\n" % (t, desc))
904
905        if ui.quiet:
906            pass
907        elif ui.verbose:
908            rst.append(
909                b'\n%s\n'
910                % optrst(
911                    _(b"global options"), commands.globalopts, ui.verbose, ui
912                )
913            )
914            if name == b'shortlist':
915                rst.append(
916                    _(b"\n(use 'hg help' for the full list of commands)\n")
917                )
918        else:
919            if name == b'shortlist':
920                rst.append(
921                    _(
922                        b"\n(use 'hg help' for the full list of commands "
923                        b"or 'hg -v' for details)\n"
924                    )
925                )
926            elif name and not full:
927                rst.append(
928                    _(b"\n(use 'hg help %s' to show the full help text)\n")
929                    % name
930                )
931            elif name and syns and name in syns.keys():
932                rst.append(
933                    _(
934                        b"\n(use 'hg help -v -e %s' to show built-in "
935                        b"aliases and global options)\n"
936                    )
937                    % name
938                )
939            else:
940                rst.append(
941                    _(
942                        b"\n(use 'hg help -v%s' to show built-in aliases "
943                        b"and global options)\n"
944                    )
945                    % (name and b" " + name or b"")
946                )
947        return rst
948
949    def helptopic(name, subtopic=None):
950        # Look for sub-topic entry first.
951        header, doc = None, None
952        if subtopic and name in subtopics:
953            for names, header, doc in subtopics[name]:
954                if subtopic in names:
955                    break
956            if not any(subtopic in s[0] for s in subtopics[name]):
957                raise error.UnknownCommand(name)
958
959        if not header:
960            for topic in helptable:
961                names, header, doc = topic[0:3]
962                if name in names:
963                    break
964            else:
965                raise error.UnknownCommand(name)
966
967        rst = [minirst.section(header)]
968
969        # description
970        if not doc:
971            rst.append(b"    %s\n" % _(b"(no help text available)"))
972        if callable(doc):
973            rst += [b"    %s\n" % l for l in doc(ui).splitlines()]
974
975        if not ui.verbose:
976            omitted = _(
977                b'(some details hidden, use --verbose'
978                b' to show complete help)'
979            )
980            indicateomitted(rst, omitted)
981
982        try:
983            cmdutil.findcmd(name, commands.table)
984            rst.append(
985                _(b"\nuse 'hg help -c %s' to see help for the %s command\n")
986                % (name, name)
987            )
988        except error.UnknownCommand:
989            pass
990        return rst
991
992    def helpext(name, subtopic=None):
993        try:
994            mod = extensions.find(name)
995            doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
996        except KeyError:
997            mod = None
998            doc = extensions.disabled_help(name)
999            if not doc:
1000                raise error.UnknownCommand(name)
1001
1002        if b'\n' not in doc:
1003            head, tail = doc, b""
1004        else:
1005            head, tail = doc.split(b'\n', 1)
1006        rst = [_(b'%s extension - %s\n\n') % (name.rpartition(b'.')[-1], head)]
1007        if tail:
1008            rst.extend(tail.splitlines(True))
1009            rst.append(b'\n')
1010
1011        if not ui.verbose:
1012            omitted = _(
1013                b'(some details hidden, use --verbose'
1014                b' to show complete help)'
1015            )
1016            indicateomitted(rst, omitted)
1017
1018        if mod:
1019            try:
1020                ct = mod.cmdtable
1021            except AttributeError:
1022                ct = {}
1023            modcmds = {c.partition(b'|')[0] for c in ct}
1024            rst.extend(helplist(modcmds.__contains__))
1025        else:
1026            rst.append(
1027                _(
1028                    b"(use 'hg help extensions' for information on enabling"
1029                    b" extensions)\n"
1030                )
1031            )
1032        return rst
1033
1034    def helpextcmd(name, subtopic=None):
1035        cmd, ext, doc = extensions.disabledcmd(
1036            ui, name, ui.configbool(b'ui', b'strict')
1037        )
1038        doc = doc.splitlines()[0]
1039
1040        rst = listexts(
1041            _(b"'%s' is provided by the following extension:") % cmd,
1042            {ext: doc},
1043            indent=4,
1044            showdeprecated=True,
1045        )
1046        rst.append(b'\n')
1047        rst.append(
1048            _(
1049                b"(use 'hg help extensions' for information on enabling "
1050                b"extensions)\n"
1051            )
1052        )
1053        return rst
1054
1055    rst = []
1056    kw = opts.get(b'keyword')
1057    if kw or name is None and any(opts[o] for o in opts):
1058        matches = topicmatch(ui, commands, name or b'')
1059        helpareas = []
1060        if opts.get(b'extension'):
1061            helpareas += [(b'extensions', _(b'Extensions'))]
1062        if opts.get(b'command'):
1063            helpareas += [(b'commands', _(b'Commands'))]
1064        if not helpareas:
1065            helpareas = [
1066                (b'topics', _(b'Topics')),
1067                (b'commands', _(b'Commands')),
1068                (b'extensions', _(b'Extensions')),
1069                (b'extensioncommands', _(b'Extension Commands')),
1070            ]
1071        for t, title in helpareas:
1072            if matches[t]:
1073                rst.append(b'%s:\n\n' % title)
1074                rst.extend(minirst.maketable(sorted(matches[t]), 1))
1075                rst.append(b'\n')
1076        if not rst:
1077            msg = _(b'no matches')
1078            hint = _(b"try 'hg help' for a list of topics")
1079            raise error.InputError(msg, hint=hint)
1080    elif name and name != b'shortlist':
1081        queries = []
1082        if unknowncmd:
1083            queries += [helpextcmd]
1084        if opts.get(b'extension'):
1085            queries += [helpext]
1086        if opts.get(b'command'):
1087            queries += [helpcmd]
1088        if not queries:
1089            queries = (helptopic, helpcmd, helpext, helpextcmd)
1090        for f in queries:
1091            try:
1092                rst = f(name, subtopic)
1093                break
1094            except error.UnknownCommand:
1095                pass
1096        else:
1097            if unknowncmd:
1098                raise error.UnknownCommand(name)
1099            else:
1100                if fullname:
1101                    formatname = fullname
1102                else:
1103                    formatname = name
1104                if subtopic:
1105                    hintname = subtopic
1106                else:
1107                    hintname = name
1108                msg = _(b'no such help topic: %s') % formatname
1109                hint = _(b"try 'hg help --keyword %s'") % hintname
1110                raise error.InputError(msg, hint=hint)
1111    else:
1112        # program name
1113        if not ui.quiet:
1114            rst = [_(b"Mercurial Distributed SCM\n"), b'\n']
1115        rst.extend(helplist(None, **pycompat.strkwargs(opts)))
1116
1117    return b''.join(rst)
1118
1119
1120def formattedhelp(
1121    ui, commands, fullname, keep=None, unknowncmd=False, full=True, **opts
1122):
1123    """get help for a given topic (as a dotted name) as rendered rst
1124
1125    Either returns the rendered help text or raises an exception.
1126    """
1127    if keep is None:
1128        keep = []
1129    else:
1130        keep = list(keep)  # make a copy so we can mutate this later
1131
1132    # <fullname> := <name>[.<subtopic][.<section>]
1133    name = subtopic = section = None
1134    if fullname is not None:
1135        nameparts = fullname.split(b'.')
1136        name = nameparts.pop(0)
1137        if nameparts and name in subtopics:
1138            subtopic = nameparts.pop(0)
1139        if nameparts:
1140            section = encoding.lower(b'.'.join(nameparts))
1141
1142    textwidth = ui.configint(b'ui', b'textwidth')
1143    termwidth = ui.termwidth() - 2
1144    if textwidth <= 0 or termwidth < textwidth:
1145        textwidth = termwidth
1146    text = help_(
1147        ui,
1148        commands,
1149        name,
1150        fullname=fullname,
1151        subtopic=subtopic,
1152        unknowncmd=unknowncmd,
1153        full=full,
1154        **opts
1155    )
1156
1157    blocks, pruned = minirst.parse(text, keep=keep)
1158    if b'verbose' in pruned:
1159        keep.append(b'omitted')
1160    else:
1161        keep.append(b'notomitted')
1162    blocks, pruned = minirst.parse(text, keep=keep)
1163    if section:
1164        blocks = minirst.filtersections(blocks, section)
1165
1166    # We could have been given a weird ".foo" section without a name
1167    # to look for, or we could have simply failed to found "foo.bar"
1168    # because bar isn't a section of foo
1169    if section and not (blocks and name):
1170        raise error.InputError(_(b"help section not found: %s") % fullname)
1171
1172    return minirst.formatplain(blocks, textwidth)
1173