view hglib/hglib.py @ 0:79f88b4db15f

Initial commit
author Idan Kamara <idankk86@gmail.com>
date Wed, 20 Jul 2011 16:09:34 -0500
parents
children
line wrap: on
line source

import subprocess, os, struct, cStringIO, collections
import error, util

HGPATH = 'hg'

def connect(path=None, encoding=None, configs=None):
    ''' starts a cmdserver for the given path (or for a repository found in the
    cwd). HGENCODING is set to the given encoding. configs is a list of key, value,
    similar to those passed to hg --config. '''
    return hgclient(path, encoding, configs)

class hgclient(object):
    inputfmt = '>I'
    outputfmt = '>cI'
    outputfmtsize = struct.calcsize(outputfmt)
    retfmt = '>i'

    # XXX fix this hack
    _stylesdir = os.path.join(os.path.dirname(__file__), 'styles')
    revstyle = ['--style', os.path.join(_stylesdir, 'rev.style')]

    revision = collections.namedtuple('revision', 'rev, node, tags, '
                                                  'branch, author, desc')

    def __init__(self, path, encoding, configs):
        args = [HGPATH, 'serve', '--cmdserver', 'pipe']
        if path:
            args += ['-R', path]
        if configs:
            args += ['--config'] + configs
        env = dict(os.environ)
        if encoding:
            env['HGENCODING'] = encoding

        self.server = subprocess.Popen(args, stdin=subprocess.PIPE,
                                       stdout=subprocess.PIPE, env=env)

        self._readhello()
        self._config = {}

    def _readhello(self):
        """ read the hello message the server sends when started """
        ch, msg = self._readchannel()
        assert ch == 'o'

        msg = msg.split('\n')

        self.capabilities = msg[0][len('capabilities: '):]
        if not self.capabilities:
            raise error.ResponseError("bad hello message: expected 'capabilities: '"
                                      ", got %r" % msg[0])

        self.capabilities = set(self.capabilities.split())

        # at the very least the server should be able to run commands
        assert 'runcommand' in self.capabilities

        self._encoding = msg[1][len('encoding: '):]
        if not self._encoding:
            raise error.ResponseError("bad hello message: expected 'encoding: '"
                                      ", got %r" % msg[1])

    def _readchannel(self):
        data = self.server.stdout.read(hgclient.outputfmtsize)
        if not data:
            raise error.ServerError()
        channel, length = struct.unpack(hgclient.outputfmt, data)
        if channel in 'IL':
            return channel, length
        else:
            return channel, self.server.stdout.read(length)

    def _parserevs(self, splitted):
        ''' splitted is a list of fields according to our rev.style, where each 6
        fields compose one revision. '''
        return [self.revision._make(rev) for rev in util.grouper(6, splitted)]

    def _eatlines(self, s, n):
        idx = 0
        for i in xrange(n):
            idx = s.find('\n', idx) + 1

        return s[idx:]

    def runcommand(self, args, inchannels, outchannels):
        def writeblock(data):
            self.server.stdin.write(struct.pack(self.inputfmt, len(data)))
            self.server.stdin.write(data)
            self.server.stdin.flush()

        if not self.server:
            raise ValueError("server not connected")

        self.server.stdin.write('runcommand\n')
        writeblock('\0'.join(args))

        while True:
            channel, data = self._readchannel()

            # input channels
            if channel in inchannels:
                writeblock(inchannels[channel](data))
            # output channels
            elif channel in outchannels:
                outchannels[channel](data)
            # result channel, command finished
            elif channel == 'r':
                return struct.unpack(hgclient.retfmt, data)[0]
            # a channel that we don't know and can't ignore
            elif channel.isupper():
                raise error.ResponseError("unexpected data on required channel '%s'"
                                          % channel)
            # optional channel
            else:
                pass

    def outputruncommand(self, args, inchannels = {}, raiseonerror=True):
        ''' run the command specified by args, returning (ret, output, error) '''
        out, err = cStringIO.StringIO(), cStringIO.StringIO()
        outchannels = {'o' : out.write, 'e' : err.write}
        ret = self.runcommand(args, inchannels, outchannels)
        if ret and raiseonerror:
            raise error.CommandError(args, ret, out.getvalue(), err.getvalue())
        return ret, out.getvalue(), err.getvalue()

    def close(self):
        self.server.stdin.close()
        self.server.wait()
        ret = self.server.returncode
        self.server = None
        return ret

    @property
    def encoding(self):
        """ get the servers encoding """
        if not 'getencoding' in self.capabilities:
            raise CapabilityError('getencoding')

        if not self._encoding:
            self.server.stdin.write('getencoding\n')
            self._encoding = self._readfromchannel('r')

        return self._encoding

    def config(self, refresh=False):
        if not self._config or refresh:
            self._config.clear()

            ret, out, err = self.outputruncommand(['showconfig'])
            if ret:
                raise error.CommandError(['showconfig'], ret, out, err)

            for entry in cStringIO.StringIO(out):
                k, v = entry.rstrip().split('=', 1)
                section, name = k.split('.', 1)
                self._config.setdefault(section, {})[name] = v

        return self._config

    def status(self):
        ret, out = self.outputruncommand(['status', '-0'])

        d = dict((c, []) for c in 'MARC!?I')

        for entry in out.split('\0'):
            if entry:
                t, f = entry.split(' ', 1)
                d[t].append(f)

        return d

    def log(self, revrange=None):
        args = ['log'] + self.revstyle
        if revrange:
            args.append('-r')
            args += revrange

        out = self.outputruncommand(args)[1]
        out = out.split('\0')[:-1]

        return self._parserevs(out)

    def incoming(self, revrange=None, path=None):
        args = ['incoming'] + self.revstyle
        if revrange:
            args.append('-r')
            args += revrange

        if path:
            args += [path]

        ret, out, err = self.outputruncommand(args, raiseonerror=False)
        if not ret:
            out = self._eatlines(out, 2).split('\0')[:-1]
            return self._parserevs(out)
        elif ret == 1:
            return []
        else:
            raise error.CommandError(args, ret, out, err)

    def outgoing(self, revrange=None, path=None):
        args = ['outgoing'] + self.revstyle
        if revrange:
            args.append('-r')
            args += revrange

        if path:
            args += [path]

        ret, out, err = self.outputruncommand(args, raiseonerror=False)
        if not ret:
            out = self._eatlines(out, 2).split('\0')[:-1]
            return self._parserevs(out)
        elif ret == 1:
            return []
        else:
            raise error.CommandError(args, ret, out, err)

    def commit(self, message, addremove=False):
        args = ['commit', '-m', message]

        if addremove:
            args += ['-A']

        self.outputruncommand(args)

        # hope the tip hasn't changed since we committed
        return self.tip()

    def import_(self, patch):
        if isinstance(patch, str):
            fp = open(patch)
        else:
            assert hasattr(patch, 'read')
            assert hasattr(patch, 'readline')

            fp = patch

        try:
            inchannels = {'I' : fp.read, 'L' : fp.readline}
            self.outputruncommand(['import', '-'], inchannels)
        finally:
            if fp != patch:
                fp.close()

    def root(self):
        return self.outputruncommand(['root'])[1].rstrip()

    def clone(self, source='.', dest=None, branch=None, updaterev=None,
              revrange=None):
        args = ['clone']

        if branch:
            args += ['-b', branch]
        if updaterev:
            args += ['-u', updaterev]
        if revrange:
            args.append('-r')
            args += revrange
        args.append(source)

        if dest:
            args.append(dest)

        self.outputruncommand(args)

    def tip(self):
        out = self.outputruncommand(['tip'] + self.revstyle)[1]
        out = out.split('\0')

        return self._parserevs(out)[0]

    def branch(self, name=None):
        if not name:
            return self.outputruncommand(['branch'])[1].rstrip()

    def branches(self):
        out = self.outputruncommand(['branches'])[1]
        branches = {}
        for line in out.rstrip().split('\n'):
            branch, revnode = line.split()
            branches[branch] = self.log(revrange=[revnode.split(':')[0]])[0]

        return branches

    def paths(self, name=None):
        if not name:
            out = self.outputruncommand(['paths'])[1]
            if not out:
                return {}

            return dict([s.split(' = ') for s in out.rstrip().split('\n')])
        else:
            args = ['paths', name]
            ret, out, err = self.outputruncommand(args, raiseonerror=False)
            if ret:
                raise error.CommandError(args, ret, out, err)
            return out.rstrip()

    def cat(self, files, rev=None, output=None):
        args = ['cat']
        if rev:
            args += ['-r', rev]
        if output:
            args += ['-o', output]

        args += files
        ret, out, err = self.outputruncommand(args)

        if not output:
            return out