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