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