##// END OF EJS Templates
profiling: move profiling code from dispatch.py (API)...
Gregory Szorc -
r29781:2654a0aa default
parent child Browse files
Show More
@@ -0,0 +1,132 b''
1 # profiling.py - profiling functions
2 #
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
4 #
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.
7
8 from __future__ import absolute_import, print_function
9
10 import os
11 import sys
12 import time
13
14 from .i18n import _
15 from . import (
16 error,
17 util,
18 )
19
20 def lsprofile(ui, func, fp):
21 format = ui.config('profiling', 'format', default='text')
22 field = ui.config('profiling', 'sort', default='inlinetime')
23 limit = ui.configint('profiling', 'limit', default=30)
24 climit = ui.configint('profiling', 'nested', default=0)
25
26 if format not in ['text', 'kcachegrind']:
27 ui.warn(_("unrecognized profiling format '%s'"
28 " - Ignored\n") % format)
29 format = 'text'
30
31 try:
32 from . import lsprof
33 except ImportError:
34 raise error.Abort(_(
35 'lsprof not available - install from '
36 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
37 p = lsprof.Profiler()
38 p.enable(subcalls=True)
39 try:
40 return func()
41 finally:
42 p.disable()
43
44 if format == 'kcachegrind':
45 from . import lsprofcalltree
46 calltree = lsprofcalltree.KCacheGrind(p)
47 calltree.output(fp)
48 else:
49 # format == 'text'
50 stats = lsprof.Stats(p.getstats())
51 stats.sort(field)
52 stats.pprint(limit=limit, file=fp, climit=climit)
53
54 def flameprofile(ui, func, fp):
55 try:
56 from flamegraph import flamegraph
57 except ImportError:
58 raise error.Abort(_(
59 'flamegraph not available - install from '
60 'https://github.com/evanhempel/python-flamegraph'))
61 # developer config: profiling.freq
62 freq = ui.configint('profiling', 'freq', default=1000)
63 filter_ = None
64 collapse_recursion = True
65 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
66 filter_, collapse_recursion)
67 start_time = time.clock()
68 try:
69 thread.start()
70 func()
71 finally:
72 thread.stop()
73 thread.join()
74 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
75 time.clock() - start_time, thread.num_frames(),
76 thread.num_frames(unique=True)))
77
78 def statprofile(ui, func, fp):
79 try:
80 import statprof
81 except ImportError:
82 raise error.Abort(_(
83 'statprof not available - install using "easy_install statprof"'))
84
85 freq = ui.configint('profiling', 'freq', default=1000)
86 if freq > 0:
87 statprof.reset(freq)
88 else:
89 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
90
91 statprof.start()
92 try:
93 return func()
94 finally:
95 statprof.stop()
96 statprof.display(fp)
97
98 def profile(ui, fn):
99 """Profile a function call."""
100 profiler = os.getenv('HGPROF')
101 if profiler is None:
102 profiler = ui.config('profiling', 'type', default='ls')
103 if profiler not in ('ls', 'stat', 'flame'):
104 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
105 profiler = 'ls'
106
107 output = ui.config('profiling', 'output')
108
109 if output == 'blackbox':
110 fp = util.stringio()
111 elif output:
112 path = ui.expandpath(output)
113 fp = open(path, 'wb')
114 else:
115 fp = sys.stderr
116
117 try:
118 if profiler == 'ls':
119 return lsprofile(ui, fn, fp)
120 elif profiler == 'flame':
121 return flameprofile(ui, fn, fp)
122 else:
123 return statprofile(ui, fn, fp)
124 finally:
125 if output:
126 if output == 'blackbox':
127 val = 'Profile:\n%s' % fp.getvalue()
128 # ui.log treats the input as a format string,
129 # so we need to escape any % signs.
130 val = val.replace('%', '%%')
131 ui.log('profile', val)
132 fp.close()
@@ -1,1087 +1,977 b''
1 1 # dispatch.py - command dispatching for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import atexit
11 11 import difflib
12 12 import errno
13 13 import os
14 14 import pdb
15 15 import re
16 16 import shlex
17 17 import signal
18 18 import socket
19 19 import sys
20 20 import time
21 21 import traceback
22 22
23 23
24 24 from .i18n import _
25 25
26 26 from . import (
27 27 cmdutil,
28 28 commands,
29 29 demandimport,
30 30 encoding,
31 31 error,
32 32 extensions,
33 33 fancyopts,
34 34 fileset,
35 35 hg,
36 36 hook,
37 profiling,
37 38 revset,
38 39 templatefilters,
39 40 templatekw,
40 41 templater,
41 42 ui as uimod,
42 43 util,
43 44 )
44 45
45 46 class request(object):
46 47 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
47 48 ferr=None):
48 49 self.args = args
49 50 self.ui = ui
50 51 self.repo = repo
51 52
52 53 # input/output/error streams
53 54 self.fin = fin
54 55 self.fout = fout
55 56 self.ferr = ferr
56 57
57 58 def run():
58 59 "run the command in sys.argv"
59 60 sys.exit((dispatch(request(sys.argv[1:])) or 0) & 255)
60 61
61 62 def _getsimilar(symbols, value):
62 63 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
63 64 # The cutoff for similarity here is pretty arbitrary. It should
64 65 # probably be investigated and tweaked.
65 66 return [s for s in symbols if sim(s) > 0.6]
66 67
67 68 def _reportsimilar(write, similar):
68 69 if len(similar) == 1:
69 70 write(_("(did you mean %s?)\n") % similar[0])
70 71 elif similar:
71 72 ss = ", ".join(sorted(similar))
72 73 write(_("(did you mean one of %s?)\n") % ss)
73 74
74 75 def _formatparse(write, inst):
75 76 similar = []
76 77 if isinstance(inst, error.UnknownIdentifier):
77 78 # make sure to check fileset first, as revset can invoke fileset
78 79 similar = _getsimilar(inst.symbols, inst.function)
79 80 if len(inst.args) > 1:
80 81 write(_("hg: parse error at %s: %s\n") %
81 82 (inst.args[1], inst.args[0]))
82 83 if (inst.args[0][0] == ' '):
83 84 write(_("unexpected leading whitespace\n"))
84 85 else:
85 86 write(_("hg: parse error: %s\n") % inst.args[0])
86 87 _reportsimilar(write, similar)
87 88 if inst.hint:
88 89 write(_("(%s)\n") % inst.hint)
89 90
90 91 def dispatch(req):
91 92 "run the command specified in req.args"
92 93 if req.ferr:
93 94 ferr = req.ferr
94 95 elif req.ui:
95 96 ferr = req.ui.ferr
96 97 else:
97 98 ferr = sys.stderr
98 99
99 100 try:
100 101 if not req.ui:
101 102 req.ui = uimod.ui()
102 103 if '--traceback' in req.args:
103 104 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
104 105
105 106 # set ui streams from the request
106 107 if req.fin:
107 108 req.ui.fin = req.fin
108 109 if req.fout:
109 110 req.ui.fout = req.fout
110 111 if req.ferr:
111 112 req.ui.ferr = req.ferr
112 113 except error.Abort as inst:
113 114 ferr.write(_("abort: %s\n") % inst)
114 115 if inst.hint:
115 116 ferr.write(_("(%s)\n") % inst.hint)
116 117 return -1
117 118 except error.ParseError as inst:
118 119 _formatparse(ferr.write, inst)
119 120 return -1
120 121
121 122 msg = ' '.join(' ' in a and repr(a) or a for a in req.args)
122 123 starttime = time.time()
123 124 ret = None
124 125 try:
125 126 ret = _runcatch(req)
126 127 except KeyboardInterrupt:
127 128 try:
128 129 req.ui.warn(_("interrupted!\n"))
129 130 except IOError as inst:
130 131 if inst.errno != errno.EPIPE:
131 132 raise
132 133 ret = -1
133 134 finally:
134 135 duration = time.time() - starttime
135 136 req.ui.flush()
136 137 req.ui.log("commandfinish", "%s exited %s after %0.2f seconds\n",
137 138 msg, ret or 0, duration)
138 139 return ret
139 140
140 141 def _runcatch(req):
141 142 def catchterm(*args):
142 143 raise error.SignalInterrupt
143 144
144 145 ui = req.ui
145 146 try:
146 147 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
147 148 num = getattr(signal, name, None)
148 149 if num:
149 150 signal.signal(num, catchterm)
150 151 except ValueError:
151 152 pass # happens if called in a thread
152 153
153 154 def _runcatchfunc():
154 155 try:
155 156 debugger = 'pdb'
156 157 debugtrace = {
157 158 'pdb' : pdb.set_trace
158 159 }
159 160 debugmortem = {
160 161 'pdb' : pdb.post_mortem
161 162 }
162 163
163 164 # read --config before doing anything else
164 165 # (e.g. to change trust settings for reading .hg/hgrc)
165 166 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
166 167
167 168 if req.repo:
168 169 # copy configs that were passed on the cmdline (--config) to
169 170 # the repo ui
170 171 for sec, name, val in cfgs:
171 172 req.repo.ui.setconfig(sec, name, val, source='--config')
172 173
173 174 # developer config: ui.debugger
174 175 debugger = ui.config("ui", "debugger")
175 176 debugmod = pdb
176 177 if not debugger or ui.plain():
177 178 # if we are in HGPLAIN mode, then disable custom debugging
178 179 debugger = 'pdb'
179 180 elif '--debugger' in req.args:
180 181 # This import can be slow for fancy debuggers, so only
181 182 # do it when absolutely necessary, i.e. when actual
182 183 # debugging has been requested
183 184 with demandimport.deactivated():
184 185 try:
185 186 debugmod = __import__(debugger)
186 187 except ImportError:
187 188 pass # Leave debugmod = pdb
188 189
189 190 debugtrace[debugger] = debugmod.set_trace
190 191 debugmortem[debugger] = debugmod.post_mortem
191 192
192 193 # enter the debugger before command execution
193 194 if '--debugger' in req.args:
194 195 ui.warn(_("entering debugger - "
195 196 "type c to continue starting hg or h for help\n"))
196 197
197 198 if (debugger != 'pdb' and
198 199 debugtrace[debugger] == debugtrace['pdb']):
199 200 ui.warn(_("%s debugger specified "
200 201 "but its module was not found\n") % debugger)
201 202 with demandimport.deactivated():
202 203 debugtrace[debugger]()
203 204 try:
204 205 return _dispatch(req)
205 206 finally:
206 207 ui.flush()
207 208 except: # re-raises
208 209 # enter the debugger when we hit an exception
209 210 if '--debugger' in req.args:
210 211 traceback.print_exc()
211 212 debugmortem[debugger](sys.exc_info()[2])
212 213 ui.traceback()
213 214 raise
214 215
215 216 return callcatch(ui, _runcatchfunc)
216 217
217 218 def callcatch(ui, func):
218 219 """call func() with global exception handling
219 220
220 221 return func() if no exception happens. otherwise do some error handling
221 222 and return an exit code accordingly.
222 223 """
223 224 try:
224 225 return func()
225 226 # Global exception handling, alphabetically
226 227 # Mercurial-specific first, followed by built-in and library exceptions
227 228 except error.AmbiguousCommand as inst:
228 229 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
229 230 (inst.args[0], " ".join(inst.args[1])))
230 231 except error.ParseError as inst:
231 232 _formatparse(ui.warn, inst)
232 233 return -1
233 234 except error.LockHeld as inst:
234 235 if inst.errno == errno.ETIMEDOUT:
235 236 reason = _('timed out waiting for lock held by %s') % inst.locker
236 237 else:
237 238 reason = _('lock held by %s') % inst.locker
238 239 ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
239 240 except error.LockUnavailable as inst:
240 241 ui.warn(_("abort: could not lock %s: %s\n") %
241 242 (inst.desc or inst.filename, inst.strerror))
242 243 except error.CommandError as inst:
243 244 if inst.args[0]:
244 245 ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
245 246 commands.help_(ui, inst.args[0], full=False, command=True)
246 247 else:
247 248 ui.warn(_("hg: %s\n") % inst.args[1])
248 249 commands.help_(ui, 'shortlist')
249 250 except error.OutOfBandError as inst:
250 251 if inst.args:
251 252 msg = _("abort: remote error:\n")
252 253 else:
253 254 msg = _("abort: remote error\n")
254 255 ui.warn(msg)
255 256 if inst.args:
256 257 ui.warn(''.join(inst.args))
257 258 if inst.hint:
258 259 ui.warn('(%s)\n' % inst.hint)
259 260 except error.RepoError as inst:
260 261 ui.warn(_("abort: %s!\n") % inst)
261 262 if inst.hint:
262 263 ui.warn(_("(%s)\n") % inst.hint)
263 264 except error.ResponseError as inst:
264 265 ui.warn(_("abort: %s") % inst.args[0])
265 266 if not isinstance(inst.args[1], basestring):
266 267 ui.warn(" %r\n" % (inst.args[1],))
267 268 elif not inst.args[1]:
268 269 ui.warn(_(" empty string\n"))
269 270 else:
270 271 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
271 272 except error.CensoredNodeError as inst:
272 273 ui.warn(_("abort: file censored %s!\n") % inst)
273 274 except error.RevlogError as inst:
274 275 ui.warn(_("abort: %s!\n") % inst)
275 276 except error.SignalInterrupt:
276 277 ui.warn(_("killed!\n"))
277 278 except error.UnknownCommand as inst:
278 279 ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
279 280 try:
280 281 # check if the command is in a disabled extension
281 282 # (but don't check for extensions themselves)
282 283 commands.help_(ui, inst.args[0], unknowncmd=True)
283 284 except (error.UnknownCommand, error.Abort):
284 285 suggested = False
285 286 if len(inst.args) == 2:
286 287 sim = _getsimilar(inst.args[1], inst.args[0])
287 288 if sim:
288 289 _reportsimilar(ui.warn, sim)
289 290 suggested = True
290 291 if not suggested:
291 292 commands.help_(ui, 'shortlist')
292 293 except error.InterventionRequired as inst:
293 294 ui.warn("%s\n" % inst)
294 295 if inst.hint:
295 296 ui.warn(_("(%s)\n") % inst.hint)
296 297 return 1
297 298 except error.Abort as inst:
298 299 ui.warn(_("abort: %s\n") % inst)
299 300 if inst.hint:
300 301 ui.warn(_("(%s)\n") % inst.hint)
301 302 except ImportError as inst:
302 303 ui.warn(_("abort: %s!\n") % inst)
303 304 m = str(inst).split()[-1]
304 305 if m in "mpatch bdiff".split():
305 306 ui.warn(_("(did you forget to compile extensions?)\n"))
306 307 elif m in "zlib".split():
307 308 ui.warn(_("(is your Python install correct?)\n"))
308 309 except IOError as inst:
309 310 if util.safehasattr(inst, "code"):
310 311 ui.warn(_("abort: %s\n") % inst)
311 312 elif util.safehasattr(inst, "reason"):
312 313 try: # usually it is in the form (errno, strerror)
313 314 reason = inst.reason.args[1]
314 315 except (AttributeError, IndexError):
315 316 # it might be anything, for example a string
316 317 reason = inst.reason
317 318 if isinstance(reason, unicode):
318 319 # SSLError of Python 2.7.9 contains a unicode
319 320 reason = reason.encode(encoding.encoding, 'replace')
320 321 ui.warn(_("abort: error: %s\n") % reason)
321 322 elif (util.safehasattr(inst, "args")
322 323 and inst.args and inst.args[0] == errno.EPIPE):
323 324 pass
324 325 elif getattr(inst, "strerror", None):
325 326 if getattr(inst, "filename", None):
326 327 ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
327 328 else:
328 329 ui.warn(_("abort: %s\n") % inst.strerror)
329 330 else:
330 331 raise
331 332 except OSError as inst:
332 333 if getattr(inst, "filename", None) is not None:
333 334 ui.warn(_("abort: %s: '%s'\n") % (inst.strerror, inst.filename))
334 335 else:
335 336 ui.warn(_("abort: %s\n") % inst.strerror)
336 337 except KeyboardInterrupt:
337 338 raise
338 339 except MemoryError:
339 340 ui.warn(_("abort: out of memory\n"))
340 341 except SystemExit as inst:
341 342 # Commands shouldn't sys.exit directly, but give a return code.
342 343 # Just in case catch this and and pass exit code to caller.
343 344 return inst.code
344 345 except socket.error as inst:
345 346 ui.warn(_("abort: %s\n") % inst.args[-1])
346 347 except: # perhaps re-raises
347 348 if not handlecommandexception(ui):
348 349 raise
349 350
350 351 return -1
351 352
352 353 def aliasargs(fn, givenargs):
353 354 args = getattr(fn, 'args', [])
354 355 if args:
355 356 cmd = ' '.join(map(util.shellquote, args))
356 357
357 358 nums = []
358 359 def replacer(m):
359 360 num = int(m.group(1)) - 1
360 361 nums.append(num)
361 362 if num < len(givenargs):
362 363 return givenargs[num]
363 364 raise error.Abort(_('too few arguments for command alias'))
364 365 cmd = re.sub(r'\$(\d+|\$)', replacer, cmd)
365 366 givenargs = [x for i, x in enumerate(givenargs)
366 367 if i not in nums]
367 368 args = shlex.split(cmd)
368 369 return args + givenargs
369 370
370 371 def aliasinterpolate(name, args, cmd):
371 372 '''interpolate args into cmd for shell aliases
372 373
373 374 This also handles $0, $@ and "$@".
374 375 '''
375 376 # util.interpolate can't deal with "$@" (with quotes) because it's only
376 377 # built to match prefix + patterns.
377 378 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
378 379 replacemap['$0'] = name
379 380 replacemap['$$'] = '$'
380 381 replacemap['$@'] = ' '.join(args)
381 382 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
382 383 # parameters, separated out into words. Emulate the same behavior here by
383 384 # quoting the arguments individually. POSIX shells will then typically
384 385 # tokenize each argument into exactly one word.
385 386 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
386 387 # escape '\$' for regex
387 388 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
388 389 r = re.compile(regex)
389 390 return r.sub(lambda x: replacemap[x.group()], cmd)
390 391
391 392 class cmdalias(object):
392 393 def __init__(self, name, definition, cmdtable, source):
393 394 self.name = self.cmd = name
394 395 self.cmdname = ''
395 396 self.definition = definition
396 397 self.fn = None
397 398 self.givenargs = []
398 399 self.opts = []
399 400 self.help = ''
400 401 self.badalias = None
401 402 self.unknowncmd = False
402 403 self.source = source
403 404
404 405 try:
405 406 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
406 407 for alias, e in cmdtable.iteritems():
407 408 if e is entry:
408 409 self.cmd = alias
409 410 break
410 411 self.shadows = True
411 412 except error.UnknownCommand:
412 413 self.shadows = False
413 414
414 415 if not self.definition:
415 416 self.badalias = _("no definition for alias '%s'") % self.name
416 417 return
417 418
418 419 if self.definition.startswith('!'):
419 420 self.shell = True
420 421 def fn(ui, *args):
421 422 env = {'HG_ARGS': ' '.join((self.name,) + args)}
422 423 def _checkvar(m):
423 424 if m.groups()[0] == '$':
424 425 return m.group()
425 426 elif int(m.groups()[0]) <= len(args):
426 427 return m.group()
427 428 else:
428 429 ui.debug("No argument found for substitution "
429 430 "of %i variable in alias '%s' definition."
430 431 % (int(m.groups()[0]), self.name))
431 432 return ''
432 433 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
433 434 cmd = aliasinterpolate(self.name, args, cmd)
434 435 return ui.system(cmd, environ=env)
435 436 self.fn = fn
436 437 return
437 438
438 439 try:
439 440 args = shlex.split(self.definition)
440 441 except ValueError as inst:
441 442 self.badalias = (_("error in definition for alias '%s': %s")
442 443 % (self.name, inst))
443 444 return
444 445 self.cmdname = cmd = args.pop(0)
445 446 self.givenargs = args
446 447
447 448 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
448 449 if _earlygetopt([invalidarg], args):
449 450 self.badalias = (_("error in definition for alias '%s': %s may "
450 451 "only be given on the command line")
451 452 % (self.name, invalidarg))
452 453 return
453 454
454 455 try:
455 456 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
456 457 if len(tableentry) > 2:
457 458 self.fn, self.opts, self.help = tableentry
458 459 else:
459 460 self.fn, self.opts = tableentry
460 461
461 462 if self.help.startswith("hg " + cmd):
462 463 # drop prefix in old-style help lines so hg shows the alias
463 464 self.help = self.help[4 + len(cmd):]
464 465 self.__doc__ = self.fn.__doc__
465 466
466 467 except error.UnknownCommand:
467 468 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
468 469 % (self.name, cmd))
469 470 self.unknowncmd = True
470 471 except error.AmbiguousCommand:
471 472 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
472 473 % (self.name, cmd))
473 474
474 475 @property
475 476 def args(self):
476 477 args = map(util.expandpath, self.givenargs)
477 478 return aliasargs(self.fn, args)
478 479
479 480 def __getattr__(self, name):
480 481 adefaults = {'norepo': True, 'optionalrepo': False, 'inferrepo': False}
481 482 if name not in adefaults:
482 483 raise AttributeError(name)
483 484 if self.badalias or util.safehasattr(self, 'shell'):
484 485 return adefaults[name]
485 486 return getattr(self.fn, name)
486 487
487 488 def __call__(self, ui, *args, **opts):
488 489 if self.badalias:
489 490 hint = None
490 491 if self.unknowncmd:
491 492 try:
492 493 # check if the command is in a disabled extension
493 494 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
494 495 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
495 496 except error.UnknownCommand:
496 497 pass
497 498 raise error.Abort(self.badalias, hint=hint)
498 499 if self.shadows:
499 500 ui.debug("alias '%s' shadows command '%s'\n" %
500 501 (self.name, self.cmdname))
501 502
502 503 if util.safehasattr(self, 'shell'):
503 504 return self.fn(ui, *args, **opts)
504 505 else:
505 506 try:
506 507 return util.checksignature(self.fn)(ui, *args, **opts)
507 508 except error.SignatureError:
508 509 args = ' '.join([self.cmdname] + self.args)
509 510 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
510 511 raise
511 512
512 513 def addaliases(ui, cmdtable):
513 514 # aliases are processed after extensions have been loaded, so they
514 515 # may use extension commands. Aliases can also use other alias definitions,
515 516 # but only if they have been defined prior to the current definition.
516 517 for alias, definition in ui.configitems('alias'):
517 518 source = ui.configsource('alias', alias)
518 519 aliasdef = cmdalias(alias, definition, cmdtable, source)
519 520
520 521 try:
521 522 olddef = cmdtable[aliasdef.cmd][0]
522 523 if olddef.definition == aliasdef.definition:
523 524 continue
524 525 except (KeyError, AttributeError):
525 526 # definition might not exist or it might not be a cmdalias
526 527 pass
527 528
528 529 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
529 530
530 531 def _parse(ui, args):
531 532 options = {}
532 533 cmdoptions = {}
533 534
534 535 try:
535 536 args = fancyopts.fancyopts(args, commands.globalopts, options)
536 537 except fancyopts.getopt.GetoptError as inst:
537 538 raise error.CommandError(None, inst)
538 539
539 540 if args:
540 541 cmd, args = args[0], args[1:]
541 542 aliases, entry = cmdutil.findcmd(cmd, commands.table,
542 543 ui.configbool("ui", "strict"))
543 544 cmd = aliases[0]
544 545 args = aliasargs(entry[0], args)
545 546 defaults = ui.config("defaults", cmd)
546 547 if defaults:
547 548 args = map(util.expandpath, shlex.split(defaults)) + args
548 549 c = list(entry[1])
549 550 else:
550 551 cmd = None
551 552 c = []
552 553
553 554 # combine global options into local
554 555 for o in commands.globalopts:
555 556 c.append((o[0], o[1], options[o[1]], o[3]))
556 557
557 558 try:
558 559 args = fancyopts.fancyopts(args, c, cmdoptions, True)
559 560 except fancyopts.getopt.GetoptError as inst:
560 561 raise error.CommandError(cmd, inst)
561 562
562 563 # separate global options back out
563 564 for o in commands.globalopts:
564 565 n = o[1]
565 566 options[n] = cmdoptions[n]
566 567 del cmdoptions[n]
567 568
568 569 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
569 570
570 571 def _parseconfig(ui, config):
571 572 """parse the --config options from the command line"""
572 573 configs = []
573 574
574 575 for cfg in config:
575 576 try:
576 577 name, value = [cfgelem.strip()
577 578 for cfgelem in cfg.split('=', 1)]
578 579 section, name = name.split('.', 1)
579 580 if not section or not name:
580 581 raise IndexError
581 582 ui.setconfig(section, name, value, '--config')
582 583 configs.append((section, name, value))
583 584 except (IndexError, ValueError):
584 585 raise error.Abort(_('malformed --config option: %r '
585 586 '(use --config section.name=value)') % cfg)
586 587
587 588 return configs
588 589
589 590 def _earlygetopt(aliases, args):
590 591 """Return list of values for an option (or aliases).
591 592
592 593 The values are listed in the order they appear in args.
593 594 The options and values are removed from args.
594 595
595 596 >>> args = ['x', '--cwd', 'foo', 'y']
596 597 >>> _earlygetopt(['--cwd'], args), args
597 598 (['foo'], ['x', 'y'])
598 599
599 600 >>> args = ['x', '--cwd=bar', 'y']
600 601 >>> _earlygetopt(['--cwd'], args), args
601 602 (['bar'], ['x', 'y'])
602 603
603 604 >>> args = ['x', '-R', 'foo', 'y']
604 605 >>> _earlygetopt(['-R'], args), args
605 606 (['foo'], ['x', 'y'])
606 607
607 608 >>> args = ['x', '-Rbar', 'y']
608 609 >>> _earlygetopt(['-R'], args), args
609 610 (['bar'], ['x', 'y'])
610 611 """
611 612 try:
612 613 argcount = args.index("--")
613 614 except ValueError:
614 615 argcount = len(args)
615 616 shortopts = [opt for opt in aliases if len(opt) == 2]
616 617 values = []
617 618 pos = 0
618 619 while pos < argcount:
619 620 fullarg = arg = args[pos]
620 621 equals = arg.find('=')
621 622 if equals > -1:
622 623 arg = arg[:equals]
623 624 if arg in aliases:
624 625 del args[pos]
625 626 if equals > -1:
626 627 values.append(fullarg[equals + 1:])
627 628 argcount -= 1
628 629 else:
629 630 if pos + 1 >= argcount:
630 631 # ignore and let getopt report an error if there is no value
631 632 break
632 633 values.append(args.pop(pos))
633 634 argcount -= 2
634 635 elif arg[:2] in shortopts:
635 636 # short option can have no following space, e.g. hg log -Rfoo
636 637 values.append(args.pop(pos)[2:])
637 638 argcount -= 1
638 639 else:
639 640 pos += 1
640 641 return values
641 642
642 643 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
643 644 # run pre-hook, and abort if it fails
644 645 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
645 646 pats=cmdpats, opts=cmdoptions)
646 647 try:
647 648 ret = _runcommand(ui, options, cmd, d)
648 649 # run post-hook, passing command result
649 650 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
650 651 result=ret, pats=cmdpats, opts=cmdoptions)
651 652 except Exception:
652 653 # run failure hook and re-raise
653 654 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
654 655 pats=cmdpats, opts=cmdoptions)
655 656 raise
656 657 return ret
657 658
658 659 def _getlocal(ui, rpath, wd=None):
659 660 """Return (path, local ui object) for the given target path.
660 661
661 662 Takes paths in [cwd]/.hg/hgrc into account."
662 663 """
663 664 if wd is None:
664 665 try:
665 666 wd = os.getcwd()
666 667 except OSError as e:
667 668 raise error.Abort(_("error getting current working directory: %s") %
668 669 e.strerror)
669 670 path = cmdutil.findrepo(wd) or ""
670 671 if not path:
671 672 lui = ui
672 673 else:
673 674 lui = ui.copy()
674 675 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
675 676
676 677 if rpath and rpath[-1]:
677 678 path = lui.expandpath(rpath[-1])
678 679 lui = ui.copy()
679 680 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
680 681
681 682 return path, lui
682 683
683 684 def _checkshellalias(lui, ui, args):
684 685 """Return the function to run the shell alias, if it is required"""
685 686 options = {}
686 687
687 688 try:
688 689 args = fancyopts.fancyopts(args, commands.globalopts, options)
689 690 except fancyopts.getopt.GetoptError:
690 691 return
691 692
692 693 if not args:
693 694 return
694 695
695 696 cmdtable = commands.table
696 697
697 698 cmd = args[0]
698 699 try:
699 700 strict = ui.configbool("ui", "strict")
700 701 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
701 702 except (error.AmbiguousCommand, error.UnknownCommand):
702 703 return
703 704
704 705 cmd = aliases[0]
705 706 fn = entry[0]
706 707
707 708 if cmd and util.safehasattr(fn, 'shell'):
708 709 d = lambda: fn(ui, *args[1:])
709 710 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
710 711 [], {})
711 712
712 713 def _cmdattr(ui, cmd, func, attr):
713 714 try:
714 715 return getattr(func, attr)
715 716 except AttributeError:
716 717 ui.deprecwarn("missing attribute '%s', use @command decorator "
717 718 "to register '%s'" % (attr, cmd), '3.8')
718 719 return False
719 720
720 721 _loaded = set()
721 722
722 723 # list of (objname, loadermod, loadername) tuple:
723 724 # - objname is the name of an object in extension module, from which
724 725 # extra information is loaded
725 726 # - loadermod is the module where loader is placed
726 727 # - loadername is the name of the function, which takes (ui, extensionname,
727 728 # extraobj) arguments
728 729 extraloaders = [
729 730 ('cmdtable', commands, 'loadcmdtable'),
730 731 ('filesetpredicate', fileset, 'loadpredicate'),
731 732 ('revsetpredicate', revset, 'loadpredicate'),
732 733 ('templatefilter', templatefilters, 'loadfilter'),
733 734 ('templatefunc', templater, 'loadfunction'),
734 735 ('templatekeyword', templatekw, 'loadkeyword'),
735 736 ]
736 737
737 738 def _dispatch(req):
738 739 args = req.args
739 740 ui = req.ui
740 741
741 742 # check for cwd
742 743 cwd = _earlygetopt(['--cwd'], args)
743 744 if cwd:
744 745 os.chdir(cwd[-1])
745 746
746 747 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
747 748 path, lui = _getlocal(ui, rpath)
748 749
749 750 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
750 751 # reposetup. Programs like TortoiseHg will call _dispatch several
751 752 # times so we keep track of configured extensions in _loaded.
752 753 extensions.loadall(lui)
753 754 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
754 755 # Propagate any changes to lui.__class__ by extensions
755 756 ui.__class__ = lui.__class__
756 757
757 758 # (uisetup and extsetup are handled in extensions.loadall)
758 759
759 760 for name, module in exts:
760 761 for objname, loadermod, loadername in extraloaders:
761 762 extraobj = getattr(module, objname, None)
762 763 if extraobj is not None:
763 764 getattr(loadermod, loadername)(ui, name, extraobj)
764 765 _loaded.add(name)
765 766
766 767 # (reposetup is handled in hg.repository)
767 768
768 769 addaliases(lui, commands.table)
769 770
770 771 # All aliases and commands are completely defined, now.
771 772 # Check abbreviation/ambiguity of shell alias.
772 773 shellaliasfn = _checkshellalias(lui, ui, args)
773 774 if shellaliasfn:
774 775 return shellaliasfn()
775 776
776 777 # check for fallback encoding
777 778 fallback = lui.config('ui', 'fallbackencoding')
778 779 if fallback:
779 780 encoding.fallbackencoding = fallback
780 781
781 782 fullargs = args
782 783 cmd, func, args, options, cmdoptions = _parse(lui, args)
783 784
784 785 if options["config"]:
785 786 raise error.Abort(_("option --config may not be abbreviated!"))
786 787 if options["cwd"]:
787 788 raise error.Abort(_("option --cwd may not be abbreviated!"))
788 789 if options["repository"]:
789 790 raise error.Abort(_(
790 791 "option -R has to be separated from other options (e.g. not -qR) "
791 792 "and --repository may only be abbreviated as --repo!"))
792 793
793 794 if options["encoding"]:
794 795 encoding.encoding = options["encoding"]
795 796 if options["encodingmode"]:
796 797 encoding.encodingmode = options["encodingmode"]
797 798 if options["time"]:
798 799 def get_times():
799 800 t = os.times()
800 801 if t[4] == 0.0: # Windows leaves this as zero, so use time.clock()
801 802 t = (t[0], t[1], t[2], t[3], time.clock())
802 803 return t
803 804 s = get_times()
804 805 def print_time():
805 806 t = get_times()
806 807 ui.warn(_("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
807 808 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
808 809 atexit.register(print_time)
809 810
810 811 uis = set([ui, lui])
811 812
812 813 if req.repo:
813 814 uis.add(req.repo.ui)
814 815
815 816 if options['verbose'] or options['debug'] or options['quiet']:
816 817 for opt in ('verbose', 'debug', 'quiet'):
817 818 val = str(bool(options[opt]))
818 819 for ui_ in uis:
819 820 ui_.setconfig('ui', opt, val, '--' + opt)
820 821
821 822 if options['traceback']:
822 823 for ui_ in uis:
823 824 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
824 825
825 826 if options['noninteractive']:
826 827 for ui_ in uis:
827 828 ui_.setconfig('ui', 'interactive', 'off', '-y')
828 829
829 830 if cmdoptions.get('insecure', False):
830 831 for ui_ in uis:
831 832 ui_.insecureconnections = True
832 833
833 834 if options['version']:
834 835 return commands.version_(ui)
835 836 if options['help']:
836 837 return commands.help_(ui, cmd, command=cmd is not None)
837 838 elif not cmd:
838 839 return commands.help_(ui, 'shortlist')
839 840
840 841 repo = None
841 842 cmdpats = args[:]
842 843 if not _cmdattr(ui, cmd, func, 'norepo'):
843 844 # use the repo from the request only if we don't have -R
844 845 if not rpath and not cwd:
845 846 repo = req.repo
846 847
847 848 if repo:
848 849 # set the descriptors of the repo ui to those of ui
849 850 repo.ui.fin = ui.fin
850 851 repo.ui.fout = ui.fout
851 852 repo.ui.ferr = ui.ferr
852 853 else:
853 854 try:
854 855 repo = hg.repository(ui, path=path)
855 856 if not repo.local():
856 857 raise error.Abort(_("repository '%s' is not local") % path)
857 858 repo.ui.setconfig("bundle", "mainreporoot", repo.root, 'repo')
858 859 except error.RequirementError:
859 860 raise
860 861 except error.RepoError:
861 862 if rpath and rpath[-1]: # invalid -R path
862 863 raise
863 864 if not _cmdattr(ui, cmd, func, 'optionalrepo'):
864 865 if (_cmdattr(ui, cmd, func, 'inferrepo') and
865 866 args and not path):
866 867 # try to infer -R from command args
867 868 repos = map(cmdutil.findrepo, args)
868 869 guess = repos[0]
869 870 if guess and repos.count(guess) == len(repos):
870 871 req.args = ['--repository', guess] + fullargs
871 872 return _dispatch(req)
872 873 if not path:
873 874 raise error.RepoError(_("no repository found in '%s'"
874 875 " (.hg not found)")
875 876 % os.getcwd())
876 877 raise
877 878 if repo:
878 879 ui = repo.ui
879 880 if options['hidden']:
880 881 repo = repo.unfiltered()
881 882 args.insert(0, repo)
882 883 elif rpath:
883 884 ui.warn(_("warning: --repository ignored\n"))
884 885
885 886 msg = ' '.join(' ' in a and repr(a) or a for a in fullargs)
886 887 ui.log("command", '%s\n', msg)
887 888 d = lambda: util.checksignature(func)(ui, *args, **cmdoptions)
888 889 try:
889 890 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
890 891 cmdpats, cmdoptions)
891 892 finally:
892 893 if repo and repo != req.repo:
893 894 repo.close()
894 895
895 def lsprofile(ui, func, fp):
896 format = ui.config('profiling', 'format', default='text')
897 field = ui.config('profiling', 'sort', default='inlinetime')
898 limit = ui.configint('profiling', 'limit', default=30)
899 climit = ui.configint('profiling', 'nested', default=0)
900
901 if format not in ['text', 'kcachegrind']:
902 ui.warn(_("unrecognized profiling format '%s'"
903 " - Ignored\n") % format)
904 format = 'text'
905
906 try:
907 from . import lsprof
908 except ImportError:
909 raise error.Abort(_(
910 'lsprof not available - install from '
911 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
912 p = lsprof.Profiler()
913 p.enable(subcalls=True)
914 try:
915 return func()
916 finally:
917 p.disable()
918
919 if format == 'kcachegrind':
920 from . import lsprofcalltree
921 calltree = lsprofcalltree.KCacheGrind(p)
922 calltree.output(fp)
923 else:
924 # format == 'text'
925 stats = lsprof.Stats(p.getstats())
926 stats.sort(field)
927 stats.pprint(limit=limit, file=fp, climit=climit)
928
929 def flameprofile(ui, func, fp):
930 try:
931 from flamegraph import flamegraph
932 except ImportError:
933 raise error.Abort(_(
934 'flamegraph not available - install from '
935 'https://github.com/evanhempel/python-flamegraph'))
936 # developer config: profiling.freq
937 freq = ui.configint('profiling', 'freq', default=1000)
938 filter_ = None
939 collapse_recursion = True
940 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
941 filter_, collapse_recursion)
942 start_time = time.clock()
943 try:
944 thread.start()
945 func()
946 finally:
947 thread.stop()
948 thread.join()
949 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
950 time.clock() - start_time, thread.num_frames(),
951 thread.num_frames(unique=True)))
952
953
954 def statprofile(ui, func, fp):
955 try:
956 import statprof
957 except ImportError:
958 raise error.Abort(_(
959 'statprof not available - install using "easy_install statprof"'))
960
961 freq = ui.configint('profiling', 'freq', default=1000)
962 if freq > 0:
963 statprof.reset(freq)
964 else:
965 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
966
967 statprof.start()
968 try:
969 return func()
970 finally:
971 statprof.stop()
972 statprof.display(fp)
973
974 896 def _runcommand(ui, options, cmd, cmdfunc):
975 897 """Enables the profiler if applicable.
976 898
977 899 ``profiling.enabled`` - boolean config that enables or disables profiling
978 900 """
979 901 def checkargs():
980 902 try:
981 903 return cmdfunc()
982 904 except error.SignatureError:
983 905 raise error.CommandError(cmd, _("invalid arguments"))
984 906
985 907 if options['profile'] or ui.configbool('profiling', 'enabled'):
986 profiler = os.getenv('HGPROF')
987 if profiler is None:
988 profiler = ui.config('profiling', 'type', default='ls')
989 if profiler not in ('ls', 'stat', 'flame'):
990 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
991 profiler = 'ls'
992
993 output = ui.config('profiling', 'output')
994
995 if output == 'blackbox':
996 fp = util.stringio()
997 elif output:
998 path = ui.expandpath(output)
999 fp = open(path, 'wb')
1000 else:
1001 fp = sys.stderr
1002
1003 try:
1004 if profiler == 'ls':
1005 return lsprofile(ui, checkargs, fp)
1006 elif profiler == 'flame':
1007 return flameprofile(ui, checkargs, fp)
1008 else:
1009 return statprofile(ui, checkargs, fp)
1010 finally:
1011 if output:
1012 if output == 'blackbox':
1013 val = "Profile:\n%s" % fp.getvalue()
1014 # ui.log treats the input as a format string,
1015 # so we need to escape any % signs.
1016 val = val.replace('%', '%%')
1017 ui.log('profile', val)
1018 fp.close()
908 return profiling.profile(ui, checkargs)
1019 909 else:
1020 910 return checkargs()
1021 911
1022 912 def _exceptionwarning(ui):
1023 913 """Produce a warning message for the current active exception"""
1024 914
1025 915 # For compatibility checking, we discard the portion of the hg
1026 916 # version after the + on the assumption that if a "normal
1027 917 # user" is running a build with a + in it the packager
1028 918 # probably built from fairly close to a tag and anyone with a
1029 919 # 'make local' copy of hg (where the version number can be out
1030 920 # of date) will be clueful enough to notice the implausible
1031 921 # version number and try updating.
1032 922 ct = util.versiontuple(n=2)
1033 923 worst = None, ct, ''
1034 924 if ui.config('ui', 'supportcontact', None) is None:
1035 925 for name, mod in extensions.extensions():
1036 926 testedwith = getattr(mod, 'testedwith', '')
1037 927 report = getattr(mod, 'buglink', _('the extension author.'))
1038 928 if not testedwith.strip():
1039 929 # We found an untested extension. It's likely the culprit.
1040 930 worst = name, 'unknown', report
1041 931 break
1042 932
1043 933 # Never blame on extensions bundled with Mercurial.
1044 934 if testedwith == 'internal':
1045 935 continue
1046 936
1047 937 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1048 938 if ct in tested:
1049 939 continue
1050 940
1051 941 lower = [t for t in tested if t < ct]
1052 942 nearest = max(lower or tested)
1053 943 if worst[0] is None or nearest < worst[1]:
1054 944 worst = name, nearest, report
1055 945 if worst[0] is not None:
1056 946 name, testedwith, report = worst
1057 947 if not isinstance(testedwith, str):
1058 948 testedwith = '.'.join([str(c) for c in testedwith])
1059 949 warning = (_('** Unknown exception encountered with '
1060 950 'possibly-broken third-party extension %s\n'
1061 951 '** which supports versions %s of Mercurial.\n'
1062 952 '** Please disable %s and try your action again.\n'
1063 953 '** If that fixes the bug please report it to %s\n')
1064 954 % (name, testedwith, name, report))
1065 955 else:
1066 956 bugtracker = ui.config('ui', 'supportcontact', None)
1067 957 if bugtracker is None:
1068 958 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1069 959 warning = (_("** unknown exception encountered, "
1070 960 "please report by visiting\n** ") + bugtracker + '\n')
1071 961 warning += ((_("** Python %s\n") % sys.version.replace('\n', '')) +
1072 962 (_("** Mercurial Distributed SCM (version %s)\n") %
1073 963 util.version()) +
1074 964 (_("** Extensions loaded: %s\n") %
1075 965 ", ".join([x[0] for x in extensions.extensions()])))
1076 966 return warning
1077 967
1078 968 def handlecommandexception(ui):
1079 969 """Produce a warning message for broken commands
1080 970
1081 971 Called when handling an exception; the exception is reraised if
1082 972 this function returns False, ignored otherwise.
1083 973 """
1084 974 warning = _exceptionwarning(ui)
1085 975 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
1086 976 ui.warn(warning)
1087 977 return False # re-raise the exception
General Comments 0
You need to be logged in to leave comments. Login now