# dispatch.py - command dispatching for mercurial
#
# Copyright 2005-2007 Matt Mackall <[email protected]>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

from i18n import _
import os, sys, atexit, signal, pdb, socket, errno, shlex, time
import util, commands, hg, fancyopts, extensions, hook, error
import cmdutil, encoding
import ui as _ui

def run():
   "run the command in sys.argv"
   sys.exit(dispatch(sys.argv[1:]))

def dispatch(args):
   "run the command specified in args"
   try:
       u = _ui.ui()
       if '--traceback' in args:
           u.setconfig('ui', 'traceback', 'on')
   except util.Abort, inst:
       sys.stderr.write(_("abort: %s\n") % inst)
       return -1
   return _runcatch(u, args)

def _runcatch(ui, args):
   def catchterm(*args):
       raise error.SignalInterrupt

   for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
       num = getattr(signal, name, None)
       if num: signal.signal(num, catchterm)

   try:
       try:
           # enter the debugger before command execution
           if '--debugger' in args:
               pdb.set_trace()
           try:
               return _dispatch(ui, args)
           finally:
               ui.flush()
       except:
           # enter the debugger when we hit an exception
           if '--debugger' in args:
               pdb.post_mortem(sys.exc_info()[2])
           ui.traceback()
           raise

   # Global exception handling, alphabetically
   # Mercurial-specific first, followed by built-in and library exceptions
   except error.AmbiguousCommand, inst:
       ui.warn(_("hg: command '%s' is ambiguous:\n    %s\n") %
               (inst.args[0], " ".join(inst.args[1])))
   except error.ConfigError, inst:
       ui.warn(_("hg: %s\n") % inst.args[0])
   except error.LockHeld, inst:
       if inst.errno == errno.ETIMEDOUT:
           reason = _('timed out waiting for lock held by %s') % inst.locker
       else:
           reason = _('lock held by %s') % inst.locker
       ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
   except error.LockUnavailable, inst:
       ui.warn(_("abort: could not lock %s: %s\n") %
              (inst.desc or inst.filename, inst.strerror))
   except error.ParseError, inst:
       if inst.args[0]:
           ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
           commands.help_(ui, inst.args[0])
       else:
           ui.warn(_("hg: %s\n") % inst.args[1])
           commands.help_(ui, 'shortlist')
   except error.RepoError, inst:
       ui.warn(_("abort: %s!\n") % inst)
   except error.ResponseError, inst:
       ui.warn(_("abort: %s") % inst.args[0])
       if not isinstance(inst.args[1], basestring):
           ui.warn(" %r\n" % (inst.args[1],))
       elif not inst.args[1]:
           ui.warn(_(" empty string\n"))
       else:
           ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
   except error.RevlogError, inst:
       ui.warn(_("abort: %s!\n") % inst)
   except error.SignalInterrupt:
       ui.warn(_("killed!\n"))
   except error.UnknownCommand, inst:
       ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
       commands.help_(ui, 'shortlist')
   except util.Abort, inst:
       ui.warn(_("abort: %s\n") % inst)
   except ImportError, inst:
       m = str(inst).split()[-1]
       ui.warn(_("abort: could not import module %s!\n") % m)
       if m in "mpatch bdiff".split():
           ui.warn(_("(did you forget to compile extensions?)\n"))
       elif m in "zlib".split():
           ui.warn(_("(is your Python install correct?)\n"))
   except IOError, inst:
       if hasattr(inst, "code"):
           ui.warn(_("abort: %s\n") % inst)
       elif hasattr(inst, "reason"):
           try: # usually it is in the form (errno, strerror)
               reason = inst.reason.args[1]
           except: # it might be anything, for example a string
               reason = inst.reason
           ui.warn(_("abort: error: %s\n") % reason)
       elif hasattr(inst, "args") and inst.args[0] == errno.EPIPE:
           if ui.debugflag:
               ui.warn(_("broken pipe\n"))
       elif getattr(inst, "strerror", None):
           if getattr(inst, "filename", None):
               ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
           else:
               ui.warn(_("abort: %s\n") % inst.strerror)
       else:
           raise
   except OSError, inst:
       if getattr(inst, "filename", None):
           ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename))
       else:
           ui.warn(_("abort: %s\n") % inst.strerror)
   except KeyboardInterrupt:
       try:
           ui.warn(_("interrupted!\n"))
       except IOError, inst:
           if inst.errno == errno.EPIPE:
               if ui.debugflag:
                   ui.warn(_("\nbroken pipe\n"))
           else:
               raise
   except MemoryError:
       ui.warn(_("abort: out of memory\n"))
   except SystemExit, inst:
       # Commands shouldn't sys.exit directly, but give a return code.
       # Just in case catch this and and pass exit code to caller.
       return inst.code
   except socket.error, inst:
       ui.warn(_("abort: %s\n") % inst.args[-1])
   except:
       ui.warn(_("** unknown exception encountered, details follow\n"))
       ui.warn(_("** report bug details to "
                "http://mercurial.selenic.com/bts/\n"))
       ui.warn(_("** or [email protected]\n"))
       ui.warn(_("** Mercurial Distributed SCM (version %s)\n")
              % util.version())
       ui.warn(_("** Extensions loaded: %s\n")
              % ", ".join([x[0] for x in extensions.extensions()]))
       raise

   return -1

