mercurial/help.py
changeset 49646 2a70d1fc70c4
parent 49565 04e6add9e4dc
child 49664 f56873a7284c
equal deleted inserted replaced
49644:5744ceeb9067 49646:2a70d1fc70c4
     7 
     7 
     8 
     8 
     9 import itertools
     9 import itertools
    10 import re
    10 import re
    11 import textwrap
    11 import textwrap
       
    12 
       
    13 from typing import (
       
    14     Callable,
       
    15     Dict,
       
    16     Iterable,
       
    17     List,
       
    18     Optional,
       
    19     Set,
       
    20     Tuple,
       
    21     Union,
       
    22     cast,
       
    23 )
    12 
    24 
    13 from .i18n import (
    25 from .i18n import (
    14     _,
    26     _,
    15     gettext,
    27     gettext,
    16 )
    28 )
    38     compression,
    50     compression,
    39     resourceutil,
    51     resourceutil,
    40     stringutil,
    52     stringutil,
    41 )
    53 )
    42 
    54 
    43 _exclkeywords = {
    55 _DocLoader = Callable[[uimod.ui], bytes]
       
    56 # Old extensions may not register with a category
       
    57 _HelpEntry = Union["_HelpEntryNoCategory", "_HelpEntryWithCategory"]
       
    58 _HelpEntryNoCategory = Tuple[List[bytes], bytes, _DocLoader]
       
    59 _HelpEntryWithCategory = Tuple[List[bytes], bytes, _DocLoader, bytes]
       
    60 _SelectFn = Callable[[object], bool]
       
    61 _SynonymTable = Dict[bytes, List[bytes]]
       
    62 _TopicHook = Callable[[uimod.ui, bytes, bytes], bytes]
       
    63 
       
    64 _exclkeywords: Set[bytes] = {
    44     b"(ADVANCED)",
    65     b"(ADVANCED)",
    45     b"(DEPRECATED)",
    66     b"(DEPRECATED)",
    46     b"(EXPERIMENTAL)",
    67     b"(EXPERIMENTAL)",
    47     # i18n: "(ADVANCED)" is a keyword, must be translated consistently
    68     # i18n: "(ADVANCED)" is a keyword, must be translated consistently
    48     _(b"(ADVANCED)"),
    69     _(b"(ADVANCED)"),
    54 
    75 
    55 # The order in which command categories will be displayed.
    76 # The order in which command categories will be displayed.
    56 # Extensions with custom categories should insert them into this list
    77 # Extensions with custom categories should insert them into this list
    57 # after/before the appropriate item, rather than replacing the list or
    78 # after/before the appropriate item, rather than replacing the list or
    58 # assuming absolute positions.
    79 # assuming absolute positions.
    59 CATEGORY_ORDER = [
    80 CATEGORY_ORDER: List[bytes] = [
    60     registrar.command.CATEGORY_REPO_CREATION,
    81     registrar.command.CATEGORY_REPO_CREATION,
    61     registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
    82     registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
    62     registrar.command.CATEGORY_COMMITTING,
    83     registrar.command.CATEGORY_COMMITTING,
    63     registrar.command.CATEGORY_CHANGE_MANAGEMENT,
    84     registrar.command.CATEGORY_CHANGE_MANAGEMENT,
    64     registrar.command.CATEGORY_CHANGE_ORGANIZATION,
    85     registrar.command.CATEGORY_CHANGE_ORGANIZATION,
    72     registrar.command.CATEGORY_NONE,
    93     registrar.command.CATEGORY_NONE,
    73 ]
    94 ]
    74 
    95 
    75 # Human-readable category names. These are translated.
    96 # Human-readable category names. These are translated.
    76 # Extensions with custom categories should add their names here.
    97 # Extensions with custom categories should add their names here.
    77 CATEGORY_NAMES = {
    98 CATEGORY_NAMES: Dict[bytes, bytes] = {
    78     registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
    99     registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
    79     registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
   100     registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
    80     registrar.command.CATEGORY_COMMITTING: b'Change creation',
   101     registrar.command.CATEGORY_COMMITTING: b'Change creation',
    81     registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
   102     registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
    82     registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
   103     registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
   100 
   121 
   101 # The order in which topic categories will be displayed.
   122 # The order in which topic categories will be displayed.
   102 # Extensions with custom categories should insert them into this list
   123 # Extensions with custom categories should insert them into this list
   103 # after/before the appropriate item, rather than replacing the list or
   124 # after/before the appropriate item, rather than replacing the list or
   104 # assuming absolute positions.
   125 # assuming absolute positions.
   105 TOPIC_CATEGORY_ORDER = [
   126 TOPIC_CATEGORY_ORDER: List[bytes] = [
   106     TOPIC_CATEGORY_IDS,
   127     TOPIC_CATEGORY_IDS,
   107     TOPIC_CATEGORY_OUTPUT,
   128     TOPIC_CATEGORY_OUTPUT,
   108     TOPIC_CATEGORY_CONFIG,
   129     TOPIC_CATEGORY_CONFIG,
   109     TOPIC_CATEGORY_CONCEPTS,
   130     TOPIC_CATEGORY_CONCEPTS,
   110     TOPIC_CATEGORY_MISC,
   131     TOPIC_CATEGORY_MISC,
   111     TOPIC_CATEGORY_NONE,
   132     TOPIC_CATEGORY_NONE,
   112 ]
   133 ]
   113 
   134 
   114 # Human-readable topic category names. These are translated.
   135 # Human-readable topic category names. These are translated.
   115 TOPIC_CATEGORY_NAMES = {
   136 TOPIC_CATEGORY_NAMES: Dict[bytes, bytes] = {
   116     TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
   137     TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
   117     TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
   138     TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
   118     TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
   139     TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
   119     TOPIC_CATEGORY_CONCEPTS: b'Concepts',
   140     TOPIC_CATEGORY_CONCEPTS: b'Concepts',
   120     TOPIC_CATEGORY_MISC: b'Miscellaneous',
   141     TOPIC_CATEGORY_MISC: b'Miscellaneous',
   121     TOPIC_CATEGORY_NONE: b'Uncategorized topics',
   142     TOPIC_CATEGORY_NONE: b'Uncategorized topics',
   122 }
   143 }
   123 
   144 
   124 
   145 
   125 def listexts(header, exts, indent=1, showdeprecated=False):
   146 def listexts(
       
   147     header: bytes,
       
   148     exts: Dict[bytes, bytes],
       
   149     indent: int = 1,
       
   150     showdeprecated: bool = False,
       
   151 ) -> List[bytes]:
   126     '''return a text listing of the given extensions'''
   152     '''return a text listing of the given extensions'''
   127     rst = []
   153     rst = []
   128     if exts:
   154     if exts:
   129         for name, desc in sorted(exts.items()):
   155         for name, desc in sorted(exts.items()):
   130             if not showdeprecated and any(w in desc for w in _exclkeywords):
   156             if not showdeprecated and any(w in desc for w in _exclkeywords):
   133     if rst:
   159     if rst:
   134         rst.insert(0, b'\n%s\n\n' % header)
   160         rst.insert(0, b'\n%s\n\n' % header)
   135     return rst
   161     return rst
   136 
   162 
   137 
   163 
   138 def extshelp(ui):
   164 def extshelp(ui: uimod.ui) -> bytes:
   139     rst = loaddoc(b'extensions')(ui).splitlines(True)
   165     rst = loaddoc(b'extensions')(ui).splitlines(True)
   140     rst.extend(
   166     rst.extend(
   141         listexts(
   167         listexts(
   142             _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
   168             _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
   143         )
   169         )
   151     )
   177     )
   152     doc = b''.join(rst)
   178     doc = b''.join(rst)
   153     return doc
   179     return doc
   154 
   180 
   155 
   181 
   156 def parsedefaultmarker(text):
   182 def parsedefaultmarker(text: bytes) -> Optional[Tuple[bytes, List[bytes]]]:
   157     """given a text 'abc (DEFAULT: def.ghi)',
   183     """given a text 'abc (DEFAULT: def.ghi)',
   158     returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
   184     returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
   159     if text[-1:] == b')':
   185     if text[-1:] == b')':
   160         marker = b' (DEFAULT: '
   186         marker = b' (DEFAULT: '
   161         pos = text.find(marker)
   187         pos = text.find(marker)
   162         if pos >= 0:
   188         if pos >= 0:
   163             item = text[pos + len(marker) : -1]
   189             item = text[pos + len(marker) : -1]
   164             return text[:pos], item.split(b'.', 2)
   190             return text[:pos], item.split(b'.', 2)
   165 
   191 
   166 
   192 
   167 def optrst(header, options, verbose, ui):
   193 def optrst(header: bytes, options, verbose: bool, ui: uimod.ui) -> bytes:
   168     data = []
   194     data = []
   169     multioccur = False
   195     multioccur = False
   170     for option in options:
   196     for option in options:
   171         if len(option) == 5:
   197         if len(option) == 5:
   172             shortopt, longopt, default, desc, optlabel = option
   198             shortopt, longopt, default, desc, optlabel = option
   218     rst.extend(minirst.maketable(data, 1))
   244     rst.extend(minirst.maketable(data, 1))
   219 
   245 
   220     return b''.join(rst)
   246     return b''.join(rst)
   221 
   247 
   222 
   248 
   223 def indicateomitted(rst, omitted, notomitted=None):
   249 def indicateomitted(
       
   250     rst: List[bytes], omitted: bytes, notomitted: Optional[bytes] = None
       
   251 ) -> None:
   224     rst.append(b'\n\n.. container:: omitted\n\n    %s\n\n' % omitted)
   252     rst.append(b'\n\n.. container:: omitted\n\n    %s\n\n' % omitted)
   225     if notomitted:
   253     if notomitted:
   226         rst.append(b'\n\n.. container:: notomitted\n\n    %s\n\n' % notomitted)
   254         rst.append(b'\n\n.. container:: notomitted\n\n    %s\n\n' % notomitted)
   227 
   255 
   228 
   256 
   229 def filtercmd(ui, cmd, func, kw, doc):
   257 def filtercmd(ui: uimod.ui, cmd: bytes, func, kw: bytes, doc: bytes) -> bool:
   230     if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
   258     if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
   231         # Debug command, and user is not looking for those.
   259         # Debug command, and user is not looking for those.
   232         return True
   260         return True
   233     if not ui.verbose:
   261     if not ui.verbose:
   234         if not kw and not doc:
   262         if not kw and not doc:
   247         # Configuration explicitly hides the command.
   275         # Configuration explicitly hides the command.
   248         return True
   276         return True
   249     return False
   277     return False
   250 
   278 
   251 
   279 
   252 def filtertopic(ui, topic):
   280 def filtertopic(ui: uimod.ui, topic: bytes) -> bool:
   253     return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
   281     return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
   254 
   282 
   255 
   283 
   256 def topicmatch(ui, commands, kw):
   284 def topicmatch(
       
   285     ui: uimod.ui, commands, kw: bytes
       
   286 ) -> Dict[bytes, List[Tuple[bytes, bytes]]]:
   257     """Return help topics matching kw.
   287     """Return help topics matching kw.
   258 
   288 
   259     Returns {'section': [(name, summary), ...], ...} where section is
   289     Returns {'section': [(name, summary), ...], ...} where section is
   260     one of topics, commands, extensions, or extensioncommands.
   290     one of topics, commands, extensions, or extensioncommands.
   261     """
   291     """
   324                     continue
   354                     continue
   325                 results[b'extensioncommands'].append((cmdname, cmddoc))
   355                 results[b'extensioncommands'].append((cmdname, cmddoc))
   326     return results
   356     return results
   327 
   357 
   328 
   358 
   329 def loaddoc(topic, subdir=None):
   359 def loaddoc(topic: bytes, subdir: Optional[bytes] = None) -> _DocLoader:
   330     """Return a delayed loader for help/topic.txt."""
   360     """Return a delayed loader for help/topic.txt."""
   331 
   361 
   332     def loader(ui):
   362     def loader(ui: uimod.ui) -> bytes:
   333         package = b'mercurial.helptext'
   363         package = b'mercurial.helptext'
   334         if subdir:
   364         if subdir:
   335             package += b'.' + subdir
   365             package += b'.' + subdir
   336         with resourceutil.open_resource(package, topic + b'.txt') as fp:
   366         with resourceutil.open_resource(package, topic + b'.txt') as fp:
   337             doc = gettext(fp.read())
   367             doc = gettext(fp.read())
   340         return doc
   370         return doc
   341 
   371 
   342     return loader
   372     return loader
   343 
   373 
   344 
   374 
   345 internalstable = sorted(
   375 internalstable: List[_HelpEntryNoCategory] = sorted(
   346     [
   376     [
   347         (
   377         (
   348             [b'bid-merge'],
   378             [b'bid-merge'],
   349             _(b'Bid Merge Algorithm'),
   379             _(b'Bid Merge Algorithm'),
   350             loaddoc(b'bid-merge', subdir=b'internals'),
   380             loaddoc(b'bid-merge', subdir=b'internals'),
   405         ),
   435         ),
   406     ]
   436     ]
   407 )
   437 )
   408 
   438 
   409 
   439 
   410 def internalshelp(ui):
   440 def internalshelp(ui: uimod.ui) -> bytes:
   411     """Generate the index for the "internals" topic."""
   441     """Generate the index for the "internals" topic."""
   412     lines = [
   442     lines = [
   413         b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
   443         b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
   414         b'\n',
   444         b'\n',
   415     ]
   445     ]
   417         lines.append(b' :%s: %s\n' % (names[0], header))
   447         lines.append(b' :%s: %s\n' % (names[0], header))
   418 
   448 
   419     return b''.join(lines)
   449     return b''.join(lines)
   420 
   450 
   421 
   451 
   422 helptable = sorted(
   452 helptable: List[_HelpEntryWithCategory] = sorted(
   423     [
   453     [
   424         (
   454         (
   425             [b'bundlespec'],
   455             [b'bundlespec'],
   426             _(b"Bundle File Formats"),
   456             _(b"Bundle File Formats"),
   427             loaddoc(b'bundlespec'),
   457             loaddoc(b'bundlespec'),
   579         ),
   609         ),
   580     ]
   610     ]
   581 )
   611 )
   582 
   612 
   583 # Maps topics with sub-topics to a list of their sub-topics.
   613 # Maps topics with sub-topics to a list of their sub-topics.
   584 subtopics = {
   614 subtopics: Dict[bytes, List[_HelpEntryNoCategory]] = {
   585     b'internals': internalstable,
   615     b'internals': internalstable,
   586 }
   616 }
   587 
   617 
   588 # Map topics to lists of callable taking the current topic help and
   618 # Map topics to lists of callable taking the current topic help and
   589 # returning the updated version
   619 # returning the updated version
   590 helphooks = {}
   620 helphooks: Dict[bytes, List[_TopicHook]] = {}
   591 
   621 
   592 
   622 
   593 def addtopichook(topic, rewriter):
   623 def addtopichook(topic: bytes, rewriter: _TopicHook) -> None:
   594     helphooks.setdefault(topic, []).append(rewriter)
   624     helphooks.setdefault(topic, []).append(rewriter)
   595 
   625 
   596 
   626 
   597 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
   627 def makeitemsdoc(
       
   628     ui: uimod.ui,
       
   629     topic: bytes,
       
   630     doc: bytes,
       
   631     marker: bytes,
       
   632     items: Dict[bytes, bytes],
       
   633     dedent: bool = False,
       
   634 ) -> bytes:
   598     """Extract docstring from the items key to function mapping, build a
   635     """Extract docstring from the items key to function mapping, build a
   599     single documentation block and use it to overwrite the marker in doc.
   636     single documentation block and use it to overwrite the marker in doc.
   600     """
   637     """
   601     entries = []
   638     entries = []
   602     for name in sorted(items):
   639     for name in sorted(items):
   620         entries.append(b'\n'.join(doclines))
   657         entries.append(b'\n'.join(doclines))
   621     entries = b'\n\n'.join(entries)
   658     entries = b'\n\n'.join(entries)
   622     return doc.replace(marker, entries)
   659     return doc.replace(marker, entries)
   623 
   660 
   624 
   661 
   625 def addtopicsymbols(topic, marker, symbols, dedent=False):
   662 def addtopicsymbols(
   626     def add(ui, topic, doc):
   663     topic: bytes, marker: bytes, symbols, dedent: bool = False
       
   664 ) -> None:
       
   665     def add(ui: uimod.ui, topic: bytes, doc: bytes):
   627         return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
   666         return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
   628 
   667 
   629     addtopichook(topic, add)
   668     addtopichook(topic, add)
   630 
   669 
   631 
   670 
   645 addtopicsymbols(
   684 addtopicsymbols(
   646     b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
   685     b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
   647 )
   686 )
   648 
   687 
   649 
   688 
   650 def inserttweakrc(ui, topic, doc):
   689 def inserttweakrc(ui: uimod.ui, topic: bytes, doc: bytes) -> bytes:
   651     marker = b'.. tweakdefaultsmarker'
   690     marker = b'.. tweakdefaultsmarker'
   652     repl = uimod.tweakrc
   691     repl = uimod.tweakrc
   653 
   692 
   654     def sub(m):
   693     def sub(m):
   655         lines = [m.group(1) + s for s in repl.splitlines()]
   694         lines = [m.group(1) + s for s in repl.splitlines()]
   656         return b'\n'.join(lines)
   695         return b'\n'.join(lines)
   657 
   696 
   658     return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
   697     return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
   659 
   698 
   660 
   699 
   661 def _getcategorizedhelpcmds(ui, cmdtable, name, select=None):
   700 def _getcategorizedhelpcmds(
       
   701     ui: uimod.ui, cmdtable, name: bytes, select: Optional[_SelectFn] = None
       
   702 ) -> Tuple[Dict[bytes, List[bytes]], Dict[bytes, bytes], _SynonymTable]:
   662     # Category -> list of commands
   703     # Category -> list of commands
   663     cats = {}
   704     cats = {}
   664     # Command -> short description
   705     # Command -> short description
   665     h = {}
   706     h = {}
   666     # Command -> string showing synonyms
   707     # Command -> string showing synonyms
   685         )
   726         )
   686         cats.setdefault(cat, []).append(f)
   727         cats.setdefault(cat, []).append(f)
   687     return cats, h, syns
   728     return cats, h, syns
   688 
   729 
   689 
   730 
   690 def _getcategorizedhelptopics(ui, topictable):
   731 def _getcategorizedhelptopics(
       
   732     ui: uimod.ui, topictable: List[_HelpEntry]
       
   733 ) -> Tuple[Dict[bytes, List[Tuple[bytes, bytes]]], Dict[bytes, List[bytes]]]:
   691     # Group commands by category.
   734     # Group commands by category.
   692     topiccats = {}
   735     topiccats = {}
   693     syns = {}
   736     syns = {}
   694     for topic in topictable:
   737     for topic in topictable:
   695         names, header, doc = topic[0:3]
   738         names, header, doc = topic[0:3]
   696         if len(topic) > 3 and topic[3]:
   739         if len(topic) > 3 and topic[3]:
   697             category = topic[3]
   740             category: bytes = cast(bytes, topic[3])  # help pytype
   698         else:
   741         else:
   699             category = TOPIC_CATEGORY_NONE
   742             category: bytes = TOPIC_CATEGORY_NONE
   700 
   743 
   701         topicname = names[0]
   744         topicname = names[0]
   702         syns[topicname] = list(names)
   745         syns[topicname] = list(names)
   703         if not filtertopic(ui, topicname):
   746         if not filtertopic(ui, topicname):
   704             topiccats.setdefault(category, []).append((topicname, header))
   747             topiccats.setdefault(category, []).append((topicname, header))
   707 
   750 
   708 addtopichook(b'config', inserttweakrc)
   751 addtopichook(b'config', inserttweakrc)
   709 
   752 
   710 
   753 
   711 def help_(
   754 def help_(
   712     ui,
   755     ui: uimod.ui,
   713     commands,
   756     commands,
   714     name,
   757     name: bytes,
   715     unknowncmd=False,
   758     unknowncmd: bool = False,
   716     full=True,
   759     full: bool = True,
   717     subtopic=None,
   760     subtopic: Optional[bytes] = None,
   718     fullname=None,
   761     fullname: Optional[bytes] = None,
   719     **opts
   762     **opts
   720 ):
   763 ) -> bytes:
   721     """
   764     """
   722     Generate the help for 'name' as unformatted restructured text. If
   765     Generate the help for 'name' as unformatted restructured text. If
   723     'name' is None, describe the commands available.
   766     'name' is None, describe the commands available.
   724     """
   767     """
   725 
   768 
   726     opts = pycompat.byteskwargs(opts)
   769     opts = pycompat.byteskwargs(opts)
   727 
   770 
   728     def helpcmd(name, subtopic=None):
   771     def helpcmd(name: bytes, subtopic: Optional[bytes]) -> List[bytes]:
   729         try:
   772         try:
   730             aliases, entry = cmdutil.findcmd(
   773             aliases, entry = cmdutil.findcmd(
   731                 name, commands.table, strict=unknowncmd
   774                 name, commands.table, strict=unknowncmd
   732             )
   775             )
   733         except error.AmbiguousCommand as inst:
   776         except error.AmbiguousCommand as inst:
   824                     )
   867                     )
   825                 )
   868                 )
   826 
   869 
   827         return rst
   870         return rst
   828 
   871 
   829     def helplist(select=None, **opts):
   872     def helplist(select: Optional[_SelectFn] = None, **opts) -> List[bytes]:
   830         cats, h, syns = _getcategorizedhelpcmds(
   873         cats, h, syns = _getcategorizedhelpcmds(
   831             ui, commands.table, name, select
   874             ui, commands.table, name, select
   832         )
   875         )
   833 
   876 
   834         rst = []
   877         rst = []
   844             elif name == b"debug":
   887             elif name == b"debug":
   845                 rst.append(_(b'debug commands (internal and unsupported):\n\n'))
   888                 rst.append(_(b'debug commands (internal and unsupported):\n\n'))
   846             else:
   889             else:
   847                 rst.append(_(b'list of commands:\n'))
   890                 rst.append(_(b'list of commands:\n'))
   848 
   891 
   849         def appendcmds(cmds):
   892         def appendcmds(cmds: Iterable[bytes]) -> None:
   850             cmds = sorted(cmds)
   893             cmds = sorted(cmds)
   851             for c in cmds:
   894             for c in cmds:
   852                 display_cmd = c
   895                 display_cmd = c
   853                 if ui.verbose:
   896                 if ui.verbose:
   854                     display_cmd = b', '.join(syns[c])
   897                     display_cmd = b', '.join(syns[c])
   953                     )
   996                     )
   954                     % (name and b" " + name or b"")
   997                     % (name and b" " + name or b"")
   955                 )
   998                 )
   956         return rst
   999         return rst
   957 
  1000 
   958     def helptopic(name, subtopic=None):
  1001     def helptopic(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
   959         # Look for sub-topic entry first.
  1002         # Look for sub-topic entry first.
   960         header, doc = None, None
  1003         header, doc = None, None
   961         if subtopic and name in subtopics:
  1004         if subtopic and name in subtopics:
   962             for names, header, doc in subtopics[name]:
  1005             for names, header, doc in subtopics[name]:
   963                 if subtopic in names:
  1006                 if subtopic in names:
   996             )
  1039             )
   997         except error.UnknownCommand:
  1040         except error.UnknownCommand:
   998             pass
  1041             pass
   999         return rst
  1042         return rst
  1000 
  1043 
  1001     def helpext(name, subtopic=None):
  1044     def helpext(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
  1002         try:
  1045         try:
  1003             mod = extensions.find(name)
  1046             mod = extensions.find(name)
  1004             doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
  1047             doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
  1005         except KeyError:
  1048         except KeyError:
  1006             mod = None
  1049             mod = None
  1038                     b" extensions)\n"
  1081                     b" extensions)\n"
  1039                 )
  1082                 )
  1040             )
  1083             )
  1041         return rst
  1084         return rst
  1042 
  1085 
  1043     def helpextcmd(name, subtopic=None):
  1086     def helpextcmd(
       
  1087         name: bytes, subtopic: Optional[bytes] = None
       
  1088     ) -> List[bytes]:
  1044         cmd, ext, doc = extensions.disabledcmd(
  1089         cmd, ext, doc = extensions.disabledcmd(
  1045             ui, name, ui.configbool(b'ui', b'strict')
  1090             ui, name, ui.configbool(b'ui', b'strict')
  1046         )
  1091         )
  1047         doc = stringutil.firstline(doc)
  1092         doc = stringutil.firstline(doc)
  1048 
  1093 
  1125 
  1170 
  1126     return b''.join(rst)
  1171     return b''.join(rst)
  1127 
  1172 
  1128 
  1173 
  1129 def formattedhelp(
  1174 def formattedhelp(
  1130     ui, commands, fullname, keep=None, unknowncmd=False, full=True, **opts
  1175     ui: uimod.ui,
  1131 ):
  1176     commands,
       
  1177     fullname: Optional[bytes],
       
  1178     keep: Optional[Iterable[bytes]] = None,
       
  1179     unknowncmd: bool = False,
       
  1180     full: bool = True,
       
  1181     **opts
       
  1182 ) -> bytes:
  1132     """get help for a given topic (as a dotted name) as rendered rst
  1183     """get help for a given topic (as a dotted name) as rendered rst
  1133 
  1184 
  1134     Either returns the rendered help text or raises an exception.
  1185     Either returns the rendered help text or raises an exception.
  1135     """
  1186     """
  1136     if keep is None:
  1187     if keep is None: