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