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