Mercurial > public > mercurial-scm > python-hglib
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 |