##// END OF EJS Templates
tracing: trace command function execution...
Boris Feld -
r39548:ec0a2601 default
parent child Browse files
Show More
@@ -1,1069 +1,1070 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 difflib
11 11 import errno
12 12 import getopt
13 13 import os
14 14 import pdb
15 15 import re
16 16 import signal
17 17 import sys
18 18 import time
19 19 import traceback
20 20
21 21
22 22 from .i18n import _
23 23
24 24 from hgdemandimport import tracing
25 25
26 26 from . import (
27 27 cmdutil,
28 28 color,
29 29 commands,
30 30 demandimport,
31 31 encoding,
32 32 error,
33 33 extensions,
34 34 fancyopts,
35 35 help,
36 36 hg,
37 37 hook,
38 38 profiling,
39 39 pycompat,
40 40 scmutil,
41 41 ui as uimod,
42 42 util,
43 43 )
44 44
45 45 from .utils import (
46 46 procutil,
47 47 stringutil,
48 48 )
49 49
50 50 class request(object):
51 51 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
52 52 ferr=None, prereposetups=None):
53 53 self.args = args
54 54 self.ui = ui
55 55 self.repo = repo
56 56
57 57 # input/output/error streams
58 58 self.fin = fin
59 59 self.fout = fout
60 60 self.ferr = ferr
61 61
62 62 # remember options pre-parsed by _earlyparseopts()
63 63 self.earlyoptions = {}
64 64
65 65 # reposetups which run before extensions, useful for chg to pre-fill
66 66 # low-level repo state (for example, changelog) before extensions.
67 67 self.prereposetups = prereposetups or []
68 68
69 69 def _runexithandlers(self):
70 70 exc = None
71 71 handlers = self.ui._exithandlers
72 72 try:
73 73 while handlers:
74 74 func, args, kwargs = handlers.pop()
75 75 try:
76 76 func(*args, **kwargs)
77 77 except: # re-raises below
78 78 if exc is None:
79 79 exc = sys.exc_info()[1]
80 80 self.ui.warn(('error in exit handlers:\n'))
81 81 self.ui.traceback(force=True)
82 82 finally:
83 83 if exc is not None:
84 84 raise exc
85 85
86 86 def run():
87 87 "run the command in sys.argv"
88 88 initstdio()
89 89 with tracing.log('parse args into request'):
90 90 req = request(pycompat.sysargv[1:])
91 91 err = None
92 92 try:
93 93 status = dispatch(req)
94 94 except error.StdioError as e:
95 95 err = e
96 96 status = -1
97 97
98 98 # In all cases we try to flush stdio streams.
99 99 if util.safehasattr(req.ui, 'fout'):
100 100 try:
101 101 req.ui.fout.flush()
102 102 except IOError as e:
103 103 err = e
104 104 status = -1
105 105
106 106 if util.safehasattr(req.ui, 'ferr'):
107 107 try:
108 108 if err is not None and err.errno != errno.EPIPE:
109 109 req.ui.ferr.write('abort: %s\n' %
110 110 encoding.strtolocal(err.strerror))
111 111 req.ui.ferr.flush()
112 112 # There's not much we can do about an I/O error here. So (possibly)
113 113 # change the status code and move on.
114 114 except IOError:
115 115 status = -1
116 116
117 117 _silencestdio()
118 118 sys.exit(status & 255)
119 119
120 120 if pycompat.ispy3:
121 121 def initstdio():
122 122 pass
123 123
124 124 def _silencestdio():
125 125 for fp in (sys.stdout, sys.stderr):
126 126 # Check if the file is okay
127 127 try:
128 128 fp.flush()
129 129 continue
130 130 except IOError:
131 131 pass
132 132 # Otherwise mark it as closed to silence "Exception ignored in"
133 133 # message emitted by the interpreter finalizer. Be careful to
134 134 # not close procutil.stdout, which may be a fdopen-ed file object
135 135 # and its close() actually closes the underlying file descriptor.
136 136 try:
137 137 fp.close()
138 138 except IOError:
139 139 pass
140 140 else:
141 141 def initstdio():
142 142 for fp in (sys.stdin, sys.stdout, sys.stderr):
143 143 procutil.setbinary(fp)
144 144
145 145 def _silencestdio():
146 146 pass
147 147
148 148 def _getsimilar(symbols, value):
149 149 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
150 150 # The cutoff for similarity here is pretty arbitrary. It should
151 151 # probably be investigated and tweaked.
152 152 return [s for s in symbols if sim(s) > 0.6]
153 153
154 154 def _reportsimilar(write, similar):
155 155 if len(similar) == 1:
156 156 write(_("(did you mean %s?)\n") % similar[0])
157 157 elif similar:
158 158 ss = ", ".join(sorted(similar))
159 159 write(_("(did you mean one of %s?)\n") % ss)
160 160
161 161 def _formatparse(write, inst):
162 162 similar = []
163 163 if isinstance(inst, error.UnknownIdentifier):
164 164 # make sure to check fileset first, as revset can invoke fileset
165 165 similar = _getsimilar(inst.symbols, inst.function)
166 166 if len(inst.args) > 1:
167 167 write(_("hg: parse error at %s: %s\n") %
168 168 (pycompat.bytestr(inst.args[1]), inst.args[0]))
169 169 if inst.args[0].startswith(' '):
170 170 write(_("unexpected leading whitespace\n"))
171 171 else:
172 172 write(_("hg: parse error: %s\n") % inst.args[0])
173 173 _reportsimilar(write, similar)
174 174 if inst.hint:
175 175 write(_("(%s)\n") % inst.hint)
176 176
177 177 def _formatargs(args):
178 178 return ' '.join(procutil.shellquote(a) for a in args)
179 179
180 180 def dispatch(req):
181 181 """run the command specified in req.args; returns an integer status code"""
182 182 with tracing.log('dispatch.dispatch'):
183 183 if req.ferr:
184 184 ferr = req.ferr
185 185 elif req.ui:
186 186 ferr = req.ui.ferr
187 187 else:
188 188 ferr = procutil.stderr
189 189
190 190 try:
191 191 if not req.ui:
192 192 req.ui = uimod.ui.load()
193 193 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
194 194 if req.earlyoptions['traceback']:
195 195 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
196 196
197 197 # set ui streams from the request
198 198 if req.fin:
199 199 req.ui.fin = req.fin
200 200 if req.fout:
201 201 req.ui.fout = req.fout
202 202 if req.ferr:
203 203 req.ui.ferr = req.ferr
204 204 except error.Abort as inst:
205 205 ferr.write(_("abort: %s\n") % inst)
206 206 if inst.hint:
207 207 ferr.write(_("(%s)\n") % inst.hint)
208 208 return -1
209 209 except error.ParseError as inst:
210 210 _formatparse(ferr.write, inst)
211 211 return -1
212 212
213 213 msg = _formatargs(req.args)
214 214 starttime = util.timer()
215 215 ret = 1 # default of Python exit code on unhandled exception
216 216 try:
217 217 ret = _runcatch(req) or 0
218 218 except error.ProgrammingError as inst:
219 219 req.ui.error(_('** ProgrammingError: %s\n') % inst)
220 220 if inst.hint:
221 221 req.ui.error(_('** (%s)\n') % inst.hint)
222 222 raise
223 223 except KeyboardInterrupt as inst:
224 224 try:
225 225 if isinstance(inst, error.SignalInterrupt):
226 226 msg = _("killed!\n")
227 227 else:
228 228 msg = _("interrupted!\n")
229 229 req.ui.error(msg)
230 230 except error.SignalInterrupt:
231 231 # maybe pager would quit without consuming all the output, and
232 232 # SIGPIPE was raised. we cannot print anything in this case.
233 233 pass
234 234 except IOError as inst:
235 235 if inst.errno != errno.EPIPE:
236 236 raise
237 237 ret = -1
238 238 finally:
239 239 duration = util.timer() - starttime
240 240 req.ui.flush()
241 241 if req.ui.logblockedtimes:
242 242 req.ui._blockedtimes['command_duration'] = duration * 1000
243 243 req.ui.log('uiblocked', 'ui blocked ms',
244 244 **pycompat.strkwargs(req.ui._blockedtimes))
245 245 req.ui.log("commandfinish", "%s exited %d after %0.2f seconds\n",
246 246 msg, ret & 255, duration)
247 247 try:
248 248 req._runexithandlers()
249 249 except: # exiting, so no re-raises
250 250 ret = ret or -1
251 251 return ret
252 252
253 253 def _runcatch(req):
254 254 with tracing.log('dispatch._runcatch'):
255 255 def catchterm(*args):
256 256 raise error.SignalInterrupt
257 257
258 258 ui = req.ui
259 259 try:
260 260 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
261 261 num = getattr(signal, name, None)
262 262 if num:
263 263 signal.signal(num, catchterm)
264 264 except ValueError:
265 265 pass # happens if called in a thread
266 266
267 267 def _runcatchfunc():
268 268 realcmd = None
269 269 try:
270 270 cmdargs = fancyopts.fancyopts(
271 271 req.args[:], commands.globalopts, {})
272 272 cmd = cmdargs[0]
273 273 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
274 274 realcmd = aliases[0]
275 275 except (error.UnknownCommand, error.AmbiguousCommand,
276 276 IndexError, getopt.GetoptError):
277 277 # Don't handle this here. We know the command is
278 278 # invalid, but all we're worried about for now is that
279 279 # it's not a command that server operators expect to
280 280 # be safe to offer to users in a sandbox.
281 281 pass
282 282 if realcmd == 'serve' and '--stdio' in cmdargs:
283 283 # We want to constrain 'hg serve --stdio' instances pretty
284 284 # closely, as many shared-ssh access tools want to grant
285 285 # access to run *only* 'hg -R $repo serve --stdio'. We
286 286 # restrict to exactly that set of arguments, and prohibit
287 287 # any repo name that starts with '--' to prevent
288 288 # shenanigans wherein a user does something like pass
289 289 # --debugger or --config=ui.debugger=1 as a repo
290 290 # name. This used to actually run the debugger.
291 291 if (len(req.args) != 4 or
292 292 req.args[0] != '-R' or
293 293 req.args[1].startswith('--') or
294 294 req.args[2] != 'serve' or
295 295 req.args[3] != '--stdio'):
296 296 raise error.Abort(
297 297 _('potentially unsafe serve --stdio invocation: %s') %
298 298 (stringutil.pprint(req.args),))
299 299
300 300 try:
301 301 debugger = 'pdb'
302 302 debugtrace = {
303 303 'pdb': pdb.set_trace
304 304 }
305 305 debugmortem = {
306 306 'pdb': pdb.post_mortem
307 307 }
308 308
309 309 # read --config before doing anything else
310 310 # (e.g. to change trust settings for reading .hg/hgrc)
311 311 cfgs = _parseconfig(req.ui, req.earlyoptions['config'])
312 312
313 313 if req.repo:
314 314 # copy configs that were passed on the cmdline (--config) to
315 315 # the repo ui
316 316 for sec, name, val in cfgs:
317 317 req.repo.ui.setconfig(sec, name, val, source='--config')
318 318
319 319 # developer config: ui.debugger
320 320 debugger = ui.config("ui", "debugger")
321 321 debugmod = pdb
322 322 if not debugger or ui.plain():
323 323 # if we are in HGPLAIN mode, then disable custom debugging
324 324 debugger = 'pdb'
325 325 elif req.earlyoptions['debugger']:
326 326 # This import can be slow for fancy debuggers, so only
327 327 # do it when absolutely necessary, i.e. when actual
328 328 # debugging has been requested
329 329 with demandimport.deactivated():
330 330 try:
331 331 debugmod = __import__(debugger)
332 332 except ImportError:
333 333 pass # Leave debugmod = pdb
334 334
335 335 debugtrace[debugger] = debugmod.set_trace
336 336 debugmortem[debugger] = debugmod.post_mortem
337 337
338 338 # enter the debugger before command execution
339 339 if req.earlyoptions['debugger']:
340 340 ui.warn(_("entering debugger - "
341 341 "type c to continue starting hg or h for help\n"))
342 342
343 343 if (debugger != 'pdb' and
344 344 debugtrace[debugger] == debugtrace['pdb']):
345 345 ui.warn(_("%s debugger specified "
346 346 "but its module was not found\n") % debugger)
347 347 with demandimport.deactivated():
348 348 debugtrace[debugger]()
349 349 try:
350 350 return _dispatch(req)
351 351 finally:
352 352 ui.flush()
353 353 except: # re-raises
354 354 # enter the debugger when we hit an exception
355 355 if req.earlyoptions['debugger']:
356 356 traceback.print_exc()
357 357 debugmortem[debugger](sys.exc_info()[2])
358 358 raise
359 359 return _callcatch(ui, _runcatchfunc)
360 360
361 361 def _callcatch(ui, func):
362 362 """like scmutil.callcatch but handles more high-level exceptions about
363 363 config parsing and commands. besides, use handlecommandexception to handle
364 364 uncaught exceptions.
365 365 """
366 366 try:
367 367 return scmutil.callcatch(ui, func)
368 368 except error.AmbiguousCommand as inst:
369 369 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
370 370 (inst.args[0], " ".join(inst.args[1])))
371 371 except error.CommandError as inst:
372 372 if inst.args[0]:
373 373 ui.pager('help')
374 374 msgbytes = pycompat.bytestr(inst.args[1])
375 375 ui.warn(_("hg %s: %s\n") % (inst.args[0], msgbytes))
376 376 commands.help_(ui, inst.args[0], full=False, command=True)
377 377 else:
378 378 ui.warn(_("hg: %s\n") % inst.args[1])
379 379 ui.warn(_("(use 'hg help -v' for a list of global options)\n"))
380 380 except error.ParseError as inst:
381 381 _formatparse(ui.warn, inst)
382 382 return -1
383 383 except error.UnknownCommand as inst:
384 384 nocmdmsg = _("hg: unknown command '%s'\n") % inst.args[0]
385 385 try:
386 386 # check if the command is in a disabled extension
387 387 # (but don't check for extensions themselves)
388 388 formatted = help.formattedhelp(ui, commands, inst.args[0],
389 389 unknowncmd=True)
390 390 ui.warn(nocmdmsg)
391 391 ui.write(formatted)
392 392 except (error.UnknownCommand, error.Abort):
393 393 suggested = False
394 394 if len(inst.args) == 2:
395 395 sim = _getsimilar(inst.args[1], inst.args[0])
396 396 if sim:
397 397 ui.warn(nocmdmsg)
398 398 _reportsimilar(ui.warn, sim)
399 399 suggested = True
400 400 if not suggested:
401 401 ui.warn(nocmdmsg)
402 402 ui.warn(_("(use 'hg help' for a list of commands)\n"))
403 403 except IOError:
404 404 raise
405 405 except KeyboardInterrupt:
406 406 raise
407 407 except: # probably re-raises
408 408 if not handlecommandexception(ui):
409 409 raise
410 410
411 411 return -1
412 412
413 413 def aliasargs(fn, givenargs):
414 414 args = []
415 415 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
416 416 if not util.safehasattr(fn, '_origfunc'):
417 417 args = getattr(fn, 'args', args)
418 418 if args:
419 419 cmd = ' '.join(map(procutil.shellquote, args))
420 420
421 421 nums = []
422 422 def replacer(m):
423 423 num = int(m.group(1)) - 1
424 424 nums.append(num)
425 425 if num < len(givenargs):
426 426 return givenargs[num]
427 427 raise error.Abort(_('too few arguments for command alias'))
428 428 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
429 429 givenargs = [x for i, x in enumerate(givenargs)
430 430 if i not in nums]
431 431 args = pycompat.shlexsplit(cmd)
432 432 return args + givenargs
433 433
434 434 def aliasinterpolate(name, args, cmd):
435 435 '''interpolate args into cmd for shell aliases
436 436
437 437 This also handles $0, $@ and "$@".
438 438 '''
439 439 # util.interpolate can't deal with "$@" (with quotes) because it's only
440 440 # built to match prefix + patterns.
441 441 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
442 442 replacemap['$0'] = name
443 443 replacemap['$$'] = '$'
444 444 replacemap['$@'] = ' '.join(args)
445 445 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
446 446 # parameters, separated out into words. Emulate the same behavior here by
447 447 # quoting the arguments individually. POSIX shells will then typically
448 448 # tokenize each argument into exactly one word.
449 449 replacemap['"$@"'] = ' '.join(procutil.shellquote(arg) for arg in args)
450 450 # escape '\$' for regex
451 451 regex = '|'.join(replacemap.keys()).replace('$', br'\$')
452 452 r = re.compile(regex)
453 453 return r.sub(lambda x: replacemap[x.group()], cmd)
454 454
455 455 class cmdalias(object):
456 456 def __init__(self, ui, name, definition, cmdtable, source):
457 457 self.name = self.cmd = name
458 458 self.cmdname = ''
459 459 self.definition = definition
460 460 self.fn = None
461 461 self.givenargs = []
462 462 self.opts = []
463 463 self.help = ''
464 464 self.badalias = None
465 465 self.unknowncmd = False
466 466 self.source = source
467 467
468 468 try:
469 469 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
470 470 for alias, e in cmdtable.iteritems():
471 471 if e is entry:
472 472 self.cmd = alias
473 473 break
474 474 self.shadows = True
475 475 except error.UnknownCommand:
476 476 self.shadows = False
477 477
478 478 if not self.definition:
479 479 self.badalias = _("no definition for alias '%s'") % self.name
480 480 return
481 481
482 482 if self.definition.startswith('!'):
483 483 shdef = self.definition[1:]
484 484 self.shell = True
485 485 def fn(ui, *args):
486 486 env = {'HG_ARGS': ' '.join((self.name,) + args)}
487 487 def _checkvar(m):
488 488 if m.groups()[0] == '$':
489 489 return m.group()
490 490 elif int(m.groups()[0]) <= len(args):
491 491 return m.group()
492 492 else:
493 493 ui.debug("No argument found for substitution "
494 494 "of %i variable in alias '%s' definition.\n"
495 495 % (int(m.groups()[0]), self.name))
496 496 return ''
497 497 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
498 498 cmd = aliasinterpolate(self.name, args, cmd)
499 499 return ui.system(cmd, environ=env,
500 500 blockedtag='alias_%s' % self.name)
501 501 self.fn = fn
502 502 self._populatehelp(ui, name, shdef, self.fn)
503 503 return
504 504
505 505 try:
506 506 args = pycompat.shlexsplit(self.definition)
507 507 except ValueError as inst:
508 508 self.badalias = (_("error in definition for alias '%s': %s")
509 509 % (self.name, stringutil.forcebytestr(inst)))
510 510 return
511 511 earlyopts, args = _earlysplitopts(args)
512 512 if earlyopts:
513 513 self.badalias = (_("error in definition for alias '%s': %s may "
514 514 "only be given on the command line")
515 515 % (self.name, '/'.join(pycompat.ziplist(*earlyopts)
516 516 [0])))
517 517 return
518 518 self.cmdname = cmd = args.pop(0)
519 519 self.givenargs = args
520 520
521 521 try:
522 522 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
523 523 if len(tableentry) > 2:
524 524 self.fn, self.opts, cmdhelp = tableentry
525 525 else:
526 526 self.fn, self.opts = tableentry
527 527 cmdhelp = None
528 528
529 529 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
530 530
531 531 except error.UnknownCommand:
532 532 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
533 533 % (self.name, cmd))
534 534 self.unknowncmd = True
535 535 except error.AmbiguousCommand:
536 536 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
537 537 % (self.name, cmd))
538 538
539 539 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
540 540 # confine strings to be passed to i18n.gettext()
541 541 cfg = {}
542 542 for k in ('doc', 'help'):
543 543 v = ui.config('alias', '%s:%s' % (name, k), None)
544 544 if v is None:
545 545 continue
546 546 if not encoding.isasciistr(v):
547 547 self.badalias = (_("non-ASCII character in alias definition "
548 548 "'%s:%s'") % (name, k))
549 549 return
550 550 cfg[k] = v
551 551
552 552 self.help = cfg.get('help', defaulthelp or '')
553 553 if self.help and self.help.startswith("hg " + cmd):
554 554 # drop prefix in old-style help lines so hg shows the alias
555 555 self.help = self.help[4 + len(cmd):]
556 556
557 557 doc = cfg.get('doc', pycompat.getdoc(fn))
558 558 if doc is not None:
559 559 doc = pycompat.sysstr(doc)
560 560 self.__doc__ = doc
561 561
562 562 @property
563 563 def args(self):
564 564 args = pycompat.maplist(util.expandpath, self.givenargs)
565 565 return aliasargs(self.fn, args)
566 566
567 567 def __getattr__(self, name):
568 568 adefaults = {r'norepo': True, r'intents': set(),
569 569 r'optionalrepo': False, r'inferrepo': False}
570 570 if name not in adefaults:
571 571 raise AttributeError(name)
572 572 if self.badalias or util.safehasattr(self, 'shell'):
573 573 return adefaults[name]
574 574 return getattr(self.fn, name)
575 575
576 576 def __call__(self, ui, *args, **opts):
577 577 if self.badalias:
578 578 hint = None
579 579 if self.unknowncmd:
580 580 try:
581 581 # check if the command is in a disabled extension
582 582 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
583 583 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
584 584 except error.UnknownCommand:
585 585 pass
586 586 raise error.Abort(self.badalias, hint=hint)
587 587 if self.shadows:
588 588 ui.debug("alias '%s' shadows command '%s'\n" %
589 589 (self.name, self.cmdname))
590 590
591 591 ui.log('commandalias', "alias '%s' expands to '%s'\n",
592 592 self.name, self.definition)
593 593 if util.safehasattr(self, 'shell'):
594 594 return self.fn(ui, *args, **opts)
595 595 else:
596 596 try:
597 597 return util.checksignature(self.fn)(ui, *args, **opts)
598 598 except error.SignatureError:
599 599 args = ' '.join([self.cmdname] + self.args)
600 600 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
601 601 raise
602 602
603 603 class lazyaliasentry(object):
604 604 """like a typical command entry (func, opts, help), but is lazy"""
605 605
606 606 def __init__(self, ui, name, definition, cmdtable, source):
607 607 self.ui = ui
608 608 self.name = name
609 609 self.definition = definition
610 610 self.cmdtable = cmdtable.copy()
611 611 self.source = source
612 612
613 613 @util.propertycache
614 614 def _aliasdef(self):
615 615 return cmdalias(self.ui, self.name, self.definition, self.cmdtable,
616 616 self.source)
617 617
618 618 def __getitem__(self, n):
619 619 aliasdef = self._aliasdef
620 620 if n == 0:
621 621 return aliasdef
622 622 elif n == 1:
623 623 return aliasdef.opts
624 624 elif n == 2:
625 625 return aliasdef.help
626 626 else:
627 627 raise IndexError
628 628
629 629 def __iter__(self):
630 630 for i in range(3):
631 631 yield self[i]
632 632
633 633 def __len__(self):
634 634 return 3
635 635
636 636 def addaliases(ui, cmdtable):
637 637 # aliases are processed after extensions have been loaded, so they
638 638 # may use extension commands. Aliases can also use other alias definitions,
639 639 # but only if they have been defined prior to the current definition.
640 640 for alias, definition in ui.configitems('alias', ignoresub=True):
641 641 try:
642 642 if cmdtable[alias].definition == definition:
643 643 continue
644 644 except (KeyError, AttributeError):
645 645 # definition might not exist or it might not be a cmdalias
646 646 pass
647 647
648 648 source = ui.configsource('alias', alias)
649 649 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
650 650 cmdtable[alias] = entry
651 651
652 652 def _parse(ui, args):
653 653 options = {}
654 654 cmdoptions = {}
655 655
656 656 try:
657 657 args = fancyopts.fancyopts(args, commands.globalopts, options)
658 658 except getopt.GetoptError as inst:
659 659 raise error.CommandError(None, stringutil.forcebytestr(inst))
660 660
661 661 if args:
662 662 cmd, args = args[0], args[1:]
663 663 aliases, entry = cmdutil.findcmd(cmd, commands.table,
664 664 ui.configbool("ui", "strict"))
665 665 cmd = aliases[0]
666 666 args = aliasargs(entry[0], args)
667 667 defaults = ui.config("defaults", cmd)
668 668 if defaults:
669 669 args = pycompat.maplist(
670 670 util.expandpath, pycompat.shlexsplit(defaults)) + args
671 671 c = list(entry[1])
672 672 else:
673 673 cmd = None
674 674 c = []
675 675
676 676 # combine global options into local
677 677 for o in commands.globalopts:
678 678 c.append((o[0], o[1], options[o[1]], o[3]))
679 679
680 680 try:
681 681 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
682 682 except getopt.GetoptError as inst:
683 683 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
684 684
685 685 # separate global options back out
686 686 for o in commands.globalopts:
687 687 n = o[1]
688 688 options[n] = cmdoptions[n]
689 689 del cmdoptions[n]
690 690
691 691 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
692 692
693 693 def _parseconfig(ui, config):
694 694 """parse the --config options from the command line"""
695 695 configs = []
696 696
697 697 for cfg in config:
698 698 try:
699 699 name, value = [cfgelem.strip()
700 700 for cfgelem in cfg.split('=', 1)]
701 701 section, name = name.split('.', 1)
702 702 if not section or not name:
703 703 raise IndexError
704 704 ui.setconfig(section, name, value, '--config')
705 705 configs.append((section, name, value))
706 706 except (IndexError, ValueError):
707 707 raise error.Abort(_('malformed --config option: %r '
708 708 '(use --config section.name=value)')
709 709 % pycompat.bytestr(cfg))
710 710
711 711 return configs
712 712
713 713 def _earlyparseopts(ui, args):
714 714 options = {}
715 715 fancyopts.fancyopts(args, commands.globalopts, options,
716 716 gnu=not ui.plain('strictflags'), early=True,
717 717 optaliases={'repository': ['repo']})
718 718 return options
719 719
720 720 def _earlysplitopts(args):
721 721 """Split args into a list of possible early options and remainder args"""
722 722 shortoptions = 'R:'
723 723 # TODO: perhaps 'debugger' should be included
724 724 longoptions = ['cwd=', 'repository=', 'repo=', 'config=']
725 725 return fancyopts.earlygetopt(args, shortoptions, longoptions,
726 726 gnu=True, keepsep=True)
727 727
728 728 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
729 729 # run pre-hook, and abort if it fails
730 730 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
731 731 pats=cmdpats, opts=cmdoptions)
732 732 try:
733 733 ret = _runcommand(ui, options, cmd, d)
734 734 # run post-hook, passing command result
735 735 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
736 736 result=ret, pats=cmdpats, opts=cmdoptions)
737 737 except Exception:
738 738 # run failure hook and re-raise
739 739 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
740 740 pats=cmdpats, opts=cmdoptions)
741 741 raise
742 742 return ret
743 743
744 744 def _getlocal(ui, rpath, wd=None):
745 745 """Return (path, local ui object) for the given target path.
746 746
747 747 Takes paths in [cwd]/.hg/hgrc into account."
748 748 """
749 749 if wd is None:
750 750 try:
751 751 wd = pycompat.getcwd()
752 752 except OSError as e:
753 753 raise error.Abort(_("error getting current working directory: %s") %
754 754 encoding.strtolocal(e.strerror))
755 755 path = cmdutil.findrepo(wd) or ""
756 756 if not path:
757 757 lui = ui
758 758 else:
759 759 lui = ui.copy()
760 760 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
761 761
762 762 if rpath:
763 763 path = lui.expandpath(rpath)
764 764 lui = ui.copy()
765 765 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
766 766
767 767 return path, lui
768 768
769 769 def _checkshellalias(lui, ui, args):
770 770 """Return the function to run the shell alias, if it is required"""
771 771 options = {}
772 772
773 773 try:
774 774 args = fancyopts.fancyopts(args, commands.globalopts, options)
775 775 except getopt.GetoptError:
776 776 return
777 777
778 778 if not args:
779 779 return
780 780
781 781 cmdtable = commands.table
782 782
783 783 cmd = args[0]
784 784 try:
785 785 strict = ui.configbool("ui", "strict")
786 786 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
787 787 except (error.AmbiguousCommand, error.UnknownCommand):
788 788 return
789 789
790 790 cmd = aliases[0]
791 791 fn = entry[0]
792 792
793 793 if cmd and util.safehasattr(fn, 'shell'):
794 794 # shell alias shouldn't receive early options which are consumed by hg
795 795 _earlyopts, args = _earlysplitopts(args)
796 796 d = lambda: fn(ui, *args[1:])
797 797 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
798 798 [], {})
799 799
800 800 def _dispatch(req):
801 801 args = req.args
802 802 ui = req.ui
803 803
804 804 # check for cwd
805 805 cwd = req.earlyoptions['cwd']
806 806 if cwd:
807 807 os.chdir(cwd)
808 808
809 809 rpath = req.earlyoptions['repository']
810 810 path, lui = _getlocal(ui, rpath)
811 811
812 812 uis = {ui, lui}
813 813
814 814 if req.repo:
815 815 uis.add(req.repo.ui)
816 816
817 817 if (req.earlyoptions['verbose'] or req.earlyoptions['debug']
818 818 or req.earlyoptions['quiet']):
819 819 for opt in ('verbose', 'debug', 'quiet'):
820 820 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
821 821 for ui_ in uis:
822 822 ui_.setconfig('ui', opt, val, '--' + opt)
823 823
824 824 if req.earlyoptions['profile']:
825 825 for ui_ in uis:
826 826 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
827 827
828 828 profile = lui.configbool('profiling', 'enabled')
829 829 with profiling.profile(lui, enabled=profile) as profiler:
830 830 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
831 831 # reposetup
832 832 extensions.loadall(lui)
833 833 # Propagate any changes to lui.__class__ by extensions
834 834 ui.__class__ = lui.__class__
835 835
836 836 # (uisetup and extsetup are handled in extensions.loadall)
837 837
838 838 # (reposetup is handled in hg.repository)
839 839
840 840 addaliases(lui, commands.table)
841 841
842 842 # All aliases and commands are completely defined, now.
843 843 # Check abbreviation/ambiguity of shell alias.
844 844 shellaliasfn = _checkshellalias(lui, ui, args)
845 845 if shellaliasfn:
846 846 return shellaliasfn()
847 847
848 848 # check for fallback encoding
849 849 fallback = lui.config('ui', 'fallbackencoding')
850 850 if fallback:
851 851 encoding.fallbackencoding = fallback
852 852
853 853 fullargs = args
854 854 cmd, func, args, options, cmdoptions = _parse(lui, args)
855 855
856 856 if options["config"] != req.earlyoptions["config"]:
857 857 raise error.Abort(_("option --config may not be abbreviated!"))
858 858 if options["cwd"] != req.earlyoptions["cwd"]:
859 859 raise error.Abort(_("option --cwd may not be abbreviated!"))
860 860 if options["repository"] != req.earlyoptions["repository"]:
861 861 raise error.Abort(_(
862 862 "option -R has to be separated from other options (e.g. not "
863 863 "-qR) and --repository may only be abbreviated as --repo!"))
864 864 if options["debugger"] != req.earlyoptions["debugger"]:
865 865 raise error.Abort(_("option --debugger may not be abbreviated!"))
866 866 # don't validate --profile/--traceback, which can be enabled from now
867 867
868 868 if options["encoding"]:
869 869 encoding.encoding = options["encoding"]
870 870 if options["encodingmode"]:
871 871 encoding.encodingmode = options["encodingmode"]
872 872 if options["time"]:
873 873 def get_times():
874 874 t = os.times()
875 875 if t[4] == 0.0:
876 876 # Windows leaves this as zero, so use time.clock()
877 877 t = (t[0], t[1], t[2], t[3], time.clock())
878 878 return t
879 879 s = get_times()
880 880 def print_time():
881 881 t = get_times()
882 882 ui.warn(
883 883 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
884 884 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
885 885 ui.atexit(print_time)
886 886 if options["profile"]:
887 887 profiler.start()
888 888
889 889 # if abbreviated version of this were used, take them in account, now
890 890 if options['verbose'] or options['debug'] or options['quiet']:
891 891 for opt in ('verbose', 'debug', 'quiet'):
892 892 if options[opt] == req.earlyoptions[opt]:
893 893 continue
894 894 val = pycompat.bytestr(bool(options[opt]))
895 895 for ui_ in uis:
896 896 ui_.setconfig('ui', opt, val, '--' + opt)
897 897
898 898 if options['traceback']:
899 899 for ui_ in uis:
900 900 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
901 901
902 902 if options['noninteractive']:
903 903 for ui_ in uis:
904 904 ui_.setconfig('ui', 'interactive', 'off', '-y')
905 905
906 906 if cmdoptions.get('insecure', False):
907 907 for ui_ in uis:
908 908 ui_.insecureconnections = True
909 909
910 910 # setup color handling before pager, because setting up pager
911 911 # might cause incorrect console information
912 912 coloropt = options['color']
913 913 for ui_ in uis:
914 914 if coloropt:
915 915 ui_.setconfig('ui', 'color', coloropt, '--color')
916 916 color.setup(ui_)
917 917
918 918 if stringutil.parsebool(options['pager']):
919 919 # ui.pager() expects 'internal-always-' prefix in this case
920 920 ui.pager('internal-always-' + cmd)
921 921 elif options['pager'] != 'auto':
922 922 for ui_ in uis:
923 923 ui_.disablepager()
924 924
925 925 if options['version']:
926 926 return commands.version_(ui)
927 927 if options['help']:
928 928 return commands.help_(ui, cmd, command=cmd is not None)
929 929 elif not cmd:
930 930 return commands.help_(ui, 'shortlist')
931 931
932 932 repo = None
933 933 cmdpats = args[:]
934 934 if not func.norepo:
935 935 # use the repo from the request only if we don't have -R
936 936 if not rpath and not cwd:
937 937 repo = req.repo
938 938
939 939 if repo:
940 940 # set the descriptors of the repo ui to those of ui
941 941 repo.ui.fin = ui.fin
942 942 repo.ui.fout = ui.fout
943 943 repo.ui.ferr = ui.ferr
944 944 else:
945 945 try:
946 946 repo = hg.repository(ui, path=path,
947 947 presetupfuncs=req.prereposetups,
948 948 intents=func.intents)
949 949 if not repo.local():
950 950 raise error.Abort(_("repository '%s' is not local")
951 951 % path)
952 952 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
953 953 'repo')
954 954 except error.RequirementError:
955 955 raise
956 956 except error.RepoError:
957 957 if rpath: # invalid -R path
958 958 raise
959 959 if not func.optionalrepo:
960 960 if func.inferrepo and args and not path:
961 961 # try to infer -R from command args
962 962 repos = pycompat.maplist(cmdutil.findrepo, args)
963 963 guess = repos[0]
964 964 if guess and repos.count(guess) == len(repos):
965 965 req.args = ['--repository', guess] + fullargs
966 966 req.earlyoptions['repository'] = guess
967 967 return _dispatch(req)
968 968 if not path:
969 969 raise error.RepoError(_("no repository found in"
970 970 " '%s' (.hg not found)")
971 971 % pycompat.getcwd())
972 972 raise
973 973 if repo:
974 974 ui = repo.ui
975 975 if options['hidden']:
976 976 repo = repo.unfiltered()
977 977 args.insert(0, repo)
978 978 elif rpath:
979 979 ui.warn(_("warning: --repository ignored\n"))
980 980
981 981 msg = _formatargs(fullargs)
982 982 ui.log("command", '%s\n', msg)
983 983 strcmdopt = pycompat.strkwargs(cmdoptions)
984 984 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
985 985 try:
986 986 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
987 987 cmdpats, cmdoptions)
988 988 finally:
989 989 if repo and repo != req.repo:
990 990 repo.close()
991 991
992 992 def _runcommand(ui, options, cmd, cmdfunc):
993 993 """Run a command function, possibly with profiling enabled."""
994 994 try:
995 return cmdfunc()
995 with tracing.log("Running %s command" % cmd):
996 return cmdfunc()
996 997 except error.SignatureError:
997 998 raise error.CommandError(cmd, _('invalid arguments'))
998 999
999 1000 def _exceptionwarning(ui):
1000 1001 """Produce a warning message for the current active exception"""
1001 1002
1002 1003 # For compatibility checking, we discard the portion of the hg
1003 1004 # version after the + on the assumption that if a "normal
1004 1005 # user" is running a build with a + in it the packager
1005 1006 # probably built from fairly close to a tag and anyone with a
1006 1007 # 'make local' copy of hg (where the version number can be out
1007 1008 # of date) will be clueful enough to notice the implausible
1008 1009 # version number and try updating.
1009 1010 ct = util.versiontuple(n=2)
1010 1011 worst = None, ct, ''
1011 1012 if ui.config('ui', 'supportcontact') is None:
1012 1013 for name, mod in extensions.extensions():
1013 1014 # 'testedwith' should be bytes, but not all extensions are ported
1014 1015 # to py3 and we don't want UnicodeException because of that.
1015 1016 testedwith = stringutil.forcebytestr(getattr(mod, 'testedwith', ''))
1016 1017 report = getattr(mod, 'buglink', _('the extension author.'))
1017 1018 if not testedwith.strip():
1018 1019 # We found an untested extension. It's likely the culprit.
1019 1020 worst = name, 'unknown', report
1020 1021 break
1021 1022
1022 1023 # Never blame on extensions bundled with Mercurial.
1023 1024 if extensions.ismoduleinternal(mod):
1024 1025 continue
1025 1026
1026 1027 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1027 1028 if ct in tested:
1028 1029 continue
1029 1030
1030 1031 lower = [t for t in tested if t < ct]
1031 1032 nearest = max(lower or tested)
1032 1033 if worst[0] is None or nearest < worst[1]:
1033 1034 worst = name, nearest, report
1034 1035 if worst[0] is not None:
1035 1036 name, testedwith, report = worst
1036 1037 if not isinstance(testedwith, (bytes, str)):
1037 1038 testedwith = '.'.join([stringutil.forcebytestr(c)
1038 1039 for c in testedwith])
1039 1040 warning = (_('** Unknown exception encountered with '
1040 1041 'possibly-broken third-party extension %s\n'
1041 1042 '** which supports versions %s of Mercurial.\n'
1042 1043 '** Please disable %s and try your action again.\n'
1043 1044 '** If that fixes the bug please report it to %s\n')
1044 1045 % (name, testedwith, name, stringutil.forcebytestr(report)))
1045 1046 else:
1046 1047 bugtracker = ui.config('ui', 'supportcontact')
1047 1048 if bugtracker is None:
1048 1049 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
1049 1050 warning = (_("** unknown exception encountered, "
1050 1051 "please report by visiting\n** ") + bugtracker + '\n')
1051 1052 sysversion = pycompat.sysbytes(sys.version).replace('\n', '')
1052 1053 warning += ((_("** Python %s\n") % sysversion) +
1053 1054 (_("** Mercurial Distributed SCM (version %s)\n") %
1054 1055 util.version()) +
1055 1056 (_("** Extensions loaded: %s\n") %
1056 1057 ", ".join([x[0] for x in extensions.extensions()])))
1057 1058 return warning
1058 1059
1059 1060 def handlecommandexception(ui):
1060 1061 """Produce a warning message for broken commands
1061 1062
1062 1063 Called when handling an exception; the exception is reraised if
1063 1064 this function returns False, ignored otherwise.
1064 1065 """
1065 1066 warning = _exceptionwarning(ui)
1066 1067 ui.log("commandexception", "%s\n%s\n", warning,
1067 1068 pycompat.sysbytes(traceback.format_exc()))
1068 1069 ui.warn(warning)
1069 1070 return False # re-raise the exception
General Comments 0
You need to be logged in to leave comments. Login now