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