##// END OF EJS Templates
bundle2: add generic debug output at the end of bundle processing...
Pierre-Yves David -
r25332:0cdd1c81 default
parent child Browse files
Show More
@@ -1,1328 +1,1332
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: int32
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: int32
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 (restricted to [a-zA-Z0-9_:-]*)
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 Each parameter's key MUST be unique within the part.
117 117
118 118 :payload:
119 119
120 120 payload is a series of `<chunksize><chunkdata>`.
121 121
122 122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124 124
125 125 The current implementation always produces either zero or one chunk.
126 126 This is an implementation limitation that will ultimately be lifted.
127 127
128 128 `chunksize` can be negative to trigger special case processing. No such
129 129 processing is in place yet.
130 130
131 131 Bundle processing
132 132 ============================
133 133
134 134 Each part is processed in order using a "part handler". Handler are registered
135 135 for a certain part type.
136 136
137 137 The matching of a part to its handler is case insensitive. The case of the
138 138 part type is used to know if a part is mandatory or advisory. If the Part type
139 139 contains any uppercase char it is considered mandatory. When no handler is
140 140 known for a Mandatory part, the process is aborted and an exception is raised.
141 141 If the part is advisory and no handler is known, the part is ignored. When the
142 142 process is aborted, the full bundle is still read from the stream to keep the
143 143 channel usable. But none of the part read from an abort are processed. In the
144 144 future, dropping the stream may become an option for channel we do not care to
145 145 preserve.
146 146 """
147 147
148 148 import errno
149 149 import sys
150 150 import util
151 151 import struct
152 152 import urllib
153 153 import string
154 154 import obsolete
155 155 import pushkey
156 156 import url
157 157 import re
158 158
159 159 import changegroup, error
160 160 from i18n import _
161 161
162 162 _pack = struct.pack
163 163 _unpack = struct.unpack
164 164
165 165 _fstreamparamsize = '>i'
166 166 _fpartheadersize = '>i'
167 167 _fparttypesize = '>B'
168 168 _fpartid = '>I'
169 169 _fpayloadsize = '>i'
170 170 _fpartparamcount = '>BB'
171 171
172 172 preferedchunksize = 4096
173 173
174 174 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
175 175
176 176 def outdebug(ui, message):
177 177 """debug regarding output stream (bundling)"""
178 178 ui.debug('bundle2-output: %s\n' % message)
179 179
180 180 def indebug(ui, message):
181 181 """debug on input stream (unbundling)"""
182 182 ui.debug('bundle2-input: %s\n' % message)
183 183
184 184 def validateparttype(parttype):
185 185 """raise ValueError if a parttype contains invalid character"""
186 186 if _parttypeforbidden.search(parttype):
187 187 raise ValueError(parttype)
188 188
189 189 def _makefpartparamsizes(nbparams):
190 190 """return a struct format to read part parameter sizes
191 191
192 192 The number parameters is variable so we need to build that format
193 193 dynamically.
194 194 """
195 195 return '>'+('BB'*nbparams)
196 196
197 197 parthandlermapping = {}
198 198
199 199 def parthandler(parttype, params=()):
200 200 """decorator that register a function as a bundle2 part handler
201 201
202 202 eg::
203 203
204 204 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
205 205 def myparttypehandler(...):
206 206 '''process a part of type "my part".'''
207 207 ...
208 208 """
209 209 validateparttype(parttype)
210 210 def _decorator(func):
211 211 lparttype = parttype.lower() # enforce lower case matching.
212 212 assert lparttype not in parthandlermapping
213 213 parthandlermapping[lparttype] = func
214 214 func.params = frozenset(params)
215 215 return func
216 216 return _decorator
217 217
218 218 class unbundlerecords(object):
219 219 """keep record of what happens during and unbundle
220 220
221 221 New records are added using `records.add('cat', obj)`. Where 'cat' is a
222 222 category of record and obj is an arbitrary object.
223 223
224 224 `records['cat']` will return all entries of this category 'cat'.
225 225
226 226 Iterating on the object itself will yield `('category', obj)` tuples
227 227 for all entries.
228 228
229 229 All iterations happens in chronological order.
230 230 """
231 231
232 232 def __init__(self):
233 233 self._categories = {}
234 234 self._sequences = []
235 235 self._replies = {}
236 236
237 237 def add(self, category, entry, inreplyto=None):
238 238 """add a new record of a given category.
239 239
240 240 The entry can then be retrieved in the list returned by
241 241 self['category']."""
242 242 self._categories.setdefault(category, []).append(entry)
243 243 self._sequences.append((category, entry))
244 244 if inreplyto is not None:
245 245 self.getreplies(inreplyto).add(category, entry)
246 246
247 247 def getreplies(self, partid):
248 248 """get the records that are replies to a specific part"""
249 249 return self._replies.setdefault(partid, unbundlerecords())
250 250
251 251 def __getitem__(self, cat):
252 252 return tuple(self._categories.get(cat, ()))
253 253
254 254 def __iter__(self):
255 255 return iter(self._sequences)
256 256
257 257 def __len__(self):
258 258 return len(self._sequences)
259 259
260 260 def __nonzero__(self):
261 261 return bool(self._sequences)
262 262
263 263 class bundleoperation(object):
264 264 """an object that represents a single bundling process
265 265
266 266 Its purpose is to carry unbundle-related objects and states.
267 267
268 268 A new object should be created at the beginning of each bundle processing.
269 269 The object is to be returned by the processing function.
270 270
271 271 The object has very little content now it will ultimately contain:
272 272 * an access to the repo the bundle is applied to,
273 273 * a ui object,
274 274 * a way to retrieve a transaction to add changes to the repo,
275 275 * a way to record the result of processing each part,
276 276 * a way to construct a bundle response when applicable.
277 277 """
278 278
279 279 def __init__(self, repo, transactiongetter, captureoutput=True):
280 280 self.repo = repo
281 281 self.ui = repo.ui
282 282 self.records = unbundlerecords()
283 283 self.gettransaction = transactiongetter
284 284 self.reply = None
285 285 self.captureoutput = captureoutput
286 286
287 287 class TransactionUnavailable(RuntimeError):
288 288 pass
289 289
290 290 def _notransaction():
291 291 """default method to get a transaction while processing a bundle
292 292
293 293 Raise an exception to highlight the fact that no transaction was expected
294 294 to be created"""
295 295 raise TransactionUnavailable()
296 296
297 297 def processbundle(repo, unbundler, transactiongetter=None, op=None):
298 298 """This function process a bundle, apply effect to/from a repo
299 299
300 300 It iterates over each part then searches for and uses the proper handling
301 301 code to process the part. Parts are processed in order.
302 302
303 303 This is very early version of this function that will be strongly reworked
304 304 before final usage.
305 305
306 306 Unknown Mandatory part will abort the process.
307 307
308 308 It is temporarily possible to provide a prebuilt bundleoperation to the
309 309 function. This is used to ensure output is properly propagated in case of
310 310 an error during the unbundling. This output capturing part will likely be
311 311 reworked and this ability will probably go away in the process.
312 312 """
313 313 if op is None:
314 314 if transactiongetter is None:
315 315 transactiongetter = _notransaction
316 316 op = bundleoperation(repo, transactiongetter)
317 317 # todo:
318 318 # - replace this is a init function soon.
319 319 # - exception catching
320 320 unbundler.params
321 321 if repo.ui.debugflag:
322 322 msg = ['bundle2-input-bundle:']
323 323 if unbundler.params:
324 324 msg.append(' %i params')
325 325 if op.gettransaction is None:
326 326 msg.append(' no-transaction')
327 327 else:
328 328 msg.append(' with-transaction')
329 329 msg.append('\n')
330 330 repo.ui.debug(''.join(msg))
331 iterparts = unbundler.iterparts()
331 iterparts = enumerate(unbundler.iterparts())
332 332 part = None
333 nbpart = 0
333 334 try:
334 for part in iterparts:
335 for nbpart, part in iterparts:
335 336 _processpart(op, part)
336 337 except BaseException, exc:
337 for part in iterparts:
338 for nbpart, part in iterparts:
338 339 # consume the bundle content
339 340 part.seek(0, 2)
340 341 # Small hack to let caller code distinguish exceptions from bundle2
341 342 # processing from processing the old format. This is mostly
342 343 # needed to handle different return codes to unbundle according to the
343 344 # type of bundle. We should probably clean up or drop this return code
344 345 # craziness in a future version.
345 346 exc.duringunbundle2 = True
346 347 salvaged = []
347 348 if op.reply is not None:
348 349 salvaged = op.reply.salvageoutput()
349 350 exc._bundle2salvagedoutput = salvaged
350 351 raise
352 finally:
353 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
354
351 355 return op
352 356
353 357 def _processpart(op, part):
354 358 """process a single part from a bundle
355 359
356 360 The part is guaranteed to have been fully consumed when the function exits
357 361 (even if an exception is raised)."""
358 362 try:
359 363 try:
360 364 handler = parthandlermapping.get(part.type)
361 365 if handler is None:
362 366 raise error.UnsupportedPartError(parttype=part.type)
363 367 indebug(op.ui, 'found a handler for part %r' % part.type)
364 368 unknownparams = part.mandatorykeys - handler.params
365 369 if unknownparams:
366 370 unknownparams = list(unknownparams)
367 371 unknownparams.sort()
368 372 raise error.UnsupportedPartError(parttype=part.type,
369 373 params=unknownparams)
370 374 except error.UnsupportedPartError, exc:
371 375 if part.mandatory: # mandatory parts
372 376 raise
373 377 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
374 378 return # skip to part processing
375 379
376 380 # handler is called outside the above try block so that we don't
377 381 # risk catching KeyErrors from anything other than the
378 382 # parthandlermapping lookup (any KeyError raised by handler()
379 383 # itself represents a defect of a different variety).
380 384 output = None
381 385 if op.captureoutput and op.reply is not None:
382 386 op.ui.pushbuffer(error=True, subproc=True)
383 387 output = ''
384 388 try:
385 389 handler(op, part)
386 390 finally:
387 391 if output is not None:
388 392 output = op.ui.popbuffer()
389 393 if output:
390 394 outpart = op.reply.newpart('output', data=output,
391 395 mandatory=False)
392 396 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
393 397 finally:
394 398 # consume the part content to not corrupt the stream.
395 399 part.seek(0, 2)
396 400
397 401
398 402 def decodecaps(blob):
399 403 """decode a bundle2 caps bytes blob into a dictionary
400 404
401 405 The blob is a list of capabilities (one per line)
402 406 Capabilities may have values using a line of the form::
403 407
404 408 capability=value1,value2,value3
405 409
406 410 The values are always a list."""
407 411 caps = {}
408 412 for line in blob.splitlines():
409 413 if not line:
410 414 continue
411 415 if '=' not in line:
412 416 key, vals = line, ()
413 417 else:
414 418 key, vals = line.split('=', 1)
415 419 vals = vals.split(',')
416 420 key = urllib.unquote(key)
417 421 vals = [urllib.unquote(v) for v in vals]
418 422 caps[key] = vals
419 423 return caps
420 424
421 425 def encodecaps(caps):
422 426 """encode a bundle2 caps dictionary into a bytes blob"""
423 427 chunks = []
424 428 for ca in sorted(caps):
425 429 vals = caps[ca]
426 430 ca = urllib.quote(ca)
427 431 vals = [urllib.quote(v) for v in vals]
428 432 if vals:
429 433 ca = "%s=%s" % (ca, ','.join(vals))
430 434 chunks.append(ca)
431 435 return '\n'.join(chunks)
432 436
433 437 class bundle20(object):
434 438 """represent an outgoing bundle2 container
435 439
436 440 Use the `addparam` method to add stream level parameter. and `newpart` to
437 441 populate it. Then call `getchunks` to retrieve all the binary chunks of
438 442 data that compose the bundle2 container."""
439 443
440 444 _magicstring = 'HG20'
441 445
442 446 def __init__(self, ui, capabilities=()):
443 447 self.ui = ui
444 448 self._params = []
445 449 self._parts = []
446 450 self.capabilities = dict(capabilities)
447 451
448 452 @property
449 453 def nbparts(self):
450 454 """total number of parts added to the bundler"""
451 455 return len(self._parts)
452 456
453 457 # methods used to defines the bundle2 content
454 458 def addparam(self, name, value=None):
455 459 """add a stream level parameter"""
456 460 if not name:
457 461 raise ValueError('empty parameter name')
458 462 if name[0] not in string.letters:
459 463 raise ValueError('non letter first character: %r' % name)
460 464 self._params.append((name, value))
461 465
462 466 def addpart(self, part):
463 467 """add a new part to the bundle2 container
464 468
465 469 Parts contains the actual applicative payload."""
466 470 assert part.id is None
467 471 part.id = len(self._parts) # very cheap counter
468 472 self._parts.append(part)
469 473
470 474 def newpart(self, typeid, *args, **kwargs):
471 475 """create a new part and add it to the containers
472 476
473 477 As the part is directly added to the containers. For now, this means
474 478 that any failure to properly initialize the part after calling
475 479 ``newpart`` should result in a failure of the whole bundling process.
476 480
477 481 You can still fall back to manually create and add if you need better
478 482 control."""
479 483 part = bundlepart(typeid, *args, **kwargs)
480 484 self.addpart(part)
481 485 return part
482 486
483 487 # methods used to generate the bundle2 stream
484 488 def getchunks(self):
485 489 if self.ui.debugflag:
486 490 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
487 491 if self._params:
488 492 msg.append(' (%i params)' % len(self._params))
489 493 msg.append(' %i parts total\n' % len(self._parts))
490 494 self.ui.debug(''.join(msg))
491 495 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
492 496 yield self._magicstring
493 497 param = self._paramchunk()
494 498 outdebug(self.ui, 'bundle parameter: %s' % param)
495 499 yield _pack(_fstreamparamsize, len(param))
496 500 if param:
497 501 yield param
498 502
499 503 outdebug(self.ui, 'start of parts')
500 504 for part in self._parts:
501 505 outdebug(self.ui, 'bundle part: "%s"' % part.type)
502 506 for chunk in part.getchunks(ui=self.ui):
503 507 yield chunk
504 508 outdebug(self.ui, 'end of bundle')
505 509 yield _pack(_fpartheadersize, 0)
506 510
507 511 def _paramchunk(self):
508 512 """return a encoded version of all stream parameters"""
509 513 blocks = []
510 514 for par, value in self._params:
511 515 par = urllib.quote(par)
512 516 if value is not None:
513 517 value = urllib.quote(value)
514 518 par = '%s=%s' % (par, value)
515 519 blocks.append(par)
516 520 return ' '.join(blocks)
517 521
518 522 def salvageoutput(self):
519 523 """return a list with a copy of all output parts in the bundle
520 524
521 525 This is meant to be used during error handling to make sure we preserve
522 526 server output"""
523 527 salvaged = []
524 528 for part in self._parts:
525 529 if part.type.startswith('output'):
526 530 salvaged.append(part.copy())
527 531 return salvaged
528 532
529 533
530 534 class unpackermixin(object):
531 535 """A mixin to extract bytes and struct data from a stream"""
532 536
533 537 def __init__(self, fp):
534 538 self._fp = fp
535 539 self._seekable = (util.safehasattr(fp, 'seek') and
536 540 util.safehasattr(fp, 'tell'))
537 541
538 542 def _unpack(self, format):
539 543 """unpack this struct format from the stream"""
540 544 data = self._readexact(struct.calcsize(format))
541 545 return _unpack(format, data)
542 546
543 547 def _readexact(self, size):
544 548 """read exactly <size> bytes from the stream"""
545 549 return changegroup.readexactly(self._fp, size)
546 550
547 551 def seek(self, offset, whence=0):
548 552 """move the underlying file pointer"""
549 553 if self._seekable:
550 554 return self._fp.seek(offset, whence)
551 555 else:
552 556 raise NotImplementedError(_('File pointer is not seekable'))
553 557
554 558 def tell(self):
555 559 """return the file offset, or None if file is not seekable"""
556 560 if self._seekable:
557 561 try:
558 562 return self._fp.tell()
559 563 except IOError, e:
560 564 if e.errno == errno.ESPIPE:
561 565 self._seekable = False
562 566 else:
563 567 raise
564 568 return None
565 569
566 570 def close(self):
567 571 """close underlying file"""
568 572 if util.safehasattr(self._fp, 'close'):
569 573 return self._fp.close()
570 574
571 575 def getunbundler(ui, fp, header=None):
572 576 """return a valid unbundler object for a given header"""
573 577 if header is None:
574 578 header = changegroup.readexactly(fp, 4)
575 579 magic, version = header[0:2], header[2:4]
576 580 if magic != 'HG':
577 581 raise util.Abort(_('not a Mercurial bundle'))
578 582 unbundlerclass = formatmap.get(version)
579 583 if unbundlerclass is None:
580 584 raise util.Abort(_('unknown bundle version %s') % version)
581 585 unbundler = unbundlerclass(ui, fp)
582 586 indebug(ui, 'start processing of %s stream' % header)
583 587 return unbundler
584 588
585 589 class unbundle20(unpackermixin):
586 590 """interpret a bundle2 stream
587 591
588 592 This class is fed with a binary stream and yields parts through its
589 593 `iterparts` methods."""
590 594
591 595 def __init__(self, ui, fp):
592 596 """If header is specified, we do not read it out of the stream."""
593 597 self.ui = ui
594 598 super(unbundle20, self).__init__(fp)
595 599
596 600 @util.propertycache
597 601 def params(self):
598 602 """dictionary of stream level parameters"""
599 603 indebug(self.ui, 'reading bundle2 stream parameters')
600 604 params = {}
601 605 paramssize = self._unpack(_fstreamparamsize)[0]
602 606 if paramssize < 0:
603 607 raise error.BundleValueError('negative bundle param size: %i'
604 608 % paramssize)
605 609 if paramssize:
606 610 for p in self._readexact(paramssize).split(' '):
607 611 p = p.split('=', 1)
608 612 p = [urllib.unquote(i) for i in p]
609 613 if len(p) < 2:
610 614 p.append(None)
611 615 self._processparam(*p)
612 616 params[p[0]] = p[1]
613 617 return params
614 618
615 619 def _processparam(self, name, value):
616 620 """process a parameter, applying its effect if needed
617 621
618 622 Parameter starting with a lower case letter are advisory and will be
619 623 ignored when unknown. Those starting with an upper case letter are
620 624 mandatory and will this function will raise a KeyError when unknown.
621 625
622 626 Note: no option are currently supported. Any input will be either
623 627 ignored or failing.
624 628 """
625 629 if not name:
626 630 raise ValueError('empty parameter name')
627 631 if name[0] not in string.letters:
628 632 raise ValueError('non letter first character: %r' % name)
629 633 # Some logic will be later added here to try to process the option for
630 634 # a dict of known parameter.
631 635 if name[0].islower():
632 636 indebug(self.ui, "ignoring unknown parameter %r" % name)
633 637 else:
634 638 raise error.UnsupportedPartError(params=(name,))
635 639
636 640
637 641 def iterparts(self):
638 642 """yield all parts contained in the stream"""
639 643 # make sure param have been loaded
640 644 self.params
641 645 indebug(self.ui, 'start extraction of bundle2 parts')
642 646 headerblock = self._readpartheader()
643 647 while headerblock is not None:
644 648 part = unbundlepart(self.ui, headerblock, self._fp)
645 649 yield part
646 650 part.seek(0, 2)
647 651 headerblock = self._readpartheader()
648 652 indebug(self.ui, 'end of bundle2 stream')
649 653
650 654 def _readpartheader(self):
651 655 """reads a part header size and return the bytes blob
652 656
653 657 returns None if empty"""
654 658 headersize = self._unpack(_fpartheadersize)[0]
655 659 if headersize < 0:
656 660 raise error.BundleValueError('negative part header size: %i'
657 661 % headersize)
658 662 indebug(self.ui, 'part header size: %i' % headersize)
659 663 if headersize:
660 664 return self._readexact(headersize)
661 665 return None
662 666
663 667 def compressed(self):
664 668 return False
665 669
666 670 formatmap = {'20': unbundle20}
667 671
668 672 class bundlepart(object):
669 673 """A bundle2 part contains application level payload
670 674
671 675 The part `type` is used to route the part to the application level
672 676 handler.
673 677
674 678 The part payload is contained in ``part.data``. It could be raw bytes or a
675 679 generator of byte chunks.
676 680
677 681 You can add parameters to the part using the ``addparam`` method.
678 682 Parameters can be either mandatory (default) or advisory. Remote side
679 683 should be able to safely ignore the advisory ones.
680 684
681 685 Both data and parameters cannot be modified after the generation has begun.
682 686 """
683 687
684 688 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
685 689 data='', mandatory=True):
686 690 validateparttype(parttype)
687 691 self.id = None
688 692 self.type = parttype
689 693 self._data = data
690 694 self._mandatoryparams = list(mandatoryparams)
691 695 self._advisoryparams = list(advisoryparams)
692 696 # checking for duplicated entries
693 697 self._seenparams = set()
694 698 for pname, __ in self._mandatoryparams + self._advisoryparams:
695 699 if pname in self._seenparams:
696 700 raise RuntimeError('duplicated params: %s' % pname)
697 701 self._seenparams.add(pname)
698 702 # status of the part's generation:
699 703 # - None: not started,
700 704 # - False: currently generated,
701 705 # - True: generation done.
702 706 self._generated = None
703 707 self.mandatory = mandatory
704 708
705 709 def copy(self):
706 710 """return a copy of the part
707 711
708 712 The new part have the very same content but no partid assigned yet.
709 713 Parts with generated data cannot be copied."""
710 714 assert not util.safehasattr(self.data, 'next')
711 715 return self.__class__(self.type, self._mandatoryparams,
712 716 self._advisoryparams, self._data, self.mandatory)
713 717
714 718 # methods used to defines the part content
715 719 def __setdata(self, data):
716 720 if self._generated is not None:
717 721 raise error.ReadOnlyPartError('part is being generated')
718 722 self._data = data
719 723 def __getdata(self):
720 724 return self._data
721 725 data = property(__getdata, __setdata)
722 726
723 727 @property
724 728 def mandatoryparams(self):
725 729 # make it an immutable tuple to force people through ``addparam``
726 730 return tuple(self._mandatoryparams)
727 731
728 732 @property
729 733 def advisoryparams(self):
730 734 # make it an immutable tuple to force people through ``addparam``
731 735 return tuple(self._advisoryparams)
732 736
733 737 def addparam(self, name, value='', mandatory=True):
734 738 if self._generated is not None:
735 739 raise error.ReadOnlyPartError('part is being generated')
736 740 if name in self._seenparams:
737 741 raise ValueError('duplicated params: %s' % name)
738 742 self._seenparams.add(name)
739 743 params = self._advisoryparams
740 744 if mandatory:
741 745 params = self._mandatoryparams
742 746 params.append((name, value))
743 747
744 748 # methods used to generates the bundle2 stream
745 749 def getchunks(self, ui):
746 750 if self._generated is not None:
747 751 raise RuntimeError('part can only be consumed once')
748 752 self._generated = False
749 753
750 754 if ui.debugflag:
751 755 msg = ['bundle2-output-part: "%s"' % self.type]
752 756 if not self.mandatory:
753 757 msg.append(' (advisory)')
754 758 nbmp = len(self.mandatoryparams)
755 759 nbap = len(self.advisoryparams)
756 760 if nbmp or nbap:
757 761 msg.append(' (params:')
758 762 if nbmp:
759 763 msg.append(' %i mandatory' % nbmp)
760 764 if nbap:
761 765 msg.append(' %i advisory' % nbmp)
762 766 msg.append(')')
763 767 if not self.data:
764 768 msg.append(' empty payload')
765 769 elif util.safehasattr(self.data, 'next'):
766 770 msg.append(' streamed payload')
767 771 else:
768 772 msg.append(' %i bytes payload' % len(self.data))
769 773 msg.append('\n')
770 774 ui.debug(''.join(msg))
771 775
772 776 #### header
773 777 if self.mandatory:
774 778 parttype = self.type.upper()
775 779 else:
776 780 parttype = self.type.lower()
777 781 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
778 782 ## parttype
779 783 header = [_pack(_fparttypesize, len(parttype)),
780 784 parttype, _pack(_fpartid, self.id),
781 785 ]
782 786 ## parameters
783 787 # count
784 788 manpar = self.mandatoryparams
785 789 advpar = self.advisoryparams
786 790 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
787 791 # size
788 792 parsizes = []
789 793 for key, value in manpar:
790 794 parsizes.append(len(key))
791 795 parsizes.append(len(value))
792 796 for key, value in advpar:
793 797 parsizes.append(len(key))
794 798 parsizes.append(len(value))
795 799 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
796 800 header.append(paramsizes)
797 801 # key, value
798 802 for key, value in manpar:
799 803 header.append(key)
800 804 header.append(value)
801 805 for key, value in advpar:
802 806 header.append(key)
803 807 header.append(value)
804 808 ## finalize header
805 809 headerchunk = ''.join(header)
806 810 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
807 811 yield _pack(_fpartheadersize, len(headerchunk))
808 812 yield headerchunk
809 813 ## payload
810 814 try:
811 815 for chunk in self._payloadchunks():
812 816 outdebug(ui, 'payload chunk size: %i' % len(chunk))
813 817 yield _pack(_fpayloadsize, len(chunk))
814 818 yield chunk
815 819 except BaseException, exc:
816 820 # backup exception data for later
817 821 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
818 822 % exc)
819 823 exc_info = sys.exc_info()
820 824 msg = 'unexpected error: %s' % exc
821 825 interpart = bundlepart('error:abort', [('message', msg)],
822 826 mandatory=False)
823 827 interpart.id = 0
824 828 yield _pack(_fpayloadsize, -1)
825 829 for chunk in interpart.getchunks(ui=ui):
826 830 yield chunk
827 831 outdebug(ui, 'closing payload chunk')
828 832 # abort current part payload
829 833 yield _pack(_fpayloadsize, 0)
830 834 raise exc_info[0], exc_info[1], exc_info[2]
831 835 # end of payload
832 836 outdebug(ui, 'closing payload chunk')
833 837 yield _pack(_fpayloadsize, 0)
834 838 self._generated = True
835 839
836 840 def _payloadchunks(self):
837 841 """yield chunks of a the part payload
838 842
839 843 Exists to handle the different methods to provide data to a part."""
840 844 # we only support fixed size data now.
841 845 # This will be improved in the future.
842 846 if util.safehasattr(self.data, 'next'):
843 847 buff = util.chunkbuffer(self.data)
844 848 chunk = buff.read(preferedchunksize)
845 849 while chunk:
846 850 yield chunk
847 851 chunk = buff.read(preferedchunksize)
848 852 elif len(self.data):
849 853 yield self.data
850 854
851 855
852 856 flaginterrupt = -1
853 857
854 858 class interrupthandler(unpackermixin):
855 859 """read one part and process it with restricted capability
856 860
857 861 This allows to transmit exception raised on the producer size during part
858 862 iteration while the consumer is reading a part.
859 863
860 864 Part processed in this manner only have access to a ui object,"""
861 865
862 866 def __init__(self, ui, fp):
863 867 super(interrupthandler, self).__init__(fp)
864 868 self.ui = ui
865 869
866 870 def _readpartheader(self):
867 871 """reads a part header size and return the bytes blob
868 872
869 873 returns None if empty"""
870 874 headersize = self._unpack(_fpartheadersize)[0]
871 875 if headersize < 0:
872 876 raise error.BundleValueError('negative part header size: %i'
873 877 % headersize)
874 878 indebug(self.ui, 'part header size: %i\n' % headersize)
875 879 if headersize:
876 880 return self._readexact(headersize)
877 881 return None
878 882
879 883 def __call__(self):
880 884 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
881 885 headerblock = self._readpartheader()
882 886 if headerblock is None:
883 887 indebug(self.ui, 'no part found during interruption.')
884 888 return
885 889 part = unbundlepart(self.ui, headerblock, self._fp)
886 890 op = interruptoperation(self.ui)
887 891 _processpart(op, part)
888 892
889 893 class interruptoperation(object):
890 894 """A limited operation to be use by part handler during interruption
891 895
892 896 It only have access to an ui object.
893 897 """
894 898
895 899 def __init__(self, ui):
896 900 self.ui = ui
897 901 self.reply = None
898 902 self.captureoutput = False
899 903
900 904 @property
901 905 def repo(self):
902 906 raise RuntimeError('no repo access from stream interruption')
903 907
904 908 def gettransaction(self):
905 909 raise TransactionUnavailable('no repo access from stream interruption')
906 910
907 911 class unbundlepart(unpackermixin):
908 912 """a bundle part read from a bundle"""
909 913
910 914 def __init__(self, ui, header, fp):
911 915 super(unbundlepart, self).__init__(fp)
912 916 self.ui = ui
913 917 # unbundle state attr
914 918 self._headerdata = header
915 919 self._headeroffset = 0
916 920 self._initialized = False
917 921 self.consumed = False
918 922 # part data
919 923 self.id = None
920 924 self.type = None
921 925 self.mandatoryparams = None
922 926 self.advisoryparams = None
923 927 self.params = None
924 928 self.mandatorykeys = ()
925 929 self._payloadstream = None
926 930 self._readheader()
927 931 self._mandatory = None
928 932 self._chunkindex = [] #(payload, file) position tuples for chunk starts
929 933 self._pos = 0
930 934
931 935 def _fromheader(self, size):
932 936 """return the next <size> byte from the header"""
933 937 offset = self._headeroffset
934 938 data = self._headerdata[offset:(offset + size)]
935 939 self._headeroffset = offset + size
936 940 return data
937 941
938 942 def _unpackheader(self, format):
939 943 """read given format from header
940 944
941 945 This automatically compute the size of the format to read."""
942 946 data = self._fromheader(struct.calcsize(format))
943 947 return _unpack(format, data)
944 948
945 949 def _initparams(self, mandatoryparams, advisoryparams):
946 950 """internal function to setup all logic related parameters"""
947 951 # make it read only to prevent people touching it by mistake.
948 952 self.mandatoryparams = tuple(mandatoryparams)
949 953 self.advisoryparams = tuple(advisoryparams)
950 954 # user friendly UI
951 955 self.params = dict(self.mandatoryparams)
952 956 self.params.update(dict(self.advisoryparams))
953 957 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
954 958
955 959 def _payloadchunks(self, chunknum=0):
956 960 '''seek to specified chunk and start yielding data'''
957 961 if len(self._chunkindex) == 0:
958 962 assert chunknum == 0, 'Must start with chunk 0'
959 963 self._chunkindex.append((0, super(unbundlepart, self).tell()))
960 964 else:
961 965 assert chunknum < len(self._chunkindex), \
962 966 'Unknown chunk %d' % chunknum
963 967 super(unbundlepart, self).seek(self._chunkindex[chunknum][1])
964 968
965 969 pos = self._chunkindex[chunknum][0]
966 970 payloadsize = self._unpack(_fpayloadsize)[0]
967 971 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
968 972 while payloadsize:
969 973 if payloadsize == flaginterrupt:
970 974 # interruption detection, the handler will now read a
971 975 # single part and process it.
972 976 interrupthandler(self.ui, self._fp)()
973 977 elif payloadsize < 0:
974 978 msg = 'negative payload chunk size: %i' % payloadsize
975 979 raise error.BundleValueError(msg)
976 980 else:
977 981 result = self._readexact(payloadsize)
978 982 chunknum += 1
979 983 pos += payloadsize
980 984 if chunknum == len(self._chunkindex):
981 985 self._chunkindex.append((pos,
982 986 super(unbundlepart, self).tell()))
983 987 yield result
984 988 payloadsize = self._unpack(_fpayloadsize)[0]
985 989 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
986 990
987 991 def _findchunk(self, pos):
988 992 '''for a given payload position, return a chunk number and offset'''
989 993 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
990 994 if ppos == pos:
991 995 return chunk, 0
992 996 elif ppos > pos:
993 997 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
994 998 raise ValueError('Unknown chunk')
995 999
996 1000 def _readheader(self):
997 1001 """read the header and setup the object"""
998 1002 typesize = self._unpackheader(_fparttypesize)[0]
999 1003 self.type = self._fromheader(typesize)
1000 1004 indebug(self.ui, 'part type: "%s"' % self.type)
1001 1005 self.id = self._unpackheader(_fpartid)[0]
1002 1006 indebug(self.ui, 'part id: "%s"' % self.id)
1003 1007 # extract mandatory bit from type
1004 1008 self.mandatory = (self.type != self.type.lower())
1005 1009 self.type = self.type.lower()
1006 1010 ## reading parameters
1007 1011 # param count
1008 1012 mancount, advcount = self._unpackheader(_fpartparamcount)
1009 1013 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1010 1014 # param size
1011 1015 fparamsizes = _makefpartparamsizes(mancount + advcount)
1012 1016 paramsizes = self._unpackheader(fparamsizes)
1013 1017 # make it a list of couple again
1014 1018 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1015 1019 # split mandatory from advisory
1016 1020 mansizes = paramsizes[:mancount]
1017 1021 advsizes = paramsizes[mancount:]
1018 1022 # retrieve param value
1019 1023 manparams = []
1020 1024 for key, value in mansizes:
1021 1025 manparams.append((self._fromheader(key), self._fromheader(value)))
1022 1026 advparams = []
1023 1027 for key, value in advsizes:
1024 1028 advparams.append((self._fromheader(key), self._fromheader(value)))
1025 1029 self._initparams(manparams, advparams)
1026 1030 ## part payload
1027 1031 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1028 1032 # we read the data, tell it
1029 1033 self._initialized = True
1030 1034
1031 1035 def read(self, size=None):
1032 1036 """read payload data"""
1033 1037 if not self._initialized:
1034 1038 self._readheader()
1035 1039 if size is None:
1036 1040 data = self._payloadstream.read()
1037 1041 else:
1038 1042 data = self._payloadstream.read(size)
1039 1043 if size is None or len(data) < size:
1040 1044 self.consumed = True
1041 1045 self._pos += len(data)
1042 1046 return data
1043 1047
1044 1048 def tell(self):
1045 1049 return self._pos
1046 1050
1047 1051 def seek(self, offset, whence=0):
1048 1052 if whence == 0:
1049 1053 newpos = offset
1050 1054 elif whence == 1:
1051 1055 newpos = self._pos + offset
1052 1056 elif whence == 2:
1053 1057 if not self.consumed:
1054 1058 self.read()
1055 1059 newpos = self._chunkindex[-1][0] - offset
1056 1060 else:
1057 1061 raise ValueError('Unknown whence value: %r' % (whence,))
1058 1062
1059 1063 if newpos > self._chunkindex[-1][0] and not self.consumed:
1060 1064 self.read()
1061 1065 if not 0 <= newpos <= self._chunkindex[-1][0]:
1062 1066 raise ValueError('Offset out of range')
1063 1067
1064 1068 if self._pos != newpos:
1065 1069 chunk, internaloffset = self._findchunk(newpos)
1066 1070 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1067 1071 adjust = self.read(internaloffset)
1068 1072 if len(adjust) != internaloffset:
1069 1073 raise util.Abort(_('Seek failed\n'))
1070 1074 self._pos = newpos
1071 1075
1072 1076 # These are only the static capabilities.
1073 1077 # Check the 'getrepocaps' function for the rest.
1074 1078 capabilities = {'HG20': (),
1075 1079 'listkeys': (),
1076 1080 'pushkey': (),
1077 1081 'digests': tuple(sorted(util.DIGESTS.keys())),
1078 1082 'remote-changegroup': ('http', 'https'),
1079 1083 }
1080 1084
1081 1085 def getrepocaps(repo, allowpushback=False):
1082 1086 """return the bundle2 capabilities for a given repo
1083 1087
1084 1088 Exists to allow extensions (like evolution) to mutate the capabilities.
1085 1089 """
1086 1090 caps = capabilities.copy()
1087 1091 caps['changegroup'] = tuple(sorted(changegroup.packermap.keys()))
1088 1092 if obsolete.isenabled(repo, obsolete.exchangeopt):
1089 1093 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1090 1094 caps['obsmarkers'] = supportedformat
1091 1095 if allowpushback:
1092 1096 caps['pushback'] = ()
1093 1097 return caps
1094 1098
1095 1099 def bundle2caps(remote):
1096 1100 """return the bundle capabilities of a peer as dict"""
1097 1101 raw = remote.capable('bundle2')
1098 1102 if not raw and raw != '':
1099 1103 return {}
1100 1104 capsblob = urllib.unquote(remote.capable('bundle2'))
1101 1105 return decodecaps(capsblob)
1102 1106
1103 1107 def obsmarkersversion(caps):
1104 1108 """extract the list of supported obsmarkers versions from a bundle2caps dict
1105 1109 """
1106 1110 obscaps = caps.get('obsmarkers', ())
1107 1111 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1108 1112
1109 1113 @parthandler('changegroup', ('version',))
1110 1114 def handlechangegroup(op, inpart):
1111 1115 """apply a changegroup part on the repo
1112 1116
1113 1117 This is a very early implementation that will massive rework before being
1114 1118 inflicted to any end-user.
1115 1119 """
1116 1120 # Make sure we trigger a transaction creation
1117 1121 #
1118 1122 # The addchangegroup function will get a transaction object by itself, but
1119 1123 # we need to make sure we trigger the creation of a transaction object used
1120 1124 # for the whole processing scope.
1121 1125 op.gettransaction()
1122 1126 unpackerversion = inpart.params.get('version', '01')
1123 1127 # We should raise an appropriate exception here
1124 1128 unpacker = changegroup.packermap[unpackerversion][1]
1125 1129 cg = unpacker(inpart, 'UN')
1126 1130 # the source and url passed here are overwritten by the one contained in
1127 1131 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1128 1132 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
1129 1133 op.records.add('changegroup', {'return': ret})
1130 1134 if op.reply is not None:
1131 1135 # This is definitely not the final form of this
1132 1136 # return. But one need to start somewhere.
1133 1137 part = op.reply.newpart('reply:changegroup', mandatory=False)
1134 1138 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1135 1139 part.addparam('return', '%i' % ret, mandatory=False)
1136 1140 assert not inpart.read()
1137 1141
1138 1142 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1139 1143 ['digest:%s' % k for k in util.DIGESTS.keys()])
1140 1144 @parthandler('remote-changegroup', _remotechangegroupparams)
1141 1145 def handleremotechangegroup(op, inpart):
1142 1146 """apply a bundle10 on the repo, given an url and validation information
1143 1147
1144 1148 All the information about the remote bundle to import are given as
1145 1149 parameters. The parameters include:
1146 1150 - url: the url to the bundle10.
1147 1151 - size: the bundle10 file size. It is used to validate what was
1148 1152 retrieved by the client matches the server knowledge about the bundle.
1149 1153 - digests: a space separated list of the digest types provided as
1150 1154 parameters.
1151 1155 - digest:<digest-type>: the hexadecimal representation of the digest with
1152 1156 that name. Like the size, it is used to validate what was retrieved by
1153 1157 the client matches what the server knows about the bundle.
1154 1158
1155 1159 When multiple digest types are given, all of them are checked.
1156 1160 """
1157 1161 try:
1158 1162 raw_url = inpart.params['url']
1159 1163 except KeyError:
1160 1164 raise util.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1161 1165 parsed_url = util.url(raw_url)
1162 1166 if parsed_url.scheme not in capabilities['remote-changegroup']:
1163 1167 raise util.Abort(_('remote-changegroup does not support %s urls') %
1164 1168 parsed_url.scheme)
1165 1169
1166 1170 try:
1167 1171 size = int(inpart.params['size'])
1168 1172 except ValueError:
1169 1173 raise util.Abort(_('remote-changegroup: invalid value for param "%s"')
1170 1174 % 'size')
1171 1175 except KeyError:
1172 1176 raise util.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1173 1177
1174 1178 digests = {}
1175 1179 for typ in inpart.params.get('digests', '').split():
1176 1180 param = 'digest:%s' % typ
1177 1181 try:
1178 1182 value = inpart.params[param]
1179 1183 except KeyError:
1180 1184 raise util.Abort(_('remote-changegroup: missing "%s" param') %
1181 1185 param)
1182 1186 digests[typ] = value
1183 1187
1184 1188 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1185 1189
1186 1190 # Make sure we trigger a transaction creation
1187 1191 #
1188 1192 # The addchangegroup function will get a transaction object by itself, but
1189 1193 # we need to make sure we trigger the creation of a transaction object used
1190 1194 # for the whole processing scope.
1191 1195 op.gettransaction()
1192 1196 import exchange
1193 1197 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1194 1198 if not isinstance(cg, changegroup.cg1unpacker):
1195 1199 raise util.Abort(_('%s: not a bundle version 1.0') %
1196 1200 util.hidepassword(raw_url))
1197 1201 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
1198 1202 op.records.add('changegroup', {'return': ret})
1199 1203 if op.reply is not None:
1200 1204 # This is definitely not the final form of this
1201 1205 # return. But one need to start somewhere.
1202 1206 part = op.reply.newpart('reply:changegroup')
1203 1207 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1204 1208 part.addparam('return', '%i' % ret, mandatory=False)
1205 1209 try:
1206 1210 real_part.validate()
1207 1211 except util.Abort, e:
1208 1212 raise util.Abort(_('bundle at %s is corrupted:\n%s') %
1209 1213 (util.hidepassword(raw_url), str(e)))
1210 1214 assert not inpart.read()
1211 1215
1212 1216 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1213 1217 def handlereplychangegroup(op, inpart):
1214 1218 ret = int(inpart.params['return'])
1215 1219 replyto = int(inpart.params['in-reply-to'])
1216 1220 op.records.add('changegroup', {'return': ret}, replyto)
1217 1221
1218 1222 @parthandler('check:heads')
1219 1223 def handlecheckheads(op, inpart):
1220 1224 """check that head of the repo did not change
1221 1225
1222 1226 This is used to detect a push race when using unbundle.
1223 1227 This replaces the "heads" argument of unbundle."""
1224 1228 h = inpart.read(20)
1225 1229 heads = []
1226 1230 while len(h) == 20:
1227 1231 heads.append(h)
1228 1232 h = inpart.read(20)
1229 1233 assert not h
1230 1234 if heads != op.repo.heads():
1231 1235 raise error.PushRaced('repository changed while pushing - '
1232 1236 'please try again')
1233 1237
1234 1238 @parthandler('output')
1235 1239 def handleoutput(op, inpart):
1236 1240 """forward output captured on the server to the client"""
1237 1241 for line in inpart.read().splitlines():
1238 1242 op.ui.status(('remote: %s\n' % line))
1239 1243
1240 1244 @parthandler('replycaps')
1241 1245 def handlereplycaps(op, inpart):
1242 1246 """Notify that a reply bundle should be created
1243 1247
1244 1248 The payload contains the capabilities information for the reply"""
1245 1249 caps = decodecaps(inpart.read())
1246 1250 if op.reply is None:
1247 1251 op.reply = bundle20(op.ui, caps)
1248 1252
1249 1253 @parthandler('error:abort', ('message', 'hint'))
1250 1254 def handleerrorabort(op, inpart):
1251 1255 """Used to transmit abort error over the wire"""
1252 1256 raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint'))
1253 1257
1254 1258 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1255 1259 def handleerrorunsupportedcontent(op, inpart):
1256 1260 """Used to transmit unknown content error over the wire"""
1257 1261 kwargs = {}
1258 1262 parttype = inpart.params.get('parttype')
1259 1263 if parttype is not None:
1260 1264 kwargs['parttype'] = parttype
1261 1265 params = inpart.params.get('params')
1262 1266 if params is not None:
1263 1267 kwargs['params'] = params.split('\0')
1264 1268
1265 1269 raise error.UnsupportedPartError(**kwargs)
1266 1270
1267 1271 @parthandler('error:pushraced', ('message',))
1268 1272 def handleerrorpushraced(op, inpart):
1269 1273 """Used to transmit push race error over the wire"""
1270 1274 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1271 1275
1272 1276 @parthandler('listkeys', ('namespace',))
1273 1277 def handlelistkeys(op, inpart):
1274 1278 """retrieve pushkey namespace content stored in a bundle2"""
1275 1279 namespace = inpart.params['namespace']
1276 1280 r = pushkey.decodekeys(inpart.read())
1277 1281 op.records.add('listkeys', (namespace, r))
1278 1282
1279 1283 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1280 1284 def handlepushkey(op, inpart):
1281 1285 """process a pushkey request"""
1282 1286 dec = pushkey.decode
1283 1287 namespace = dec(inpart.params['namespace'])
1284 1288 key = dec(inpart.params['key'])
1285 1289 old = dec(inpart.params['old'])
1286 1290 new = dec(inpart.params['new'])
1287 1291 ret = op.repo.pushkey(namespace, key, old, new)
1288 1292 record = {'namespace': namespace,
1289 1293 'key': key,
1290 1294 'old': old,
1291 1295 'new': new}
1292 1296 op.records.add('pushkey', record)
1293 1297 if op.reply is not None:
1294 1298 rpart = op.reply.newpart('reply:pushkey')
1295 1299 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1296 1300 rpart.addparam('return', '%i' % ret, mandatory=False)
1297 1301
1298 1302 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1299 1303 def handlepushkeyreply(op, inpart):
1300 1304 """retrieve the result of a pushkey request"""
1301 1305 ret = int(inpart.params['return'])
1302 1306 partid = int(inpart.params['in-reply-to'])
1303 1307 op.records.add('pushkey', {'return': ret}, partid)
1304 1308
1305 1309 @parthandler('obsmarkers')
1306 1310 def handleobsmarker(op, inpart):
1307 1311 """add a stream of obsmarkers to the repo"""
1308 1312 tr = op.gettransaction()
1309 1313 markerdata = inpart.read()
1310 1314 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1311 1315 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1312 1316 % len(markerdata))
1313 1317 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1314 1318 if new:
1315 1319 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1316 1320 op.records.add('obsmarkers', {'new': new})
1317 1321 if op.reply is not None:
1318 1322 rpart = op.reply.newpart('reply:obsmarkers')
1319 1323 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1320 1324 rpart.addparam('new', '%i' % new, mandatory=False)
1321 1325
1322 1326
1323 1327 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1324 1328 def handlepushkeyreply(op, inpart):
1325 1329 """retrieve the result of a pushkey request"""
1326 1330 ret = int(inpart.params['new'])
1327 1331 partid = int(inpart.params['in-reply-to'])
1328 1332 op.records.add('obsmarkers', {'new': ret}, partid)
@@ -1,842 +1,843
1 1 This test is dedicated to test the bundle2 container format
2 2
3 3 It test multiple existing parts to test different feature of the container. You
4 4 probably do not need to touch this test unless you change the binary encoding
5 5 of the bundle2 format itself.
6 6
7 7 Create an extension to test bundle2 API
8 8
9 9 $ cat > bundle2.py << EOF
10 10 > """A small extension to test bundle2 implementation
11 11 >
12 12 > Current bundle2 implementation is far too limited to be used in any core
13 13 > code. We still need to be able to test it while it grow up.
14 14 > """
15 15 >
16 16 > import sys, os
17 17 > from mercurial import cmdutil
18 18 > from mercurial import util
19 19 > from mercurial import bundle2
20 20 > from mercurial import scmutil
21 21 > from mercurial import discovery
22 22 > from mercurial import changegroup
23 23 > from mercurial import error
24 24 > from mercurial import obsolete
25 25 >
26 26 >
27 27 > try:
28 28 > import msvcrt
29 29 > msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
30 30 > msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
31 31 > msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
32 32 > except ImportError:
33 33 > pass
34 34 >
35 35 > cmdtable = {}
36 36 > command = cmdutil.command(cmdtable)
37 37 >
38 38 > ELEPHANTSSONG = """Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
39 39 > Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
40 40 > Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko."""
41 41 > assert len(ELEPHANTSSONG) == 178 # future test say 178 bytes, trust it.
42 42 >
43 43 > @bundle2.parthandler('test:song')
44 44 > def songhandler(op, part):
45 45 > """handle a "test:song" bundle2 part, printing the lyrics on stdin"""
46 46 > op.ui.write('The choir starts singing:\n')
47 47 > verses = 0
48 48 > for line in part.read().split('\n'):
49 49 > op.ui.write(' %s\n' % line)
50 50 > verses += 1
51 51 > op.records.add('song', {'verses': verses})
52 52 >
53 53 > @bundle2.parthandler('test:ping')
54 54 > def pinghandler(op, part):
55 55 > op.ui.write('received ping request (id %i)\n' % part.id)
56 56 > if op.reply is not None and 'ping-pong' in op.reply.capabilities:
57 57 > op.ui.write_err('replying to ping request (id %i)\n' % part.id)
58 58 > op.reply.newpart('test:pong', [('in-reply-to', str(part.id))],
59 59 > mandatory=False)
60 60 >
61 61 > @bundle2.parthandler('test:debugreply')
62 62 > def debugreply(op, part):
63 63 > """print data about the capacity of the bundle reply"""
64 64 > if op.reply is None:
65 65 > op.ui.write('debugreply: no reply\n')
66 66 > else:
67 67 > op.ui.write('debugreply: capabilities:\n')
68 68 > for cap in sorted(op.reply.capabilities):
69 69 > op.ui.write('debugreply: %r\n' % cap)
70 70 > for val in op.reply.capabilities[cap]:
71 71 > op.ui.write('debugreply: %r\n' % val)
72 72 >
73 73 > @command('bundle2',
74 74 > [('', 'param', [], 'stream level parameter'),
75 75 > ('', 'unknown', False, 'include an unknown mandatory part in the bundle'),
76 76 > ('', 'unknownparams', False, 'include an unknown part parameters in the bundle'),
77 77 > ('', 'parts', False, 'include some arbitrary parts to the bundle'),
78 78 > ('', 'reply', False, 'produce a reply bundle'),
79 79 > ('', 'pushrace', False, 'includes a check:head part with unknown nodes'),
80 80 > ('', 'genraise', False, 'includes a part that raise an exception during generation'),
81 81 > ('r', 'rev', [], 'includes those changeset in the bundle'),],
82 82 > '[OUTPUTFILE]')
83 83 > def cmdbundle2(ui, repo, path=None, **opts):
84 84 > """write a bundle2 container on standard output"""
85 85 > bundler = bundle2.bundle20(ui)
86 86 > for p in opts['param']:
87 87 > p = p.split('=', 1)
88 88 > try:
89 89 > bundler.addparam(*p)
90 90 > except ValueError, exc:
91 91 > raise util.Abort('%s' % exc)
92 92 >
93 93 > if opts['reply']:
94 94 > capsstring = 'ping-pong\nelephants=babar,celeste\ncity%3D%21=celeste%2Cville'
95 95 > bundler.newpart('replycaps', data=capsstring)
96 96 >
97 97 > if opts['pushrace']:
98 98 > # also serve to test the assignement of data outside of init
99 99 > part = bundler.newpart('check:heads')
100 100 > part.data = '01234567890123456789'
101 101 >
102 102 > revs = opts['rev']
103 103 > if 'rev' in opts:
104 104 > revs = scmutil.revrange(repo, opts['rev'])
105 105 > if revs:
106 106 > # very crude version of a changegroup part creation
107 107 > bundled = repo.revs('%ld::%ld', revs, revs)
108 108 > headmissing = [c.node() for c in repo.set('heads(%ld)', revs)]
109 109 > headcommon = [c.node() for c in repo.set('parents(%ld) - %ld', revs, revs)]
110 110 > outgoing = discovery.outgoing(repo.changelog, headcommon, headmissing)
111 111 > cg = changegroup.getlocalchangegroup(repo, 'test:bundle2', outgoing, None)
112 112 > bundler.newpart('changegroup', data=cg.getchunks(),
113 113 > mandatory=False)
114 114 >
115 115 > if opts['parts']:
116 116 > bundler.newpart('test:empty', mandatory=False)
117 117 > # add a second one to make sure we handle multiple parts
118 118 > bundler.newpart('test:empty', mandatory=False)
119 119 > bundler.newpart('test:song', data=ELEPHANTSSONG, mandatory=False)
120 120 > bundler.newpart('test:debugreply', mandatory=False)
121 121 > mathpart = bundler.newpart('test:math')
122 122 > mathpart.addparam('pi', '3.14')
123 123 > mathpart.addparam('e', '2.72')
124 124 > mathpart.addparam('cooking', 'raw', mandatory=False)
125 125 > mathpart.data = '42'
126 126 > mathpart.mandatory = False
127 127 > # advisory known part with unknown mandatory param
128 128 > bundler.newpart('test:song', [('randomparam','')], mandatory=False)
129 129 > if opts['unknown']:
130 130 > bundler.newpart('test:unknown', data='some random content')
131 131 > if opts['unknownparams']:
132 132 > bundler.newpart('test:song', [('randomparams', '')])
133 133 > if opts['parts']:
134 134 > bundler.newpart('test:ping', mandatory=False)
135 135 > if opts['genraise']:
136 136 > def genraise():
137 137 > yield 'first line\n'
138 138 > raise RuntimeError('Someone set up us the bomb!')
139 139 > bundler.newpart('output', data=genraise(), mandatory=False)
140 140 >
141 141 > if path is None:
142 142 > file = sys.stdout
143 143 > else:
144 144 > file = open(path, 'wb')
145 145 >
146 146 > try:
147 147 > for chunk in bundler.getchunks():
148 148 > file.write(chunk)
149 149 > except RuntimeError, exc:
150 150 > raise util.Abort(exc)
151 151 >
152 152 > @command('unbundle2', [], '')
153 153 > def cmdunbundle2(ui, repo, replypath=None):
154 154 > """process a bundle2 stream from stdin on the current repo"""
155 155 > try:
156 156 > tr = None
157 157 > lock = repo.lock()
158 158 > tr = repo.transaction('processbundle')
159 159 > try:
160 160 > unbundler = bundle2.getunbundler(ui, sys.stdin)
161 161 > op = bundle2.processbundle(repo, unbundler, lambda: tr)
162 162 > tr.close()
163 163 > except error.BundleValueError, exc:
164 164 > raise util.Abort('missing support for %s' % exc)
165 165 > except error.PushRaced, exc:
166 166 > raise util.Abort('push race: %s' % exc)
167 167 > finally:
168 168 > if tr is not None:
169 169 > tr.release()
170 170 > lock.release()
171 171 > remains = sys.stdin.read()
172 172 > ui.write('%i unread bytes\n' % len(remains))
173 173 > if op.records['song']:
174 174 > totalverses = sum(r['verses'] for r in op.records['song'])
175 175 > ui.write('%i total verses sung\n' % totalverses)
176 176 > for rec in op.records['changegroup']:
177 177 > ui.write('addchangegroup return: %i\n' % rec['return'])
178 178 > if op.reply is not None and replypath is not None:
179 179 > file = open(replypath, 'wb')
180 180 > for chunk in op.reply.getchunks():
181 181 > file.write(chunk)
182 182 >
183 183 > @command('statbundle2', [], '')
184 184 > def cmdstatbundle2(ui, repo):
185 185 > """print statistic on the bundle2 container read from stdin"""
186 186 > unbundler = bundle2.getunbundler(ui, sys.stdin)
187 187 > try:
188 188 > params = unbundler.params
189 189 > except error.BundleValueError, exc:
190 190 > raise util.Abort('unknown parameters: %s' % exc)
191 191 > ui.write('options count: %i\n' % len(params))
192 192 > for key in sorted(params):
193 193 > ui.write('- %s\n' % key)
194 194 > value = params[key]
195 195 > if value is not None:
196 196 > ui.write(' %s\n' % value)
197 197 > count = 0
198 198 > for p in unbundler.iterparts():
199 199 > count += 1
200 200 > ui.write(' :%s:\n' % p.type)
201 201 > ui.write(' mandatory: %i\n' % len(p.mandatoryparams))
202 202 > ui.write(' advisory: %i\n' % len(p.advisoryparams))
203 203 > ui.write(' payload: %i bytes\n' % len(p.read()))
204 204 > ui.write('parts count: %i\n' % count)
205 205 > EOF
206 206 $ cat >> $HGRCPATH << EOF
207 207 > [extensions]
208 208 > bundle2=$TESTTMP/bundle2.py
209 209 > [experimental]
210 210 > bundle2-exp=True
211 211 > evolution=createmarkers
212 212 > [ui]
213 213 > ssh=python "$TESTDIR/dummyssh"
214 214 > logtemplate={rev}:{node|short} {phase} {author} {bookmarks} {desc|firstline}
215 215 > [web]
216 216 > push_ssl = false
217 217 > allow_push = *
218 218 > [phases]
219 219 > publish=False
220 220 > EOF
221 221
222 222 The extension requires a repo (currently unused)
223 223
224 224 $ hg init main
225 225 $ cd main
226 226 $ touch a
227 227 $ hg add a
228 228 $ hg commit -m 'a'
229 229
230 230
231 231 Empty bundle
232 232 =================
233 233
234 234 - no option
235 235 - no parts
236 236
237 237 Test bundling
238 238
239 239 $ hg bundle2
240 240 HG20\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
241 241
242 242 Test unbundling
243 243
244 244 $ hg bundle2 | hg statbundle2
245 245 options count: 0
246 246 parts count: 0
247 247
248 248 Test old style bundle are detected and refused
249 249
250 250 $ hg bundle --all ../bundle.hg
251 251 1 changesets found
252 252 $ hg statbundle2 < ../bundle.hg
253 253 abort: unknown bundle version 10
254 254 [255]
255 255
256 256 Test parameters
257 257 =================
258 258
259 259 - some options
260 260 - no parts
261 261
262 262 advisory parameters, no value
263 263 -------------------------------
264 264
265 265 Simplest possible parameters form
266 266
267 267 Test generation simple option
268 268
269 269 $ hg bundle2 --param 'caution'
270 270 HG20\x00\x00\x00\x07caution\x00\x00\x00\x00 (no-eol) (esc)
271 271
272 272 Test unbundling
273 273
274 274 $ hg bundle2 --param 'caution' | hg statbundle2
275 275 options count: 1
276 276 - caution
277 277 parts count: 0
278 278
279 279 Test generation multiple option
280 280
281 281 $ hg bundle2 --param 'caution' --param 'meal'
282 282 HG20\x00\x00\x00\x0ccaution meal\x00\x00\x00\x00 (no-eol) (esc)
283 283
284 284 Test unbundling
285 285
286 286 $ hg bundle2 --param 'caution' --param 'meal' | hg statbundle2
287 287 options count: 2
288 288 - caution
289 289 - meal
290 290 parts count: 0
291 291
292 292 advisory parameters, with value
293 293 -------------------------------
294 294
295 295 Test generation
296 296
297 297 $ hg bundle2 --param 'caution' --param 'meal=vegan' --param 'elephants'
298 298 HG20\x00\x00\x00\x1ccaution meal=vegan elephants\x00\x00\x00\x00 (no-eol) (esc)
299 299
300 300 Test unbundling
301 301
302 302 $ hg bundle2 --param 'caution' --param 'meal=vegan' --param 'elephants' | hg statbundle2
303 303 options count: 3
304 304 - caution
305 305 - elephants
306 306 - meal
307 307 vegan
308 308 parts count: 0
309 309
310 310 parameter with special char in value
311 311 ---------------------------------------------------
312 312
313 313 Test generation
314 314
315 315 $ hg bundle2 --param 'e|! 7/=babar%#==tutu' --param simple
316 316 HG20\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc)
317 317
318 318 Test unbundling
319 319
320 320 $ hg bundle2 --param 'e|! 7/=babar%#==tutu' --param simple | hg statbundle2
321 321 options count: 2
322 322 - e|! 7/
323 323 babar%#==tutu
324 324 - simple
325 325 parts count: 0
326 326
327 327 Test unknown mandatory option
328 328 ---------------------------------------------------
329 329
330 330 $ hg bundle2 --param 'Gravity' | hg statbundle2
331 331 abort: unknown parameters: Stream Parameter - Gravity
332 332 [255]
333 333
334 334 Test debug output
335 335 ---------------------------------------------------
336 336
337 337 bundling debug
338 338
339 339 $ hg bundle2 --debug --param 'e|! 7/=babar%#==tutu' --param simple ../out.hg2 --config progress.debug=true
340 340 bundle2-output-bundle: "HG20", (2 params) 0 parts total
341 341 bundle2-output: start emission of HG20 stream
342 342 bundle2-output: bundle parameter: e%7C%21%207/=babar%25%23%3D%3Dtutu simple
343 343 bundle2-output: start of parts
344 344 bundle2-output: end of bundle
345 345
346 346 file content is ok
347 347
348 348 $ cat ../out.hg2
349 349 HG20\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc)
350 350
351 351 unbundling debug
352 352
353 353 $ hg statbundle2 --debug --config progress.debug=true < ../out.hg2
354 354 bundle2-input: start processing of HG20 stream
355 355 bundle2-input: reading bundle2 stream parameters
356 356 bundle2-input: ignoring unknown parameter 'e|! 7/'
357 357 bundle2-input: ignoring unknown parameter 'simple'
358 358 options count: 2
359 359 - e|! 7/
360 360 babar%#==tutu
361 361 - simple
362 362 bundle2-input: start extraction of bundle2 parts
363 363 bundle2-input: part header size: 0
364 364 bundle2-input: end of bundle2 stream
365 365 parts count: 0
366 366
367 367
368 368 Test buggy input
369 369 ---------------------------------------------------
370 370
371 371 empty parameter name
372 372
373 373 $ hg bundle2 --param '' --quiet
374 374 abort: empty parameter name
375 375 [255]
376 376
377 377 bad parameter name
378 378
379 379 $ hg bundle2 --param 42babar
380 380 abort: non letter first character: '42babar'
381 381 [255]
382 382
383 383
384 384 Test part
385 385 =================
386 386
387 387 $ hg bundle2 --parts ../parts.hg2 --debug --config progress.debug=true
388 388 bundle2-output-bundle: "HG20", 7 parts total
389 389 bundle2-output: start emission of HG20 stream
390 390 bundle2-output: bundle parameter:
391 391 bundle2-output: start of parts
392 392 bundle2-output: bundle part: "test:empty"
393 393 bundle2-output-part: "test:empty" (advisory) empty payload
394 394 bundle2-output: part 0: "test:empty"
395 395 bundle2-output: header chunk size: 17
396 396 bundle2-output: closing payload chunk
397 397 bundle2-output: bundle part: "test:empty"
398 398 bundle2-output-part: "test:empty" (advisory) empty payload
399 399 bundle2-output: part 1: "test:empty"
400 400 bundle2-output: header chunk size: 17
401 401 bundle2-output: closing payload chunk
402 402 bundle2-output: bundle part: "test:song"
403 403 bundle2-output-part: "test:song" (advisory) 178 bytes payload
404 404 bundle2-output: part 2: "test:song"
405 405 bundle2-output: header chunk size: 16
406 406 bundle2-output: payload chunk size: 178
407 407 bundle2-output: closing payload chunk
408 408 bundle2-output: bundle part: "test:debugreply"
409 409 bundle2-output-part: "test:debugreply" (advisory) empty payload
410 410 bundle2-output: part 3: "test:debugreply"
411 411 bundle2-output: header chunk size: 22
412 412 bundle2-output: closing payload chunk
413 413 bundle2-output: bundle part: "test:math"
414 414 bundle2-output-part: "test:math" (advisory) (params: 2 mandatory 2 advisory) 2 bytes payload
415 415 bundle2-output: part 4: "test:math"
416 416 bundle2-output: header chunk size: 43
417 417 bundle2-output: payload chunk size: 2
418 418 bundle2-output: closing payload chunk
419 419 bundle2-output: bundle part: "test:song"
420 420 bundle2-output-part: "test:song" (advisory) (params: 1 mandatory) empty payload
421 421 bundle2-output: part 5: "test:song"
422 422 bundle2-output: header chunk size: 29
423 423 bundle2-output: closing payload chunk
424 424 bundle2-output: bundle part: "test:ping"
425 425 bundle2-output-part: "test:ping" (advisory) empty payload
426 426 bundle2-output: part 6: "test:ping"
427 427 bundle2-output: header chunk size: 16
428 428 bundle2-output: closing payload chunk
429 429 bundle2-output: end of bundle
430 430
431 431 $ cat ../parts.hg2
432 432 HG20\x00\x00\x00\x00\x00\x00\x00\x11 (esc)
433 433 test:empty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11 (esc)
434 434 test:empty\x00\x00\x00\x01\x00\x00\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)
435 435 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
436 436 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.\x00\x00\x00\x00\x00\x00\x00\x16\x0ftest:debugreply\x00\x00\x00\x03\x00\x00\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\x00\x00\x1d test:song\x00\x00\x00\x05\x01\x00\x0b\x00randomparam\x00\x00\x00\x00\x00\x00\x00\x10 test:ping\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
437 437
438 438
439 439 $ hg statbundle2 < ../parts.hg2
440 440 options count: 0
441 441 :test:empty:
442 442 mandatory: 0
443 443 advisory: 0
444 444 payload: 0 bytes
445 445 :test:empty:
446 446 mandatory: 0
447 447 advisory: 0
448 448 payload: 0 bytes
449 449 :test:song:
450 450 mandatory: 0
451 451 advisory: 0
452 452 payload: 178 bytes
453 453 :test:debugreply:
454 454 mandatory: 0
455 455 advisory: 0
456 456 payload: 0 bytes
457 457 :test:math:
458 458 mandatory: 2
459 459 advisory: 1
460 460 payload: 2 bytes
461 461 :test:song:
462 462 mandatory: 1
463 463 advisory: 0
464 464 payload: 0 bytes
465 465 :test:ping:
466 466 mandatory: 0
467 467 advisory: 0
468 468 payload: 0 bytes
469 469 parts count: 7
470 470
471 471 $ hg statbundle2 --debug --config progress.debug=true < ../parts.hg2
472 472 bundle2-input: start processing of HG20 stream
473 473 bundle2-input: reading bundle2 stream parameters
474 474 options count: 0
475 475 bundle2-input: start extraction of bundle2 parts
476 476 bundle2-input: part header size: 17
477 477 bundle2-input: part type: "test:empty"
478 478 bundle2-input: part id: "0"
479 479 bundle2-input: part parameters: 0
480 480 :test:empty:
481 481 mandatory: 0
482 482 advisory: 0
483 483 bundle2-input: payload chunk size: 0
484 484 payload: 0 bytes
485 485 bundle2-input: part header size: 17
486 486 bundle2-input: part type: "test:empty"
487 487 bundle2-input: part id: "1"
488 488 bundle2-input: part parameters: 0
489 489 :test:empty:
490 490 mandatory: 0
491 491 advisory: 0
492 492 bundle2-input: payload chunk size: 0
493 493 payload: 0 bytes
494 494 bundle2-input: part header size: 16
495 495 bundle2-input: part type: "test:song"
496 496 bundle2-input: part id: "2"
497 497 bundle2-input: part parameters: 0
498 498 :test:song:
499 499 mandatory: 0
500 500 advisory: 0
501 501 bundle2-input: payload chunk size: 178
502 502 bundle2-input: payload chunk size: 0
503 503 payload: 178 bytes
504 504 bundle2-input: part header size: 22
505 505 bundle2-input: part type: "test:debugreply"
506 506 bundle2-input: part id: "3"
507 507 bundle2-input: part parameters: 0
508 508 :test:debugreply:
509 509 mandatory: 0
510 510 advisory: 0
511 511 bundle2-input: payload chunk size: 0
512 512 payload: 0 bytes
513 513 bundle2-input: part header size: 43
514 514 bundle2-input: part type: "test:math"
515 515 bundle2-input: part id: "4"
516 516 bundle2-input: part parameters: 3
517 517 :test:math:
518 518 mandatory: 2
519 519 advisory: 1
520 520 bundle2-input: payload chunk size: 2
521 521 bundle2-input: payload chunk size: 0
522 522 payload: 2 bytes
523 523 bundle2-input: part header size: 29
524 524 bundle2-input: part type: "test:song"
525 525 bundle2-input: part id: "5"
526 526 bundle2-input: part parameters: 1
527 527 :test:song:
528 528 mandatory: 1
529 529 advisory: 0
530 530 bundle2-input: payload chunk size: 0
531 531 payload: 0 bytes
532 532 bundle2-input: part header size: 16
533 533 bundle2-input: part type: "test:ping"
534 534 bundle2-input: part id: "6"
535 535 bundle2-input: part parameters: 0
536 536 :test:ping:
537 537 mandatory: 0
538 538 advisory: 0
539 539 bundle2-input: payload chunk size: 0
540 540 payload: 0 bytes
541 541 bundle2-input: part header size: 0
542 542 bundle2-input: end of bundle2 stream
543 543 parts count: 7
544 544
545 545 Test actual unbundling of test part
546 546 =======================================
547 547
548 548 Process the bundle
549 549
550 550 $ hg unbundle2 --debug --config progress.debug=true < ../parts.hg2
551 551 bundle2-input: start processing of HG20 stream
552 552 bundle2-input: reading bundle2 stream parameters
553 553 bundle2-input-bundle: with-transaction
554 554 bundle2-input: start extraction of bundle2 parts
555 555 bundle2-input: part header size: 17
556 556 bundle2-input: part type: "test:empty"
557 557 bundle2-input: part id: "0"
558 558 bundle2-input: part parameters: 0
559 559 bundle2-input: ignoring unsupported advisory part test:empty
560 560 bundle2-input: payload chunk size: 0
561 561 bundle2-input: part header size: 17
562 562 bundle2-input: part type: "test:empty"
563 563 bundle2-input: part id: "1"
564 564 bundle2-input: part parameters: 0
565 565 bundle2-input: ignoring unsupported advisory part test:empty
566 566 bundle2-input: payload chunk size: 0
567 567 bundle2-input: part header size: 16
568 568 bundle2-input: part type: "test:song"
569 569 bundle2-input: part id: "2"
570 570 bundle2-input: part parameters: 0
571 571 bundle2-input: found a handler for part 'test:song'
572 572 The choir starts singing:
573 573 bundle2-input: payload chunk size: 178
574 574 bundle2-input: payload chunk size: 0
575 575 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
576 576 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
577 577 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
578 578 bundle2-input: part header size: 22
579 579 bundle2-input: part type: "test:debugreply"
580 580 bundle2-input: part id: "3"
581 581 bundle2-input: part parameters: 0
582 582 bundle2-input: found a handler for part 'test:debugreply'
583 583 debugreply: no reply
584 584 bundle2-input: payload chunk size: 0
585 585 bundle2-input: part header size: 43
586 586 bundle2-input: part type: "test:math"
587 587 bundle2-input: part id: "4"
588 588 bundle2-input: part parameters: 3
589 589 bundle2-input: ignoring unsupported advisory part test:math
590 590 bundle2-input: payload chunk size: 2
591 591 bundle2-input: payload chunk size: 0
592 592 bundle2-input: part header size: 29
593 593 bundle2-input: part type: "test:song"
594 594 bundle2-input: part id: "5"
595 595 bundle2-input: part parameters: 1
596 596 bundle2-input: found a handler for part 'test:song'
597 597 bundle2-input: ignoring unsupported advisory part test:song - randomparam
598 598 bundle2-input: payload chunk size: 0
599 599 bundle2-input: part header size: 16
600 600 bundle2-input: part type: "test:ping"
601 601 bundle2-input: part id: "6"
602 602 bundle2-input: part parameters: 0
603 603 bundle2-input: found a handler for part 'test:ping'
604 604 received ping request (id 6)
605 605 bundle2-input: payload chunk size: 0
606 606 bundle2-input: part header size: 0
607 607 bundle2-input: end of bundle2 stream
608 bundle2-input-bundle: 6 parts total
608 609 0 unread bytes
609 610 3 total verses sung
610 611
611 612 Unbundle with an unknown mandatory part
612 613 (should abort)
613 614
614 615 $ hg bundle2 --parts --unknown ../unknown.hg2
615 616
616 617 $ hg unbundle2 < ../unknown.hg2
617 618 The choir starts singing:
618 619 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
619 620 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
620 621 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
621 622 debugreply: no reply
622 623 0 unread bytes
623 624 abort: missing support for test:unknown
624 625 [255]
625 626
626 627 Unbundle with an unknown mandatory part parameters
627 628 (should abort)
628 629
629 630 $ hg bundle2 --unknownparams ../unknown.hg2
630 631
631 632 $ hg unbundle2 < ../unknown.hg2
632 633 0 unread bytes
633 634 abort: missing support for test:song - randomparams
634 635 [255]
635 636
636 637 unbundle with a reply
637 638
638 639 $ hg bundle2 --parts --reply ../parts-reply.hg2
639 640 $ hg unbundle2 ../reply.hg2 < ../parts-reply.hg2
640 641 0 unread bytes
641 642 3 total verses sung
642 643
643 644 The reply is a bundle
644 645
645 646 $ cat ../reply.hg2
646 647 HG20\x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x00\x00\x01\x0b\x01in-reply-to3\x00\x00\x00\xd9The choir starts singing: (esc)
647 648 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
648 649 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
649 650 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
650 651 \x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to4\x00\x00\x00\xc9debugreply: capabilities: (esc)
651 652 debugreply: 'city=!'
652 653 debugreply: 'celeste,ville'
653 654 debugreply: 'elephants'
654 655 debugreply: 'babar'
655 656 debugreply: 'celeste'
656 657 debugreply: 'ping-pong'
657 658 \x00\x00\x00\x00\x00\x00\x00\x1e test:pong\x00\x00\x00\x02\x01\x00\x0b\x01in-reply-to7\x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x03\x00\x01\x0b\x01in-reply-to7\x00\x00\x00=received ping request (id 7) (esc)
658 659 replying to ping request (id 7)
659 660 \x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
660 661
661 662 The reply is valid
662 663
663 664 $ hg statbundle2 < ../reply.hg2
664 665 options count: 0
665 666 :output:
666 667 mandatory: 0
667 668 advisory: 1
668 669 payload: 217 bytes
669 670 :output:
670 671 mandatory: 0
671 672 advisory: 1
672 673 payload: 201 bytes
673 674 :test:pong:
674 675 mandatory: 1
675 676 advisory: 0
676 677 payload: 0 bytes
677 678 :output:
678 679 mandatory: 0
679 680 advisory: 1
680 681 payload: 61 bytes
681 682 parts count: 4
682 683
683 684 Unbundle the reply to get the output:
684 685
685 686 $ hg unbundle2 < ../reply.hg2
686 687 remote: The choir starts singing:
687 688 remote: Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
688 689 remote: Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
689 690 remote: Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
690 691 remote: debugreply: capabilities:
691 692 remote: debugreply: 'city=!'
692 693 remote: debugreply: 'celeste,ville'
693 694 remote: debugreply: 'elephants'
694 695 remote: debugreply: 'babar'
695 696 remote: debugreply: 'celeste'
696 697 remote: debugreply: 'ping-pong'
697 698 remote: received ping request (id 7)
698 699 remote: replying to ping request (id 7)
699 700 0 unread bytes
700 701
701 702 Test push race detection
702 703
703 704 $ hg bundle2 --pushrace ../part-race.hg2
704 705
705 706 $ hg unbundle2 < ../part-race.hg2
706 707 0 unread bytes
707 708 abort: push race: repository changed while pushing - please try again
708 709 [255]
709 710
710 711 Support for changegroup
711 712 ===================================
712 713
713 714 $ hg unbundle $TESTDIR/bundles/rebase.hg
714 715 adding changesets
715 716 adding manifests
716 717 adding file changes
717 718 added 8 changesets with 7 changes to 7 files (+3 heads)
718 719 (run 'hg heads' to see heads, 'hg merge' to merge)
719 720
720 721 $ hg log -G
721 722 o 8:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> H
722 723 |
723 724 | o 7:eea13746799a draft Nicolas Dumazet <nicdumz.commits@gmail.com> G
724 725 |/|
725 726 o | 6:24b6387c8c8c draft Nicolas Dumazet <nicdumz.commits@gmail.com> F
726 727 | |
727 728 | o 5:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
728 729 |/
729 730 | o 4:32af7686d403 draft Nicolas Dumazet <nicdumz.commits@gmail.com> D
730 731 | |
731 732 | o 3:5fddd98957c8 draft Nicolas Dumazet <nicdumz.commits@gmail.com> C
732 733 | |
733 734 | o 2:42ccdea3bb16 draft Nicolas Dumazet <nicdumz.commits@gmail.com> B
734 735 |/
735 736 o 1:cd010b8cd998 draft Nicolas Dumazet <nicdumz.commits@gmail.com> A
736 737
737 738 @ 0:3903775176ed draft test a
738 739
739 740
740 741 $ hg bundle2 --debug --config progress.debug=true --rev '8+7+5+4' ../rev.hg2
741 742 4 changesets found
742 743 list of changesets:
743 744 32af7686d403cf45b5d95f2d70cebea587ac806a
744 745 9520eea781bcca16c1e15acc0ba14335a0e8e5ba
745 746 eea13746799a9e0bfd88f29d3c2e9dc9389f524f
746 747 02de42196ebee42ef284b6780a87cdc96e8eaab6
747 748 bundle2-output-bundle: "HG20", 1 parts total
748 749 bundle2-output: start emission of HG20 stream
749 750 bundle2-output: bundle parameter:
750 751 bundle2-output: start of parts
751 752 bundle2-output: bundle part: "changegroup"
752 753 bundle2-output-part: "changegroup" (advisory) streamed payload
753 754 bundle2-output: part 0: "changegroup"
754 755 bundle2-output: header chunk size: 18
755 756 bundling: 1/4 changesets (25.00%)
756 757 bundling: 2/4 changesets (50.00%)
757 758 bundling: 3/4 changesets (75.00%)
758 759 bundling: 4/4 changesets (100.00%)
759 760 bundling: 1/4 manifests (25.00%)
760 761 bundling: 2/4 manifests (50.00%)
761 762 bundling: 3/4 manifests (75.00%)
762 763 bundling: 4/4 manifests (100.00%)
763 764 bundling: D 1/3 files (33.33%)
764 765 bundling: E 2/3 files (66.67%)
765 766 bundling: H 3/3 files (100.00%)
766 767 bundle2-output: payload chunk size: 1555
767 768 bundle2-output: closing payload chunk
768 769 bundle2-output: end of bundle
769 770
770 771 $ cat ../rev.hg2
771 772 HG20\x00\x00\x00\x00\x00\x00\x00\x12\x0bchangegroup\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)
772 773 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02D (esc)
773 774 \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)
774 775 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02E (esc)
775 776 \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)
776 777 \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)
777 778 \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)
778 779 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)8bee48edc7318541fc0013ee41b089276a8c24bf (esc)
779 780 \x00\x00\x00f\x00\x00\x00f\x00\x00\x00\x02H (esc)
780 781 \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)
781 782 \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)
782 783 \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)
783 784 \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)
784 785 \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)
785 786 \x00\x00\x00\x00\x00\x00\x00\x05D\x00\x00\x00b\xc3\xf1\xca)$\xc1j\x19\xb0ej\x84\x90\x0ePN[ (esc)
786 787 \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)
787 788 \x00\x00\x00\x00\x00\x00\x00\x05E\x00\x00\x00b\x9co\xd05 (esc)
788 789 l\r (no-eol) (esc)
789 790 \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)
790 791 \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)
791 792 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02H (esc)
792 793 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
793 794
794 795 $ hg debugbundle ../rev.hg2
795 796 Stream params: {}
796 797 changegroup -- '{}'
797 798 32af7686d403cf45b5d95f2d70cebea587ac806a
798 799 9520eea781bcca16c1e15acc0ba14335a0e8e5ba
799 800 eea13746799a9e0bfd88f29d3c2e9dc9389f524f
800 801 02de42196ebee42ef284b6780a87cdc96e8eaab6
801 802 $ hg unbundle ../rev.hg2
802 803 adding changesets
803 804 adding manifests
804 805 adding file changes
805 806 added 0 changesets with 0 changes to 3 files
806 807
807 808 with reply
808 809
809 810 $ hg bundle2 --rev '8+7+5+4' --reply ../rev-rr.hg2
810 811 $ hg unbundle2 ../rev-reply.hg2 < ../rev-rr.hg2
811 812 0 unread bytes
812 813 addchangegroup return: 1
813 814
814 815 $ cat ../rev-reply.hg2
815 816 HG20\x00\x00\x00\x00\x00\x00\x00/\x11reply:changegroup\x00\x00\x00\x00\x00\x02\x0b\x01\x06\x01in-reply-to1return1\x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to1\x00\x00\x00dadding changesets (esc)
816 817 adding manifests
817 818 adding file changes
818 819 added 0 changesets with 0 changes to 3 files
819 820 \x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
820 821
821 822 Check handling of exception during generation.
822 823 ----------------------------------------------
823 824
824 825 $ hg bundle2 --genraise > ../genfailed.hg2
825 826 abort: Someone set up us the bomb!
826 827 [255]
827 828
828 829 Should still be a valid bundle
829 830
830 831 $ cat ../genfailed.hg2
831 832 HG20\x00\x00\x00\x00\x00\x00\x00\r (no-eol) (esc)
832 833 \x06output\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00H\x0berror:abort\x00\x00\x00\x00\x01\x00\x07-messageunexpected error: Someone set up us the bomb!\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
833 834
834 835 And its handling on the other size raise a clean exception
835 836
836 837 $ cat ../genfailed.hg2 | hg unbundle2
837 838 0 unread bytes
838 839 abort: unexpected error: Someone set up us the bomb!
839 840 [255]
840 841
841 842
842 843 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now