# common.py - common code for the convert extension
#
#  Copyright 2005-2009 Matt Mackall <[email protected]> and others
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

import base64, errno
import os
import cPickle as pickle
from mercurial import util
from mercurial.i18n import _

def encodeargs(args):
   def encodearg(s):
       lines = base64.encodestring(s)
       lines = [l.splitlines()[0] for l in lines]
       return ''.join(lines)

   s = pickle.dumps(args)
   return encodearg(s)

def decodeargs(s):
   s = base64.decodestring(s)
   return pickle.loads(s)

class MissingTool(Exception): pass

def checktool(exe, name=None, abort=True):
   name = name or exe
   if not util.find_exe(exe):
       exc = abort and util.Abort or MissingTool
       raise exc(_('cannot find required "%s" tool') % name)

class NoRepo(Exception): pass

SKIPREV = 'SKIP'

class commit(object):
   def __init__(self, author, date, desc, parents, branch=None, rev=None,
                extra={}, sortkey=None):
       self.author = author or 'unknown'
       self.date = date or '0 0'
       self.desc = desc
       self.parents = parents
       self.branch = branch
       self.rev = rev
       self.extra = extra
       self.sortkey = sortkey

class converter_source(object):
   """Conversion source interface"""

   def __init__(self, ui, path=None, rev=None):
       """Initialize conversion source (or raise NoRepo("message")
       exception if path is not a valid repository)"""
       self.ui = ui
       self.path = path
       self.rev = rev

       self.encoding = 'utf-8'

   def before(self):
       pass

   def after(self):
       pass

   def setrevmap(self, revmap):
       """set the map of already-converted revisions"""
       pass

   def getheads(self):
       """Return a list of this repository's heads"""
       raise NotImplementedError()

   def getfile(self, name, rev):
       """Return file contents as a string. rev is the identifier returned
       by a previous call to getchanges(). Raise IOError to indicate that
       name was deleted in rev.
       """
       raise NotImplementedError()

   def getmode(self, name, rev):
       """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
       returned by a previous call to getchanges().
       """
       raise NotImplementedError()

   def getchanges(self, version):
       """Returns a tuple of (files, copies).

       files is a sorted list of (filename, id) tuples for all files
       changed between version and its first parent returned by
       getcommit(). id is the source revision id of the file.

       copies is a dictionary of dest: source
       """
       raise NotImplementedError()

   def getcommit(self, version):
       """Return the commit object for version"""
       raise NotImplementedError()

   def gettags(self):
       """Return the tags as a dictionary of name: revision

       Tag names must be UTF-8 strings.
       """
       raise NotImplementedError()

   def recode(self, s, encoding=None):
       if not encoding:
           encoding = self.encoding or 'utf-8'

       if isinstance(s, unicode):
           return s.encode("utf-8")
       try:
           return s.decode(encoding).encode("utf-8")
       except:
           try:
               return s.decode("latin-1").encode("utf-8")
           except:
               return s.decode(encoding, "replace").encode("utf-8")

   def getchangedfiles(self, rev, i):
       """Return the files changed by rev compared to parent[i].

       i is an index selecting one of the parents of rev.  The return
       value should be the list of files that are different in rev and
       this parent.

       If rev has no parents, i is None.

       This function is only needed to support --filemap
       """
       raise NotImplementedError()

   def converted(self, rev, sinkrev):
       '''Notify the source that a revision has been converted.'''
       pass

   def hasnativeorder(self):
       """Return true if this source has a meaningful, native revision
       order. For instance, Mercurial revisions are store sequentially
       while there is no such global ordering with Darcs.
       """
       return False

   def lookuprev(self, rev):
       """If rev is a meaningful revision reference in source, return
       the referenced identifier in the same format used by getcommit().
       return None otherwise.
       """
       return None

class converter_sink(object):
   """Conversion sink (target) interface"""

   def __init__(self, ui, path):
       """Initialize conversion sink (or raise NoRepo("message")
       exception if path is not a valid repository)

       created is a list of paths to remove if a fatal error occurs
       later"""
       self.ui = ui
       self.path = path
       self.created = []

   def getheads(self):
       """Return a list of this repository's heads"""
       raise NotImplementedError()

   def revmapfile(self):
       """Path to a file that will contain lines
       source_rev_id sink_rev_id
       mapping equivalent revision identifiers for each system."""
       raise NotImplementedError()

   def authorfile(self):
       """Path to a file that will contain lines
       srcauthor=dstauthor
       mapping equivalent authors identifiers for each system."""
       return None

   def putcommit(self, files, copies, parents, commit, source, revmap):
       """Create a revision with all changed files listed in 'files'
       and having listed parents. 'commit' is a commit object
       containing at a minimum the author, date, and message for this
       changeset.  'files' is a list of (path, version) tuples,
       'copies' is a dictionary mapping destinations to sources,
       'source' is the source repository, and 'revmap' is a mapfile
       of source revisions to converted revisions. Only getfile(),
       getmode(), and lookuprev() should be called on 'source'.

       Note that the sink repository is not told to update itself to
       a particular revision (or even what that revision would be)
       before it receives the file data.
       """
       raise NotImplementedError()

   def puttags(self, tags):
       """Put tags into sink.

       tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
       """
       raise NotImplementedError()

   def setbranch(self, branch, pbranches):
       """Set the current branch name. Called before the first putcommit
       on the branch.
       branch: branch name for subsequent commits
       pbranches: (converted parent revision, parent branch) tuples"""
       pass

   def setfilemapmode(self, active):
       """Tell the destination that we're using a filemap

       Some converter_sources (svn in particular) can claim that a file
       was changed in a revision, even if there was no change.  This method
       tells the destination that we're using a filemap and that it should
       filter empty revisions.
       """
       pass

   def before(self):
       pass

   def after(self):
       pass


