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