##// END OF EJS Templates
extension: add a summary of total loading time per extension...
Boris Feld -
r39547:1ab185c7 default
parent child Browse files
Show More
@@ -1,812 +1,823 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 ast
10 import ast
11 import collections
11 import collections
12 import functools
12 import functools
13 import imp
13 import imp
14 import inspect
14 import inspect
15 import os
15 import os
16
16
17 from .i18n import (
17 from .i18n import (
18 _,
18 _,
19 gettext,
19 gettext,
20 )
20 )
21
21
22 from . import (
22 from . import (
23 cmdutil,
23 cmdutil,
24 configitems,
24 configitems,
25 error,
25 error,
26 pycompat,
26 pycompat,
27 util,
27 util,
28 )
28 )
29
29
30 from .utils import (
30 from .utils import (
31 stringutil,
31 stringutil,
32 )
32 )
33
33
34 _extensions = {}
34 _extensions = {}
35 _disabledextensions = {}
35 _disabledextensions = {}
36 _aftercallbacks = {}
36 _aftercallbacks = {}
37 _order = []
37 _order = []
38 _builtin = {
38 _builtin = {
39 'hbisect',
39 'hbisect',
40 'bookmarks',
40 'bookmarks',
41 'color',
41 'color',
42 'parentrevspec',
42 'parentrevspec',
43 'progress',
43 'progress',
44 'interhg',
44 'interhg',
45 'inotify',
45 'inotify',
46 'hgcia'
46 'hgcia'
47 }
47 }
48
48
49 def extensions(ui=None):
49 def extensions(ui=None):
50 if ui:
50 if ui:
51 def enabled(name):
51 def enabled(name):
52 for format in ['%s', 'hgext.%s']:
52 for format in ['%s', 'hgext.%s']:
53 conf = ui.config('extensions', format % name)
53 conf = ui.config('extensions', format % name)
54 if conf is not None and not conf.startswith('!'):
54 if conf is not None and not conf.startswith('!'):
55 return True
55 return True
56 else:
56 else:
57 enabled = lambda name: True
57 enabled = lambda name: True
58 for name in _order:
58 for name in _order:
59 module = _extensions[name]
59 module = _extensions[name]
60 if module and enabled(name):
60 if module and enabled(name):
61 yield name, module
61 yield name, module
62
62
63 def find(name):
63 def find(name):
64 '''return module with given extension name'''
64 '''return module with given extension name'''
65 mod = None
65 mod = None
66 try:
66 try:
67 mod = _extensions[name]
67 mod = _extensions[name]
68 except KeyError:
68 except KeyError:
69 for k, v in _extensions.iteritems():
69 for k, v in _extensions.iteritems():
70 if k.endswith('.' + name) or k.endswith('/' + name):
70 if k.endswith('.' + name) or k.endswith('/' + name):
71 mod = v
71 mod = v
72 break
72 break
73 if not mod:
73 if not mod:
74 raise KeyError(name)
74 raise KeyError(name)
75 return mod
75 return mod
76
76
77 def loadpath(path, module_name):
77 def loadpath(path, module_name):
78 module_name = module_name.replace('.', '_')
78 module_name = module_name.replace('.', '_')
79 path = util.normpath(util.expandpath(path))
79 path = util.normpath(util.expandpath(path))
80 module_name = pycompat.fsdecode(module_name)
80 module_name = pycompat.fsdecode(module_name)
81 path = pycompat.fsdecode(path)
81 path = pycompat.fsdecode(path)
82 if os.path.isdir(path):
82 if os.path.isdir(path):
83 # module/__init__.py style
83 # module/__init__.py style
84 d, f = os.path.split(path)
84 d, f = os.path.split(path)
85 fd, fpath, desc = imp.find_module(f, [d])
85 fd, fpath, desc = imp.find_module(f, [d])
86 return imp.load_module(module_name, fd, fpath, desc)
86 return imp.load_module(module_name, fd, fpath, desc)
87 else:
87 else:
88 try:
88 try:
89 return imp.load_source(module_name, path)
89 return imp.load_source(module_name, path)
90 except IOError as exc:
90 except IOError as exc:
91 if not exc.filename:
91 if not exc.filename:
92 exc.filename = path # python does not fill this
92 exc.filename = path # python does not fill this
93 raise
93 raise
94
94
95 def _importh(name):
95 def _importh(name):
96 """import and return the <name> module"""
96 """import and return the <name> module"""
97 mod = __import__(pycompat.sysstr(name))
97 mod = __import__(pycompat.sysstr(name))
98 components = name.split('.')
98 components = name.split('.')
99 for comp in components[1:]:
99 for comp in components[1:]:
100 mod = getattr(mod, comp)
100 mod = getattr(mod, comp)
101 return mod
101 return mod
102
102
103 def _importext(name, path=None, reportfunc=None):
103 def _importext(name, path=None, reportfunc=None):
104 if path:
104 if path:
105 # the module will be loaded in sys.modules
105 # the module will be loaded in sys.modules
106 # choose an unique name so that it doesn't
106 # choose an unique name so that it doesn't
107 # conflicts with other modules
107 # conflicts with other modules
108 mod = loadpath(path, 'hgext.%s' % name)
108 mod = loadpath(path, 'hgext.%s' % name)
109 else:
109 else:
110 try:
110 try:
111 mod = _importh("hgext.%s" % name)
111 mod = _importh("hgext.%s" % name)
112 except ImportError as err:
112 except ImportError as err:
113 if reportfunc:
113 if reportfunc:
114 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
114 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
115 try:
115 try:
116 mod = _importh("hgext3rd.%s" % name)
116 mod = _importh("hgext3rd.%s" % name)
117 except ImportError as err:
117 except ImportError as err:
118 if reportfunc:
118 if reportfunc:
119 reportfunc(err, "hgext3rd.%s" % name, name)
119 reportfunc(err, "hgext3rd.%s" % name, name)
120 mod = _importh(name)
120 mod = _importh(name)
121 return mod
121 return mod
122
122
123 def _reportimporterror(ui, err, failed, next):
123 def _reportimporterror(ui, err, failed, next):
124 # note: this ui.debug happens before --debug is processed,
124 # note: this ui.debug happens before --debug is processed,
125 # Use --config ui.debug=1 to see them.
125 # Use --config ui.debug=1 to see them.
126 if ui.configbool('devel', 'debug.extensions'):
126 if ui.configbool('devel', 'debug.extensions'):
127 ui.debug('debug.extensions: - could not import %s (%s): trying %s\n'
127 ui.debug('debug.extensions: - could not import %s (%s): trying %s\n'
128 % (failed, stringutil.forcebytestr(err), next))
128 % (failed, stringutil.forcebytestr(err), next))
129 if ui.debugflag:
129 if ui.debugflag:
130 ui.traceback()
130 ui.traceback()
131
131
132 def _rejectunicode(name, xs):
132 def _rejectunicode(name, xs):
133 if isinstance(xs, (list, set, tuple)):
133 if isinstance(xs, (list, set, tuple)):
134 for x in xs:
134 for x in xs:
135 _rejectunicode(name, x)
135 _rejectunicode(name, x)
136 elif isinstance(xs, dict):
136 elif isinstance(xs, dict):
137 for k, v in xs.items():
137 for k, v in xs.items():
138 _rejectunicode(name, k)
138 _rejectunicode(name, k)
139 _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
139 _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
140 elif isinstance(xs, type(u'')):
140 elif isinstance(xs, type(u'')):
141 raise error.ProgrammingError(b"unicode %r found in %s" % (xs, name),
141 raise error.ProgrammingError(b"unicode %r found in %s" % (xs, name),
142 hint="use b'' to make it byte string")
142 hint="use b'' to make it byte string")
143
143
144 # attributes set by registrar.command
144 # attributes set by registrar.command
145 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
145 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
146
146
147 def _validatecmdtable(ui, cmdtable):
147 def _validatecmdtable(ui, cmdtable):
148 """Check if extension commands have required attributes"""
148 """Check if extension commands have required attributes"""
149 for c, e in cmdtable.iteritems():
149 for c, e in cmdtable.iteritems():
150 f = e[0]
150 f = e[0]
151 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
151 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
152 if not missing:
152 if not missing:
153 continue
153 continue
154 raise error.ProgrammingError(
154 raise error.ProgrammingError(
155 'missing attributes: %s' % ', '.join(missing),
155 'missing attributes: %s' % ', '.join(missing),
156 hint="use @command decorator to register '%s'" % c)
156 hint="use @command decorator to register '%s'" % c)
157
157
158 def _validatetables(ui, mod):
158 def _validatetables(ui, mod):
159 """Sanity check for loadable tables provided by extension module"""
159 """Sanity check for loadable tables provided by extension module"""
160 for t in ['cmdtable', 'colortable', 'configtable']:
160 for t in ['cmdtable', 'colortable', 'configtable']:
161 _rejectunicode(t, getattr(mod, t, {}))
161 _rejectunicode(t, getattr(mod, t, {}))
162 for t in ['filesetpredicate', 'internalmerge', 'revsetpredicate',
162 for t in ['filesetpredicate', 'internalmerge', 'revsetpredicate',
163 'templatefilter', 'templatefunc', 'templatekeyword']:
163 'templatefilter', 'templatefunc', 'templatekeyword']:
164 o = getattr(mod, t, None)
164 o = getattr(mod, t, None)
165 if o:
165 if o:
166 _rejectunicode(t, o._table)
166 _rejectunicode(t, o._table)
167 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
167 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
168
168
169 def load(ui, name, path, log=lambda *a: None):
169 def load(ui, name, path, log=lambda *a: None, loadingtime=None):
170 if name.startswith('hgext.') or name.startswith('hgext/'):
170 if name.startswith('hgext.') or name.startswith('hgext/'):
171 shortname = name[6:]
171 shortname = name[6:]
172 else:
172 else:
173 shortname = name
173 shortname = name
174 if shortname in _builtin:
174 if shortname in _builtin:
175 return None
175 return None
176 if shortname in _extensions:
176 if shortname in _extensions:
177 return _extensions[shortname]
177 return _extensions[shortname]
178 log(' - loading extension: %r\n', shortname)
178 log(' - loading extension: %r\n', shortname)
179 _extensions[shortname] = None
179 _extensions[shortname] = None
180 with util.timedcm('load extension %r', shortname) as stats:
180 with util.timedcm('load extension %r', shortname) as stats:
181 mod = _importext(name, path, bind(_reportimporterror, ui))
181 mod = _importext(name, path, bind(_reportimporterror, ui))
182 log(' > %r extension loaded in %s\n', shortname, stats)
182 log(' > %r extension loaded in %s\n', shortname, stats)
183 if loadingtime is not None:
184 loadingtime[shortname] += stats.elapsed
183
185
184 # Before we do anything with the extension, check against minimum stated
186 # Before we do anything with the extension, check against minimum stated
185 # compatibility. This gives extension authors a mechanism to have their
187 # compatibility. This gives extension authors a mechanism to have their
186 # extensions short circuit when loaded with a known incompatible version
188 # extensions short circuit when loaded with a known incompatible version
187 # of Mercurial.
189 # of Mercurial.
188 minver = getattr(mod, 'minimumhgversion', None)
190 minver = getattr(mod, 'minimumhgversion', None)
189 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
191 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
190 ui.warn(_('(third party extension %s requires version %s or newer '
192 ui.warn(_('(third party extension %s requires version %s or newer '
191 'of Mercurial; disabling)\n') % (shortname, minver))
193 'of Mercurial; disabling)\n') % (shortname, minver))
192 return
194 return
193 log(' - validating extension tables: %r\n', shortname)
195 log(' - validating extension tables: %r\n', shortname)
194 _validatetables(ui, mod)
196 _validatetables(ui, mod)
195
197
196 _extensions[shortname] = mod
198 _extensions[shortname] = mod
197 _order.append(shortname)
199 _order.append(shortname)
198 log(' - invoking registered callbacks: %r\n', shortname)
200 log(' - invoking registered callbacks: %r\n', shortname)
199 with util.timedcm('callbacks extension %r', shortname) as stats:
201 with util.timedcm('callbacks extension %r', shortname) as stats:
200 for fn in _aftercallbacks.get(shortname, []):
202 for fn in _aftercallbacks.get(shortname, []):
201 fn(loaded=True)
203 fn(loaded=True)
202 log(' > callbacks completed in %s\n', stats)
204 log(' > callbacks completed in %s\n', stats)
203 return mod
205 return mod
204
206
205 def _runuisetup(name, ui):
207 def _runuisetup(name, ui):
206 uisetup = getattr(_extensions[name], 'uisetup', None)
208 uisetup = getattr(_extensions[name], 'uisetup', None)
207 if uisetup:
209 if uisetup:
208 try:
210 try:
209 uisetup(ui)
211 uisetup(ui)
210 except Exception as inst:
212 except Exception as inst:
211 ui.traceback(force=True)
213 ui.traceback(force=True)
212 msg = stringutil.forcebytestr(inst)
214 msg = stringutil.forcebytestr(inst)
213 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
215 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
214 return False
216 return False
215 return True
217 return True
216
218
217 def _runextsetup(name, ui):
219 def _runextsetup(name, ui):
218 extsetup = getattr(_extensions[name], 'extsetup', None)
220 extsetup = getattr(_extensions[name], 'extsetup', None)
219 if extsetup:
221 if extsetup:
220 try:
222 try:
221 try:
223 try:
222 extsetup(ui)
224 extsetup(ui)
223 except TypeError:
225 except TypeError:
224 if pycompat.getargspec(extsetup).args:
226 if pycompat.getargspec(extsetup).args:
225 raise
227 raise
226 extsetup() # old extsetup with no ui argument
228 extsetup() # old extsetup with no ui argument
227 except Exception as inst:
229 except Exception as inst:
228 ui.traceback(force=True)
230 ui.traceback(force=True)
229 msg = stringutil.forcebytestr(inst)
231 msg = stringutil.forcebytestr(inst)
230 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
232 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
231 return False
233 return False
232 return True
234 return True
233
235
234 def loadall(ui, whitelist=None):
236 def loadall(ui, whitelist=None):
235 if ui.configbool('devel', 'debug.extensions'):
237 if ui.configbool('devel', 'debug.extensions'):
236 log = lambda msg, *values: ui.debug('debug.extensions: ',
238 log = lambda msg, *values: ui.debug('debug.extensions: ',
237 msg % values, label='debug.extensions')
239 msg % values, label='debug.extensions')
238 else:
240 else:
239 log = lambda *a, **kw: None
241 log = lambda *a, **kw: None
242 loadingtime = collections.defaultdict(int)
240 result = ui.configitems("extensions")
243 result = ui.configitems("extensions")
241 if whitelist is not None:
244 if whitelist is not None:
242 result = [(k, v) for (k, v) in result if k in whitelist]
245 result = [(k, v) for (k, v) in result if k in whitelist]
243 newindex = len(_order)
246 newindex = len(_order)
244 log('loading %sextensions\n', 'additional ' if newindex else '')
247 log('loading %sextensions\n', 'additional ' if newindex else '')
245 log('- processing %d entries\n', len(result))
248 log('- processing %d entries\n', len(result))
246 with util.timedcm('load all extensions') as stats:
249 with util.timedcm('load all extensions') as stats:
247 for (name, path) in result:
250 for (name, path) in result:
248 if path:
251 if path:
249 if path[0:1] == '!':
252 if path[0:1] == '!':
250 if name not in _disabledextensions:
253 if name not in _disabledextensions:
251 log(' - skipping disabled extension: %r\n', name)
254 log(' - skipping disabled extension: %r\n', name)
252 _disabledextensions[name] = path[1:]
255 _disabledextensions[name] = path[1:]
253 continue
256 continue
254 try:
257 try:
255 load(ui, name, path, log)
258 load(ui, name, path, log, loadingtime)
256 except Exception as inst:
259 except Exception as inst:
257 msg = stringutil.forcebytestr(inst)
260 msg = stringutil.forcebytestr(inst)
258 if path:
261 if path:
259 ui.warn(_("*** failed to import extension %s from %s: %s\n")
262 ui.warn(_("*** failed to import extension %s from %s: %s\n")
260 % (name, path, msg))
263 % (name, path, msg))
261 else:
264 else:
262 ui.warn(_("*** failed to import extension %s: %s\n")
265 ui.warn(_("*** failed to import extension %s: %s\n")
263 % (name, msg))
266 % (name, msg))
264 if isinstance(inst, error.Hint) and inst.hint:
267 if isinstance(inst, error.Hint) and inst.hint:
265 ui.warn(_("*** (%s)\n") % inst.hint)
268 ui.warn(_("*** (%s)\n") % inst.hint)
266 ui.traceback()
269 ui.traceback()
267
270
268 log('> loaded %d extensions, total time %s\n',
271 log('> loaded %d extensions, total time %s\n',
269 len(_order) - newindex, stats)
272 len(_order) - newindex, stats)
270 # list of (objname, loadermod, loadername) tuple:
273 # list of (objname, loadermod, loadername) tuple:
271 # - objname is the name of an object in extension module,
274 # - objname is the name of an object in extension module,
272 # from which extra information is loaded
275 # from which extra information is loaded
273 # - loadermod is the module where loader is placed
276 # - loadermod is the module where loader is placed
274 # - loadername is the name of the function,
277 # - loadername is the name of the function,
275 # which takes (ui, extensionname, extraobj) arguments
278 # which takes (ui, extensionname, extraobj) arguments
276 #
279 #
277 # This one is for the list of item that must be run before running any setup
280 # This one is for the list of item that must be run before running any setup
278 earlyextraloaders = [
281 earlyextraloaders = [
279 ('configtable', configitems, 'loadconfigtable'),
282 ('configtable', configitems, 'loadconfigtable'),
280 ]
283 ]
281
284
282 log('- loading configtable attributes\n')
285 log('- loading configtable attributes\n')
283 _loadextra(ui, newindex, earlyextraloaders)
286 _loadextra(ui, newindex, earlyextraloaders)
284
287
285 broken = set()
288 broken = set()
286 log('- executing uisetup hooks\n')
289 log('- executing uisetup hooks\n')
287 with util.timedcm('all uisetup') as alluisetupstats:
290 with util.timedcm('all uisetup') as alluisetupstats:
288 for name in _order[newindex:]:
291 for name in _order[newindex:]:
289 log(' - running uisetup for %r\n', name)
292 log(' - running uisetup for %r\n', name)
290 with util.timedcm('uisetup %r', name) as stats:
293 with util.timedcm('uisetup %r', name) as stats:
291 if not _runuisetup(name, ui):
294 if not _runuisetup(name, ui):
292 log(' - the %r extension uisetup failed\n', name)
295 log(' - the %r extension uisetup failed\n', name)
293 broken.add(name)
296 broken.add(name)
294 log(' > uisetup for %r took %s\n', name, stats)
297 log(' > uisetup for %r took %s\n', name, stats)
298 loadingtime[name] += stats.elapsed
295 log('> all uisetup took %s\n', alluisetupstats)
299 log('> all uisetup took %s\n', alluisetupstats)
296
300
297 log('- executing extsetup hooks\n')
301 log('- executing extsetup hooks\n')
298 with util.timedcm('all extsetup') as allextetupstats:
302 with util.timedcm('all extsetup') as allextetupstats:
299 for name in _order[newindex:]:
303 for name in _order[newindex:]:
300 if name in broken:
304 if name in broken:
301 continue
305 continue
302 log(' - running extsetup for %r\n', name)
306 log(' - running extsetup for %r\n', name)
303 with util.timedcm('extsetup %r', name) as stats:
307 with util.timedcm('extsetup %r', name) as stats:
304 if not _runextsetup(name, ui):
308 if not _runextsetup(name, ui):
305 log(' - the %r extension extsetup failed\n', name)
309 log(' - the %r extension extsetup failed\n', name)
306 broken.add(name)
310 broken.add(name)
307 log(' > extsetup for %r took %s\n', name, stats)
311 log(' > extsetup for %r took %s\n', name, stats)
312 loadingtime[name] += stats.elapsed
308 log('> all extsetup took %s\n', allextetupstats)
313 log('> all extsetup took %s\n', allextetupstats)
309
314
310 for name in broken:
315 for name in broken:
311 log(' - disabling broken %r extension\n', name)
316 log(' - disabling broken %r extension\n', name)
312 _extensions[name] = None
317 _extensions[name] = None
313
318
314 # Call aftercallbacks that were never met.
319 # Call aftercallbacks that were never met.
315 log('- executing remaining aftercallbacks\n')
320 log('- executing remaining aftercallbacks\n')
316 with util.timedcm('aftercallbacks') as stats:
321 with util.timedcm('aftercallbacks') as stats:
317 for shortname in _aftercallbacks:
322 for shortname in _aftercallbacks:
318 if shortname in _extensions:
323 if shortname in _extensions:
319 continue
324 continue
320
325
321 for fn in _aftercallbacks[shortname]:
326 for fn in _aftercallbacks[shortname]:
322 log(' - extension %r not loaded, notify callbacks\n',
327 log(' - extension %r not loaded, notify callbacks\n',
323 shortname)
328 shortname)
324 fn(loaded=False)
329 fn(loaded=False)
325 log('> remaining aftercallbacks completed in %s\n', stats)
330 log('> remaining aftercallbacks completed in %s\n', stats)
326
331
327 # loadall() is called multiple times and lingering _aftercallbacks
332 # loadall() is called multiple times and lingering _aftercallbacks
328 # entries could result in double execution. See issue4646.
333 # entries could result in double execution. See issue4646.
329 _aftercallbacks.clear()
334 _aftercallbacks.clear()
330
335
331 # delay importing avoids cyclic dependency (especially commands)
336 # delay importing avoids cyclic dependency (especially commands)
332 from . import (
337 from . import (
333 color,
338 color,
334 commands,
339 commands,
335 filemerge,
340 filemerge,
336 fileset,
341 fileset,
337 revset,
342 revset,
338 templatefilters,
343 templatefilters,
339 templatefuncs,
344 templatefuncs,
340 templatekw,
345 templatekw,
341 )
346 )
342
347
343 # list of (objname, loadermod, loadername) tuple:
348 # list of (objname, loadermod, loadername) tuple:
344 # - objname is the name of an object in extension module,
349 # - objname is the name of an object in extension module,
345 # from which extra information is loaded
350 # from which extra information is loaded
346 # - loadermod is the module where loader is placed
351 # - loadermod is the module where loader is placed
347 # - loadername is the name of the function,
352 # - loadername is the name of the function,
348 # which takes (ui, extensionname, extraobj) arguments
353 # which takes (ui, extensionname, extraobj) arguments
349 log('- loading extension registration objects\n')
354 log('- loading extension registration objects\n')
350 extraloaders = [
355 extraloaders = [
351 ('cmdtable', commands, 'loadcmdtable'),
356 ('cmdtable', commands, 'loadcmdtable'),
352 ('colortable', color, 'loadcolortable'),
357 ('colortable', color, 'loadcolortable'),
353 ('filesetpredicate', fileset, 'loadpredicate'),
358 ('filesetpredicate', fileset, 'loadpredicate'),
354 ('internalmerge', filemerge, 'loadinternalmerge'),
359 ('internalmerge', filemerge, 'loadinternalmerge'),
355 ('revsetpredicate', revset, 'loadpredicate'),
360 ('revsetpredicate', revset, 'loadpredicate'),
356 ('templatefilter', templatefilters, 'loadfilter'),
361 ('templatefilter', templatefilters, 'loadfilter'),
357 ('templatefunc', templatefuncs, 'loadfunction'),
362 ('templatefunc', templatefuncs, 'loadfunction'),
358 ('templatekeyword', templatekw, 'loadkeyword'),
363 ('templatekeyword', templatekw, 'loadkeyword'),
359 ]
364 ]
360 with util.timedcm('load registration objects') as stats:
365 with util.timedcm('load registration objects') as stats:
361 _loadextra(ui, newindex, extraloaders)
366 _loadextra(ui, newindex, extraloaders)
362 log('> extension registration object loading took %s\n', stats)
367 log('> extension registration object loading took %s\n', stats)
368
369 # Report per extension loading time (except reposetup)
370 for name in sorted(loadingtime):
371 extension_msg = '> extension %s take a total of %s to load\n'
372 log(extension_msg, name, util.timecount(loadingtime[name]))
373
363 log('extension loading complete\n')
374 log('extension loading complete\n')
364
375
365 def _loadextra(ui, newindex, extraloaders):
376 def _loadextra(ui, newindex, extraloaders):
366 for name in _order[newindex:]:
377 for name in _order[newindex:]:
367 module = _extensions[name]
378 module = _extensions[name]
368 if not module:
379 if not module:
369 continue # loading this module failed
380 continue # loading this module failed
370
381
371 for objname, loadermod, loadername in extraloaders:
382 for objname, loadermod, loadername in extraloaders:
372 extraobj = getattr(module, objname, None)
383 extraobj = getattr(module, objname, None)
373 if extraobj is not None:
384 if extraobj is not None:
374 getattr(loadermod, loadername)(ui, name, extraobj)
385 getattr(loadermod, loadername)(ui, name, extraobj)
375
386
376 def afterloaded(extension, callback):
387 def afterloaded(extension, callback):
377 '''Run the specified function after a named extension is loaded.
388 '''Run the specified function after a named extension is loaded.
378
389
379 If the named extension is already loaded, the callback will be called
390 If the named extension is already loaded, the callback will be called
380 immediately.
391 immediately.
381
392
382 If the named extension never loads, the callback will be called after
393 If the named extension never loads, the callback will be called after
383 all extensions have been loaded.
394 all extensions have been loaded.
384
395
385 The callback receives the named argument ``loaded``, which is a boolean
396 The callback receives the named argument ``loaded``, which is a boolean
386 indicating whether the dependent extension actually loaded.
397 indicating whether the dependent extension actually loaded.
387 '''
398 '''
388
399
389 if extension in _extensions:
400 if extension in _extensions:
390 # Report loaded as False if the extension is disabled
401 # Report loaded as False if the extension is disabled
391 loaded = (_extensions[extension] is not None)
402 loaded = (_extensions[extension] is not None)
392 callback(loaded=loaded)
403 callback(loaded=loaded)
393 else:
404 else:
394 _aftercallbacks.setdefault(extension, []).append(callback)
405 _aftercallbacks.setdefault(extension, []).append(callback)
395
406
396 def bind(func, *args):
407 def bind(func, *args):
397 '''Partial function application
408 '''Partial function application
398
409
399 Returns a new function that is the partial application of args and kwargs
410 Returns a new function that is the partial application of args and kwargs
400 to func. For example,
411 to func. For example,
401
412
402 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
413 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
403 assert callable(func)
414 assert callable(func)
404 def closure(*a, **kw):
415 def closure(*a, **kw):
405 return func(*(args + a), **kw)
416 return func(*(args + a), **kw)
406 return closure
417 return closure
407
418
408 def _updatewrapper(wrap, origfn, unboundwrapper):
419 def _updatewrapper(wrap, origfn, unboundwrapper):
409 '''Copy and add some useful attributes to wrapper'''
420 '''Copy and add some useful attributes to wrapper'''
410 try:
421 try:
411 wrap.__name__ = origfn.__name__
422 wrap.__name__ = origfn.__name__
412 except AttributeError:
423 except AttributeError:
413 pass
424 pass
414 wrap.__module__ = getattr(origfn, '__module__')
425 wrap.__module__ = getattr(origfn, '__module__')
415 wrap.__doc__ = getattr(origfn, '__doc__')
426 wrap.__doc__ = getattr(origfn, '__doc__')
416 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
427 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
417 wrap._origfunc = origfn
428 wrap._origfunc = origfn
418 wrap._unboundwrapper = unboundwrapper
429 wrap._unboundwrapper = unboundwrapper
419
430
420 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
431 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
421 '''Wrap the command named `command' in table
432 '''Wrap the command named `command' in table
422
433
423 Replace command in the command table with wrapper. The wrapped command will
434 Replace command in the command table with wrapper. The wrapped command will
424 be inserted into the command table specified by the table argument.
435 be inserted into the command table specified by the table argument.
425
436
426 The wrapper will be called like
437 The wrapper will be called like
427
438
428 wrapper(orig, *args, **kwargs)
439 wrapper(orig, *args, **kwargs)
429
440
430 where orig is the original (wrapped) function, and *args, **kwargs
441 where orig is the original (wrapped) function, and *args, **kwargs
431 are the arguments passed to it.
442 are the arguments passed to it.
432
443
433 Optionally append to the command synopsis and docstring, used for help.
444 Optionally append to the command synopsis and docstring, used for help.
434 For example, if your extension wraps the ``bookmarks`` command to add the
445 For example, if your extension wraps the ``bookmarks`` command to add the
435 flags ``--remote`` and ``--all`` you might call this function like so:
446 flags ``--remote`` and ``--all`` you might call this function like so:
436
447
437 synopsis = ' [-a] [--remote]'
448 synopsis = ' [-a] [--remote]'
438 docstring = """
449 docstring = """
439
450
440 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
451 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
441 flags to the bookmarks command. Either flag will show the remote bookmarks
452 flags to the bookmarks command. Either flag will show the remote bookmarks
442 known to the repository; ``--remote`` will also suppress the output of the
453 known to the repository; ``--remote`` will also suppress the output of the
443 local bookmarks.
454 local bookmarks.
444 """
455 """
445
456
446 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
457 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
447 synopsis, docstring)
458 synopsis, docstring)
448 '''
459 '''
449 assert callable(wrapper)
460 assert callable(wrapper)
450 aliases, entry = cmdutil.findcmd(command, table)
461 aliases, entry = cmdutil.findcmd(command, table)
451 for alias, e in table.iteritems():
462 for alias, e in table.iteritems():
452 if e is entry:
463 if e is entry:
453 key = alias
464 key = alias
454 break
465 break
455
466
456 origfn = entry[0]
467 origfn = entry[0]
457 wrap = functools.partial(util.checksignature(wrapper),
468 wrap = functools.partial(util.checksignature(wrapper),
458 util.checksignature(origfn))
469 util.checksignature(origfn))
459 _updatewrapper(wrap, origfn, wrapper)
470 _updatewrapper(wrap, origfn, wrapper)
460 if docstring is not None:
471 if docstring is not None:
461 wrap.__doc__ += docstring
472 wrap.__doc__ += docstring
462
473
463 newentry = list(entry)
474 newentry = list(entry)
464 newentry[0] = wrap
475 newentry[0] = wrap
465 if synopsis is not None:
476 if synopsis is not None:
466 newentry[2] += synopsis
477 newentry[2] += synopsis
467 table[key] = tuple(newentry)
478 table[key] = tuple(newentry)
468 return entry
479 return entry
469
480
470 def wrapfilecache(cls, propname, wrapper):
481 def wrapfilecache(cls, propname, wrapper):
471 """Wraps a filecache property.
482 """Wraps a filecache property.
472
483
473 These can't be wrapped using the normal wrapfunction.
484 These can't be wrapped using the normal wrapfunction.
474 """
485 """
475 propname = pycompat.sysstr(propname)
486 propname = pycompat.sysstr(propname)
476 assert callable(wrapper)
487 assert callable(wrapper)
477 for currcls in cls.__mro__:
488 for currcls in cls.__mro__:
478 if propname in currcls.__dict__:
489 if propname in currcls.__dict__:
479 origfn = currcls.__dict__[propname].func
490 origfn = currcls.__dict__[propname].func
480 assert callable(origfn)
491 assert callable(origfn)
481 def wrap(*args, **kwargs):
492 def wrap(*args, **kwargs):
482 return wrapper(origfn, *args, **kwargs)
493 return wrapper(origfn, *args, **kwargs)
483 currcls.__dict__[propname].func = wrap
494 currcls.__dict__[propname].func = wrap
484 break
495 break
485
496
486 if currcls is object:
497 if currcls is object:
487 raise AttributeError(r"type '%s' has no property '%s'" % (
498 raise AttributeError(r"type '%s' has no property '%s'" % (
488 cls, propname))
499 cls, propname))
489
500
490 class wrappedfunction(object):
501 class wrappedfunction(object):
491 '''context manager for temporarily wrapping a function'''
502 '''context manager for temporarily wrapping a function'''
492
503
493 def __init__(self, container, funcname, wrapper):
504 def __init__(self, container, funcname, wrapper):
494 assert callable(wrapper)
505 assert callable(wrapper)
495 self._container = container
506 self._container = container
496 self._funcname = funcname
507 self._funcname = funcname
497 self._wrapper = wrapper
508 self._wrapper = wrapper
498
509
499 def __enter__(self):
510 def __enter__(self):
500 wrapfunction(self._container, self._funcname, self._wrapper)
511 wrapfunction(self._container, self._funcname, self._wrapper)
501
512
502 def __exit__(self, exctype, excvalue, traceback):
513 def __exit__(self, exctype, excvalue, traceback):
503 unwrapfunction(self._container, self._funcname, self._wrapper)
514 unwrapfunction(self._container, self._funcname, self._wrapper)
504
515
505 def wrapfunction(container, funcname, wrapper):
516 def wrapfunction(container, funcname, wrapper):
506 '''Wrap the function named funcname in container
517 '''Wrap the function named funcname in container
507
518
508 Replace the funcname member in the given container with the specified
519 Replace the funcname member in the given container with the specified
509 wrapper. The container is typically a module, class, or instance.
520 wrapper. The container is typically a module, class, or instance.
510
521
511 The wrapper will be called like
522 The wrapper will be called like
512
523
513 wrapper(orig, *args, **kwargs)
524 wrapper(orig, *args, **kwargs)
514
525
515 where orig is the original (wrapped) function, and *args, **kwargs
526 where orig is the original (wrapped) function, and *args, **kwargs
516 are the arguments passed to it.
527 are the arguments passed to it.
517
528
518 Wrapping methods of the repository object is not recommended since
529 Wrapping methods of the repository object is not recommended since
519 it conflicts with extensions that extend the repository by
530 it conflicts with extensions that extend the repository by
520 subclassing. All extensions that need to extend methods of
531 subclassing. All extensions that need to extend methods of
521 localrepository should use this subclassing trick: namely,
532 localrepository should use this subclassing trick: namely,
522 reposetup() should look like
533 reposetup() should look like
523
534
524 def reposetup(ui, repo):
535 def reposetup(ui, repo):
525 class myrepo(repo.__class__):
536 class myrepo(repo.__class__):
526 def whatever(self, *args, **kwargs):
537 def whatever(self, *args, **kwargs):
527 [...extension stuff...]
538 [...extension stuff...]
528 super(myrepo, self).whatever(*args, **kwargs)
539 super(myrepo, self).whatever(*args, **kwargs)
529 [...extension stuff...]
540 [...extension stuff...]
530
541
531 repo.__class__ = myrepo
542 repo.__class__ = myrepo
532
543
533 In general, combining wrapfunction() with subclassing does not
544 In general, combining wrapfunction() with subclassing does not
534 work. Since you cannot control what other extensions are loaded by
545 work. Since you cannot control what other extensions are loaded by
535 your end users, you should play nicely with others by using the
546 your end users, you should play nicely with others by using the
536 subclass trick.
547 subclass trick.
537 '''
548 '''
538 assert callable(wrapper)
549 assert callable(wrapper)
539
550
540 origfn = getattr(container, funcname)
551 origfn = getattr(container, funcname)
541 assert callable(origfn)
552 assert callable(origfn)
542 if inspect.ismodule(container):
553 if inspect.ismodule(container):
543 # origfn is not an instance or class method. "partial" can be used.
554 # origfn is not an instance or class method. "partial" can be used.
544 # "partial" won't insert a frame in traceback.
555 # "partial" won't insert a frame in traceback.
545 wrap = functools.partial(wrapper, origfn)
556 wrap = functools.partial(wrapper, origfn)
546 else:
557 else:
547 # "partial" cannot be safely used. Emulate its effect by using "bind".
558 # "partial" cannot be safely used. Emulate its effect by using "bind".
548 # The downside is one more frame in traceback.
559 # The downside is one more frame in traceback.
549 wrap = bind(wrapper, origfn)
560 wrap = bind(wrapper, origfn)
550 _updatewrapper(wrap, origfn, wrapper)
561 _updatewrapper(wrap, origfn, wrapper)
551 setattr(container, funcname, wrap)
562 setattr(container, funcname, wrap)
552 return origfn
563 return origfn
553
564
554 def unwrapfunction(container, funcname, wrapper=None):
565 def unwrapfunction(container, funcname, wrapper=None):
555 '''undo wrapfunction
566 '''undo wrapfunction
556
567
557 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
568 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
558 from the chain of wrappers.
569 from the chain of wrappers.
559
570
560 Return the removed wrapper.
571 Return the removed wrapper.
561 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
572 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
562 wrapper is not None but is not found in the wrapper chain.
573 wrapper is not None but is not found in the wrapper chain.
563 '''
574 '''
564 chain = getwrapperchain(container, funcname)
575 chain = getwrapperchain(container, funcname)
565 origfn = chain.pop()
576 origfn = chain.pop()
566 if wrapper is None:
577 if wrapper is None:
567 wrapper = chain[0]
578 wrapper = chain[0]
568 chain.remove(wrapper)
579 chain.remove(wrapper)
569 setattr(container, funcname, origfn)
580 setattr(container, funcname, origfn)
570 for w in reversed(chain):
581 for w in reversed(chain):
571 wrapfunction(container, funcname, w)
582 wrapfunction(container, funcname, w)
572 return wrapper
583 return wrapper
573
584
574 def getwrapperchain(container, funcname):
585 def getwrapperchain(container, funcname):
575 '''get a chain of wrappers of a function
586 '''get a chain of wrappers of a function
576
587
577 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
588 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
578
589
579 The wrapper functions are the ones passed to wrapfunction, whose first
590 The wrapper functions are the ones passed to wrapfunction, whose first
580 argument is origfunc.
591 argument is origfunc.
581 '''
592 '''
582 result = []
593 result = []
583 fn = getattr(container, funcname)
594 fn = getattr(container, funcname)
584 while fn:
595 while fn:
585 assert callable(fn)
596 assert callable(fn)
586 result.append(getattr(fn, '_unboundwrapper', fn))
597 result.append(getattr(fn, '_unboundwrapper', fn))
587 fn = getattr(fn, '_origfunc', None)
598 fn = getattr(fn, '_origfunc', None)
588 return result
599 return result
589
600
590 def _disabledpaths():
601 def _disabledpaths():
591 '''find paths of disabled extensions. returns a dict of {name: path}'''
602 '''find paths of disabled extensions. returns a dict of {name: path}'''
592 import hgext
603 import hgext
593 extpath = os.path.dirname(
604 extpath = os.path.dirname(
594 os.path.abspath(pycompat.fsencode(hgext.__file__)))
605 os.path.abspath(pycompat.fsencode(hgext.__file__)))
595 try: # might not be a filesystem path
606 try: # might not be a filesystem path
596 files = os.listdir(extpath)
607 files = os.listdir(extpath)
597 except OSError:
608 except OSError:
598 return {}
609 return {}
599
610
600 exts = {}
611 exts = {}
601 for e in files:
612 for e in files:
602 if e.endswith('.py'):
613 if e.endswith('.py'):
603 name = e.rsplit('.', 1)[0]
614 name = e.rsplit('.', 1)[0]
604 path = os.path.join(extpath, e)
615 path = os.path.join(extpath, e)
605 else:
616 else:
606 name = e
617 name = e
607 path = os.path.join(extpath, e, '__init__.py')
618 path = os.path.join(extpath, e, '__init__.py')
608 if not os.path.exists(path):
619 if not os.path.exists(path):
609 continue
620 continue
610 if name in exts or name in _order or name == '__init__':
621 if name in exts or name in _order or name == '__init__':
611 continue
622 continue
612 exts[name] = path
623 exts[name] = path
613 for name, path in _disabledextensions.iteritems():
624 for name, path in _disabledextensions.iteritems():
614 # If no path was provided for a disabled extension (e.g. "color=!"),
625 # If no path was provided for a disabled extension (e.g. "color=!"),
615 # don't replace the path we already found by the scan above.
626 # don't replace the path we already found by the scan above.
616 if path:
627 if path:
617 exts[name] = path
628 exts[name] = path
618 return exts
629 return exts
619
630
620 def _moduledoc(file):
631 def _moduledoc(file):
621 '''return the top-level python documentation for the given file
632 '''return the top-level python documentation for the given file
622
633
623 Loosely inspired by pydoc.source_synopsis(), but rewritten to
634 Loosely inspired by pydoc.source_synopsis(), but rewritten to
624 handle triple quotes and to return the whole text instead of just
635 handle triple quotes and to return the whole text instead of just
625 the synopsis'''
636 the synopsis'''
626 result = []
637 result = []
627
638
628 line = file.readline()
639 line = file.readline()
629 while line[:1] == '#' or not line.strip():
640 while line[:1] == '#' or not line.strip():
630 line = file.readline()
641 line = file.readline()
631 if not line:
642 if not line:
632 break
643 break
633
644
634 start = line[:3]
645 start = line[:3]
635 if start == '"""' or start == "'''":
646 if start == '"""' or start == "'''":
636 line = line[3:]
647 line = line[3:]
637 while line:
648 while line:
638 if line.rstrip().endswith(start):
649 if line.rstrip().endswith(start):
639 line = line.split(start)[0]
650 line = line.split(start)[0]
640 if line:
651 if line:
641 result.append(line)
652 result.append(line)
642 break
653 break
643 elif not line:
654 elif not line:
644 return None # unmatched delimiter
655 return None # unmatched delimiter
645 result.append(line)
656 result.append(line)
646 line = file.readline()
657 line = file.readline()
647 else:
658 else:
648 return None
659 return None
649
660
650 return ''.join(result)
661 return ''.join(result)
651
662
652 def _disabledhelp(path):
663 def _disabledhelp(path):
653 '''retrieve help synopsis of a disabled extension (without importing)'''
664 '''retrieve help synopsis of a disabled extension (without importing)'''
654 try:
665 try:
655 with open(path, 'rb') as src:
666 with open(path, 'rb') as src:
656 doc = _moduledoc(src)
667 doc = _moduledoc(src)
657 except IOError:
668 except IOError:
658 return
669 return
659
670
660 if doc: # extracting localized synopsis
671 if doc: # extracting localized synopsis
661 return gettext(doc)
672 return gettext(doc)
662 else:
673 else:
663 return _('(no help text available)')
674 return _('(no help text available)')
664
675
665 def disabled():
676 def disabled():
666 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
677 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
667 try:
678 try:
668 from hgext import __index__
679 from hgext import __index__
669 return dict((name, gettext(desc))
680 return dict((name, gettext(desc))
670 for name, desc in __index__.docs.iteritems()
681 for name, desc in __index__.docs.iteritems()
671 if name not in _order)
682 if name not in _order)
672 except (ImportError, AttributeError):
683 except (ImportError, AttributeError):
673 pass
684 pass
674
685
675 paths = _disabledpaths()
686 paths = _disabledpaths()
676 if not paths:
687 if not paths:
677 return {}
688 return {}
678
689
679 exts = {}
690 exts = {}
680 for name, path in paths.iteritems():
691 for name, path in paths.iteritems():
681 doc = _disabledhelp(path)
692 doc = _disabledhelp(path)
682 if doc:
693 if doc:
683 exts[name] = doc.splitlines()[0]
694 exts[name] = doc.splitlines()[0]
684
695
685 return exts
696 return exts
686
697
687 def disabledext(name):
698 def disabledext(name):
688 '''find a specific disabled extension from hgext. returns desc'''
699 '''find a specific disabled extension from hgext. returns desc'''
689 try:
700 try:
690 from hgext import __index__
701 from hgext import __index__
691 if name in _order: # enabled
702 if name in _order: # enabled
692 return
703 return
693 else:
704 else:
694 return gettext(__index__.docs.get(name))
705 return gettext(__index__.docs.get(name))
695 except (ImportError, AttributeError):
706 except (ImportError, AttributeError):
696 pass
707 pass
697
708
698 paths = _disabledpaths()
709 paths = _disabledpaths()
699 if name in paths:
710 if name in paths:
700 return _disabledhelp(paths[name])
711 return _disabledhelp(paths[name])
701
712
702 def _walkcommand(node):
713 def _walkcommand(node):
703 """Scan @command() decorators in the tree starting at node"""
714 """Scan @command() decorators in the tree starting at node"""
704 todo = collections.deque([node])
715 todo = collections.deque([node])
705 while todo:
716 while todo:
706 node = todo.popleft()
717 node = todo.popleft()
707 if not isinstance(node, ast.FunctionDef):
718 if not isinstance(node, ast.FunctionDef):
708 todo.extend(ast.iter_child_nodes(node))
719 todo.extend(ast.iter_child_nodes(node))
709 continue
720 continue
710 for d in node.decorator_list:
721 for d in node.decorator_list:
711 if not isinstance(d, ast.Call):
722 if not isinstance(d, ast.Call):
712 continue
723 continue
713 if not isinstance(d.func, ast.Name):
724 if not isinstance(d.func, ast.Name):
714 continue
725 continue
715 if d.func.id != r'command':
726 if d.func.id != r'command':
716 continue
727 continue
717 yield d
728 yield d
718
729
719 def _disabledcmdtable(path):
730 def _disabledcmdtable(path):
720 """Construct a dummy command table without loading the extension module
731 """Construct a dummy command table without loading the extension module
721
732
722 This may raise IOError or SyntaxError.
733 This may raise IOError or SyntaxError.
723 """
734 """
724 with open(path, 'rb') as src:
735 with open(path, 'rb') as src:
725 root = ast.parse(src.read(), path)
736 root = ast.parse(src.read(), path)
726 cmdtable = {}
737 cmdtable = {}
727 for node in _walkcommand(root):
738 for node in _walkcommand(root):
728 if not node.args:
739 if not node.args:
729 continue
740 continue
730 a = node.args[0]
741 a = node.args[0]
731 if isinstance(a, ast.Str):
742 if isinstance(a, ast.Str):
732 name = pycompat.sysbytes(a.s)
743 name = pycompat.sysbytes(a.s)
733 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
744 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
734 name = a.s
745 name = a.s
735 else:
746 else:
736 continue
747 continue
737 cmdtable[name] = (None, [], b'')
748 cmdtable[name] = (None, [], b'')
738 return cmdtable
749 return cmdtable
739
750
740 def _finddisabledcmd(ui, cmd, name, path, strict):
751 def _finddisabledcmd(ui, cmd, name, path, strict):
741 try:
752 try:
742 cmdtable = _disabledcmdtable(path)
753 cmdtable = _disabledcmdtable(path)
743 except (IOError, SyntaxError):
754 except (IOError, SyntaxError):
744 return
755 return
745 try:
756 try:
746 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
757 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
747 except (error.AmbiguousCommand, error.UnknownCommand):
758 except (error.AmbiguousCommand, error.UnknownCommand):
748 return
759 return
749 for c in aliases:
760 for c in aliases:
750 if c.startswith(cmd):
761 if c.startswith(cmd):
751 cmd = c
762 cmd = c
752 break
763 break
753 else:
764 else:
754 cmd = aliases[0]
765 cmd = aliases[0]
755 doc = _disabledhelp(path)
766 doc = _disabledhelp(path)
756 return (cmd, name, doc)
767 return (cmd, name, doc)
757
768
758 def disabledcmd(ui, cmd, strict=False):
769 def disabledcmd(ui, cmd, strict=False):
759 '''find cmd from disabled extensions without importing.
770 '''find cmd from disabled extensions without importing.
760 returns (cmdname, extname, doc)'''
771 returns (cmdname, extname, doc)'''
761
772
762 paths = _disabledpaths()
773 paths = _disabledpaths()
763 if not paths:
774 if not paths:
764 raise error.UnknownCommand(cmd)
775 raise error.UnknownCommand(cmd)
765
776
766 ext = None
777 ext = None
767 # first, search for an extension with the same name as the command
778 # first, search for an extension with the same name as the command
768 path = paths.pop(cmd, None)
779 path = paths.pop(cmd, None)
769 if path:
780 if path:
770 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
781 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
771 if not ext:
782 if not ext:
772 # otherwise, interrogate each extension until there's a match
783 # otherwise, interrogate each extension until there's a match
773 for name, path in paths.iteritems():
784 for name, path in paths.iteritems():
774 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
785 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
775 if ext:
786 if ext:
776 break
787 break
777 if ext:
788 if ext:
778 return ext
789 return ext
779
790
780 raise error.UnknownCommand(cmd)
791 raise error.UnknownCommand(cmd)
781
792
782 def enabled(shortname=True):
793 def enabled(shortname=True):
783 '''return a dict of {name: desc} of extensions'''
794 '''return a dict of {name: desc} of extensions'''
784 exts = {}
795 exts = {}
785 for ename, ext in extensions():
796 for ename, ext in extensions():
786 doc = (gettext(ext.__doc__) or _('(no help text available)'))
797 doc = (gettext(ext.__doc__) or _('(no help text available)'))
787 if shortname:
798 if shortname:
788 ename = ename.split('.')[-1]
799 ename = ename.split('.')[-1]
789 exts[ename] = doc.splitlines()[0].strip()
800 exts[ename] = doc.splitlines()[0].strip()
790
801
791 return exts
802 return exts
792
803
793 def notloaded():
804 def notloaded():
794 '''return short names of extensions that failed to load'''
805 '''return short names of extensions that failed to load'''
795 return [name for name, mod in _extensions.iteritems() if mod is None]
806 return [name for name, mod in _extensions.iteritems() if mod is None]
796
807
797 def moduleversion(module):
808 def moduleversion(module):
798 '''return version information from given module as a string'''
809 '''return version information from given module as a string'''
799 if (util.safehasattr(module, 'getversion')
810 if (util.safehasattr(module, 'getversion')
800 and callable(module.getversion)):
811 and callable(module.getversion)):
801 version = module.getversion()
812 version = module.getversion()
802 elif util.safehasattr(module, '__version__'):
813 elif util.safehasattr(module, '__version__'):
803 version = module.__version__
814 version = module.__version__
804 else:
815 else:
805 version = ''
816 version = ''
806 if isinstance(version, (list, tuple)):
817 if isinstance(version, (list, tuple)):
807 version = '.'.join(pycompat.bytestr(o) for o in version)
818 version = '.'.join(pycompat.bytestr(o) for o in version)
808 return version
819 return version
809
820
810 def ismoduleinternal(module):
821 def ismoduleinternal(module):
811 exttestedwith = getattr(module, 'testedwith', None)
822 exttestedwith = getattr(module, 'testedwith', None)
812 return exttestedwith == "ships-with-hg-core"
823 return exttestedwith == "ships-with-hg-core"
@@ -1,135 +1,137 b''
1 ensure that failing ui.atexit handlers report sensibly
1 ensure that failing ui.atexit handlers report sensibly
2
2
3 $ cat > $TESTTMP/bailatexit.py <<EOF
3 $ cat > $TESTTMP/bailatexit.py <<EOF
4 > from mercurial import util
4 > from mercurial import util
5 > def bail():
5 > def bail():
6 > raise RuntimeError('ui.atexit handler exception')
6 > raise RuntimeError('ui.atexit handler exception')
7 >
7 >
8 > def extsetup(ui):
8 > def extsetup(ui):
9 > ui.atexit(bail)
9 > ui.atexit(bail)
10 > EOF
10 > EOF
11 $ hg -q --config extensions.bailatexit=$TESTTMP/bailatexit.py \
11 $ hg -q --config extensions.bailatexit=$TESTTMP/bailatexit.py \
12 > help help
12 > help help
13 hg help [-ecks] [TOPIC]
13 hg help [-ecks] [TOPIC]
14
14
15 show help for a given topic or a help overview
15 show help for a given topic or a help overview
16 error in exit handlers:
16 error in exit handlers:
17 Traceback (most recent call last):
17 Traceback (most recent call last):
18 File "*/mercurial/dispatch.py", line *, in _runexithandlers (glob)
18 File "*/mercurial/dispatch.py", line *, in _runexithandlers (glob)
19 func(*args, **kwargs)
19 func(*args, **kwargs)
20 File "$TESTTMP/bailatexit.py", line *, in bail (glob)
20 File "$TESTTMP/bailatexit.py", line *, in bail (glob)
21 raise RuntimeError('ui.atexit handler exception')
21 raise RuntimeError('ui.atexit handler exception')
22 RuntimeError: ui.atexit handler exception
22 RuntimeError: ui.atexit handler exception
23 [255]
23 [255]
24
24
25 $ rm $TESTTMP/bailatexit.py
25 $ rm $TESTTMP/bailatexit.py
26
26
27 another bad extension
27 another bad extension
28
28
29 $ echo 'raise Exception("bit bucket overflow")' > badext.py
29 $ echo 'raise Exception("bit bucket overflow")' > badext.py
30 $ abspathexc=`pwd`/badext.py
30 $ abspathexc=`pwd`/badext.py
31
31
32 $ cat >baddocext.py <<EOF
32 $ cat >baddocext.py <<EOF
33 > """
33 > """
34 > baddocext is bad
34 > baddocext is bad
35 > """
35 > """
36 > EOF
36 > EOF
37 $ abspathdoc=`pwd`/baddocext.py
37 $ abspathdoc=`pwd`/baddocext.py
38
38
39 $ cat <<EOF >> $HGRCPATH
39 $ cat <<EOF >> $HGRCPATH
40 > [extensions]
40 > [extensions]
41 > gpg =
41 > gpg =
42 > hgext.gpg =
42 > hgext.gpg =
43 > badext = $abspathexc
43 > badext = $abspathexc
44 > baddocext = $abspathdoc
44 > baddocext = $abspathdoc
45 > badext2 =
45 > badext2 =
46 > EOF
46 > EOF
47
47
48 $ hg -q help help 2>&1 |grep extension
48 $ hg -q help help 2>&1 |grep extension
49 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
49 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
50 *** failed to import extension badext2: No module named badext2
50 *** failed to import extension badext2: No module named badext2
51
51
52 show traceback
52 show traceback
53
53
54 $ hg -q help help --traceback 2>&1 | egrep ' extension|^Exception|Traceback|ImportError'
54 $ hg -q help help --traceback 2>&1 | egrep ' extension|^Exception|Traceback|ImportError'
55 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
55 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
56 Traceback (most recent call last):
56 Traceback (most recent call last):
57 Exception: bit bucket overflow
57 Exception: bit bucket overflow
58 *** failed to import extension badext2: No module named badext2
58 *** failed to import extension badext2: No module named badext2
59 Traceback (most recent call last):
59 Traceback (most recent call last):
60 ImportError: No module named badext2
60 ImportError: No module named badext2
61
61
62 names of extensions failed to load can be accessed via extensions.notloaded()
62 names of extensions failed to load can be accessed via extensions.notloaded()
63
63
64 $ cat <<EOF > showbadexts.py
64 $ cat <<EOF > showbadexts.py
65 > from mercurial import commands, extensions, registrar
65 > from mercurial import commands, extensions, registrar
66 > cmdtable = {}
66 > cmdtable = {}
67 > command = registrar.command(cmdtable)
67 > command = registrar.command(cmdtable)
68 > @command(b'showbadexts', norepo=True)
68 > @command(b'showbadexts', norepo=True)
69 > def showbadexts(ui, *pats, **opts):
69 > def showbadexts(ui, *pats, **opts):
70 > ui.write('BADEXTS: %s\n' % ' '.join(sorted(extensions.notloaded())))
70 > ui.write('BADEXTS: %s\n' % ' '.join(sorted(extensions.notloaded())))
71 > EOF
71 > EOF
72 $ hg --config extensions.badexts=showbadexts.py showbadexts 2>&1 | grep '^BADEXTS'
72 $ hg --config extensions.badexts=showbadexts.py showbadexts 2>&1 | grep '^BADEXTS'
73 BADEXTS: badext badext2
73 BADEXTS: badext badext2
74
74
75 #if no-extraextensions
75 #if no-extraextensions
76 show traceback for ImportError of hgext.name if devel.debug.extensions is set
76 show traceback for ImportError of hgext.name if devel.debug.extensions is set
77
77
78 $ (hg help help --traceback --debug --config devel.debug.extensions=yes 2>&1) \
78 $ (hg help help --traceback --debug --config devel.debug.extensions=yes 2>&1) \
79 > | grep -v '^ ' \
79 > | grep -v '^ ' \
80 > | egrep 'extension..[^p]|^Exception|Traceback|ImportError|not import'
80 > | egrep 'extension..[^p]|^Exception|Traceback|ImportError|not import'
81 debug.extensions: loading extensions
81 debug.extensions: loading extensions
82 debug.extensions: - processing 5 entries
82 debug.extensions: - processing 5 entries
83 debug.extensions: - loading extension: 'gpg'
83 debug.extensions: - loading extension: 'gpg'
84 debug.extensions: > 'gpg' extension loaded in * (glob)
84 debug.extensions: > 'gpg' extension loaded in * (glob)
85 debug.extensions: - validating extension tables: 'gpg'
85 debug.extensions: - validating extension tables: 'gpg'
86 debug.extensions: - invoking registered callbacks: 'gpg'
86 debug.extensions: - invoking registered callbacks: 'gpg'
87 debug.extensions: > callbacks completed in * (glob)
87 debug.extensions: > callbacks completed in * (glob)
88 debug.extensions: - loading extension: 'badext'
88 debug.extensions: - loading extension: 'badext'
89 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
89 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
90 Traceback (most recent call last):
90 Traceback (most recent call last):
91 Exception: bit bucket overflow
91 Exception: bit bucket overflow
92 debug.extensions: - loading extension: 'baddocext'
92 debug.extensions: - loading extension: 'baddocext'
93 debug.extensions: > 'baddocext' extension loaded in * (glob)
93 debug.extensions: > 'baddocext' extension loaded in * (glob)
94 debug.extensions: - validating extension tables: 'baddocext'
94 debug.extensions: - validating extension tables: 'baddocext'
95 debug.extensions: - invoking registered callbacks: 'baddocext'
95 debug.extensions: - invoking registered callbacks: 'baddocext'
96 debug.extensions: > callbacks completed in * (glob)
96 debug.extensions: > callbacks completed in * (glob)
97 debug.extensions: - loading extension: 'badext2'
97 debug.extensions: - loading extension: 'badext2'
98 debug.extensions: - could not import hgext.badext2 (No module named badext2): trying hgext3rd.badext2
98 debug.extensions: - could not import hgext.badext2 (No module named badext2): trying hgext3rd.badext2
99 Traceback (most recent call last):
99 Traceback (most recent call last):
100 ImportError: No module named *badext2 (glob)
100 ImportError: No module named *badext2 (glob)
101 debug.extensions: - could not import hgext3rd.badext2 (No module named badext2): trying badext2
101 debug.extensions: - could not import hgext3rd.badext2 (No module named badext2): trying badext2
102 Traceback (most recent call last):
102 Traceback (most recent call last):
103 ImportError: No module named *badext2 (glob)
103 ImportError: No module named *badext2 (glob)
104 *** failed to import extension badext2: No module named badext2
104 *** failed to import extension badext2: No module named badext2
105 Traceback (most recent call last):
105 Traceback (most recent call last):
106 ImportError: No module named badext2
106 ImportError: No module named badext2
107 debug.extensions: > loaded 2 extensions, total time * (glob)
107 debug.extensions: > loaded 2 extensions, total time * (glob)
108 debug.extensions: - loading configtable attributes
108 debug.extensions: - loading configtable attributes
109 debug.extensions: - executing uisetup hooks
109 debug.extensions: - executing uisetup hooks
110 debug.extensions: - running uisetup for 'gpg'
110 debug.extensions: - running uisetup for 'gpg'
111 debug.extensions: > uisetup for 'gpg' took * (glob)
111 debug.extensions: > uisetup for 'gpg' took * (glob)
112 debug.extensions: - running uisetup for 'baddocext'
112 debug.extensions: - running uisetup for 'baddocext'
113 debug.extensions: > uisetup for 'baddocext' took * (glob)
113 debug.extensions: > uisetup for 'baddocext' took * (glob)
114 debug.extensions: > all uisetup took * (glob)
114 debug.extensions: > all uisetup took * (glob)
115 debug.extensions: - executing extsetup hooks
115 debug.extensions: - executing extsetup hooks
116 debug.extensions: - running extsetup for 'gpg'
116 debug.extensions: - running extsetup for 'gpg'
117 debug.extensions: > extsetup for 'gpg' took * (glob)
117 debug.extensions: > extsetup for 'gpg' took * (glob)
118 debug.extensions: - running extsetup for 'baddocext'
118 debug.extensions: - running extsetup for 'baddocext'
119 debug.extensions: > extsetup for 'baddocext' took * (glob)
119 debug.extensions: > extsetup for 'baddocext' took * (glob)
120 debug.extensions: > all extsetup took * (glob)
120 debug.extensions: > all extsetup took * (glob)
121 debug.extensions: - executing remaining aftercallbacks
121 debug.extensions: - executing remaining aftercallbacks
122 debug.extensions: > remaining aftercallbacks completed in * (glob)
122 debug.extensions: > remaining aftercallbacks completed in * (glob)
123 debug.extensions: - loading extension registration objects
123 debug.extensions: - loading extension registration objects
124 debug.extensions: > extension registration object loading took * (glob)
124 debug.extensions: > extension registration object loading took * (glob)
125 debug.extensions: > extension baddocext take a total of * to load (glob)
126 debug.extensions: > extension gpg take a total of * to load (glob)
125 debug.extensions: extension loading complete
127 debug.extensions: extension loading complete
126 #endif
128 #endif
127
129
128 confirm that there's no crash when an extension's documentation is bad
130 confirm that there's no crash when an extension's documentation is bad
129
131
130 $ hg help --keyword baddocext
132 $ hg help --keyword baddocext
131 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
133 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
132 *** failed to import extension badext2: No module named badext2
134 *** failed to import extension badext2: No module named badext2
133 Topics:
135 Topics:
134
136
135 extensions Using Additional Features
137 extensions Using Additional Features
@@ -1,95 +1,96 b''
1 Test basic extension support
1 Test basic extension support
2
2
3 $ cat > foobar.py <<EOF
3 $ cat > foobar.py <<EOF
4 > import os
4 > import os
5 > from mercurial import commands, registrar
5 > from mercurial import commands, registrar
6 > cmdtable = {}
6 > cmdtable = {}
7 > command = registrar.command(cmdtable)
7 > command = registrar.command(cmdtable)
8 > configtable = {}
8 > configtable = {}
9 > configitem = registrar.configitem(configtable)
9 > configitem = registrar.configitem(configtable)
10 > configitem(b'tests', b'foo', default=b"Foo")
10 > configitem(b'tests', b'foo', default=b"Foo")
11 > def uisetup(ui):
11 > def uisetup(ui):
12 > ui.debug(b"uisetup called [debug]\\n")
12 > ui.debug(b"uisetup called [debug]\\n")
13 > ui.write(b"uisetup called\\n")
13 > ui.write(b"uisetup called\\n")
14 > ui.status(b"uisetup called [status]\\n")
14 > ui.status(b"uisetup called [status]\\n")
15 > ui.flush()
15 > ui.flush()
16 > def reposetup(ui, repo):
16 > def reposetup(ui, repo):
17 > ui.write(b"reposetup called for %s\\n" % os.path.basename(repo.root))
17 > ui.write(b"reposetup called for %s\\n" % os.path.basename(repo.root))
18 > ui.write(b"ui %s= repo.ui\\n" % (ui == repo.ui and b"=" or b"!"))
18 > ui.write(b"ui %s= repo.ui\\n" % (ui == repo.ui and b"=" or b"!"))
19 > ui.flush()
19 > ui.flush()
20 > @command(b'foo', [], b'hg foo')
20 > @command(b'foo', [], b'hg foo')
21 > def foo(ui, *args, **kwargs):
21 > def foo(ui, *args, **kwargs):
22 > foo = ui.config(b'tests', b'foo')
22 > foo = ui.config(b'tests', b'foo')
23 > ui.write(foo)
23 > ui.write(foo)
24 > ui.write(b"\\n")
24 > ui.write(b"\\n")
25 > @command(b'bar', [], b'hg bar', norepo=True)
25 > @command(b'bar', [], b'hg bar', norepo=True)
26 > def bar(ui, *args, **kwargs):
26 > def bar(ui, *args, **kwargs):
27 > ui.write(b"Bar\\n")
27 > ui.write(b"Bar\\n")
28 > EOF
28 > EOF
29 $ abspath=`pwd`/foobar.py
29 $ abspath=`pwd`/foobar.py
30
30
31 $ mkdir barfoo
31 $ mkdir barfoo
32 $ cp foobar.py barfoo/__init__.py
32 $ cp foobar.py barfoo/__init__.py
33 $ barfoopath=`pwd`/barfoo
33 $ barfoopath=`pwd`/barfoo
34
34
35 $ hg init a
35 $ hg init a
36 $ cd a
36 $ cd a
37 $ echo foo > file
37 $ echo foo > file
38 $ hg add file
38 $ hg add file
39 $ hg commit -m 'add file'
39 $ hg commit -m 'add file'
40
40
41 $ echo '[extensions]' >> $HGRCPATH
41 $ echo '[extensions]' >> $HGRCPATH
42 $ echo "foobar = $abspath" >> $HGRCPATH
42 $ echo "foobar = $abspath" >> $HGRCPATH
43
43
44 Test extension setup timings
44 Test extension setup timings
45
45
46 $ hg foo --traceback --config devel.debug.extensions=yes --debug 2>&1
46 $ hg foo --traceback --config devel.debug.extensions=yes --debug 2>&1
47 debug.extensions: loading extensions
47 debug.extensions: loading extensions
48 debug.extensions: - processing 1 entries
48 debug.extensions: - processing 1 entries
49 debug.extensions: - loading extension: 'foobar'
49 debug.extensions: - loading extension: 'foobar'
50 debug.extensions: > 'foobar' extension loaded in * (glob)
50 debug.extensions: > 'foobar' extension loaded in * (glob)
51 debug.extensions: - validating extension tables: 'foobar'
51 debug.extensions: - validating extension tables: 'foobar'
52 debug.extensions: - invoking registered callbacks: 'foobar'
52 debug.extensions: - invoking registered callbacks: 'foobar'
53 debug.extensions: > callbacks completed in * (glob)
53 debug.extensions: > callbacks completed in * (glob)
54 debug.extensions: > loaded 1 extensions, total time * (glob)
54 debug.extensions: > loaded 1 extensions, total time * (glob)
55 debug.extensions: - loading configtable attributes
55 debug.extensions: - loading configtable attributes
56 debug.extensions: - executing uisetup hooks
56 debug.extensions: - executing uisetup hooks
57 debug.extensions: - running uisetup for 'foobar'
57 debug.extensions: - running uisetup for 'foobar'
58 uisetup called [debug]
58 uisetup called [debug]
59 uisetup called
59 uisetup called
60 uisetup called [status]
60 uisetup called [status]
61 debug.extensions: > uisetup for 'foobar' took * (glob)
61 debug.extensions: > uisetup for 'foobar' took * (glob)
62 debug.extensions: > all uisetup took * (glob)
62 debug.extensions: > all uisetup took * (glob)
63 debug.extensions: - executing extsetup hooks
63 debug.extensions: - executing extsetup hooks
64 debug.extensions: - running extsetup for 'foobar'
64 debug.extensions: - running extsetup for 'foobar'
65 debug.extensions: > extsetup for 'foobar' took * (glob)
65 debug.extensions: > extsetup for 'foobar' took * (glob)
66 debug.extensions: > all extsetup took * (glob)
66 debug.extensions: > all extsetup took * (glob)
67 debug.extensions: - executing remaining aftercallbacks
67 debug.extensions: - executing remaining aftercallbacks
68 debug.extensions: > remaining aftercallbacks completed in * (glob)
68 debug.extensions: > remaining aftercallbacks completed in * (glob)
69 debug.extensions: - loading extension registration objects
69 debug.extensions: - loading extension registration objects
70 debug.extensions: > extension registration object loading took * (glob)
70 debug.extensions: > extension registration object loading took * (glob)
71 debug.extensions: > extension foobar take a total of * to load (glob)
71 debug.extensions: extension loading complete
72 debug.extensions: extension loading complete
72 debug.extensions: loading additional extensions
73 debug.extensions: loading additional extensions
73 debug.extensions: - processing 1 entries
74 debug.extensions: - processing 1 entries
74 debug.extensions: > loaded 0 extensions, total time * (glob)
75 debug.extensions: > loaded 0 extensions, total time * (glob)
75 debug.extensions: - loading configtable attributes
76 debug.extensions: - loading configtable attributes
76 debug.extensions: - executing uisetup hooks
77 debug.extensions: - executing uisetup hooks
77 debug.extensions: > all uisetup took * (glob)
78 debug.extensions: > all uisetup took * (glob)
78 debug.extensions: - executing extsetup hooks
79 debug.extensions: - executing extsetup hooks
79 debug.extensions: > all extsetup took * (glob)
80 debug.extensions: > all extsetup took * (glob)
80 debug.extensions: - executing remaining aftercallbacks
81 debug.extensions: - executing remaining aftercallbacks
81 debug.extensions: > remaining aftercallbacks completed in * (glob)
82 debug.extensions: > remaining aftercallbacks completed in * (glob)
82 debug.extensions: - loading extension registration objects
83 debug.extensions: - loading extension registration objects
83 debug.extensions: > extension registration object loading took * (glob)
84 debug.extensions: > extension registration object loading took * (glob)
84 debug.extensions: extension loading complete
85 debug.extensions: extension loading complete
85 debug.extensions: - executing reposetup hooks
86 debug.extensions: - executing reposetup hooks
86 debug.extensions: - running reposetup for foobar
87 debug.extensions: - running reposetup for foobar
87 reposetup called for a
88 reposetup called for a
88 ui == repo.ui
89 ui == repo.ui
89 debug.extensions: > reposetup for 'foobar' took * (glob)
90 debug.extensions: > reposetup for 'foobar' took * (glob)
90 debug.extensions: > all reposetup took * (glob)
91 debug.extensions: > all reposetup took * (glob)
91 Foo
92 Foo
92
93
93 $ cd ..
94 $ cd ..
94
95
95 $ echo 'foobar = !' >> $HGRCPATH
96 $ echo 'foobar = !' >> $HGRCPATH
General Comments 0
You need to be logged in to leave comments. Login now