##// END OF EJS Templates
bundle2: client side support for a part to import external bundles...
Mike Hommey -
r23029:149fc8a4 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (592 lines changed) Show them Hide them
@@ -0,0 +1,592 b''
1 #require killdaemons
2
3 Create an extension to test bundle2 remote-changegroup parts
4
5 $ cat > bundle2.py << EOF
6 > """A small extension to test bundle2 remote-changegroup parts.
7 >
8 > Current bundle2 implementation doesn't provide a way to generate those
9 > parts, so they must be created by extensions.
10 > """
11 > from mercurial import bundle2, changegroup, exchange, util
12 >
13 > def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
14 > b2caps=None, heads=None, common=None,
15 > **kwargs):
16 > """this function replaces the changegroup part handler for getbundle.
17 > It allows to create a set of arbitrary parts containing changegroups
18 > and remote-changegroups, as described in a bundle2maker file in the
19 > repository .hg/ directory.
20 >
21 > Each line of that bundle2maker file contain a description of the
22 > part to add:
23 > - changegroup common_revset heads_revset
24 > Creates a changegroup part based, using common_revset and
25 > heads_revset for changegroup.getchangegroup.
26 > - remote-changegroup url file
27 > Creates a remote-changegroup part for a bundle at the given
28 > url. Size and digest, as required by the client, are computed
29 > from the given file.
30 > - raw-remote-changegroup <python expression>
31 > Creates a remote-changegroup part with the data given in the
32 > python expression as parameters. The python expression is
33 > evaluated with eval, and is expected to be a dict.
34 > """
35 > def newpart(name, data=''):
36 > """wrapper around bundler.newpart adding an extra part making the
37 > client output information about each processed part"""
38 > bundler.newpart('b2x:output', data=name)
39 > part = bundler.newpart(name, data=data)
40 > return part
41 >
42 > for line in open(repo.join('bundle2maker'), 'r'):
43 > line = line.strip()
44 > try:
45 > verb, args = line.split(None, 1)
46 > except ValueError:
47 > verb, args = line, ''
48 > if verb == 'remote-changegroup':
49 > url, file = args.split()
50 > bundledata = open(file, 'rb').read()
51 > digest = util.digester.preferred(b2caps['digests'])
52 > d = util.digester([digest], bundledata)
53 > part = newpart('B2X:REMOTE-CHANGEGROUP')
54 > part.addparam('url', url)
55 > part.addparam('size', str(len(bundledata)))
56 > part.addparam('digests', digest)
57 > part.addparam('digest:%s' % digest, d[digest])
58 > elif verb == 'raw-remote-changegroup':
59 > part = newpart('B2X:REMOTE-CHANGEGROUP')
60 > for k, v in eval(args).items():
61 > part.addparam(k, str(v))
62 > elif verb == 'changegroup':
63 > _common, heads = args.split()
64 > common.extend(repo.lookup(r) for r in repo.revs(_common))
65 > heads = [repo.lookup(r) for r in repo.revs(heads)]
66 > cg = changegroup.getchangegroup(repo, 'changegroup',
67 > heads=heads, common=common)
68 > newpart('B2X:CHANGEGROUP', cg.getchunks())
69 > else:
70 > raise Exception('unknown verb')
71 >
72 > exchange.getbundle2partsmapping['changegroup'] = _getbundlechangegrouppart
73 > EOF
74
75 Start a simple HTTP server to serve bundles
76
77 $ python "$TESTDIR/dumbhttp.py" -p $HGPORT --pid dumb.pid
78 $ cat dumb.pid >> $DAEMON_PIDS
79
80 $ cat >> $HGRCPATH << EOF
81 > [experimental]
82 > bundle2-exp=True
83 > [ui]
84 > ssh=python "$TESTDIR/dummyssh"
85 > logtemplate={rev}:{node|short} {phase} {author} {bookmarks} {desc|firstline}
86 > EOF
87
88 $ hg init repo
89
90 $ hg -R repo unbundle $TESTDIR/bundles/rebase.hg
91 adding changesets
92 adding manifests
93 adding file changes
94 added 8 changesets with 7 changes to 7 files (+2 heads)
95 (run 'hg heads' to see heads, 'hg merge' to merge)
96
97 $ hg -R repo log -G
98 o 7:02de42196ebe draft Nicolas Dumazet <nicdumz.commits@gmail.com> H
99 |
100 | o 6:eea13746799a draft Nicolas Dumazet <nicdumz.commits@gmail.com> G
101 |/|
102 o | 5:24b6387c8c8c draft Nicolas Dumazet <nicdumz.commits@gmail.com> F
103 | |
104 | o 4:9520eea781bc draft Nicolas Dumazet <nicdumz.commits@gmail.com> E
105 |/
106 | o 3:32af7686d403 draft Nicolas Dumazet <nicdumz.commits@gmail.com> D
107 | |
108 | o 2:5fddd98957c8 draft Nicolas Dumazet <nicdumz.commits@gmail.com> C
109 | |
110 | o 1:42ccdea3bb16 draft Nicolas Dumazet <nicdumz.commits@gmail.com> B
111 |/
112 o 0:cd010b8cd998 draft Nicolas Dumazet <nicdumz.commits@gmail.com> A
113
114 $ hg clone repo orig
115 updating to branch default
116 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
117
118 $ cat > repo/.hg/hgrc << EOF
119 > [extensions]
120 > bundle2=$TESTTMP/bundle2.py
121 > EOF
122
123 Test a pull with an remote-changegroup
124
125 $ hg bundle -R repo --base '0:4' -r '5:7' bundle.hg
126 3 changesets found
127 $ cat > repo/.hg/bundle2maker << EOF
128 > remote-changegroup http://localhost:$HGPORT/bundle.hg bundle.hg
129 > EOF
130 $ hg clone orig clone -r 3 -r 4
131 adding changesets
132 adding manifests
133 adding file changes
134 added 5 changesets with 5 changes to 5 files (+1 heads)
135 updating to branch default
136 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
137 $ hg pull -R clone ssh://user@dummy/repo
138 pulling from ssh://user@dummy/repo
139 searching for changes
140 remote: B2X:REMOTE-CHANGEGROUP
141 adding changesets
142 adding manifests
143 adding file changes
144 added 3 changesets with 2 changes to 2 files (+1 heads)
145 (run 'hg heads .' to see heads, 'hg merge' to merge)
146 $ hg -R clone log -G
147 o 7:02de42196ebe public Nicolas Dumazet <nicdumz.commits@gmail.com> H
148 |
149 | o 6:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
150 |/|
151 o | 5:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
152 | |
153 | o 4:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
154 |/
155 | @ 3:32af7686d403 public Nicolas Dumazet <nicdumz.commits@gmail.com> D
156 | |
157 | o 2:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
158 | |
159 | o 1:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
160 |/
161 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
162
163 $ rm -rf clone
164
165 Test a pull with an remote-changegroup and a following changegroup
166
167 $ hg bundle -R repo --base 2 -r '3:4' bundle2.hg
168 2 changesets found
169 $ cat > repo/.hg/bundle2maker << EOF
170 > remote-changegroup http://localhost:$HGPORT/bundle2.hg bundle2.hg
171 > changegroup 0:4 5:7
172 > EOF
173 $ hg clone orig clone -r 2
174 adding changesets
175 adding manifests
176 adding file changes
177 added 3 changesets with 3 changes to 3 files
178 updating to branch default
179 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
180 $ hg pull -R clone ssh://user@dummy/repo
181 pulling from ssh://user@dummy/repo
182 searching for changes
183 remote: B2X:REMOTE-CHANGEGROUP
184 adding changesets
185 adding manifests
186 adding file changes
187 added 2 changesets with 2 changes to 2 files (+1 heads)
188 remote: B2X:CHANGEGROUP
189 adding changesets
190 adding manifests
191 adding file changes
192 added 3 changesets with 2 changes to 2 files (+1 heads)
193 (run 'hg heads' to see heads, 'hg merge' to merge)
194 $ hg -R clone log -G
195 o 7:02de42196ebe public Nicolas Dumazet <nicdumz.commits@gmail.com> H
196 |
197 | o 6:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
198 |/|
199 o | 5:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
200 | |
201 | o 4:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
202 |/
203 | o 3:32af7686d403 public Nicolas Dumazet <nicdumz.commits@gmail.com> D
204 | |
205 | @ 2:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
206 | |
207 | o 1:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
208 |/
209 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
210
211 $ rm -rf clone
212
213 Test a pull with a changegroup followed by an remote-changegroup
214
215 $ hg bundle -R repo --base '0:4' -r '5:7' bundle3.hg
216 3 changesets found
217 $ cat > repo/.hg/bundle2maker << EOF
218 > changegroup 000000000000 :4
219 > remote-changegroup http://localhost:$HGPORT/bundle3.hg bundle3.hg
220 > EOF
221 $ hg clone orig clone -r 2
222 adding changesets
223 adding manifests
224 adding file changes
225 added 3 changesets with 3 changes to 3 files
226 updating to branch default
227 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
228 $ hg pull -R clone ssh://user@dummy/repo
229 pulling from ssh://user@dummy/repo
230 searching for changes
231 remote: B2X:CHANGEGROUP
232 adding changesets
233 adding manifests
234 adding file changes
235 added 2 changesets with 2 changes to 2 files (+1 heads)
236 remote: B2X:REMOTE-CHANGEGROUP
237 adding changesets
238 adding manifests
239 adding file changes
240 added 3 changesets with 2 changes to 2 files (+1 heads)
241 (run 'hg heads' to see heads, 'hg merge' to merge)
242 $ hg -R clone log -G
243 o 7:02de42196ebe public Nicolas Dumazet <nicdumz.commits@gmail.com> H
244 |
245 | o 6:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
246 |/|
247 o | 5:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
248 | |
249 | o 4:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
250 |/
251 | o 3:32af7686d403 public Nicolas Dumazet <nicdumz.commits@gmail.com> D
252 | |
253 | @ 2:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
254 | |
255 | o 1:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
256 |/
257 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
258
259 $ rm -rf clone
260
261 Test a pull with two remote-changegroups and a changegroup
262
263 $ hg bundle -R repo --base 2 -r '3:4' bundle4.hg
264 2 changesets found
265 $ hg bundle -R repo --base '3:4' -r '5:6' bundle5.hg
266 2 changesets found
267 $ cat > repo/.hg/bundle2maker << EOF
268 > remote-changegroup http://localhost:$HGPORT/bundle4.hg bundle4.hg
269 > remote-changegroup http://localhost:$HGPORT/bundle5.hg bundle5.hg
270 > changegroup 0:6 7
271 > EOF
272 $ hg clone orig clone -r 2
273 adding changesets
274 adding manifests
275 adding file changes
276 added 3 changesets with 3 changes to 3 files
277 updating to branch default
278 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
279 $ hg pull -R clone ssh://user@dummy/repo
280 pulling from ssh://user@dummy/repo
281 searching for changes
282 remote: B2X:REMOTE-CHANGEGROUP
283 adding changesets
284 adding manifests
285 adding file changes
286 added 2 changesets with 2 changes to 2 files (+1 heads)
287 remote: B2X:REMOTE-CHANGEGROUP
288 adding changesets
289 adding manifests
290 adding file changes
291 added 2 changesets with 1 changes to 1 files
292 remote: B2X:CHANGEGROUP
293 adding changesets
294 adding manifests
295 adding file changes
296 added 1 changesets with 1 changes to 1 files (+1 heads)
297 (run 'hg heads' to see heads, 'hg merge' to merge)
298 $ hg -R clone log -G
299 o 7:02de42196ebe public Nicolas Dumazet <nicdumz.commits@gmail.com> H
300 |
301 | o 6:eea13746799a public Nicolas Dumazet <nicdumz.commits@gmail.com> G
302 |/|
303 o | 5:24b6387c8c8c public Nicolas Dumazet <nicdumz.commits@gmail.com> F
304 | |
305 | o 4:9520eea781bc public Nicolas Dumazet <nicdumz.commits@gmail.com> E
306 |/
307 | o 3:32af7686d403 public Nicolas Dumazet <nicdumz.commits@gmail.com> D
308 | |
309 | @ 2:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
310 | |
311 | o 1:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
312 |/
313 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
314
315 $ rm -rf clone
316
317 Hash digest tests
318
319 $ hg bundle -R repo -a bundle6.hg
320 8 changesets found
321
322 $ cat > repo/.hg/bundle2maker << EOF
323 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle6.hg', 'size': 1663, 'digests': 'sha1', 'digest:sha1': '2c880cfec23cff7d8f80c2f12958d1563cbdaba6'}
324 > EOF
325 $ hg clone ssh://user@dummy/repo clone
326 requesting all changes
327 remote: B2X:REMOTE-CHANGEGROUP
328 adding changesets
329 adding manifests
330 adding file changes
331 added 8 changesets with 7 changes to 7 files (+2 heads)
332 updating to branch default
333 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 $ rm -rf clone
335
336 $ cat > repo/.hg/bundle2maker << EOF
337 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle6.hg', 'size': 1663, 'digests': 'md5', 'digest:md5': 'e22172c2907ef88794b7bea6642c2394'}
338 > EOF
339 $ hg clone ssh://user@dummy/repo clone
340 requesting all changes
341 remote: B2X:REMOTE-CHANGEGROUP
342 adding changesets
343 adding manifests
344 adding file changes
345 added 8 changesets with 7 changes to 7 files (+2 heads)
346 updating to branch default
347 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
348 $ rm -rf clone
349
350 Hash digest mismatch throws an error
351
352 $ cat > repo/.hg/bundle2maker << EOF
353 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle6.hg', 'size': 1663, 'digests': 'sha1', 'digest:sha1': '0' * 40}
354 > EOF
355 $ hg clone ssh://user@dummy/repo clone
356 requesting all changes
357 remote: B2X:REMOTE-CHANGEGROUP
358 adding changesets
359 adding manifests
360 adding file changes
361 added 8 changesets with 7 changes to 7 files (+2 heads)
362 transaction abort!
363 rollback completed
364 abort: bundle at http://localhost:$HGPORT/bundle6.hg is corrupted:
365 sha1 mismatch: expected 0000000000000000000000000000000000000000, got 2c880cfec23cff7d8f80c2f12958d1563cbdaba6
366 [255]
367
368 Multiple hash digests can be given
369
370 $ cat > repo/.hg/bundle2maker << EOF
371 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle6.hg', 'size': 1663, 'digests': 'md5 sha1', 'digest:md5': 'e22172c2907ef88794b7bea6642c2394', 'digest:sha1': '2c880cfec23cff7d8f80c2f12958d1563cbdaba6'}
372 > EOF
373 $ hg clone ssh://user@dummy/repo clone
374 requesting all changes
375 remote: B2X:REMOTE-CHANGEGROUP
376 adding changesets
377 adding manifests
378 adding file changes
379 added 8 changesets with 7 changes to 7 files (+2 heads)
380 updating to branch default
381 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
382 $ rm -rf clone
383
384 If either of the multiple hash digests mismatches, an error is thrown
385
386 $ cat > repo/.hg/bundle2maker << EOF
387 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle6.hg', 'size': 1663, 'digests': 'md5 sha1', 'digest:md5': '0' * 32, 'digest:sha1': '2c880cfec23cff7d8f80c2f12958d1563cbdaba6'}
388 > EOF
389 $ hg clone ssh://user@dummy/repo clone
390 requesting all changes
391 remote: B2X:REMOTE-CHANGEGROUP
392 adding changesets
393 adding manifests
394 adding file changes
395 added 8 changesets with 7 changes to 7 files (+2 heads)
396 transaction abort!
397 rollback completed
398 abort: bundle at http://localhost:$HGPORT/bundle6.hg is corrupted:
399 md5 mismatch: expected 00000000000000000000000000000000, got e22172c2907ef88794b7bea6642c2394
400 [255]
401
402 $ cat > repo/.hg/bundle2maker << EOF
403 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle6.hg', 'size': 1663, 'digests': 'md5 sha1', 'digest:md5': 'e22172c2907ef88794b7bea6642c2394', 'digest:sha1': '0' * 40}
404 > EOF
405 $ hg clone ssh://user@dummy/repo clone
406 requesting all changes
407 remote: B2X:REMOTE-CHANGEGROUP
408 adding changesets
409 adding manifests
410 adding file changes
411 added 8 changesets with 7 changes to 7 files (+2 heads)
412 transaction abort!
413 rollback completed
414 abort: bundle at http://localhost:$HGPORT/bundle6.hg is corrupted:
415 sha1 mismatch: expected 0000000000000000000000000000000000000000, got 2c880cfec23cff7d8f80c2f12958d1563cbdaba6
416 [255]
417
418 Corruption tests
419
420 $ hg clone orig clone -r 2
421 adding changesets
422 adding manifests
423 adding file changes
424 added 3 changesets with 3 changes to 3 files
425 updating to branch default
426 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
427
428 $ cat > repo/.hg/bundle2maker << EOF
429 > remote-changegroup http://localhost:$HGPORT/bundle4.hg bundle4.hg
430 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle5.hg', 'size': 578, 'digests': 'sha1', 'digest:sha1': '0' * 40}
431 > changegroup 0:6 7
432 > EOF
433 $ hg pull -R clone ssh://user@dummy/repo
434 pulling from ssh://user@dummy/repo
435 searching for changes
436 remote: B2X:REMOTE-CHANGEGROUP
437 adding changesets
438 adding manifests
439 adding file changes
440 added 2 changesets with 2 changes to 2 files (+1 heads)
441 remote: B2X:REMOTE-CHANGEGROUP
442 adding changesets
443 adding manifests
444 adding file changes
445 added 2 changesets with 1 changes to 1 files
446 transaction abort!
447 rollback completed
448 abort: bundle at http://localhost:$HGPORT/bundle5.hg is corrupted:
449 sha1 mismatch: expected 0000000000000000000000000000000000000000, got f29485d6bfd37db99983cfc95ecb52f8ca396106
450 [255]
451
452 The entire transaction has been rolled back in the pull above
453
454 $ hg -R clone log -G
455 @ 2:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
456 |
457 o 1:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
458 |
459 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
460
461
462 No params
463
464 $ cat > repo/.hg/bundle2maker << EOF
465 > raw-remote-changegroup {}
466 > EOF
467 $ hg pull -R clone ssh://user@dummy/repo
468 pulling from ssh://user@dummy/repo
469 searching for changes
470 remote: B2X:REMOTE-CHANGEGROUP
471 abort: remote-changegroup: missing "url" param
472 [255]
473
474 Missing size
475
476 $ cat > repo/.hg/bundle2maker << EOF
477 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle4.hg'}
478 > EOF
479 $ hg pull -R clone ssh://user@dummy/repo
480 pulling from ssh://user@dummy/repo
481 searching for changes
482 remote: B2X:REMOTE-CHANGEGROUP
483 abort: remote-changegroup: missing "size" param
484 [255]
485
486 Invalid size
487
488 $ cat > repo/.hg/bundle2maker << EOF
489 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle4.hg', 'size': 'foo'}
490 > EOF
491 $ hg pull -R clone ssh://user@dummy/repo
492 pulling from ssh://user@dummy/repo
493 searching for changes
494 remote: B2X:REMOTE-CHANGEGROUP
495 abort: remote-changegroup: invalid value for param "size"
496 [255]
497
498 Size mismatch
499
500 $ cat > repo/.hg/bundle2maker << EOF
501 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle4.hg', 'size': 42}
502 > EOF
503 $ hg pull -R clone ssh://user@dummy/repo
504 pulling from ssh://user@dummy/repo
505 searching for changes
506 remote: B2X:REMOTE-CHANGEGROUP
507 adding changesets
508 adding manifests
509 adding file changes
510 added 2 changesets with 2 changes to 2 files (+1 heads)
511 transaction abort!
512 rollback completed
513 abort: bundle at http://localhost:$HGPORT/bundle4.hg is corrupted:
514 size mismatch: expected 42, got 581
515 [255]
516
517 Unknown digest
518
519 $ cat > repo/.hg/bundle2maker << EOF
520 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle4.hg', 'size': 581, 'digests': 'foo', 'digest:foo': 'bar'}
521 > EOF
522 $ hg pull -R clone ssh://user@dummy/repo
523 pulling from ssh://user@dummy/repo
524 searching for changes
525 remote: B2X:REMOTE-CHANGEGROUP
526 abort: missing support for b2x:remote-changegroup - digest:foo
527 [255]
528
529 Missing digest
530
531 $ cat > repo/.hg/bundle2maker << EOF
532 > raw-remote-changegroup {'url': 'http://localhost:$HGPORT/bundle4.hg', 'size': 581, 'digests': 'sha1'}
533 > EOF
534 $ hg pull -R clone ssh://user@dummy/repo
535 pulling from ssh://user@dummy/repo
536 searching for changes
537 remote: B2X:REMOTE-CHANGEGROUP
538 abort: remote-changegroup: missing "digest:sha1" param
539 [255]
540
541 Not an HTTP url
542
543 $ cat > repo/.hg/bundle2maker << EOF
544 > raw-remote-changegroup {'url': 'ssh://localhost:$HGPORT/bundle4.hg', 'size': 581}
545 > EOF
546 $ hg pull -R clone ssh://user@dummy/repo
547 pulling from ssh://user@dummy/repo
548 searching for changes
549 remote: B2X:REMOTE-CHANGEGROUP
550 abort: remote-changegroup does not support ssh urls
551 [255]
552
553 Not a bundle
554
555 $ cat > notbundle.hg << EOF
556 > foo
557 > EOF
558 $ cat > repo/.hg/bundle2maker << EOF
559 > remote-changegroup http://localhost:$HGPORT/notbundle.hg notbundle.hg
560 > EOF
561 $ hg pull -R clone ssh://user@dummy/repo
562 pulling from ssh://user@dummy/repo
563 searching for changes
564 remote: B2X:REMOTE-CHANGEGROUP
565 abort: http://localhost:$HGPORT/notbundle.hg: not a Mercurial bundle
566 [255]
567
568 Not a bundle 1.0
569
570 $ cat > notbundle10.hg << EOF
571 > HG2Y
572 > EOF
573 $ cat > repo/.hg/bundle2maker << EOF
574 > remote-changegroup http://localhost:$HGPORT/notbundle10.hg notbundle10.hg
575 > EOF
576 $ hg pull -R clone ssh://user@dummy/repo
577 pulling from ssh://user@dummy/repo
578 searching for changes
579 remote: B2X:REMOTE-CHANGEGROUP
580 abort: http://localhost:$HGPORT/notbundle10.hg: not a bundle version 1.0
581 [255]
582
583 $ hg -R clone log -G
584 @ 2:5fddd98957c8 public Nicolas Dumazet <nicdumz.commits@gmail.com> C
585 |
586 o 1:42ccdea3bb16 public Nicolas Dumazet <nicdumz.commits@gmail.com> B
587 |
588 o 0:cd010b8cd998 public Nicolas Dumazet <nicdumz.commits@gmail.com> A
589
590 $ rm -rf clone
591
592 $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS
@@ -1,965 +1,1042 b''
1 # bundle2.py - generic container format to transmit arbitrary data.
1 # bundle2.py - generic container format to transmit arbitrary data.
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """Handling of the new bundle2 format
7 """Handling of the new bundle2 format
8
8
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 payloads in an application agnostic way. It consist in a sequence of "parts"
10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 that will be handed to and processed by the application layer.
11 that will be handed to and processed by the application layer.
12
12
13
13
14 General format architecture
14 General format architecture
15 ===========================
15 ===========================
16
16
17 The format is architectured as follow
17 The format is architectured as follow
18
18
19 - magic string
19 - magic string
20 - stream level parameters
20 - stream level parameters
21 - payload parts (any number)
21 - payload parts (any number)
22 - end of stream marker.
22 - end of stream marker.
23
23
24 the Binary format
24 the Binary format
25 ============================
25 ============================
26
26
27 All numbers are unsigned and big-endian.
27 All numbers are unsigned and big-endian.
28
28
29 stream level parameters
29 stream level parameters
30 ------------------------
30 ------------------------
31
31
32 Binary format is as follow
32 Binary format is as follow
33
33
34 :params size: int32
34 :params size: int32
35
35
36 The total number of Bytes used by the parameters
36 The total number of Bytes used by the parameters
37
37
38 :params value: arbitrary number of Bytes
38 :params value: arbitrary number of Bytes
39
39
40 A blob of `params size` containing the serialized version of all stream level
40 A blob of `params size` containing the serialized version of all stream level
41 parameters.
41 parameters.
42
42
43 The blob contains a space separated list of parameters. Parameters with value
43 The blob contains a space separated list of parameters. Parameters with value
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45
45
46 Empty name are obviously forbidden.
46 Empty name are obviously forbidden.
47
47
48 Name MUST start with a letter. If this first letter is lower case, the
48 Name MUST start with a letter. If this first letter is lower case, the
49 parameter is advisory and can be safely ignored. However when the first
49 parameter is advisory and can be safely ignored. However when the first
50 letter is capital, the parameter is mandatory and the bundling process MUST
50 letter is capital, the parameter is mandatory and the bundling process MUST
51 stop if he is not able to proceed it.
51 stop if he is not able to proceed it.
52
52
53 Stream parameters use a simple textual format for two main reasons:
53 Stream parameters use a simple textual format for two main reasons:
54
54
55 - Stream level parameters should remain simple and we want to discourage any
55 - Stream level parameters should remain simple and we want to discourage any
56 crazy usage.
56 crazy usage.
57 - Textual data allow easy human inspection of a bundle2 header in case of
57 - Textual data allow easy human inspection of a bundle2 header in case of
58 troubles.
58 troubles.
59
59
60 Any Applicative level options MUST go into a bundle2 part instead.
60 Any Applicative level options MUST go into a bundle2 part instead.
61
61
62 Payload part
62 Payload part
63 ------------------------
63 ------------------------
64
64
65 Binary format is as follow
65 Binary format is as follow
66
66
67 :header size: int32
67 :header size: int32
68
68
69 The total number of Bytes used by the part headers. When the header is empty
69 The total number of Bytes used by the part headers. When the header is empty
70 (size = 0) this is interpreted as the end of stream marker.
70 (size = 0) this is interpreted as the end of stream marker.
71
71
72 :header:
72 :header:
73
73
74 The header defines how to interpret the part. It contains two piece of
74 The header defines how to interpret the part. It contains two piece of
75 data: the part type, and the part parameters.
75 data: the part type, and the part parameters.
76
76
77 The part type is used to route an application level handler, that can
77 The part type is used to route an application level handler, that can
78 interpret payload.
78 interpret payload.
79
79
80 Part parameters are passed to the application level handler. They are
80 Part parameters are passed to the application level handler. They are
81 meant to convey information that will help the application level object to
81 meant to convey information that will help the application level object to
82 interpret the part payload.
82 interpret the part payload.
83
83
84 The binary format of the header is has follow
84 The binary format of the header is has follow
85
85
86 :typesize: (one byte)
86 :typesize: (one byte)
87
87
88 :parttype: alphanumerical part name
88 :parttype: alphanumerical part name
89
89
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 to this part.
91 to this part.
92
92
93 :parameters:
93 :parameters:
94
94
95 Part's parameter may have arbitrary content, the binary structure is::
95 Part's parameter may have arbitrary content, the binary structure is::
96
96
97 <mandatory-count><advisory-count><param-sizes><param-data>
97 <mandatory-count><advisory-count><param-sizes><param-data>
98
98
99 :mandatory-count: 1 byte, number of mandatory parameters
99 :mandatory-count: 1 byte, number of mandatory parameters
100
100
101 :advisory-count: 1 byte, number of advisory parameters
101 :advisory-count: 1 byte, number of advisory parameters
102
102
103 :param-sizes:
103 :param-sizes:
104
104
105 N couple of bytes, where N is the total number of parameters. Each
105 N couple of bytes, where N is the total number of parameters. Each
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107
107
108 :param-data:
108 :param-data:
109
109
110 A blob of bytes from which each parameter key and value can be
110 A blob of bytes from which each parameter key and value can be
111 retrieved using the list of size couples stored in the previous
111 retrieved using the list of size couples stored in the previous
112 field.
112 field.
113
113
114 Mandatory parameters comes first, then the advisory ones.
114 Mandatory parameters comes first, then the advisory ones.
115
115
116 Each parameter's key MUST be unique within the part.
116 Each parameter's key MUST be unique within the part.
117
117
118 :payload:
118 :payload:
119
119
120 payload is a series of `<chunksize><chunkdata>`.
120 payload is a series of `<chunksize><chunkdata>`.
121
121
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124
124
125 The current implementation always produces either zero or one chunk.
125 The current implementation always produces either zero or one chunk.
126 This is an implementation limitation that will ultimately be lifted.
126 This is an implementation limitation that will ultimately be lifted.
127
127
128 `chunksize` can be negative to trigger special case processing. No such
128 `chunksize` can be negative to trigger special case processing. No such
129 processing is in place yet.
129 processing is in place yet.
130
130
131 Bundle processing
131 Bundle processing
132 ============================
132 ============================
133
133
134 Each part is processed in order using a "part handler". Handler are registered
134 Each part is processed in order using a "part handler". Handler are registered
135 for a certain part type.
135 for a certain part type.
136
136
137 The matching of a part to its handler is case insensitive. The case of the
137 The matching of a part to its handler is case insensitive. The case of the
138 part type is used to know if a part is mandatory or advisory. If the Part type
138 part type is used to know if a part is mandatory or advisory. If the Part type
139 contains any uppercase char it is considered mandatory. When no handler is
139 contains any uppercase char it is considered mandatory. When no handler is
140 known for a Mandatory part, the process is aborted and an exception is raised.
140 known for a Mandatory part, the process is aborted and an exception is raised.
141 If the part is advisory and no handler is known, the part is ignored. When the
141 If the part is advisory and no handler is known, the part is ignored. When the
142 process is aborted, the full bundle is still read from the stream to keep the
142 process is aborted, the full bundle is still read from the stream to keep the
143 channel usable. But none of the part read from an abort are processed. In the
143 channel usable. But none of the part read from an abort are processed. In the
144 future, dropping the stream may become an option for channel we do not care to
144 future, dropping the stream may become an option for channel we do not care to
145 preserve.
145 preserve.
146 """
146 """
147
147
148 import util
148 import util
149 import struct
149 import struct
150 import urllib
150 import urllib
151 import string
151 import string
152 import obsolete
152 import obsolete
153 import pushkey
153 import pushkey
154 import url
154
155
155 import changegroup, error
156 import changegroup, error
156 from i18n import _
157 from i18n import _
157
158
158 _pack = struct.pack
159 _pack = struct.pack
159 _unpack = struct.unpack
160 _unpack = struct.unpack
160
161
161 _magicstring = 'HG2Y'
162 _magicstring = 'HG2Y'
162
163
163 _fstreamparamsize = '>i'
164 _fstreamparamsize = '>i'
164 _fpartheadersize = '>i'
165 _fpartheadersize = '>i'
165 _fparttypesize = '>B'
166 _fparttypesize = '>B'
166 _fpartid = '>I'
167 _fpartid = '>I'
167 _fpayloadsize = '>i'
168 _fpayloadsize = '>i'
168 _fpartparamcount = '>BB'
169 _fpartparamcount = '>BB'
169
170
170 preferedchunksize = 4096
171 preferedchunksize = 4096
171
172
172 def _makefpartparamsizes(nbparams):
173 def _makefpartparamsizes(nbparams):
173 """return a struct format to read part parameter sizes
174 """return a struct format to read part parameter sizes
174
175
175 The number parameters is variable so we need to build that format
176 The number parameters is variable so we need to build that format
176 dynamically.
177 dynamically.
177 """
178 """
178 return '>'+('BB'*nbparams)
179 return '>'+('BB'*nbparams)
179
180
180 parthandlermapping = {}
181 parthandlermapping = {}
181
182
182 def parthandler(parttype, params=()):
183 def parthandler(parttype, params=()):
183 """decorator that register a function as a bundle2 part handler
184 """decorator that register a function as a bundle2 part handler
184
185
185 eg::
186 eg::
186
187
187 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
188 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
188 def myparttypehandler(...):
189 def myparttypehandler(...):
189 '''process a part of type "my part".'''
190 '''process a part of type "my part".'''
190 ...
191 ...
191 """
192 """
192 def _decorator(func):
193 def _decorator(func):
193 lparttype = parttype.lower() # enforce lower case matching.
194 lparttype = parttype.lower() # enforce lower case matching.
194 assert lparttype not in parthandlermapping
195 assert lparttype not in parthandlermapping
195 parthandlermapping[lparttype] = func
196 parthandlermapping[lparttype] = func
196 func.params = frozenset(params)
197 func.params = frozenset(params)
197 return func
198 return func
198 return _decorator
199 return _decorator
199
200
200 class unbundlerecords(object):
201 class unbundlerecords(object):
201 """keep record of what happens during and unbundle
202 """keep record of what happens during and unbundle
202
203
203 New records are added using `records.add('cat', obj)`. Where 'cat' is a
204 New records are added using `records.add('cat', obj)`. Where 'cat' is a
204 category of record and obj is an arbitrary object.
205 category of record and obj is an arbitrary object.
205
206
206 `records['cat']` will return all entries of this category 'cat'.
207 `records['cat']` will return all entries of this category 'cat'.
207
208
208 Iterating on the object itself will yield `('category', obj)` tuples
209 Iterating on the object itself will yield `('category', obj)` tuples
209 for all entries.
210 for all entries.
210
211
211 All iterations happens in chronological order.
212 All iterations happens in chronological order.
212 """
213 """
213
214
214 def __init__(self):
215 def __init__(self):
215 self._categories = {}
216 self._categories = {}
216 self._sequences = []
217 self._sequences = []
217 self._replies = {}
218 self._replies = {}
218
219
219 def add(self, category, entry, inreplyto=None):
220 def add(self, category, entry, inreplyto=None):
220 """add a new record of a given category.
221 """add a new record of a given category.
221
222
222 The entry can then be retrieved in the list returned by
223 The entry can then be retrieved in the list returned by
223 self['category']."""
224 self['category']."""
224 self._categories.setdefault(category, []).append(entry)
225 self._categories.setdefault(category, []).append(entry)
225 self._sequences.append((category, entry))
226 self._sequences.append((category, entry))
226 if inreplyto is not None:
227 if inreplyto is not None:
227 self.getreplies(inreplyto).add(category, entry)
228 self.getreplies(inreplyto).add(category, entry)
228
229
229 def getreplies(self, partid):
230 def getreplies(self, partid):
230 """get the subrecords that replies to a specific part"""
231 """get the subrecords that replies to a specific part"""
231 return self._replies.setdefault(partid, unbundlerecords())
232 return self._replies.setdefault(partid, unbundlerecords())
232
233
233 def __getitem__(self, cat):
234 def __getitem__(self, cat):
234 return tuple(self._categories.get(cat, ()))
235 return tuple(self._categories.get(cat, ()))
235
236
236 def __iter__(self):
237 def __iter__(self):
237 return iter(self._sequences)
238 return iter(self._sequences)
238
239
239 def __len__(self):
240 def __len__(self):
240 return len(self._sequences)
241 return len(self._sequences)
241
242
242 def __nonzero__(self):
243 def __nonzero__(self):
243 return bool(self._sequences)
244 return bool(self._sequences)
244
245
245 class bundleoperation(object):
246 class bundleoperation(object):
246 """an object that represents a single bundling process
247 """an object that represents a single bundling process
247
248
248 Its purpose is to carry unbundle-related objects and states.
249 Its purpose is to carry unbundle-related objects and states.
249
250
250 A new object should be created at the beginning of each bundle processing.
251 A new object should be created at the beginning of each bundle processing.
251 The object is to be returned by the processing function.
252 The object is to be returned by the processing function.
252
253
253 The object has very little content now it will ultimately contain:
254 The object has very little content now it will ultimately contain:
254 * an access to the repo the bundle is applied to,
255 * an access to the repo the bundle is applied to,
255 * a ui object,
256 * a ui object,
256 * a way to retrieve a transaction to add changes to the repo,
257 * a way to retrieve a transaction to add changes to the repo,
257 * a way to record the result of processing each part,
258 * a way to record the result of processing each part,
258 * a way to construct a bundle response when applicable.
259 * a way to construct a bundle response when applicable.
259 """
260 """
260
261
261 def __init__(self, repo, transactiongetter):
262 def __init__(self, repo, transactiongetter):
262 self.repo = repo
263 self.repo = repo
263 self.ui = repo.ui
264 self.ui = repo.ui
264 self.records = unbundlerecords()
265 self.records = unbundlerecords()
265 self.gettransaction = transactiongetter
266 self.gettransaction = transactiongetter
266 self.reply = None
267 self.reply = None
267
268
268 class TransactionUnavailable(RuntimeError):
269 class TransactionUnavailable(RuntimeError):
269 pass
270 pass
270
271
271 def _notransaction():
272 def _notransaction():
272 """default method to get a transaction while processing a bundle
273 """default method to get a transaction while processing a bundle
273
274
274 Raise an exception to highlight the fact that no transaction was expected
275 Raise an exception to highlight the fact that no transaction was expected
275 to be created"""
276 to be created"""
276 raise TransactionUnavailable()
277 raise TransactionUnavailable()
277
278
278 def processbundle(repo, unbundler, transactiongetter=_notransaction):
279 def processbundle(repo, unbundler, transactiongetter=_notransaction):
279 """This function process a bundle, apply effect to/from a repo
280 """This function process a bundle, apply effect to/from a repo
280
281
281 It iterates over each part then searches for and uses the proper handling
282 It iterates over each part then searches for and uses the proper handling
282 code to process the part. Parts are processed in order.
283 code to process the part. Parts are processed in order.
283
284
284 This is very early version of this function that will be strongly reworked
285 This is very early version of this function that will be strongly reworked
285 before final usage.
286 before final usage.
286
287
287 Unknown Mandatory part will abort the process.
288 Unknown Mandatory part will abort the process.
288 """
289 """
289 op = bundleoperation(repo, transactiongetter)
290 op = bundleoperation(repo, transactiongetter)
290 # todo:
291 # todo:
291 # - replace this is a init function soon.
292 # - replace this is a init function soon.
292 # - exception catching
293 # - exception catching
293 unbundler.params
294 unbundler.params
294 iterparts = unbundler.iterparts()
295 iterparts = unbundler.iterparts()
295 part = None
296 part = None
296 try:
297 try:
297 for part in iterparts:
298 for part in iterparts:
298 _processpart(op, part)
299 _processpart(op, part)
299 except Exception, exc:
300 except Exception, exc:
300 for part in iterparts:
301 for part in iterparts:
301 # consume the bundle content
302 # consume the bundle content
302 part.read()
303 part.read()
303 # Small hack to let caller code distinguish exceptions from bundle2
304 # Small hack to let caller code distinguish exceptions from bundle2
304 # processing fron the ones from bundle1 processing. This is mostly
305 # processing fron the ones from bundle1 processing. This is mostly
305 # needed to handle different return codes to unbundle according to the
306 # needed to handle different return codes to unbundle according to the
306 # type of bundle. We should probably clean up or drop this return code
307 # type of bundle. We should probably clean up or drop this return code
307 # craziness in a future version.
308 # craziness in a future version.
308 exc.duringunbundle2 = True
309 exc.duringunbundle2 = True
309 raise
310 raise
310 return op
311 return op
311
312
312 def _processpart(op, part):
313 def _processpart(op, part):
313 """process a single part from a bundle
314 """process a single part from a bundle
314
315
315 The part is guaranteed to have been fully consumed when the function exits
316 The part is guaranteed to have been fully consumed when the function exits
316 (even if an exception is raised)."""
317 (even if an exception is raised)."""
317 try:
318 try:
318 parttype = part.type
319 parttype = part.type
319 # part key are matched lower case
320 # part key are matched lower case
320 key = parttype.lower()
321 key = parttype.lower()
321 try:
322 try:
322 handler = parthandlermapping.get(key)
323 handler = parthandlermapping.get(key)
323 if handler is None:
324 if handler is None:
324 raise error.UnsupportedPartError(parttype=key)
325 raise error.UnsupportedPartError(parttype=key)
325 op.ui.debug('found a handler for part %r\n' % parttype)
326 op.ui.debug('found a handler for part %r\n' % parttype)
326 unknownparams = part.mandatorykeys - handler.params
327 unknownparams = part.mandatorykeys - handler.params
327 if unknownparams:
328 if unknownparams:
328 unknownparams = list(unknownparams)
329 unknownparams = list(unknownparams)
329 unknownparams.sort()
330 unknownparams.sort()
330 raise error.UnsupportedPartError(parttype=key,
331 raise error.UnsupportedPartError(parttype=key,
331 params=unknownparams)
332 params=unknownparams)
332 except error.UnsupportedPartError, exc:
333 except error.UnsupportedPartError, exc:
333 if key != parttype: # mandatory parts
334 if key != parttype: # mandatory parts
334 raise
335 raise
335 op.ui.debug('ignoring unsupported advisory part %s\n' % exc)
336 op.ui.debug('ignoring unsupported advisory part %s\n' % exc)
336 return # skip to part processing
337 return # skip to part processing
337
338
338 # handler is called outside the above try block so that we don't
339 # handler is called outside the above try block so that we don't
339 # risk catching KeyErrors from anything other than the
340 # risk catching KeyErrors from anything other than the
340 # parthandlermapping lookup (any KeyError raised by handler()
341 # parthandlermapping lookup (any KeyError raised by handler()
341 # itself represents a defect of a different variety).
342 # itself represents a defect of a different variety).
342 output = None
343 output = None
343 if op.reply is not None:
344 if op.reply is not None:
344 op.ui.pushbuffer(error=True)
345 op.ui.pushbuffer(error=True)
345 output = ''
346 output = ''
346 try:
347 try:
347 handler(op, part)
348 handler(op, part)
348 finally:
349 finally:
349 if output is not None:
350 if output is not None:
350 output = op.ui.popbuffer()
351 output = op.ui.popbuffer()
351 if output:
352 if output:
352 outpart = op.reply.newpart('b2x:output', data=output)
353 outpart = op.reply.newpart('b2x:output', data=output)
353 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
354 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
354 finally:
355 finally:
355 # consume the part content to not corrupt the stream.
356 # consume the part content to not corrupt the stream.
356 part.read()
357 part.read()
357
358
358
359
359 def decodecaps(blob):
360 def decodecaps(blob):
360 """decode a bundle2 caps bytes blob into a dictionnary
361 """decode a bundle2 caps bytes blob into a dictionnary
361
362
362 The blob is a list of capabilities (one per line)
363 The blob is a list of capabilities (one per line)
363 Capabilities may have values using a line of the form::
364 Capabilities may have values using a line of the form::
364
365
365 capability=value1,value2,value3
366 capability=value1,value2,value3
366
367
367 The values are always a list."""
368 The values are always a list."""
368 caps = {}
369 caps = {}
369 for line in blob.splitlines():
370 for line in blob.splitlines():
370 if not line:
371 if not line:
371 continue
372 continue
372 if '=' not in line:
373 if '=' not in line:
373 key, vals = line, ()
374 key, vals = line, ()
374 else:
375 else:
375 key, vals = line.split('=', 1)
376 key, vals = line.split('=', 1)
376 vals = vals.split(',')
377 vals = vals.split(',')
377 key = urllib.unquote(key)
378 key = urllib.unquote(key)
378 vals = [urllib.unquote(v) for v in vals]
379 vals = [urllib.unquote(v) for v in vals]
379 caps[key] = vals
380 caps[key] = vals
380 return caps
381 return caps
381
382
382 def encodecaps(caps):
383 def encodecaps(caps):
383 """encode a bundle2 caps dictionary into a bytes blob"""
384 """encode a bundle2 caps dictionary into a bytes blob"""
384 chunks = []
385 chunks = []
385 for ca in sorted(caps):
386 for ca in sorted(caps):
386 vals = caps[ca]
387 vals = caps[ca]
387 ca = urllib.quote(ca)
388 ca = urllib.quote(ca)
388 vals = [urllib.quote(v) for v in vals]
389 vals = [urllib.quote(v) for v in vals]
389 if vals:
390 if vals:
390 ca = "%s=%s" % (ca, ','.join(vals))
391 ca = "%s=%s" % (ca, ','.join(vals))
391 chunks.append(ca)
392 chunks.append(ca)
392 return '\n'.join(chunks)
393 return '\n'.join(chunks)
393
394
394 class bundle20(object):
395 class bundle20(object):
395 """represent an outgoing bundle2 container
396 """represent an outgoing bundle2 container
396
397
397 Use the `addparam` method to add stream level parameter. and `newpart` to
398 Use the `addparam` method to add stream level parameter. and `newpart` to
398 populate it. Then call `getchunks` to retrieve all the binary chunks of
399 populate it. Then call `getchunks` to retrieve all the binary chunks of
399 data that compose the bundle2 container."""
400 data that compose the bundle2 container."""
400
401
401 def __init__(self, ui, capabilities=()):
402 def __init__(self, ui, capabilities=()):
402 self.ui = ui
403 self.ui = ui
403 self._params = []
404 self._params = []
404 self._parts = []
405 self._parts = []
405 self.capabilities = dict(capabilities)
406 self.capabilities = dict(capabilities)
406
407
407 @property
408 @property
408 def nbparts(self):
409 def nbparts(self):
409 """total number of parts added to the bundler"""
410 """total number of parts added to the bundler"""
410 return len(self._parts)
411 return len(self._parts)
411
412
412 # methods used to defines the bundle2 content
413 # methods used to defines the bundle2 content
413 def addparam(self, name, value=None):
414 def addparam(self, name, value=None):
414 """add a stream level parameter"""
415 """add a stream level parameter"""
415 if not name:
416 if not name:
416 raise ValueError('empty parameter name')
417 raise ValueError('empty parameter name')
417 if name[0] not in string.letters:
418 if name[0] not in string.letters:
418 raise ValueError('non letter first character: %r' % name)
419 raise ValueError('non letter first character: %r' % name)
419 self._params.append((name, value))
420 self._params.append((name, value))
420
421
421 def addpart(self, part):
422 def addpart(self, part):
422 """add a new part to the bundle2 container
423 """add a new part to the bundle2 container
423
424
424 Parts contains the actual applicative payload."""
425 Parts contains the actual applicative payload."""
425 assert part.id is None
426 assert part.id is None
426 part.id = len(self._parts) # very cheap counter
427 part.id = len(self._parts) # very cheap counter
427 self._parts.append(part)
428 self._parts.append(part)
428
429
429 def newpart(self, typeid, *args, **kwargs):
430 def newpart(self, typeid, *args, **kwargs):
430 """create a new part and add it to the containers
431 """create a new part and add it to the containers
431
432
432 As the part is directly added to the containers. For now, this means
433 As the part is directly added to the containers. For now, this means
433 that any failure to properly initialize the part after calling
434 that any failure to properly initialize the part after calling
434 ``newpart`` should result in a failure of the whole bundling process.
435 ``newpart`` should result in a failure of the whole bundling process.
435
436
436 You can still fall back to manually create and add if you need better
437 You can still fall back to manually create and add if you need better
437 control."""
438 control."""
438 part = bundlepart(typeid, *args, **kwargs)
439 part = bundlepart(typeid, *args, **kwargs)
439 self.addpart(part)
440 self.addpart(part)
440 return part
441 return part
441
442
442 # methods used to generate the bundle2 stream
443 # methods used to generate the bundle2 stream
443 def getchunks(self):
444 def getchunks(self):
444 self.ui.debug('start emission of %s stream\n' % _magicstring)
445 self.ui.debug('start emission of %s stream\n' % _magicstring)
445 yield _magicstring
446 yield _magicstring
446 param = self._paramchunk()
447 param = self._paramchunk()
447 self.ui.debug('bundle parameter: %s\n' % param)
448 self.ui.debug('bundle parameter: %s\n' % param)
448 yield _pack(_fstreamparamsize, len(param))
449 yield _pack(_fstreamparamsize, len(param))
449 if param:
450 if param:
450 yield param
451 yield param
451
452
452 self.ui.debug('start of parts\n')
453 self.ui.debug('start of parts\n')
453 for part in self._parts:
454 for part in self._parts:
454 self.ui.debug('bundle part: "%s"\n' % part.type)
455 self.ui.debug('bundle part: "%s"\n' % part.type)
455 for chunk in part.getchunks():
456 for chunk in part.getchunks():
456 yield chunk
457 yield chunk
457 self.ui.debug('end of bundle\n')
458 self.ui.debug('end of bundle\n')
458 yield _pack(_fpartheadersize, 0)
459 yield _pack(_fpartheadersize, 0)
459
460
460 def _paramchunk(self):
461 def _paramchunk(self):
461 """return a encoded version of all stream parameters"""
462 """return a encoded version of all stream parameters"""
462 blocks = []
463 blocks = []
463 for par, value in self._params:
464 for par, value in self._params:
464 par = urllib.quote(par)
465 par = urllib.quote(par)
465 if value is not None:
466 if value is not None:
466 value = urllib.quote(value)
467 value = urllib.quote(value)
467 par = '%s=%s' % (par, value)
468 par = '%s=%s' % (par, value)
468 blocks.append(par)
469 blocks.append(par)
469 return ' '.join(blocks)
470 return ' '.join(blocks)
470
471
471 class unpackermixin(object):
472 class unpackermixin(object):
472 """A mixin to extract bytes and struct data from a stream"""
473 """A mixin to extract bytes and struct data from a stream"""
473
474
474 def __init__(self, fp):
475 def __init__(self, fp):
475 self._fp = fp
476 self._fp = fp
476
477
477 def _unpack(self, format):
478 def _unpack(self, format):
478 """unpack this struct format from the stream"""
479 """unpack this struct format from the stream"""
479 data = self._readexact(struct.calcsize(format))
480 data = self._readexact(struct.calcsize(format))
480 return _unpack(format, data)
481 return _unpack(format, data)
481
482
482 def _readexact(self, size):
483 def _readexact(self, size):
483 """read exactly <size> bytes from the stream"""
484 """read exactly <size> bytes from the stream"""
484 return changegroup.readexactly(self._fp, size)
485 return changegroup.readexactly(self._fp, size)
485
486
486
487
487 class unbundle20(unpackermixin):
488 class unbundle20(unpackermixin):
488 """interpret a bundle2 stream
489 """interpret a bundle2 stream
489
490
490 This class is fed with a binary stream and yields parts through its
491 This class is fed with a binary stream and yields parts through its
491 `iterparts` methods."""
492 `iterparts` methods."""
492
493
493 def __init__(self, ui, fp, header=None):
494 def __init__(self, ui, fp, header=None):
494 """If header is specified, we do not read it out of the stream."""
495 """If header is specified, we do not read it out of the stream."""
495 self.ui = ui
496 self.ui = ui
496 super(unbundle20, self).__init__(fp)
497 super(unbundle20, self).__init__(fp)
497 if header is None:
498 if header is None:
498 header = self._readexact(4)
499 header = self._readexact(4)
499 magic, version = header[0:2], header[2:4]
500 magic, version = header[0:2], header[2:4]
500 if magic != 'HG':
501 if magic != 'HG':
501 raise util.Abort(_('not a Mercurial bundle'))
502 raise util.Abort(_('not a Mercurial bundle'))
502 if version != '2Y':
503 if version != '2Y':
503 raise util.Abort(_('unknown bundle version %s') % version)
504 raise util.Abort(_('unknown bundle version %s') % version)
504 self.ui.debug('start processing of %s stream\n' % header)
505 self.ui.debug('start processing of %s stream\n' % header)
505
506
506 @util.propertycache
507 @util.propertycache
507 def params(self):
508 def params(self):
508 """dictionary of stream level parameters"""
509 """dictionary of stream level parameters"""
509 self.ui.debug('reading bundle2 stream parameters\n')
510 self.ui.debug('reading bundle2 stream parameters\n')
510 params = {}
511 params = {}
511 paramssize = self._unpack(_fstreamparamsize)[0]
512 paramssize = self._unpack(_fstreamparamsize)[0]
512 if paramssize < 0:
513 if paramssize < 0:
513 raise error.BundleValueError('negative bundle param size: %i'
514 raise error.BundleValueError('negative bundle param size: %i'
514 % paramssize)
515 % paramssize)
515 if paramssize:
516 if paramssize:
516 for p in self._readexact(paramssize).split(' '):
517 for p in self._readexact(paramssize).split(' '):
517 p = p.split('=', 1)
518 p = p.split('=', 1)
518 p = [urllib.unquote(i) for i in p]
519 p = [urllib.unquote(i) for i in p]
519 if len(p) < 2:
520 if len(p) < 2:
520 p.append(None)
521 p.append(None)
521 self._processparam(*p)
522 self._processparam(*p)
522 params[p[0]] = p[1]
523 params[p[0]] = p[1]
523 return params
524 return params
524
525
525 def _processparam(self, name, value):
526 def _processparam(self, name, value):
526 """process a parameter, applying its effect if needed
527 """process a parameter, applying its effect if needed
527
528
528 Parameter starting with a lower case letter are advisory and will be
529 Parameter starting with a lower case letter are advisory and will be
529 ignored when unknown. Those starting with an upper case letter are
530 ignored when unknown. Those starting with an upper case letter are
530 mandatory and will this function will raise a KeyError when unknown.
531 mandatory and will this function will raise a KeyError when unknown.
531
532
532 Note: no option are currently supported. Any input will be either
533 Note: no option are currently supported. Any input will be either
533 ignored or failing.
534 ignored or failing.
534 """
535 """
535 if not name:
536 if not name:
536 raise ValueError('empty parameter name')
537 raise ValueError('empty parameter name')
537 if name[0] not in string.letters:
538 if name[0] not in string.letters:
538 raise ValueError('non letter first character: %r' % name)
539 raise ValueError('non letter first character: %r' % name)
539 # Some logic will be later added here to try to process the option for
540 # Some logic will be later added here to try to process the option for
540 # a dict of known parameter.
541 # a dict of known parameter.
541 if name[0].islower():
542 if name[0].islower():
542 self.ui.debug("ignoring unknown parameter %r\n" % name)
543 self.ui.debug("ignoring unknown parameter %r\n" % name)
543 else:
544 else:
544 raise error.UnsupportedPartError(params=(name,))
545 raise error.UnsupportedPartError(params=(name,))
545
546
546
547
547 def iterparts(self):
548 def iterparts(self):
548 """yield all parts contained in the stream"""
549 """yield all parts contained in the stream"""
549 # make sure param have been loaded
550 # make sure param have been loaded
550 self.params
551 self.params
551 self.ui.debug('start extraction of bundle2 parts\n')
552 self.ui.debug('start extraction of bundle2 parts\n')
552 headerblock = self._readpartheader()
553 headerblock = self._readpartheader()
553 while headerblock is not None:
554 while headerblock is not None:
554 part = unbundlepart(self.ui, headerblock, self._fp)
555 part = unbundlepart(self.ui, headerblock, self._fp)
555 yield part
556 yield part
556 headerblock = self._readpartheader()
557 headerblock = self._readpartheader()
557 self.ui.debug('end of bundle2 stream\n')
558 self.ui.debug('end of bundle2 stream\n')
558
559
559 def _readpartheader(self):
560 def _readpartheader(self):
560 """reads a part header size and return the bytes blob
561 """reads a part header size and return the bytes blob
561
562
562 returns None if empty"""
563 returns None if empty"""
563 headersize = self._unpack(_fpartheadersize)[0]
564 headersize = self._unpack(_fpartheadersize)[0]
564 if headersize < 0:
565 if headersize < 0:
565 raise error.BundleValueError('negative part header size: %i'
566 raise error.BundleValueError('negative part header size: %i'
566 % headersize)
567 % headersize)
567 self.ui.debug('part header size: %i\n' % headersize)
568 self.ui.debug('part header size: %i\n' % headersize)
568 if headersize:
569 if headersize:
569 return self._readexact(headersize)
570 return self._readexact(headersize)
570 return None
571 return None
571
572
572
573
573 class bundlepart(object):
574 class bundlepart(object):
574 """A bundle2 part contains application level payload
575 """A bundle2 part contains application level payload
575
576
576 The part `type` is used to route the part to the application level
577 The part `type` is used to route the part to the application level
577 handler.
578 handler.
578
579
579 The part payload is contained in ``part.data``. It could be raw bytes or a
580 The part payload is contained in ``part.data``. It could be raw bytes or a
580 generator of byte chunks.
581 generator of byte chunks.
581
582
582 You can add parameters to the part using the ``addparam`` method.
583 You can add parameters to the part using the ``addparam`` method.
583 Parameters can be either mandatory (default) or advisory. Remote side
584 Parameters can be either mandatory (default) or advisory. Remote side
584 should be able to safely ignore the advisory ones.
585 should be able to safely ignore the advisory ones.
585
586
586 Both data and parameters cannot be modified after the generation has begun.
587 Both data and parameters cannot be modified after the generation has begun.
587 """
588 """
588
589
589 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
590 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
590 data=''):
591 data=''):
591 self.id = None
592 self.id = None
592 self.type = parttype
593 self.type = parttype
593 self._data = data
594 self._data = data
594 self._mandatoryparams = list(mandatoryparams)
595 self._mandatoryparams = list(mandatoryparams)
595 self._advisoryparams = list(advisoryparams)
596 self._advisoryparams = list(advisoryparams)
596 # checking for duplicated entries
597 # checking for duplicated entries
597 self._seenparams = set()
598 self._seenparams = set()
598 for pname, __ in self._mandatoryparams + self._advisoryparams:
599 for pname, __ in self._mandatoryparams + self._advisoryparams:
599 if pname in self._seenparams:
600 if pname in self._seenparams:
600 raise RuntimeError('duplicated params: %s' % pname)
601 raise RuntimeError('duplicated params: %s' % pname)
601 self._seenparams.add(pname)
602 self._seenparams.add(pname)
602 # status of the part's generation:
603 # status of the part's generation:
603 # - None: not started,
604 # - None: not started,
604 # - False: currently generated,
605 # - False: currently generated,
605 # - True: generation done.
606 # - True: generation done.
606 self._generated = None
607 self._generated = None
607
608
608 # methods used to defines the part content
609 # methods used to defines the part content
609 def __setdata(self, data):
610 def __setdata(self, data):
610 if self._generated is not None:
611 if self._generated is not None:
611 raise error.ReadOnlyPartError('part is being generated')
612 raise error.ReadOnlyPartError('part is being generated')
612 self._data = data
613 self._data = data
613 def __getdata(self):
614 def __getdata(self):
614 return self._data
615 return self._data
615 data = property(__getdata, __setdata)
616 data = property(__getdata, __setdata)
616
617
617 @property
618 @property
618 def mandatoryparams(self):
619 def mandatoryparams(self):
619 # make it an immutable tuple to force people through ``addparam``
620 # make it an immutable tuple to force people through ``addparam``
620 return tuple(self._mandatoryparams)
621 return tuple(self._mandatoryparams)
621
622
622 @property
623 @property
623 def advisoryparams(self):
624 def advisoryparams(self):
624 # make it an immutable tuple to force people through ``addparam``
625 # make it an immutable tuple to force people through ``addparam``
625 return tuple(self._advisoryparams)
626 return tuple(self._advisoryparams)
626
627
627 def addparam(self, name, value='', mandatory=True):
628 def addparam(self, name, value='', mandatory=True):
628 if self._generated is not None:
629 if self._generated is not None:
629 raise error.ReadOnlyPartError('part is being generated')
630 raise error.ReadOnlyPartError('part is being generated')
630 if name in self._seenparams:
631 if name in self._seenparams:
631 raise ValueError('duplicated params: %s' % name)
632 raise ValueError('duplicated params: %s' % name)
632 self._seenparams.add(name)
633 self._seenparams.add(name)
633 params = self._advisoryparams
634 params = self._advisoryparams
634 if mandatory:
635 if mandatory:
635 params = self._mandatoryparams
636 params = self._mandatoryparams
636 params.append((name, value))
637 params.append((name, value))
637
638
638 # methods used to generates the bundle2 stream
639 # methods used to generates the bundle2 stream
639 def getchunks(self):
640 def getchunks(self):
640 if self._generated is not None:
641 if self._generated is not None:
641 raise RuntimeError('part can only be consumed once')
642 raise RuntimeError('part can only be consumed once')
642 self._generated = False
643 self._generated = False
643 #### header
644 #### header
644 ## parttype
645 ## parttype
645 header = [_pack(_fparttypesize, len(self.type)),
646 header = [_pack(_fparttypesize, len(self.type)),
646 self.type, _pack(_fpartid, self.id),
647 self.type, _pack(_fpartid, self.id),
647 ]
648 ]
648 ## parameters
649 ## parameters
649 # count
650 # count
650 manpar = self.mandatoryparams
651 manpar = self.mandatoryparams
651 advpar = self.advisoryparams
652 advpar = self.advisoryparams
652 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
653 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
653 # size
654 # size
654 parsizes = []
655 parsizes = []
655 for key, value in manpar:
656 for key, value in manpar:
656 parsizes.append(len(key))
657 parsizes.append(len(key))
657 parsizes.append(len(value))
658 parsizes.append(len(value))
658 for key, value in advpar:
659 for key, value in advpar:
659 parsizes.append(len(key))
660 parsizes.append(len(key))
660 parsizes.append(len(value))
661 parsizes.append(len(value))
661 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
662 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
662 header.append(paramsizes)
663 header.append(paramsizes)
663 # key, value
664 # key, value
664 for key, value in manpar:
665 for key, value in manpar:
665 header.append(key)
666 header.append(key)
666 header.append(value)
667 header.append(value)
667 for key, value in advpar:
668 for key, value in advpar:
668 header.append(key)
669 header.append(key)
669 header.append(value)
670 header.append(value)
670 ## finalize header
671 ## finalize header
671 headerchunk = ''.join(header)
672 headerchunk = ''.join(header)
672 yield _pack(_fpartheadersize, len(headerchunk))
673 yield _pack(_fpartheadersize, len(headerchunk))
673 yield headerchunk
674 yield headerchunk
674 ## payload
675 ## payload
675 for chunk in self._payloadchunks():
676 for chunk in self._payloadchunks():
676 yield _pack(_fpayloadsize, len(chunk))
677 yield _pack(_fpayloadsize, len(chunk))
677 yield chunk
678 yield chunk
678 # end of payload
679 # end of payload
679 yield _pack(_fpayloadsize, 0)
680 yield _pack(_fpayloadsize, 0)
680 self._generated = True
681 self._generated = True
681
682
682 def _payloadchunks(self):
683 def _payloadchunks(self):
683 """yield chunks of a the part payload
684 """yield chunks of a the part payload
684
685
685 Exists to handle the different methods to provide data to a part."""
686 Exists to handle the different methods to provide data to a part."""
686 # we only support fixed size data now.
687 # we only support fixed size data now.
687 # This will be improved in the future.
688 # This will be improved in the future.
688 if util.safehasattr(self.data, 'next'):
689 if util.safehasattr(self.data, 'next'):
689 buff = util.chunkbuffer(self.data)
690 buff = util.chunkbuffer(self.data)
690 chunk = buff.read(preferedchunksize)
691 chunk = buff.read(preferedchunksize)
691 while chunk:
692 while chunk:
692 yield chunk
693 yield chunk
693 chunk = buff.read(preferedchunksize)
694 chunk = buff.read(preferedchunksize)
694 elif len(self.data):
695 elif len(self.data):
695 yield self.data
696 yield self.data
696
697
697 class unbundlepart(unpackermixin):
698 class unbundlepart(unpackermixin):
698 """a bundle part read from a bundle"""
699 """a bundle part read from a bundle"""
699
700
700 def __init__(self, ui, header, fp):
701 def __init__(self, ui, header, fp):
701 super(unbundlepart, self).__init__(fp)
702 super(unbundlepart, self).__init__(fp)
702 self.ui = ui
703 self.ui = ui
703 # unbundle state attr
704 # unbundle state attr
704 self._headerdata = header
705 self._headerdata = header
705 self._headeroffset = 0
706 self._headeroffset = 0
706 self._initialized = False
707 self._initialized = False
707 self.consumed = False
708 self.consumed = False
708 # part data
709 # part data
709 self.id = None
710 self.id = None
710 self.type = None
711 self.type = None
711 self.mandatoryparams = None
712 self.mandatoryparams = None
712 self.advisoryparams = None
713 self.advisoryparams = None
713 self.params = None
714 self.params = None
714 self.mandatorykeys = ()
715 self.mandatorykeys = ()
715 self._payloadstream = None
716 self._payloadstream = None
716 self._readheader()
717 self._readheader()
717
718
718 def _fromheader(self, size):
719 def _fromheader(self, size):
719 """return the next <size> byte from the header"""
720 """return the next <size> byte from the header"""
720 offset = self._headeroffset
721 offset = self._headeroffset
721 data = self._headerdata[offset:(offset + size)]
722 data = self._headerdata[offset:(offset + size)]
722 self._headeroffset = offset + size
723 self._headeroffset = offset + size
723 return data
724 return data
724
725
725 def _unpackheader(self, format):
726 def _unpackheader(self, format):
726 """read given format from header
727 """read given format from header
727
728
728 This automatically compute the size of the format to read."""
729 This automatically compute the size of the format to read."""
729 data = self._fromheader(struct.calcsize(format))
730 data = self._fromheader(struct.calcsize(format))
730 return _unpack(format, data)
731 return _unpack(format, data)
731
732
732 def _initparams(self, mandatoryparams, advisoryparams):
733 def _initparams(self, mandatoryparams, advisoryparams):
733 """internal function to setup all logic related parameters"""
734 """internal function to setup all logic related parameters"""
734 # make it read only to prevent people touching it by mistake.
735 # make it read only to prevent people touching it by mistake.
735 self.mandatoryparams = tuple(mandatoryparams)
736 self.mandatoryparams = tuple(mandatoryparams)
736 self.advisoryparams = tuple(advisoryparams)
737 self.advisoryparams = tuple(advisoryparams)
737 # user friendly UI
738 # user friendly UI
738 self.params = dict(self.mandatoryparams)
739 self.params = dict(self.mandatoryparams)
739 self.params.update(dict(self.advisoryparams))
740 self.params.update(dict(self.advisoryparams))
740 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
741 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
741
742
742 def _readheader(self):
743 def _readheader(self):
743 """read the header and setup the object"""
744 """read the header and setup the object"""
744 typesize = self._unpackheader(_fparttypesize)[0]
745 typesize = self._unpackheader(_fparttypesize)[0]
745 self.type = self._fromheader(typesize)
746 self.type = self._fromheader(typesize)
746 self.ui.debug('part type: "%s"\n' % self.type)
747 self.ui.debug('part type: "%s"\n' % self.type)
747 self.id = self._unpackheader(_fpartid)[0]
748 self.id = self._unpackheader(_fpartid)[0]
748 self.ui.debug('part id: "%s"\n' % self.id)
749 self.ui.debug('part id: "%s"\n' % self.id)
749 ## reading parameters
750 ## reading parameters
750 # param count
751 # param count
751 mancount, advcount = self._unpackheader(_fpartparamcount)
752 mancount, advcount = self._unpackheader(_fpartparamcount)
752 self.ui.debug('part parameters: %i\n' % (mancount + advcount))
753 self.ui.debug('part parameters: %i\n' % (mancount + advcount))
753 # param size
754 # param size
754 fparamsizes = _makefpartparamsizes(mancount + advcount)
755 fparamsizes = _makefpartparamsizes(mancount + advcount)
755 paramsizes = self._unpackheader(fparamsizes)
756 paramsizes = self._unpackheader(fparamsizes)
756 # make it a list of couple again
757 # make it a list of couple again
757 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
758 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
758 # split mandatory from advisory
759 # split mandatory from advisory
759 mansizes = paramsizes[:mancount]
760 mansizes = paramsizes[:mancount]
760 advsizes = paramsizes[mancount:]
761 advsizes = paramsizes[mancount:]
761 # retrive param value
762 # retrive param value
762 manparams = []
763 manparams = []
763 for key, value in mansizes:
764 for key, value in mansizes:
764 manparams.append((self._fromheader(key), self._fromheader(value)))
765 manparams.append((self._fromheader(key), self._fromheader(value)))
765 advparams = []
766 advparams = []
766 for key, value in advsizes:
767 for key, value in advsizes:
767 advparams.append((self._fromheader(key), self._fromheader(value)))
768 advparams.append((self._fromheader(key), self._fromheader(value)))
768 self._initparams(manparams, advparams)
769 self._initparams(manparams, advparams)
769 ## part payload
770 ## part payload
770 def payloadchunks():
771 def payloadchunks():
771 payloadsize = self._unpack(_fpayloadsize)[0]
772 payloadsize = self._unpack(_fpayloadsize)[0]
772 self.ui.debug('payload chunk size: %i\n' % payloadsize)
773 self.ui.debug('payload chunk size: %i\n' % payloadsize)
773 while payloadsize:
774 while payloadsize:
774 if payloadsize < 0:
775 if payloadsize < 0:
775 msg = 'negative payload chunk size: %i' % payloadsize
776 msg = 'negative payload chunk size: %i' % payloadsize
776 raise error.BundleValueError(msg)
777 raise error.BundleValueError(msg)
777 yield self._readexact(payloadsize)
778 yield self._readexact(payloadsize)
778 payloadsize = self._unpack(_fpayloadsize)[0]
779 payloadsize = self._unpack(_fpayloadsize)[0]
779 self.ui.debug('payload chunk size: %i\n' % payloadsize)
780 self.ui.debug('payload chunk size: %i\n' % payloadsize)
780 self._payloadstream = util.chunkbuffer(payloadchunks())
781 self._payloadstream = util.chunkbuffer(payloadchunks())
781 # we read the data, tell it
782 # we read the data, tell it
782 self._initialized = True
783 self._initialized = True
783
784
784 def read(self, size=None):
785 def read(self, size=None):
785 """read payload data"""
786 """read payload data"""
786 if not self._initialized:
787 if not self._initialized:
787 self._readheader()
788 self._readheader()
788 if size is None:
789 if size is None:
789 data = self._payloadstream.read()
790 data = self._payloadstream.read()
790 else:
791 else:
791 data = self._payloadstream.read(size)
792 data = self._payloadstream.read(size)
792 if size is None or len(data) < size:
793 if size is None or len(data) < size:
793 self.consumed = True
794 self.consumed = True
794 return data
795 return data
795
796
796 capabilities = {'HG2Y': (),
797 capabilities = {'HG2Y': (),
797 'b2x:listkeys': (),
798 'b2x:listkeys': (),
798 'b2x:pushkey': (),
799 'b2x:pushkey': (),
799 'b2x:changegroup': (),
800 'b2x:changegroup': (),
801 'digests': tuple(sorted(util.DIGESTS.keys())),
802 'b2x:remote-changegroup': ('http', 'https'),
800 }
803 }
801
804
802 def getrepocaps(repo):
805 def getrepocaps(repo):
803 """return the bundle2 capabilities for a given repo
806 """return the bundle2 capabilities for a given repo
804
807
805 Exists to allow extensions (like evolution) to mutate the capabilities.
808 Exists to allow extensions (like evolution) to mutate the capabilities.
806 """
809 """
807 caps = capabilities.copy()
810 caps = capabilities.copy()
808 if obsolete.isenabled(repo, obsolete.exchangeopt):
811 if obsolete.isenabled(repo, obsolete.exchangeopt):
809 supportedformat = tuple('V%i' % v for v in obsolete.formats)
812 supportedformat = tuple('V%i' % v for v in obsolete.formats)
810 caps['b2x:obsmarkers'] = supportedformat
813 caps['b2x:obsmarkers'] = supportedformat
811 return caps
814 return caps
812
815
813 def bundle2caps(remote):
816 def bundle2caps(remote):
814 """return the bundlecapabilities of a peer as dict"""
817 """return the bundlecapabilities of a peer as dict"""
815 raw = remote.capable('bundle2-exp')
818 raw = remote.capable('bundle2-exp')
816 if not raw and raw != '':
819 if not raw and raw != '':
817 return {}
820 return {}
818 capsblob = urllib.unquote(remote.capable('bundle2-exp'))
821 capsblob = urllib.unquote(remote.capable('bundle2-exp'))
819 return decodecaps(capsblob)
822 return decodecaps(capsblob)
820
823
821 def obsmarkersversion(caps):
824 def obsmarkersversion(caps):
822 """extract the list of supported obsmarkers versions from a bundle2caps dict
825 """extract the list of supported obsmarkers versions from a bundle2caps dict
823 """
826 """
824 obscaps = caps.get('b2x:obsmarkers', ())
827 obscaps = caps.get('b2x:obsmarkers', ())
825 return [int(c[1:]) for c in obscaps if c.startswith('V')]
828 return [int(c[1:]) for c in obscaps if c.startswith('V')]
826
829
827 @parthandler('b2x:changegroup')
830 @parthandler('b2x:changegroup')
828 def handlechangegroup(op, inpart):
831 def handlechangegroup(op, inpart):
829 """apply a changegroup part on the repo
832 """apply a changegroup part on the repo
830
833
831 This is a very early implementation that will massive rework before being
834 This is a very early implementation that will massive rework before being
832 inflicted to any end-user.
835 inflicted to any end-user.
833 """
836 """
834 # Make sure we trigger a transaction creation
837 # Make sure we trigger a transaction creation
835 #
838 #
836 # The addchangegroup function will get a transaction object by itself, but
839 # The addchangegroup function will get a transaction object by itself, but
837 # we need to make sure we trigger the creation of a transaction object used
840 # we need to make sure we trigger the creation of a transaction object used
838 # for the whole processing scope.
841 # for the whole processing scope.
839 op.gettransaction()
842 op.gettransaction()
840 cg = changegroup.cg1unpacker(inpart, 'UN')
843 cg = changegroup.cg1unpacker(inpart, 'UN')
841 # the source and url passed here are overwritten by the one contained in
844 # the source and url passed here are overwritten by the one contained in
842 # the transaction.hookargs argument. So 'bundle2' is a placeholder
845 # the transaction.hookargs argument. So 'bundle2' is a placeholder
843 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
846 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
844 op.records.add('changegroup', {'return': ret})
847 op.records.add('changegroup', {'return': ret})
845 if op.reply is not None:
848 if op.reply is not None:
846 # This is definitly not the final form of this
849 # This is definitly not the final form of this
847 # return. But one need to start somewhere.
850 # return. But one need to start somewhere.
848 part = op.reply.newpart('b2x:reply:changegroup')
851 part = op.reply.newpart('b2x:reply:changegroup')
849 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
852 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
850 part.addparam('return', '%i' % ret, mandatory=False)
853 part.addparam('return', '%i' % ret, mandatory=False)
851 assert not inpart.read()
854 assert not inpart.read()
852
855
856 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
857 ['digest:%s' % k for k in util.DIGESTS.keys()])
858 @parthandler('b2x:remote-changegroup', _remotechangegroupparams)
859 def handleremotechangegroup(op, inpart):
860 """apply a bundle10 on the repo, given an url and validation information
861
862 All the information about the remote bundle to import are given as
863 parameters. The parameters include:
864 - url: the url to the bundle10.
865 - size: the bundle10 file size. It is used to validate what was
866 retrieved by the client matches the server knowledge about the bundle.
867 - digests: a space separated list of the digest types provided as
868 parameters.
869 - digest:<digest-type>: the hexadecimal representation of the digest with
870 that name. Like the size, it is used to validate what was retrieved by
871 the client matches what the server knows about the bundle.
872
873 When multiple digest types are given, all of them are checked.
874 """
875 try:
876 raw_url = inpart.params['url']
877 except KeyError:
878 raise util.Abort(_('remote-changegroup: missing "%s" param') % 'url')
879 parsed_url = util.url(raw_url)
880 if parsed_url.scheme not in capabilities['b2x:remote-changegroup']:
881 raise util.Abort(_('remote-changegroup does not support %s urls') %
882 parsed_url.scheme)
883
884 try:
885 size = int(inpart.params['size'])
886 except ValueError:
887 raise util.Abort(_('remote-changegroup: invalid value for param "%s"')
888 % 'size')
889 except KeyError:
890 raise util.Abort(_('remote-changegroup: missing "%s" param') % 'size')
891
892 digests = {}
893 for typ in inpart.params.get('digests', '').split():
894 param = 'digest:%s' % typ
895 try:
896 value = inpart.params[param]
897 except KeyError:
898 raise util.Abort(_('remote-changegroup: missing "%s" param') %
899 param)
900 digests[typ] = value
901
902 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
903
904 # Make sure we trigger a transaction creation
905 #
906 # The addchangegroup function will get a transaction object by itself, but
907 # we need to make sure we trigger the creation of a transaction object used
908 # for the whole processing scope.
909 op.gettransaction()
910 import exchange
911 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
912 if not isinstance(cg, changegroup.cg1unpacker):
913 raise util.Abort(_('%s: not a bundle version 1.0') %
914 util.hidepassword(raw_url))
915 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
916 op.records.add('changegroup', {'return': ret})
917 if op.reply is not None:
918 # This is definitly not the final form of this
919 # return. But one need to start somewhere.
920 part = op.reply.newpart('b2x:reply:changegroup')
921 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
922 part.addparam('return', '%i' % ret, mandatory=False)
923 try:
924 real_part.validate()
925 except util.Abort, e:
926 raise util.Abort(_('bundle at %s is corrupted:\n%s') %
927 (util.hidepassword(raw_url), str(e)))
928 assert not inpart.read()
929
853 @parthandler('b2x:reply:changegroup', ('return', 'in-reply-to'))
930 @parthandler('b2x:reply:changegroup', ('return', 'in-reply-to'))
854 def handlereplychangegroup(op, inpart):
931 def handlereplychangegroup(op, inpart):
855 ret = int(inpart.params['return'])
932 ret = int(inpart.params['return'])
856 replyto = int(inpart.params['in-reply-to'])
933 replyto = int(inpart.params['in-reply-to'])
857 op.records.add('changegroup', {'return': ret}, replyto)
934 op.records.add('changegroup', {'return': ret}, replyto)
858
935
859 @parthandler('b2x:check:heads')
936 @parthandler('b2x:check:heads')
860 def handlecheckheads(op, inpart):
937 def handlecheckheads(op, inpart):
861 """check that head of the repo did not change
938 """check that head of the repo did not change
862
939
863 This is used to detect a push race when using unbundle.
940 This is used to detect a push race when using unbundle.
864 This replaces the "heads" argument of unbundle."""
941 This replaces the "heads" argument of unbundle."""
865 h = inpart.read(20)
942 h = inpart.read(20)
866 heads = []
943 heads = []
867 while len(h) == 20:
944 while len(h) == 20:
868 heads.append(h)
945 heads.append(h)
869 h = inpart.read(20)
946 h = inpart.read(20)
870 assert not h
947 assert not h
871 if heads != op.repo.heads():
948 if heads != op.repo.heads():
872 raise error.PushRaced('repository changed while pushing - '
949 raise error.PushRaced('repository changed while pushing - '
873 'please try again')
950 'please try again')
874
951
875 @parthandler('b2x:output')
952 @parthandler('b2x:output')
876 def handleoutput(op, inpart):
953 def handleoutput(op, inpart):
877 """forward output captured on the server to the client"""
954 """forward output captured on the server to the client"""
878 for line in inpart.read().splitlines():
955 for line in inpart.read().splitlines():
879 op.ui.write(('remote: %s\n' % line))
956 op.ui.write(('remote: %s\n' % line))
880
957
881 @parthandler('b2x:replycaps')
958 @parthandler('b2x:replycaps')
882 def handlereplycaps(op, inpart):
959 def handlereplycaps(op, inpart):
883 """Notify that a reply bundle should be created
960 """Notify that a reply bundle should be created
884
961
885 The payload contains the capabilities information for the reply"""
962 The payload contains the capabilities information for the reply"""
886 caps = decodecaps(inpart.read())
963 caps = decodecaps(inpart.read())
887 if op.reply is None:
964 if op.reply is None:
888 op.reply = bundle20(op.ui, caps)
965 op.reply = bundle20(op.ui, caps)
889
966
890 @parthandler('b2x:error:abort', ('message', 'hint'))
967 @parthandler('b2x:error:abort', ('message', 'hint'))
891 def handlereplycaps(op, inpart):
968 def handlereplycaps(op, inpart):
892 """Used to transmit abort error over the wire"""
969 """Used to transmit abort error over the wire"""
893 raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint'))
970 raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint'))
894
971
895 @parthandler('b2x:error:unsupportedcontent', ('parttype', 'params'))
972 @parthandler('b2x:error:unsupportedcontent', ('parttype', 'params'))
896 def handlereplycaps(op, inpart):
973 def handlereplycaps(op, inpart):
897 """Used to transmit unknown content error over the wire"""
974 """Used to transmit unknown content error over the wire"""
898 kwargs = {}
975 kwargs = {}
899 parttype = inpart.params.get('parttype')
976 parttype = inpart.params.get('parttype')
900 if parttype is not None:
977 if parttype is not None:
901 kwargs['parttype'] = parttype
978 kwargs['parttype'] = parttype
902 params = inpart.params.get('params')
979 params = inpart.params.get('params')
903 if params is not None:
980 if params is not None:
904 kwargs['params'] = params.split('\0')
981 kwargs['params'] = params.split('\0')
905
982
906 raise error.UnsupportedPartError(**kwargs)
983 raise error.UnsupportedPartError(**kwargs)
907
984
908 @parthandler('b2x:error:pushraced', ('message',))
985 @parthandler('b2x:error:pushraced', ('message',))
909 def handlereplycaps(op, inpart):
986 def handlereplycaps(op, inpart):
910 """Used to transmit push race error over the wire"""
987 """Used to transmit push race error over the wire"""
911 raise error.ResponseError(_('push failed:'), inpart.params['message'])
988 raise error.ResponseError(_('push failed:'), inpart.params['message'])
912
989
913 @parthandler('b2x:listkeys', ('namespace',))
990 @parthandler('b2x:listkeys', ('namespace',))
914 def handlelistkeys(op, inpart):
991 def handlelistkeys(op, inpart):
915 """retrieve pushkey namespace content stored in a bundle2"""
992 """retrieve pushkey namespace content stored in a bundle2"""
916 namespace = inpart.params['namespace']
993 namespace = inpart.params['namespace']
917 r = pushkey.decodekeys(inpart.read())
994 r = pushkey.decodekeys(inpart.read())
918 op.records.add('listkeys', (namespace, r))
995 op.records.add('listkeys', (namespace, r))
919
996
920 @parthandler('b2x:pushkey', ('namespace', 'key', 'old', 'new'))
997 @parthandler('b2x:pushkey', ('namespace', 'key', 'old', 'new'))
921 def handlepushkey(op, inpart):
998 def handlepushkey(op, inpart):
922 """process a pushkey request"""
999 """process a pushkey request"""
923 dec = pushkey.decode
1000 dec = pushkey.decode
924 namespace = dec(inpart.params['namespace'])
1001 namespace = dec(inpart.params['namespace'])
925 key = dec(inpart.params['key'])
1002 key = dec(inpart.params['key'])
926 old = dec(inpart.params['old'])
1003 old = dec(inpart.params['old'])
927 new = dec(inpart.params['new'])
1004 new = dec(inpart.params['new'])
928 ret = op.repo.pushkey(namespace, key, old, new)
1005 ret = op.repo.pushkey(namespace, key, old, new)
929 record = {'namespace': namespace,
1006 record = {'namespace': namespace,
930 'key': key,
1007 'key': key,
931 'old': old,
1008 'old': old,
932 'new': new}
1009 'new': new}
933 op.records.add('pushkey', record)
1010 op.records.add('pushkey', record)
934 if op.reply is not None:
1011 if op.reply is not None:
935 rpart = op.reply.newpart('b2x:reply:pushkey')
1012 rpart = op.reply.newpart('b2x:reply:pushkey')
936 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1013 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
937 rpart.addparam('return', '%i' % ret, mandatory=False)
1014 rpart.addparam('return', '%i' % ret, mandatory=False)
938
1015
939 @parthandler('b2x:reply:pushkey', ('return', 'in-reply-to'))
1016 @parthandler('b2x:reply:pushkey', ('return', 'in-reply-to'))
940 def handlepushkeyreply(op, inpart):
1017 def handlepushkeyreply(op, inpart):
941 """retrieve the result of a pushkey request"""
1018 """retrieve the result of a pushkey request"""
942 ret = int(inpart.params['return'])
1019 ret = int(inpart.params['return'])
943 partid = int(inpart.params['in-reply-to'])
1020 partid = int(inpart.params['in-reply-to'])
944 op.records.add('pushkey', {'return': ret}, partid)
1021 op.records.add('pushkey', {'return': ret}, partid)
945
1022
946 @parthandler('b2x:obsmarkers')
1023 @parthandler('b2x:obsmarkers')
947 def handleobsmarker(op, inpart):
1024 def handleobsmarker(op, inpart):
948 """add a stream of obsmarkers to the repo"""
1025 """add a stream of obsmarkers to the repo"""
949 tr = op.gettransaction()
1026 tr = op.gettransaction()
950 new = op.repo.obsstore.mergemarkers(tr, inpart.read())
1027 new = op.repo.obsstore.mergemarkers(tr, inpart.read())
951 if new:
1028 if new:
952 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1029 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
953 op.records.add('obsmarkers', {'new': new})
1030 op.records.add('obsmarkers', {'new': new})
954 if op.reply is not None:
1031 if op.reply is not None:
955 rpart = op.reply.newpart('b2x:reply:obsmarkers')
1032 rpart = op.reply.newpart('b2x:reply:obsmarkers')
956 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1033 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
957 rpart.addparam('new', '%i' % new, mandatory=False)
1034 rpart.addparam('new', '%i' % new, mandatory=False)
958
1035
959
1036
960 @parthandler('b2x:reply:obsmarkers', ('new', 'in-reply-to'))
1037 @parthandler('b2x:reply:obsmarkers', ('new', 'in-reply-to'))
961 def handlepushkeyreply(op, inpart):
1038 def handlepushkeyreply(op, inpart):
962 """retrieve the result of a pushkey request"""
1039 """retrieve the result of a pushkey request"""
963 ret = int(inpart.params['new'])
1040 ret = int(inpart.params['new'])
964 partid = int(inpart.params['in-reply-to'])
1041 partid = int(inpart.params['in-reply-to'])
965 op.records.add('obsmarkers', {'new': ret}, partid)
1042 op.records.add('obsmarkers', {'new': ret}, partid)
General Comments 0
You need to be logged in to leave comments. Login now