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