def _findrepo(p):
   while not os.path.isdir(os.path.join(p, ".hg")):
       oldp, p = p, os.path.dirname(p)
       if p == oldp:
           return None

   return p

def aliasargs(fn):
   if hasattr(fn, 'args'):
       return fn.args
   return []

class cmdalias(object):
   def __init__(self, name, definition, cmdtable):
       self.name = name
       self.definition = definition
       self.args = []
       self.opts = []
       self.help = ''
       self.norepo = True

       try:
           cmdutil.findcmd(self.name, cmdtable, True)
           self.shadows = True
       except error.UnknownCommand:
           self.shadows = False

       if not self.definition:
           def fn(ui, *args):
               ui.warn(_("no definition for alias '%s'\n") % self.name)
               return 1
           self.fn = fn

           return

       args = shlex.split(self.definition)
       cmd = args.pop(0)
       opts = []
       help = ''

       try:
           self.fn, self.opts, self.help = cmdutil.findcmd(cmd, cmdtable, False)[1]
           self.args = aliasargs(self.fn) + args
           if cmd not in commands.norepo.split(' '):
               self.norepo = False
       except error.UnknownCommand:
           def fn(ui, *args):
               ui.warn(_("alias '%s' resolves to unknown command '%s'\n") \
                           % (self.name, cmd))
               return 1
           self.fn = fn
       except error.AmbiguousCommand:
           def fn(ui, *args):
               ui.warn(_("alias '%s' resolves to ambiguous command '%s'\n") \
                           % (self.name, cmd))
               return 1
           self.fn = fn

   def __call__(self, ui, *args, **opts):
       if self.shadows:
           ui.debug(_("alias '%s' shadows command\n") % self.name)

       return self.fn(ui, *args, **opts)

def addaliases(ui, cmdtable):
   # aliases are processed after extensions have been loaded, so they
   # may use extension commands. Aliases can also use other alias definitions,
   # but only if they have been defined prior to the current definition.
   for alias, definition in ui.configitems('alias'):
       aliasdef = cmdalias(alias, definition, cmdtable)
       cmdtable[alias] = (aliasdef, aliasdef.opts, aliasdef.help)
       if aliasdef.norepo:
           commands.norepo += ' %s' % alias

