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