##// END OF EJS Templates
bundle2: gracefully handle UnknownPartError during unbundle...
Pierre-Yves David -
r21183:4345274a stable
parent child Browse files
Show More
@@ -1,755 +1,761 b''
1 1 # bundle2.py - generic container format to transmit arbitrary data.
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
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 """Handling of the new bundle2 format
8 8
9 9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 11 that will be handed to and processed by the application layer.
12 12
13 13
14 14 General format architecture
15 15 ===========================
16 16
17 17 The format is architectured as follow
18 18
19 19 - magic string
20 20 - stream level parameters
21 21 - payload parts (any number)
22 22 - end of stream marker.
23 23
24 24 the Binary format
25 25 ============================
26 26
27 27 All numbers are unsigned and big-endian.
28 28
29 29 stream level parameters
30 30 ------------------------
31 31
32 32 Binary format is as follow
33 33
34 34 :params size: (16 bits integer)
35 35
36 36 The total number of Bytes used by the parameters
37 37
38 38 :params value: arbitrary number of Bytes
39 39
40 40 A blob of `params size` containing the serialized version of all stream level
41 41 parameters.
42 42
43 43 The blob contains a space separated list of parameters. Parameters with value
44 44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45 45
46 46 Empty name are obviously forbidden.
47 47
48 48 Name MUST start with a letter. If this first letter is lower case, the
49 49 parameter is advisory and can be safely ignored. However when the first
50 50 letter is capital, the parameter is mandatory and the bundling process MUST
51 51 stop if he is not able to proceed it.
52 52
53 53 Stream parameters use a simple textual format for two main reasons:
54 54
55 55 - Stream level parameters should remain simple and we want to discourage any
56 56 crazy usage.
57 57 - Textual data allow easy human inspection of a bundle2 header in case of
58 58 troubles.
59 59
60 60 Any Applicative level options MUST go into a bundle2 part instead.
61 61
62 62 Payload part
63 63 ------------------------
64 64
65 65 Binary format is as follow
66 66
67 67 :header size: (16 bits inter)
68 68
69 69 The total number of Bytes used by the part headers. When the header is empty
70 70 (size = 0) this is interpreted as the end of stream marker.
71 71
72 72 :header:
73 73
74 74 The header defines how to interpret the part. It contains two piece of
75 75 data: the part type, and the part parameters.
76 76
77 77 The part type is used to route an application level handler, that can
78 78 interpret payload.
79 79
80 80 Part parameters are passed to the application level handler. They are
81 81 meant to convey information that will help the application level object to
82 82 interpret the part payload.
83 83
84 84 The binary format of the header is has follow
85 85
86 86 :typesize: (one byte)
87 87
88 88 :parttype: alphanumerical part name
89 89
90 90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 91 to this part.
92 92
93 93 :parameters:
94 94
95 95 Part's parameter may have arbitrary content, the binary structure is::
96 96
97 97 <mandatory-count><advisory-count><param-sizes><param-data>
98 98
99 99 :mandatory-count: 1 byte, number of mandatory parameters
100 100
101 101 :advisory-count: 1 byte, number of advisory parameters
102 102
103 103 :param-sizes:
104 104
105 105 N couple of bytes, where N is the total number of parameters. Each
106 106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107 107
108 108 :param-data:
109 109
110 110 A blob of bytes from which each parameter key and value can be
111 111 retrieved using the list of size couples stored in the previous
112 112 field.
113 113
114 114 Mandatory parameters comes first, then the advisory ones.
115 115
116 116 :payload:
117 117
118 118 payload is a series of `<chunksize><chunkdata>`.
119 119
120 120 `chunksize` is a 32 bits integer, `chunkdata` are plain bytes (as much as
121 121 `chunksize` says)` The payload part is concluded by a zero size chunk.
122 122
123 123 The current implementation always produces either zero or one chunk.
124 124 This is an implementation limitation that will ultimately be lifted.
125 125
126 126 Bundle processing
127 127 ============================
128 128
129 129 Each part is processed in order using a "part handler". Handler are registered
130 130 for a certain part type.
131 131
132 132 The matching of a part to its handler is case insensitive. The case of the
133 133 part type is used to know if a part is mandatory or advisory. If the Part type
134 134 contains any uppercase char it is considered mandatory. When no handler is
135 135 known for a Mandatory part, the process is aborted and an exception is raised.
136 136 If the part is advisory and no handler is known, the part is ignored. When the
137 137 process is aborted, the full bundle is still read from the stream to keep the
138 138 channel usable. But none of the part read from an abort are processed. In the
139 139 future, dropping the stream may become an option for channel we do not care to
140 140 preserve.
141 141 """
142 142
143 143 import util
144 144 import struct
145 145 import urllib
146 146 import string
147 147
148 148 import changegroup
149 149 from i18n import _
150 150
151 151 _pack = struct.pack
152 152 _unpack = struct.unpack
153 153
154 154 _magicstring = 'HG2X'
155 155
156 156 _fstreamparamsize = '>H'
157 157 _fpartheadersize = '>H'
158 158 _fparttypesize = '>B'
159 159 _fpartid = '>I'
160 160 _fpayloadsize = '>I'
161 161 _fpartparamcount = '>BB'
162 162
163 163 preferedchunksize = 4096
164 164
165 165 def _makefpartparamsizes(nbparams):
166 166 """return a struct format to read part parameter sizes
167 167
168 168 The number parameters is variable so we need to build that format
169 169 dynamically.
170 170 """
171 171 return '>'+('BB'*nbparams)
172 172
173 173 class UnknownPartError(KeyError):
174 174 """error raised when no handler is found for a Mandatory part"""
175 175 pass
176 176
177 177 parthandlermapping = {}
178 178
179 179 def parthandler(parttype):
180 180 """decorator that register a function as a bundle2 part handler
181 181
182 182 eg::
183 183
184 184 @parthandler('myparttype')
185 185 def myparttypehandler(...):
186 186 '''process a part of type "my part".'''
187 187 ...
188 188 """
189 189 def _decorator(func):
190 190 lparttype = parttype.lower() # enforce lower case matching.
191 191 assert lparttype not in parthandlermapping
192 192 parthandlermapping[lparttype] = func
193 193 return func
194 194 return _decorator
195 195
196 196 class unbundlerecords(object):
197 197 """keep record of what happens during and unbundle
198 198
199 199 New records are added using `records.add('cat', obj)`. Where 'cat' is a
200 200 category of record and obj is an arbitrary object.
201 201
202 202 `records['cat']` will return all entries of this category 'cat'.
203 203
204 204 Iterating on the object itself will yield `('category', obj)` tuples
205 205 for all entries.
206 206
207 207 All iterations happens in chronological order.
208 208 """
209 209
210 210 def __init__(self):
211 211 self._categories = {}
212 212 self._sequences = []
213 213 self._replies = {}
214 214
215 215 def add(self, category, entry, inreplyto=None):
216 216 """add a new record of a given category.
217 217
218 218 The entry can then be retrieved in the list returned by
219 219 self['category']."""
220 220 self._categories.setdefault(category, []).append(entry)
221 221 self._sequences.append((category, entry))
222 222 if inreplyto is not None:
223 223 self.getreplies(inreplyto).add(category, entry)
224 224
225 225 def getreplies(self, partid):
226 226 """get the subrecords that replies to a specific part"""
227 227 return self._replies.setdefault(partid, unbundlerecords())
228 228
229 229 def __getitem__(self, cat):
230 230 return tuple(self._categories.get(cat, ()))
231 231
232 232 def __iter__(self):
233 233 return iter(self._sequences)
234 234
235 235 def __len__(self):
236 236 return len(self._sequences)
237 237
238 238 def __nonzero__(self):
239 239 return bool(self._sequences)
240 240
241 241 class bundleoperation(object):
242 242 """an object that represents a single bundling process
243 243
244 244 Its purpose is to carry unbundle-related objects and states.
245 245
246 246 A new object should be created at the beginning of each bundle processing.
247 247 The object is to be returned by the processing function.
248 248
249 249 The object has very little content now it will ultimately contain:
250 250 * an access to the repo the bundle is applied to,
251 251 * a ui object,
252 252 * a way to retrieve a transaction to add changes to the repo,
253 253 * a way to record the result of processing each part,
254 254 * a way to construct a bundle response when applicable.
255 255 """
256 256
257 257 def __init__(self, repo, transactiongetter):
258 258 self.repo = repo
259 259 self.ui = repo.ui
260 260 self.records = unbundlerecords()
261 261 self.gettransaction = transactiongetter
262 262 self.reply = None
263 263
264 264 class TransactionUnavailable(RuntimeError):
265 265 pass
266 266
267 267 def _notransaction():
268 268 """default method to get a transaction while processing a bundle
269 269
270 270 Raise an exception to highlight the fact that no transaction was expected
271 271 to be created"""
272 272 raise TransactionUnavailable()
273 273
274 274 def processbundle(repo, unbundler, transactiongetter=_notransaction):
275 275 """This function process a bundle, apply effect to/from a repo
276 276
277 277 It iterates over each part then searches for and uses the proper handling
278 278 code to process the part. Parts are processed in order.
279 279
280 280 This is very early version of this function that will be strongly reworked
281 281 before final usage.
282 282
283 283 Unknown Mandatory part will abort the process.
284 284 """
285 285 op = bundleoperation(repo, transactiongetter)
286 286 # todo:
287 287 # - replace this is a init function soon.
288 288 # - exception catching
289 289 unbundler.params
290 290 iterparts = unbundler.iterparts()
291 291 part = None
292 292 try:
293 293 for part in iterparts:
294 294 parttype = part.type
295 295 # part key are matched lower case
296 296 key = parttype.lower()
297 297 try:
298 298 handler = parthandlermapping[key]
299 299 op.ui.debug('found a handler for part %r\n' % parttype)
300 300 except KeyError:
301 301 if key != parttype: # mandatory parts
302 302 # todo:
303 303 # - use a more precise exception
304 304 raise UnknownPartError(key)
305 305 op.ui.debug('ignoring unknown advisory part %r\n' % key)
306 306 # consuming the part
307 307 part.read()
308 308 continue
309 309
310 310 # handler is called outside the above try block so that we don't
311 311 # risk catching KeyErrors from anything other than the
312 312 # parthandlermapping lookup (any KeyError raised by handler()
313 313 # itself represents a defect of a different variety).
314 314 output = None
315 315 if op.reply is not None:
316 316 op.ui.pushbuffer(error=True)
317 317 output = ''
318 318 try:
319 319 handler(op, part)
320 320 finally:
321 321 if output is not None:
322 322 output = op.ui.popbuffer()
323 323 if output:
324 324 outpart = bundlepart('b2x:output',
325 325 advisoryparams=[('in-reply-to',
326 326 str(part.id))],
327 327 data=output)
328 328 op.reply.addpart(outpart)
329 329 part.read()
330 330 except Exception, exc:
331 331 if part is not None:
332 332 # consume the bundle content
333 333 part.read()
334 334 for part in iterparts:
335 335 # consume the bundle content
336 336 part.read()
337 337 # Small hack to let caller code distinguish exceptions from bundle2
338 338 # processing fron the ones from bundle1 processing. This is mostly
339 339 # needed to handle different return codes to unbundle according to the
340 340 # type of bundle. We should probably clean up or drop this return code
341 341 # craziness in a future version.
342 342 exc.duringunbundle2 = True
343 343 raise
344 344 return op
345 345
346 346 def decodecaps(blob):
347 347 """decode a bundle2 caps bytes blob into a dictionnary
348 348
349 349 The blob is a list of capabilities (one per line)
350 350 Capabilities may have values using a line of the form::
351 351
352 352 capability=value1,value2,value3
353 353
354 354 The values are always a list."""
355 355 caps = {}
356 356 for line in blob.splitlines():
357 357 if not line:
358 358 continue
359 359 if '=' not in line:
360 360 key, vals = line, ()
361 361 else:
362 362 key, vals = line.split('=', 1)
363 363 vals = vals.split(',')
364 364 key = urllib.unquote(key)
365 365 vals = [urllib.unquote(v) for v in vals]
366 366 caps[key] = vals
367 367 return caps
368 368
369 369 def encodecaps(caps):
370 370 """encode a bundle2 caps dictionary into a bytes blob"""
371 371 chunks = []
372 372 for ca in sorted(caps):
373 373 vals = caps[ca]
374 374 ca = urllib.quote(ca)
375 375 vals = [urllib.quote(v) for v in vals]
376 376 if vals:
377 377 ca = "%s=%s" % (ca, ','.join(vals))
378 378 chunks.append(ca)
379 379 return '\n'.join(chunks)
380 380
381 381 class bundle20(object):
382 382 """represent an outgoing bundle2 container
383 383
384 384 Use the `addparam` method to add stream level parameter. and `addpart` to
385 385 populate it. Then call `getchunks` to retrieve all the binary chunks of
386 386 data that compose the bundle2 container."""
387 387
388 388 def __init__(self, ui, capabilities=()):
389 389 self.ui = ui
390 390 self._params = []
391 391 self._parts = []
392 392 self.capabilities = dict(capabilities)
393 393
394 394 def addparam(self, name, value=None):
395 395 """add a stream level parameter"""
396 396 if not name:
397 397 raise ValueError('empty parameter name')
398 398 if name[0] not in string.letters:
399 399 raise ValueError('non letter first character: %r' % name)
400 400 self._params.append((name, value))
401 401
402 402 def addpart(self, part):
403 403 """add a new part to the bundle2 container
404 404
405 405 Parts contains the actual applicative payload."""
406 406 assert part.id is None
407 407 part.id = len(self._parts) # very cheap counter
408 408 self._parts.append(part)
409 409
410 410 def getchunks(self):
411 411 self.ui.debug('start emission of %s stream\n' % _magicstring)
412 412 yield _magicstring
413 413 param = self._paramchunk()
414 414 self.ui.debug('bundle parameter: %s\n' % param)
415 415 yield _pack(_fstreamparamsize, len(param))
416 416 if param:
417 417 yield param
418 418
419 419 self.ui.debug('start of parts\n')
420 420 for part in self._parts:
421 421 self.ui.debug('bundle part: "%s"\n' % part.type)
422 422 for chunk in part.getchunks():
423 423 yield chunk
424 424 self.ui.debug('end of bundle\n')
425 425 yield '\0\0'
426 426
427 427 def _paramchunk(self):
428 428 """return a encoded version of all stream parameters"""
429 429 blocks = []
430 430 for par, value in self._params:
431 431 par = urllib.quote(par)
432 432 if value is not None:
433 433 value = urllib.quote(value)
434 434 par = '%s=%s' % (par, value)
435 435 blocks.append(par)
436 436 return ' '.join(blocks)
437 437
438 438 class unpackermixin(object):
439 439 """A mixin to extract bytes and struct data from a stream"""
440 440
441 441 def __init__(self, fp):
442 442 self._fp = fp
443 443
444 444 def _unpack(self, format):
445 445 """unpack this struct format from the stream"""
446 446 data = self._readexact(struct.calcsize(format))
447 447 return _unpack(format, data)
448 448
449 449 def _readexact(self, size):
450 450 """read exactly <size> bytes from the stream"""
451 451 return changegroup.readexactly(self._fp, size)
452 452
453 453
454 454 class unbundle20(unpackermixin):
455 455 """interpret a bundle2 stream
456 456
457 457 This class is fed with a binary stream and yields parts through its
458 458 `iterparts` methods."""
459 459
460 460 def __init__(self, ui, fp, header=None):
461 461 """If header is specified, we do not read it out of the stream."""
462 462 self.ui = ui
463 463 super(unbundle20, self).__init__(fp)
464 464 if header is None:
465 465 header = self._readexact(4)
466 466 magic, version = header[0:2], header[2:4]
467 467 if magic != 'HG':
468 468 raise util.Abort(_('not a Mercurial bundle'))
469 469 if version != '2X':
470 470 raise util.Abort(_('unknown bundle version %s') % version)
471 471 self.ui.debug('start processing of %s stream\n' % header)
472 472
473 473 @util.propertycache
474 474 def params(self):
475 475 """dictionary of stream level parameters"""
476 476 self.ui.debug('reading bundle2 stream parameters\n')
477 477 params = {}
478 478 paramssize = self._unpack(_fstreamparamsize)[0]
479 479 if paramssize:
480 480 for p in self._readexact(paramssize).split(' '):
481 481 p = p.split('=', 1)
482 482 p = [urllib.unquote(i) for i in p]
483 483 if len(p) < 2:
484 484 p.append(None)
485 485 self._processparam(*p)
486 486 params[p[0]] = p[1]
487 487 return params
488 488
489 489 def _processparam(self, name, value):
490 490 """process a parameter, applying its effect if needed
491 491
492 492 Parameter starting with a lower case letter are advisory and will be
493 493 ignored when unknown. Those starting with an upper case letter are
494 494 mandatory and will this function will raise a KeyError when unknown.
495 495
496 496 Note: no option are currently supported. Any input will be either
497 497 ignored or failing.
498 498 """
499 499 if not name:
500 500 raise ValueError('empty parameter name')
501 501 if name[0] not in string.letters:
502 502 raise ValueError('non letter first character: %r' % name)
503 503 # Some logic will be later added here to try to process the option for
504 504 # a dict of known parameter.
505 505 if name[0].islower():
506 506 self.ui.debug("ignoring unknown parameter %r\n" % name)
507 507 else:
508 508 raise KeyError(name)
509 509
510 510
511 511 def iterparts(self):
512 512 """yield all parts contained in the stream"""
513 513 # make sure param have been loaded
514 514 self.params
515 515 self.ui.debug('start extraction of bundle2 parts\n')
516 516 headerblock = self._readpartheader()
517 517 while headerblock is not None:
518 518 part = unbundlepart(self.ui, headerblock, self._fp)
519 519 yield part
520 520 headerblock = self._readpartheader()
521 521 self.ui.debug('end of bundle2 stream\n')
522 522
523 523 def _readpartheader(self):
524 524 """reads a part header size and return the bytes blob
525 525
526 526 returns None if empty"""
527 527 headersize = self._unpack(_fpartheadersize)[0]
528 528 self.ui.debug('part header size: %i\n' % headersize)
529 529 if headersize:
530 530 return self._readexact(headersize)
531 531 return None
532 532
533 533
534 534 class bundlepart(object):
535 535 """A bundle2 part contains application level payload
536 536
537 537 The part `type` is used to route the part to the application level
538 538 handler.
539 539 """
540 540
541 541 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
542 542 data=''):
543 543 self.id = None
544 544 self.type = parttype
545 545 self.data = data
546 546 self.mandatoryparams = mandatoryparams
547 547 self.advisoryparams = advisoryparams
548 548
549 549 def getchunks(self):
550 550 #### header
551 551 ## parttype
552 552 header = [_pack(_fparttypesize, len(self.type)),
553 553 self.type, _pack(_fpartid, self.id),
554 554 ]
555 555 ## parameters
556 556 # count
557 557 manpar = self.mandatoryparams
558 558 advpar = self.advisoryparams
559 559 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
560 560 # size
561 561 parsizes = []
562 562 for key, value in manpar:
563 563 parsizes.append(len(key))
564 564 parsizes.append(len(value))
565 565 for key, value in advpar:
566 566 parsizes.append(len(key))
567 567 parsizes.append(len(value))
568 568 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
569 569 header.append(paramsizes)
570 570 # key, value
571 571 for key, value in manpar:
572 572 header.append(key)
573 573 header.append(value)
574 574 for key, value in advpar:
575 575 header.append(key)
576 576 header.append(value)
577 577 ## finalize header
578 578 headerchunk = ''.join(header)
579 579 yield _pack(_fpartheadersize, len(headerchunk))
580 580 yield headerchunk
581 581 ## payload
582 582 for chunk in self._payloadchunks():
583 583 yield _pack(_fpayloadsize, len(chunk))
584 584 yield chunk
585 585 # end of payload
586 586 yield _pack(_fpayloadsize, 0)
587 587
588 588 def _payloadchunks(self):
589 589 """yield chunks of a the part payload
590 590
591 591 Exists to handle the different methods to provide data to a part."""
592 592 # we only support fixed size data now.
593 593 # This will be improved in the future.
594 594 if util.safehasattr(self.data, 'next'):
595 595 buff = util.chunkbuffer(self.data)
596 596 chunk = buff.read(preferedchunksize)
597 597 while chunk:
598 598 yield chunk
599 599 chunk = buff.read(preferedchunksize)
600 600 elif len(self.data):
601 601 yield self.data
602 602
603 603 class unbundlepart(unpackermixin):
604 604 """a bundle part read from a bundle"""
605 605
606 606 def __init__(self, ui, header, fp):
607 607 super(unbundlepart, self).__init__(fp)
608 608 self.ui = ui
609 609 # unbundle state attr
610 610 self._headerdata = header
611 611 self._headeroffset = 0
612 612 self._initialized = False
613 613 self.consumed = False
614 614 # part data
615 615 self.id = None
616 616 self.type = None
617 617 self.mandatoryparams = None
618 618 self.advisoryparams = None
619 619 self._payloadstream = None
620 620 self._readheader()
621 621
622 622 def _fromheader(self, size):
623 623 """return the next <size> byte from the header"""
624 624 offset = self._headeroffset
625 625 data = self._headerdata[offset:(offset + size)]
626 626 self._headeroffset = offset + size
627 627 return data
628 628
629 629 def _unpackheader(self, format):
630 630 """read given format from header
631 631
632 632 This automatically compute the size of the format to read."""
633 633 data = self._fromheader(struct.calcsize(format))
634 634 return _unpack(format, data)
635 635
636 636 def _readheader(self):
637 637 """read the header and setup the object"""
638 638 typesize = self._unpackheader(_fparttypesize)[0]
639 639 self.type = self._fromheader(typesize)
640 640 self.ui.debug('part type: "%s"\n' % self.type)
641 641 self.id = self._unpackheader(_fpartid)[0]
642 642 self.ui.debug('part id: "%s"\n' % self.id)
643 643 ## reading parameters
644 644 # param count
645 645 mancount, advcount = self._unpackheader(_fpartparamcount)
646 646 self.ui.debug('part parameters: %i\n' % (mancount + advcount))
647 647 # param size
648 648 fparamsizes = _makefpartparamsizes(mancount + advcount)
649 649 paramsizes = self._unpackheader(fparamsizes)
650 650 # make it a list of couple again
651 651 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
652 652 # split mandatory from advisory
653 653 mansizes = paramsizes[:mancount]
654 654 advsizes = paramsizes[mancount:]
655 655 # retrive param value
656 656 manparams = []
657 657 for key, value in mansizes:
658 658 manparams.append((self._fromheader(key), self._fromheader(value)))
659 659 advparams = []
660 660 for key, value in advsizes:
661 661 advparams.append((self._fromheader(key), self._fromheader(value)))
662 662 self.mandatoryparams = manparams
663 663 self.advisoryparams = advparams
664 664 ## part payload
665 665 def payloadchunks():
666 666 payloadsize = self._unpack(_fpayloadsize)[0]
667 667 self.ui.debug('payload chunk size: %i\n' % payloadsize)
668 668 while payloadsize:
669 669 yield self._readexact(payloadsize)
670 670 payloadsize = self._unpack(_fpayloadsize)[0]
671 671 self.ui.debug('payload chunk size: %i\n' % payloadsize)
672 672 self._payloadstream = util.chunkbuffer(payloadchunks())
673 673 # we read the data, tell it
674 674 self._initialized = True
675 675
676 676 def read(self, size=None):
677 677 """read payload data"""
678 678 if not self._initialized:
679 679 self._readheader()
680 680 if size is None:
681 681 data = self._payloadstream.read()
682 682 else:
683 683 data = self._payloadstream.read(size)
684 684 if size is None or len(data) < size:
685 685 self.consumed = True
686 686 return data
687 687
688 688
689 689 @parthandler('b2x:changegroup')
690 690 def handlechangegroup(op, inpart):
691 691 """apply a changegroup part on the repo
692 692
693 693 This is a very early implementation that will massive rework before being
694 694 inflicted to any end-user.
695 695 """
696 696 # Make sure we trigger a transaction creation
697 697 #
698 698 # The addchangegroup function will get a transaction object by itself, but
699 699 # we need to make sure we trigger the creation of a transaction object used
700 700 # for the whole processing scope.
701 701 op.gettransaction()
702 702 cg = changegroup.unbundle10(inpart, 'UN')
703 703 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
704 704 op.records.add('changegroup', {'return': ret})
705 705 if op.reply is not None:
706 706 # This is definitly not the final form of this
707 707 # return. But one need to start somewhere.
708 708 part = bundlepart('b2x:reply:changegroup', (),
709 709 [('in-reply-to', str(inpart.id)),
710 710 ('return', '%i' % ret)])
711 711 op.reply.addpart(part)
712 712 assert not inpart.read()
713 713
714 714 @parthandler('b2x:reply:changegroup')
715 715 def handlechangegroup(op, inpart):
716 716 p = dict(inpart.advisoryparams)
717 717 ret = int(p['return'])
718 718 op.records.add('changegroup', {'return': ret}, int(p['in-reply-to']))
719 719
720 720 @parthandler('b2x:check:heads')
721 721 def handlechangegroup(op, inpart):
722 722 """check that head of the repo did not change
723 723
724 724 This is used to detect a push race when using unbundle.
725 725 This replaces the "heads" argument of unbundle."""
726 726 h = inpart.read(20)
727 727 heads = []
728 728 while len(h) == 20:
729 729 heads.append(h)
730 730 h = inpart.read(20)
731 731 assert not h
732 732 if heads != op.repo.heads():
733 733 raise exchange.PushRaced()
734 734
735 735 @parthandler('b2x:output')
736 736 def handleoutput(op, inpart):
737 737 """forward output captured on the server to the client"""
738 738 for line in inpart.read().splitlines():
739 739 op.ui.write(('remote: %s\n' % line))
740 740
741 741 @parthandler('b2x:replycaps')
742 742 def handlereplycaps(op, inpart):
743 743 """Notify that a reply bundle should be created
744 744
745 745 The payload contains the capabilities information for the reply"""
746 746 caps = decodecaps(inpart.read())
747 747 if op.reply is None:
748 748 op.reply = bundle20(op.ui, caps)
749 749
750 750 @parthandler('b2x:error:abort')
751 751 def handlereplycaps(op, inpart):
752 752 """Used to transmit abort error over the wire"""
753 753 manargs = dict(inpart.mandatoryparams)
754 754 advargs = dict(inpart.advisoryparams)
755 755 raise util.Abort(manargs['message'], hint=advargs.get('hint'))
756
757 @parthandler('b2x:error:unknownpart')
758 def handlereplycaps(op, inpart):
759 """Used to transmit unknown part error over the wire"""
760 manargs = dict(inpart.mandatoryparams)
761 raise UnknownPartError(manargs['parttype'])
@@ -1,824 +1,830 b''
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
12 12 import peer, error, encoding, util, store, 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 return sep.join(map(hex, l))
176 176
177 177 # batched call argument encoding
178 178
179 179 def escapearg(plain):
180 180 return (plain
181 181 .replace(':', '::')
182 182 .replace(',', ':,')
183 183 .replace(';', ':;')
184 184 .replace('=', ':='))
185 185
186 186 def unescapearg(escaped):
187 187 return (escaped
188 188 .replace(':=', '=')
189 189 .replace(':;', ';')
190 190 .replace(':,', ',')
191 191 .replace('::', ':'))
192 192
193 193 # client side
194 194
195 195 class wirepeer(peer.peerrepository):
196 196
197 197 def batch(self):
198 198 return remotebatch(self)
199 199 def _submitbatch(self, req):
200 200 cmds = []
201 201 for op, argsdict in req:
202 202 args = ','.join('%s=%s' % p for p in argsdict.iteritems())
203 203 cmds.append('%s %s' % (op, args))
204 204 rsp = self._call("batch", cmds=';'.join(cmds))
205 205 return rsp.split(';')
206 206 def _submitone(self, op, args):
207 207 return self._call(op, **args)
208 208
209 209 @batchable
210 210 def lookup(self, key):
211 211 self.requirecap('lookup', _('look up remote revision'))
212 212 f = future()
213 213 yield {'key': encoding.fromlocal(key)}, f
214 214 d = f.value
215 215 success, data = d[:-1].split(" ", 1)
216 216 if int(success):
217 217 yield bin(data)
218 218 self._abort(error.RepoError(data))
219 219
220 220 @batchable
221 221 def heads(self):
222 222 f = future()
223 223 yield {}, f
224 224 d = f.value
225 225 try:
226 226 yield decodelist(d[:-1])
227 227 except ValueError:
228 228 self._abort(error.ResponseError(_("unexpected response:"), d))
229 229
230 230 @batchable
231 231 def known(self, nodes):
232 232 f = future()
233 233 yield {'nodes': encodelist(nodes)}, f
234 234 d = f.value
235 235 try:
236 236 yield [bool(int(f)) for f in d]
237 237 except ValueError:
238 238 self._abort(error.ResponseError(_("unexpected response:"), d))
239 239
240 240 @batchable
241 241 def branchmap(self):
242 242 f = future()
243 243 yield {}, f
244 244 d = f.value
245 245 try:
246 246 branchmap = {}
247 247 for branchpart in d.splitlines():
248 248 branchname, branchheads = branchpart.split(' ', 1)
249 249 branchname = encoding.tolocal(urllib.unquote(branchname))
250 250 branchheads = decodelist(branchheads)
251 251 branchmap[branchname] = branchheads
252 252 yield branchmap
253 253 except TypeError:
254 254 self._abort(error.ResponseError(_("unexpected response:"), d))
255 255
256 256 def branches(self, nodes):
257 257 n = encodelist(nodes)
258 258 d = self._call("branches", nodes=n)
259 259 try:
260 260 br = [tuple(decodelist(b)) for b in d.splitlines()]
261 261 return br
262 262 except ValueError:
263 263 self._abort(error.ResponseError(_("unexpected response:"), d))
264 264
265 265 def between(self, pairs):
266 266 batch = 8 # avoid giant requests
267 267 r = []
268 268 for i in xrange(0, len(pairs), batch):
269 269 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
270 270 d = self._call("between", pairs=n)
271 271 try:
272 272 r.extend(l and decodelist(l) or [] for l in d.splitlines())
273 273 except ValueError:
274 274 self._abort(error.ResponseError(_("unexpected response:"), d))
275 275 return r
276 276
277 277 @batchable
278 278 def pushkey(self, namespace, key, old, new):
279 279 if not self.capable('pushkey'):
280 280 yield False, None
281 281 f = future()
282 282 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
283 283 yield {'namespace': encoding.fromlocal(namespace),
284 284 'key': encoding.fromlocal(key),
285 285 'old': encoding.fromlocal(old),
286 286 'new': encoding.fromlocal(new)}, f
287 287 d = f.value
288 288 d, output = d.split('\n', 1)
289 289 try:
290 290 d = bool(int(d))
291 291 except ValueError:
292 292 raise error.ResponseError(
293 293 _('push failed (unexpected response):'), d)
294 294 for l in output.splitlines(True):
295 295 self.ui.status(_('remote: '), l)
296 296 yield d
297 297
298 298 @batchable
299 299 def listkeys(self, namespace):
300 300 if not self.capable('pushkey'):
301 301 yield {}, None
302 302 f = future()
303 303 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
304 304 yield {'namespace': encoding.fromlocal(namespace)}, f
305 305 d = f.value
306 306 r = {}
307 307 for l in d.splitlines():
308 308 k, v = l.split('\t')
309 309 r[encoding.tolocal(k)] = encoding.tolocal(v)
310 310 yield r
311 311
312 312 def stream_out(self):
313 313 return self._callstream('stream_out')
314 314
315 315 def changegroup(self, nodes, kind):
316 316 n = encodelist(nodes)
317 317 f = self._callcompressable("changegroup", roots=n)
318 318 return changegroupmod.unbundle10(f, 'UN')
319 319
320 320 def changegroupsubset(self, bases, heads, kind):
321 321 self.requirecap('changegroupsubset', _('look up remote changes'))
322 322 bases = encodelist(bases)
323 323 heads = encodelist(heads)
324 324 f = self._callcompressable("changegroupsubset",
325 325 bases=bases, heads=heads)
326 326 return changegroupmod.unbundle10(f, 'UN')
327 327
328 328 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
329 329 **kwargs):
330 330 self.requirecap('getbundle', _('look up remote changes'))
331 331 opts = {}
332 332 if heads is not None:
333 333 opts['heads'] = encodelist(heads)
334 334 if common is not None:
335 335 opts['common'] = encodelist(common)
336 336 if bundlecaps is not None:
337 337 opts['bundlecaps'] = ','.join(bundlecaps)
338 338 opts.update(kwargs)
339 339 f = self._callcompressable("getbundle", **opts)
340 340 if bundlecaps is not None and 'HG2X' in bundlecaps:
341 341 return bundle2.unbundle20(self.ui, f)
342 342 else:
343 343 return changegroupmod.unbundle10(f, 'UN')
344 344
345 345 def unbundle(self, cg, heads, source):
346 346 '''Send cg (a readable file-like object representing the
347 347 changegroup to push, typically a chunkbuffer object) to the
348 348 remote server as a bundle.
349 349
350 350 When pushing a bundle10 stream, return an integer indicating the
351 351 result of the push (see localrepository.addchangegroup()).
352 352
353 353 When pushing a bundle20 stream, return a bundle20 stream.'''
354 354
355 355 if heads != ['force'] and self.capable('unbundlehash'):
356 356 heads = encodelist(['hashed',
357 357 util.sha1(''.join(sorted(heads))).digest()])
358 358 else:
359 359 heads = encodelist(heads)
360 360
361 361 if util.safehasattr(cg, 'deltaheader'):
362 362 # this a bundle10, do the old style call sequence
363 363 ret, output = self._callpush("unbundle", cg, heads=heads)
364 364 if ret == "":
365 365 raise error.ResponseError(
366 366 _('push failed:'), output)
367 367 try:
368 368 ret = int(ret)
369 369 except ValueError:
370 370 raise error.ResponseError(
371 371 _('push failed (unexpected response):'), ret)
372 372
373 373 for l in output.splitlines(True):
374 374 self.ui.status(_('remote: '), l)
375 375 else:
376 376 # bundle2 push. Send a stream, fetch a stream.
377 377 stream = self._calltwowaystream('unbundle', cg, heads=heads)
378 378 ret = bundle2.unbundle20(self.ui, stream)
379 379 return ret
380 380
381 381 def debugwireargs(self, one, two, three=None, four=None, five=None):
382 382 # don't pass optional arguments left at their default value
383 383 opts = {}
384 384 if three is not None:
385 385 opts['three'] = three
386 386 if four is not None:
387 387 opts['four'] = four
388 388 return self._call('debugwireargs', one=one, two=two, **opts)
389 389
390 390 def _call(self, cmd, **args):
391 391 """execute <cmd> on the server
392 392
393 393 The command is expected to return a simple string.
394 394
395 395 returns the server reply as a string."""
396 396 raise NotImplementedError()
397 397
398 398 def _callstream(self, cmd, **args):
399 399 """execute <cmd> on the server
400 400
401 401 The command is expected to return a stream.
402 402
403 403 returns the server reply as a file like object."""
404 404 raise NotImplementedError()
405 405
406 406 def _callcompressable(self, cmd, **args):
407 407 """execute <cmd> on the server
408 408
409 409 The command is expected to return a stream.
410 410
411 411 The stream may have been compressed in some implementations. This
412 412 function takes care of the decompression. This is the only difference
413 413 with _callstream.
414 414
415 415 returns the server reply as a file like object.
416 416 """
417 417 raise NotImplementedError()
418 418
419 419 def _callpush(self, cmd, fp, **args):
420 420 """execute a <cmd> on server
421 421
422 422 The command is expected to be related to a push. Push has a special
423 423 return method.
424 424
425 425 returns the server reply as a (ret, output) tuple. ret is either
426 426 empty (error) or a stringified int.
427 427 """
428 428 raise NotImplementedError()
429 429
430 430 def _calltwowaystream(self, cmd, fp, **args):
431 431 """execute <cmd> on server
432 432
433 433 The command will send a stream to the server and get a stream in reply.
434 434 """
435 435 raise NotImplementedError()
436 436
437 437 def _abort(self, exception):
438 438 """clearly abort the wire protocol connection and raise the exception
439 439 """
440 440 raise NotImplementedError()
441 441
442 442 # server side
443 443
444 444 # wire protocol command can either return a string or one of these classes.
445 445 class streamres(object):
446 446 """wireproto reply: binary stream
447 447
448 448 The call was successful and the result is a stream.
449 449 Iterate on the `self.gen` attribute to retrieve chunks.
450 450 """
451 451 def __init__(self, gen):
452 452 self.gen = gen
453 453
454 454 class pushres(object):
455 455 """wireproto reply: success with simple integer return
456 456
457 457 The call was successful and returned an integer contained in `self.res`.
458 458 """
459 459 def __init__(self, res):
460 460 self.res = res
461 461
462 462 class pusherr(object):
463 463 """wireproto reply: failure
464 464
465 465 The call failed. The `self.res` attribute contains the error message.
466 466 """
467 467 def __init__(self, res):
468 468 self.res = res
469 469
470 470 class ooberror(object):
471 471 """wireproto reply: failure of a batch of operation
472 472
473 473 Something failed during a batch call. The error message is stored in
474 474 `self.message`.
475 475 """
476 476 def __init__(self, message):
477 477 self.message = message
478 478
479 479 def dispatch(repo, proto, command):
480 480 repo = repo.filtered("served")
481 481 func, spec = commands[command]
482 482 args = proto.getargs(spec)
483 483 return func(repo, proto, *args)
484 484
485 485 def options(cmd, keys, others):
486 486 opts = {}
487 487 for k in keys:
488 488 if k in others:
489 489 opts[k] = others[k]
490 490 del others[k]
491 491 if others:
492 492 sys.stderr.write("abort: %s got unexpected arguments %s\n"
493 493 % (cmd, ",".join(others)))
494 494 return opts
495 495
496 496 # list of commands
497 497 commands = {}
498 498
499 499 def wireprotocommand(name, args=''):
500 500 """decorator for wire protocol command"""
501 501 def register(func):
502 502 commands[name] = (func, args)
503 503 return func
504 504 return register
505 505
506 506 @wireprotocommand('batch', 'cmds *')
507 507 def batch(repo, proto, cmds, others):
508 508 repo = repo.filtered("served")
509 509 res = []
510 510 for pair in cmds.split(';'):
511 511 op, args = pair.split(' ', 1)
512 512 vals = {}
513 513 for a in args.split(','):
514 514 if a:
515 515 n, v = a.split('=')
516 516 vals[n] = unescapearg(v)
517 517 func, spec = commands[op]
518 518 if spec:
519 519 keys = spec.split()
520 520 data = {}
521 521 for k in keys:
522 522 if k == '*':
523 523 star = {}
524 524 for key in vals.keys():
525 525 if key not in keys:
526 526 star[key] = vals[key]
527 527 data['*'] = star
528 528 else:
529 529 data[k] = vals[k]
530 530 result = func(repo, proto, *[data[k] for k in keys])
531 531 else:
532 532 result = func(repo, proto)
533 533 if isinstance(result, ooberror):
534 534 return result
535 535 res.append(escapearg(result))
536 536 return ';'.join(res)
537 537
538 538 @wireprotocommand('between', 'pairs')
539 539 def between(repo, proto, pairs):
540 540 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
541 541 r = []
542 542 for b in repo.between(pairs):
543 543 r.append(encodelist(b) + "\n")
544 544 return "".join(r)
545 545
546 546 @wireprotocommand('branchmap')
547 547 def branchmap(repo, proto):
548 548 branchmap = repo.branchmap()
549 549 heads = []
550 550 for branch, nodes in branchmap.iteritems():
551 551 branchname = urllib.quote(encoding.fromlocal(branch))
552 552 branchnodes = encodelist(nodes)
553 553 heads.append('%s %s' % (branchname, branchnodes))
554 554 return '\n'.join(heads)
555 555
556 556 @wireprotocommand('branches', 'nodes')
557 557 def branches(repo, proto, nodes):
558 558 nodes = decodelist(nodes)
559 559 r = []
560 560 for b in repo.branches(nodes):
561 561 r.append(encodelist(b) + "\n")
562 562 return "".join(r)
563 563
564 564
565 565 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
566 566 'known', 'getbundle', 'unbundlehash', 'batch']
567 567
568 568 def _capabilities(repo, proto):
569 569 """return a list of capabilities for a repo
570 570
571 571 This function exists to allow extensions to easily wrap capabilities
572 572 computation
573 573
574 574 - returns a lists: easy to alter
575 575 - change done here will be propagated to both `capabilities` and `hello`
576 576 command without any other action needed.
577 577 """
578 578 # copy to prevent modification of the global list
579 579 caps = list(wireprotocaps)
580 580 if _allowstream(repo.ui):
581 581 if repo.ui.configbool('server', 'preferuncompressed', False):
582 582 caps.append('stream-preferred')
583 583 requiredformats = repo.requirements & repo.supportedformats
584 584 # if our local revlogs are just revlogv1, add 'stream' cap
585 585 if not requiredformats - set(('revlogv1',)):
586 586 caps.append('stream')
587 587 # otherwise, add 'streamreqs' detailing our local revlog format
588 588 else:
589 589 caps.append('streamreqs=%s' % ','.join(requiredformats))
590 590 if repo.ui.configbool('experimental', 'bundle2-exp', False):
591 591 capsblob = bundle2.encodecaps(repo.bundle2caps)
592 592 caps.append('bundle2-exp=' + urllib.quote(capsblob))
593 593 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
594 594 caps.append('httpheader=1024')
595 595 return caps
596 596
597 597 # If you are writing an extension and consider wrapping this function. Wrap
598 598 # `_capabilities` instead.
599 599 @wireprotocommand('capabilities')
600 600 def capabilities(repo, proto):
601 601 return ' '.join(_capabilities(repo, proto))
602 602
603 603 @wireprotocommand('changegroup', 'roots')
604 604 def changegroup(repo, proto, roots):
605 605 nodes = decodelist(roots)
606 606 cg = changegroupmod.changegroup(repo, nodes, 'serve')
607 607 return streamres(proto.groupchunks(cg))
608 608
609 609 @wireprotocommand('changegroupsubset', 'bases heads')
610 610 def changegroupsubset(repo, proto, bases, heads):
611 611 bases = decodelist(bases)
612 612 heads = decodelist(heads)
613 613 cg = changegroupmod.changegroupsubset(repo, bases, heads, 'serve')
614 614 return streamres(proto.groupchunks(cg))
615 615
616 616 @wireprotocommand('debugwireargs', 'one two *')
617 617 def debugwireargs(repo, proto, one, two, others):
618 618 # only accept optional args from the known set
619 619 opts = options('debugwireargs', ['three', 'four'], others)
620 620 return repo.debugwireargs(one, two, **opts)
621 621
622 622 @wireprotocommand('getbundle', '*')
623 623 def getbundle(repo, proto, others):
624 624 opts = options('getbundle', ['heads', 'common', 'bundlecaps'], others)
625 625 for k, v in opts.iteritems():
626 626 if k in ('heads', 'common'):
627 627 opts[k] = decodelist(v)
628 628 elif k == 'bundlecaps':
629 629 opts[k] = set(v.split(','))
630 630 cg = exchange.getbundle(repo, 'serve', **opts)
631 631 return streamres(proto.groupchunks(cg))
632 632
633 633 @wireprotocommand('heads')
634 634 def heads(repo, proto):
635 635 h = repo.heads()
636 636 return encodelist(h) + "\n"
637 637
638 638 @wireprotocommand('hello')
639 639 def hello(repo, proto):
640 640 '''the hello command returns a set of lines describing various
641 641 interesting things about the server, in an RFC822-like format.
642 642 Currently the only one defined is "capabilities", which
643 643 consists of a line in the form:
644 644
645 645 capabilities: space separated list of tokens
646 646 '''
647 647 return "capabilities: %s\n" % (capabilities(repo, proto))
648 648
649 649 @wireprotocommand('listkeys', 'namespace')
650 650 def listkeys(repo, proto, namespace):
651 651 d = repo.listkeys(encoding.tolocal(namespace)).items()
652 652 t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v))
653 653 for k, v in d])
654 654 return t
655 655
656 656 @wireprotocommand('lookup', 'key')
657 657 def lookup(repo, proto, key):
658 658 try:
659 659 k = encoding.tolocal(key)
660 660 c = repo[k]
661 661 r = c.hex()
662 662 success = 1
663 663 except Exception, inst:
664 664 r = str(inst)
665 665 success = 0
666 666 return "%s %s\n" % (success, r)
667 667
668 668 @wireprotocommand('known', 'nodes *')
669 669 def known(repo, proto, nodes, others):
670 670 return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes)))
671 671
672 672 @wireprotocommand('pushkey', 'namespace key old new')
673 673 def pushkey(repo, proto, namespace, key, old, new):
674 674 # compatibility with pre-1.8 clients which were accidentally
675 675 # sending raw binary nodes rather than utf-8-encoded hex
676 676 if len(new) == 20 and new.encode('string-escape') != new:
677 677 # looks like it could be a binary node
678 678 try:
679 679 new.decode('utf-8')
680 680 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
681 681 except UnicodeDecodeError:
682 682 pass # binary, leave unmodified
683 683 else:
684 684 new = encoding.tolocal(new) # normal path
685 685
686 686 if util.safehasattr(proto, 'restore'):
687 687
688 688 proto.redirect()
689 689
690 690 try:
691 691 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
692 692 encoding.tolocal(old), new) or False
693 693 except util.Abort:
694 694 r = False
695 695
696 696 output = proto.restore()
697 697
698 698 return '%s\n%s' % (int(r), output)
699 699
700 700 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
701 701 encoding.tolocal(old), new)
702 702 return '%s\n' % int(r)
703 703
704 704 def _allowstream(ui):
705 705 return ui.configbool('server', 'uncompressed', True, untrusted=True)
706 706
707 707 def _walkstreamfiles(repo):
708 708 # this is it's own function so extensions can override it
709 709 return repo.store.walk()
710 710
711 711 @wireprotocommand('stream_out')
712 712 def stream(repo, proto):
713 713 '''If the server supports streaming clone, it advertises the "stream"
714 714 capability with a value representing the version and flags of the repo
715 715 it is serving. Client checks to see if it understands the format.
716 716
717 717 The format is simple: the server writes out a line with the amount
718 718 of files, then the total amount of bytes to be transferred (separated
719 719 by a space). Then, for each file, the server first writes the filename
720 720 and file size (separated by the null character), then the file contents.
721 721 '''
722 722
723 723 if not _allowstream(repo.ui):
724 724 return '1\n'
725 725
726 726 entries = []
727 727 total_bytes = 0
728 728 try:
729 729 # get consistent snapshot of repo, lock during scan
730 730 lock = repo.lock()
731 731 try:
732 732 repo.ui.debug('scanning\n')
733 733 for name, ename, size in _walkstreamfiles(repo):
734 734 if size:
735 735 entries.append((name, size))
736 736 total_bytes += size
737 737 finally:
738 738 lock.release()
739 739 except error.LockError:
740 740 return '2\n' # error: 2
741 741
742 742 def streamer(repo, entries, total):
743 743 '''stream out all metadata files in repository.'''
744 744 yield '0\n' # success
745 745 repo.ui.debug('%d files, %d bytes to transfer\n' %
746 746 (len(entries), total_bytes))
747 747 yield '%d %d\n' % (len(entries), total_bytes)
748 748
749 749 sopener = repo.sopener
750 750 oldaudit = sopener.mustaudit
751 751 debugflag = repo.ui.debugflag
752 752 sopener.mustaudit = False
753 753
754 754 try:
755 755 for name, size in entries:
756 756 if debugflag:
757 757 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
758 758 # partially encode name over the wire for backwards compat
759 759 yield '%s\0%d\n' % (store.encodedir(name), size)
760 760 if size <= 65536:
761 761 fp = sopener(name)
762 762 try:
763 763 data = fp.read(size)
764 764 finally:
765 765 fp.close()
766 766 yield data
767 767 else:
768 768 for chunk in util.filechunkiter(sopener(name), limit=size):
769 769 yield chunk
770 770 # replace with "finally:" when support for python 2.4 has been dropped
771 771 except Exception:
772 772 sopener.mustaudit = oldaudit
773 773 raise
774 774 sopener.mustaudit = oldaudit
775 775
776 776 return streamres(streamer(repo, entries, total_bytes))
777 777
778 778 @wireprotocommand('unbundle', 'heads')
779 779 def unbundle(repo, proto, heads):
780 780 their_heads = decodelist(heads)
781 781
782 782 try:
783 783 proto.redirect()
784 784
785 785 exchange.check_heads(repo, their_heads, 'preparing changes')
786 786
787 787 # write bundle data to temporary file because it can be big
788 788 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
789 789 fp = os.fdopen(fd, 'wb+')
790 790 r = 0
791 791 try:
792 792 proto.getfile(fp)
793 793 fp.seek(0)
794 794 gen = exchange.readbundle(repo.ui, fp, None)
795 795 r = exchange.unbundle(repo, gen, their_heads, 'serve',
796 796 proto._client())
797 797 if util.safehasattr(r, 'addpart'):
798 798 # The return looks streameable, we are in the bundle2 case and
799 799 # should return a stream.
800 800 return streamres(r.getchunks())
801 801 return pushres(r)
802 802
803 803 finally:
804 804 fp.close()
805 805 os.unlink(tempname)
806 except bundle2.UnknownPartError, exc:
807 bundler = bundle2.bundle20(repo.ui)
808 part = bundle2.bundlepart('B2X:ERROR:UNKNOWNPART',
809 [('parttype', str(exc))])
810 bundler.addpart(part)
811 return streamres(bundler.getchunks())
806 812 except util.Abort, inst:
807 813 # The old code we moved used sys.stderr directly.
808 814 # We did not change it to minimise code change.
809 815 # This need to be moved to something proper.
810 816 # Feel free to do it.
811 817 if getattr(inst, 'duringunbundle2', False):
812 818 bundler = bundle2.bundle20(repo.ui)
813 819 manargs = [('message', str(inst))]
814 820 advargs = []
815 821 if inst.hint is not None:
816 822 advargs.append(('hint', inst.hint))
817 823 bundler.addpart(bundle2.bundlepart('B2X:ERROR:ABORT',
818 824 manargs, advargs))
819 825 return streamres(bundler.getchunks())
820 826 else:
821 827 sys.stderr.write("abort: %s\n" % inst)
822 828 return pushres(0)
823 829 except exchange.PushRaced, exc:
824 830 return pusherr(str(exc))
@@ -1,971 +1,997 b''
1 1
2 2 Create an extension to test bundle2 API
3 3
4 4 $ cat > bundle2.py << EOF
5 5 > """A small extension to test bundle2 implementation
6 6 >
7 7 > Current bundle2 implementation is far too limited to be used in any core
8 8 > code. We still need to be able to test it while it grow up.
9 9 > """
10 10 >
11 11 > import sys
12 12 > from mercurial import cmdutil
13 13 > from mercurial import util
14 14 > from mercurial import bundle2
15 15 > from mercurial import scmutil
16 16 > from mercurial import discovery
17 17 > from mercurial import changegroup
18 18 > cmdtable = {}
19 19 > command = cmdutil.command(cmdtable)
20 20 >
21 21 > ELEPHANTSSONG = """Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
22 22 > Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
23 23 > Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko."""
24 24 > assert len(ELEPHANTSSONG) == 178 # future test say 178 bytes, trust it.
25 25 >
26 26 > @bundle2.parthandler('test:song')
27 27 > def songhandler(op, part):
28 28 > """handle a "test:song" bundle2 part, printing the lyrics on stdin"""
29 29 > op.ui.write('The choir starts singing:\n')
30 30 > verses = 0
31 31 > for line in part.read().split('\n'):
32 32 > op.ui.write(' %s\n' % line)
33 33 > verses += 1
34 34 > op.records.add('song', {'verses': verses})
35 35 >
36 36 > @bundle2.parthandler('test:ping')
37 37 > def pinghandler(op, part):
38 38 > op.ui.write('received ping request (id %i)\n' % part.id)
39 39 > if op.reply is not None and 'ping-pong' in op.reply.capabilities:
40 40 > op.ui.write_err('replying to ping request (id %i)\n' % part.id)
41 41 > rpart = bundle2.bundlepart('test:pong',
42 42 > [('in-reply-to', str(part.id))])
43 43 > op.reply.addpart(rpart)
44 44 >
45 45 > @bundle2.parthandler('test:debugreply')
46 46 > def debugreply(op, part):
47 47 > """print data about the capacity of the bundle reply"""
48 48 > if op.reply is None:
49 49 > op.ui.write('debugreply: no reply\n')
50 50 > else:
51 51 > op.ui.write('debugreply: capabilities:\n')
52 52 > for cap in sorted(op.reply.capabilities):
53 53 > op.ui.write('debugreply: %r\n' % cap)
54 54 > for val in op.reply.capabilities[cap]:
55 55 > op.ui.write('debugreply: %r\n' % val)
56 56 >
57 57 > @command('bundle2',
58 58 > [('', 'param', [], 'stream level parameter'),
59 59 > ('', 'unknown', False, 'include an unknown mandatory part in the bundle'),
60 60 > ('', 'parts', False, 'include some arbitrary parts to the bundle'),
61 61 > ('', 'reply', False, 'produce a reply bundle'),
62 62 > ('r', 'rev', [], 'includes those changeset in the bundle'),],
63 63 > '[OUTPUTFILE]')
64 64 > def cmdbundle2(ui, repo, path=None, **opts):
65 65 > """write a bundle2 container on standard ouput"""
66 66 > bundler = bundle2.bundle20(ui)
67 67 > for p in opts['param']:
68 68 > p = p.split('=', 1)
69 69 > try:
70 70 > bundler.addparam(*p)
71 71 > except ValueError, exc:
72 72 > raise util.Abort('%s' % exc)
73 73 >
74 74 > if opts['reply']:
75 75 > capsstring = 'ping-pong\nelephants=babar,celeste\ncity%3D%21=celeste%2Cville'
76 76 > bundler.addpart(bundle2.bundlepart('b2x:replycaps', data=capsstring))
77 77 >
78 78 > revs = opts['rev']
79 79 > if 'rev' in opts:
80 80 > revs = scmutil.revrange(repo, opts['rev'])
81 81 > if revs:
82 82 > # very crude version of a changegroup part creation
83 83 > bundled = repo.revs('%ld::%ld', revs, revs)
84 84 > headmissing = [c.node() for c in repo.set('heads(%ld)', revs)]
85 85 > headcommon = [c.node() for c in repo.set('parents(%ld) - %ld', revs, revs)]
86 86 > outgoing = discovery.outgoing(repo.changelog, headcommon, headmissing)
87 87 > cg = changegroup.getlocalbundle(repo, 'test:bundle2', outgoing, None)
88 88 > part = bundle2.bundlepart('b2x:changegroup', data=cg.getchunks())
89 89 > bundler.addpart(part)
90 90 >
91 91 > if opts['parts']:
92 92 > part = bundle2.bundlepart('test:empty')
93 93 > bundler.addpart(part)
94 94 > # add a second one to make sure we handle multiple parts
95 95 > part = bundle2.bundlepart('test:empty')
96 96 > bundler.addpart(part)
97 97 > part = bundle2.bundlepart('test:song', data=ELEPHANTSSONG)
98 98 > bundler.addpart(part)
99 99 > part = bundle2.bundlepart('test:debugreply')
100 100 > bundler.addpart(part)
101 101 > part = bundle2.bundlepart('test:math',
102 102 > [('pi', '3.14'), ('e', '2.72')],
103 103 > [('cooking', 'raw')],
104 104 > '42')
105 105 > bundler.addpart(part)
106 106 > if opts['unknown']:
107 107 > part = bundle2.bundlepart('test:UNKNOWN',
108 108 > data='some random content')
109 109 > bundler.addpart(part)
110 110 > if opts['parts']:
111 111 > part = bundle2.bundlepart('test:ping')
112 112 > bundler.addpart(part)
113 113 >
114 114 > if path is None:
115 115 > file = sys.stdout
116 116 > else:
117 117 > file = open(path, 'w')
118 118 >
119 119 > for chunk in bundler.getchunks():
120 120 > file.write(chunk)
121 121 >
122 122 > @command('unbundle2', [], '')
123 123 > def cmdunbundle2(ui, repo, replypath=None):
124 124 > """process a bundle2 stream from stdin on the current repo"""
125 125 > try:
126 126 > tr = None
127 127 > lock = repo.lock()
128 128 > tr = repo.transaction('processbundle')
129 129 > try:
130 130 > unbundler = bundle2.unbundle20(ui, sys.stdin)
131 131 > op = bundle2.processbundle(repo, unbundler, lambda: tr)
132 132 > tr.close()
133 133 > except KeyError, exc:
134 134 > raise util.Abort('missing support for %s' % exc)
135 135 > finally:
136 136 > if tr is not None:
137 137 > tr.release()
138 138 > lock.release()
139 139 > remains = sys.stdin.read()
140 140 > ui.write('%i unread bytes\n' % len(remains))
141 141 > if op.records['song']:
142 142 > totalverses = sum(r['verses'] for r in op.records['song'])
143 143 > ui.write('%i total verses sung\n' % totalverses)
144 144 > for rec in op.records['changegroup']:
145 145 > ui.write('addchangegroup return: %i\n' % rec['return'])
146 146 > if op.reply is not None and replypath is not None:
147 147 > file = open(replypath, 'w')
148 148 > for chunk in op.reply.getchunks():
149 149 > file.write(chunk)
150 150 >
151 151 > @command('statbundle2', [], '')
152 152 > def cmdstatbundle2(ui, repo):
153 153 > """print statistic on the bundle2 container read from stdin"""
154 154 > unbundler = bundle2.unbundle20(ui, sys.stdin)
155 155 > try:
156 156 > params = unbundler.params
157 157 > except KeyError, exc:
158 158 > raise util.Abort('unknown parameters: %s' % exc)
159 159 > ui.write('options count: %i\n' % len(params))
160 160 > for key in sorted(params):
161 161 > ui.write('- %s\n' % key)
162 162 > value = params[key]
163 163 > if value is not None:
164 164 > ui.write(' %s\n' % value)
165 165 > count = 0
166 166 > for p in unbundler.iterparts():
167 167 > count += 1
168 168 > ui.write(' :%s:\n' % p.type)
169 169 > ui.write(' mandatory: %i\n' % len(p.mandatoryparams))
170 170 > ui.write(' advisory: %i\n' % len(p.advisoryparams))
171 171 > ui.write(' payload: %i bytes\n' % len(p.read()))
172 172 > ui.write('parts count: %i\n' % count)
173 173 > EOF
174 174 $ cat >> $HGRCPATH << EOF
175 175 > [extensions]
176 176 > bundle2=$TESTTMP/bundle2.py
177 177 > [experimental]
178 178 > bundle2-exp=True
179 179 > [ui]
180 180 > ssh=python "$TESTDIR/dummyssh"
181 181 > [web]
182 182 > push_ssl = false
183 183 > allow_push = *
184 184 > EOF
185 185
186 186 The extension requires a repo (currently unused)
187 187
188 188 $ hg init main
189 189 $ cd main
190 190 $ touch a
191 191 $ hg add a
192 192 $ hg commit -m 'a'
193 193
194 194
195 195 Empty bundle
196 196 =================
197 197
198 198 - no option
199 199 - no parts
200 200
201 201 Test bundling
202 202
203 203 $ hg bundle2
204 204 HG2X\x00\x00\x00\x00 (no-eol) (esc)
205 205
206 206 Test unbundling
207 207
208 208 $ hg bundle2 | hg statbundle2
209 209 options count: 0
210 210 parts count: 0
211 211
212 212 Test old style bundle are detected and refused
213 213
214 214 $ hg bundle --all ../bundle.hg
215 215 1 changesets found
216 216 $ hg statbundle2 < ../bundle.hg
217 217 abort: unknown bundle version 10
218 218 [255]
219 219
220 220 Test parameters
221 221 =================
222 222
223 223 - some options
224 224 - no parts
225 225
226 226 advisory parameters, no value
227 227 -------------------------------
228 228
229 229 Simplest possible parameters form
230 230
231 231 Test generation simple option
232 232
233 233 $ hg bundle2 --param 'caution'
234 234 HG2X\x00\x07caution\x00\x00 (no-eol) (esc)
235 235
236 236 Test unbundling
237 237
238 238 $ hg bundle2 --param 'caution' | hg statbundle2
239 239 options count: 1
240 240 - caution
241 241 parts count: 0
242 242
243 243 Test generation multiple option
244 244
245 245 $ hg bundle2 --param 'caution' --param 'meal'
246 246 HG2X\x00\x0ccaution meal\x00\x00 (no-eol) (esc)
247 247
248 248 Test unbundling
249 249
250 250 $ hg bundle2 --param 'caution' --param 'meal' | hg statbundle2
251 251 options count: 2
252 252 - caution
253 253 - meal
254 254 parts count: 0
255 255
256 256 advisory parameters, with value
257 257 -------------------------------
258 258
259 259 Test generation
260 260
261 261 $ hg bundle2 --param 'caution' --param 'meal=vegan' --param 'elephants'
262 262 HG2X\x00\x1ccaution meal=vegan elephants\x00\x00 (no-eol) (esc)
263 263
264 264 Test unbundling
265 265
266 266 $ hg bundle2 --param 'caution' --param 'meal=vegan' --param 'elephants' | hg statbundle2
267 267 options count: 3
268 268 - caution
269 269 - elephants
270 270 - meal
271 271 vegan
272 272 parts count: 0
273 273
274 274 parameter with special char in value
275 275 ---------------------------------------------------
276 276
277 277 Test generation
278 278
279 279 $ hg bundle2 --param 'e|! 7/=babar%#==tutu' --param simple
280 280 HG2X\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00 (no-eol) (esc)
281 281
282 282 Test unbundling
283 283
284 284 $ hg bundle2 --param 'e|! 7/=babar%#==tutu' --param simple | hg statbundle2
285 285 options count: 2
286 286 - e|! 7/
287 287 babar%#==tutu
288 288 - simple
289 289 parts count: 0
290 290
291 291 Test unknown mandatory option
292 292 ---------------------------------------------------
293 293
294 294 $ hg bundle2 --param 'Gravity' | hg statbundle2
295 295 abort: unknown parameters: 'Gravity'
296 296 [255]
297 297
298 298 Test debug output
299 299 ---------------------------------------------------
300 300
301 301 bundling debug
302 302
303 303 $ hg bundle2 --debug --param 'e|! 7/=babar%#==tutu' --param simple ../out.hg2
304 304 start emission of HG2X stream
305 305 bundle parameter: e%7C%21%207/=babar%25%23%3D%3Dtutu simple
306 306 start of parts
307 307 end of bundle
308 308
309 309 file content is ok
310 310
311 311 $ cat ../out.hg2
312 312 HG2X\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00 (no-eol) (esc)
313 313
314 314 unbundling debug
315 315
316 316 $ hg statbundle2 --debug < ../out.hg2
317 317 start processing of HG2X stream
318 318 reading bundle2 stream parameters
319 319 ignoring unknown parameter 'e|! 7/'
320 320 ignoring unknown parameter 'simple'
321 321 options count: 2
322 322 - e|! 7/
323 323 babar%#==tutu
324 324 - simple
325 325 start extraction of bundle2 parts
326 326 part header size: 0
327 327 end of bundle2 stream
328 328 parts count: 0
329 329
330 330
331 331 Test buggy input
332 332 ---------------------------------------------------
333 333
334 334 empty parameter name
335 335
336 336 $ hg bundle2 --param '' --quiet
337 337 abort: empty parameter name
338 338 [255]
339 339
340 340 bad parameter name
341 341
342 342 $ hg bundle2 --param 42babar
343 343 abort: non letter first character: '42babar'
344 344 [255]
345 345
346 346
347 347 Test part
348 348 =================
349 349
350 350 $ hg bundle2 --parts ../parts.hg2 --debug
351 351 start emission of HG2X stream
352 352 bundle parameter:
353 353 start of parts
354 354 bundle part: "test:empty"
355 355 bundle part: "test:empty"
356 356 bundle part: "test:song"
357 357 bundle part: "test:debugreply"
358 358 bundle part: "test:math"
359 359 bundle part: "test:ping"
360 360 end of bundle
361 361
362 362 $ cat ../parts.hg2
363 363 HG2X\x00\x00\x00\x11 (esc)
364 364 test:empty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11 (esc)
365 365 test:empty\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x10 test:song\x00\x00\x00\x02\x00\x00\x00\x00\x00\xb2Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko (esc)
366 366 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
367 367 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.\x00\x00\x00\x00\x00\x16\x0ftest:debugreply\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00+ test:math\x00\x00\x00\x04\x02\x01\x02\x04\x01\x04\x07\x03pi3.14e2.72cookingraw\x00\x00\x00\x0242\x00\x00\x00\x00\x00\x10 test:ping\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
368 368
369 369
370 370 $ hg statbundle2 < ../parts.hg2
371 371 options count: 0
372 372 :test:empty:
373 373 mandatory: 0
374 374 advisory: 0
375 375 payload: 0 bytes
376 376 :test:empty:
377 377 mandatory: 0
378 378 advisory: 0
379 379 payload: 0 bytes
380 380 :test:song:
381 381 mandatory: 0
382 382 advisory: 0
383 383 payload: 178 bytes
384 384 :test:debugreply:
385 385 mandatory: 0
386 386 advisory: 0
387 387 payload: 0 bytes
388 388 :test:math:
389 389 mandatory: 2
390 390 advisory: 1
391 391 payload: 2 bytes
392 392 :test:ping:
393 393 mandatory: 0
394 394 advisory: 0
395 395 payload: 0 bytes
396 396 parts count: 6
397 397
398 398 $ hg statbundle2 --debug < ../parts.hg2
399 399 start processing of HG2X stream
400 400 reading bundle2 stream parameters
401 401 options count: 0
402 402 start extraction of bundle2 parts
403 403 part header size: 17
404 404 part type: "test:empty"
405 405 part id: "0"
406 406 part parameters: 0
407 407 :test:empty:
408 408 mandatory: 0
409 409 advisory: 0
410 410 payload chunk size: 0
411 411 payload: 0 bytes
412 412 part header size: 17
413 413 part type: "test:empty"
414 414 part id: "1"
415 415 part parameters: 0
416 416 :test:empty:
417 417 mandatory: 0
418 418 advisory: 0
419 419 payload chunk size: 0
420 420 payload: 0 bytes
421 421 part header size: 16
422 422 part type: "test:song"
423 423 part id: "2"
424 424 part parameters: 0
425 425 :test:song:
426 426 mandatory: 0
427 427 advisory: 0
428 428 payload chunk size: 178
429 429 payload chunk size: 0
430 430 payload: 178 bytes
431 431 part header size: 22
432 432 part type: "test:debugreply"
433 433 part id: "3"
434 434 part parameters: 0
435 435 :test:debugreply:
436 436 mandatory: 0
437 437 advisory: 0
438 438 payload chunk size: 0
439 439 payload: 0 bytes
440 440 part header size: 43
441 441 part type: "test:math"
442 442 part id: "4"
443 443 part parameters: 3
444 444 :test:math:
445 445 mandatory: 2
446 446 advisory: 1
447 447 payload chunk size: 2
448 448 payload chunk size: 0
449 449 payload: 2 bytes
450 450 part header size: 16
451 451 part type: "test:ping"
452 452 part id: "5"
453 453 part parameters: 0
454 454 :test:ping:
455 455 mandatory: 0
456 456 advisory: 0
457 457 payload chunk size: 0
458 458 payload: 0 bytes
459 459 part header size: 0
460 460 end of bundle2 stream
461 461 parts count: 6
462 462
463 463 Test actual unbundling of test part
464 464 =======================================
465 465
466 466 Process the bundle
467 467
468 468 $ hg unbundle2 --debug < ../parts.hg2
469 469 start processing of HG2X stream
470 470 reading bundle2 stream parameters
471 471 start extraction of bundle2 parts
472 472 part header size: 17
473 473 part type: "test:empty"
474 474 part id: "0"
475 475 part parameters: 0
476 476 ignoring unknown advisory part 'test:empty'
477 477 payload chunk size: 0
478 478 part header size: 17
479 479 part type: "test:empty"
480 480 part id: "1"
481 481 part parameters: 0
482 482 ignoring unknown advisory part 'test:empty'
483 483 payload chunk size: 0
484 484 part header size: 16
485 485 part type: "test:song"
486 486 part id: "2"
487 487 part parameters: 0
488 488 found a handler for part 'test:song'
489 489 The choir starts singing:
490 490 payload chunk size: 178
491 491 payload chunk size: 0
492 492 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
493 493 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
494 494 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
495 495 part header size: 22
496 496 part type: "test:debugreply"
497 497 part id: "3"
498 498 part parameters: 0
499 499 found a handler for part 'test:debugreply'
500 500 debugreply: no reply
501 501 payload chunk size: 0
502 502 part header size: 43
503 503 part type: "test:math"
504 504 part id: "4"
505 505 part parameters: 3
506 506 ignoring unknown advisory part 'test:math'
507 507 payload chunk size: 2
508 508 payload chunk size: 0
509 509 part header size: 16
510 510 part type: "test:ping"
511 511 part id: "5"
512 512 part parameters: 0
513 513 found a handler for part 'test:ping'
514 514 received ping request (id 5)
515 515 payload chunk size: 0
516 516 part header size: 0
517 517 end of bundle2 stream
518 518 0 unread bytes
519 519 3 total verses sung
520 520
521 521 Unbundle with an unknown mandatory part
522 522 (should abort)
523 523
524 524 $ hg bundle2 --parts --unknown ../unknown.hg2
525 525
526 526 $ hg unbundle2 < ../unknown.hg2
527 527 The choir starts singing:
528 528 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
529 529 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
530 530 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
531 531 debugreply: no reply
532 532 0 unread bytes
533 533 abort: missing support for 'test:unknown'
534 534 [255]
535 535
536 536 unbundle with a reply
537 537
538 538 $ hg bundle2 --parts --reply ../parts-reply.hg2
539 539 $ hg unbundle2 ../reply.hg2 < ../parts-reply.hg2
540 540 0 unread bytes
541 541 3 total verses sung
542 542
543 543 The reply is a bundle
544 544
545 545 $ cat ../reply.hg2
546 546 HG2X\x00\x00\x00\x1f (esc)
547 547 b2x:output\x00\x00\x00\x00\x00\x01\x0b\x01in-reply-to3\x00\x00\x00\xd9The choir starts singing: (esc)
548 548 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
549 549 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
550 550 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
551 551 \x00\x00\x00\x00\x00\x1f (esc)
552 552 b2x:output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to4\x00\x00\x00\xc9debugreply: capabilities: (esc)
553 553 debugreply: 'city=!'
554 554 debugreply: 'celeste,ville'
555 555 debugreply: 'elephants'
556 556 debugreply: 'babar'
557 557 debugreply: 'celeste'
558 558 debugreply: 'ping-pong'
559 559 \x00\x00\x00\x00\x00\x1e test:pong\x00\x00\x00\x02\x01\x00\x0b\x01in-reply-to6\x00\x00\x00\x00\x00\x1f (esc)
560 560 b2x:output\x00\x00\x00\x03\x00\x01\x0b\x01in-reply-to6\x00\x00\x00=received ping request (id 6) (esc)
561 561 replying to ping request (id 6)
562 562 \x00\x00\x00\x00\x00\x00 (no-eol) (esc)
563 563
564 564 The reply is valid
565 565
566 566 $ hg statbundle2 < ../reply.hg2
567 567 options count: 0
568 568 :b2x:output:
569 569 mandatory: 0
570 570 advisory: 1
571 571 payload: 217 bytes
572 572 :b2x:output:
573 573 mandatory: 0
574 574 advisory: 1
575 575 payload: 201 bytes
576 576 :test:pong:
577 577 mandatory: 1
578 578 advisory: 0
579 579 payload: 0 bytes
580 580 :b2x:output:
581 581 mandatory: 0
582 582 advisory: 1
583 583 payload: 61 bytes
584 584 parts count: 4
585 585
586 586 Unbundle the reply to get the output:
587 587
588 588 $ hg unbundle2 < ../reply.hg2
589 589 remote: The choir starts singing:
590 590 remote: Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
591 591 remote: Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
592 592 remote: Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
593 593 remote: debugreply: capabilities:
594 594 remote: debugreply: 'city=!'
595 595 remote: debugreply: 'celeste,ville'
596 596 remote: debugreply: 'elephants'
597 597 remote: debugreply: 'babar'
598 598 remote: debugreply: 'celeste'
599 599 remote: debugreply: 'ping-pong'
600 600 remote: received ping request (id 6)
601 601 remote: replying to ping request (id 6)
602 602 0 unread bytes
603 603
604 604 Support for changegroup
605 605 ===================================
606 606
607 607 $ hg unbundle $TESTDIR/bundles/rebase.hg
608 608 adding changesets
609 609 adding manifests
610 610 adding file changes
611 611 added 8 changesets with 7 changes to 7 files (+3 heads)
612 612 (run 'hg heads' to see heads, 'hg merge' to merge)
613 613
614 614 $ hg log -G
615 615 o changeset: 8:02de42196ebe
616 616 | tag: tip
617 617 | parent: 6:24b6387c8c8c
618 618 | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
619 619 | date: Sat Apr 30 15:24:48 2011 +0200
620 620 | summary: H
621 621 |
622 622 | o changeset: 7:eea13746799a
623 623 |/| parent: 6:24b6387c8c8c
624 624 | | parent: 5:9520eea781bc
625 625 | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
626 626 | | date: Sat Apr 30 15:24:48 2011 +0200
627 627 | | summary: G
628 628 | |
629 629 o | changeset: 6:24b6387c8c8c
630 630 | | parent: 1:cd010b8cd998
631 631 | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
632 632 | | date: Sat Apr 30 15:24:48 2011 +0200
633 633 | | summary: F
634 634 | |
635 635 | o changeset: 5:9520eea781bc
636 636 |/ parent: 1:cd010b8cd998
637 637 | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
638 638 | date: Sat Apr 30 15:24:48 2011 +0200
639 639 | summary: E
640 640 |
641 641 | o changeset: 4:32af7686d403
642 642 | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
643 643 | | date: Sat Apr 30 15:24:48 2011 +0200
644 644 | | summary: D
645 645 | |
646 646 | o changeset: 3:5fddd98957c8
647 647 | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
648 648 | | date: Sat Apr 30 15:24:48 2011 +0200
649 649 | | summary: C
650 650 | |
651 651 | o changeset: 2:42ccdea3bb16
652 652 |/ user: Nicolas Dumazet <nicdumz.commits@gmail.com>
653 653 | date: Sat Apr 30 15:24:48 2011 +0200
654 654 | summary: B
655 655 |
656 656 o changeset: 1:cd010b8cd998
657 657 parent: -1:000000000000
658 658 user: Nicolas Dumazet <nicdumz.commits@gmail.com>
659 659 date: Sat Apr 30 15:24:48 2011 +0200
660 660 summary: A
661 661
662 662 @ changeset: 0:3903775176ed
663 663 user: test
664 664 date: Thu Jan 01 00:00:00 1970 +0000
665 665 summary: a
666 666
667 667
668 668 $ hg bundle2 --debug --rev '8+7+5+4' ../rev.hg2
669 669 4 changesets found
670 670 list of changesets:
671 671 32af7686d403cf45b5d95f2d70cebea587ac806a
672 672 9520eea781bcca16c1e15acc0ba14335a0e8e5ba
673 673 eea13746799a9e0bfd88f29d3c2e9dc9389f524f
674 674 02de42196ebee42ef284b6780a87cdc96e8eaab6
675 675 start emission of HG2X stream
676 676 bundle parameter:
677 677 start of parts
678 678 bundle part: "b2x:changegroup"
679 679 bundling: 1/4 changesets (25.00%)
680 680 bundling: 2/4 changesets (50.00%)
681 681 bundling: 3/4 changesets (75.00%)
682 682 bundling: 4/4 changesets (100.00%)
683 683 bundling: 1/4 manifests (25.00%)
684 684 bundling: 2/4 manifests (50.00%)
685 685 bundling: 3/4 manifests (75.00%)
686 686 bundling: 4/4 manifests (100.00%)
687 687 bundling: D 1/3 files (33.33%)
688 688 bundling: E 2/3 files (66.67%)
689 689 bundling: H 3/3 files (100.00%)
690 690 end of bundle
691 691
692 692 $ cat ../rev.hg2
693 693 HG2X\x00\x00\x00\x16\x0fb2x:changegroup\x00\x00\x00\x00\x00\x00\x00\x00\x06\x13\x00\x00\x00\xa42\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j_\xdd\xd9\x89W\xc8\xa5JMCm\xfe\x1d\xa9\xd8\x7f!\xa1\xb9{\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)6e1f4c47ecb533ffd0c8e52cdc88afb6cd39e20c (esc)
694 694 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02D (esc)
695 695 \x00\x00\x00i\x00\x00\x00j\x00\x00\x00\x01D\x00\x00\x00\xa4\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\xcd\x01\x0b\x8c\xd9\x98\xf3\x98\x1aZ\x81\x15\xf9O\x8d\xa4\xabP`\x89\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)4dece9c826f69490507b98c6383a3009b295837d (esc)
696 696 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02E (esc)
697 697 \x00\x00\x00i\x00\x00\x00j\x00\x00\x00\x01E\x00\x00\x00\xa2\xee\xa17Fy\x9a\x9e\x0b\xfd\x88\xf2\x9d<.\x9d\xc98\x9fRO$\xb68|\x8c\x8c\xae7\x17\x88\x80\xf3\xfa\x95\xde\xd3\xcb\x1c\xf7\x85\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\xee\xa17Fy\x9a\x9e\x0b\xfd\x88\xf2\x9d<.\x9d\xc98\x9fRO\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)365b93d57fdf4814e2b5911d6bacff2b12014441 (esc)
698 698 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00i\x00\x00\x00j\x00\x00\x00\x01G\x00\x00\x00\xa4\x02\xdeB\x19n\xbe\xe4.\xf2\x84\xb6x (esc)
699 699 \x87\xcd\xc9n\x8e\xaa\xb6$\xb68|\x8c\x8c\xae7\x17\x88\x80\xf3\xfa\x95\xde\xd3\xcb\x1c\xf7\x85\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xdeB\x19n\xbe\xe4.\xf2\x84\xb6x (esc)
700 700 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)8bee48edc7318541fc0013ee41b089276a8c24bf (esc)
701 701 \x00\x00\x00f\x00\x00\x00f\x00\x00\x00\x02H (esc)
702 702 \x00\x00\x00g\x00\x00\x00h\x00\x00\x00\x01H\x00\x00\x00\x00\x00\x00\x00\x8bn\x1fLG\xec\xb53\xff\xd0\xc8\xe5,\xdc\x88\xaf\xb6\xcd9\xe2\x0cf\xa5\xa0\x18\x17\xfd\xf5#\x9c'8\x02\xb5\xb7a\x8d\x05\x1c\x89\xe4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j\x00\x00\x00\x81\x00\x00\x00\x81\x00\x00\x00+D\x00c3f1ca2924c16a19b0656a84900e504e5b0aec2d (esc)
703 703 \x00\x00\x00\x8bM\xec\xe9\xc8&\xf6\x94\x90P{\x98\xc68:0 \xb2\x95\x83}\x00}\x8c\x9d\x88\x84\x13%\xf5\xc6\xb0cq\xb3[N\x8a+\x1a\x83\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\x00\x00\x00+\x00\x00\x00\xac\x00\x00\x00+E\x009c6fd0350a6c0d0c49d4a9c5017cf07043f54e58 (esc)
704 704 \x00\x00\x00\x8b6[\x93\xd5\x7f\xdfH\x14\xe2\xb5\x91\x1dk\xac\xff+\x12\x01DA(\xa5\x84\xc6^\xf1!\xf8\x9e\xb6j\xb7\xd0\xbc\x15=\x80\x99\xe7\xceM\xec\xe9\xc8&\xf6\x94\x90P{\x98\xc68:0 \xb2\x95\x83}\xee\xa17Fy\x9a\x9e\x0b\xfd\x88\xf2\x9d<.\x9d\xc98\x9fRO\x00\x00\x00V\x00\x00\x00V\x00\x00\x00+F\x0022bfcfd62a21a3287edbd4d656218d0f525ed76a (esc)
705 705 \x00\x00\x00\x97\x8b\xeeH\xed\xc71\x85A\xfc\x00\x13\xeeA\xb0\x89'j\x8c$\xbf(\xa5\x84\xc6^\xf1!\xf8\x9e\xb6j\xb7\xd0\xbc\x15=\x80\x99\xe7\xce\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xdeB\x19n\xbe\xe4.\xf2\x84\xb6x (esc)
706 706 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00+\x00\x00\x00V\x00\x00\x00\x00\x00\x00\x00\x81\x00\x00\x00\x81\x00\x00\x00+H\x008500189e74a9e0475e822093bc7db0d631aeb0b4 (esc)
707 707 \x00\x00\x00\x00\x00\x00\x00\x05D\x00\x00\x00b\xc3\xf1\xca)$\xc1j\x19\xb0ej\x84\x90\x0ePN[ (esc)
708 708 \xec-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02D (esc)
709 709 \x00\x00\x00\x00\x00\x00\x00\x05E\x00\x00\x00b\x9co\xd05 (esc)
710 710 l\r (no-eol) (esc)
711 711 \x0cI\xd4\xa9\xc5\x01|\xf0pC\xf5NX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02E (esc)
712 712 \x00\x00\x00\x00\x00\x00\x00\x05H\x00\x00\x00b\x85\x00\x18\x9et\xa9\xe0G^\x82 \x93\xbc}\xb0\xd61\xae\xb0\xb4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xdeB\x19n\xbe\xe4.\xf2\x84\xb6x (esc)
713 713 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02H (esc)
714 714 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
715 715
716 716 $ hg unbundle2 < ../rev.hg2
717 717 adding changesets
718 718 adding manifests
719 719 adding file changes
720 720 added 0 changesets with 0 changes to 3 files
721 721 0 unread bytes
722 722 addchangegroup return: 1
723 723
724 724 with reply
725 725
726 726 $ hg bundle2 --rev '8+7+5+4' --reply ../rev-rr.hg2
727 727 $ hg unbundle2 ../rev-reply.hg2 < ../rev-rr.hg2
728 728 0 unread bytes
729 729 addchangegroup return: 1
730 730
731 731 $ cat ../rev-reply.hg2
732 732 HG2X\x00\x00\x003\x15b2x:reply:changegroup\x00\x00\x00\x00\x00\x02\x0b\x01\x06\x01in-reply-to1return1\x00\x00\x00\x00\x00\x1f (esc)
733 733 b2x:output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to1\x00\x00\x00dadding changesets (esc)
734 734 adding manifests
735 735 adding file changes
736 736 added 0 changesets with 0 changes to 3 files
737 737 \x00\x00\x00\x00\x00\x00 (no-eol) (esc)
738 738
739 739 Real world exchange
740 740 =====================
741 741
742 742
743 743 clone --pull
744 744
745 745 $ cd ..
746 746 $ hg clone main other --pull --rev 9520eea781bc
747 747 adding changesets
748 748 adding manifests
749 749 adding file changes
750 750 added 2 changesets with 2 changes to 2 files
751 751 updating to branch default
752 752 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
753 753 $ hg -R other log -G
754 754 @ changeset: 1:9520eea781bc
755 755 | tag: tip
756 756 | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
757 757 | date: Sat Apr 30 15:24:48 2011 +0200
758 758 | summary: E
759 759 |
760 760 o changeset: 0:cd010b8cd998
761 761 user: Nicolas Dumazet <nicdumz.commits@gmail.com>
762 762 date: Sat Apr 30 15:24:48 2011 +0200
763 763 summary: A
764 764
765 765
766 766 pull
767 767
768 768 $ hg -R other pull -r 24b6387c8c8c
769 769 pulling from $TESTTMP/main (glob)
770 770 searching for changes
771 771 adding changesets
772 772 adding manifests
773 773 adding file changes
774 774 added 1 changesets with 1 changes to 1 files (+1 heads)
775 775 (run 'hg heads' to see heads, 'hg merge' to merge)
776 776
777 777 push
778 778
779 779 $ hg -R main push other --rev eea13746799a
780 780 pushing to other
781 781 searching for changes
782 782 remote: adding changesets
783 783 remote: adding manifests
784 784 remote: adding file changes
785 785 remote: added 1 changesets with 0 changes to 0 files (-1 heads)
786 786
787 787 pull over ssh
788 788
789 789 $ hg -R other pull ssh://user@dummy/main -r 02de42196ebe --traceback
790 790 pulling from ssh://user@dummy/main
791 791 searching for changes
792 792 adding changesets
793 793 adding manifests
794 794 adding file changes
795 795 added 1 changesets with 1 changes to 1 files (+1 heads)
796 796 (run 'hg heads' to see heads, 'hg merge' to merge)
797 797
798 798 pull over http
799 799
800 800 $ hg -R main serve -p $HGPORT -d --pid-file=main.pid -E main-error.log
801 801 $ cat main.pid >> $DAEMON_PIDS
802 802
803 803 $ hg -R other pull http://localhost:$HGPORT/ -r 42ccdea3bb16
804 804 pulling from http://localhost:$HGPORT/
805 805 searching for changes
806 806 adding changesets
807 807 adding manifests
808 808 adding file changes
809 809 added 1 changesets with 1 changes to 1 files (+1 heads)
810 810 (run 'hg heads .' to see heads, 'hg merge' to merge)
811 811 $ cat main-error.log
812 812
813 813 push over ssh
814 814
815 815 $ hg -R main push ssh://user@dummy/other -r 5fddd98957c8
816 816 pushing to ssh://user@dummy/other
817 817 searching for changes
818 818 remote: adding changesets
819 819 remote: adding manifests
820 820 remote: adding file changes
821 821 remote: added 1 changesets with 1 changes to 1 files
822 822
823 823 push over http
824 824
825 825 $ hg -R other serve -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
826 826 $ cat other.pid >> $DAEMON_PIDS
827 827
828 828 $ hg -R main push http://localhost:$HGPORT2/ -r 32af7686d403
829 829 pushing to http://localhost:$HGPORT2/
830 830 searching for changes
831 831 remote: adding changesets
832 832 remote: adding manifests
833 833 remote: adding file changes
834 834 remote: added 1 changesets with 1 changes to 1 files
835 835 $ cat other-error.log
836 836
837 837 Check final content.
838 838
839 839 $ hg -R other log -G
840 840 o changeset: 7:32af7686d403
841 841 | tag: tip
842 842 | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
843 843 | date: Sat Apr 30 15:24:48 2011 +0200
844 844 | summary: D
845 845 |
846 846 o changeset: 6:5fddd98957c8
847 847 | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
848 848 | date: Sat Apr 30 15:24:48 2011 +0200
849 849 | summary: C
850 850 |
851 851 o changeset: 5:42ccdea3bb16
852 852 | parent: 0:cd010b8cd998
853 853 | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
854 854 | date: Sat Apr 30 15:24:48 2011 +0200
855 855 | summary: B
856 856 |
857 857 | o changeset: 4:02de42196ebe
858 858 | | parent: 2:24b6387c8c8c
859 859 | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
860 860 | | date: Sat Apr 30 15:24:48 2011 +0200
861 861 | | summary: H
862 862 | |
863 863 | | o changeset: 3:eea13746799a
864 864 | |/| parent: 2:24b6387c8c8c
865 865 | | | parent: 1:9520eea781bc
866 866 | | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
867 867 | | | date: Sat Apr 30 15:24:48 2011 +0200
868 868 | | | summary: G
869 869 | | |
870 870 | o | changeset: 2:24b6387c8c8c
871 871 |/ / parent: 0:cd010b8cd998
872 872 | | user: Nicolas Dumazet <nicdumz.commits@gmail.com>
873 873 | | date: Sat Apr 30 15:24:48 2011 +0200
874 874 | | summary: F
875 875 | |
876 876 | @ changeset: 1:9520eea781bc
877 877 |/ user: Nicolas Dumazet <nicdumz.commits@gmail.com>
878 878 | date: Sat Apr 30 15:24:48 2011 +0200
879 879 | summary: E
880 880 |
881 881 o changeset: 0:cd010b8cd998
882 882 user: Nicolas Dumazet <nicdumz.commits@gmail.com>
883 883 date: Sat Apr 30 15:24:48 2011 +0200
884 884 summary: A
885 885
886 886
887 887 Error Handling
888 888 ==============
889 889
890 890 Check that errors are properly returned to the client during push.
891 891
892 892 Setting up
893 893
894 894 $ cat > failpush.py << EOF
895 895 > """A small extension that makes push fails when using bundle2
896 896 >
897 897 > used to test error handling in bundle2
898 898 > """
899 899 >
900 900 > from mercurial import util
901 901 > from mercurial import bundle2
902 902 > from mercurial import exchange
903 903 > from mercurial import extensions
904 904 >
905 905 > def _pushbundle2failpart(orig, pushop, bundler):
906 906 > extradata = orig(pushop, bundler)
907 907 > reason = pushop.ui.config('failpush', 'reason', None)
908 908 > part = None
909 909 > if reason == 'abort':
910 910 > part = bundle2.bundlepart('test:abort')
911 > if reason == 'unknown':
912 > part = bundle2.bundlepart('TEST:UNKNOWN')
911 913 > if part is not None:
912 914 > bundler.addpart(part)
913 915 > return extradata
914 916 >
915 917 > @bundle2.parthandler("test:abort")
916 918 > def handleabort(op, part):
917 919 > raise util.Abort('Abandon ship!', hint="don't panic")
918 920 >
919 921 > def uisetup(ui):
920 922 > extensions.wrapfunction(exchange, '_pushbundle2extraparts', _pushbundle2failpart)
921 923 >
922 924 > EOF
923 925
924 926 $ cd main
925 927 $ hg up tip
926 928 3 files updated, 0 files merged, 1 files removed, 0 files unresolved
927 929 $ echo 'I' > I
928 930 $ hg add I
929 931 $ hg ci -m 'I'
930 932 $ hg id
931 933 e7ec4e813ba6 tip
932 934 $ cd ..
933 935
934 936 $ cat << EOF >> $HGRCPATH
935 937 > [extensions]
936 938 > failpush=$TESTTMP/failpush.py
937 939 > EOF
938 940
939 941 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
940 942 $ hg -R other serve -p $HGPORT2 -d --pid-file=other.pid -E other-error.log
941 943 $ cat other.pid >> $DAEMON_PIDS
942 944
943 945 Doing the actual push: Abort error
944 946
945 947 $ cat << EOF >> $HGRCPATH
946 948 > [failpush]
947 949 > reason = abort
948 950 > EOF
949 951
950 952 $ hg -R main push other -r e7ec4e813ba6
951 953 pushing to other
952 954 searching for changes
953 955 abort: Abandon ship!
954 956 (don't panic)
955 957 [255]
956 958
957 959 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
958 960 pushing to ssh://user@dummy/other
959 961 searching for changes
960 962 abort: Abandon ship!
961 963 (don't panic)
962 964 [255]
963 965
964 966 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
965 967 pushing to http://localhost:$HGPORT2/
966 968 searching for changes
967 969 abort: Abandon ship!
968 970 (don't panic)
969 971 [255]
970 972
971 973
974 Doing the actual push: unknown mandatory parts
975
976 $ cat << EOF >> $HGRCPATH
977 > [failpush]
978 > reason = unknown
979 > EOF
980
981 $ hg -R main push other -r e7ec4e813ba6
982 pushing to other
983 searching for changes
984 abort: missing support for 'test:unknown'
985 [255]
986
987 $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6
988 pushing to ssh://user@dummy/other
989 searching for changes
990 abort: missing support for "'test:unknown'"
991 [255]
992
993 $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6
994 pushing to http://localhost:$HGPORT2/
995 searching for changes
996 abort: missing support for "'test:unknown'"
997 [255]
General Comments 0
You need to be logged in to leave comments. Login now