def _parse(ui, args):
   options = {}
   cmdoptions = {}

   try:
       args = fancyopts.fancyopts(args, commands.globalopts, options)
   except fancyopts.getopt.GetoptError, inst:
       raise error.ParseError(None, inst)

   if args:
       cmd, args = args[0], args[1:]
       aliases, i = cmdutil.findcmd(cmd, commands.table,
                                    ui.config("ui", "strict"))
       cmd = aliases[0]
       args = aliasargs(i[0]) + args
       defaults = ui.config("defaults", cmd)
       if defaults:
           args = shlex.split(defaults) + args
       c = list(i[1])
   else:
       cmd = None
       c = []

   # combine global options into local
   for o in commands.globalopts:
       c.append((o[0], o[1], options[o[1]], o[3]))

   try:
       args = fancyopts.fancyopts(args, c, cmdoptions, True)
   except fancyopts.getopt.GetoptError, inst:
       raise error.ParseError(cmd, inst)

   # separate global options back out
   for o in commands.globalopts:
       n = o[1]
       options[n] = cmdoptions[n]
       del cmdoptions[n]

   return (cmd, cmd and i[0] or None, args, options, cmdoptions)

def _parseconfig(ui, config):
   """parse the --config options from the command line"""
   for cfg in config:
       try:
           name, value = cfg.split('=', 1)
           section, name = name.split('.', 1)
           if not section or not name:
               raise IndexError
           ui.setconfig(section, name, value)
       except (IndexError, ValueError):
           raise util.Abort(_('malformed --config option: %s') % cfg)

def _earlygetopt(aliases, args):
   """Return list of values for an option (or aliases).

   The values are listed in the order they appear in args.
   The options and values are removed from args.
   """
   try:
       argcount = args.index("--")
   except ValueError:
       argcount = len(args)
   shortopts = [opt for opt in aliases if len(opt) == 2]
   values = []
   pos = 0
   while pos < argcount:
       if args[pos] in aliases:
           if pos + 1 >= argcount:
               # ignore and let getopt report an error if there is no value
               break
           del args[pos]
           values.append(args.pop(pos))
           argcount -= 2
       elif args[pos][:2] in shortopts:
           # short option can have no following space, e.g. hg log -Rfoo
           values.append(args.pop(pos)[2:])
           argcount -= 1
       else:
           pos += 1
   return values

def runcommand(lui, repo, cmd, fullargs, ui, options, d):
   # run pre-hook, and abort if it fails
   ret = hook.hook(lui, repo, "pre-%s" % cmd, False, args=" ".join(fullargs))
   if ret:
       return ret
   ret = _runcommand(ui, options, cmd, d)
   # run post-hook, passing command result
   hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
             result = ret)
   return ret

