##// END OF EJS Templates
help: don't crash in keyword search if an extension fails to provide docs...
Simon Farnsworth -
r28058:ff6e8dc6 default
parent child Browse files
Show More
@@ -1,600 +1,602 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 os
12 12 import textwrap
13 13
14 14 from .i18n import (
15 15 _,
16 16 gettext,
17 17 )
18 18 from . import (
19 19 cmdutil,
20 20 encoding,
21 21 error,
22 22 extensions,
23 23 filemerge,
24 24 fileset,
25 25 minirst,
26 26 revset,
27 27 templatefilters,
28 28 templatekw,
29 29 templater,
30 30 util,
31 31 )
32 32 from .hgweb import (
33 33 webcommands,
34 34 )
35 35
36 36 _exclkeywords = [
37 37 "(DEPRECATED)",
38 38 "(EXPERIMENTAL)",
39 39 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
40 40 _("(DEPRECATED)"),
41 41 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
42 42 _("(EXPERIMENTAL)"),
43 43 ]
44 44
45 45 def listexts(header, exts, indent=1, showdeprecated=False):
46 46 '''return a text listing of the given extensions'''
47 47 rst = []
48 48 if exts:
49 49 for name, desc in sorted(exts.iteritems()):
50 50 if not showdeprecated and any(w in desc for w in _exclkeywords):
51 51 continue
52 52 rst.append('%s:%s: %s\n' % (' ' * indent, name, desc))
53 53 if rst:
54 54 rst.insert(0, '\n%s\n\n' % header)
55 55 return rst
56 56
57 57 def extshelp(ui):
58 58 rst = loaddoc('extensions')(ui).splitlines(True)
59 59 rst.extend(listexts(
60 60 _('enabled extensions:'), extensions.enabled(), showdeprecated=True))
61 61 rst.extend(listexts(_('disabled extensions:'), extensions.disabled()))
62 62 doc = ''.join(rst)
63 63 return doc
64 64
65 65 def optrst(header, options, verbose):
66 66 data = []
67 67 multioccur = False
68 68 for option in options:
69 69 if len(option) == 5:
70 70 shortopt, longopt, default, desc, optlabel = option
71 71 else:
72 72 shortopt, longopt, default, desc = option
73 73 optlabel = _("VALUE") # default label
74 74
75 75 if not verbose and any(w in desc for w in _exclkeywords):
76 76 continue
77 77
78 78 so = ''
79 79 if shortopt:
80 80 so = '-' + shortopt
81 81 lo = '--' + longopt
82 82 if default:
83 83 desc += _(" (default: %s)") % default
84 84
85 85 if isinstance(default, list):
86 86 lo += " %s [+]" % optlabel
87 87 multioccur = True
88 88 elif (default is not None) and not isinstance(default, bool):
89 89 lo += " %s" % optlabel
90 90
91 91 data.append((so, lo, desc))
92 92
93 93 if multioccur:
94 94 header += (_(" ([+] can be repeated)"))
95 95
96 96 rst = ['\n%s:\n\n' % header]
97 97 rst.extend(minirst.maketable(data, 1))
98 98
99 99 return ''.join(rst)
100 100
101 101 def indicateomitted(rst, omitted, notomitted=None):
102 102 rst.append('\n\n.. container:: omitted\n\n %s\n\n' % omitted)
103 103 if notomitted:
104 104 rst.append('\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
105 105
106 106 def filtercmd(ui, cmd, kw, doc):
107 107 if not ui.debugflag and cmd.startswith("debug") and kw != "debug":
108 108 return True
109 109 if not ui.verbose and doc and any(w in doc for w in _exclkeywords):
110 110 return True
111 111 return False
112 112
113 113 def topicmatch(ui, kw):
114 114 """Return help topics matching kw.
115 115
116 116 Returns {'section': [(name, summary), ...], ...} where section is
117 117 one of topics, commands, extensions, or extensioncommands.
118 118 """
119 119 kw = encoding.lower(kw)
120 120 def lowercontains(container):
121 121 return kw in encoding.lower(container) # translated in helptable
122 122 results = {'topics': [],
123 123 'commands': [],
124 124 'extensions': [],
125 125 'extensioncommands': [],
126 126 }
127 127 for names, header, doc in helptable:
128 128 # Old extensions may use a str as doc.
129 129 if (sum(map(lowercontains, names))
130 130 or lowercontains(header)
131 131 or (callable(doc) and lowercontains(doc(ui)))):
132 132 results['topics'].append((names[0], header))
133 133 from . import commands # avoid cycle
134 134 for cmd, entry in commands.table.iteritems():
135 135 if len(entry) == 3:
136 136 summary = entry[2]
137 137 else:
138 138 summary = ''
139 139 # translate docs *before* searching there
140 140 docs = _(getattr(entry[0], '__doc__', None)) or ''
141 141 if kw in cmd or lowercontains(summary) or lowercontains(docs):
142 142 doclines = docs.splitlines()
143 143 if doclines:
144 144 summary = doclines[0]
145 145 cmdname = cmd.partition('|')[0].lstrip('^')
146 146 if filtercmd(ui, cmdname, kw, docs):
147 147 continue
148 148 results['commands'].append((cmdname, summary))
149 149 for name, docs in itertools.chain(
150 150 extensions.enabled(False).iteritems(),
151 151 extensions.disabled().iteritems()):
152 if not docs:
153 continue
152 154 mod = extensions.load(ui, name, '')
153 155 name = name.rpartition('.')[-1]
154 156 if lowercontains(name) or lowercontains(docs):
155 157 # extension docs are already translated
156 158 results['extensions'].append((name, docs.splitlines()[0]))
157 159 for cmd, entry in getattr(mod, 'cmdtable', {}).iteritems():
158 160 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
159 161 cmdname = cmd.partition('|')[0].lstrip('^')
160 162 if entry[0].__doc__:
161 163 cmddoc = gettext(entry[0].__doc__).splitlines()[0]
162 164 else:
163 165 cmddoc = _('(no help text available)')
164 166 if filtercmd(ui, cmdname, kw, cmddoc):
165 167 continue
166 168 results['extensioncommands'].append((cmdname, cmddoc))
167 169 return results
168 170
169 171 def loaddoc(topic, subdir=None):
170 172 """Return a delayed loader for help/topic.txt."""
171 173
172 174 def loader(ui):
173 175 docdir = os.path.join(util.datapath, 'help')
174 176 if subdir:
175 177 docdir = os.path.join(docdir, subdir)
176 178 path = os.path.join(docdir, topic + ".txt")
177 179 doc = gettext(util.readfile(path))
178 180 for rewriter in helphooks.get(topic, []):
179 181 doc = rewriter(ui, topic, doc)
180 182 return doc
181 183
182 184 return loader
183 185
184 186 internalstable = sorted([
185 187 (['bundles'], _('container for exchange of repository data'),
186 188 loaddoc('bundles', subdir='internals')),
187 189 (['changegroups'], _('representation of revlog data'),
188 190 loaddoc('changegroups', subdir='internals')),
189 191 (['revlogs'], _('revision storage mechanism'),
190 192 loaddoc('revlogs', subdir='internals')),
191 193 ])
192 194
193 195 def internalshelp(ui):
194 196 """Generate the index for the "internals" topic."""
195 197 lines = []
196 198 for names, header, doc in internalstable:
197 199 lines.append(' :%s: %s\n' % (names[0], header))
198 200
199 201 return ''.join(lines)
200 202
201 203 helptable = sorted([
202 204 (["config", "hgrc"], _("Configuration Files"), loaddoc('config')),
203 205 (["dates"], _("Date Formats"), loaddoc('dates')),
204 206 (["patterns"], _("File Name Patterns"), loaddoc('patterns')),
205 207 (['environment', 'env'], _('Environment Variables'),
206 208 loaddoc('environment')),
207 209 (['revisions', 'revs'], _('Specifying Single Revisions'),
208 210 loaddoc('revisions')),
209 211 (['multirevs', 'mrevs'], _('Specifying Multiple Revisions'),
210 212 loaddoc('multirevs')),
211 213 (['revsets', 'revset'], _("Specifying Revision Sets"), loaddoc('revsets')),
212 214 (['filesets', 'fileset'], _("Specifying File Sets"), loaddoc('filesets')),
213 215 (['diffs'], _('Diff Formats'), loaddoc('diffs')),
214 216 (['merge-tools', 'mergetools'], _('Merge Tools'), loaddoc('merge-tools')),
215 217 (['templating', 'templates', 'template', 'style'], _('Template Usage'),
216 218 loaddoc('templates')),
217 219 (['urls'], _('URL Paths'), loaddoc('urls')),
218 220 (["extensions"], _("Using Additional Features"), extshelp),
219 221 (["subrepos", "subrepo"], _("Subrepositories"), loaddoc('subrepos')),
220 222 (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')),
221 223 (["glossary"], _("Glossary"), loaddoc('glossary')),
222 224 (["hgignore", "ignore"], _("Syntax for Mercurial Ignore Files"),
223 225 loaddoc('hgignore')),
224 226 (["phases"], _("Working with Phases"), loaddoc('phases')),
225 227 (['scripting'], _('Using Mercurial from scripts and automation'),
226 228 loaddoc('scripting')),
227 229 (['internals'], _("Technical implementation topics"),
228 230 internalshelp),
229 231 ])
230 232
231 233 # Maps topics with sub-topics to a list of their sub-topics.
232 234 subtopics = {
233 235 'internals': internalstable,
234 236 }
235 237
236 238 # Map topics to lists of callable taking the current topic help and
237 239 # returning the updated version
238 240 helphooks = {}
239 241
240 242 def addtopichook(topic, rewriter):
241 243 helphooks.setdefault(topic, []).append(rewriter)
242 244
243 245 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
244 246 """Extract docstring from the items key to function mapping, build a
245 247 single documentation block and use it to overwrite the marker in doc.
246 248 """
247 249 entries = []
248 250 for name in sorted(items):
249 251 text = (items[name].__doc__ or '').rstrip()
250 252 if (not text
251 253 or not ui.verbose and any(w in text for w in _exclkeywords)):
252 254 continue
253 255 text = gettext(text)
254 256 if dedent:
255 257 text = textwrap.dedent(text)
256 258 lines = text.splitlines()
257 259 doclines = [(lines[0])]
258 260 for l in lines[1:]:
259 261 # Stop once we find some Python doctest
260 262 if l.strip().startswith('>>>'):
261 263 break
262 264 if dedent:
263 265 doclines.append(l.rstrip())
264 266 else:
265 267 doclines.append(' ' + l.strip())
266 268 entries.append('\n'.join(doclines))
267 269 entries = '\n\n'.join(entries)
268 270 return doc.replace(marker, entries)
269 271
270 272 def addtopicsymbols(topic, marker, symbols, dedent=False):
271 273 def add(ui, topic, doc):
272 274 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
273 275 addtopichook(topic, add)
274 276
275 277 addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols)
276 278 addtopicsymbols('merge-tools', '.. internaltoolsmarker',
277 279 filemerge.internalsdoc)
278 280 addtopicsymbols('revsets', '.. predicatesmarker', revset.symbols)
279 281 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
280 282 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
281 283 addtopicsymbols('templates', '.. functionsmarker', templater.funcs)
282 284 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
283 285 dedent=True)
284 286
285 287 def help_(ui, name, unknowncmd=False, full=True, subtopic=None, **opts):
286 288 '''
287 289 Generate the help for 'name' as unformatted restructured text. If
288 290 'name' is None, describe the commands available.
289 291 '''
290 292
291 293 from . import commands # avoid cycle
292 294
293 295 def helpcmd(name, subtopic=None):
294 296 try:
295 297 aliases, entry = cmdutil.findcmd(name, commands.table,
296 298 strict=unknowncmd)
297 299 except error.AmbiguousCommand as inst:
298 300 # py3k fix: except vars can't be used outside the scope of the
299 301 # except block, nor can be used inside a lambda. python issue4617
300 302 prefix = inst.args[0]
301 303 select = lambda c: c.lstrip('^').startswith(prefix)
302 304 rst = helplist(select)
303 305 return rst
304 306
305 307 rst = []
306 308
307 309 # check if it's an invalid alias and display its error if it is
308 310 if getattr(entry[0], 'badalias', None):
309 311 rst.append(entry[0].badalias + '\n')
310 312 if entry[0].unknowncmd:
311 313 try:
312 314 rst.extend(helpextcmd(entry[0].cmdname))
313 315 except error.UnknownCommand:
314 316 pass
315 317 return rst
316 318
317 319 # synopsis
318 320 if len(entry) > 2:
319 321 if entry[2].startswith('hg'):
320 322 rst.append("%s\n" % entry[2])
321 323 else:
322 324 rst.append('hg %s %s\n' % (aliases[0], entry[2]))
323 325 else:
324 326 rst.append('hg %s\n' % aliases[0])
325 327 # aliases
326 328 if full and not ui.quiet and len(aliases) > 1:
327 329 rst.append(_("\naliases: %s\n") % ', '.join(aliases[1:]))
328 330 rst.append('\n')
329 331
330 332 # description
331 333 doc = gettext(entry[0].__doc__)
332 334 if not doc:
333 335 doc = _("(no help text available)")
334 336 if util.safehasattr(entry[0], 'definition'): # aliased command
335 337 if entry[0].definition.startswith('!'): # shell alias
336 338 doc = _('shell alias for::\n\n %s') % entry[0].definition[1:]
337 339 else:
338 340 doc = _('alias for: hg %s\n\n%s') % (entry[0].definition, doc)
339 341 doc = doc.splitlines(True)
340 342 if ui.quiet or not full:
341 343 rst.append(doc[0])
342 344 else:
343 345 rst.extend(doc)
344 346 rst.append('\n')
345 347
346 348 # check if this command shadows a non-trivial (multi-line)
347 349 # extension help text
348 350 try:
349 351 mod = extensions.find(name)
350 352 doc = gettext(mod.__doc__) or ''
351 353 if '\n' in doc.strip():
352 354 msg = _('(use "hg help -e %s" to show help for '
353 355 'the %s extension)') % (name, name)
354 356 rst.append('\n%s\n' % msg)
355 357 except KeyError:
356 358 pass
357 359
358 360 # options
359 361 if not ui.quiet and entry[1]:
360 362 rst.append(optrst(_("options"), entry[1], ui.verbose))
361 363
362 364 if ui.verbose:
363 365 rst.append(optrst(_("global options"),
364 366 commands.globalopts, ui.verbose))
365 367
366 368 if not ui.verbose:
367 369 if not full:
368 370 rst.append(_('\n(use "hg %s -h" to show more help)\n')
369 371 % name)
370 372 elif not ui.quiet:
371 373 rst.append(_('\n(some details hidden, use --verbose '
372 374 'to show complete help)'))
373 375
374 376 return rst
375 377
376 378
377 379 def helplist(select=None, **opts):
378 380 # list of commands
379 381 if name == "shortlist":
380 382 header = _('basic commands:\n\n')
381 383 elif name == "debug":
382 384 header = _('debug commands (internal and unsupported):\n\n')
383 385 else:
384 386 header = _('list of commands:\n\n')
385 387
386 388 h = {}
387 389 cmds = {}
388 390 for c, e in commands.table.iteritems():
389 391 f = c.partition("|")[0]
390 392 if select and not select(f):
391 393 continue
392 394 if (not select and name != 'shortlist' and
393 395 e[0].__module__ != commands.__name__):
394 396 continue
395 397 if name == "shortlist" and not f.startswith("^"):
396 398 continue
397 399 f = f.lstrip("^")
398 400 doc = e[0].__doc__
399 401 if filtercmd(ui, f, name, doc):
400 402 continue
401 403 doc = gettext(doc)
402 404 if not doc:
403 405 doc = _("(no help text available)")
404 406 h[f] = doc.splitlines()[0].rstrip()
405 407 cmds[f] = c.lstrip("^")
406 408
407 409 rst = []
408 410 if not h:
409 411 if not ui.quiet:
410 412 rst.append(_('no commands defined\n'))
411 413 return rst
412 414
413 415 if not ui.quiet:
414 416 rst.append(header)
415 417 fns = sorted(h)
416 418 for f in fns:
417 419 if ui.verbose:
418 420 commacmds = cmds[f].replace("|",", ")
419 421 rst.append(" :%s: %s\n" % (commacmds, h[f]))
420 422 else:
421 423 rst.append(' :%s: %s\n' % (f, h[f]))
422 424
423 425 ex = opts.get
424 426 anyopts = (ex('keyword') or not (ex('command') or ex('extension')))
425 427 if not name and anyopts:
426 428 exts = listexts(_('enabled extensions:'), extensions.enabled())
427 429 if exts:
428 430 rst.append('\n')
429 431 rst.extend(exts)
430 432
431 433 rst.append(_("\nadditional help topics:\n\n"))
432 434 topics = []
433 435 for names, header, doc in helptable:
434 436 topics.append((names[0], header))
435 437 for t, desc in topics:
436 438 rst.append(" :%s: %s\n" % (t, desc))
437 439
438 440 if ui.quiet:
439 441 pass
440 442 elif ui.verbose:
441 443 rst.append('\n%s\n' % optrst(_("global options"),
442 444 commands.globalopts, ui.verbose))
443 445 if name == 'shortlist':
444 446 rst.append(_('\n(use "hg help" for the full list '
445 447 'of commands)\n'))
446 448 else:
447 449 if name == 'shortlist':
448 450 rst.append(_('\n(use "hg help" for the full list of commands '
449 451 'or "hg -v" for details)\n'))
450 452 elif name and not full:
451 453 rst.append(_('\n(use "hg help %s" to show the full help '
452 454 'text)\n') % name)
453 455 elif name and cmds and name in cmds.keys():
454 456 rst.append(_('\n(use "hg help -v -e %s" to show built-in '
455 457 'aliases and global options)\n') % name)
456 458 else:
457 459 rst.append(_('\n(use "hg help -v%s" to show built-in aliases '
458 460 'and global options)\n')
459 461 % (name and " " + name or ""))
460 462 return rst
461 463
462 464 def helptopic(name, subtopic=None):
463 465 # Look for sub-topic entry first.
464 466 header, doc = None, None
465 467 if subtopic and name in subtopics:
466 468 for names, header, doc in subtopics[name]:
467 469 if subtopic in names:
468 470 break
469 471
470 472 if not header:
471 473 for names, header, doc in helptable:
472 474 if name in names:
473 475 break
474 476 else:
475 477 raise error.UnknownCommand(name)
476 478
477 479 rst = [minirst.section(header)]
478 480
479 481 # description
480 482 if not doc:
481 483 rst.append(" %s\n" % _("(no help text available)"))
482 484 if callable(doc):
483 485 rst += [" %s\n" % l for l in doc(ui).splitlines()]
484 486
485 487 if not ui.verbose:
486 488 omitted = _('(some details hidden, use --verbose'
487 489 ' to show complete help)')
488 490 indicateomitted(rst, omitted)
489 491
490 492 try:
491 493 cmdutil.findcmd(name, commands.table)
492 494 rst.append(_('\nuse "hg help -c %s" to see help for '
493 495 'the %s command\n') % (name, name))
494 496 except error.UnknownCommand:
495 497 pass
496 498 return rst
497 499
498 500 def helpext(name, subtopic=None):
499 501 try:
500 502 mod = extensions.find(name)
501 503 doc = gettext(mod.__doc__) or _('no help text available')
502 504 except KeyError:
503 505 mod = None
504 506 doc = extensions.disabledext(name)
505 507 if not doc:
506 508 raise error.UnknownCommand(name)
507 509
508 510 if '\n' not in doc:
509 511 head, tail = doc, ""
510 512 else:
511 513 head, tail = doc.split('\n', 1)
512 514 rst = [_('%s extension - %s\n\n') % (name.rpartition('.')[-1], head)]
513 515 if tail:
514 516 rst.extend(tail.splitlines(True))
515 517 rst.append('\n')
516 518
517 519 if not ui.verbose:
518 520 omitted = _('(some details hidden, use --verbose'
519 521 ' to show complete help)')
520 522 indicateomitted(rst, omitted)
521 523
522 524 if mod:
523 525 try:
524 526 ct = mod.cmdtable
525 527 except AttributeError:
526 528 ct = {}
527 529 modcmds = set([c.partition('|')[0] for c in ct])
528 530 rst.extend(helplist(modcmds.__contains__))
529 531 else:
530 532 rst.append(_('(use "hg help extensions" for information on enabling'
531 533 ' extensions)\n'))
532 534 return rst
533 535
534 536 def helpextcmd(name, subtopic=None):
535 537 cmd, ext, mod = extensions.disabledcmd(ui, name,
536 538 ui.configbool('ui', 'strict'))
537 539 doc = gettext(mod.__doc__).splitlines()[0]
538 540
539 541 rst = listexts(_("'%s' is provided by the following "
540 542 "extension:") % cmd, {ext: doc}, indent=4,
541 543 showdeprecated=True)
542 544 rst.append('\n')
543 545 rst.append(_('(use "hg help extensions" for information on enabling '
544 546 'extensions)\n'))
545 547 return rst
546 548
547 549
548 550 rst = []
549 551 kw = opts.get('keyword')
550 552 if kw or name is None and any(opts[o] for o in opts):
551 553 matches = topicmatch(ui, name or '')
552 554 helpareas = []
553 555 if opts.get('extension'):
554 556 helpareas += [('extensions', _('Extensions'))]
555 557 if opts.get('command'):
556 558 helpareas += [('commands', _('Commands'))]
557 559 if not helpareas:
558 560 helpareas = [('topics', _('Topics')),
559 561 ('commands', _('Commands')),
560 562 ('extensions', _('Extensions')),
561 563 ('extensioncommands', _('Extension Commands'))]
562 564 for t, title in helpareas:
563 565 if matches[t]:
564 566 rst.append('%s:\n\n' % title)
565 567 rst.extend(minirst.maketable(sorted(matches[t]), 1))
566 568 rst.append('\n')
567 569 if not rst:
568 570 msg = _('no matches')
569 571 hint = _('try "hg help" for a list of topics')
570 572 raise error.Abort(msg, hint=hint)
571 573 elif name and name != 'shortlist':
572 574 queries = []
573 575 if unknowncmd:
574 576 queries += [helpextcmd]
575 577 if opts.get('extension'):
576 578 queries += [helpext]
577 579 if opts.get('command'):
578 580 queries += [helpcmd]
579 581 if not queries:
580 582 queries = (helptopic, helpcmd, helpext, helpextcmd)
581 583 for f in queries:
582 584 try:
583 585 rst = f(name, subtopic)
584 586 break
585 587 except error.UnknownCommand:
586 588 pass
587 589 else:
588 590 if unknowncmd:
589 591 raise error.UnknownCommand(name)
590 592 else:
591 593 msg = _('no such help topic: %s') % name
592 594 hint = _('try "hg help --keyword %s"') % name
593 595 raise error.Abort(msg, hint=hint)
594 596 else:
595 597 # program name
596 598 if not ui.quiet:
597 599 rst = [_("Mercurial Distributed SCM\n"), '\n']
598 600 rst.extend(helplist(None, **opts))
599 601
600 602 return ''.join(rst)
General Comments 0
You need to be logged in to leave comments. Login now