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