# GNU Arch support for the convert extension

from common import NoRepo, commit, converter_source
from mercurial.i18n import _
from mercurial import util
import os, shutil, tempfile, stat

class gnuarch_source(converter_source):
    def __init__(self, ui, path, rev=None):
        super(gnuarch_source, self).__init__(ui, path, rev=rev)

        if not os.path.exists(os.path.join(path, '{arch}')):
            raise NoRepo("couldn't open GNU Arch repo %s" % path)

        self.execmd = None

        if os.system('tla help > %s' % util.nulldev) == 0:
            self.execmd = 'tla'
        else:
            if os.system('baz help > %s' % util.nulldev) == 0:
                self.execmd = 'baz'
            else:
                raise util.Abort(_('unable to find a GNU Arch command'))

        self.path = os.path.realpath(path)
        self.tmppath = None

        self.treeversion = None
        self.lastrev = None
        self.changes = {}
        self.parents = {}
        self.tags = {}
        self.modecache = {}

    def before(self):
        if self.execmd == 'tla':
            output, status = self._run('tree-version', self.path)
        else:
            output, status = self._run('tree-version', '-d', self.path)
        self._checkexit(status)
        self.treeversion = output[0].strip()

        self.ui.status('analyzing tree version %s...\n' % self.treeversion)

        # Get name of temporary directory
        version = self.treeversion.split('/')
        self.tmppath = os.path.join(tempfile.gettempdir(),
                                    'hg-%s' % version[1])

        # Generate parents dictionary
        child = []
        output, status = self._run('revisions', self.treeversion)
        self._checkexit(status, 'archive registered?')
        for l in output:
            rev = l.strip()
            self._initrev(rev)

            # Read author, date and summary
            catlog, status = self._run('cat-log', '-d', self.path, rev)
            self._checkexit(status)
            self._parsecatlog(catlog, rev)

            self.parents[rev] = child
            child = [rev]
        self.parents[None] = child

    def _initrev(self, rev):
        self.changes[rev] = {}
        self.changes[rev]['summary'] = ''
        self.changes[rev]['date'] = ''
        self.changes[rev]['author'] = ''
        self.changes[rev]['add_files'] = []
        self.changes[rev]['mod_files'] = []
        self.changes[rev]['del_files'] = []
        self.changes[rev]['ren_files'] = {}

    def after(self):
        self.ui.debug('cleaning up %s\n' % self.tmppath)
        shutil.rmtree(self.tmppath, ignore_errors=True)

    def getheads(self):
        return self.parents[None]

    def getfile(self, name, rev):
        if rev != self.lastrev:
            raise util.Abort(_('internal calling inconsistency'))

        # Raise IOError if necessary (i.e. deleted files).
        if not os.path.exists(os.path.join(self.tmppath, name)):
            raise IOError

        data, mode = self._getfile(name, rev)
        self.modecache[(name, rev)] = mode

        return data

    def getmode(self, name, rev):
        return self.modecache[(name, rev)]

    def getchanges(self, rev):
        self.modecache = {}
        self._update(rev)
        changes = []
        copies = {}

        for f in self.changes[rev]['add_files']:
            changes.append((f, rev))

        for f in self.changes[rev]['mod_files']:
            changes.append((f, rev))

        for f in self.changes[rev]['del_files']:
            changes.append((f, rev))

        for src in self.changes[rev]['ren_files']:
            to = self.changes[rev]['ren_files'][src]
            changes.append((src, rev))
            changes.append((to, rev))
            copies[src] = to

        changes.sort()
        self.lastrev = rev

        return changes, copies

    def getcommit(self, rev):
        changes = self.changes[rev]
        date = util.strdate(changes['date'], '%Y-%m-%d %H:%M:%S')
        return commit(author = changes['author'], date = util.datestr(date),
                      desc = changes['summary'], parents = self.parents[rev])

    def gettags(self):
        return self.tags

    def _run(self, cmd, *args, **kwargs):
        cmdline = [self.execmd, cmd]
        cmdline += args
        cmdline = [util.shellquote(arg) for arg in cmdline]
        cmdline += ['2>', util.nulldev]
        cmdline = util.quotecommand(' '.join(cmdline))
        self.ui.debug(cmdline, '\n')
        fp = os.popen(cmdline, 'r')
        return fp.readlines(), fp.close()

    def _execute(self, cmd, *args, **kwargs):
        cmdline = [self.execmd, cmd]
        cmdline += args
        cmdline = [util.shellquote(arg) for arg in cmdline]
        cmdline += ['>', util.nulldev, '2>', util.nulldev]
        cmdline = util.quotecommand(' '.join(cmdline))
        self.ui.debug(cmdline, '\n')
        return os.system(cmdline)

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

    def _update(self, rev):
        if rev == 'base-0':
            # Initialise 'base-0' revision
            self.ui.debug('obtaining revision %s...\n' % rev)
            revision = '%s--%s' % (self.treeversion, rev)
            output = self._execute('get', revision, self.tmppath)
            self.ui.debug('analysing revision %s...\n' % rev)
            self._readcontents(self.tmppath, rev)
        else:
            self.ui.debug('applying revision %s...\n' % rev)
            revision = '%s--%s' % (self.treeversion, rev)
            output = self._execute('replay', '-d', self.tmppath, revision)

            old_rev = self.parents[rev][0]
            self.ui.debug('computing changeset between %s and %s...\n' \
                               % (old_rev, rev))
            rev_a = '%s--%s' % (self.treeversion, old_rev)
            rev_b = '%s--%s' % (self.treeversion, rev)
            delta, status = self._run('delta', '-n', rev_a, rev_b)
            self._checkexit(status)
            self._parsedelta(delta, rev)

    def _getfile(self, name, rev):
        mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
        if stat.S_ISLNK(mode):
            data = os.readlink(os.path.join(self.tmppath, name))
            mode = mode and 'l' or ''
        else:
            data = open(os.path.join(self.tmppath, name), 'rb').read()
            mode = (mode & 0111) and 'x' or ''
        return data, mode

    def _exclude(self, name):
        exclude = [ '{arch}', '.arch-ids', '.arch-inventory' ]
        for exc in exclude:
            if name.find(exc) != -1:
                return True
        return False

    def _readcontents(self, path, rev, basepath = ''):
        contents = os.listdir(os.path.join(basepath, path))

        if basepath == '':
            basepath = path

        for c in contents:
            p = os.path.join(path, c)
            if os.path.isdir(p):
                self._readcontents(p, rev, basepath)
            else:
                f = p[len(basepath) + 1:]
                if not self._exclude(f):
                    self.changes[rev]['add_files'].append(f)

    def _parsecatlog(self, data, rev):
        for l in data:
            l = l.strip()
            if l.startswith('Summary:'):
                self.changes[rev]['summary'] = l[len('Summary: '):]

            if l.startswith('Standard-date:'):
                self.changes[rev]['date'] = l[len('Standard-date: '):]

            if l.startswith('Creator:'):
                self.changes[rev]['author'] = l[len('Creator: '):]

    def _parsedelta(self, data, rev):
        for l in data:
            l = l.strip()
            if l.startswith('A') and not l.startswith('A/'):
                file = l[1:].strip()
                if not self._exclude(file):
                    self.changes[rev]['add_files'].append(file)
            elif l.startswith('M'):
                file = l[1:].strip()
                if not self._exclude(file):
                    self.changes[rev]['mod_files'].append(file)
            elif l.startswith('D') and not l.startswith('D/'):
                file = l[1:].strip()
                if not self._exclude(file):
                    self.changes[rev]['del_files'].append(file)
            elif l.startswith('=>') and not l.startswith('/>'):
                files = l[2:].strip().split(' ')
                if len(files) == 1:
                    files = l[2:].strip().split('\t')
                if not self._exclude(files[0]) and not self._exclude(files[1]):
                    self.changes[rev]['ren_files'][files[0]] = files[1]