class commandline(object):
   def __init__(self, ui, command):
       self.ui = ui
       self.command = command

   def prerun(self):
       pass

   def postrun(self):
       pass

   def _cmdline(self, cmd, *args, **kwargs):
       cmdline = [self.command, cmd] + list(args)
       for k, v in kwargs.iteritems():
           if len(k) == 1:
               cmdline.append('-' + k)
           else:
               cmdline.append('--' + k.replace('_', '-'))
           try:
               if len(k) == 1:
                   cmdline.append('' + v)
               else:
                   cmdline[-1] += '=' + v
           except TypeError:
               pass
       cmdline = [util.shellquote(arg) for arg in cmdline]
       if not self.ui.debugflag:
           cmdline += ['2>', util.nulldev]
       cmdline += ['<', util.nulldev]
       cmdline = ' '.join(cmdline)
       return cmdline

   def _run(self, cmd, *args, **kwargs):
       cmdline = self._cmdline(cmd, *args, **kwargs)
       self.ui.debug(_('running: %s\n') % (cmdline,))
       self.prerun()
       try:
           return util.popen(cmdline)
       finally:
           self.postrun()

   def run(self, cmd, *args, **kwargs):
       fp = self._run(cmd, *args, **kwargs)
       output = fp.read()
       self.ui.debug(output)
       return output, fp.close()

   def runlines(self, cmd, *args, **kwargs):
       fp = self._run(cmd, *args, **kwargs)
       output = fp.readlines()
       self.ui.debug(''.join(output))
       return output, fp.close()

   def checkexit(self, status, output=''):
       if status:
           if output:
               self.ui.warn(_('%s error:\n') % self.command)
               self.ui.warn(output)
           msg = util.explain_exit(status)[0]
           raise util.Abort('%s %s' % (self.command, msg))

   def run0(self, cmd, *args, **kwargs):
       output, status = self.run(cmd, *args, **kwargs)
       self.checkexit(status, output)
       return output

   def runlines0(self, cmd, *args, **kwargs):
       output, status = self.runlines(cmd, *args, **kwargs)
       self.checkexit(status, ''.join(output))
       return output

   def getargmax(self):
       if '_argmax' in self.__dict__:
           return self._argmax

       # POSIX requires at least 4096 bytes for ARG_MAX
       self._argmax = 4096
       try:
           self._argmax = os.sysconf("SC_ARG_MAX")
       except:
           pass

       # Windows shells impose their own limits on command line length,
       # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
       # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
       # details about cmd.exe limitations.

       # Since ARG_MAX is for command line _and_ environment, lower our limit
       # (and make happy Windows shells while doing this).

       self._argmax = self._argmax/2 - 1
       return self._argmax

   def limit_arglist(self, arglist, cmd, *args, **kwargs):
       limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
       bytes = 0
       fl = []
       for fn in arglist:
           b = len(fn) + 3
           if bytes + b < limit or len(fl) == 0:
               fl.append(fn)
               bytes += b
           else:
               yield fl
               fl = [fn]
               bytes = b
       if fl:
           yield fl

   def xargs(self, arglist, cmd, *args, **kwargs):
       for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
           self.run0(cmd, *(list(args) + l), **kwargs)

class mapfile(dict):
   def __init__(self, ui, path):
       super(mapfile, self).__init__()
       self.ui = ui
       self.path = path
       self.fp = None
       self.order = []
       self._read()

   def _read(self):
       if not self.path:
           return
       try:
           fp = open(self.path, 'r')
       except IOError, err:
           if err.errno != errno.ENOENT:
               raise
           return
       for i, line in enumerate(fp):
           try:
               key, value = line[:-1].rsplit(' ', 1)
           except ValueError:
               raise util.Abort(_('syntax error in %s(%d): key/value pair expected')
                                % (self.path, i+1))
           if key not in self:
               self.order.append(key)
           super(mapfile, self).__setitem__(key, value)
       fp.close()

   def __setitem__(self, key, value):
       if self.fp is None:
           try:
               self.fp = open(self.path, 'a')
           except IOError, err:
               raise util.Abort(_('could not open map file %r: %s') %
                                (self.path, err.strerror))
       self.fp.write('%s %s\n' % (key, value))
       self.fp.flush()
       super(mapfile, self).__setitem__(key, value)

   def close(self):
       if self.fp:
           self.fp.close()
           self.fp = None