##// 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
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
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
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 util
149 149 import struct
150 150 import urllib
151 151 import string
152 152 import obsolete
153 153 import pushkey
154 import url
154 155
155 156 import changegroup, error
156 157 from i18n import _
157 158
158 159 _pack = struct.pack
159 160 _unpack = struct.unpack
160 161
161 162 _magicstring = 'HG2Y'
162 163
163 164 _fstreamparamsize = '>i'
164 165 _fpartheadersize = '>i'
165 166 _fparttypesize = '>B'
166 167 _fpartid = '>I'
167 168 _fpayloadsize = '>i'
168 169 _fpartparamcount = '>BB'
169 170
170 171 preferedchunksize = 4096
171 172
172 173 def _makefpartparamsizes(nbparams):
173 174 """return a struct format to read part parameter sizes
174 175
175 176 The number parameters is variable so we need to build that format
176 177 dynamically.
177 178 """
178 179 return '>'+('BB'*nbparams)
179 180
180 181 parthandlermapping = {}
181 182
182 183 def parthandler(parttype, params=()):
183 184 """decorator that register a function as a bundle2 part handler
184 185
185 186 eg::
186 187
187 188 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
188 189 def myparttypehandler(...):
189 190 '''process a part of type "my part".'''
190 191 ...
191 192 """
192 193 def _decorator(func):
193 194 lparttype = parttype.lower() # enforce lower case matching.
194 195 assert lparttype not in parthandlermapping
195 196 parthandlermapping[lparttype] = func
196 197 func.params = frozenset(params)
197 198 return func
198 199 return _decorator
199 200
200 201 class unbundlerecords(object):
201 202 """keep record of what happens during and unbundle
202 203
203 204 New records are added using `records.add('cat', obj)`. Where 'cat' is a
204 205 category of record and obj is an arbitrary object.
205 206
206 207 `records['cat']` will return all entries of this category 'cat'.
207 208
208 209 Iterating on the object itself will yield `('category', obj)` tuples
209 210 for all entries.
210 211
211 212 All iterations happens in chronological order.
212 213 """
213 214
214 215 def __init__(self):
215 216 self._categories = {}
216 217 self._sequences = []
217 218 self._replies = {}
218 219
219 220 def add(self, category, entry, inreplyto=None):
220 221 """add a new record of a given category.
221 222
222 223 The entry can then be retrieved in the list returned by
223 224 self['category']."""
224 225 self._categories.setdefault(category, []).append(entry)
225 226 self._sequences.append((category, entry))
226 227 if inreplyto is not None:
227 228 self.getreplies(inreplyto).add(category, entry)
228 229
229 230 def getreplies(self, partid):
230 231 """get the subrecords that replies to a specific part"""
231 232 return self._replies.setdefault(partid, unbundlerecords())
232 233
233 234 def __getitem__(self, cat):
234 235 return tuple(self._categories.get(cat, ()))
235 236
236 237 def __iter__(self):
237 238 return iter(self._sequences)
238 239
239 240 def __len__(self):
240 241 return len(self._sequences)
241 242
242 243 def __nonzero__(self):
243 244 return bool(self._sequences)
244 245
245 246 class bundleoperation(object):
246 247 """an object that represents a single bundling process
247 248
248 249 Its purpose is to carry unbundle-related objects and states.
249 250
250 251 A new object should be created at the beginning of each bundle processing.
251 252 The object is to be returned by the processing function.
252 253
253 254 The object has very little content now it will ultimately contain:
254 255 * an access to the repo the bundle is applied to,
255 256 * a ui object,
256 257 * a way to retrieve a transaction to add changes to the repo,
257 258 * a way to record the result of processing each part,
258 259 * a way to construct a bundle response when applicable.
259 260 """
260 261
261 262 def __init__(self, repo, transactiongetter):
262 263 self.repo = repo
263 264 self.ui = repo.ui
264 265 self.records = unbundlerecords()
265 266 self.gettransaction = transactiongetter
266 267 self.reply = None
267 268
268 269 class TransactionUnavailable(RuntimeError):
269 270 pass
270 271
271 272 def _notransaction():
272 273 """default method to get a transaction while processing a bundle
273 274
274 275 Raise an exception to highlight the fact that no transaction was expected
275 276 to be created"""
276 277 raise TransactionUnavailable()
277 278
278 279 def processbundle(repo, unbundler, transactiongetter=_notransaction):
279 280 """This function process a bundle, apply effect to/from a repo
280 281
281 282 It iterates over each part then searches for and uses the proper handling
282 283 code to process the part. Parts are processed in order.
283 284
284 285 This is very early version of this function that will be strongly reworked
285 286 before final usage.
286 287
287 288 Unknown Mandatory part will abort the process.
288 289 """
289 290 op = bundleoperation(repo, transactiongetter)
290 291 # todo:
291 292 # - replace this is a init function soon.
292 293 # - exception catching
293 294 unbundler.params
294 295 iterparts = unbundler.iterparts()
295 296 part = None
296 297 try:
297 298 for part in iterparts:
298 299 _processpart(op, part)
299 300 except Exception, exc:
300 301 for part in iterparts:
301 302 # consume the bundle content
302 303 part.read()
303 304 # Small hack to let caller code distinguish exceptions from bundle2
304 305 # processing fron the ones from bundle1 processing. This is mostly
305 306 # needed to handle different return codes to unbundle according to the
306 307 # type of bundle. We should probably clean up or drop this return code
307 308 # craziness in a future version.
308 309 exc.duringunbundle2 = True
309 310 raise
310 311 return op
311 312
312 313 def _processpart(op, part):
313 314 """process a single part from a bundle
314 315
315 316 The part is guaranteed to have been fully consumed when the function exits
316 317 (even if an exception is raised)."""
317 318 try:
318 319 parttype = part.type
319 320 # part key are matched lower case
320 321 key = parttype.lower()
321 322 try:
322 323 handler = parthandlermapping.get(key)
323 324 if handler is None:
324 325 raise error.UnsupportedPartError(parttype=key)
325 326 op.ui.debug('found a handler for part %r\n' % parttype)
326 327 unknownparams = part.mandatorykeys - handler.params
327 328 if unknownparams:
328 329 unknownparams = list(unknownparams)
329 330 unknownparams.sort()
330 331 raise error.UnsupportedPartError(parttype=key,
331 332 params=unknownparams)
332 333 except error.UnsupportedPartError, exc:
333 334 if key != parttype: # mandatory parts
334 335 raise
335 336 op.ui.debug('ignoring unsupported advisory part %s\n' % exc)
336 337 return # skip to part processing
337 338
338 339 # handler is called outside the above try block so that we don't
339 340 # risk catching KeyErrors from anything other than the
340 341 # parthandlermapping lookup (any KeyError raised by handler()
341 342 # itself represents a defect of a different variety).
342 343 output = None
343 344 if op.reply is not None:
344 345 op.ui.pushbuffer(error=True)
345 346 output = ''
346 347 try:
347 348 handler(op, part)
348 349 finally:
349 350 if output is not None:
350 351 output = op.ui.popbuffer()
351 352 if output:
352 353 outpart = op.reply.newpart('b2x:output', data=output)
353 354 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
354 355 finally:
355 356 # consume the part content to not corrupt the stream.
356 357 part.read()
357 358
358 359
359 360 def decodecaps(blob):
360 361 """decode a bundle2 caps bytes blob into a dictionnary
361 362
362 363 The blob is a list of capabilities (one per line)
363 364 Capabilities may have values using a line of the form::
364 365
365 366 capability=value1,value2,value3
366 367
367 368 The values are always a list."""
368 369 caps = {}
369 370 for line in blob.splitlines():
370 371 if not line:
371 372 continue
372 373 if '=' not in line:
373 374 key, vals = line, ()
374 375 else:
375 376 key, vals = line.split('=', 1)
376 377 vals = vals.split(',')
377 378 key = urllib.unquote(key)
378 379 vals = [urllib.unquote(v) for v in vals]
379 380 caps[key] = vals
380 381 return caps
381 382
382 383 def encodecaps(caps):
383 384 """encode a bundle2 caps dictionary into a bytes blob"""
384 385 chunks = []
385 386 for ca in sorted(caps):
386 387 vals = caps[ca]
387 388 ca = urllib.quote(ca)
388 389 vals = [urllib.quote(v) for v in vals]
389 390 if vals:
390 391 ca = "%s=%s" % (ca, ','.join(vals))
391 392 chunks.append(ca)
392 393 return '\n'.join(chunks)
393 394
394 395 class bundle20(object):
395 396 """represent an outgoing bundle2 container
396 397
397 398 Use the `addparam` method to add stream level parameter. and `newpart` to
398 399 populate it. Then call `getchunks` to retrieve all the binary chunks of
399 400 data that compose the bundle2 container."""
400 401
401 402 def __init__(self, ui, capabilities=()):
402 403 self.ui = ui
403 404 self._params = []
404 405 self._parts = []
405 406 self.capabilities = dict(capabilities)
406 407
407 408 @property
408 409 def nbparts(self):
409 410 """total number of parts added to the bundler"""
410 411 return len(self._parts)
411 412
412 413 # methods used to defines the bundle2 content
413 414 def addparam(self, name, value=None):
414 415 """add a stream level parameter"""
415 416 if not name:
416 417 raise ValueError('empty parameter name')
417 418 if name[0] not in string.letters:
418 419 raise ValueError('non letter first character: %r' % name)
419 420 self._params.append((name, value))
420 421
421 422 def addpart(self, part):
422 423 """add a new part to the bundle2 container
423 424
424 425 Parts contains the actual applicative payload."""
425 426 assert part.id is None
426 427 part.id = len(self._parts) # very cheap counter
427 428 self._parts.append(part)
428 429
429 430 def newpart(self, typeid, *args, **kwargs):
430 431 """create a new part and add it to the containers
431 432
432 433 As the part is directly added to the containers. For now, this means
433 434 that any failure to properly initialize the part after calling
434 435 ``newpart`` should result in a failure of the whole bundling process.
435 436
436 437 You can still fall back to manually create and add if you need better
437 438 control."""
438 439 part = bundlepart(typeid, *args, **kwargs)
439 440 self.addpart(part)
440 441 return part
441 442
442 443 # methods used to generate the bundle2 stream
443 444 def getchunks(self):
444 445 self.ui.debug('start emission of %s stream\n' % _magicstring)
445 446 yield _magicstring
446 447 param = self._paramchunk()
447 448 self.ui.debug('bundle parameter: %s\n' % param)
448 449 yield _pack(_fstreamparamsize, len(param))
449 450 if param:
450 451 yield param
451 452
452 453 self.ui.debug('start of parts\n')
453 454 for part in self._parts:
454 455 self.ui.debug('bundle part: "%s"\n' % part.type)
455 456 for chunk in part.getchunks():
456 457 yield chunk
457 458 self.ui.debug('end of bundle\n')
458 459 yield _pack(_fpartheadersize, 0)
459 460
460 461 def _paramchunk(self):
461 462 """return a encoded version of all stream parameters"""
462 463 blocks = []
463 464 for par, value in self._params:
464 465 par = urllib.quote(par)
465 466 if value is not None:
466 467 value = urllib.quote(value)
467 468 par = '%s=%s' % (par, value)
468 469 blocks.append(par)
469 470 return ' '.join(blocks)
470 471
471 472 class unpackermixin(object):
472 473 """A mixin to extract bytes and struct data from a stream"""
473 474
474 475 def __init__(self, fp):
475 476 self._fp = fp
476 477
477 478 def _unpack(self, format):
478 479 """unpack this struct format from the stream"""
479 480 data = self._readexact(struct.calcsize(format))
480 481 return _unpack(format, data)
481 482
482 483 def _readexact(self, size):
483 484 """read exactly <size> bytes from the stream"""
484 485 return changegroup.readexactly(self._fp, size)
485 486
486 487
487 488 class unbundle20(unpackermixin):
488 489 """interpret a bundle2 stream
489 490
490 491 This class is fed with a binary stream and yields parts through its
491 492 `iterparts` methods."""
492 493
493 494 def __init__(self, ui, fp, header=None):
494 495 """If header is specified, we do not read it out of the stream."""
495 496 self.ui = ui
496 497 super(unbundle20, self).__init__(fp)
497 498 if header is None:
498 499 header = self._readexact(4)
499 500 magic, version = header[0:2], header[2:4]
500 501 if magic != 'HG':
501 502 raise util.Abort(_('not a Mercurial bundle'))
502 503 if version != '2Y':
503 504 raise util.Abort(_('unknown bundle version %s') % version)
504 505 self.ui.debug('start processing of %s stream\n' % header)
505 506
506 507 @util.propertycache
507 508 def params(self):
508 509 """dictionary of stream level parameters"""
509 510 self.ui.debug('reading bundle2 stream parameters\n')
510 511 params = {}
511 512 paramssize = self._unpack(_fstreamparamsize)[0]
512 513 if paramssize < 0:
513 514 raise error.BundleValueError('negative bundle param size: %i'
514 515 % paramssize)
515 516 if paramssize:
516 517 for p in self._readexact(paramssize).split(' '):
517 518 p = p.split('=', 1)
518 519 p = [urllib.unquote(i) for i in p]
519 520 if len(p) < 2:
520 521 p.append(None)
521 522 self._processparam(*p)
522 523 params[p[0]] = p[1]
523 524 return params
524 525
525 526 def _processparam(self, name, value):
526 527 """process a parameter, applying its effect if needed
527 528
528 529 Parameter starting with a lower case letter are advisory and will be
529 530 ignored when unknown. Those starting with an upper case letter are
530 531 mandatory and will this function will raise a KeyError when unknown.
531 532
532 533 Note: no option are currently supported. Any input will be either
533 534 ignored or failing.
534 535 """
535 536 if not name:
536 537 raise ValueError('empty parameter name')
537 538 if name[0] not in string.letters:
538 539 raise ValueError('non letter first character: %r' % name)
539 540 # Some logic will be later added here to try to process the option for
540 541 # a dict of known parameter.
541 542 if name[0].islower():
542 543 self.ui.debug("ignoring unknown parameter %r\n" % name)
543 544 else:
544 545 raise error.UnsupportedPartError(params=(name,))
545 546
546 547
547 548 def iterparts(self):
548 549 """yield all parts contained in the stream"""
549 550 # make sure param have been loaded
550 551 self.params
551 552 self.ui.debug('start extraction of bundle2 parts\n')
552 553 headerblock = self._readpartheader()
553 554 while headerblock is not None:
554 555 part = unbundlepart(self.ui, headerblock, self._fp)
555 556 yield part
556 557 headerblock = self._readpartheader()
557 558 self.ui.debug('end of bundle2 stream\n')
558 559
559 560 def _readpartheader(self):
560 561 """reads a part header size and return the bytes blob
561 562
562 563 returns None if empty"""
563 564 headersize = self._unpack(_fpartheadersize)[0]
564 565 if headersize < 0:
565 566 raise error.BundleValueError('negative part header size: %i'
566 567 % headersize)
567 568 self.ui.debug('part header size: %i\n' % headersize)
568 569 if headersize:
569 570 return self._readexact(headersize)
570 571 return None
571 572
572 573
573 574 class bundlepart(object):
574 575 """A bundle2 part contains application level payload
575 576
576 577 The part `type` is used to route the part to the application level
577 578 handler.
578 579
579 580 The part payload is contained in ``part.data``. It could be raw bytes or a
580 581 generator of byte chunks.
581 582
582 583 You can add parameters to the part using the ``addparam`` method.
583 584 Parameters can be either mandatory (default) or advisory. Remote side
584 585 should be able to safely ignore the advisory ones.
585 586
586 587 Both data and parameters cannot be modified after the generation has begun.
587 588 """
588 589
589 590 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
590 591 data=''):
591 592 self.id = None
592 593 self.type = parttype
593 594 self._data = data
594 595 self._mandatoryparams = list(mandatoryparams)
595 596 self._advisoryparams = list(advisoryparams)
596 597 # checking for duplicated entries
597 598 self._seenparams = set()
598 599 for pname, __ in self._mandatoryparams + self._advisoryparams:
599 600 if pname in self._seenparams:
600 601 raise RuntimeError('duplicated params: %s' % pname)
601 602 self._seenparams.add(pname)
602 603 # status of the part's generation:
603 604 # - None: not started,
604 605 # - False: currently generated,
605 606 # - True: generation done.
606 607 self._generated = None
607 608
608 609 # methods used to defines the part content
609 610 def __setdata(self, data):
610 611 if self._generated is not None:
611 612 raise error.ReadOnlyPartError('part is being generated')
612 613 self._data = data
613 614 def __getdata(self):
614 615 return self._data
615 616 data = property(__getdata, __setdata)
616 617
617 618 @property
618 619 def mandatoryparams(self):
619 620 # make it an immutable tuple to force people through ``addparam``
620 621 return tuple(self._mandatoryparams)
621 622
622 623 @property
623 624 def advisoryparams(self):
624 625 # make it an immutable tuple to force people through ``addparam``
625 626 return tuple(self._advisoryparams)
626 627
627 628 def addparam(self, name, value='', mandatory=True):
628 629 if self._generated is not None:
629 630 raise error.ReadOnlyPartError('part is being generated')
630 631 if name in self._seenparams:
631 632 raise ValueError('duplicated params: %s' % name)
632 633 self._seenparams.add(name)
633 634 params = self._advisoryparams
634 635 if mandatory:
635 636 params = self._mandatoryparams
636 637 params.append((name, value))
637 638
638 639 # methods used to generates the bundle2 stream
639 640 def getchunks(self):
640 641 if self._generated is not None:
641 642 raise RuntimeError('part can only be consumed once')
642 643 self._generated = False
643 644 #### header
644 645 ## parttype
645 646 header = [_pack(_fparttypesize, len(self.type)),
646 647 self.type, _pack(_fpartid, self.id),
647 648 ]
648 649 ## parameters
649 650 # count
650 651 manpar = self.mandatoryparams
651 652 advpar = self.advisoryparams
652 653 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
653 654 # size
654 655 parsizes = []
655 656 for key, value in manpar:
656 657 parsizes.append(len(key))
657 658 parsizes.append(len(value))
658 659 for key, value in advpar:
659 660 parsizes.append(len(key))
660 661 parsizes.append(len(value))
661 662 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
662 663 header.append(paramsizes)
663 664 # key, value
664 665 for key, value in manpar:
665 666 header.append(key)
666 667 header.append(value)
667 668 for key, value in advpar:
668 669 header.append(key)
669 670 header.append(value)
670 671 ## finalize header
671 672 headerchunk = ''.join(header)
672 673 yield _pack(_fpartheadersize, len(headerchunk))
673 674 yield headerchunk
674 675 ## payload
675 676 for chunk in self._payloadchunks():
676 677 yield _pack(_fpayloadsize, len(chunk))
677 678 yield chunk
678 679 # end of payload
679 680 yield _pack(_fpayloadsize, 0)
680 681 self._generated = True
681 682
682 683 def _payloadchunks(self):
683 684 """yield chunks of a the part payload
684 685
685 686 Exists to handle the different methods to provide data to a part."""
686 687 # we only support fixed size data now.
687 688 # This will be improved in the future.
688 689 if util.safehasattr(self.data, 'next'):
689 690 buff = util.chunkbuffer(self.data)
690 691 chunk = buff.read(preferedchunksize)
691 692 while chunk:
692 693 yield chunk
693 694 chunk = buff.read(preferedchunksize)
694 695 elif len(self.data):
695 696 yield self.data
696 697
697 698 class unbundlepart(unpackermixin):
698 699 """a bundle part read from a bundle"""
699 700
700 701 def __init__(self, ui, header, fp):
701 702 super(unbundlepart, self).__init__(fp)
702 703 self.ui = ui
703 704 # unbundle state attr
704 705 self._headerdata = header
705 706 self._headeroffset = 0
706 707 self._initialized = False
707 708 self.consumed = False
708 709 # part data
709 710 self.id = None
710 711 self.type = None
711 712 self.mandatoryparams = None
712 713 self.advisoryparams = None
713 714 self.params = None
714 715 self.mandatorykeys = ()
715 716 self._payloadstream = None
716 717 self._readheader()
717 718
718 719 def _fromheader(self, size):
719 720 """return the next <size> byte from the header"""
720 721 offset = self._headeroffset
721 722 data = self._headerdata[offset:(offset + size)]
722 723 self._headeroffset = offset + size
723 724 return data
724 725
725 726 def _unpackheader(self, format):
726 727 """read given format from header
727 728
728 729 This automatically compute the size of the format to read."""
729 730 data = self._fromheader(struct.calcsize(format))
730 731 return _unpack(format, data)
731 732
732 733 def _initparams(self, mandatoryparams, advisoryparams):
733 734 """internal function to setup all logic related parameters"""
734 735 # make it read only to prevent people touching it by mistake.
735 736 self.mandatoryparams = tuple(mandatoryparams)
736 737 self.advisoryparams = tuple(advisoryparams)
737 738 # user friendly UI
738 739 self.params = dict(self.mandatoryparams)
739 740 self.params.update(dict(self.advisoryparams))
740 741 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
741 742
742 743 def _readheader(self):
743 744 """read the header and setup the object"""
744 745 typesize = self._unpackheader(_fparttypesize)[0]
745 746 self.type = self._fromheader(typesize)
746 747 self.ui.debug('part type: "%s"\n' % self.type)
747 748 self.id = self._unpackheader(_fpartid)[0]
748 749 self.ui.debug('part id: "%s"\n' % self.id)
749 750 ## reading parameters
750 751 # param count
751 752 mancount, advcount = self._unpackheader(_fpartparamcount)
752 753 self.ui.debug('part parameters: %i\n' % (mancount + advcount))
753 754 # param size
754 755 fparamsizes = _makefpartparamsizes(mancount + advcount)
755 756 paramsizes = self._unpackheader(fparamsizes)
756 757 # make it a list of couple again
757 758 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
758 759 # split mandatory from advisory
759 760 mansizes = paramsizes[:mancount]
760 761 advsizes = paramsizes[mancount:]
761 762 # retrive param value
762 763 manparams = []
763 764 for key, value in mansizes:
764 765 manparams.append((self._fromheader(key), self._fromheader(value)))
765 766 advparams = []
766 767 for key, value in advsizes:
767 768 advparams.append((self._fromheader(key), self._fromheader(value)))
768 769 self._initparams(manparams, advparams)
769 770 ## part payload
770 771 def payloadchunks():
771 772 payloadsize = self._unpack(_fpayloadsize)[0]
772 773 self.ui.debug('payload chunk size: %i\n' % payloadsize)
773 774 while payloadsize:
774 775 if payloadsize < 0:
775 776 msg = 'negative payload chunk size: %i' % payloadsize
776 777 raise error.BundleValueError(msg)
777 778 yield self._readexact(payloadsize)
778 779 payloadsize = self._unpack(_fpayloadsize)[0]
779 780 self.ui.debug('payload chunk size: %i\n' % payloadsize)
780 781 self._payloadstream = util.chunkbuffer(payloadchunks())
781 782 # we read the data, tell it
782 783 self._initialized = True
783 784
784 785 def read(self, size=None):
785 786 """read payload data"""
786 787 if not self._initialized:
787 788 self._readheader()
788 789 if size is None:
789 790 data = self._payloadstream.read()
790 791 else:
791 792 data = self._payloadstream.read(size)
792 793 if size is None or len(data) < size:
793 794 self.consumed = True
794 795 return data
795 796
796 797 capabilities = {'HG2Y': (),
797 798 'b2x:listkeys': (),
798 799 'b2x:pushkey': (),
799 800 'b2x:changegroup': (),
801 'digests': tuple(sorted(util.DIGESTS.keys())),
802 'b2x:remote-changegroup': ('http', 'https'),
800 803 }
801 804
802 805 def getrepocaps(repo):
803 806 """return the bundle2 capabilities for a given repo
804 807
805 808 Exists to allow extensions (like evolution) to mutate the capabilities.
806 809 """
807 810 caps = capabilities.copy()
808 811 if obsolete.isenabled(repo, obsolete.exchangeopt):
809 812 supportedformat = tuple('V%i' % v for v in obsolete.formats)
810 813 caps['b2x:obsmarkers'] = supportedformat
811 814 return caps
812 815
813 816 def bundle2caps(remote):
814 817 """return the bundlecapabilities of a peer as dict"""
815 818 raw = remote.capable('bundle2-exp')
816 819 if not raw and raw != '':
817 820 return {}
818 821 capsblob = urllib.unquote(remote.capable('bundle2-exp'))
819 822 return decodecaps(capsblob)
820 823
821 824 def obsmarkersversion(caps):
822 825 """extract the list of supported obsmarkers versions from a bundle2caps dict
823 826 """
824 827 obscaps = caps.get('b2x:obsmarkers', ())
825 828 return [int(c[1:]) for c in obscaps if c.startswith('V')]
826 829
827 830 @parthandler('b2x:changegroup')
828 831 def handlechangegroup(op, inpart):
829 832 """apply a changegroup part on the repo
830 833
831 834 This is a very early implementation that will massive rework before being
832 835 inflicted to any end-user.
833 836 """
834 837 # Make sure we trigger a transaction creation
835 838 #
836 839 # The addchangegroup function will get a transaction object by itself, but
837 840 # we need to make sure we trigger the creation of a transaction object used
838 841 # for the whole processing scope.
839 842 op.gettransaction()
840 843 cg = changegroup.cg1unpacker(inpart, 'UN')
841 844 # the source and url passed here are overwritten by the one contained in
842 845 # the transaction.hookargs argument. So 'bundle2' is a placeholder
843 846 ret = changegroup.addchangegroup(op.repo, cg, 'bundle2', 'bundle2')
844 847 op.records.add('changegroup', {'return': ret})
845 848 if op.reply is not None:
846 849 # This is definitly not the final form of this
847 850 # return. But one need to start somewhere.
848 851 part = op.reply.newpart('b2x:reply:changegroup')
849 852 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
850 853 part.addparam('return', '%i' % ret, mandatory=False)
851 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 930 @parthandler('b2x:reply:changegroup', ('return', 'in-reply-to'))
854 931 def handlereplychangegroup(op, inpart):
855 932 ret = int(inpart.params['return'])
856 933 replyto = int(inpart.params['in-reply-to'])
857 934 op.records.add('changegroup', {'return': ret}, replyto)
858 935
859 936 @parthandler('b2x:check:heads')
860 937 def handlecheckheads(op, inpart):
861 938 """check that head of the repo did not change
862 939
863 940 This is used to detect a push race when using unbundle.
864 941 This replaces the "heads" argument of unbundle."""
865 942 h = inpart.read(20)
866 943 heads = []
867 944 while len(h) == 20:
868 945 heads.append(h)
869 946 h = inpart.read(20)
870 947 assert not h
871 948 if heads != op.repo.heads():
872 949 raise error.PushRaced('repository changed while pushing - '
873 950 'please try again')
874 951
875 952 @parthandler('b2x:output')
876 953 def handleoutput(op, inpart):
877 954 """forward output captured on the server to the client"""
878 955 for line in inpart.read().splitlines():
879 956 op.ui.write(('remote: %s\n' % line))
880 957
881 958 @parthandler('b2x:replycaps')
882 959 def handlereplycaps(op, inpart):
883 960 """Notify that a reply bundle should be created
884 961
885 962 The payload contains the capabilities information for the reply"""
886 963 caps = decodecaps(inpart.read())
887 964 if op.reply is None:
888 965 op.reply = bundle20(op.ui, caps)
889 966
890 967 @parthandler('b2x:error:abort', ('message', 'hint'))
891 968 def handlereplycaps(op, inpart):
892 969 """Used to transmit abort error over the wire"""
893 970 raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint'))
894 971
895 972 @parthandler('b2x:error:unsupportedcontent', ('parttype', 'params'))
896 973 def handlereplycaps(op, inpart):
897 974 """Used to transmit unknown content error over the wire"""
898 975 kwargs = {}
899 976 parttype = inpart.params.get('parttype')
900 977 if parttype is not None:
901 978 kwargs['parttype'] = parttype
902 979 params = inpart.params.get('params')
903 980 if params is not None:
904 981 kwargs['params'] = params.split('\0')
905 982
906 983 raise error.UnsupportedPartError(**kwargs)
907 984
908 985 @parthandler('b2x:error:pushraced', ('message',))
909 986 def handlereplycaps(op, inpart):
910 987 """Used to transmit push race error over the wire"""
911 988 raise error.ResponseError(_('push failed:'), inpart.params['message'])
912 989
913 990 @parthandler('b2x:listkeys', ('namespace',))
914 991 def handlelistkeys(op, inpart):
915 992 """retrieve pushkey namespace content stored in a bundle2"""
916 993 namespace = inpart.params['namespace']
917 994 r = pushkey.decodekeys(inpart.read())
918 995 op.records.add('listkeys', (namespace, r))
919 996
920 997 @parthandler('b2x:pushkey', ('namespace', 'key', 'old', 'new'))
921 998 def handlepushkey(op, inpart):
922 999 """process a pushkey request"""
923 1000 dec = pushkey.decode
924 1001 namespace = dec(inpart.params['namespace'])
925 1002 key = dec(inpart.params['key'])
926 1003 old = dec(inpart.params['old'])
927 1004 new = dec(inpart.params['new'])
928 1005 ret = op.repo.pushkey(namespace, key, old, new)
929 1006 record = {'namespace': namespace,
930 1007 'key': key,
931 1008 'old': old,
932 1009 'new': new}
933 1010 op.records.add('pushkey', record)
934 1011 if op.reply is not None:
935 1012 rpart = op.reply.newpart('b2x:reply:pushkey')
936 1013 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
937 1014 rpart.addparam('return', '%i' % ret, mandatory=False)
938 1015
939 1016 @parthandler('b2x:reply:pushkey', ('return', 'in-reply-to'))
940 1017 def handlepushkeyreply(op, inpart):
941 1018 """retrieve the result of a pushkey request"""
942 1019 ret = int(inpart.params['return'])
943 1020 partid = int(inpart.params['in-reply-to'])
944 1021 op.records.add('pushkey', {'return': ret}, partid)
945 1022
946 1023 @parthandler('b2x:obsmarkers')
947 1024 def handleobsmarker(op, inpart):
948 1025 """add a stream of obsmarkers to the repo"""
949 1026 tr = op.gettransaction()
950 1027 new = op.repo.obsstore.mergemarkers(tr, inpart.read())
951 1028 if new:
952 1029 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
953 1030 op.records.add('obsmarkers', {'new': new})
954 1031 if op.reply is not None:
955 1032 rpart = op.reply.newpart('b2x:reply:obsmarkers')
956 1033 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
957 1034 rpart.addparam('new', '%i' % new, mandatory=False)
958 1035
959 1036
960 1037 @parthandler('b2x:reply:obsmarkers', ('new', 'in-reply-to'))
961 1038 def handlepushkeyreply(op, inpart):
962 1039 """retrieve the result of a pushkey request"""
963 1040 ret = int(inpart.params['new'])
964 1041 partid = int(inpart.params['in-reply-to'])
965 1042 op.records.add('obsmarkers', {'new': ret}, partid)
General Comments 0
You need to be logged in to leave comments. Login now