##// END OF EJS Templates
wireprotocol: distinguish list and set in getbundle argument...
Pierre-Yves David -
r25403:30ab130a default
parent child Browse files
Show More
@@ -1,835 +1,838
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, bundle2, pushkey as pushkeymod
12 12 import peer, error, encoding, util, exchange
13 13
14 14
15 15 class abstractserverproto(object):
16 16 """abstract class that summarizes the protocol API
17 17
18 18 Used as reference and documentation.
19 19 """
20 20
21 21 def getargs(self, args):
22 22 """return the value for arguments in <args>
23 23
24 24 returns a list of values (same order as <args>)"""
25 25 raise NotImplementedError()
26 26
27 27 def getfile(self, fp):
28 28 """write the whole content of a file into a file like object
29 29
30 30 The file is in the form::
31 31
32 32 (<chunk-size>\n<chunk>)+0\n
33 33
34 34 chunk size is the ascii version of the int.
35 35 """
36 36 raise NotImplementedError()
37 37
38 38 def redirect(self):
39 39 """may setup interception for stdout and stderr
40 40
41 41 See also the `restore` method."""
42 42 raise NotImplementedError()
43 43
44 44 # If the `redirect` function does install interception, the `restore`
45 45 # function MUST be defined. If interception is not used, this function
46 46 # MUST NOT be defined.
47 47 #
48 48 # left commented here on purpose
49 49 #
50 50 #def restore(self):
51 51 # """reinstall previous stdout and stderr and return intercepted stdout
52 52 # """
53 53 # raise NotImplementedError()
54 54
55 55 def groupchunks(self, cg):
56 56 """return 4096 chunks from a changegroup object
57 57
58 58 Some protocols may have compressed the contents."""
59 59 raise NotImplementedError()
60 60
61 61 # abstract batching support
62 62
63 63 class future(object):
64 64 '''placeholder for a value to be set later'''
65 65 def set(self, value):
66 66 if util.safehasattr(self, 'value'):
67 67 raise error.RepoError("future is already set")
68 68 self.value = value
69 69
70 70 class batcher(object):
71 71 '''base class for batches of commands submittable in a single request
72 72
73 73 All methods invoked on instances of this class are simply queued and
74 74 return a a future for the result. Once you call submit(), all the queued
75 75 calls are performed and the results set in their respective futures.
76 76 '''
77 77 def __init__(self):
78 78 self.calls = []
79 79 def __getattr__(self, name):
80 80 def call(*args, **opts):
81 81 resref = future()
82 82 self.calls.append((name, args, opts, resref,))
83 83 return resref
84 84 return call
85 85 def submit(self):
86 86 pass
87 87
88 88 class localbatch(batcher):
89 89 '''performs the queued calls directly'''
90 90 def __init__(self, local):
91 91 batcher.__init__(self)
92 92 self.local = local
93 93 def submit(self):
94 94 for name, args, opts, resref in self.calls:
95 95 resref.set(getattr(self.local, name)(*args, **opts))
96 96
97 97 class remotebatch(batcher):
98 98 '''batches the queued calls; uses as few roundtrips as possible'''
99 99 def __init__(self, remote):
100 100 '''remote must support _submitbatch(encbatch) and
101 101 _submitone(op, encargs)'''
102 102 batcher.__init__(self)
103 103 self.remote = remote
104 104 def submit(self):
105 105 req, rsp = [], []
106 106 for name, args, opts, resref in self.calls:
107 107 mtd = getattr(self.remote, name)
108 108 batchablefn = getattr(mtd, 'batchable', None)
109 109 if batchablefn is not None:
110 110 batchable = batchablefn(mtd.im_self, *args, **opts)
111 111 encargsorres, encresref = batchable.next()
112 112 if encresref:
113 113 req.append((name, encargsorres,))
114 114 rsp.append((batchable, encresref, resref,))
115 115 else:
116 116 resref.set(encargsorres)
117 117 else:
118 118 if req:
119 119 self._submitreq(req, rsp)
120 120 req, rsp = [], []
121 121 resref.set(mtd(*args, **opts))
122 122 if req:
123 123 self._submitreq(req, rsp)
124 124 def _submitreq(self, req, rsp):
125 125 encresults = self.remote._submitbatch(req)
126 126 for encres, r in zip(encresults, rsp):
127 127 batchable, encresref, resref = r
128 128 encresref.set(encres)
129 129 resref.set(batchable.next())
130 130
131 131 def batchable(f):
132 132 '''annotation for batchable methods
133 133
134 134 Such methods must implement a coroutine as follows:
135 135
136 136 @batchable
137 137 def sample(self, one, two=None):
138 138 # Handle locally computable results first:
139 139 if not one:
140 140 yield "a local result", None
141 141 # Build list of encoded arguments suitable for your wire protocol:
142 142 encargs = [('one', encode(one),), ('two', encode(two),)]
143 143 # Create future for injection of encoded result:
144 144 encresref = future()
145 145 # Return encoded arguments and future:
146 146 yield encargs, encresref
147 147 # Assuming the future to be filled with the result from the batched
148 148 # request now. Decode it:
149 149 yield decode(encresref.value)
150 150
151 151 The decorator returns a function which wraps this coroutine as a plain
152 152 method, but adds the original method as an attribute called "batchable",
153 153 which is used by remotebatch to split the call into separate encoding and
154 154 decoding phases.
155 155 '''
156 156 def plain(*args, **opts):
157 157 batchable = f(*args, **opts)
158 158 encargsorres, encresref = batchable.next()
159 159 if not encresref:
160 160 return encargsorres # a local result in this case
161 161 self = args[0]
162 162 encresref.set(self._submitone(f.func_name, encargsorres))
163 163 return batchable.next()
164 164 setattr(plain, 'batchable', f)
165 165 return plain
166 166
167 167 # list of nodes encoding / decoding
168 168
169 169 def decodelist(l, sep=' '):
170 170 if l:
171 171 return map(bin, l.split(sep))
172 172 return []
173 173
174 174 def encodelist(l, sep=' '):
175 175 try:
176 176 return sep.join(map(hex, l))
177 177 except TypeError:
178 178 print l
179 179 raise
180 180
181 181 # batched call argument encoding
182 182
183 183 def escapearg(plain):
184 184 return (plain
185 185 .replace(':', '::')
186 186 .replace(',', ':,')
187 187 .replace(';', ':;')
188 188 .replace('=', ':='))
189 189
190 190 def unescapearg(escaped):
191 191 return (escaped
192 192 .replace(':=', '=')
193 193 .replace(':;', ';')
194 194 .replace(':,', ',')
195 195 .replace('::', ':'))
196 196
197 197 # mapping of options accepted by getbundle and their types
198 198 #
199 199 # Meant to be extended by extensions. It is extensions responsibility to ensure
200 200 # such options are properly processed in exchange.getbundle.
201 201 #
202 202 # supported types are:
203 203 #
204 204 # :nodes: list of binary nodes
205 205 # :csv: list of comma-separated values
206 # :scsv: list of comma-separated values return as set
206 207 # :plain: string with no transformation needed.
207 208 gboptsmap = {'heads': 'nodes',
208 209 'common': 'nodes',
209 210 'obsmarkers': 'boolean',
210 'bundlecaps': 'csv',
211 'bundlecaps': 'scsv',
211 212 'listkeys': 'csv',
212 213 'cg': 'boolean'}
213 214
214 215 # client side
215 216
216 217 class wirepeer(peer.peerrepository):
217 218
218 219 def batch(self):
219 220 return remotebatch(self)
220 221 def _submitbatch(self, req):
221 222 cmds = []
222 223 for op, argsdict in req:
223 224 args = ','.join('%s=%s' % p for p in argsdict.iteritems())
224 225 cmds.append('%s %s' % (op, args))
225 226 rsp = self._call("batch", cmds=';'.join(cmds))
226 227 return rsp.split(';')
227 228 def _submitone(self, op, args):
228 229 return self._call(op, **args)
229 230
230 231 @batchable
231 232 def lookup(self, key):
232 233 self.requirecap('lookup', _('look up remote revision'))
233 234 f = future()
234 235 yield {'key': encoding.fromlocal(key)}, f
235 236 d = f.value
236 237 success, data = d[:-1].split(" ", 1)
237 238 if int(success):
238 239 yield bin(data)
239 240 self._abort(error.RepoError(data))
240 241
241 242 @batchable
242 243 def heads(self):
243 244 f = future()
244 245 yield {}, f
245 246 d = f.value
246 247 try:
247 248 yield decodelist(d[:-1])
248 249 except ValueError:
249 250 self._abort(error.ResponseError(_("unexpected response:"), d))
250 251
251 252 @batchable
252 253 def known(self, nodes):
253 254 f = future()
254 255 yield {'nodes': encodelist(nodes)}, f
255 256 d = f.value
256 257 try:
257 258 yield [bool(int(b)) for b in d]
258 259 except ValueError:
259 260 self._abort(error.ResponseError(_("unexpected response:"), d))
260 261
261 262 @batchable
262 263 def branchmap(self):
263 264 f = future()
264 265 yield {}, f
265 266 d = f.value
266 267 try:
267 268 branchmap = {}
268 269 for branchpart in d.splitlines():
269 270 branchname, branchheads = branchpart.split(' ', 1)
270 271 branchname = encoding.tolocal(urllib.unquote(branchname))
271 272 branchheads = decodelist(branchheads)
272 273 branchmap[branchname] = branchheads
273 274 yield branchmap
274 275 except TypeError:
275 276 self._abort(error.ResponseError(_("unexpected response:"), d))
276 277
277 278 def branches(self, nodes):
278 279 n = encodelist(nodes)
279 280 d = self._call("branches", nodes=n)
280 281 try:
281 282 br = [tuple(decodelist(b)) for b in d.splitlines()]
282 283 return br
283 284 except ValueError:
284 285 self._abort(error.ResponseError(_("unexpected response:"), d))
285 286
286 287 def between(self, pairs):
287 288 batch = 8 # avoid giant requests
288 289 r = []
289 290 for i in xrange(0, len(pairs), batch):
290 291 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
291 292 d = self._call("between", pairs=n)
292 293 try:
293 294 r.extend(l and decodelist(l) or [] for l in d.splitlines())
294 295 except ValueError:
295 296 self._abort(error.ResponseError(_("unexpected response:"), d))
296 297 return r
297 298
298 299 @batchable
299 300 def pushkey(self, namespace, key, old, new):
300 301 if not self.capable('pushkey'):
301 302 yield False, None
302 303 f = future()
303 304 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
304 305 yield {'namespace': encoding.fromlocal(namespace),
305 306 'key': encoding.fromlocal(key),
306 307 'old': encoding.fromlocal(old),
307 308 'new': encoding.fromlocal(new)}, f
308 309 d = f.value
309 310 d, output = d.split('\n', 1)
310 311 try:
311 312 d = bool(int(d))
312 313 except ValueError:
313 314 raise error.ResponseError(
314 315 _('push failed (unexpected response):'), d)
315 316 for l in output.splitlines(True):
316 317 self.ui.status(_('remote: '), l)
317 318 yield d
318 319
319 320 @batchable
320 321 def listkeys(self, namespace):
321 322 if not self.capable('pushkey'):
322 323 yield {}, None
323 324 f = future()
324 325 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
325 326 yield {'namespace': encoding.fromlocal(namespace)}, f
326 327 d = f.value
327 328 self.ui.debug('received listkey for "%s": %i bytes\n'
328 329 % (namespace, len(d)))
329 330 yield pushkeymod.decodekeys(d)
330 331
331 332 def stream_out(self):
332 333 return self._callstream('stream_out')
333 334
334 335 def changegroup(self, nodes, kind):
335 336 n = encodelist(nodes)
336 337 f = self._callcompressable("changegroup", roots=n)
337 338 return changegroupmod.cg1unpacker(f, 'UN')
338 339
339 340 def changegroupsubset(self, bases, heads, kind):
340 341 self.requirecap('changegroupsubset', _('look up remote changes'))
341 342 bases = encodelist(bases)
342 343 heads = encodelist(heads)
343 344 f = self._callcompressable("changegroupsubset",
344 345 bases=bases, heads=heads)
345 346 return changegroupmod.cg1unpacker(f, 'UN')
346 347
347 348 def getbundle(self, source, **kwargs):
348 349 self.requirecap('getbundle', _('look up remote changes'))
349 350 opts = {}
350 351 bundlecaps = kwargs.get('bundlecaps')
351 352 if bundlecaps is not None:
352 353 kwargs['bundlecaps'] = sorted(bundlecaps)
353 354 else:
354 355 bundlecaps = () # kwargs could have it to None
355 356 for key, value in kwargs.iteritems():
356 357 if value is None:
357 358 continue
358 359 keytype = gboptsmap.get(key)
359 360 if keytype is None:
360 361 assert False, 'unexpected'
361 362 elif keytype == 'nodes':
362 363 value = encodelist(value)
363 elif keytype == 'csv':
364 elif keytype in ('csv', 'scsv'):
364 365 value = ','.join(value)
365 366 elif keytype == 'boolean':
366 367 value = '%i' % bool(value)
367 368 elif keytype != 'plain':
368 369 raise KeyError('unknown getbundle option type %s'
369 370 % keytype)
370 371 opts[key] = value
371 372 f = self._callcompressable("getbundle", **opts)
372 373 if any((cap.startswith('HG2') for cap in bundlecaps)):
373 374 return bundle2.getunbundler(self.ui, f)
374 375 else:
375 376 return changegroupmod.cg1unpacker(f, 'UN')
376 377
377 378 def unbundle(self, cg, heads, source):
378 379 '''Send cg (a readable file-like object representing the
379 380 changegroup to push, typically a chunkbuffer object) to the
380 381 remote server as a bundle.
381 382
382 383 When pushing a bundle10 stream, return an integer indicating the
383 384 result of the push (see localrepository.addchangegroup()).
384 385
385 386 When pushing a bundle20 stream, return a bundle20 stream.'''
386 387
387 388 if heads != ['force'] and self.capable('unbundlehash'):
388 389 heads = encodelist(['hashed',
389 390 util.sha1(''.join(sorted(heads))).digest()])
390 391 else:
391 392 heads = encodelist(heads)
392 393
393 394 if util.safehasattr(cg, 'deltaheader'):
394 395 # this a bundle10, do the old style call sequence
395 396 ret, output = self._callpush("unbundle", cg, heads=heads)
396 397 if ret == "":
397 398 raise error.ResponseError(
398 399 _('push failed:'), output)
399 400 try:
400 401 ret = int(ret)
401 402 except ValueError:
402 403 raise error.ResponseError(
403 404 _('push failed (unexpected response):'), ret)
404 405
405 406 for l in output.splitlines(True):
406 407 self.ui.status(_('remote: '), l)
407 408 else:
408 409 # bundle2 push. Send a stream, fetch a stream.
409 410 stream = self._calltwowaystream('unbundle', cg, heads=heads)
410 411 ret = bundle2.getunbundler(self.ui, stream)
411 412 return ret
412 413
413 414 def debugwireargs(self, one, two, three=None, four=None, five=None):
414 415 # don't pass optional arguments left at their default value
415 416 opts = {}
416 417 if three is not None:
417 418 opts['three'] = three
418 419 if four is not None:
419 420 opts['four'] = four
420 421 return self._call('debugwireargs', one=one, two=two, **opts)
421 422
422 423 def _call(self, cmd, **args):
423 424 """execute <cmd> on the server
424 425
425 426 The command is expected to return a simple string.
426 427
427 428 returns the server reply as a string."""
428 429 raise NotImplementedError()
429 430
430 431 def _callstream(self, cmd, **args):
431 432 """execute <cmd> on the server
432 433
433 434 The command is expected to return a stream.
434 435
435 436 returns the server reply as a file like object."""
436 437 raise NotImplementedError()
437 438
438 439 def _callcompressable(self, cmd, **args):
439 440 """execute <cmd> on the server
440 441
441 442 The command is expected to return a stream.
442 443
443 444 The stream may have been compressed in some implementations. This
444 445 function takes care of the decompression. This is the only difference
445 446 with _callstream.
446 447
447 448 returns the server reply as a file like object.
448 449 """
449 450 raise NotImplementedError()
450 451
451 452 def _callpush(self, cmd, fp, **args):
452 453 """execute a <cmd> on server
453 454
454 455 The command is expected to be related to a push. Push has a special
455 456 return method.
456 457
457 458 returns the server reply as a (ret, output) tuple. ret is either
458 459 empty (error) or a stringified int.
459 460 """
460 461 raise NotImplementedError()
461 462
462 463 def _calltwowaystream(self, cmd, fp, **args):
463 464 """execute <cmd> on server
464 465
465 466 The command will send a stream to the server and get a stream in reply.
466 467 """
467 468 raise NotImplementedError()
468 469
469 470 def _abort(self, exception):
470 471 """clearly abort the wire protocol connection and raise the exception
471 472 """
472 473 raise NotImplementedError()
473 474
474 475 # server side
475 476
476 477 # wire protocol command can either return a string or one of these classes.
477 478 class streamres(object):
478 479 """wireproto reply: binary stream
479 480
480 481 The call was successful and the result is a stream.
481 482 Iterate on the `self.gen` attribute to retrieve chunks.
482 483 """
483 484 def __init__(self, gen):
484 485 self.gen = gen
485 486
486 487 class pushres(object):
487 488 """wireproto reply: success with simple integer return
488 489
489 490 The call was successful and returned an integer contained in `self.res`.
490 491 """
491 492 def __init__(self, res):
492 493 self.res = res
493 494
494 495 class pusherr(object):
495 496 """wireproto reply: failure
496 497
497 498 The call failed. The `self.res` attribute contains the error message.
498 499 """
499 500 def __init__(self, res):
500 501 self.res = res
501 502
502 503 class ooberror(object):
503 504 """wireproto reply: failure of a batch of operation
504 505
505 506 Something failed during a batch call. The error message is stored in
506 507 `self.message`.
507 508 """
508 509 def __init__(self, message):
509 510 self.message = message
510 511
511 512 def dispatch(repo, proto, command):
512 513 repo = repo.filtered("served")
513 514 func, spec = commands[command]
514 515 args = proto.getargs(spec)
515 516 return func(repo, proto, *args)
516 517
517 518 def options(cmd, keys, others):
518 519 opts = {}
519 520 for k in keys:
520 521 if k in others:
521 522 opts[k] = others[k]
522 523 del others[k]
523 524 if others:
524 525 sys.stderr.write("warning: %s ignored unexpected arguments %s\n"
525 526 % (cmd, ",".join(others)))
526 527 return opts
527 528
528 529 # list of commands
529 530 commands = {}
530 531
531 532 def wireprotocommand(name, args=''):
532 533 """decorator for wire protocol command"""
533 534 def register(func):
534 535 commands[name] = (func, args)
535 536 return func
536 537 return register
537 538
538 539 @wireprotocommand('batch', 'cmds *')
539 540 def batch(repo, proto, cmds, others):
540 541 repo = repo.filtered("served")
541 542 res = []
542 543 for pair in cmds.split(';'):
543 544 op, args = pair.split(' ', 1)
544 545 vals = {}
545 546 for a in args.split(','):
546 547 if a:
547 548 n, v = a.split('=')
548 549 vals[n] = unescapearg(v)
549 550 func, spec = commands[op]
550 551 if spec:
551 552 keys = spec.split()
552 553 data = {}
553 554 for k in keys:
554 555 if k == '*':
555 556 star = {}
556 557 for key in vals.keys():
557 558 if key not in keys:
558 559 star[key] = vals[key]
559 560 data['*'] = star
560 561 else:
561 562 data[k] = vals[k]
562 563 result = func(repo, proto, *[data[k] for k in keys])
563 564 else:
564 565 result = func(repo, proto)
565 566 if isinstance(result, ooberror):
566 567 return result
567 568 res.append(escapearg(result))
568 569 return ';'.join(res)
569 570
570 571 @wireprotocommand('between', 'pairs')
571 572 def between(repo, proto, pairs):
572 573 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
573 574 r = []
574 575 for b in repo.between(pairs):
575 576 r.append(encodelist(b) + "\n")
576 577 return "".join(r)
577 578
578 579 @wireprotocommand('branchmap')
579 580 def branchmap(repo, proto):
580 581 branchmap = repo.branchmap()
581 582 heads = []
582 583 for branch, nodes in branchmap.iteritems():
583 584 branchname = urllib.quote(encoding.fromlocal(branch))
584 585 branchnodes = encodelist(nodes)
585 586 heads.append('%s %s' % (branchname, branchnodes))
586 587 return '\n'.join(heads)
587 588
588 589 @wireprotocommand('branches', 'nodes')
589 590 def branches(repo, proto, nodes):
590 591 nodes = decodelist(nodes)
591 592 r = []
592 593 for b in repo.branches(nodes):
593 594 r.append(encodelist(b) + "\n")
594 595 return "".join(r)
595 596
596 597
597 598 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
598 599 'known', 'getbundle', 'unbundlehash', 'batch']
599 600
600 601 def _capabilities(repo, proto):
601 602 """return a list of capabilities for a repo
602 603
603 604 This function exists to allow extensions to easily wrap capabilities
604 605 computation
605 606
606 607 - returns a lists: easy to alter
607 608 - change done here will be propagated to both `capabilities` and `hello`
608 609 command without any other action needed.
609 610 """
610 611 # copy to prevent modification of the global list
611 612 caps = list(wireprotocaps)
612 613 if _allowstream(repo.ui):
613 614 if repo.ui.configbool('server', 'preferuncompressed', False):
614 615 caps.append('stream-preferred')
615 616 requiredformats = repo.requirements & repo.supportedformats
616 617 # if our local revlogs are just revlogv1, add 'stream' cap
617 618 if not requiredformats - set(('revlogv1',)):
618 619 caps.append('stream')
619 620 # otherwise, add 'streamreqs' detailing our local revlog format
620 621 else:
621 622 caps.append('streamreqs=%s' % ','.join(requiredformats))
622 623 if repo.ui.configbool('experimental', 'bundle2-advertise', True):
623 624 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
624 625 caps.append('bundle2=' + urllib.quote(capsblob))
625 626 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
626 627 caps.append('httpheader=1024')
627 628 return caps
628 629
629 630 # If you are writing an extension and consider wrapping this function. Wrap
630 631 # `_capabilities` instead.
631 632 @wireprotocommand('capabilities')
632 633 def capabilities(repo, proto):
633 634 return ' '.join(_capabilities(repo, proto))
634 635
635 636 @wireprotocommand('changegroup', 'roots')
636 637 def changegroup(repo, proto, roots):
637 638 nodes = decodelist(roots)
638 639 cg = changegroupmod.changegroup(repo, nodes, 'serve')
639 640 return streamres(proto.groupchunks(cg))
640 641
641 642 @wireprotocommand('changegroupsubset', 'bases heads')
642 643 def changegroupsubset(repo, proto, bases, heads):
643 644 bases = decodelist(bases)
644 645 heads = decodelist(heads)
645 646 cg = changegroupmod.changegroupsubset(repo, bases, heads, 'serve')
646 647 return streamres(proto.groupchunks(cg))
647 648
648 649 @wireprotocommand('debugwireargs', 'one two *')
649 650 def debugwireargs(repo, proto, one, two, others):
650 651 # only accept optional args from the known set
651 652 opts = options('debugwireargs', ['three', 'four'], others)
652 653 return repo.debugwireargs(one, two, **opts)
653 654
654 655 # List of options accepted by getbundle.
655 656 #
656 657 # Meant to be extended by extensions. It is the extension's responsibility to
657 658 # ensure such options are properly processed in exchange.getbundle.
658 659 gboptslist = ['heads', 'common', 'bundlecaps']
659 660
660 661 @wireprotocommand('getbundle', '*')
661 662 def getbundle(repo, proto, others):
662 663 opts = options('getbundle', gboptsmap.keys(), others)
663 664 for k, v in opts.iteritems():
664 665 keytype = gboptsmap[k]
665 666 if keytype == 'nodes':
666 667 opts[k] = decodelist(v)
667 668 elif keytype == 'csv':
669 opts[k] = list(v.split(','))
670 elif keytype == 'scsv':
668 671 opts[k] = set(v.split(','))
669 672 elif keytype == 'boolean':
670 673 opts[k] = bool(v)
671 674 elif keytype != 'plain':
672 675 raise KeyError('unknown getbundle option type %s'
673 676 % keytype)
674 677 cg = exchange.getbundle(repo, 'serve', **opts)
675 678 return streamres(proto.groupchunks(cg))
676 679
677 680 @wireprotocommand('heads')
678 681 def heads(repo, proto):
679 682 h = repo.heads()
680 683 return encodelist(h) + "\n"
681 684
682 685 @wireprotocommand('hello')
683 686 def hello(repo, proto):
684 687 '''the hello command returns a set of lines describing various
685 688 interesting things about the server, in an RFC822-like format.
686 689 Currently the only one defined is "capabilities", which
687 690 consists of a line in the form:
688 691
689 692 capabilities: space separated list of tokens
690 693 '''
691 694 return "capabilities: %s\n" % (capabilities(repo, proto))
692 695
693 696 @wireprotocommand('listkeys', 'namespace')
694 697 def listkeys(repo, proto, namespace):
695 698 d = repo.listkeys(encoding.tolocal(namespace)).items()
696 699 return pushkeymod.encodekeys(d)
697 700
698 701 @wireprotocommand('lookup', 'key')
699 702 def lookup(repo, proto, key):
700 703 try:
701 704 k = encoding.tolocal(key)
702 705 c = repo[k]
703 706 r = c.hex()
704 707 success = 1
705 708 except Exception, inst:
706 709 r = str(inst)
707 710 success = 0
708 711 return "%s %s\n" % (success, r)
709 712
710 713 @wireprotocommand('known', 'nodes *')
711 714 def known(repo, proto, nodes, others):
712 715 return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes)))
713 716
714 717 @wireprotocommand('pushkey', 'namespace key old new')
715 718 def pushkey(repo, proto, namespace, key, old, new):
716 719 # compatibility with pre-1.8 clients which were accidentally
717 720 # sending raw binary nodes rather than utf-8-encoded hex
718 721 if len(new) == 20 and new.encode('string-escape') != new:
719 722 # looks like it could be a binary node
720 723 try:
721 724 new.decode('utf-8')
722 725 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
723 726 except UnicodeDecodeError:
724 727 pass # binary, leave unmodified
725 728 else:
726 729 new = encoding.tolocal(new) # normal path
727 730
728 731 if util.safehasattr(proto, 'restore'):
729 732
730 733 proto.redirect()
731 734
732 735 try:
733 736 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
734 737 encoding.tolocal(old), new) or False
735 738 except util.Abort:
736 739 r = False
737 740
738 741 output = proto.restore()
739 742
740 743 return '%s\n%s' % (int(r), output)
741 744
742 745 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
743 746 encoding.tolocal(old), new)
744 747 return '%s\n' % int(r)
745 748
746 749 def _allowstream(ui):
747 750 return ui.configbool('server', 'uncompressed', True, untrusted=True)
748 751
749 752 @wireprotocommand('stream_out')
750 753 def stream(repo, proto):
751 754 '''If the server supports streaming clone, it advertises the "stream"
752 755 capability with a value representing the version and flags of the repo
753 756 it is serving. Client checks to see if it understands the format.
754 757 '''
755 758 if not _allowstream(repo.ui):
756 759 return '1\n'
757 760
758 761 def getstream(it):
759 762 yield '0\n'
760 763 for chunk in it:
761 764 yield chunk
762 765
763 766 try:
764 767 # LockError may be raised before the first result is yielded. Don't
765 768 # emit output until we're sure we got the lock successfully.
766 769 it = exchange.generatestreamclone(repo)
767 770 return streamres(getstream(it))
768 771 except error.LockError:
769 772 return '2\n'
770 773
771 774 @wireprotocommand('unbundle', 'heads')
772 775 def unbundle(repo, proto, heads):
773 776 their_heads = decodelist(heads)
774 777
775 778 try:
776 779 proto.redirect()
777 780
778 781 exchange.check_heads(repo, their_heads, 'preparing changes')
779 782
780 783 # write bundle data to temporary file because it can be big
781 784 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
782 785 fp = os.fdopen(fd, 'wb+')
783 786 r = 0
784 787 try:
785 788 proto.getfile(fp)
786 789 fp.seek(0)
787 790 gen = exchange.readbundle(repo.ui, fp, None)
788 791 r = exchange.unbundle(repo, gen, their_heads, 'serve',
789 792 proto._client())
790 793 if util.safehasattr(r, 'addpart'):
791 794 # The return looks streamable, we are in the bundle2 case and
792 795 # should return a stream.
793 796 return streamres(r.getchunks())
794 797 return pushres(r)
795 798
796 799 finally:
797 800 fp.close()
798 801 os.unlink(tempname)
799 802
800 803 except (error.BundleValueError, util.Abort, error.PushRaced), exc:
801 804 # handle non-bundle2 case first
802 805 if not getattr(exc, 'duringunbundle2', False):
803 806 try:
804 807 raise
805 808 except util.Abort:
806 809 # The old code we moved used sys.stderr directly.
807 810 # We did not change it to minimise code change.
808 811 # This need to be moved to something proper.
809 812 # Feel free to do it.
810 813 sys.stderr.write("abort: %s\n" % exc)
811 814 return pushres(0)
812 815 except error.PushRaced:
813 816 return pusherr(str(exc))
814 817
815 818 bundler = bundle2.bundle20(repo.ui)
816 819 for out in getattr(exc, '_bundle2salvagedoutput', ()):
817 820 bundler.addpart(out)
818 821 try:
819 822 raise
820 823 except error.BundleValueError, exc:
821 824 errpart = bundler.newpart('error:unsupportedcontent')
822 825 if exc.parttype is not None:
823 826 errpart.addparam('parttype', exc.parttype)
824 827 if exc.params:
825 828 errpart.addparam('params', '\0'.join(exc.params))
826 829 except util.Abort, exc:
827 830 manargs = [('message', str(exc))]
828 831 advargs = []
829 832 if exc.hint is not None:
830 833 advargs.append(('hint', exc.hint))
831 834 bundler.addpart(bundle2.bundlepart('error:abort',
832 835 manargs, advargs))
833 836 except error.PushRaced, exc:
834 837 bundler.newpart('error:pushraced', [('message', str(exc))])
835 838 return streamres(bundler.getchunks())
@@ -1,510 +1,510
1 1 $ cat << EOF >> $HGRCPATH
2 2 > [experimental]
3 3 > # drop me once bundle2 is the default,
4 4 > # added to get test change early.
5 5 > bundle2-exp = True
6 6 > EOF
7 7
8 8
9 9 This test tries to exercise the ssh functionality with a dummy script
10 10
11 11 creating 'remote' repo
12 12
13 13 $ hg init remote
14 14 $ cd remote
15 15 $ echo this > foo
16 16 $ echo this > fooO
17 17 $ hg ci -A -m "init" foo fooO
18 18
19 19 insert a closed branch (issue4428)
20 20
21 21 $ hg up null
22 22 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
23 23 $ hg branch closed
24 24 marked working directory as branch closed
25 25 (branches are permanent and global, did you want a bookmark?)
26 26 $ hg ci -mc0
27 27 $ hg ci --close-branch -mc1
28 28 $ hg up -q default
29 29
30 30 configure for serving
31 31
32 32 $ cat <<EOF > .hg/hgrc
33 33 > [server]
34 34 > uncompressed = True
35 35 >
36 36 > [hooks]
37 37 > changegroup = python "$TESTDIR/printenv.py" changegroup-in-remote 0 ../dummylog
38 38 > EOF
39 39 $ cd ..
40 40
41 41 repo not found error
42 42
43 43 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/nonexistent local
44 44 remote: abort: there is no Mercurial repository here (.hg not found)!
45 45 abort: no suitable response from remote hg!
46 46 [255]
47 47
48 48 non-existent absolute path
49 49
50 50 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy//`pwd`/nonexistent local
51 51 remote: abort: there is no Mercurial repository here (.hg not found)!
52 52 abort: no suitable response from remote hg!
53 53 [255]
54 54
55 55 clone remote via stream
56 56
57 57 $ hg clone -e "python \"$TESTDIR/dummyssh\"" --uncompressed ssh://user@dummy/remote local-stream
58 58 streaming all changes
59 59 4 files to transfer, 615 bytes of data
60 60 transferred 615 bytes in * seconds (*) (glob)
61 61 searching for changes
62 62 no changes found
63 63 updating to branch default
64 64 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 65 $ cd local-stream
66 66 $ hg verify
67 67 checking changesets
68 68 checking manifests
69 69 crosschecking files in changesets and manifests
70 70 checking files
71 71 2 files, 3 changesets, 2 total revisions
72 72 $ hg branches
73 73 default 0:1160648e36ce
74 74 $ cd ..
75 75
76 76 clone bookmarks via stream
77 77
78 78 $ hg -R local-stream book mybook
79 79 $ hg clone -e "python \"$TESTDIR/dummyssh\"" --uncompressed ssh://user@dummy/local-stream stream2
80 80 streaming all changes
81 81 4 files to transfer, 615 bytes of data
82 82 transferred 615 bytes in * seconds (*) (glob)
83 83 searching for changes
84 84 no changes found
85 85 updating to branch default
86 86 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 87 $ cd stream2
88 88 $ hg book
89 89 mybook 0:1160648e36ce
90 90 $ cd ..
91 91 $ rm -rf local-stream stream2
92 92
93 93 clone remote via pull
94 94
95 95 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local
96 96 requesting all changes
97 97 adding changesets
98 98 adding manifests
99 99 adding file changes
100 100 added 3 changesets with 2 changes to 2 files
101 101 updating to branch default
102 102 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 103
104 104 verify
105 105
106 106 $ cd local
107 107 $ hg verify
108 108 checking changesets
109 109 checking manifests
110 110 crosschecking files in changesets and manifests
111 111 checking files
112 112 2 files, 3 changesets, 2 total revisions
113 113 $ echo '[hooks]' >> .hg/hgrc
114 114 $ echo "changegroup = python \"$TESTDIR/printenv.py\" changegroup-in-local 0 ../dummylog" >> .hg/hgrc
115 115
116 116 empty default pull
117 117
118 118 $ hg paths
119 119 default = ssh://user@dummy/remote
120 120 $ hg pull -e "python \"$TESTDIR/dummyssh\""
121 121 pulling from ssh://user@dummy/remote
122 122 searching for changes
123 123 no changes found
124 124
125 125 pull from wrong ssh URL
126 126
127 127 $ hg pull -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/doesnotexist
128 128 pulling from ssh://user@dummy/doesnotexist
129 129 remote: abort: there is no Mercurial repository here (.hg not found)!
130 130 abort: no suitable response from remote hg!
131 131 [255]
132 132
133 133 local change
134 134
135 135 $ echo bleah > foo
136 136 $ hg ci -m "add"
137 137
138 138 updating rc
139 139
140 140 $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc
141 141 $ echo "[ui]" >> .hg/hgrc
142 142 $ echo "ssh = python \"$TESTDIR/dummyssh\"" >> .hg/hgrc
143 143
144 144 find outgoing
145 145
146 146 $ hg out ssh://user@dummy/remote
147 147 comparing with ssh://user@dummy/remote
148 148 searching for changes
149 149 changeset: 3:a28a9d1a809c
150 150 tag: tip
151 151 parent: 0:1160648e36ce
152 152 user: test
153 153 date: Thu Jan 01 00:00:00 1970 +0000
154 154 summary: add
155 155
156 156
157 157 find incoming on the remote side
158 158
159 159 $ hg incoming -R ../remote -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/local
160 160 comparing with ssh://user@dummy/local
161 161 searching for changes
162 162 changeset: 3:a28a9d1a809c
163 163 tag: tip
164 164 parent: 0:1160648e36ce
165 165 user: test
166 166 date: Thu Jan 01 00:00:00 1970 +0000
167 167 summary: add
168 168
169 169
170 170 find incoming on the remote side (using absolute path)
171 171
172 172 $ hg incoming -R ../remote -e "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/`pwd`"
173 173 comparing with ssh://user@dummy/$TESTTMP/local
174 174 searching for changes
175 175 changeset: 3:a28a9d1a809c
176 176 tag: tip
177 177 parent: 0:1160648e36ce
178 178 user: test
179 179 date: Thu Jan 01 00:00:00 1970 +0000
180 180 summary: add
181 181
182 182
183 183 push
184 184
185 185 $ hg push
186 186 pushing to ssh://user@dummy/remote
187 187 searching for changes
188 188 remote: adding changesets
189 189 remote: adding manifests
190 190 remote: adding file changes
191 191 remote: added 1 changesets with 1 changes to 1 files
192 192 $ cd ../remote
193 193
194 194 check remote tip
195 195
196 196 $ hg tip
197 197 changeset: 3:a28a9d1a809c
198 198 tag: tip
199 199 parent: 0:1160648e36ce
200 200 user: test
201 201 date: Thu Jan 01 00:00:00 1970 +0000
202 202 summary: add
203 203
204 204 $ hg verify
205 205 checking changesets
206 206 checking manifests
207 207 crosschecking files in changesets and manifests
208 208 checking files
209 209 2 files, 4 changesets, 3 total revisions
210 210 $ hg cat -r tip foo
211 211 bleah
212 212 $ echo z > z
213 213 $ hg ci -A -m z z
214 214 created new head
215 215
216 216 test pushkeys and bookmarks
217 217
218 218 $ cd ../local
219 219 $ hg debugpushkey --config ui.ssh="python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote namespaces
220 220 bookmarks
221 221 namespaces
222 222 phases
223 223 $ hg book foo -r 0
224 224 $ hg out -B
225 225 comparing with ssh://user@dummy/remote
226 226 searching for changed bookmarks
227 227 foo 1160648e36ce
228 228 $ hg push -B foo
229 229 pushing to ssh://user@dummy/remote
230 230 searching for changes
231 231 no changes found
232 232 exporting bookmark foo
233 233 [1]
234 234 $ hg debugpushkey --config ui.ssh="python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote bookmarks
235 235 foo 1160648e36cec0054048a7edc4110c6f84fde594
236 236 $ hg book -f foo
237 237 $ hg push --traceback
238 238 pushing to ssh://user@dummy/remote
239 239 searching for changes
240 240 no changes found
241 241 updating bookmark foo
242 242 [1]
243 243 $ hg book -d foo
244 244 $ hg in -B
245 245 comparing with ssh://user@dummy/remote
246 246 searching for changed bookmarks
247 247 foo a28a9d1a809c
248 248 $ hg book -f -r 0 foo
249 249 $ hg pull -B foo
250 250 pulling from ssh://user@dummy/remote
251 251 no changes found
252 252 updating bookmark foo
253 253 $ hg book -d foo
254 254 $ hg push -B foo
255 255 pushing to ssh://user@dummy/remote
256 256 searching for changes
257 257 no changes found
258 258 deleting remote bookmark foo
259 259 [1]
260 260
261 261 a bad, evil hook that prints to stdout
262 262
263 263 $ cat <<EOF > $TESTTMP/badhook
264 264 > import sys
265 265 > sys.stdout.write("KABOOM\n")
266 266 > EOF
267 267
268 268 $ echo '[hooks]' >> ../remote/.hg/hgrc
269 269 $ echo "changegroup.stdout = python $TESTTMP/badhook" >> ../remote/.hg/hgrc
270 270 $ echo r > r
271 271 $ hg ci -A -m z r
272 272
273 273 push should succeed even though it has an unexpected response
274 274
275 275 $ hg push
276 276 pushing to ssh://user@dummy/remote
277 277 searching for changes
278 278 remote has heads on branch 'default' that are not known locally: 6c0482d977a3
279 279 remote: adding changesets
280 280 remote: adding manifests
281 281 remote: adding file changes
282 282 remote: added 1 changesets with 1 changes to 1 files
283 283 remote: KABOOM
284 284 $ hg -R ../remote heads
285 285 changeset: 5:1383141674ec
286 286 tag: tip
287 287 parent: 3:a28a9d1a809c
288 288 user: test
289 289 date: Thu Jan 01 00:00:00 1970 +0000
290 290 summary: z
291 291
292 292 changeset: 4:6c0482d977a3
293 293 parent: 0:1160648e36ce
294 294 user: test
295 295 date: Thu Jan 01 00:00:00 1970 +0000
296 296 summary: z
297 297
298 298
299 299 clone bookmarks
300 300
301 301 $ hg -R ../remote bookmark test
302 302 $ hg -R ../remote bookmarks
303 303 * test 4:6c0482d977a3
304 304 $ hg clone -e "python \"$TESTDIR/dummyssh\"" ssh://user@dummy/remote local-bookmarks
305 305 requesting all changes
306 306 adding changesets
307 307 adding manifests
308 308 adding file changes
309 309 added 6 changesets with 5 changes to 4 files (+1 heads)
310 310 updating to branch default
311 311 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
312 312 $ hg -R local-bookmarks bookmarks
313 313 test 4:6c0482d977a3
314 314
315 315 passwords in ssh urls are not supported
316 316 (we use a glob here because different Python versions give different
317 317 results here)
318 318
319 319 $ hg push ssh://user:erroneouspwd@dummy/remote
320 320 pushing to ssh://user:*@dummy/remote (glob)
321 321 abort: password in URL not supported!
322 322 [255]
323 323
324 324 $ cd ..
325 325
326 326 hide outer repo
327 327 $ hg init
328 328
329 329 Test remote paths with spaces (issue2983):
330 330
331 331 $ hg init --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
332 332 $ touch "$TESTTMP/a repo/test"
333 333 $ hg -R 'a repo' commit -A -m "test"
334 334 adding test
335 335 $ hg -R 'a repo' tag tag
336 336 $ hg id --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
337 337 73649e48688a
338 338
339 339 $ hg id --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo#noNoNO"
340 340 abort: unknown revision 'noNoNO'!
341 341 [255]
342 342
343 343 Test (non-)escaping of remote paths with spaces when cloning (issue3145):
344 344
345 345 $ hg clone --ssh "python \"$TESTDIR/dummyssh\"" "ssh://user@dummy/a repo"
346 346 destination directory: a repo
347 347 abort: destination 'a repo' is not empty
348 348 [255]
349 349
350 350 Test hg-ssh using a helper script that will restore PYTHONPATH (which might
351 351 have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right
352 352 parameters:
353 353
354 354 $ cat > ssh.sh << EOF
355 355 > userhost="\$1"
356 356 > SSH_ORIGINAL_COMMAND="\$2"
357 357 > export SSH_ORIGINAL_COMMAND
358 358 > PYTHONPATH="$PYTHONPATH"
359 359 > export PYTHONPATH
360 360 > python "$TESTDIR/../contrib/hg-ssh" "$TESTTMP/a repo"
361 361 > EOF
362 362
363 363 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a repo"
364 364 73649e48688a
365 365
366 366 $ hg id --ssh "sh ssh.sh" "ssh://user@dummy/a'repo"
367 367 remote: Illegal repository "$TESTTMP/a'repo" (glob)
368 368 abort: no suitable response from remote hg!
369 369 [255]
370 370
371 371 $ hg id --ssh "sh ssh.sh" --remotecmd hacking "ssh://user@dummy/a'repo"
372 372 remote: Illegal command "hacking -R 'a'\''repo' serve --stdio"
373 373 abort: no suitable response from remote hg!
374 374 [255]
375 375
376 376 $ SSH_ORIGINAL_COMMAND="'hg' -R 'a'repo' serve --stdio" python "$TESTDIR/../contrib/hg-ssh"
377 377 Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation
378 378 [255]
379 379
380 380 Test hg-ssh in read-only mode:
381 381
382 382 $ cat > ssh.sh << EOF
383 383 > userhost="\$1"
384 384 > SSH_ORIGINAL_COMMAND="\$2"
385 385 > export SSH_ORIGINAL_COMMAND
386 386 > PYTHONPATH="$PYTHONPATH"
387 387 > export PYTHONPATH
388 388 > python "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote"
389 389 > EOF
390 390
391 391 $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local
392 392 requesting all changes
393 393 adding changesets
394 394 adding manifests
395 395 adding file changes
396 396 added 6 changesets with 5 changes to 4 files (+1 heads)
397 397 updating to branch default
398 398 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
399 399
400 400 $ cd read-only-local
401 401 $ echo "baz" > bar
402 402 $ hg ci -A -m "unpushable commit" bar
403 403 $ hg push --ssh "sh ../ssh.sh"
404 404 pushing to ssh://user@dummy/*/remote (glob)
405 405 searching for changes
406 406 abort: pretxnopen.hg-ssh hook failed
407 407 remote: Permission denied
408 408 [255]
409 409
410 410 $ cd ..
411 411
412 412 stderr from remote commands should be printed before stdout from local code (issue4336)
413 413
414 414 $ hg clone remote stderr-ordering
415 415 updating to branch default
416 416 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
417 417 $ cd stderr-ordering
418 418 $ cat >> localwrite.py << EOF
419 419 > from mercurial import exchange, extensions
420 420 >
421 421 > def wrappedpush(orig, repo, *args, **kwargs):
422 422 > res = orig(repo, *args, **kwargs)
423 423 > repo.ui.write('local stdout\n')
424 424 > return res
425 425 >
426 426 > def extsetup(ui):
427 427 > extensions.wrapfunction(exchange, 'push', wrappedpush)
428 428 > EOF
429 429
430 430 $ cat >> .hg/hgrc << EOF
431 431 > [paths]
432 432 > default-push = ssh://user@dummy/remote
433 433 > [ui]
434 434 > ssh = python "$TESTDIR/dummyssh"
435 435 > [extensions]
436 436 > localwrite = localwrite.py
437 437 > EOF
438 438
439 439 $ echo localwrite > foo
440 440 $ hg commit -m 'testing localwrite'
441 441 $ hg push
442 442 pushing to ssh://user@dummy/remote
443 443 searching for changes
444 444 remote: adding changesets
445 445 remote: adding manifests
446 446 remote: adding file changes
447 447 remote: added 1 changesets with 1 changes to 1 files
448 448 remote: KABOOM
449 449 local stdout
450 450
451 451 debug output
452 452
453 453 $ hg pull --debug ssh://user@dummy/remote
454 454 pulling from ssh://user@dummy/remote
455 455 running python "*/dummyssh" user@dummy 'hg -R remote serve --stdio' (glob)
456 456 sending hello command
457 457 sending between command
458 458 remote: 286
459 459 remote: capabilities: lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Ahgtagsfnodes%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024
460 460 remote: 1
461 461 query 1; heads
462 462 sending batch command
463 463 searching for changes
464 464 all remote heads known locally
465 465 no changes found
466 466 sending getbundle command
467 467 bundle2-input-bundle: with-transaction
468 468 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
469 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
469 470 bundle2-input-part: total payload size 45
470 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
471 471 bundle2-input-bundle: 1 parts total
472 472 checking for updated bookmarks
473 473 preparing listkeys for "phases"
474 474 sending listkeys command
475 475 received listkey for "phases": 15 bytes
476 476
477 477 $ cd ..
478 478
479 479 $ cat dummylog
480 480 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
481 481 Got arguments 1:user@dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio
482 482 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
483 483 Got arguments 1:user@dummy 2:hg -R local-stream serve --stdio
484 484 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
485 485 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
486 486 Got arguments 1:user@dummy 2:hg -R doesnotexist serve --stdio
487 487 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
488 488 Got arguments 1:user@dummy 2:hg -R local serve --stdio
489 489 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
490 490 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
491 491 changegroup-in-remote hook: HG_BUNDLE2=1 HG_NODE=a28a9d1a809cab7d4e2fde4bee738a9ede948b60 HG_SOURCE=serve HG_TXNID=TXN:* HG_URL=remote:ssh:127.0.0.1 (glob)
492 492 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
493 493 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
494 494 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
495 495 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
496 496 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
497 497 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
498 498 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
499 499 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
500 500 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
501 501 changegroup-in-remote hook: HG_BUNDLE2=1 HG_NODE=1383141674ec756a6056f6a9097618482fe0f4a6 HG_SOURCE=serve HG_TXNID=TXN:* HG_URL=remote:ssh:127.0.0.1 (glob)
502 502 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
503 503 Got arguments 1:user@dummy 2:hg init 'a repo'
504 504 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
505 505 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
506 506 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
507 507 Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio
508 508 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
509 509 changegroup-in-remote hook: HG_BUNDLE2=1 HG_NODE=65c38f4125f9602c8db4af56530cc221d93b8ef8 HG_SOURCE=serve HG_TXNID=TXN:* HG_URL=remote:ssh:127.0.0.1 (glob)
510 510 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
General Comments 0
You need to be logged in to leave comments. Login now