##// END OF EJS Templates
help: load module doc of disabled extension in extensions.disabledcmd()...
Yuya Nishihara -
r37996:6e526b09 default
parent child Browse files
Show More
@@ -1,734 +1,735 b''
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 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 functools
11 11 import imp
12 12 import inspect
13 13 import os
14 14
15 15 from .i18n import (
16 16 _,
17 17 gettext,
18 18 )
19 19
20 20 from . import (
21 21 cmdutil,
22 22 configitems,
23 23 error,
24 24 pycompat,
25 25 util,
26 26 )
27 27
28 28 from .utils import (
29 29 stringutil,
30 30 )
31 31
32 32 _extensions = {}
33 33 _disabledextensions = {}
34 34 _aftercallbacks = {}
35 35 _order = []
36 36 _builtin = {
37 37 'hbisect',
38 38 'bookmarks',
39 39 'color',
40 40 'parentrevspec',
41 41 'progress',
42 42 'interhg',
43 43 'inotify',
44 44 'hgcia'
45 45 }
46 46
47 47 def extensions(ui=None):
48 48 if ui:
49 49 def enabled(name):
50 50 for format in ['%s', 'hgext.%s']:
51 51 conf = ui.config('extensions', format % name)
52 52 if conf is not None and not conf.startswith('!'):
53 53 return True
54 54 else:
55 55 enabled = lambda name: True
56 56 for name in _order:
57 57 module = _extensions[name]
58 58 if module and enabled(name):
59 59 yield name, module
60 60
61 61 def find(name):
62 62 '''return module with given extension name'''
63 63 mod = None
64 64 try:
65 65 mod = _extensions[name]
66 66 except KeyError:
67 67 for k, v in _extensions.iteritems():
68 68 if k.endswith('.' + name) or k.endswith('/' + name):
69 69 mod = v
70 70 break
71 71 if not mod:
72 72 raise KeyError(name)
73 73 return mod
74 74
75 75 def loadpath(path, module_name):
76 76 module_name = module_name.replace('.', '_')
77 77 path = util.normpath(util.expandpath(path))
78 78 module_name = pycompat.fsdecode(module_name)
79 79 path = pycompat.fsdecode(path)
80 80 if os.path.isdir(path):
81 81 # module/__init__.py style
82 82 d, f = os.path.split(path)
83 83 fd, fpath, desc = imp.find_module(f, [d])
84 84 return imp.load_module(module_name, fd, fpath, desc)
85 85 else:
86 86 try:
87 87 return imp.load_source(module_name, path)
88 88 except IOError as exc:
89 89 if not exc.filename:
90 90 exc.filename = path # python does not fill this
91 91 raise
92 92
93 93 def _importh(name):
94 94 """import and return the <name> module"""
95 95 mod = __import__(pycompat.sysstr(name))
96 96 components = name.split('.')
97 97 for comp in components[1:]:
98 98 mod = getattr(mod, comp)
99 99 return mod
100 100
101 101 def _importext(name, path=None, reportfunc=None):
102 102 if path:
103 103 # the module will be loaded in sys.modules
104 104 # choose an unique name so that it doesn't
105 105 # conflicts with other modules
106 106 mod = loadpath(path, 'hgext.%s' % name)
107 107 else:
108 108 try:
109 109 mod = _importh("hgext.%s" % name)
110 110 except ImportError as err:
111 111 if reportfunc:
112 112 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
113 113 try:
114 114 mod = _importh("hgext3rd.%s" % name)
115 115 except ImportError as err:
116 116 if reportfunc:
117 117 reportfunc(err, "hgext3rd.%s" % name, name)
118 118 mod = _importh(name)
119 119 return mod
120 120
121 121 def _reportimporterror(ui, err, failed, next):
122 122 # note: this ui.debug happens before --debug is processed,
123 123 # Use --config ui.debug=1 to see them.
124 124 ui.debug('could not import %s (%s): trying %s\n'
125 125 % (failed, stringutil.forcebytestr(err), next))
126 126 if ui.debugflag:
127 127 ui.traceback()
128 128
129 129 def _rejectunicode(name, xs):
130 130 if isinstance(xs, (list, set, tuple)):
131 131 for x in xs:
132 132 _rejectunicode(name, x)
133 133 elif isinstance(xs, dict):
134 134 for k, v in xs.items():
135 135 _rejectunicode(name, k)
136 136 _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
137 137 elif isinstance(xs, type(u'')):
138 138 raise error.ProgrammingError(b"unicode %r found in %s" % (xs, name),
139 139 hint="use b'' to make it byte string")
140 140
141 141 # attributes set by registrar.command
142 142 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
143 143
144 144 def _validatecmdtable(ui, cmdtable):
145 145 """Check if extension commands have required attributes"""
146 146 for c, e in cmdtable.iteritems():
147 147 f = e[0]
148 148 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
149 149 if not missing:
150 150 continue
151 151 raise error.ProgrammingError(
152 152 'missing attributes: %s' % ', '.join(missing),
153 153 hint="use @command decorator to register '%s'" % c)
154 154
155 155 def _validatetables(ui, mod):
156 156 """Sanity check for loadable tables provided by extension module"""
157 157 for t in ['cmdtable', 'colortable', 'configtable']:
158 158 _rejectunicode(t, getattr(mod, t, {}))
159 159 for t in ['filesetpredicate', 'internalmerge', 'revsetpredicate',
160 160 'templatefilter', 'templatefunc', 'templatekeyword']:
161 161 o = getattr(mod, t, None)
162 162 if o:
163 163 _rejectunicode(t, o._table)
164 164 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
165 165
166 166 def load(ui, name, path):
167 167 if name.startswith('hgext.') or name.startswith('hgext/'):
168 168 shortname = name[6:]
169 169 else:
170 170 shortname = name
171 171 if shortname in _builtin:
172 172 return None
173 173 if shortname in _extensions:
174 174 return _extensions[shortname]
175 175 _extensions[shortname] = None
176 176 mod = _importext(name, path, bind(_reportimporterror, ui))
177 177
178 178 # Before we do anything with the extension, check against minimum stated
179 179 # compatibility. This gives extension authors a mechanism to have their
180 180 # extensions short circuit when loaded with a known incompatible version
181 181 # of Mercurial.
182 182 minver = getattr(mod, 'minimumhgversion', None)
183 183 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
184 184 ui.warn(_('(third party extension %s requires version %s or newer '
185 185 'of Mercurial; disabling)\n') % (shortname, minver))
186 186 return
187 187 _validatetables(ui, mod)
188 188
189 189 _extensions[shortname] = mod
190 190 _order.append(shortname)
191 191 for fn in _aftercallbacks.get(shortname, []):
192 192 fn(loaded=True)
193 193 return mod
194 194
195 195 def _runuisetup(name, ui):
196 196 uisetup = getattr(_extensions[name], 'uisetup', None)
197 197 if uisetup:
198 198 try:
199 199 uisetup(ui)
200 200 except Exception as inst:
201 201 ui.traceback(force=True)
202 202 msg = stringutil.forcebytestr(inst)
203 203 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
204 204 return False
205 205 return True
206 206
207 207 def _runextsetup(name, ui):
208 208 extsetup = getattr(_extensions[name], 'extsetup', None)
209 209 if extsetup:
210 210 try:
211 211 try:
212 212 extsetup(ui)
213 213 except TypeError:
214 214 if pycompat.getargspec(extsetup).args:
215 215 raise
216 216 extsetup() # old extsetup with no ui argument
217 217 except Exception as inst:
218 218 ui.traceback(force=True)
219 219 msg = stringutil.forcebytestr(inst)
220 220 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
221 221 return False
222 222 return True
223 223
224 224 def loadall(ui, whitelist=None):
225 225 result = ui.configitems("extensions")
226 226 if whitelist is not None:
227 227 result = [(k, v) for (k, v) in result if k in whitelist]
228 228 newindex = len(_order)
229 229 for (name, path) in result:
230 230 if path:
231 231 if path[0:1] == '!':
232 232 _disabledextensions[name] = path[1:]
233 233 continue
234 234 try:
235 235 load(ui, name, path)
236 236 except Exception as inst:
237 237 msg = stringutil.forcebytestr(inst)
238 238 if path:
239 239 ui.warn(_("*** failed to import extension %s from %s: %s\n")
240 240 % (name, path, msg))
241 241 else:
242 242 ui.warn(_("*** failed to import extension %s: %s\n")
243 243 % (name, msg))
244 244 if isinstance(inst, error.Hint) and inst.hint:
245 245 ui.warn(_("*** (%s)\n") % inst.hint)
246 246 ui.traceback()
247 247 # list of (objname, loadermod, loadername) tuple:
248 248 # - objname is the name of an object in extension module,
249 249 # from which extra information is loaded
250 250 # - loadermod is the module where loader is placed
251 251 # - loadername is the name of the function,
252 252 # which takes (ui, extensionname, extraobj) arguments
253 253 #
254 254 # This one is for the list of item that must be run before running any setup
255 255 earlyextraloaders = [
256 256 ('configtable', configitems, 'loadconfigtable'),
257 257 ]
258 258 _loadextra(ui, newindex, earlyextraloaders)
259 259
260 260 broken = set()
261 261 for name in _order[newindex:]:
262 262 if not _runuisetup(name, ui):
263 263 broken.add(name)
264 264
265 265 for name in _order[newindex:]:
266 266 if name in broken:
267 267 continue
268 268 if not _runextsetup(name, ui):
269 269 broken.add(name)
270 270
271 271 for name in broken:
272 272 _extensions[name] = None
273 273
274 274 # Call aftercallbacks that were never met.
275 275 for shortname in _aftercallbacks:
276 276 if shortname in _extensions:
277 277 continue
278 278
279 279 for fn in _aftercallbacks[shortname]:
280 280 fn(loaded=False)
281 281
282 282 # loadall() is called multiple times and lingering _aftercallbacks
283 283 # entries could result in double execution. See issue4646.
284 284 _aftercallbacks.clear()
285 285
286 286 # delay importing avoids cyclic dependency (especially commands)
287 287 from . import (
288 288 color,
289 289 commands,
290 290 filemerge,
291 291 fileset,
292 292 revset,
293 293 templatefilters,
294 294 templatefuncs,
295 295 templatekw,
296 296 )
297 297
298 298 # list of (objname, loadermod, loadername) tuple:
299 299 # - objname is the name of an object in extension module,
300 300 # from which extra information is loaded
301 301 # - loadermod is the module where loader is placed
302 302 # - loadername is the name of the function,
303 303 # which takes (ui, extensionname, extraobj) arguments
304 304 extraloaders = [
305 305 ('cmdtable', commands, 'loadcmdtable'),
306 306 ('colortable', color, 'loadcolortable'),
307 307 ('filesetpredicate', fileset, 'loadpredicate'),
308 308 ('internalmerge', filemerge, 'loadinternalmerge'),
309 309 ('revsetpredicate', revset, 'loadpredicate'),
310 310 ('templatefilter', templatefilters, 'loadfilter'),
311 311 ('templatefunc', templatefuncs, 'loadfunction'),
312 312 ('templatekeyword', templatekw, 'loadkeyword'),
313 313 ]
314 314 _loadextra(ui, newindex, extraloaders)
315 315
316 316 def _loadextra(ui, newindex, extraloaders):
317 317 for name in _order[newindex:]:
318 318 module = _extensions[name]
319 319 if not module:
320 320 continue # loading this module failed
321 321
322 322 for objname, loadermod, loadername in extraloaders:
323 323 extraobj = getattr(module, objname, None)
324 324 if extraobj is not None:
325 325 getattr(loadermod, loadername)(ui, name, extraobj)
326 326
327 327 def afterloaded(extension, callback):
328 328 '''Run the specified function after a named extension is loaded.
329 329
330 330 If the named extension is already loaded, the callback will be called
331 331 immediately.
332 332
333 333 If the named extension never loads, the callback will be called after
334 334 all extensions have been loaded.
335 335
336 336 The callback receives the named argument ``loaded``, which is a boolean
337 337 indicating whether the dependent extension actually loaded.
338 338 '''
339 339
340 340 if extension in _extensions:
341 341 # Report loaded as False if the extension is disabled
342 342 loaded = (_extensions[extension] is not None)
343 343 callback(loaded=loaded)
344 344 else:
345 345 _aftercallbacks.setdefault(extension, []).append(callback)
346 346
347 347 def bind(func, *args):
348 348 '''Partial function application
349 349
350 350 Returns a new function that is the partial application of args and kwargs
351 351 to func. For example,
352 352
353 353 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
354 354 assert callable(func)
355 355 def closure(*a, **kw):
356 356 return func(*(args + a), **kw)
357 357 return closure
358 358
359 359 def _updatewrapper(wrap, origfn, unboundwrapper):
360 360 '''Copy and add some useful attributes to wrapper'''
361 361 try:
362 362 wrap.__name__ = origfn.__name__
363 363 except AttributeError:
364 364 pass
365 365 wrap.__module__ = getattr(origfn, '__module__')
366 366 wrap.__doc__ = getattr(origfn, '__doc__')
367 367 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
368 368 wrap._origfunc = origfn
369 369 wrap._unboundwrapper = unboundwrapper
370 370
371 371 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
372 372 '''Wrap the command named `command' in table
373 373
374 374 Replace command in the command table with wrapper. The wrapped command will
375 375 be inserted into the command table specified by the table argument.
376 376
377 377 The wrapper will be called like
378 378
379 379 wrapper(orig, *args, **kwargs)
380 380
381 381 where orig is the original (wrapped) function, and *args, **kwargs
382 382 are the arguments passed to it.
383 383
384 384 Optionally append to the command synopsis and docstring, used for help.
385 385 For example, if your extension wraps the ``bookmarks`` command to add the
386 386 flags ``--remote`` and ``--all`` you might call this function like so:
387 387
388 388 synopsis = ' [-a] [--remote]'
389 389 docstring = """
390 390
391 391 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
392 392 flags to the bookmarks command. Either flag will show the remote bookmarks
393 393 known to the repository; ``--remote`` will also suppress the output of the
394 394 local bookmarks.
395 395 """
396 396
397 397 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
398 398 synopsis, docstring)
399 399 '''
400 400 assert callable(wrapper)
401 401 aliases, entry = cmdutil.findcmd(command, table)
402 402 for alias, e in table.iteritems():
403 403 if e is entry:
404 404 key = alias
405 405 break
406 406
407 407 origfn = entry[0]
408 408 wrap = functools.partial(util.checksignature(wrapper),
409 409 util.checksignature(origfn))
410 410 _updatewrapper(wrap, origfn, wrapper)
411 411 if docstring is not None:
412 412 wrap.__doc__ += docstring
413 413
414 414 newentry = list(entry)
415 415 newentry[0] = wrap
416 416 if synopsis is not None:
417 417 newentry[2] += synopsis
418 418 table[key] = tuple(newentry)
419 419 return entry
420 420
421 421 def wrapfilecache(cls, propname, wrapper):
422 422 """Wraps a filecache property.
423 423
424 424 These can't be wrapped using the normal wrapfunction.
425 425 """
426 426 propname = pycompat.sysstr(propname)
427 427 assert callable(wrapper)
428 428 for currcls in cls.__mro__:
429 429 if propname in currcls.__dict__:
430 430 origfn = currcls.__dict__[propname].func
431 431 assert callable(origfn)
432 432 def wrap(*args, **kwargs):
433 433 return wrapper(origfn, *args, **kwargs)
434 434 currcls.__dict__[propname].func = wrap
435 435 break
436 436
437 437 if currcls is object:
438 438 raise AttributeError(r"type '%s' has no property '%s'" % (
439 439 cls, propname))
440 440
441 441 class wrappedfunction(object):
442 442 '''context manager for temporarily wrapping a function'''
443 443
444 444 def __init__(self, container, funcname, wrapper):
445 445 assert callable(wrapper)
446 446 self._container = container
447 447 self._funcname = funcname
448 448 self._wrapper = wrapper
449 449
450 450 def __enter__(self):
451 451 wrapfunction(self._container, self._funcname, self._wrapper)
452 452
453 453 def __exit__(self, exctype, excvalue, traceback):
454 454 unwrapfunction(self._container, self._funcname, self._wrapper)
455 455
456 456 def wrapfunction(container, funcname, wrapper):
457 457 '''Wrap the function named funcname in container
458 458
459 459 Replace the funcname member in the given container with the specified
460 460 wrapper. The container is typically a module, class, or instance.
461 461
462 462 The wrapper will be called like
463 463
464 464 wrapper(orig, *args, **kwargs)
465 465
466 466 where orig is the original (wrapped) function, and *args, **kwargs
467 467 are the arguments passed to it.
468 468
469 469 Wrapping methods of the repository object is not recommended since
470 470 it conflicts with extensions that extend the repository by
471 471 subclassing. All extensions that need to extend methods of
472 472 localrepository should use this subclassing trick: namely,
473 473 reposetup() should look like
474 474
475 475 def reposetup(ui, repo):
476 476 class myrepo(repo.__class__):
477 477 def whatever(self, *args, **kwargs):
478 478 [...extension stuff...]
479 479 super(myrepo, self).whatever(*args, **kwargs)
480 480 [...extension stuff...]
481 481
482 482 repo.__class__ = myrepo
483 483
484 484 In general, combining wrapfunction() with subclassing does not
485 485 work. Since you cannot control what other extensions are loaded by
486 486 your end users, you should play nicely with others by using the
487 487 subclass trick.
488 488 '''
489 489 assert callable(wrapper)
490 490
491 491 origfn = getattr(container, funcname)
492 492 assert callable(origfn)
493 493 if inspect.ismodule(container):
494 494 # origfn is not an instance or class method. "partial" can be used.
495 495 # "partial" won't insert a frame in traceback.
496 496 wrap = functools.partial(wrapper, origfn)
497 497 else:
498 498 # "partial" cannot be safely used. Emulate its effect by using "bind".
499 499 # The downside is one more frame in traceback.
500 500 wrap = bind(wrapper, origfn)
501 501 _updatewrapper(wrap, origfn, wrapper)
502 502 setattr(container, funcname, wrap)
503 503 return origfn
504 504
505 505 def unwrapfunction(container, funcname, wrapper=None):
506 506 '''undo wrapfunction
507 507
508 508 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
509 509 from the chain of wrappers.
510 510
511 511 Return the removed wrapper.
512 512 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
513 513 wrapper is not None but is not found in the wrapper chain.
514 514 '''
515 515 chain = getwrapperchain(container, funcname)
516 516 origfn = chain.pop()
517 517 if wrapper is None:
518 518 wrapper = chain[0]
519 519 chain.remove(wrapper)
520 520 setattr(container, funcname, origfn)
521 521 for w in reversed(chain):
522 522 wrapfunction(container, funcname, w)
523 523 return wrapper
524 524
525 525 def getwrapperchain(container, funcname):
526 526 '''get a chain of wrappers of a function
527 527
528 528 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
529 529
530 530 The wrapper functions are the ones passed to wrapfunction, whose first
531 531 argument is origfunc.
532 532 '''
533 533 result = []
534 534 fn = getattr(container, funcname)
535 535 while fn:
536 536 assert callable(fn)
537 537 result.append(getattr(fn, '_unboundwrapper', fn))
538 538 fn = getattr(fn, '_origfunc', None)
539 539 return result
540 540
541 541 def _disabledpaths(strip_init=False):
542 542 '''find paths of disabled extensions. returns a dict of {name: path}
543 543 removes /__init__.py from packages if strip_init is True'''
544 544 import hgext
545 545 extpath = os.path.dirname(
546 546 os.path.abspath(pycompat.fsencode(hgext.__file__)))
547 547 try: # might not be a filesystem path
548 548 files = os.listdir(extpath)
549 549 except OSError:
550 550 return {}
551 551
552 552 exts = {}
553 553 for e in files:
554 554 if e.endswith('.py'):
555 555 name = e.rsplit('.', 1)[0]
556 556 path = os.path.join(extpath, e)
557 557 else:
558 558 name = e
559 559 path = os.path.join(extpath, e, '__init__.py')
560 560 if not os.path.exists(path):
561 561 continue
562 562 if strip_init:
563 563 path = os.path.dirname(path)
564 564 if name in exts or name in _order or name == '__init__':
565 565 continue
566 566 exts[name] = path
567 567 for name, path in _disabledextensions.iteritems():
568 568 # If no path was provided for a disabled extension (e.g. "color=!"),
569 569 # don't replace the path we already found by the scan above.
570 570 if path:
571 571 exts[name] = path
572 572 return exts
573 573
574 574 def _moduledoc(file):
575 575 '''return the top-level python documentation for the given file
576 576
577 577 Loosely inspired by pydoc.source_synopsis(), but rewritten to
578 578 handle triple quotes and to return the whole text instead of just
579 579 the synopsis'''
580 580 result = []
581 581
582 582 line = file.readline()
583 583 while line[:1] == '#' or not line.strip():
584 584 line = file.readline()
585 585 if not line:
586 586 break
587 587
588 588 start = line[:3]
589 589 if start == '"""' or start == "'''":
590 590 line = line[3:]
591 591 while line:
592 592 if line.rstrip().endswith(start):
593 593 line = line.split(start)[0]
594 594 if line:
595 595 result.append(line)
596 596 break
597 597 elif not line:
598 598 return None # unmatched delimiter
599 599 result.append(line)
600 600 line = file.readline()
601 601 else:
602 602 return None
603 603
604 604 return ''.join(result)
605 605
606 606 def _disabledhelp(path):
607 607 '''retrieve help synopsis of a disabled extension (without importing)'''
608 608 try:
609 609 file = open(path)
610 610 except IOError:
611 611 return
612 612 else:
613 613 doc = _moduledoc(file)
614 614 file.close()
615 615
616 616 if doc: # extracting localized synopsis
617 617 return gettext(doc)
618 618 else:
619 619 return _('(no help text available)')
620 620
621 621 def disabled():
622 622 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
623 623 try:
624 624 from hgext import __index__
625 625 return dict((name, gettext(desc))
626 626 for name, desc in __index__.docs.iteritems()
627 627 if name not in _order)
628 628 except (ImportError, AttributeError):
629 629 pass
630 630
631 631 paths = _disabledpaths()
632 632 if not paths:
633 633 return {}
634 634
635 635 exts = {}
636 636 for name, path in paths.iteritems():
637 637 doc = _disabledhelp(path)
638 638 if doc:
639 639 exts[name] = doc.splitlines()[0]
640 640
641 641 return exts
642 642
643 643 def disabledext(name):
644 644 '''find a specific disabled extension from hgext. returns desc'''
645 645 try:
646 646 from hgext import __index__
647 647 if name in _order: # enabled
648 648 return
649 649 else:
650 650 return gettext(__index__.docs.get(name))
651 651 except (ImportError, AttributeError):
652 652 pass
653 653
654 654 paths = _disabledpaths()
655 655 if name in paths:
656 656 return _disabledhelp(paths[name])
657 657
658 658 def _finddisabledcmd(ui, cmd, name, path, strict):
659 659 try:
660 660 mod = loadpath(path, 'hgext.%s' % name)
661 661 except Exception:
662 662 return
663 663 try:
664 664 aliases, entry = cmdutil.findcmd(cmd,
665 665 getattr(mod, 'cmdtable', {}), strict)
666 666 except (error.AmbiguousCommand, error.UnknownCommand):
667 667 return
668 668 except Exception:
669 669 ui.warn(_('warning: error finding commands in %s\n') % path)
670 670 ui.traceback()
671 671 return
672 672 for c in aliases:
673 673 if c.startswith(cmd):
674 674 cmd = c
675 675 break
676 676 else:
677 677 cmd = aliases[0]
678 return (cmd, name, mod)
678 doc = gettext(pycompat.getdoc(mod))
679 return (cmd, name, doc)
679 680
680 681 def disabledcmd(ui, cmd, strict=False):
681 682 '''import disabled extensions until cmd is found.
682 returns (cmdname, extname, module)'''
683 returns (cmdname, extname, doc)'''
683 684
684 685 paths = _disabledpaths(strip_init=True)
685 686 if not paths:
686 687 raise error.UnknownCommand(cmd)
687 688
688 689 ext = None
689 690 # first, search for an extension with the same name as the command
690 691 path = paths.pop(cmd, None)
691 692 if path:
692 693 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
693 694 if not ext:
694 695 # otherwise, interrogate each extension until there's a match
695 696 for name, path in paths.iteritems():
696 697 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
697 698 if ext:
698 699 break
699 700 if ext:
700 701 return ext
701 702
702 703 raise error.UnknownCommand(cmd)
703 704
704 705 def enabled(shortname=True):
705 706 '''return a dict of {name: desc} of extensions'''
706 707 exts = {}
707 708 for ename, ext in extensions():
708 709 doc = (gettext(ext.__doc__) or _('(no help text available)'))
709 710 if shortname:
710 711 ename = ename.split('.')[-1]
711 712 exts[ename] = doc.splitlines()[0].strip()
712 713
713 714 return exts
714 715
715 716 def notloaded():
716 717 '''return short names of extensions that failed to load'''
717 718 return [name for name, mod in _extensions.iteritems() if mod is None]
718 719
719 720 def moduleversion(module):
720 721 '''return version information from given module as a string'''
721 722 if (util.safehasattr(module, 'getversion')
722 723 and callable(module.getversion)):
723 724 version = module.getversion()
724 725 elif util.safehasattr(module, '__version__'):
725 726 version = module.__version__
726 727 else:
727 728 version = ''
728 729 if isinstance(version, (list, tuple)):
729 730 version = '.'.join(str(o) for o in version)
730 731 return version
731 732
732 733 def ismoduleinternal(module):
733 734 exttestedwith = getattr(module, 'testedwith', None)
734 735 return exttestedwith == "ships-with-hg-core"
@@ -1,689 +1,689 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 fancyopts,
24 24 filemerge,
25 25 fileset,
26 26 minirst,
27 27 pycompat,
28 28 revset,
29 29 templatefilters,
30 30 templatefuncs,
31 31 templatekw,
32 32 util,
33 33 )
34 34 from .hgweb import (
35 35 webcommands,
36 36 )
37 37
38 38 _exclkeywords = {
39 39 "(ADVANCED)",
40 40 "(DEPRECATED)",
41 41 "(EXPERIMENTAL)",
42 42 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
43 43 _("(ADVANCED)"),
44 44 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
45 45 _("(DEPRECATED)"),
46 46 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
47 47 _("(EXPERIMENTAL)"),
48 48 }
49 49
50 50 def listexts(header, exts, indent=1, showdeprecated=False):
51 51 '''return a text listing of the given extensions'''
52 52 rst = []
53 53 if exts:
54 54 for name, desc in sorted(exts.iteritems()):
55 55 if not showdeprecated and any(w in desc for w in _exclkeywords):
56 56 continue
57 57 rst.append('%s:%s: %s\n' % (' ' * indent, name, desc))
58 58 if rst:
59 59 rst.insert(0, '\n%s\n\n' % header)
60 60 return rst
61 61
62 62 def extshelp(ui):
63 63 rst = loaddoc('extensions')(ui).splitlines(True)
64 64 rst.extend(listexts(
65 65 _('enabled extensions:'), extensions.enabled(), showdeprecated=True))
66 66 rst.extend(listexts(_('disabled extensions:'), extensions.disabled(),
67 67 showdeprecated=ui.verbose))
68 68 doc = ''.join(rst)
69 69 return doc
70 70
71 71 def optrst(header, options, verbose):
72 72 data = []
73 73 multioccur = False
74 74 for option in options:
75 75 if len(option) == 5:
76 76 shortopt, longopt, default, desc, optlabel = option
77 77 else:
78 78 shortopt, longopt, default, desc = option
79 79 optlabel = _("VALUE") # default label
80 80
81 81 if not verbose and any(w in desc for w in _exclkeywords):
82 82 continue
83 83
84 84 so = ''
85 85 if shortopt:
86 86 so = '-' + shortopt
87 87 lo = '--' + longopt
88 88
89 89 if isinstance(default, fancyopts.customopt):
90 90 default = default.getdefaultvalue()
91 91 if default and not callable(default):
92 92 # default is of unknown type, and in Python 2 we abused
93 93 # the %s-shows-repr property to handle integers etc. To
94 94 # match that behavior on Python 3, we do str(default) and
95 95 # then convert it to bytes.
96 96 desc += _(" (default: %s)") % pycompat.bytestr(default)
97 97
98 98 if isinstance(default, list):
99 99 lo += " %s [+]" % optlabel
100 100 multioccur = True
101 101 elif (default is not None) and not isinstance(default, bool):
102 102 lo += " %s" % optlabel
103 103
104 104 data.append((so, lo, desc))
105 105
106 106 if multioccur:
107 107 header += (_(" ([+] can be repeated)"))
108 108
109 109 rst = ['\n%s:\n\n' % header]
110 110 rst.extend(minirst.maketable(data, 1))
111 111
112 112 return ''.join(rst)
113 113
114 114 def indicateomitted(rst, omitted, notomitted=None):
115 115 rst.append('\n\n.. container:: omitted\n\n %s\n\n' % omitted)
116 116 if notomitted:
117 117 rst.append('\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
118 118
119 119 def filtercmd(ui, cmd, kw, doc):
120 120 if not ui.debugflag and cmd.startswith("debug") and kw != "debug":
121 121 return True
122 122 if not ui.verbose and doc and any(w in doc for w in _exclkeywords):
123 123 return True
124 124 return False
125 125
126 126 def topicmatch(ui, commands, kw):
127 127 """Return help topics matching kw.
128 128
129 129 Returns {'section': [(name, summary), ...], ...} where section is
130 130 one of topics, commands, extensions, or extensioncommands.
131 131 """
132 132 kw = encoding.lower(kw)
133 133 def lowercontains(container):
134 134 return kw in encoding.lower(container) # translated in helptable
135 135 results = {'topics': [],
136 136 'commands': [],
137 137 'extensions': [],
138 138 'extensioncommands': [],
139 139 }
140 140 for names, header, doc in helptable:
141 141 # Old extensions may use a str as doc.
142 142 if (sum(map(lowercontains, names))
143 143 or lowercontains(header)
144 144 or (callable(doc) and lowercontains(doc(ui)))):
145 145 results['topics'].append((names[0], header))
146 146 for cmd, entry in commands.table.iteritems():
147 147 if len(entry) == 3:
148 148 summary = entry[2]
149 149 else:
150 150 summary = ''
151 151 # translate docs *before* searching there
152 152 docs = _(pycompat.getdoc(entry[0])) or ''
153 153 if kw in cmd or lowercontains(summary) or lowercontains(docs):
154 154 doclines = docs.splitlines()
155 155 if doclines:
156 156 summary = doclines[0]
157 157 cmdname = cmdutil.parsealiases(cmd)[0]
158 158 if filtercmd(ui, cmdname, kw, docs):
159 159 continue
160 160 results['commands'].append((cmdname, summary))
161 161 for name, docs in itertools.chain(
162 162 extensions.enabled(False).iteritems(),
163 163 extensions.disabled().iteritems()):
164 164 if not docs:
165 165 continue
166 166 name = name.rpartition('.')[-1]
167 167 if lowercontains(name) or lowercontains(docs):
168 168 # extension docs are already translated
169 169 results['extensions'].append((name, docs.splitlines()[0]))
170 170 try:
171 171 mod = extensions.load(ui, name, '')
172 172 except ImportError:
173 173 # debug message would be printed in extensions.load()
174 174 continue
175 175 for cmd, entry in getattr(mod, 'cmdtable', {}).iteritems():
176 176 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
177 177 cmdname = cmdutil.parsealiases(cmd)[0]
178 178 cmddoc = pycompat.getdoc(entry[0])
179 179 if cmddoc:
180 180 cmddoc = gettext(cmddoc).splitlines()[0]
181 181 else:
182 182 cmddoc = _('(no help text available)')
183 183 if filtercmd(ui, cmdname, kw, cmddoc):
184 184 continue
185 185 results['extensioncommands'].append((cmdname, cmddoc))
186 186 return results
187 187
188 188 def loaddoc(topic, subdir=None):
189 189 """Return a delayed loader for help/topic.txt."""
190 190
191 191 def loader(ui):
192 192 docdir = os.path.join(util.datapath, 'help')
193 193 if subdir:
194 194 docdir = os.path.join(docdir, subdir)
195 195 path = os.path.join(docdir, topic + ".txt")
196 196 doc = gettext(util.readfile(path))
197 197 for rewriter in helphooks.get(topic, []):
198 198 doc = rewriter(ui, topic, doc)
199 199 return doc
200 200
201 201 return loader
202 202
203 203 internalstable = sorted([
204 204 (['bundle2'], _('Bundle2'),
205 205 loaddoc('bundle2', subdir='internals')),
206 206 (['bundles'], _('Bundles'),
207 207 loaddoc('bundles', subdir='internals')),
208 208 (['censor'], _('Censor'),
209 209 loaddoc('censor', subdir='internals')),
210 210 (['changegroups'], _('Changegroups'),
211 211 loaddoc('changegroups', subdir='internals')),
212 212 (['config'], _('Config Registrar'),
213 213 loaddoc('config', subdir='internals')),
214 214 (['requirements'], _('Repository Requirements'),
215 215 loaddoc('requirements', subdir='internals')),
216 216 (['revlogs'], _('Revision Logs'),
217 217 loaddoc('revlogs', subdir='internals')),
218 218 (['wireprotocol'], _('Wire Protocol'),
219 219 loaddoc('wireprotocol', subdir='internals')),
220 220 ])
221 221
222 222 def internalshelp(ui):
223 223 """Generate the index for the "internals" topic."""
224 224 lines = ['To access a subtopic, use "hg help internals.{subtopic-name}"\n',
225 225 '\n']
226 226 for names, header, doc in internalstable:
227 227 lines.append(' :%s: %s\n' % (names[0], header))
228 228
229 229 return ''.join(lines)
230 230
231 231 helptable = sorted([
232 232 (['bundlespec'], _("Bundle File Formats"), loaddoc('bundlespec')),
233 233 (['color'], _("Colorizing Outputs"), loaddoc('color')),
234 234 (["config", "hgrc"], _("Configuration Files"), loaddoc('config')),
235 235 (["dates"], _("Date Formats"), loaddoc('dates')),
236 236 (["flags"], _("Command-line flags"), loaddoc('flags')),
237 237 (["patterns"], _("File Name Patterns"), loaddoc('patterns')),
238 238 (['environment', 'env'], _('Environment Variables'),
239 239 loaddoc('environment')),
240 240 (['revisions', 'revs', 'revsets', 'revset', 'multirevs', 'mrevs'],
241 241 _('Specifying Revisions'), loaddoc('revisions')),
242 242 (['filesets', 'fileset'], _("Specifying File Sets"), loaddoc('filesets')),
243 243 (['diffs'], _('Diff Formats'), loaddoc('diffs')),
244 244 (['merge-tools', 'mergetools', 'mergetool'], _('Merge Tools'),
245 245 loaddoc('merge-tools')),
246 246 (['templating', 'templates', 'template', 'style'], _('Template Usage'),
247 247 loaddoc('templates')),
248 248 (['urls'], _('URL Paths'), loaddoc('urls')),
249 249 (["extensions"], _("Using Additional Features"), extshelp),
250 250 (["subrepos", "subrepo"], _("Subrepositories"), loaddoc('subrepos')),
251 251 (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')),
252 252 (["glossary"], _("Glossary"), loaddoc('glossary')),
253 253 (["hgignore", "ignore"], _("Syntax for Mercurial Ignore Files"),
254 254 loaddoc('hgignore')),
255 255 (["phases"], _("Working with Phases"), loaddoc('phases')),
256 256 (['scripting'], _('Using Mercurial from scripts and automation'),
257 257 loaddoc('scripting')),
258 258 (['internals'], _("Technical implementation topics"),
259 259 internalshelp),
260 260 (['pager'], _("Pager Support"), loaddoc('pager')),
261 261 ])
262 262
263 263 # Maps topics with sub-topics to a list of their sub-topics.
264 264 subtopics = {
265 265 'internals': internalstable,
266 266 }
267 267
268 268 # Map topics to lists of callable taking the current topic help and
269 269 # returning the updated version
270 270 helphooks = {}
271 271
272 272 def addtopichook(topic, rewriter):
273 273 helphooks.setdefault(topic, []).append(rewriter)
274 274
275 275 def makeitemsdoc(ui, topic, doc, marker, items, dedent=False):
276 276 """Extract docstring from the items key to function mapping, build a
277 277 single documentation block and use it to overwrite the marker in doc.
278 278 """
279 279 entries = []
280 280 for name in sorted(items):
281 281 text = (pycompat.getdoc(items[name]) or '').rstrip()
282 282 if (not text
283 283 or not ui.verbose and any(w in text for w in _exclkeywords)):
284 284 continue
285 285 text = gettext(text)
286 286 if dedent:
287 287 # Abuse latin1 to use textwrap.dedent() on bytes.
288 288 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
289 289 lines = text.splitlines()
290 290 doclines = [(lines[0])]
291 291 for l in lines[1:]:
292 292 # Stop once we find some Python doctest
293 293 if l.strip().startswith('>>>'):
294 294 break
295 295 if dedent:
296 296 doclines.append(l.rstrip())
297 297 else:
298 298 doclines.append(' ' + l.strip())
299 299 entries.append('\n'.join(doclines))
300 300 entries = '\n\n'.join(entries)
301 301 return doc.replace(marker, entries)
302 302
303 303 def addtopicsymbols(topic, marker, symbols, dedent=False):
304 304 def add(ui, topic, doc):
305 305 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
306 306 addtopichook(topic, add)
307 307
308 308 addtopicsymbols('bundlespec', '.. bundlecompressionmarker',
309 309 util.bundlecompressiontopics())
310 310 addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols)
311 311 addtopicsymbols('merge-tools', '.. internaltoolsmarker',
312 312 filemerge.internalsdoc)
313 313 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
314 314 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
315 315 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
316 316 addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
317 317 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
318 318 dedent=True)
319 319
320 320 def help_(ui, commands, name, unknowncmd=False, full=True, subtopic=None,
321 321 **opts):
322 322 '''
323 323 Generate the help for 'name' as unformatted restructured text. If
324 324 'name' is None, describe the commands available.
325 325 '''
326 326
327 327 opts = pycompat.byteskwargs(opts)
328 328
329 329 def helpcmd(name, subtopic=None):
330 330 try:
331 331 aliases, entry = cmdutil.findcmd(name, commands.table,
332 332 strict=unknowncmd)
333 333 except error.AmbiguousCommand as inst:
334 334 # py3k fix: except vars can't be used outside the scope of the
335 335 # except block, nor can be used inside a lambda. python issue4617
336 336 prefix = inst.args[0]
337 337 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
338 338 rst = helplist(select)
339 339 return rst
340 340
341 341 rst = []
342 342
343 343 # check if it's an invalid alias and display its error if it is
344 344 if getattr(entry[0], 'badalias', None):
345 345 rst.append(entry[0].badalias + '\n')
346 346 if entry[0].unknowncmd:
347 347 try:
348 348 rst.extend(helpextcmd(entry[0].cmdname))
349 349 except error.UnknownCommand:
350 350 pass
351 351 return rst
352 352
353 353 # synopsis
354 354 if len(entry) > 2:
355 355 if entry[2].startswith('hg'):
356 356 rst.append("%s\n" % entry[2])
357 357 else:
358 358 rst.append('hg %s %s\n' % (aliases[0], entry[2]))
359 359 else:
360 360 rst.append('hg %s\n' % aliases[0])
361 361 # aliases
362 362 if full and not ui.quiet and len(aliases) > 1:
363 363 rst.append(_("\naliases: %s\n") % ', '.join(aliases[1:]))
364 364 rst.append('\n')
365 365
366 366 # description
367 367 doc = gettext(pycompat.getdoc(entry[0]))
368 368 if not doc:
369 369 doc = _("(no help text available)")
370 370 if util.safehasattr(entry[0], 'definition'): # aliased command
371 371 source = entry[0].source
372 372 if entry[0].definition.startswith('!'): # shell alias
373 373 doc = (_('shell alias for: %s\n\n%s\n\ndefined by: %s\n') %
374 374 (entry[0].definition[1:], doc, source))
375 375 else:
376 376 doc = (_('alias for: hg %s\n\n%s\n\ndefined by: %s\n') %
377 377 (entry[0].definition, doc, source))
378 378 doc = doc.splitlines(True)
379 379 if ui.quiet or not full:
380 380 rst.append(doc[0])
381 381 else:
382 382 rst.extend(doc)
383 383 rst.append('\n')
384 384
385 385 # check if this command shadows a non-trivial (multi-line)
386 386 # extension help text
387 387 try:
388 388 mod = extensions.find(name)
389 389 doc = gettext(pycompat.getdoc(mod)) or ''
390 390 if '\n' in doc.strip():
391 391 msg = _("(use 'hg help -e %s' to show help for "
392 392 "the %s extension)") % (name, name)
393 393 rst.append('\n%s\n' % msg)
394 394 except KeyError:
395 395 pass
396 396
397 397 # options
398 398 if not ui.quiet and entry[1]:
399 399 rst.append(optrst(_("options"), entry[1], ui.verbose))
400 400
401 401 if ui.verbose:
402 402 rst.append(optrst(_("global options"),
403 403 commands.globalopts, ui.verbose))
404 404
405 405 if not ui.verbose:
406 406 if not full:
407 407 rst.append(_("\n(use 'hg %s -h' to show more help)\n")
408 408 % name)
409 409 elif not ui.quiet:
410 410 rst.append(_('\n(some details hidden, use --verbose '
411 411 'to show complete help)'))
412 412
413 413 return rst
414 414
415 415
416 416 def helplist(select=None, **opts):
417 417 # list of commands
418 418 if name == "shortlist":
419 419 header = _('basic commands:\n\n')
420 420 elif name == "debug":
421 421 header = _('debug commands (internal and unsupported):\n\n')
422 422 else:
423 423 header = _('list of commands:\n\n')
424 424
425 425 h = {}
426 426 cmds = {}
427 427 for c, e in commands.table.iteritems():
428 428 fs = cmdutil.parsealiases(c)
429 429 f = fs[0]
430 430 p = ''
431 431 if c.startswith("^"):
432 432 p = '^'
433 433 if select and not select(p + f):
434 434 continue
435 435 if (not select and name != 'shortlist' and
436 436 e[0].__module__ != commands.__name__):
437 437 continue
438 438 if name == "shortlist" and not p:
439 439 continue
440 440 doc = pycompat.getdoc(e[0])
441 441 if filtercmd(ui, f, name, doc):
442 442 continue
443 443 doc = gettext(doc)
444 444 if not doc:
445 445 doc = _("(no help text available)")
446 446 h[f] = doc.splitlines()[0].rstrip()
447 447 cmds[f] = '|'.join(fs)
448 448
449 449 rst = []
450 450 if not h:
451 451 if not ui.quiet:
452 452 rst.append(_('no commands defined\n'))
453 453 return rst
454 454
455 455 if not ui.quiet:
456 456 rst.append(header)
457 457 fns = sorted(h)
458 458 for f in fns:
459 459 if ui.verbose:
460 460 commacmds = cmds[f].replace("|",", ")
461 461 rst.append(" :%s: %s\n" % (commacmds, h[f]))
462 462 else:
463 463 rst.append(' :%s: %s\n' % (f, h[f]))
464 464
465 465 ex = opts.get
466 466 anyopts = (ex(r'keyword') or not (ex(r'command') or ex(r'extension')))
467 467 if not name and anyopts:
468 468 exts = listexts(_('enabled extensions:'), extensions.enabled())
469 469 if exts:
470 470 rst.append('\n')
471 471 rst.extend(exts)
472 472
473 473 rst.append(_("\nadditional help topics:\n\n"))
474 474 topics = []
475 475 for names, header, doc in helptable:
476 476 topics.append((names[0], header))
477 477 for t, desc in topics:
478 478 rst.append(" :%s: %s\n" % (t, desc))
479 479
480 480 if ui.quiet:
481 481 pass
482 482 elif ui.verbose:
483 483 rst.append('\n%s\n' % optrst(_("global options"),
484 484 commands.globalopts, ui.verbose))
485 485 if name == 'shortlist':
486 486 rst.append(_("\n(use 'hg help' for the full list "
487 487 "of commands)\n"))
488 488 else:
489 489 if name == 'shortlist':
490 490 rst.append(_("\n(use 'hg help' for the full list of commands "
491 491 "or 'hg -v' for details)\n"))
492 492 elif name and not full:
493 493 rst.append(_("\n(use 'hg help %s' to show the full help "
494 494 "text)\n") % name)
495 495 elif name and cmds and name in cmds.keys():
496 496 rst.append(_("\n(use 'hg help -v -e %s' to show built-in "
497 497 "aliases and global options)\n") % name)
498 498 else:
499 499 rst.append(_("\n(use 'hg help -v%s' to show built-in aliases "
500 500 "and global options)\n")
501 501 % (name and " " + name or ""))
502 502 return rst
503 503
504 504 def helptopic(name, subtopic=None):
505 505 # Look for sub-topic entry first.
506 506 header, doc = None, None
507 507 if subtopic and name in subtopics:
508 508 for names, header, doc in subtopics[name]:
509 509 if subtopic in names:
510 510 break
511 511
512 512 if not header:
513 513 for names, header, doc in helptable:
514 514 if name in names:
515 515 break
516 516 else:
517 517 raise error.UnknownCommand(name)
518 518
519 519 rst = [minirst.section(header)]
520 520
521 521 # description
522 522 if not doc:
523 523 rst.append(" %s\n" % _("(no help text available)"))
524 524 if callable(doc):
525 525 rst += [" %s\n" % l for l in doc(ui).splitlines()]
526 526
527 527 if not ui.verbose:
528 528 omitted = _('(some details hidden, use --verbose'
529 529 ' to show complete help)')
530 530 indicateomitted(rst, omitted)
531 531
532 532 try:
533 533 cmdutil.findcmd(name, commands.table)
534 534 rst.append(_("\nuse 'hg help -c %s' to see help for "
535 535 "the %s command\n") % (name, name))
536 536 except error.UnknownCommand:
537 537 pass
538 538 return rst
539 539
540 540 def helpext(name, subtopic=None):
541 541 try:
542 542 mod = extensions.find(name)
543 543 doc = gettext(pycompat.getdoc(mod)) or _('no help text available')
544 544 except KeyError:
545 545 mod = None
546 546 doc = extensions.disabledext(name)
547 547 if not doc:
548 548 raise error.UnknownCommand(name)
549 549
550 550 if '\n' not in doc:
551 551 head, tail = doc, ""
552 552 else:
553 553 head, tail = doc.split('\n', 1)
554 554 rst = [_('%s extension - %s\n\n') % (name.rpartition('.')[-1], head)]
555 555 if tail:
556 556 rst.extend(tail.splitlines(True))
557 557 rst.append('\n')
558 558
559 559 if not ui.verbose:
560 560 omitted = _('(some details hidden, use --verbose'
561 561 ' to show complete help)')
562 562 indicateomitted(rst, omitted)
563 563
564 564 if mod:
565 565 try:
566 566 ct = mod.cmdtable
567 567 except AttributeError:
568 568 ct = {}
569 569 modcmds = set([c.partition('|')[0] for c in ct])
570 570 rst.extend(helplist(modcmds.__contains__))
571 571 else:
572 572 rst.append(_("(use 'hg help extensions' for information on enabling"
573 573 " extensions)\n"))
574 574 return rst
575 575
576 576 def helpextcmd(name, subtopic=None):
577 cmd, ext, mod = extensions.disabledcmd(ui, name,
577 cmd, ext, doc = extensions.disabledcmd(ui, name,
578 578 ui.configbool('ui', 'strict'))
579 doc = gettext(pycompat.getdoc(mod)).splitlines()[0]
579 doc = doc.splitlines()[0]
580 580
581 581 rst = listexts(_("'%s' is provided by the following "
582 582 "extension:") % cmd, {ext: doc}, indent=4,
583 583 showdeprecated=True)
584 584 rst.append('\n')
585 585 rst.append(_("(use 'hg help extensions' for information on enabling "
586 586 "extensions)\n"))
587 587 return rst
588 588
589 589
590 590 rst = []
591 591 kw = opts.get('keyword')
592 592 if kw or name is None and any(opts[o] for o in opts):
593 593 matches = topicmatch(ui, commands, name or '')
594 594 helpareas = []
595 595 if opts.get('extension'):
596 596 helpareas += [('extensions', _('Extensions'))]
597 597 if opts.get('command'):
598 598 helpareas += [('commands', _('Commands'))]
599 599 if not helpareas:
600 600 helpareas = [('topics', _('Topics')),
601 601 ('commands', _('Commands')),
602 602 ('extensions', _('Extensions')),
603 603 ('extensioncommands', _('Extension Commands'))]
604 604 for t, title in helpareas:
605 605 if matches[t]:
606 606 rst.append('%s:\n\n' % title)
607 607 rst.extend(minirst.maketable(sorted(matches[t]), 1))
608 608 rst.append('\n')
609 609 if not rst:
610 610 msg = _('no matches')
611 611 hint = _("try 'hg help' for a list of topics")
612 612 raise error.Abort(msg, hint=hint)
613 613 elif name and name != 'shortlist':
614 614 queries = []
615 615 if unknowncmd:
616 616 queries += [helpextcmd]
617 617 if opts.get('extension'):
618 618 queries += [helpext]
619 619 if opts.get('command'):
620 620 queries += [helpcmd]
621 621 if not queries:
622 622 queries = (helptopic, helpcmd, helpext, helpextcmd)
623 623 for f in queries:
624 624 try:
625 625 rst = f(name, subtopic)
626 626 break
627 627 except error.UnknownCommand:
628 628 pass
629 629 else:
630 630 if unknowncmd:
631 631 raise error.UnknownCommand(name)
632 632 else:
633 633 msg = _('no such help topic: %s') % name
634 634 hint = _("try 'hg help --keyword %s'") % name
635 635 raise error.Abort(msg, hint=hint)
636 636 else:
637 637 # program name
638 638 if not ui.quiet:
639 639 rst = [_("Mercurial Distributed SCM\n"), '\n']
640 640 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
641 641
642 642 return ''.join(rst)
643 643
644 644 def formattedhelp(ui, commands, name, keep=None, unknowncmd=False, full=True,
645 645 **opts):
646 646 """get help for a given topic (as a dotted name) as rendered rst
647 647
648 648 Either returns the rendered help text or raises an exception.
649 649 """
650 650 if keep is None:
651 651 keep = []
652 652 else:
653 653 keep = list(keep) # make a copy so we can mutate this later
654 654 fullname = name
655 655 section = None
656 656 subtopic = None
657 657 if name and '.' in name:
658 658 name, remaining = name.split('.', 1)
659 659 remaining = encoding.lower(remaining)
660 660 if '.' in remaining:
661 661 subtopic, section = remaining.split('.', 1)
662 662 else:
663 663 if name in subtopics:
664 664 subtopic = remaining
665 665 else:
666 666 section = remaining
667 667 textwidth = ui.configint('ui', 'textwidth')
668 668 termwidth = ui.termwidth() - 2
669 669 if textwidth <= 0 or termwidth < textwidth:
670 670 textwidth = termwidth
671 671 text = help_(ui, commands, name,
672 672 subtopic=subtopic, unknowncmd=unknowncmd, full=full, **opts)
673 673
674 674 formatted, pruned = minirst.format(text, textwidth, keep=keep,
675 675 section=section)
676 676
677 677 # We could have been given a weird ".foo" section without a name
678 678 # to look for, or we could have simply failed to found "foo.bar"
679 679 # because bar isn't a section of foo
680 680 if section and not (formatted and name):
681 681 raise error.Abort(_("help section not found: %s") % fullname)
682 682
683 683 if 'verbose' in pruned:
684 684 keep.append('omitted')
685 685 else:
686 686 keep.append('notomitted')
687 687 formatted, pruned = minirst.format(text, textwidth, keep=keep,
688 688 section=section)
689 689 return formatted
General Comments 0
You need to be logged in to leave comments. Login now