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