Mercurial > python-hglib
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hglib/hglib.py Wed Jul 20 16:09:34 2011 -0500 @@ -0,0 +1,311 @@ +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