_loaded = set()
def _dispatch(ui, args):
   # read --config before doing anything else
   # (e.g. to change trust settings for reading .hg/hgrc)
   _parseconfig(ui, _earlygetopt(['--config'], args))

   # check for cwd
   cwd = _earlygetopt(['--cwd'], args)
   if cwd:
       os.chdir(cwd[-1])

   # read the local repository .hgrc into a local ui object
   path = _findrepo(os.getcwd()) or ""
   if not path:
       lui = ui
   if path:
       try:
           lui = ui.copy()
           lui.readconfig(os.path.join(path, ".hg", "hgrc"))
       except IOError:
           pass

   # now we can expand paths, even ones in .hg/hgrc
   rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
   if rpath:
       path = lui.expandpath(rpath[-1])
       lui = ui.copy()
       lui.readconfig(os.path.join(path, ".hg", "hgrc"))

   extensions.loadall(lui)
   for name, module in extensions.extensions():
       if name in _loaded:
           continue

       # setup extensions
       # TODO this should be generalized to scheme, where extensions can
       #      redepend on other extensions.  then we should toposort them, and
       #      do initialization in correct order
       extsetup = getattr(module, 'extsetup', None)
       if extsetup:
           extsetup()

       cmdtable = getattr(module, 'cmdtable', {})
       overrides = [cmd for cmd in cmdtable if cmd in commands.table]
       if overrides:
           ui.warn(_("extension '%s' overrides commands: %s\n")
                   % (name, " ".join(overrides)))
       commands.table.update(cmdtable)
       _loaded.add(name)

   addaliases(lui, commands.table)

   # check for fallback encoding
   fallback = lui.config('ui', 'fallbackencoding')
   if fallback:
       encoding.fallbackencoding = fallback

   fullargs = args
   cmd, func, args, options, cmdoptions = _parse(lui, args)

   if options["config"]:
       raise util.Abort(_("Option --config may not be abbreviated!"))
   if options["cwd"]:
       raise util.Abort(_("Option --cwd may not be abbreviated!"))
   if options["repository"]:
       raise util.Abort(_(
           "Option -R has to be separated from other options (e.g. not -qR) "
           "and --repository may only be abbreviated as --repo!"))

   if options["encoding"]:
       encoding.encoding = options["encoding"]
   if options["encodingmode"]:
       encoding.encodingmode = options["encodingmode"]
   if options["time"]:
       def get_times():
           t = os.times()
           if t[4] == 0.0: # Windows leaves this as zero, so use time.clock()
               t = (t[0], t[1], t[2], t[3], time.clock())
           return t
       s = get_times()
       def print_time():
           t = get_times()
           ui.warn(_("Time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
               (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
       atexit.register(print_time)

   if options['verbose'] or options['debug'] or options['quiet']:
       ui.setconfig('ui', 'verbose', str(bool(options['verbose'])))
       ui.setconfig('ui', 'debug', str(bool(options['debug'])))
       ui.setconfig('ui', 'quiet', str(bool(options['quiet'])))
   if options['traceback']:
       ui.setconfig('ui', 'traceback', 'on')
   if options['noninteractive']:
       ui.setconfig('ui', 'interactive', 'off')

   if options['help']:
       return commands.help_(ui, cmd, options['version'])
   elif options['version']:
       return commands.version_(ui)
   elif not cmd:
       return commands.help_(ui, 'shortlist')

   repo = None
   if cmd not in commands.norepo.split():
       try:
           repo = hg.repository(ui, path=path)
           ui = repo.ui
           if not repo.local():
               raise util.Abort(_("repository '%s' is not local") % path)
           ui.setconfig("bundle", "mainreporoot", repo.root)
       except error.RepoError:
           if cmd not in commands.optionalrepo.split():
               if args and not path: # try to infer -R from command args
                   repos = map(_findrepo, args)
                   guess = repos[0]
                   if guess and repos.count(guess) == len(repos):
                       return _dispatch(ui, ['--repository', guess] + fullargs)
               if not path:
                   raise error.RepoError(_("There is no Mercurial repository"
                                     " here (.hg not found)"))
               raise
       args.insert(0, repo)
   elif rpath:
       ui.warn("warning: --repository ignored\n")

   d = lambda: util.checksignature(func)(ui, *args, **cmdoptions)
   return runcommand(lui, repo, cmd, fullargs, ui, options, d)

def _runcommand(ui, options, cmd, cmdfunc):
   def checkargs():
       try:
           return cmdfunc()
       except error.SignatureError:
           raise error.ParseError(cmd, _("invalid arguments"))

   if options['profile']:
       format = ui.config('profiling', 'format', default='text')

       if not format in ['text', 'kcachegrind']:
           ui.warn(_("unrecognized profiling format '%s'"
                       " - Ignored\n") % format)
           format = 'text'

       output = ui.config('profiling', 'output')

       if output:
           path = os.path.expanduser(output)
           path = ui.expandpath(path)
           ostream = open(path, 'wb')
       else:
           ostream = sys.stderr

       try:
           from mercurial import lsprof
       except ImportError:
           raise util.Abort(_(
               'lsprof not available - install from '
               'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
       p = lsprof.Profiler()
       p.enable(subcalls=True)
       try:
           return checkargs()
       finally:
           p.disable()

           if format == 'kcachegrind':
               import lsprofcalltree
               calltree = lsprofcalltree.KCacheGrind(p)
               calltree.output(ostream)
           else:
               # format == 'text'
               stats = lsprof.Stats(p.getstats())
               stats.sort()
               stats.pprint(top=10, file=ostream, climit=5)

           if output:
               ostream.close()
   else:
       return checkargs()