##// END OF EJS Templates
wireproto: use decorator for the between command
Pierre-Yves David -
r20908:ae4bf69c default
parent child Browse files
Show More
@@ -1,801 +1,801 b''
1 # wireproto.py - generic wire protocol support functions
1 # wireproto.py - generic wire protocol support functions
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import urllib, tempfile, os, sys
8 import urllib, tempfile, os, sys
9 from i18n import _
9 from i18n import _
10 from node import bin, hex
10 from node import bin, hex
11 import changegroup as changegroupmod
11 import changegroup as changegroupmod
12 import peer, error, encoding, util, store
12 import peer, error, encoding, util, store
13
13
14
14
15 class abstractserverproto(object):
15 class abstractserverproto(object):
16 """abstract class that summarizes the protocol API
16 """abstract class that summarizes the protocol API
17
17
18 Used as reference and documentation.
18 Used as reference and documentation.
19 """
19 """
20
20
21 def getargs(self, args):
21 def getargs(self, args):
22 """return the value for arguments in <args>
22 """return the value for arguments in <args>
23
23
24 returns a list of values (same order as <args>)"""
24 returns a list of values (same order as <args>)"""
25 raise NotImplementedError()
25 raise NotImplementedError()
26
26
27 def getfile(self, fp):
27 def getfile(self, fp):
28 """write the whole content of a file into a file like object
28 """write the whole content of a file into a file like object
29
29
30 The file is in the form::
30 The file is in the form::
31
31
32 (<chunk-size>\n<chunk>)+0\n
32 (<chunk-size>\n<chunk>)+0\n
33
33
34 chunk size is the ascii version of the int.
34 chunk size is the ascii version of the int.
35 """
35 """
36 raise NotImplementedError()
36 raise NotImplementedError()
37
37
38 def redirect(self):
38 def redirect(self):
39 """may setup interception for stdout and stderr
39 """may setup interception for stdout and stderr
40
40
41 See also the `restore` method."""
41 See also the `restore` method."""
42 raise NotImplementedError()
42 raise NotImplementedError()
43
43
44 # If the `redirect` function does install interception, the `restore`
44 # If the `redirect` function does install interception, the `restore`
45 # function MUST be defined. If interception is not used, this function
45 # function MUST be defined. If interception is not used, this function
46 # MUST NOT be defined.
46 # MUST NOT be defined.
47 #
47 #
48 # left commented here on purpose
48 # left commented here on purpose
49 #
49 #
50 #def restore(self):
50 #def restore(self):
51 # """reinstall previous stdout and stderr and return intercepted stdout
51 # """reinstall previous stdout and stderr and return intercepted stdout
52 # """
52 # """
53 # raise NotImplementedError()
53 # raise NotImplementedError()
54
54
55 def groupchunks(self, cg):
55 def groupchunks(self, cg):
56 """return 4096 chunks from a changegroup object
56 """return 4096 chunks from a changegroup object
57
57
58 Some protocols may have compressed the contents."""
58 Some protocols may have compressed the contents."""
59 raise NotImplementedError()
59 raise NotImplementedError()
60
60
61 # abstract batching support
61 # abstract batching support
62
62
63 class future(object):
63 class future(object):
64 '''placeholder for a value to be set later'''
64 '''placeholder for a value to be set later'''
65 def set(self, value):
65 def set(self, value):
66 if util.safehasattr(self, 'value'):
66 if util.safehasattr(self, 'value'):
67 raise error.RepoError("future is already set")
67 raise error.RepoError("future is already set")
68 self.value = value
68 self.value = value
69
69
70 class batcher(object):
70 class batcher(object):
71 '''base class for batches of commands submittable in a single request
71 '''base class for batches of commands submittable in a single request
72
72
73 All methods invoked on instances of this class are simply queued and
73 All methods invoked on instances of this class are simply queued and
74 return a a future for the result. Once you call submit(), all the queued
74 return a a future for the result. Once you call submit(), all the queued
75 calls are performed and the results set in their respective futures.
75 calls are performed and the results set in their respective futures.
76 '''
76 '''
77 def __init__(self):
77 def __init__(self):
78 self.calls = []
78 self.calls = []
79 def __getattr__(self, name):
79 def __getattr__(self, name):
80 def call(*args, **opts):
80 def call(*args, **opts):
81 resref = future()
81 resref = future()
82 self.calls.append((name, args, opts, resref,))
82 self.calls.append((name, args, opts, resref,))
83 return resref
83 return resref
84 return call
84 return call
85 def submit(self):
85 def submit(self):
86 pass
86 pass
87
87
88 class localbatch(batcher):
88 class localbatch(batcher):
89 '''performs the queued calls directly'''
89 '''performs the queued calls directly'''
90 def __init__(self, local):
90 def __init__(self, local):
91 batcher.__init__(self)
91 batcher.__init__(self)
92 self.local = local
92 self.local = local
93 def submit(self):
93 def submit(self):
94 for name, args, opts, resref in self.calls:
94 for name, args, opts, resref in self.calls:
95 resref.set(getattr(self.local, name)(*args, **opts))
95 resref.set(getattr(self.local, name)(*args, **opts))
96
96
97 class remotebatch(batcher):
97 class remotebatch(batcher):
98 '''batches the queued calls; uses as few roundtrips as possible'''
98 '''batches the queued calls; uses as few roundtrips as possible'''
99 def __init__(self, remote):
99 def __init__(self, remote):
100 '''remote must support _submitbatch(encbatch) and
100 '''remote must support _submitbatch(encbatch) and
101 _submitone(op, encargs)'''
101 _submitone(op, encargs)'''
102 batcher.__init__(self)
102 batcher.__init__(self)
103 self.remote = remote
103 self.remote = remote
104 def submit(self):
104 def submit(self):
105 req, rsp = [], []
105 req, rsp = [], []
106 for name, args, opts, resref in self.calls:
106 for name, args, opts, resref in self.calls:
107 mtd = getattr(self.remote, name)
107 mtd = getattr(self.remote, name)
108 batchablefn = getattr(mtd, 'batchable', None)
108 batchablefn = getattr(mtd, 'batchable', None)
109 if batchablefn is not None:
109 if batchablefn is not None:
110 batchable = batchablefn(mtd.im_self, *args, **opts)
110 batchable = batchablefn(mtd.im_self, *args, **opts)
111 encargsorres, encresref = batchable.next()
111 encargsorres, encresref = batchable.next()
112 if encresref:
112 if encresref:
113 req.append((name, encargsorres,))
113 req.append((name, encargsorres,))
114 rsp.append((batchable, encresref, resref,))
114 rsp.append((batchable, encresref, resref,))
115 else:
115 else:
116 resref.set(encargsorres)
116 resref.set(encargsorres)
117 else:
117 else:
118 if req:
118 if req:
119 self._submitreq(req, rsp)
119 self._submitreq(req, rsp)
120 req, rsp = [], []
120 req, rsp = [], []
121 resref.set(mtd(*args, **opts))
121 resref.set(mtd(*args, **opts))
122 if req:
122 if req:
123 self._submitreq(req, rsp)
123 self._submitreq(req, rsp)
124 def _submitreq(self, req, rsp):
124 def _submitreq(self, req, rsp):
125 encresults = self.remote._submitbatch(req)
125 encresults = self.remote._submitbatch(req)
126 for encres, r in zip(encresults, rsp):
126 for encres, r in zip(encresults, rsp):
127 batchable, encresref, resref = r
127 batchable, encresref, resref = r
128 encresref.set(encres)
128 encresref.set(encres)
129 resref.set(batchable.next())
129 resref.set(batchable.next())
130
130
131 def batchable(f):
131 def batchable(f):
132 '''annotation for batchable methods
132 '''annotation for batchable methods
133
133
134 Such methods must implement a coroutine as follows:
134 Such methods must implement a coroutine as follows:
135
135
136 @batchable
136 @batchable
137 def sample(self, one, two=None):
137 def sample(self, one, two=None):
138 # Handle locally computable results first:
138 # Handle locally computable results first:
139 if not one:
139 if not one:
140 yield "a local result", None
140 yield "a local result", None
141 # Build list of encoded arguments suitable for your wire protocol:
141 # Build list of encoded arguments suitable for your wire protocol:
142 encargs = [('one', encode(one),), ('two', encode(two),)]
142 encargs = [('one', encode(one),), ('two', encode(two),)]
143 # Create future for injection of encoded result:
143 # Create future for injection of encoded result:
144 encresref = future()
144 encresref = future()
145 # Return encoded arguments and future:
145 # Return encoded arguments and future:
146 yield encargs, encresref
146 yield encargs, encresref
147 # Assuming the future to be filled with the result from the batched
147 # Assuming the future to be filled with the result from the batched
148 # request now. Decode it:
148 # request now. Decode it:
149 yield decode(encresref.value)
149 yield decode(encresref.value)
150
150
151 The decorator returns a function which wraps this coroutine as a plain
151 The decorator returns a function which wraps this coroutine as a plain
152 method, but adds the original method as an attribute called "batchable",
152 method, but adds the original method as an attribute called "batchable",
153 which is used by remotebatch to split the call into separate encoding and
153 which is used by remotebatch to split the call into separate encoding and
154 decoding phases.
154 decoding phases.
155 '''
155 '''
156 def plain(*args, **opts):
156 def plain(*args, **opts):
157 batchable = f(*args, **opts)
157 batchable = f(*args, **opts)
158 encargsorres, encresref = batchable.next()
158 encargsorres, encresref = batchable.next()
159 if not encresref:
159 if not encresref:
160 return encargsorres # a local result in this case
160 return encargsorres # a local result in this case
161 self = args[0]
161 self = args[0]
162 encresref.set(self._submitone(f.func_name, encargsorres))
162 encresref.set(self._submitone(f.func_name, encargsorres))
163 return batchable.next()
163 return batchable.next()
164 setattr(plain, 'batchable', f)
164 setattr(plain, 'batchable', f)
165 return plain
165 return plain
166
166
167 # list of nodes encoding / decoding
167 # list of nodes encoding / decoding
168
168
169 def decodelist(l, sep=' '):
169 def decodelist(l, sep=' '):
170 if l:
170 if l:
171 return map(bin, l.split(sep))
171 return map(bin, l.split(sep))
172 return []
172 return []
173
173
174 def encodelist(l, sep=' '):
174 def encodelist(l, sep=' '):
175 return sep.join(map(hex, l))
175 return sep.join(map(hex, l))
176
176
177 # batched call argument encoding
177 # batched call argument encoding
178
178
179 def escapearg(plain):
179 def escapearg(plain):
180 return (plain
180 return (plain
181 .replace(':', '::')
181 .replace(':', '::')
182 .replace(',', ':,')
182 .replace(',', ':,')
183 .replace(';', ':;')
183 .replace(';', ':;')
184 .replace('=', ':='))
184 .replace('=', ':='))
185
185
186 def unescapearg(escaped):
186 def unescapearg(escaped):
187 return (escaped
187 return (escaped
188 .replace(':=', '=')
188 .replace(':=', '=')
189 .replace(':;', ';')
189 .replace(':;', ';')
190 .replace(':,', ',')
190 .replace(':,', ',')
191 .replace('::', ':'))
191 .replace('::', ':'))
192
192
193 # client side
193 # client side
194
194
195 class wirepeer(peer.peerrepository):
195 class wirepeer(peer.peerrepository):
196
196
197 def batch(self):
197 def batch(self):
198 return remotebatch(self)
198 return remotebatch(self)
199 def _submitbatch(self, req):
199 def _submitbatch(self, req):
200 cmds = []
200 cmds = []
201 for op, argsdict in req:
201 for op, argsdict in req:
202 args = ','.join('%s=%s' % p for p in argsdict.iteritems())
202 args = ','.join('%s=%s' % p for p in argsdict.iteritems())
203 cmds.append('%s %s' % (op, args))
203 cmds.append('%s %s' % (op, args))
204 rsp = self._call("batch", cmds=';'.join(cmds))
204 rsp = self._call("batch", cmds=';'.join(cmds))
205 return rsp.split(';')
205 return rsp.split(';')
206 def _submitone(self, op, args):
206 def _submitone(self, op, args):
207 return self._call(op, **args)
207 return self._call(op, **args)
208
208
209 @batchable
209 @batchable
210 def lookup(self, key):
210 def lookup(self, key):
211 self.requirecap('lookup', _('look up remote revision'))
211 self.requirecap('lookup', _('look up remote revision'))
212 f = future()
212 f = future()
213 yield {'key': encoding.fromlocal(key)}, f
213 yield {'key': encoding.fromlocal(key)}, f
214 d = f.value
214 d = f.value
215 success, data = d[:-1].split(" ", 1)
215 success, data = d[:-1].split(" ", 1)
216 if int(success):
216 if int(success):
217 yield bin(data)
217 yield bin(data)
218 self._abort(error.RepoError(data))
218 self._abort(error.RepoError(data))
219
219
220 @batchable
220 @batchable
221 def heads(self):
221 def heads(self):
222 f = future()
222 f = future()
223 yield {}, f
223 yield {}, f
224 d = f.value
224 d = f.value
225 try:
225 try:
226 yield decodelist(d[:-1])
226 yield decodelist(d[:-1])
227 except ValueError:
227 except ValueError:
228 self._abort(error.ResponseError(_("unexpected response:"), d))
228 self._abort(error.ResponseError(_("unexpected response:"), d))
229
229
230 @batchable
230 @batchable
231 def known(self, nodes):
231 def known(self, nodes):
232 f = future()
232 f = future()
233 yield {'nodes': encodelist(nodes)}, f
233 yield {'nodes': encodelist(nodes)}, f
234 d = f.value
234 d = f.value
235 try:
235 try:
236 yield [bool(int(f)) for f in d]
236 yield [bool(int(f)) for f in d]
237 except ValueError:
237 except ValueError:
238 self._abort(error.ResponseError(_("unexpected response:"), d))
238 self._abort(error.ResponseError(_("unexpected response:"), d))
239
239
240 @batchable
240 @batchable
241 def branchmap(self):
241 def branchmap(self):
242 f = future()
242 f = future()
243 yield {}, f
243 yield {}, f
244 d = f.value
244 d = f.value
245 try:
245 try:
246 branchmap = {}
246 branchmap = {}
247 for branchpart in d.splitlines():
247 for branchpart in d.splitlines():
248 branchname, branchheads = branchpart.split(' ', 1)
248 branchname, branchheads = branchpart.split(' ', 1)
249 branchname = encoding.tolocal(urllib.unquote(branchname))
249 branchname = encoding.tolocal(urllib.unquote(branchname))
250 branchheads = decodelist(branchheads)
250 branchheads = decodelist(branchheads)
251 branchmap[branchname] = branchheads
251 branchmap[branchname] = branchheads
252 yield branchmap
252 yield branchmap
253 except TypeError:
253 except TypeError:
254 self._abort(error.ResponseError(_("unexpected response:"), d))
254 self._abort(error.ResponseError(_("unexpected response:"), d))
255
255
256 def branches(self, nodes):
256 def branches(self, nodes):
257 n = encodelist(nodes)
257 n = encodelist(nodes)
258 d = self._call("branches", nodes=n)
258 d = self._call("branches", nodes=n)
259 try:
259 try:
260 br = [tuple(decodelist(b)) for b in d.splitlines()]
260 br = [tuple(decodelist(b)) for b in d.splitlines()]
261 return br
261 return br
262 except ValueError:
262 except ValueError:
263 self._abort(error.ResponseError(_("unexpected response:"), d))
263 self._abort(error.ResponseError(_("unexpected response:"), d))
264
264
265 def between(self, pairs):
265 def between(self, pairs):
266 batch = 8 # avoid giant requests
266 batch = 8 # avoid giant requests
267 r = []
267 r = []
268 for i in xrange(0, len(pairs), batch):
268 for i in xrange(0, len(pairs), batch):
269 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
269 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
270 d = self._call("between", pairs=n)
270 d = self._call("between", pairs=n)
271 try:
271 try:
272 r.extend(l and decodelist(l) or [] for l in d.splitlines())
272 r.extend(l and decodelist(l) or [] for l in d.splitlines())
273 except ValueError:
273 except ValueError:
274 self._abort(error.ResponseError(_("unexpected response:"), d))
274 self._abort(error.ResponseError(_("unexpected response:"), d))
275 return r
275 return r
276
276
277 @batchable
277 @batchable
278 def pushkey(self, namespace, key, old, new):
278 def pushkey(self, namespace, key, old, new):
279 if not self.capable('pushkey'):
279 if not self.capable('pushkey'):
280 yield False, None
280 yield False, None
281 f = future()
281 f = future()
282 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
282 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
283 yield {'namespace': encoding.fromlocal(namespace),
283 yield {'namespace': encoding.fromlocal(namespace),
284 'key': encoding.fromlocal(key),
284 'key': encoding.fromlocal(key),
285 'old': encoding.fromlocal(old),
285 'old': encoding.fromlocal(old),
286 'new': encoding.fromlocal(new)}, f
286 'new': encoding.fromlocal(new)}, f
287 d = f.value
287 d = f.value
288 d, output = d.split('\n', 1)
288 d, output = d.split('\n', 1)
289 try:
289 try:
290 d = bool(int(d))
290 d = bool(int(d))
291 except ValueError:
291 except ValueError:
292 raise error.ResponseError(
292 raise error.ResponseError(
293 _('push failed (unexpected response):'), d)
293 _('push failed (unexpected response):'), d)
294 for l in output.splitlines(True):
294 for l in output.splitlines(True):
295 self.ui.status(_('remote: '), l)
295 self.ui.status(_('remote: '), l)
296 yield d
296 yield d
297
297
298 @batchable
298 @batchable
299 def listkeys(self, namespace):
299 def listkeys(self, namespace):
300 if not self.capable('pushkey'):
300 if not self.capable('pushkey'):
301 yield {}, None
301 yield {}, None
302 f = future()
302 f = future()
303 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
303 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
304 yield {'namespace': encoding.fromlocal(namespace)}, f
304 yield {'namespace': encoding.fromlocal(namespace)}, f
305 d = f.value
305 d = f.value
306 r = {}
306 r = {}
307 for l in d.splitlines():
307 for l in d.splitlines():
308 k, v = l.split('\t')
308 k, v = l.split('\t')
309 r[encoding.tolocal(k)] = encoding.tolocal(v)
309 r[encoding.tolocal(k)] = encoding.tolocal(v)
310 yield r
310 yield r
311
311
312 def stream_out(self):
312 def stream_out(self):
313 return self._callstream('stream_out')
313 return self._callstream('stream_out')
314
314
315 def changegroup(self, nodes, kind):
315 def changegroup(self, nodes, kind):
316 n = encodelist(nodes)
316 n = encodelist(nodes)
317 f = self._callcompressable("changegroup", roots=n)
317 f = self._callcompressable("changegroup", roots=n)
318 return changegroupmod.unbundle10(f, 'UN')
318 return changegroupmod.unbundle10(f, 'UN')
319
319
320 def changegroupsubset(self, bases, heads, kind):
320 def changegroupsubset(self, bases, heads, kind):
321 self.requirecap('changegroupsubset', _('look up remote changes'))
321 self.requirecap('changegroupsubset', _('look up remote changes'))
322 bases = encodelist(bases)
322 bases = encodelist(bases)
323 heads = encodelist(heads)
323 heads = encodelist(heads)
324 f = self._callcompressable("changegroupsubset",
324 f = self._callcompressable("changegroupsubset",
325 bases=bases, heads=heads)
325 bases=bases, heads=heads)
326 return changegroupmod.unbundle10(f, 'UN')
326 return changegroupmod.unbundle10(f, 'UN')
327
327
328 def getbundle(self, source, heads=None, common=None, bundlecaps=None):
328 def getbundle(self, source, heads=None, common=None, bundlecaps=None):
329 self.requirecap('getbundle', _('look up remote changes'))
329 self.requirecap('getbundle', _('look up remote changes'))
330 opts = {}
330 opts = {}
331 if heads is not None:
331 if heads is not None:
332 opts['heads'] = encodelist(heads)
332 opts['heads'] = encodelist(heads)
333 if common is not None:
333 if common is not None:
334 opts['common'] = encodelist(common)
334 opts['common'] = encodelist(common)
335 if bundlecaps is not None:
335 if bundlecaps is not None:
336 opts['bundlecaps'] = ','.join(bundlecaps)
336 opts['bundlecaps'] = ','.join(bundlecaps)
337 f = self._callcompressable("getbundle", **opts)
337 f = self._callcompressable("getbundle", **opts)
338 return changegroupmod.unbundle10(f, 'UN')
338 return changegroupmod.unbundle10(f, 'UN')
339
339
340 def unbundle(self, cg, heads, source):
340 def unbundle(self, cg, heads, source):
341 '''Send cg (a readable file-like object representing the
341 '''Send cg (a readable file-like object representing the
342 changegroup to push, typically a chunkbuffer object) to the
342 changegroup to push, typically a chunkbuffer object) to the
343 remote server as a bundle. Return an integer indicating the
343 remote server as a bundle. Return an integer indicating the
344 result of the push (see localrepository.addchangegroup()).'''
344 result of the push (see localrepository.addchangegroup()).'''
345
345
346 if heads != ['force'] and self.capable('unbundlehash'):
346 if heads != ['force'] and self.capable('unbundlehash'):
347 heads = encodelist(['hashed',
347 heads = encodelist(['hashed',
348 util.sha1(''.join(sorted(heads))).digest()])
348 util.sha1(''.join(sorted(heads))).digest()])
349 else:
349 else:
350 heads = encodelist(heads)
350 heads = encodelist(heads)
351
351
352 ret, output = self._callpush("unbundle", cg, heads=heads)
352 ret, output = self._callpush("unbundle", cg, heads=heads)
353 if ret == "":
353 if ret == "":
354 raise error.ResponseError(
354 raise error.ResponseError(
355 _('push failed:'), output)
355 _('push failed:'), output)
356 try:
356 try:
357 ret = int(ret)
357 ret = int(ret)
358 except ValueError:
358 except ValueError:
359 raise error.ResponseError(
359 raise error.ResponseError(
360 _('push failed (unexpected response):'), ret)
360 _('push failed (unexpected response):'), ret)
361
361
362 for l in output.splitlines(True):
362 for l in output.splitlines(True):
363 self.ui.status(_('remote: '), l)
363 self.ui.status(_('remote: '), l)
364 return ret
364 return ret
365
365
366 def debugwireargs(self, one, two, three=None, four=None, five=None):
366 def debugwireargs(self, one, two, three=None, four=None, five=None):
367 # don't pass optional arguments left at their default value
367 # don't pass optional arguments left at their default value
368 opts = {}
368 opts = {}
369 if three is not None:
369 if three is not None:
370 opts['three'] = three
370 opts['three'] = three
371 if four is not None:
371 if four is not None:
372 opts['four'] = four
372 opts['four'] = four
373 return self._call('debugwireargs', one=one, two=two, **opts)
373 return self._call('debugwireargs', one=one, two=two, **opts)
374
374
375 def _call(self, cmd, **args):
375 def _call(self, cmd, **args):
376 """execute <cmd> on the server
376 """execute <cmd> on the server
377
377
378 The command is expected to return a simple string.
378 The command is expected to return a simple string.
379
379
380 returns the server reply as a string."""
380 returns the server reply as a string."""
381 raise NotImplementedError()
381 raise NotImplementedError()
382
382
383 def _callstream(self, cmd, **args):
383 def _callstream(self, cmd, **args):
384 """execute <cmd> on the server
384 """execute <cmd> on the server
385
385
386 The command is expected to return a stream.
386 The command is expected to return a stream.
387
387
388 returns the server reply as a file like object."""
388 returns the server reply as a file like object."""
389 raise NotImplementedError()
389 raise NotImplementedError()
390
390
391 def _callcompressable(self, cmd, **args):
391 def _callcompressable(self, cmd, **args):
392 """execute <cmd> on the server
392 """execute <cmd> on the server
393
393
394 The command is expected to return a stream.
394 The command is expected to return a stream.
395
395
396 The stream may have been compressed in some implementaitons. This
396 The stream may have been compressed in some implementaitons. This
397 function takes care of the decompression. This is the only difference
397 function takes care of the decompression. This is the only difference
398 with _callstream.
398 with _callstream.
399
399
400 returns the server reply as a file like object.
400 returns the server reply as a file like object.
401 """
401 """
402 raise NotImplementedError()
402 raise NotImplementedError()
403
403
404 def _callpush(self, cmd, fp, **args):
404 def _callpush(self, cmd, fp, **args):
405 """execute a <cmd> on server
405 """execute a <cmd> on server
406
406
407 The command is expected to be related to a push. Push has a special
407 The command is expected to be related to a push. Push has a special
408 return method.
408 return method.
409
409
410 returns the server reply as a (ret, output) tuple. ret is either
410 returns the server reply as a (ret, output) tuple. ret is either
411 empty (error) or a stringified int.
411 empty (error) or a stringified int.
412 """
412 """
413 raise NotImplementedError()
413 raise NotImplementedError()
414
414
415 def _abort(self, exception):
415 def _abort(self, exception):
416 """clearly abort the wire protocol connection and raise the exception
416 """clearly abort the wire protocol connection and raise the exception
417 """
417 """
418 raise NotImplementedError()
418 raise NotImplementedError()
419
419
420 # server side
420 # server side
421
421
422 # wire protocol command can either return a string or one of these classes.
422 # wire protocol command can either return a string or one of these classes.
423 class streamres(object):
423 class streamres(object):
424 """wireproto reply: binary stream
424 """wireproto reply: binary stream
425
425
426 The call was successful and the result is a stream.
426 The call was successful and the result is a stream.
427 Iterate on the `self.gen` attribute to retrieve chunks.
427 Iterate on the `self.gen` attribute to retrieve chunks.
428 """
428 """
429 def __init__(self, gen):
429 def __init__(self, gen):
430 self.gen = gen
430 self.gen = gen
431
431
432 class pushres(object):
432 class pushres(object):
433 """wireproto reply: success with simple integer return
433 """wireproto reply: success with simple integer return
434
434
435 The call was successful and returned an integer contained in `self.res`.
435 The call was successful and returned an integer contained in `self.res`.
436 """
436 """
437 def __init__(self, res):
437 def __init__(self, res):
438 self.res = res
438 self.res = res
439
439
440 class pusherr(object):
440 class pusherr(object):
441 """wireproto reply: failure
441 """wireproto reply: failure
442
442
443 The call failed. The `self.res` attribute contains the error message.
443 The call failed. The `self.res` attribute contains the error message.
444 """
444 """
445 def __init__(self, res):
445 def __init__(self, res):
446 self.res = res
446 self.res = res
447
447
448 class ooberror(object):
448 class ooberror(object):
449 """wireproto reply: failure of a batch of operation
449 """wireproto reply: failure of a batch of operation
450
450
451 Something failed during a batch call. The error message is stored in
451 Something failed during a batch call. The error message is stored in
452 `self.message`.
452 `self.message`.
453 """
453 """
454 def __init__(self, message):
454 def __init__(self, message):
455 self.message = message
455 self.message = message
456
456
457 def dispatch(repo, proto, command):
457 def dispatch(repo, proto, command):
458 repo = repo.filtered("served")
458 repo = repo.filtered("served")
459 func, spec = commands[command]
459 func, spec = commands[command]
460 args = proto.getargs(spec)
460 args = proto.getargs(spec)
461 return func(repo, proto, *args)
461 return func(repo, proto, *args)
462
462
463 def options(cmd, keys, others):
463 def options(cmd, keys, others):
464 opts = {}
464 opts = {}
465 for k in keys:
465 for k in keys:
466 if k in others:
466 if k in others:
467 opts[k] = others[k]
467 opts[k] = others[k]
468 del others[k]
468 del others[k]
469 if others:
469 if others:
470 sys.stderr.write("abort: %s got unexpected arguments %s\n"
470 sys.stderr.write("abort: %s got unexpected arguments %s\n"
471 % (cmd, ",".join(others)))
471 % (cmd, ",".join(others)))
472 return opts
472 return opts
473
473
474 # list of commands
474 # list of commands
475 commands = {}
475 commands = {}
476
476
477 def wireprotocommand(name, args=''):
477 def wireprotocommand(name, args=''):
478 """decorator for wireprotocol command"""
478 """decorator for wireprotocol command"""
479 def register(func):
479 def register(func):
480 commands[name] = (func, args)
480 commands[name] = (func, args)
481 return func
481 return func
482 return register
482 return register
483
483
484 @wireprotocommand('batch', 'cmds *')
484 @wireprotocommand('batch', 'cmds *')
485 def batch(repo, proto, cmds, others):
485 def batch(repo, proto, cmds, others):
486 repo = repo.filtered("served")
486 repo = repo.filtered("served")
487 res = []
487 res = []
488 for pair in cmds.split(';'):
488 for pair in cmds.split(';'):
489 op, args = pair.split(' ', 1)
489 op, args = pair.split(' ', 1)
490 vals = {}
490 vals = {}
491 for a in args.split(','):
491 for a in args.split(','):
492 if a:
492 if a:
493 n, v = a.split('=')
493 n, v = a.split('=')
494 vals[n] = unescapearg(v)
494 vals[n] = unescapearg(v)
495 func, spec = commands[op]
495 func, spec = commands[op]
496 if spec:
496 if spec:
497 keys = spec.split()
497 keys = spec.split()
498 data = {}
498 data = {}
499 for k in keys:
499 for k in keys:
500 if k == '*':
500 if k == '*':
501 star = {}
501 star = {}
502 for key in vals.keys():
502 for key in vals.keys():
503 if key not in keys:
503 if key not in keys:
504 star[key] = vals[key]
504 star[key] = vals[key]
505 data['*'] = star
505 data['*'] = star
506 else:
506 else:
507 data[k] = vals[k]
507 data[k] = vals[k]
508 result = func(repo, proto, *[data[k] for k in keys])
508 result = func(repo, proto, *[data[k] for k in keys])
509 else:
509 else:
510 result = func(repo, proto)
510 result = func(repo, proto)
511 if isinstance(result, ooberror):
511 if isinstance(result, ooberror):
512 return result
512 return result
513 res.append(escapearg(result))
513 res.append(escapearg(result))
514 return ';'.join(res)
514 return ';'.join(res)
515
515
516 @wireprotocommand('between', 'pairs')
516 def between(repo, proto, pairs):
517 def between(repo, proto, pairs):
517 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
518 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
518 r = []
519 r = []
519 for b in repo.between(pairs):
520 for b in repo.between(pairs):
520 r.append(encodelist(b) + "\n")
521 r.append(encodelist(b) + "\n")
521 return "".join(r)
522 return "".join(r)
522
523
523 def branchmap(repo, proto):
524 def branchmap(repo, proto):
524 branchmap = repo.branchmap()
525 branchmap = repo.branchmap()
525 heads = []
526 heads = []
526 for branch, nodes in branchmap.iteritems():
527 for branch, nodes in branchmap.iteritems():
527 branchname = urllib.quote(encoding.fromlocal(branch))
528 branchname = urllib.quote(encoding.fromlocal(branch))
528 branchnodes = encodelist(nodes)
529 branchnodes = encodelist(nodes)
529 heads.append('%s %s' % (branchname, branchnodes))
530 heads.append('%s %s' % (branchname, branchnodes))
530 return '\n'.join(heads)
531 return '\n'.join(heads)
531
532
532 def branches(repo, proto, nodes):
533 def branches(repo, proto, nodes):
533 nodes = decodelist(nodes)
534 nodes = decodelist(nodes)
534 r = []
535 r = []
535 for b in repo.branches(nodes):
536 for b in repo.branches(nodes):
536 r.append(encodelist(b) + "\n")
537 r.append(encodelist(b) + "\n")
537 return "".join(r)
538 return "".join(r)
538
539
539
540
540 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
541 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
541 'known', 'getbundle', 'unbundlehash', 'batch']
542 'known', 'getbundle', 'unbundlehash', 'batch']
542
543
543 def _capabilities(repo, proto):
544 def _capabilities(repo, proto):
544 """return a list of capabilities for a repo
545 """return a list of capabilities for a repo
545
546
546 This function exists to allow extensions to easily wrap capabilities
547 This function exists to allow extensions to easily wrap capabilities
547 computation
548 computation
548
549
549 - returns a lists: easy to alter
550 - returns a lists: easy to alter
550 - change done here will be propagated to both `capabilities` and `hello`
551 - change done here will be propagated to both `capabilities` and `hello`
551 command without any other effort. without any other action needed.
552 command without any other effort. without any other action needed.
552 """
553 """
553 # copy to prevent modification of the global list
554 # copy to prevent modification of the global list
554 caps = list(wireprotocaps)
555 caps = list(wireprotocaps)
555 if _allowstream(repo.ui):
556 if _allowstream(repo.ui):
556 if repo.ui.configbool('server', 'preferuncompressed', False):
557 if repo.ui.configbool('server', 'preferuncompressed', False):
557 caps.append('stream-preferred')
558 caps.append('stream-preferred')
558 requiredformats = repo.requirements & repo.supportedformats
559 requiredformats = repo.requirements & repo.supportedformats
559 # if our local revlogs are just revlogv1, add 'stream' cap
560 # if our local revlogs are just revlogv1, add 'stream' cap
560 if not requiredformats - set(('revlogv1',)):
561 if not requiredformats - set(('revlogv1',)):
561 caps.append('stream')
562 caps.append('stream')
562 # otherwise, add 'streamreqs' detailing our local revlog format
563 # otherwise, add 'streamreqs' detailing our local revlog format
563 else:
564 else:
564 caps.append('streamreqs=%s' % ','.join(requiredformats))
565 caps.append('streamreqs=%s' % ','.join(requiredformats))
565 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
566 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
566 caps.append('httpheader=1024')
567 caps.append('httpheader=1024')
567 return caps
568 return caps
568
569
569 # If you are writting and extension and consider wrapping this function. Wrap
570 # If you are writting and extension and consider wrapping this function. Wrap
570 # `_capabilities` instead.
571 # `_capabilities` instead.
571 def capabilities(repo, proto):
572 def capabilities(repo, proto):
572 return ' '.join(_capabilities(repo, proto))
573 return ' '.join(_capabilities(repo, proto))
573
574
574 def changegroup(repo, proto, roots):
575 def changegroup(repo, proto, roots):
575 nodes = decodelist(roots)
576 nodes = decodelist(roots)
576 cg = repo.changegroup(nodes, 'serve')
577 cg = repo.changegroup(nodes, 'serve')
577 return streamres(proto.groupchunks(cg))
578 return streamres(proto.groupchunks(cg))
578
579
579 def changegroupsubset(repo, proto, bases, heads):
580 def changegroupsubset(repo, proto, bases, heads):
580 bases = decodelist(bases)
581 bases = decodelist(bases)
581 heads = decodelist(heads)
582 heads = decodelist(heads)
582 cg = repo.changegroupsubset(bases, heads, 'serve')
583 cg = repo.changegroupsubset(bases, heads, 'serve')
583 return streamres(proto.groupchunks(cg))
584 return streamres(proto.groupchunks(cg))
584
585
585 def debugwireargs(repo, proto, one, two, others):
586 def debugwireargs(repo, proto, one, two, others):
586 # only accept optional args from the known set
587 # only accept optional args from the known set
587 opts = options('debugwireargs', ['three', 'four'], others)
588 opts = options('debugwireargs', ['three', 'four'], others)
588 return repo.debugwireargs(one, two, **opts)
589 return repo.debugwireargs(one, two, **opts)
589
590
590 def getbundle(repo, proto, others):
591 def getbundle(repo, proto, others):
591 opts = options('getbundle', ['heads', 'common', 'bundlecaps'], others)
592 opts = options('getbundle', ['heads', 'common', 'bundlecaps'], others)
592 for k, v in opts.iteritems():
593 for k, v in opts.iteritems():
593 if k in ('heads', 'common'):
594 if k in ('heads', 'common'):
594 opts[k] = decodelist(v)
595 opts[k] = decodelist(v)
595 elif k == 'bundlecaps':
596 elif k == 'bundlecaps':
596 opts[k] = set(v.split(','))
597 opts[k] = set(v.split(','))
597 cg = repo.getbundle('serve', **opts)
598 cg = repo.getbundle('serve', **opts)
598 return streamres(proto.groupchunks(cg))
599 return streamres(proto.groupchunks(cg))
599
600
600 def heads(repo, proto):
601 def heads(repo, proto):
601 h = repo.heads()
602 h = repo.heads()
602 return encodelist(h) + "\n"
603 return encodelist(h) + "\n"
603
604
604 def hello(repo, proto):
605 def hello(repo, proto):
605 '''the hello command returns a set of lines describing various
606 '''the hello command returns a set of lines describing various
606 interesting things about the server, in an RFC822-like format.
607 interesting things about the server, in an RFC822-like format.
607 Currently the only one defined is "capabilities", which
608 Currently the only one defined is "capabilities", which
608 consists of a line in the form:
609 consists of a line in the form:
609
610
610 capabilities: space separated list of tokens
611 capabilities: space separated list of tokens
611 '''
612 '''
612 return "capabilities: %s\n" % (capabilities(repo, proto))
613 return "capabilities: %s\n" % (capabilities(repo, proto))
613
614
614 def listkeys(repo, proto, namespace):
615 def listkeys(repo, proto, namespace):
615 d = repo.listkeys(encoding.tolocal(namespace)).items()
616 d = repo.listkeys(encoding.tolocal(namespace)).items()
616 t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v))
617 t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v))
617 for k, v in d])
618 for k, v in d])
618 return t
619 return t
619
620
620 def lookup(repo, proto, key):
621 def lookup(repo, proto, key):
621 try:
622 try:
622 k = encoding.tolocal(key)
623 k = encoding.tolocal(key)
623 c = repo[k]
624 c = repo[k]
624 r = c.hex()
625 r = c.hex()
625 success = 1
626 success = 1
626 except Exception, inst:
627 except Exception, inst:
627 r = str(inst)
628 r = str(inst)
628 success = 0
629 success = 0
629 return "%s %s\n" % (success, r)
630 return "%s %s\n" % (success, r)
630
631
631 def known(repo, proto, nodes, others):
632 def known(repo, proto, nodes, others):
632 return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes)))
633 return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes)))
633
634
634 def pushkey(repo, proto, namespace, key, old, new):
635 def pushkey(repo, proto, namespace, key, old, new):
635 # compatibility with pre-1.8 clients which were accidentally
636 # compatibility with pre-1.8 clients which were accidentally
636 # sending raw binary nodes rather than utf-8-encoded hex
637 # sending raw binary nodes rather than utf-8-encoded hex
637 if len(new) == 20 and new.encode('string-escape') != new:
638 if len(new) == 20 and new.encode('string-escape') != new:
638 # looks like it could be a binary node
639 # looks like it could be a binary node
639 try:
640 try:
640 new.decode('utf-8')
641 new.decode('utf-8')
641 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
642 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
642 except UnicodeDecodeError:
643 except UnicodeDecodeError:
643 pass # binary, leave unmodified
644 pass # binary, leave unmodified
644 else:
645 else:
645 new = encoding.tolocal(new) # normal path
646 new = encoding.tolocal(new) # normal path
646
647
647 if util.safehasattr(proto, 'restore'):
648 if util.safehasattr(proto, 'restore'):
648
649
649 proto.redirect()
650 proto.redirect()
650
651
651 try:
652 try:
652 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
653 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
653 encoding.tolocal(old), new) or False
654 encoding.tolocal(old), new) or False
654 except util.Abort:
655 except util.Abort:
655 r = False
656 r = False
656
657
657 output = proto.restore()
658 output = proto.restore()
658
659
659 return '%s\n%s' % (int(r), output)
660 return '%s\n%s' % (int(r), output)
660
661
661 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
662 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
662 encoding.tolocal(old), new)
663 encoding.tolocal(old), new)
663 return '%s\n' % int(r)
664 return '%s\n' % int(r)
664
665
665 def _allowstream(ui):
666 def _allowstream(ui):
666 return ui.configbool('server', 'uncompressed', True, untrusted=True)
667 return ui.configbool('server', 'uncompressed', True, untrusted=True)
667
668
668 def _walkstreamfiles(repo):
669 def _walkstreamfiles(repo):
669 # this is it's own function so extensions can override it
670 # this is it's own function so extensions can override it
670 return repo.store.walk()
671 return repo.store.walk()
671
672
672 def stream(repo, proto):
673 def stream(repo, proto):
673 '''If the server supports streaming clone, it advertises the "stream"
674 '''If the server supports streaming clone, it advertises the "stream"
674 capability with a value representing the version and flags of the repo
675 capability with a value representing the version and flags of the repo
675 it is serving. Client checks to see if it understands the format.
676 it is serving. Client checks to see if it understands the format.
676
677
677 The format is simple: the server writes out a line with the amount
678 The format is simple: the server writes out a line with the amount
678 of files, then the total amount of bytes to be transferred (separated
679 of files, then the total amount of bytes to be transferred (separated
679 by a space). Then, for each file, the server first writes the filename
680 by a space). Then, for each file, the server first writes the filename
680 and filesize (separated by the null character), then the file contents.
681 and filesize (separated by the null character), then the file contents.
681 '''
682 '''
682
683
683 if not _allowstream(repo.ui):
684 if not _allowstream(repo.ui):
684 return '1\n'
685 return '1\n'
685
686
686 entries = []
687 entries = []
687 total_bytes = 0
688 total_bytes = 0
688 try:
689 try:
689 # get consistent snapshot of repo, lock during scan
690 # get consistent snapshot of repo, lock during scan
690 lock = repo.lock()
691 lock = repo.lock()
691 try:
692 try:
692 repo.ui.debug('scanning\n')
693 repo.ui.debug('scanning\n')
693 for name, ename, size in _walkstreamfiles(repo):
694 for name, ename, size in _walkstreamfiles(repo):
694 if size:
695 if size:
695 entries.append((name, size))
696 entries.append((name, size))
696 total_bytes += size
697 total_bytes += size
697 finally:
698 finally:
698 lock.release()
699 lock.release()
699 except error.LockError:
700 except error.LockError:
700 return '2\n' # error: 2
701 return '2\n' # error: 2
701
702
702 def streamer(repo, entries, total):
703 def streamer(repo, entries, total):
703 '''stream out all metadata files in repository.'''
704 '''stream out all metadata files in repository.'''
704 yield '0\n' # success
705 yield '0\n' # success
705 repo.ui.debug('%d files, %d bytes to transfer\n' %
706 repo.ui.debug('%d files, %d bytes to transfer\n' %
706 (len(entries), total_bytes))
707 (len(entries), total_bytes))
707 yield '%d %d\n' % (len(entries), total_bytes)
708 yield '%d %d\n' % (len(entries), total_bytes)
708
709
709 sopener = repo.sopener
710 sopener = repo.sopener
710 oldaudit = sopener.mustaudit
711 oldaudit = sopener.mustaudit
711 debugflag = repo.ui.debugflag
712 debugflag = repo.ui.debugflag
712 sopener.mustaudit = False
713 sopener.mustaudit = False
713
714
714 try:
715 try:
715 for name, size in entries:
716 for name, size in entries:
716 if debugflag:
717 if debugflag:
717 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
718 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
718 # partially encode name over the wire for backwards compat
719 # partially encode name over the wire for backwards compat
719 yield '%s\0%d\n' % (store.encodedir(name), size)
720 yield '%s\0%d\n' % (store.encodedir(name), size)
720 if size <= 65536:
721 if size <= 65536:
721 fp = sopener(name)
722 fp = sopener(name)
722 try:
723 try:
723 data = fp.read(size)
724 data = fp.read(size)
724 finally:
725 finally:
725 fp.close()
726 fp.close()
726 yield data
727 yield data
727 else:
728 else:
728 for chunk in util.filechunkiter(sopener(name), limit=size):
729 for chunk in util.filechunkiter(sopener(name), limit=size):
729 yield chunk
730 yield chunk
730 # replace with "finally:" when support for python 2.4 has been dropped
731 # replace with "finally:" when support for python 2.4 has been dropped
731 except Exception:
732 except Exception:
732 sopener.mustaudit = oldaudit
733 sopener.mustaudit = oldaudit
733 raise
734 raise
734 sopener.mustaudit = oldaudit
735 sopener.mustaudit = oldaudit
735
736
736 return streamres(streamer(repo, entries, total_bytes))
737 return streamres(streamer(repo, entries, total_bytes))
737
738
738 def unbundle(repo, proto, heads):
739 def unbundle(repo, proto, heads):
739 their_heads = decodelist(heads)
740 their_heads = decodelist(heads)
740
741
741 def check_heads():
742 def check_heads():
742 heads = repo.heads()
743 heads = repo.heads()
743 heads_hash = util.sha1(''.join(sorted(heads))).digest()
744 heads_hash = util.sha1(''.join(sorted(heads))).digest()
744 return (their_heads == ['force'] or their_heads == heads or
745 return (their_heads == ['force'] or their_heads == heads or
745 their_heads == ['hashed', heads_hash])
746 their_heads == ['hashed', heads_hash])
746
747
747 proto.redirect()
748 proto.redirect()
748
749
749 # fail early if possible
750 # fail early if possible
750 if not check_heads():
751 if not check_heads():
751 return pusherr('repository changed while preparing changes - '
752 return pusherr('repository changed while preparing changes - '
752 'please try again')
753 'please try again')
753
754
754 # write bundle data to temporary file because it can be big
755 # write bundle data to temporary file because it can be big
755 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
756 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
756 fp = os.fdopen(fd, 'wb+')
757 fp = os.fdopen(fd, 'wb+')
757 r = 0
758 r = 0
758 try:
759 try:
759 proto.getfile(fp)
760 proto.getfile(fp)
760 lock = repo.lock()
761 lock = repo.lock()
761 try:
762 try:
762 if not check_heads():
763 if not check_heads():
763 # someone else committed/pushed/unbundled while we
764 # someone else committed/pushed/unbundled while we
764 # were transferring data
765 # were transferring data
765 return pusherr('repository changed while uploading changes - '
766 return pusherr('repository changed while uploading changes - '
766 'please try again')
767 'please try again')
767
768
768 # push can proceed
769 # push can proceed
769 fp.seek(0)
770 fp.seek(0)
770 gen = changegroupmod.readbundle(fp, None)
771 gen = changegroupmod.readbundle(fp, None)
771
772
772 try:
773 try:
773 r = repo.addchangegroup(gen, 'serve', proto._client())
774 r = repo.addchangegroup(gen, 'serve', proto._client())
774 except util.Abort, inst:
775 except util.Abort, inst:
775 sys.stderr.write("abort: %s\n" % inst)
776 sys.stderr.write("abort: %s\n" % inst)
776 finally:
777 finally:
777 lock.release()
778 lock.release()
778 return pushres(r)
779 return pushres(r)
779
780
780 finally:
781 finally:
781 fp.close()
782 fp.close()
782 os.unlink(tempname)
783 os.unlink(tempname)
783
784
784 commands.update({
785 commands.update({
785 'between': (between, 'pairs'),
786 'branchmap': (branchmap, ''),
786 'branchmap': (branchmap, ''),
787 'branches': (branches, 'nodes'),
787 'branches': (branches, 'nodes'),
788 'capabilities': (capabilities, ''),
788 'capabilities': (capabilities, ''),
789 'changegroup': (changegroup, 'roots'),
789 'changegroup': (changegroup, 'roots'),
790 'changegroupsubset': (changegroupsubset, 'bases heads'),
790 'changegroupsubset': (changegroupsubset, 'bases heads'),
791 'debugwireargs': (debugwireargs, 'one two *'),
791 'debugwireargs': (debugwireargs, 'one two *'),
792 'getbundle': (getbundle, '*'),
792 'getbundle': (getbundle, '*'),
793 'heads': (heads, ''),
793 'heads': (heads, ''),
794 'hello': (hello, ''),
794 'hello': (hello, ''),
795 'known': (known, 'nodes *'),
795 'known': (known, 'nodes *'),
796 'listkeys': (listkeys, 'namespace'),
796 'listkeys': (listkeys, 'namespace'),
797 'lookup': (lookup, 'key'),
797 'lookup': (lookup, 'key'),
798 'pushkey': (pushkey, 'namespace key old new'),
798 'pushkey': (pushkey, 'namespace key old new'),
799 'stream_out': (stream, ''),
799 'stream_out': (stream, ''),
800 'unbundle': (unbundle, 'heads'),
800 'unbundle': (unbundle, 'heads'),
801 })
801 })
General Comments 0
You need to be logged in to leave comments. Login now