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