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