##// END OF EJS Templates
bundle2: add debug output for part generation...
Pierre-Yves David -
r25321:b44ee346 default
parent child Browse files
Show More
@@ -1,1282 +1,1287
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 iterparts = unbundler.iterparts()
322 322 part = None
323 323 try:
324 324 for part in iterparts:
325 325 _processpart(op, part)
326 326 except BaseException, exc:
327 327 for part in iterparts:
328 328 # consume the bundle content
329 329 part.seek(0, 2)
330 330 # Small hack to let caller code distinguish exceptions from bundle2
331 331 # processing from processing the old format. This is mostly
332 332 # needed to handle different return codes to unbundle according to the
333 333 # type of bundle. We should probably clean up or drop this return code
334 334 # craziness in a future version.
335 335 exc.duringunbundle2 = True
336 336 salvaged = []
337 337 if op.reply is not None:
338 338 salvaged = op.reply.salvageoutput()
339 339 exc._bundle2salvagedoutput = salvaged
340 340 raise
341 341 return op
342 342
343 343 def _processpart(op, part):
344 344 """process a single part from a bundle
345 345
346 346 The part is guaranteed to have been fully consumed when the function exits
347 347 (even if an exception is raised)."""
348 348 try:
349 349 try:
350 350 handler = parthandlermapping.get(part.type)
351 351 if handler is None:
352 352 raise error.UnsupportedPartError(parttype=part.type)
353 353 indebug(op.ui, 'found a handler for part %r' % part.type)
354 354 unknownparams = part.mandatorykeys - handler.params
355 355 if unknownparams:
356 356 unknownparams = list(unknownparams)
357 357 unknownparams.sort()
358 358 raise error.UnsupportedPartError(parttype=part.type,
359 359 params=unknownparams)
360 360 except error.UnsupportedPartError, exc:
361 361 if part.mandatory: # mandatory parts
362 362 raise
363 363 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
364 364 return # skip to part processing
365 365
366 366 # handler is called outside the above try block so that we don't
367 367 # risk catching KeyErrors from anything other than the
368 368 # parthandlermapping lookup (any KeyError raised by handler()
369 369 # itself represents a defect of a different variety).
370 370 output = None
371 371 if op.captureoutput and op.reply is not None:
372 372 op.ui.pushbuffer(error=True, subproc=True)
373 373 output = ''
374 374 try:
375 375 handler(op, part)
376 376 finally:
377 377 if output is not None:
378 378 output = op.ui.popbuffer()
379 379 if output:
380 380 outpart = op.reply.newpart('output', data=output,
381 381 mandatory=False)
382 382 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
383 383 finally:
384 384 # consume the part content to not corrupt the stream.
385 385 part.seek(0, 2)
386 386
387 387
388 388 def decodecaps(blob):
389 389 """decode a bundle2 caps bytes blob into a dictionary
390 390
391 391 The blob is a list of capabilities (one per line)
392 392 Capabilities may have values using a line of the form::
393 393
394 394 capability=value1,value2,value3
395 395
396 396 The values are always a list."""
397 397 caps = {}
398 398 for line in blob.splitlines():
399 399 if not line:
400 400 continue
401 401 if '=' not in line:
402 402 key, vals = line, ()
403 403 else:
404 404 key, vals = line.split('=', 1)
405 405 vals = vals.split(',')
406 406 key = urllib.unquote(key)
407 407 vals = [urllib.unquote(v) for v in vals]
408 408 caps[key] = vals
409 409 return caps
410 410
411 411 def encodecaps(caps):
412 412 """encode a bundle2 caps dictionary into a bytes blob"""
413 413 chunks = []
414 414 for ca in sorted(caps):
415 415 vals = caps[ca]
416 416 ca = urllib.quote(ca)
417 417 vals = [urllib.quote(v) for v in vals]
418 418 if vals:
419 419 ca = "%s=%s" % (ca, ','.join(vals))
420 420 chunks.append(ca)
421 421 return '\n'.join(chunks)
422 422
423 423 class bundle20(object):
424 424 """represent an outgoing bundle2 container
425 425
426 426 Use the `addparam` method to add stream level parameter. and `newpart` to
427 427 populate it. Then call `getchunks` to retrieve all the binary chunks of
428 428 data that compose the bundle2 container."""
429 429
430 430 _magicstring = 'HG20'
431 431
432 432 def __init__(self, ui, capabilities=()):
433 433 self.ui = ui
434 434 self._params = []
435 435 self._parts = []
436 436 self.capabilities = dict(capabilities)
437 437
438 438 @property
439 439 def nbparts(self):
440 440 """total number of parts added to the bundler"""
441 441 return len(self._parts)
442 442
443 443 # methods used to defines the bundle2 content
444 444 def addparam(self, name, value=None):
445 445 """add a stream level parameter"""
446 446 if not name:
447 447 raise ValueError('empty parameter name')
448 448 if name[0] not in string.letters:
449 449 raise ValueError('non letter first character: %r' % name)
450 450 self._params.append((name, value))
451 451
452 452 def addpart(self, part):
453 453 """add a new part to the bundle2 container
454 454
455 455 Parts contains the actual applicative payload."""
456 456 assert part.id is None
457 457 part.id = len(self._parts) # very cheap counter
458 458 self._parts.append(part)
459 459
460 460 def newpart(self, typeid, *args, **kwargs):
461 461 """create a new part and add it to the containers
462 462
463 463 As the part is directly added to the containers. For now, this means
464 464 that any failure to properly initialize the part after calling
465 465 ``newpart`` should result in a failure of the whole bundling process.
466 466
467 467 You can still fall back to manually create and add if you need better
468 468 control."""
469 469 part = bundlepart(typeid, *args, **kwargs)
470 470 self.addpart(part)
471 471 return part
472 472
473 473 # methods used to generate the bundle2 stream
474 474 def getchunks(self):
475 475 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
476 476 yield self._magicstring
477 477 param = self._paramchunk()
478 478 outdebug(self.ui, 'bundle parameter: %s' % param)
479 479 yield _pack(_fstreamparamsize, len(param))
480 480 if param:
481 481 yield param
482 482
483 483 outdebug(self.ui, 'start of parts')
484 484 for part in self._parts:
485 485 outdebug(self.ui, 'bundle part: "%s"' % part.type)
486 for chunk in part.getchunks():
486 for chunk in part.getchunks(ui=self.ui):
487 487 yield chunk
488 488 outdebug(self.ui, 'end of bundle')
489 489 yield _pack(_fpartheadersize, 0)
490 490
491 491 def _paramchunk(self):
492 492 """return a encoded version of all stream parameters"""
493 493 blocks = []
494 494 for par, value in self._params:
495 495 par = urllib.quote(par)
496 496 if value is not None:
497 497 value = urllib.quote(value)
498 498 par = '%s=%s' % (par, value)
499 499 blocks.append(par)
500 500 return ' '.join(blocks)
501 501
502 502 def salvageoutput(self):
503 503 """return a list with a copy of all output parts in the bundle
504 504
505 505 This is meant to be used during error handling to make sure we preserve
506 506 server output"""
507 507 salvaged = []
508 508 for part in self._parts:
509 509 if part.type.startswith('output'):
510 510 salvaged.append(part.copy())
511 511 return salvaged
512 512
513 513
514 514 class unpackermixin(object):
515 515 """A mixin to extract bytes and struct data from a stream"""
516 516
517 517 def __init__(self, fp):
518 518 self._fp = fp
519 519 self._seekable = (util.safehasattr(fp, 'seek') and
520 520 util.safehasattr(fp, 'tell'))
521 521
522 522 def _unpack(self, format):
523 523 """unpack this struct format from the stream"""
524 524 data = self._readexact(struct.calcsize(format))
525 525 return _unpack(format, data)
526 526
527 527 def _readexact(self, size):
528 528 """read exactly <size> bytes from the stream"""
529 529 return changegroup.readexactly(self._fp, size)
530 530
531 531 def seek(self, offset, whence=0):
532 532 """move the underlying file pointer"""
533 533 if self._seekable:
534 534 return self._fp.seek(offset, whence)
535 535 else:
536 536 raise NotImplementedError(_('File pointer is not seekable'))
537 537
538 538 def tell(self):
539 539 """return the file offset, or None if file is not seekable"""
540 540 if self._seekable:
541 541 try:
542 542 return self._fp.tell()
543 543 except IOError, e:
544 544 if e.errno == errno.ESPIPE:
545 545 self._seekable = False
546 546 else:
547 547 raise
548 548 return None
549 549
550 550 def close(self):
551 551 """close underlying file"""
552 552 if util.safehasattr(self._fp, 'close'):
553 553 return self._fp.close()
554 554
555 555 def getunbundler(ui, fp, header=None):
556 556 """return a valid unbundler object for a given header"""
557 557 if header is None:
558 558 header = changegroup.readexactly(fp, 4)
559 559 magic, version = header[0:2], header[2:4]
560 560 if magic != 'HG':
561 561 raise util.Abort(_('not a Mercurial bundle'))
562 562 unbundlerclass = formatmap.get(version)
563 563 if unbundlerclass is None:
564 564 raise util.Abort(_('unknown bundle version %s') % version)
565 565 unbundler = unbundlerclass(ui, fp)
566 566 indebug(ui, 'start processing of %s stream' % header)
567 567 return unbundler
568 568
569 569 class unbundle20(unpackermixin):
570 570 """interpret a bundle2 stream
571 571
572 572 This class is fed with a binary stream and yields parts through its
573 573 `iterparts` methods."""
574 574
575 575 def __init__(self, ui, fp):
576 576 """If header is specified, we do not read it out of the stream."""
577 577 self.ui = ui
578 578 super(unbundle20, self).__init__(fp)
579 579
580 580 @util.propertycache
581 581 def params(self):
582 582 """dictionary of stream level parameters"""
583 583 indebug(self.ui, 'reading bundle2 stream parameters')
584 584 params = {}
585 585 paramssize = self._unpack(_fstreamparamsize)[0]
586 586 if paramssize < 0:
587 587 raise error.BundleValueError('negative bundle param size: %i'
588 588 % paramssize)
589 589 if paramssize:
590 590 for p in self._readexact(paramssize).split(' '):
591 591 p = p.split('=', 1)
592 592 p = [urllib.unquote(i) for i in p]
593 593 if len(p) < 2:
594 594 p.append(None)
595 595 self._processparam(*p)
596 596 params[p[0]] = p[1]
597 597 return params
598 598
599 599 def _processparam(self, name, value):
600 600 """process a parameter, applying its effect if needed
601 601
602 602 Parameter starting with a lower case letter are advisory and will be
603 603 ignored when unknown. Those starting with an upper case letter are
604 604 mandatory and will this function will raise a KeyError when unknown.
605 605
606 606 Note: no option are currently supported. Any input will be either
607 607 ignored or failing.
608 608 """
609 609 if not name:
610 610 raise ValueError('empty parameter name')
611 611 if name[0] not in string.letters:
612 612 raise ValueError('non letter first character: %r' % name)
613 613 # Some logic will be later added here to try to process the option for
614 614 # a dict of known parameter.
615 615 if name[0].islower():
616 616 indebug(self.ui, "ignoring unknown parameter %r" % name)
617 617 else:
618 618 raise error.UnsupportedPartError(params=(name,))
619 619
620 620
621 621 def iterparts(self):
622 622 """yield all parts contained in the stream"""
623 623 # make sure param have been loaded
624 624 self.params
625 625 indebug(self.ui, 'start extraction of bundle2 parts')
626 626 headerblock = self._readpartheader()
627 627 while headerblock is not None:
628 628 part = unbundlepart(self.ui, headerblock, self._fp)
629 629 yield part
630 630 part.seek(0, 2)
631 631 headerblock = self._readpartheader()
632 632 indebug(self.ui, 'end of bundle2 stream')
633 633
634 634 def _readpartheader(self):
635 635 """reads a part header size and return the bytes blob
636 636
637 637 returns None if empty"""
638 638 headersize = self._unpack(_fpartheadersize)[0]
639 639 if headersize < 0:
640 640 raise error.BundleValueError('negative part header size: %i'
641 641 % headersize)
642 642 indebug(self.ui, 'part header size: %i' % headersize)
643 643 if headersize:
644 644 return self._readexact(headersize)
645 645 return None
646 646
647 647 def compressed(self):
648 648 return False
649 649
650 650 formatmap = {'20': unbundle20}
651 651
652 652 class bundlepart(object):
653 653 """A bundle2 part contains application level payload
654 654
655 655 The part `type` is used to route the part to the application level
656 656 handler.
657 657
658 658 The part payload is contained in ``part.data``. It could be raw bytes or a
659 659 generator of byte chunks.
660 660
661 661 You can add parameters to the part using the ``addparam`` method.
662 662 Parameters can be either mandatory (default) or advisory. Remote side
663 663 should be able to safely ignore the advisory ones.
664 664
665 665 Both data and parameters cannot be modified after the generation has begun.
666 666 """
667 667
668 668 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
669 669 data='', mandatory=True):
670 670 validateparttype(parttype)
671 671 self.id = None
672 672 self.type = parttype
673 673 self._data = data
674 674 self._mandatoryparams = list(mandatoryparams)
675 675 self._advisoryparams = list(advisoryparams)
676 676 # checking for duplicated entries
677 677 self._seenparams = set()
678 678 for pname, __ in self._mandatoryparams + self._advisoryparams:
679 679 if pname in self._seenparams:
680 680 raise RuntimeError('duplicated params: %s' % pname)
681 681 self._seenparams.add(pname)
682 682 # status of the part's generation:
683 683 # - None: not started,
684 684 # - False: currently generated,
685 685 # - True: generation done.
686 686 self._generated = None
687 687 self.mandatory = mandatory
688 688
689 689 def copy(self):
690 690 """return a copy of the part
691 691
692 692 The new part have the very same content but no partid assigned yet.
693 693 Parts with generated data cannot be copied."""
694 694 assert not util.safehasattr(self.data, 'next')
695 695 return self.__class__(self.type, self._mandatoryparams,
696 696 self._advisoryparams, self._data, self.mandatory)
697 697
698 698 # methods used to defines the part content
699 699 def __setdata(self, data):
700 700 if self._generated is not None:
701 701 raise error.ReadOnlyPartError('part is being generated')
702 702 self._data = data
703 703 def __getdata(self):
704 704 return self._data
705 705 data = property(__getdata, __setdata)
706 706
707 707 @property
708 708 def mandatoryparams(self):
709 709 # make it an immutable tuple to force people through ``addparam``
710 710 return tuple(self._mandatoryparams)
711 711
712 712 @property
713 713 def advisoryparams(self):
714 714 # make it an immutable tuple to force people through ``addparam``
715 715 return tuple(self._advisoryparams)
716 716
717 717 def addparam(self, name, value='', mandatory=True):
718 718 if self._generated is not None:
719 719 raise error.ReadOnlyPartError('part is being generated')
720 720 if name in self._seenparams:
721 721 raise ValueError('duplicated params: %s' % name)
722 722 self._seenparams.add(name)
723 723 params = self._advisoryparams
724 724 if mandatory:
725 725 params = self._mandatoryparams
726 726 params.append((name, value))
727 727
728 728 # methods used to generates the bundle2 stream
729 def getchunks(self):
729 def getchunks(self, ui):
730 730 if self._generated is not None:
731 731 raise RuntimeError('part can only be consumed once')
732 732 self._generated = False
733 733 #### header
734 734 if self.mandatory:
735 735 parttype = self.type.upper()
736 736 else:
737 737 parttype = self.type.lower()
738 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
738 739 ## parttype
739 740 header = [_pack(_fparttypesize, len(parttype)),
740 741 parttype, _pack(_fpartid, self.id),
741 742 ]
742 743 ## parameters
743 744 # count
744 745 manpar = self.mandatoryparams
745 746 advpar = self.advisoryparams
746 747 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
747 748 # size
748 749 parsizes = []
749 750 for key, value in manpar:
750 751 parsizes.append(len(key))
751 752 parsizes.append(len(value))
752 753 for key, value in advpar:
753 754 parsizes.append(len(key))
754 755 parsizes.append(len(value))
755 756 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
756 757 header.append(paramsizes)
757 758 # key, value
758 759 for key, value in manpar:
759 760 header.append(key)
760 761 header.append(value)
761 762 for key, value in advpar:
762 763 header.append(key)
763 764 header.append(value)
764 765 ## finalize header
765 766 headerchunk = ''.join(header)
767 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
766 768 yield _pack(_fpartheadersize, len(headerchunk))
767 769 yield headerchunk
768 770 ## payload
769 771 try:
770 772 for chunk in self._payloadchunks():
773 outdebug(ui, 'payload chunk size: %i' % len(chunk))
771 774 yield _pack(_fpayloadsize, len(chunk))
772 775 yield chunk
773 776 except BaseException, exc:
774 777 # backup exception data for later
775 778 exc_info = sys.exc_info()
776 779 msg = 'unexpected error: %s' % exc
777 780 interpart = bundlepart('error:abort', [('message', msg)],
778 781 mandatory=False)
779 782 interpart.id = 0
780 783 yield _pack(_fpayloadsize, -1)
781 for chunk in interpart.getchunks():
784 for chunk in interpart.getchunks(ui=ui):
782 785 yield chunk
786 outdebug(ui, 'closing payload chunk')
783 787 # abort current part payload
784 788 yield _pack(_fpayloadsize, 0)
785 789 raise exc_info[0], exc_info[1], exc_info[2]
786 790 # end of payload
791 outdebug(ui, 'closing payload chunk')
787 792 yield _pack(_fpayloadsize, 0)
788 793 self._generated = True
789 794
790 795 def _payloadchunks(self):
791 796 """yield chunks of a the part payload
792 797
793 798 Exists to handle the different methods to provide data to a part."""
794 799 # we only support fixed size data now.
795 800 # This will be improved in the future.
796 801 if util.safehasattr(self.data, 'next'):
797 802 buff = util.chunkbuffer(self.data)
798 803 chunk = buff.read(preferedchunksize)
799 804 while chunk:
800 805 yield chunk
801 806 chunk = buff.read(preferedchunksize)
802 807 elif len(self.data):
803 808 yield self.data
804 809
805 810
806 811 flaginterrupt = -1
807 812
808 813 class interrupthandler(unpackermixin):
809 814 """read one part and process it with restricted capability
810 815
811 816 This allows to transmit exception raised on the producer size during part
812 817 iteration while the consumer is reading a part.
813 818
814 819 Part processed in this manner only have access to a ui object,"""
815 820
816 821 def __init__(self, ui, fp):
817 822 super(interrupthandler, self).__init__(fp)
818 823 self.ui = ui
819 824
820 825 def _readpartheader(self):
821 826 """reads a part header size and return the bytes blob
822 827
823 828 returns None if empty"""
824 829 headersize = self._unpack(_fpartheadersize)[0]
825 830 if headersize < 0:
826 831 raise error.BundleValueError('negative part header size: %i'
827 832 % headersize)
828 833 indebug(self.ui, 'part header size: %i\n' % headersize)
829 834 if headersize:
830 835 return self._readexact(headersize)
831 836 return None
832 837
833 838 def __call__(self):
834 839 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
835 840 headerblock = self._readpartheader()
836 841 if headerblock is None:
837 842 indebug(self.ui, 'no part found during interruption.')
838 843 return
839 844 part = unbundlepart(self.ui, headerblock, self._fp)
840 845 op = interruptoperation(self.ui)
841 846 _processpart(op, part)
842 847
843 848 class interruptoperation(object):
844 849 """A limited operation to be use by part handler during interruption
845 850
846 851 It only have access to an ui object.
847 852 """
848 853
849 854 def __init__(self, ui):
850 855 self.ui = ui
851 856 self.reply = None
852 857 self.captureoutput = False
853 858
854 859 @property
855 860 def repo(self):
856 861 raise RuntimeError('no repo access from stream interruption')
857 862
858 863 def gettransaction(self):
859 864 raise TransactionUnavailable('no repo access from stream interruption')
860 865
861 866 class unbundlepart(unpackermixin):
862 867 """a bundle part read from a bundle"""
863 868
864 869 def __init__(self, ui, header, fp):
865 870 super(unbundlepart, self).__init__(fp)
866 871 self.ui = ui
867 872 # unbundle state attr
868 873 self._headerdata = header
869 874 self._headeroffset = 0
870 875 self._initialized = False
871 876 self.consumed = False
872 877 # part data
873 878 self.id = None
874 879 self.type = None
875 880 self.mandatoryparams = None
876 881 self.advisoryparams = None
877 882 self.params = None
878 883 self.mandatorykeys = ()
879 884 self._payloadstream = None
880 885 self._readheader()
881 886 self._mandatory = None
882 887 self._chunkindex = [] #(payload, file) position tuples for chunk starts
883 888 self._pos = 0
884 889
885 890 def _fromheader(self, size):
886 891 """return the next <size> byte from the header"""
887 892 offset = self._headeroffset
888 893 data = self._headerdata[offset:(offset + size)]
889 894 self._headeroffset = offset + size
890 895 return data
891 896
892 897 def _unpackheader(self, format):
893 898 """read given format from header
894 899
895 900 This automatically compute the size of the format to read."""
896 901 data = self._fromheader(struct.calcsize(format))
897 902 return _unpack(format, data)
898 903
899 904 def _initparams(self, mandatoryparams, advisoryparams):
900 905 """internal function to setup all logic related parameters"""
901 906 # make it read only to prevent people touching it by mistake.
902 907 self.mandatoryparams = tuple(mandatoryparams)
903 908 self.advisoryparams = tuple(advisoryparams)
904 909 # user friendly UI
905 910 self.params = dict(self.mandatoryparams)
906 911 self.params.update(dict(self.advisoryparams))
907 912 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
908 913
909 914 def _payloadchunks(self, chunknum=0):
910 915 '''seek to specified chunk and start yielding data'''
911 916 if len(self._chunkindex) == 0:
912 917 assert chunknum == 0, 'Must start with chunk 0'
913 918 self._chunkindex.append((0, super(unbundlepart, self).tell()))
914 919 else:
915 920 assert chunknum < len(self._chunkindex), \
916 921 'Unknown chunk %d' % chunknum
917 922 super(unbundlepart, self).seek(self._chunkindex[chunknum][1])
918 923
919 924 pos = self._chunkindex[chunknum][0]
920 925 payloadsize = self._unpack(_fpayloadsize)[0]
921 926 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
922 927 while payloadsize:
923 928 if payloadsize == flaginterrupt:
924 929 # interruption detection, the handler will now read a
925 930 # single part and process it.
926 931 interrupthandler(self.ui, self._fp)()
927 932 elif payloadsize < 0:
928 933 msg = 'negative payload chunk size: %i' % payloadsize
929 934 raise error.BundleValueError(msg)
930 935 else:
931 936 result = self._readexact(payloadsize)
932 937 chunknum += 1
933 938 pos += payloadsize
934 939 if chunknum == len(self._chunkindex):
935 940 self._chunkindex.append((pos,
936 941 super(unbundlepart, self).tell()))
937 942 yield result
938 943 payloadsize = self._unpack(_fpayloadsize)[0]
939 944 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
940 945
941 946 def _findchunk(self, pos):
942 947 '''for a given payload position, return a chunk number and offset'''
943 948 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
944 949 if ppos == pos:
945 950 return chunk, 0
946 951 elif ppos > pos:
947 952 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
948 953 raise ValueError('Unknown chunk')
949 954
950 955 def _readheader(self):
951 956 """read the header and setup the object"""
952 957 typesize = self._unpackheader(_fparttypesize)[0]
953 958 self.type = self._fromheader(typesize)
954 959 indebug(self.ui, 'part type: "%s"' % self.type)
955 960 self.id = self._unpackheader(_fpartid)[0]
956 961 indebug(self.ui, 'part id: "%s"' % self.id)
957 962 # extract mandatory bit from type
958 963 self.mandatory = (self.type != self.type.lower())
959 964 self.type = self.type.lower()
960 965 ## reading parameters
961 966 # param count
962 967 mancount, advcount = self._unpackheader(_fpartparamcount)
963 968 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
964 969 # param size
965 970 fparamsizes = _makefpartparamsizes(mancount + advcount)
966 971 paramsizes = self._unpackheader(fparamsizes)
967 972 # make it a list of couple again
968 973 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
969 974 # split mandatory from advisory
970 975 mansizes = paramsizes[:mancount]
971 976 advsizes = paramsizes[mancount:]
972 977 # retrieve param value
973 978 manparams = []
974 979 for key, value in mansizes:
975 980 manparams.append((self._fromheader(key), self._fromheader(value)))
976 981 advparams = []
977 982 for key, value in advsizes:
978 983 advparams.append((self._fromheader(key), self._fromheader(value)))
979 984 self._initparams(manparams, advparams)
980 985 ## part payload
981 986 self._payloadstream = util.chunkbuffer(self._payloadchunks())
982 987 # we read the data, tell it
983 988 self._initialized = True
984 989
985 990 def read(self, size=None):
986 991 """read payload data"""
987 992 if not self._initialized:
988 993 self._readheader()
989 994 if size is None:
990 995 data = self._payloadstream.read()
991 996 else:
992 997 data = self._payloadstream.read(size)
993 998 if size is None or len(data) < size:
994 999 self.consumed = True
995 1000 self._pos += len(data)
996 1001 return data
997 1002
998 1003 def tell(self):
999 1004 return self._pos
1000 1005
1001 1006 def seek(self, offset, whence=0):
1002 1007 if whence == 0:
1003 1008 newpos = offset
1004 1009 elif whence == 1:
1005 1010 newpos = self._pos + offset
1006 1011 elif whence == 2:
1007 1012 if not self.consumed:
1008 1013 self.read()
1009 1014 newpos = self._chunkindex[-1][0] - offset
1010 1015 else:
1011 1016 raise ValueError('Unknown whence value: %r' % (whence,))
1012 1017
1013 1018 if newpos > self._chunkindex[-1][0] and not self.consumed:
1014 1019 self.read()
1015 1020 if not 0 <= newpos <= self._chunkindex[-1][0]:
1016 1021 raise ValueError('Offset out of range')
1017 1022
1018 1023 if self._pos != newpos:
1019 1024 chunk, internaloffset = self._findchunk(newpos)
1020 1025 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1021 1026 adjust = self.read(internaloffset)
1022 1027 if len(adjust) != internaloffset:
1023 1028 raise util.Abort(_('Seek failed\n'))
1024 1029 self._pos = newpos
1025 1030
1026 1031 # These are only the static capabilities.
1027 1032 # Check the 'getrepocaps' function for the rest.
1028 1033 capabilities = {'HG20': (),
1029 1034 'listkeys': (),
1030 1035 'pushkey': (),
1031 1036 'digests': tuple(sorted(util.DIGESTS.keys())),
1032 1037 'remote-changegroup': ('http', 'https'),
1033 1038 }
1034 1039
1035 1040 def getrepocaps(repo, allowpushback=False):
1036 1041 """return the bundle2 capabilities for a given repo
1037 1042
1038 1043 Exists to allow extensions (like evolution) to mutate the capabilities.
1039 1044 """
1040 1045 caps = capabilities.copy()
1041 1046 caps['changegroup'] = tuple(sorted(changegroup.packermap.keys()))
1042 1047 if obsolete.isenabled(repo, obsolete.exchangeopt):
1043 1048 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1044 1049 caps['obsmarkers'] = supportedformat
1045 1050 if allowpushback:
1046 1051 caps['pushback'] = ()
1047 1052 return caps
1048 1053
1049 1054 def bundle2caps(remote):
1050 1055 """return the bundle capabilities of a peer as dict"""
1051 1056 raw = remote.capable('bundle2')
1052 1057 if not raw and raw != '':
1053 1058 return {}
1054 1059 capsblob = urllib.unquote(remote.capable('bundle2'))
1055 1060 return decodecaps(capsblob)
1056 1061
1057 1062 def obsmarkersversion(caps):
1058 1063 """extract the list of supported obsmarkers versions from a bundle2caps dict
1059 1064 """
1060 1065 obscaps = caps.get('obsmarkers', ())
1061 1066 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1062 1067
1063 1068 @parthandler('changegroup', ('version',))
1064 1069 def handlechangegroup(op, inpart):
1065 1070 """apply a changegroup part on the repo
1066 1071
1067 1072 This is a very early implementation that will massive rework before being
1068 1073 inflicted to any end-user.
1069 1074 """
1070 1075 # Make sure we trigger a transaction creation
1071 1076 #
1072 1077 # The addchangegroup function will get a transaction object by itself, but
1073 1078 # we need to make sure we trigger the creation of a transaction object used
1074 1079 # for the whole processing scope.
1075 1080 op.gettransaction()
1076 1081 unpackerversion = inpart.params.get('version', '01')
1077 1082 # We should raise an appropriate exception here
1078 1083 unpacker = changegroup.packermap[unpackerversion][1]
1079 1084 cg = unpacker(inpart, 'UN')
1080 1085 # the source and url passed here are overwritten by the one contained in
1081 1086 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1082 1087 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
1083 1088 op.records.add('changegroup', {'return': ret})
1084 1089 if op.reply is not None:
1085 1090 # This is definitely not the final form of this
1086 1091 # return. But one need to start somewhere.
1087 1092 part = op.reply.newpart('reply:changegroup', mandatory=False)
1088 1093 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1089 1094 part.addparam('return', '%i' % ret, mandatory=False)
1090 1095 assert not inpart.read()
1091 1096
1092 1097 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1093 1098 ['digest:%s' % k for k in util.DIGESTS.keys()])
1094 1099 @parthandler('remote-changegroup', _remotechangegroupparams)
1095 1100 def handleremotechangegroup(op, inpart):
1096 1101 """apply a bundle10 on the repo, given an url and validation information
1097 1102
1098 1103 All the information about the remote bundle to import are given as
1099 1104 parameters. The parameters include:
1100 1105 - url: the url to the bundle10.
1101 1106 - size: the bundle10 file size. It is used to validate what was
1102 1107 retrieved by the client matches the server knowledge about the bundle.
1103 1108 - digests: a space separated list of the digest types provided as
1104 1109 parameters.
1105 1110 - digest:<digest-type>: the hexadecimal representation of the digest with
1106 1111 that name. Like the size, it is used to validate what was retrieved by
1107 1112 the client matches what the server knows about the bundle.
1108 1113
1109 1114 When multiple digest types are given, all of them are checked.
1110 1115 """
1111 1116 try:
1112 1117 raw_url = inpart.params['url']
1113 1118 except KeyError:
1114 1119 raise util.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1115 1120 parsed_url = util.url(raw_url)
1116 1121 if parsed_url.scheme not in capabilities['remote-changegroup']:
1117 1122 raise util.Abort(_('remote-changegroup does not support %s urls') %
1118 1123 parsed_url.scheme)
1119 1124
1120 1125 try:
1121 1126 size = int(inpart.params['size'])
1122 1127 except ValueError:
1123 1128 raise util.Abort(_('remote-changegroup: invalid value for param "%s"')
1124 1129 % 'size')
1125 1130 except KeyError:
1126 1131 raise util.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1127 1132
1128 1133 digests = {}
1129 1134 for typ in inpart.params.get('digests', '').split():
1130 1135 param = 'digest:%s' % typ
1131 1136 try:
1132 1137 value = inpart.params[param]
1133 1138 except KeyError:
1134 1139 raise util.Abort(_('remote-changegroup: missing "%s" param') %
1135 1140 param)
1136 1141 digests[typ] = value
1137 1142
1138 1143 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1139 1144
1140 1145 # Make sure we trigger a transaction creation
1141 1146 #
1142 1147 # The addchangegroup function will get a transaction object by itself, but
1143 1148 # we need to make sure we trigger the creation of a transaction object used
1144 1149 # for the whole processing scope.
1145 1150 op.gettransaction()
1146 1151 import exchange
1147 1152 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1148 1153 if not isinstance(cg, changegroup.cg1unpacker):
1149 1154 raise util.Abort(_('%s: not a bundle version 1.0') %
1150 1155 util.hidepassword(raw_url))
1151 1156 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
1152 1157 op.records.add('changegroup', {'return': ret})
1153 1158 if op.reply is not None:
1154 1159 # This is definitely not the final form of this
1155 1160 # return. But one need to start somewhere.
1156 1161 part = op.reply.newpart('reply:changegroup')
1157 1162 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1158 1163 part.addparam('return', '%i' % ret, mandatory=False)
1159 1164 try:
1160 1165 real_part.validate()
1161 1166 except util.Abort, e:
1162 1167 raise util.Abort(_('bundle at %s is corrupted:\n%s') %
1163 1168 (util.hidepassword(raw_url), str(e)))
1164 1169 assert not inpart.read()
1165 1170
1166 1171 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1167 1172 def handlereplychangegroup(op, inpart):
1168 1173 ret = int(inpart.params['return'])
1169 1174 replyto = int(inpart.params['in-reply-to'])
1170 1175 op.records.add('changegroup', {'return': ret}, replyto)
1171 1176
1172 1177 @parthandler('check:heads')
1173 1178 def handlecheckheads(op, inpart):
1174 1179 """check that head of the repo did not change
1175 1180
1176 1181 This is used to detect a push race when using unbundle.
1177 1182 This replaces the "heads" argument of unbundle."""
1178 1183 h = inpart.read(20)
1179 1184 heads = []
1180 1185 while len(h) == 20:
1181 1186 heads.append(h)
1182 1187 h = inpart.read(20)
1183 1188 assert not h
1184 1189 if heads != op.repo.heads():
1185 1190 raise error.PushRaced('repository changed while pushing - '
1186 1191 'please try again')
1187 1192
1188 1193 @parthandler('output')
1189 1194 def handleoutput(op, inpart):
1190 1195 """forward output captured on the server to the client"""
1191 1196 for line in inpart.read().splitlines():
1192 1197 op.ui.status(('remote: %s\n' % line))
1193 1198
1194 1199 @parthandler('replycaps')
1195 1200 def handlereplycaps(op, inpart):
1196 1201 """Notify that a reply bundle should be created
1197 1202
1198 1203 The payload contains the capabilities information for the reply"""
1199 1204 caps = decodecaps(inpart.read())
1200 1205 if op.reply is None:
1201 1206 op.reply = bundle20(op.ui, caps)
1202 1207
1203 1208 @parthandler('error:abort', ('message', 'hint'))
1204 1209 def handleerrorabort(op, inpart):
1205 1210 """Used to transmit abort error over the wire"""
1206 1211 raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint'))
1207 1212
1208 1213 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1209 1214 def handleerrorunsupportedcontent(op, inpart):
1210 1215 """Used to transmit unknown content error over the wire"""
1211 1216 kwargs = {}
1212 1217 parttype = inpart.params.get('parttype')
1213 1218 if parttype is not None:
1214 1219 kwargs['parttype'] = parttype
1215 1220 params = inpart.params.get('params')
1216 1221 if params is not None:
1217 1222 kwargs['params'] = params.split('\0')
1218 1223
1219 1224 raise error.UnsupportedPartError(**kwargs)
1220 1225
1221 1226 @parthandler('error:pushraced', ('message',))
1222 1227 def handleerrorpushraced(op, inpart):
1223 1228 """Used to transmit push race error over the wire"""
1224 1229 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1225 1230
1226 1231 @parthandler('listkeys', ('namespace',))
1227 1232 def handlelistkeys(op, inpart):
1228 1233 """retrieve pushkey namespace content stored in a bundle2"""
1229 1234 namespace = inpart.params['namespace']
1230 1235 r = pushkey.decodekeys(inpart.read())
1231 1236 op.records.add('listkeys', (namespace, r))
1232 1237
1233 1238 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1234 1239 def handlepushkey(op, inpart):
1235 1240 """process a pushkey request"""
1236 1241 dec = pushkey.decode
1237 1242 namespace = dec(inpart.params['namespace'])
1238 1243 key = dec(inpart.params['key'])
1239 1244 old = dec(inpart.params['old'])
1240 1245 new = dec(inpart.params['new'])
1241 1246 ret = op.repo.pushkey(namespace, key, old, new)
1242 1247 record = {'namespace': namespace,
1243 1248 'key': key,
1244 1249 'old': old,
1245 1250 'new': new}
1246 1251 op.records.add('pushkey', record)
1247 1252 if op.reply is not None:
1248 1253 rpart = op.reply.newpart('reply:pushkey')
1249 1254 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1250 1255 rpart.addparam('return', '%i' % ret, mandatory=False)
1251 1256
1252 1257 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1253 1258 def handlepushkeyreply(op, inpart):
1254 1259 """retrieve the result of a pushkey request"""
1255 1260 ret = int(inpart.params['return'])
1256 1261 partid = int(inpart.params['in-reply-to'])
1257 1262 op.records.add('pushkey', {'return': ret}, partid)
1258 1263
1259 1264 @parthandler('obsmarkers')
1260 1265 def handleobsmarker(op, inpart):
1261 1266 """add a stream of obsmarkers to the repo"""
1262 1267 tr = op.gettransaction()
1263 1268 markerdata = inpart.read()
1264 1269 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1265 1270 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1266 1271 % len(markerdata))
1267 1272 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1268 1273 if new:
1269 1274 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1270 1275 op.records.add('obsmarkers', {'new': new})
1271 1276 if op.reply is not None:
1272 1277 rpart = op.reply.newpart('reply:obsmarkers')
1273 1278 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1274 1279 rpart.addparam('new', '%i' % new, mandatory=False)
1275 1280
1276 1281
1277 1282 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1278 1283 def handlepushkeyreply(op, inpart):
1279 1284 """retrieve the result of a pushkey request"""
1280 1285 ret = int(inpart.params['new'])
1281 1286 partid = int(inpart.params['in-reply-to'])
1282 1287 op.records.add('obsmarkers', {'new': ret}, partid)
@@ -1,803 +1,830
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: start emission of HG20 stream
341 341 bundle2-output: bundle parameter: e%7C%21%207/=babar%25%23%3D%3Dtutu simple
342 342 bundle2-output: start of parts
343 343 bundle2-output: end of bundle
344 344
345 345 file content is ok
346 346
347 347 $ cat ../out.hg2
348 348 HG20\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc)
349 349
350 350 unbundling debug
351 351
352 352 $ hg statbundle2 --debug --config progress.debug=true < ../out.hg2
353 353 bundle2-input: start processing of HG20 stream
354 354 bundle2-input: reading bundle2 stream parameters
355 355 bundle2-input: ignoring unknown parameter 'e|! 7/'
356 356 bundle2-input: ignoring unknown parameter 'simple'
357 357 options count: 2
358 358 - e|! 7/
359 359 babar%#==tutu
360 360 - simple
361 361 bundle2-input: start extraction of bundle2 parts
362 362 bundle2-input: part header size: 0
363 363 bundle2-input: end of bundle2 stream
364 364 parts count: 0
365 365
366 366
367 367 Test buggy input
368 368 ---------------------------------------------------
369 369
370 370 empty parameter name
371 371
372 372 $ hg bundle2 --param '' --quiet
373 373 abort: empty parameter name
374 374 [255]
375 375
376 376 bad parameter name
377 377
378 378 $ hg bundle2 --param 42babar
379 379 abort: non letter first character: '42babar'
380 380 [255]
381 381
382 382
383 383 Test part
384 384 =================
385 385
386 386 $ hg bundle2 --parts ../parts.hg2 --debug --config progress.debug=true
387 387 bundle2-output: start emission of HG20 stream
388 388 bundle2-output: bundle parameter:
389 389 bundle2-output: start of parts
390 390 bundle2-output: bundle part: "test:empty"
391 bundle2-output: part 0: "test:empty"
392 bundle2-output: header chunk size: 17
393 bundle2-output: closing payload chunk
391 394 bundle2-output: bundle part: "test:empty"
395 bundle2-output: part 1: "test:empty"
396 bundle2-output: header chunk size: 17
397 bundle2-output: closing payload chunk
392 398 bundle2-output: bundle part: "test:song"
399 bundle2-output: part 2: "test:song"
400 bundle2-output: header chunk size: 16
401 bundle2-output: payload chunk size: 178
402 bundle2-output: closing payload chunk
393 403 bundle2-output: bundle part: "test:debugreply"
404 bundle2-output: part 3: "test:debugreply"
405 bundle2-output: header chunk size: 22
406 bundle2-output: closing payload chunk
394 407 bundle2-output: bundle part: "test:math"
408 bundle2-output: part 4: "test:math"
409 bundle2-output: header chunk size: 43
410 bundle2-output: payload chunk size: 2
411 bundle2-output: closing payload chunk
395 412 bundle2-output: bundle part: "test:song"
413 bundle2-output: part 5: "test:song"
414 bundle2-output: header chunk size: 29
415 bundle2-output: closing payload chunk
396 416 bundle2-output: bundle part: "test:ping"
417 bundle2-output: part 6: "test:ping"
418 bundle2-output: header chunk size: 16
419 bundle2-output: closing payload chunk
397 420 bundle2-output: end of bundle
398 421
399 422 $ cat ../parts.hg2
400 423 HG20\x00\x00\x00\x00\x00\x00\x00\x11 (esc)
401 424 test:empty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11 (esc)
402 425 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)
403 426 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
404 427 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)
405 428
406 429
407 430 $ hg statbundle2 < ../parts.hg2
408 431 options count: 0
409 432 :test:empty:
410 433 mandatory: 0
411 434 advisory: 0
412 435 payload: 0 bytes
413 436 :test:empty:
414 437 mandatory: 0
415 438 advisory: 0
416 439 payload: 0 bytes
417 440 :test:song:
418 441 mandatory: 0
419 442 advisory: 0
420 443 payload: 178 bytes
421 444 :test:debugreply:
422 445 mandatory: 0
423 446 advisory: 0
424 447 payload: 0 bytes
425 448 :test:math:
426 449 mandatory: 2
427 450 advisory: 1
428 451 payload: 2 bytes
429 452 :test:song:
430 453 mandatory: 1
431 454 advisory: 0
432 455 payload: 0 bytes
433 456 :test:ping:
434 457 mandatory: 0
435 458 advisory: 0
436 459 payload: 0 bytes
437 460 parts count: 7
438 461
439 462 $ hg statbundle2 --debug --config progress.debug=true < ../parts.hg2
440 463 bundle2-input: start processing of HG20 stream
441 464 bundle2-input: reading bundle2 stream parameters
442 465 options count: 0
443 466 bundle2-input: start extraction of bundle2 parts
444 467 bundle2-input: part header size: 17
445 468 bundle2-input: part type: "test:empty"
446 469 bundle2-input: part id: "0"
447 470 bundle2-input: part parameters: 0
448 471 :test:empty:
449 472 mandatory: 0
450 473 advisory: 0
451 474 bundle2-input: payload chunk size: 0
452 475 payload: 0 bytes
453 476 bundle2-input: part header size: 17
454 477 bundle2-input: part type: "test:empty"
455 478 bundle2-input: part id: "1"
456 479 bundle2-input: part parameters: 0
457 480 :test:empty:
458 481 mandatory: 0
459 482 advisory: 0
460 483 bundle2-input: payload chunk size: 0
461 484 payload: 0 bytes
462 485 bundle2-input: part header size: 16
463 486 bundle2-input: part type: "test:song"
464 487 bundle2-input: part id: "2"
465 488 bundle2-input: part parameters: 0
466 489 :test:song:
467 490 mandatory: 0
468 491 advisory: 0
469 492 bundle2-input: payload chunk size: 178
470 493 bundle2-input: payload chunk size: 0
471 494 payload: 178 bytes
472 495 bundle2-input: part header size: 22
473 496 bundle2-input: part type: "test:debugreply"
474 497 bundle2-input: part id: "3"
475 498 bundle2-input: part parameters: 0
476 499 :test:debugreply:
477 500 mandatory: 0
478 501 advisory: 0
479 502 bundle2-input: payload chunk size: 0
480 503 payload: 0 bytes
481 504 bundle2-input: part header size: 43
482 505 bundle2-input: part type: "test:math"
483 506 bundle2-input: part id: "4"
484 507 bundle2-input: part parameters: 3
485 508 :test:math:
486 509 mandatory: 2
487 510 advisory: 1
488 511 bundle2-input: payload chunk size: 2
489 512 bundle2-input: payload chunk size: 0
490 513 payload: 2 bytes
491 514 bundle2-input: part header size: 29
492 515 bundle2-input: part type: "test:song"
493 516 bundle2-input: part id: "5"
494 517 bundle2-input: part parameters: 1
495 518 :test:song:
496 519 mandatory: 1
497 520 advisory: 0
498 521 bundle2-input: payload chunk size: 0
499 522 payload: 0 bytes
500 523 bundle2-input: part header size: 16
501 524 bundle2-input: part type: "test:ping"
502 525 bundle2-input: part id: "6"
503 526 bundle2-input: part parameters: 0
504 527 :test:ping:
505 528 mandatory: 0
506 529 advisory: 0
507 530 bundle2-input: payload chunk size: 0
508 531 payload: 0 bytes
509 532 bundle2-input: part header size: 0
510 533 bundle2-input: end of bundle2 stream
511 534 parts count: 7
512 535
513 536 Test actual unbundling of test part
514 537 =======================================
515 538
516 539 Process the bundle
517 540
518 541 $ hg unbundle2 --debug --config progress.debug=true < ../parts.hg2
519 542 bundle2-input: start processing of HG20 stream
520 543 bundle2-input: reading bundle2 stream parameters
521 544 bundle2-input: start extraction of bundle2 parts
522 545 bundle2-input: part header size: 17
523 546 bundle2-input: part type: "test:empty"
524 547 bundle2-input: part id: "0"
525 548 bundle2-input: part parameters: 0
526 549 bundle2-input: ignoring unsupported advisory part test:empty
527 550 bundle2-input: payload chunk size: 0
528 551 bundle2-input: part header size: 17
529 552 bundle2-input: part type: "test:empty"
530 553 bundle2-input: part id: "1"
531 554 bundle2-input: part parameters: 0
532 555 bundle2-input: ignoring unsupported advisory part test:empty
533 556 bundle2-input: payload chunk size: 0
534 557 bundle2-input: part header size: 16
535 558 bundle2-input: part type: "test:song"
536 559 bundle2-input: part id: "2"
537 560 bundle2-input: part parameters: 0
538 561 bundle2-input: found a handler for part 'test:song'
539 562 The choir starts singing:
540 563 bundle2-input: payload chunk size: 178
541 564 bundle2-input: payload chunk size: 0
542 565 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
543 566 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
544 567 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
545 568 bundle2-input: part header size: 22
546 569 bundle2-input: part type: "test:debugreply"
547 570 bundle2-input: part id: "3"
548 571 bundle2-input: part parameters: 0
549 572 bundle2-input: found a handler for part 'test:debugreply'
550 573 debugreply: no reply
551 574 bundle2-input: payload chunk size: 0
552 575 bundle2-input: part header size: 43
553 576 bundle2-input: part type: "test:math"
554 577 bundle2-input: part id: "4"
555 578 bundle2-input: part parameters: 3
556 579 bundle2-input: ignoring unsupported advisory part test:math
557 580 bundle2-input: payload chunk size: 2
558 581 bundle2-input: payload chunk size: 0
559 582 bundle2-input: part header size: 29
560 583 bundle2-input: part type: "test:song"
561 584 bundle2-input: part id: "5"
562 585 bundle2-input: part parameters: 1
563 586 bundle2-input: found a handler for part 'test:song'
564 587 bundle2-input: ignoring unsupported advisory part test:song - randomparam
565 588 bundle2-input: payload chunk size: 0
566 589 bundle2-input: part header size: 16
567 590 bundle2-input: part type: "test:ping"
568 591 bundle2-input: part id: "6"
569 592 bundle2-input: part parameters: 0
570 593 bundle2-input: found a handler for part 'test:ping'
571 594 received ping request (id 6)
572 595 bundle2-input: payload chunk size: 0
573 596 bundle2-input: part header size: 0
574 597 bundle2-input: end of bundle2 stream
575 598 0 unread bytes
576 599 3 total verses sung
577 600
578 601 Unbundle with an unknown mandatory part
579 602 (should abort)
580 603
581 604 $ hg bundle2 --parts --unknown ../unknown.hg2
582 605
583 606 $ hg unbundle2 < ../unknown.hg2
584 607 The choir starts singing:
585 608 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
586 609 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
587 610 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
588 611 debugreply: no reply
589 612 0 unread bytes
590 613 abort: missing support for test:unknown
591 614 [255]
592 615
593 616 Unbundle with an unknown mandatory part parameters
594 617 (should abort)
595 618
596 619 $ hg bundle2 --unknownparams ../unknown.hg2
597 620
598 621 $ hg unbundle2 < ../unknown.hg2
599 622 0 unread bytes
600 623 abort: missing support for test:song - randomparams
601 624 [255]
602 625
603 626 unbundle with a reply
604 627
605 628 $ hg bundle2 --parts --reply ../parts-reply.hg2
606 629 $ hg unbundle2 ../reply.hg2 < ../parts-reply.hg2
607 630 0 unread bytes
608 631 3 total verses sung
609 632
610 633 The reply is a bundle
611 634
612 635 $ cat ../reply.hg2
613 636 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)
614 637 Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
615 638 Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
616 639 Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
617 640 \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)
618 641 debugreply: 'city=!'
619 642 debugreply: 'celeste,ville'
620 643 debugreply: 'elephants'
621 644 debugreply: 'babar'
622 645 debugreply: 'celeste'
623 646 debugreply: 'ping-pong'
624 647 \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)
625 648 replying to ping request (id 7)
626 649 \x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
627 650
628 651 The reply is valid
629 652
630 653 $ hg statbundle2 < ../reply.hg2
631 654 options count: 0
632 655 :output:
633 656 mandatory: 0
634 657 advisory: 1
635 658 payload: 217 bytes
636 659 :output:
637 660 mandatory: 0
638 661 advisory: 1
639 662 payload: 201 bytes
640 663 :test:pong:
641 664 mandatory: 1
642 665 advisory: 0
643 666 payload: 0 bytes
644 667 :output:
645 668 mandatory: 0
646 669 advisory: 1
647 670 payload: 61 bytes
648 671 parts count: 4
649 672
650 673 Unbundle the reply to get the output:
651 674
652 675 $ hg unbundle2 < ../reply.hg2
653 676 remote: The choir starts singing:
654 677 remote: Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko
655 678 remote: Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko
656 679 remote: Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko.
657 680 remote: debugreply: capabilities:
658 681 remote: debugreply: 'city=!'
659 682 remote: debugreply: 'celeste,ville'
660 683 remote: debugreply: 'elephants'
661 684 remote: debugreply: 'babar'
662 685 remote: debugreply: 'celeste'
663 686 remote: debugreply: 'ping-pong'
664 687 remote: received ping request (id 7)
665 688 remote: replying to ping request (id 7)
666 689 0 unread bytes
667 690
668 691 Test push race detection
669 692
670 693 $ hg bundle2 --pushrace ../part-race.hg2
671 694
672 695 $ hg unbundle2 < ../part-race.hg2
673 696 0 unread bytes
674 697 abort: push race: repository changed while pushing - please try again
675 698 [255]
676 699
677 700 Support for changegroup
678 701 ===================================
679 702
680 703 $ hg unbundle $TESTDIR/bundles/rebase.hg
681 704 adding changesets
682 705 adding manifests
683 706 adding file changes
684 707 added 8 changesets with 7 changes to 7 files (+3 heads)
685 708 (run 'hg heads' to see heads, 'hg merge' to merge)
686 709
687 710 $ hg log -G
688 711 o 8:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> H
689 712 |
690 713 | o 7:eea13746799a draft Nicolas Dumazet <nicdumz.commits@gmail.com> G
691 714 |/|
692 715 o | 6:24b6387c8c8c draft Nicolas Dumazet <nicdumz.commits@gmail.com> F
693 716 | |
694 717 | o 5:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
695 718 |/
696 719 | o 4:32af7686d403 draft Nicolas Dumazet <nicdumz.commits@gmail.com> D
697 720 | |
698 721 | o 3:5fddd98957c8 draft Nicolas Dumazet <nicdumz.commits@gmail.com> C
699 722 | |
700 723 | o 2:42ccdea3bb16 draft Nicolas Dumazet <nicdumz.commits@gmail.com> B
701 724 |/
702 725 o 1:cd010b8cd998 draft Nicolas Dumazet <nicdumz.commits@gmail.com> A
703 726
704 727 @ 0:3903775176ed draft test a
705 728
706 729
707 730 $ hg bundle2 --debug --config progress.debug=true --rev '8+7+5+4' ../rev.hg2
708 731 4 changesets found
709 732 list of changesets:
710 733 32af7686d403cf45b5d95f2d70cebea587ac806a
711 734 9520eea781bcca16c1e15acc0ba14335a0e8e5ba
712 735 eea13746799a9e0bfd88f29d3c2e9dc9389f524f
713 736 02de42196ebee42ef284b6780a87cdc96e8eaab6
714 737 bundle2-output: start emission of HG20 stream
715 738 bundle2-output: bundle parameter:
716 739 bundle2-output: start of parts
717 740 bundle2-output: bundle part: "changegroup"
741 bundle2-output: part 0: "changegroup"
742 bundle2-output: header chunk size: 18
718 743 bundling: 1/4 changesets (25.00%)
719 744 bundling: 2/4 changesets (50.00%)
720 745 bundling: 3/4 changesets (75.00%)
721 746 bundling: 4/4 changesets (100.00%)
722 747 bundling: 1/4 manifests (25.00%)
723 748 bundling: 2/4 manifests (50.00%)
724 749 bundling: 3/4 manifests (75.00%)
725 750 bundling: 4/4 manifests (100.00%)
726 751 bundling: D 1/3 files (33.33%)
727 752 bundling: E 2/3 files (66.67%)
728 753 bundling: H 3/3 files (100.00%)
754 bundle2-output: payload chunk size: 1555
755 bundle2-output: closing payload chunk
729 756 bundle2-output: end of bundle
730 757
731 758 $ cat ../rev.hg2
732 759 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)
733 760 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02D (esc)
734 761 \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)
735 762 \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02E (esc)
736 763 \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)
737 764 \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)
738 765 \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)
739 766 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)8bee48edc7318541fc0013ee41b089276a8c24bf (esc)
740 767 \x00\x00\x00f\x00\x00\x00f\x00\x00\x00\x02H (esc)
741 768 \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)
742 769 \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)
743 770 \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)
744 771 \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)
745 772 \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)
746 773 \x00\x00\x00\x00\x00\x00\x00\x05D\x00\x00\x00b\xc3\xf1\xca)$\xc1j\x19\xb0ej\x84\x90\x0ePN[ (esc)
747 774 \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)
748 775 \x00\x00\x00\x00\x00\x00\x00\x05E\x00\x00\x00b\x9co\xd05 (esc)
749 776 l\r (no-eol) (esc)
750 777 \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)
751 778 \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)
752 779 \x87\xcd\xc9n\x8e\xaa\xb6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02H (esc)
753 780 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
754 781
755 782 $ hg debugbundle ../rev.hg2
756 783 Stream params: {}
757 784 changegroup -- '{}'
758 785 32af7686d403cf45b5d95f2d70cebea587ac806a
759 786 9520eea781bcca16c1e15acc0ba14335a0e8e5ba
760 787 eea13746799a9e0bfd88f29d3c2e9dc9389f524f
761 788 02de42196ebee42ef284b6780a87cdc96e8eaab6
762 789 $ hg unbundle ../rev.hg2
763 790 adding changesets
764 791 adding manifests
765 792 adding file changes
766 793 added 0 changesets with 0 changes to 3 files
767 794
768 795 with reply
769 796
770 797 $ hg bundle2 --rev '8+7+5+4' --reply ../rev-rr.hg2
771 798 $ hg unbundle2 ../rev-reply.hg2 < ../rev-rr.hg2
772 799 0 unread bytes
773 800 addchangegroup return: 1
774 801
775 802 $ cat ../rev-reply.hg2
776 803 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)
777 804 adding manifests
778 805 adding file changes
779 806 added 0 changesets with 0 changes to 3 files
780 807 \x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc)
781 808
782 809 Check handling of exception during generation.
783 810 ----------------------------------------------
784 811
785 812 $ hg bundle2 --genraise > ../genfailed.hg2
786 813 abort: Someone set up us the bomb!
787 814 [255]
788 815
789 816 Should still be a valid bundle
790 817
791 818 $ cat ../genfailed.hg2
792 819 HG20\x00\x00\x00\x00\x00\x00\x00\r (no-eol) (esc)
793 820 \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)
794 821
795 822 And its handling on the other size raise a clean exception
796 823
797 824 $ cat ../genfailed.hg2 | hg unbundle2
798 825 0 unread bytes
799 826 abort: unexpected error: Someone set up us the bomb!
800 827 [255]
801 828
802 829
803 830 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now