comparison hglib/hglib.py @ 0:79f88b4db15f

Initial commit
author Idan Kamara <idankk86@gmail.com>
date Wed, 20 Jul 2011 16:09:34 -0500
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:79f88b4db15f
1 import subprocess, os, struct, cStringIO, collections
2 import error, util
3
4 HGPATH = 'hg'
5
6 def connect(path=None, encoding=None, configs=None):
7 ''' starts a cmdserver for the given path (or for a repository found in the
8 cwd). HGENCODING is set to the given encoding. configs is a list of key, value,
9 similar to those passed to hg --config. '''
10 return hgclient(path, encoding, configs)
11
12 class hgclient(object):
13 inputfmt = '>I'
14 outputfmt = '>cI'
15 outputfmtsize = struct.calcsize(outputfmt)
16 retfmt = '>i'
17
18 # XXX fix this hack
19 _stylesdir = os.path.join(os.path.dirname(__file__), 'styles')
20 revstyle = ['--style', os.path.join(_stylesdir, 'rev.style')]
21
22 revision = collections.namedtuple('revision', 'rev, node, tags, '
23 'branch, author, desc')
24
25 def __init__(self, path, encoding, configs):
26 args = [HGPATH, 'serve', '--cmdserver', 'pipe']
27 if path:
28 args += ['-R', path]
29 if configs:
30 args += ['--config'] + configs
31 env = dict(os.environ)
32 if encoding:
33 env['HGENCODING'] = encoding
34
35 self.server = subprocess.Popen(args, stdin=subprocess.PIPE,
36 stdout=subprocess.PIPE, env=env)
37
38 self._readhello()
39 self._config = {}
40
41 def _readhello(self):
42 """ read the hello message the server sends when started """
43 ch, msg = self._readchannel()
44 assert ch == 'o'
45
46 msg = msg.split('\n')
47
48 self.capabilities = msg[0][len('capabilities: '):]
49 if not self.capabilities:
50 raise error.ResponseError("bad hello message: expected 'capabilities: '"
51 ", got %r" % msg[0])
52
53 self.capabilities = set(self.capabilities.split())
54
55 # at the very least the server should be able to run commands
56 assert 'runcommand' in self.capabilities
57
58 self._encoding = msg[1][len('encoding: '):]
59 if not self._encoding:
60 raise error.ResponseError("bad hello message: expected 'encoding: '"
61 ", got %r" % msg[1])
62
63 def _readchannel(self):
64 data = self.server.stdout.read(hgclient.outputfmtsize)
65 if not data:
66 raise error.ServerError()
67 channel, length = struct.unpack(hgclient.outputfmt, data)
68 if channel in 'IL':
69 return channel, length
70 else:
71 return channel, self.server.stdout.read(length)
72
73 def _parserevs(self, splitted):
74 ''' splitted is a list of fields according to our rev.style, where each 6
75 fields compose one revision. '''
76 return [self.revision._make(rev) for rev in util.grouper(6, splitted)]
77
78 def _eatlines(self, s, n):
79 idx = 0
80 for i in xrange(n):
81 idx = s.find('\n', idx) + 1
82
83 return s[idx:]
84
85 def runcommand(self, args, inchannels, outchannels):
86 def writeblock(data):
87 self.server.stdin.write(struct.pack(self.inputfmt, len(data)))
88 self.server.stdin.write(data)
89 self.server.stdin.flush()
90
91 if not self.server:
92 raise ValueError("server not connected")
93
94 self.server.stdin.write('runcommand\n')
95 writeblock('\0'.join(args))
96
97 while True:
98 channel, data = self._readchannel()
99
100 # input channels
101 if channel in inchannels:
102 writeblock(inchannels[channel](data))
103 # output channels
104 elif channel in outchannels:
105 outchannels[channel](data)
106 # result channel, command finished
107 elif channel == 'r':
108 return struct.unpack(hgclient.retfmt, data)[0]
109 # a channel that we don't know and can't ignore
110 elif channel.isupper():
111 raise error.ResponseError("unexpected data on required channel '%s'"
112 % channel)
113 # optional channel
114 else:
115 pass
116
117 def outputruncommand(self, args, inchannels = {}, raiseonerror=True):
118 ''' run the command specified by args, returning (ret, output, error) '''
119 out, err = cStringIO.StringIO(), cStringIO.StringIO()
120 outchannels = {'o' : out.write, 'e' : err.write}
121 ret = self.runcommand(args, inchannels, outchannels)
122 if ret and raiseonerror:
123 raise error.CommandError(args, ret, out.getvalue(), err.getvalue())
124 return ret, out.getvalue(), err.getvalue()
125
126 def close(self):
127 self.server.stdin.close()
128 self.server.wait()
129 ret = self.server.returncode
130 self.server = None
131 return ret
132
133 @property
134 def encoding(self):
135 """ get the servers encoding """
136 if not 'getencoding' in self.capabilities:
137 raise CapabilityError('getencoding')
138
139 if not self._encoding:
140 self.server.stdin.write('getencoding\n')
141 self._encoding = self._readfromchannel('r')
142
143 return self._encoding
144
145 def config(self, refresh=False):
146 if not self._config or refresh:
147 self._config.clear()
148
149 ret, out, err = self.outputruncommand(['showconfig'])
150 if ret:
151 raise error.CommandError(['showconfig'], ret, out, err)
152
153 for entry in cStringIO.StringIO(out):
154 k, v = entry.rstrip().split('=', 1)
155 section, name = k.split('.', 1)
156 self._config.setdefault(section, {})[name] = v
157
158 return self._config
159
160 def status(self):
161 ret, out = self.outputruncommand(['status', '-0'])
162
163 d = dict((c, []) for c in 'MARC!?I')
164
165 for entry in out.split('\0'):
166 if entry:
167 t, f = entry.split(' ', 1)
168 d[t].append(f)
169
170 return d
171
172 def log(self, revrange=None):
173 args = ['log'] + self.revstyle
174 if revrange:
175 args.append('-r')
176 args += revrange
177
178 out = self.outputruncommand(args)[1]
179 out = out.split('\0')[:-1]
180
181 return self._parserevs(out)
182
183 def incoming(self, revrange=None, path=None):
184 args = ['incoming'] + self.revstyle
185 if revrange:
186 args.append('-r')
187 args += revrange
188
189 if path:
190 args += [path]
191
192 ret, out, err = self.outputruncommand(args, raiseonerror=False)
193 if not ret:
194 out = self._eatlines(out, 2).split('\0')[:-1]
195 return self._parserevs(out)
196 elif ret == 1:
197 return []
198 else:
199 raise error.CommandError(args, ret, out, err)
200
201 def outgoing(self, revrange=None, path=None):
202 args = ['outgoing'] + self.revstyle
203 if revrange:
204 args.append('-r')
205 args += revrange
206
207 if path:
208 args += [path]
209
210 ret, out, err = self.outputruncommand(args, raiseonerror=False)
211 if not ret:
212 out = self._eatlines(out, 2).split('\0')[:-1]
213 return self._parserevs(out)
214 elif ret == 1:
215 return []
216 else:
217 raise error.CommandError(args, ret, out, err)
218
219 def commit(self, message, addremove=False):
220 args = ['commit', '-m', message]
221
222 if addremove:
223 args += ['-A']
224
225 self.outputruncommand(args)
226
227 # hope the tip hasn't changed since we committed
228 return self.tip()
229
230 def import_(self, patch):
231 if isinstance(patch, str):
232 fp = open(patch)
233 else:
234 assert hasattr(patch, 'read')
235 assert hasattr(patch, 'readline')
236
237 fp = patch
238
239 try:
240 inchannels = {'I' : fp.read, 'L' : fp.readline}
241 self.outputruncommand(['import', '-'], inchannels)
242 finally:
243 if fp != patch:
244 fp.close()
245
246 def root(self):
247 return self.outputruncommand(['root'])[1].rstrip()
248
249 def clone(self, source='.', dest=None, branch=None, updaterev=None,
250 revrange=None):
251 args = ['clone']
252
253 if branch:
254 args += ['-b', branch]
255 if updaterev:
256 args += ['-u', updaterev]
257 if revrange:
258 args.append('-r')
259 args += revrange
260 args.append(source)
261
262 if dest:
263 args.append(dest)
264
265 self.outputruncommand(args)
266
267 def tip(self):
268 out = self.outputruncommand(['tip'] + self.revstyle)[1]
269 out = out.split('\0')
270
271 return self._parserevs(out)[0]
272
273 def branch(self, name=None):
274 if not name:
275 return self.outputruncommand(['branch'])[1].rstrip()
276
277 def branches(self):
278 out = self.outputruncommand(['branches'])[1]
279 branches = {}
280 for line in out.rstrip().split('\n'):
281 branch, revnode = line.split()
282 branches[branch] = self.log(revrange=[revnode.split(':')[0]])[0]
283
284 return branches
285
286 def paths(self, name=None):
287 if not name:
288 out = self.outputruncommand(['paths'])[1]
289 if not out:
290 return {}
291
292 return dict([s.split(' = ') for s in out.rstrip().split('\n')])
293 else:
294 args = ['paths', name]
295 ret, out, err = self.outputruncommand(args, raiseonerror=False)
296 if ret:
297 raise error.CommandError(args, ret, out, err)
298 return out.rstrip()
299
300 def cat(self, files, rev=None, output=None):
301 args = ['cat']
302 if rev:
303 args += ['-r', rev]
304 if output:
305 args += ['-o', output]
306
307 args += files
308 ret, out, err = self.outputruncommand(args)
309
310 if not output:
311 return out