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