~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-04-09 20:23:07 UTC
  • mfrom: (4265.1.4 bbc-merge)
  • Revision ID: pqm@pqm.ubuntu.com-20090409202307-n0depb16qepoe21o
(jam) Change _fetch_uses_deltas = False for CHK repos until we can
        write a better fix.

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
from cStringIO import StringIO
18
18
import os
 
19
import socket
19
20
import sys
20
 
import tempfile
 
21
import threading
21
22
 
22
23
from bzrlib import (
23
24
    bzrdir,
24
25
    errors,
25
26
    inventory,
 
27
    osutils,
26
28
    repository,
 
29
    revision as _mod_revision,
27
30
    treebuilder,
28
31
    )
29
 
from bzrlib.builtins import _merge_helper
30
 
from bzrlib.bzrdir import BzrDir
 
32
from bzrlib.bundle import read_mergeable_from_url
31
33
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
32
34
from bzrlib.bundle.bundle_data import BundleTree
33
 
from bzrlib.bundle.serializer import write_bundle, read_bundle
 
35
from bzrlib.bzrdir import BzrDir
 
36
from bzrlib.directory_service import directories
 
37
from bzrlib.bundle.serializer import write_bundle, read_bundle, v09, v4
34
38
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
35
39
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
 
40
from bzrlib.bundle.serializer.v4 import BundleSerializerV4
36
41
from bzrlib.branch import Branch
37
42
from bzrlib.diff import internal_diff
38
 
from bzrlib.errors import (BzrError, TestamentMismatch, NotABundle, BadBundle, 
39
 
                           NoSuchFile,)
40
43
from bzrlib.merge import Merge3Merger
41
44
from bzrlib.repofmt import knitrepo
42
 
from bzrlib.osutils import has_symlinks, sha_file
43
 
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
44
 
                          TestCase, TestSkipped)
 
45
from bzrlib.osutils import sha_file, sha_string
 
46
from bzrlib.tests import (
 
47
    SymlinkFeature,
 
48
    TestCase,
 
49
    TestCaseInTempDir,
 
50
    TestCaseWithTransport,
 
51
    TestSkipped,
 
52
    test_read_bundle,
 
53
    test_commit,
 
54
    )
45
55
from bzrlib.transform import TreeTransform
46
 
from bzrlib.workingtree import WorkingTree
47
56
 
48
57
 
49
58
class MockTree(object):
97
106
        elif kind == 'symlink':
98
107
            ie = InventoryLink(file_id, name, parent_id)
99
108
        else:
100
 
            raise BzrError('unknown kind %r' % kind)
 
109
            raise errors.BzrError('unknown kind %r' % kind)
101
110
        ie.text_sha1 = text_sha_1
102
111
        ie.text_size = text_size
103
112
        return ie
105
114
    def add_dir(self, file_id, path):
106
115
        self.paths[file_id] = path
107
116
        self.ids[path] = file_id
108
 
    
 
117
 
109
118
    def add_file(self, file_id, path, contents):
110
119
        self.add_dir(file_id, path)
111
120
        self.contents[file_id] = contents
142
151
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
143
152
        mtree.add_dir("d", "grandparent/alt_parent")
144
153
        return BundleTree(mtree, ''), mtree
145
 
        
 
154
 
146
155
    def test_renames(self):
147
156
        """Ensure that file renames have the proper effect on children"""
148
157
        btree = self.make_tree_1()[0]
149
158
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
150
 
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
159
        self.assertEqual(btree.old_path("grandparent/parent"),
151
160
                         "grandparent/parent")
152
161
        self.assertEqual(btree.old_path("grandparent/parent/file"),
153
162
                         "grandparent/parent/file")
160
169
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
161
170
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
162
171
 
163
 
        assert btree.path2id("grandparent2") is None
164
 
        assert btree.path2id("grandparent2/parent") is None
165
 
        assert btree.path2id("grandparent2/parent/file") is None
 
172
        self.assertTrue(btree.path2id("grandparent2") is None)
 
173
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
174
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
166
175
 
167
176
        btree.note_rename("grandparent", "grandparent2")
168
 
        assert btree.old_path("grandparent") is None
169
 
        assert btree.old_path("grandparent/parent") is None
170
 
        assert btree.old_path("grandparent/parent/file") is None
 
177
        self.assertTrue(btree.old_path("grandparent") is None)
 
178
        self.assertTrue(btree.old_path("grandparent/parent") is None)
 
179
        self.assertTrue(btree.old_path("grandparent/parent/file") is None)
171
180
 
172
181
        self.assertEqual(btree.id2path("a"), "grandparent2")
173
182
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
177
186
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
178
187
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
179
188
 
180
 
        assert btree.path2id("grandparent") is None
181
 
        assert btree.path2id("grandparent/parent") is None
182
 
        assert btree.path2id("grandparent/parent/file") is None
 
189
        self.assertTrue(btree.path2id("grandparent") is None)
 
190
        self.assertTrue(btree.path2id("grandparent/parent") is None)
 
191
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
183
192
 
184
193
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
185
194
        self.assertEqual(btree.id2path("a"), "grandparent2")
190
199
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
191
200
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
192
201
 
193
 
        assert btree.path2id("grandparent2/parent") is None
194
 
        assert btree.path2id("grandparent2/parent/file") is None
 
202
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
203
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
195
204
 
196
 
        btree.note_rename("grandparent/parent/file", 
 
205
        btree.note_rename("grandparent/parent/file",
197
206
                          "grandparent2/parent2/file2")
198
207
        self.assertEqual(btree.id2path("a"), "grandparent2")
199
208
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
203
212
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
204
213
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
205
214
 
206
 
        assert btree.path2id("grandparent2/parent2/file") is None
 
215
        self.assertTrue(btree.path2id("grandparent2/parent2/file") is None)
207
216
 
208
217
    def test_moves(self):
209
218
        """Ensure that file moves have the proper effect on children"""
210
219
        btree = self.make_tree_1()[0]
211
 
        btree.note_rename("grandparent/parent/file", 
 
220
        btree.note_rename("grandparent/parent/file",
212
221
                          "grandparent/alt_parent/file")
213
222
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
214
223
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
215
 
        assert btree.path2id("grandparent/parent/file") is None
 
224
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
216
225
 
217
226
    def unified_diff(self, old, new):
218
227
        out = StringIO()
222
231
 
223
232
    def make_tree_2(self):
224
233
        btree = self.make_tree_1()[0]
225
 
        btree.note_rename("grandparent/parent/file", 
 
234
        btree.note_rename("grandparent/parent/file",
226
235
                          "grandparent/alt_parent/file")
227
 
        assert btree.id2path("e") is None
228
 
        assert btree.path2id("grandparent/parent/file") is None
 
236
        self.assertTrue(btree.id2path("e") is None)
 
237
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
229
238
        btree.note_id("e", "grandparent/parent/file")
230
239
        return btree
231
240
 
257
266
    def make_tree_3(self):
258
267
        btree, mtree = self.make_tree_1()
259
268
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
260
 
        btree.note_rename("grandparent/parent/file", 
 
269
        btree.note_rename("grandparent/parent/file",
261
270
                          "grandparent/alt_parent/file")
262
 
        btree.note_rename("grandparent/parent/topping", 
 
271
        btree.note_rename("grandparent/parent/topping",
263
272
                          "grandparent/alt_parent/stopping")
264
273
        return btree
265
274
 
289
298
        btree = self.make_tree_1()[0]
290
299
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
291
300
        btree.note_deletion("grandparent/parent/file")
292
 
        assert btree.id2path("c") is None
293
 
        assert btree.path2id("grandparent/parent/file") is None
 
301
        self.assertTrue(btree.id2path("c") is None)
 
302
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
294
303
 
295
304
    def sorted_ids(self, tree):
296
305
        ids = list(tree)
304
313
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
305
314
        btree.note_deletion("grandparent/parent/file")
306
315
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
307
 
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
316
        btree.note_last_changed("grandparent/alt_parent/fool",
308
317
                                "revisionidiguess")
309
318
        self.assertEqual(self.sorted_ids(btree),
310
319
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
317
326
        format.repository_format = knitrepo.RepositoryFormatKnit3()
318
327
        serializer = BundleSerializerV08('0.8')
319
328
        b = self.make_branch('.', format=format)
320
 
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write, 
 
329
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write,
321
330
                          b.repository, [], {}, StringIO())
322
331
 
323
332
    def test_matched_bundle(self):
336
345
        source.commit('one', rev_id='one-id')
337
346
        source.commit('two', rev_id='two-id')
338
347
        text = StringIO()
339
 
        write_bundle(source.branch.repository, 'two-id', None, text, 
 
348
        write_bundle(source.branch.repository, 'two-id', 'null:', text,
340
349
                     format='0.9')
341
350
        text.seek(0)
342
351
 
343
352
        format = bzrdir.BzrDirMetaFormat1()
344
353
        format.repository_format = knitrepo.RepositoryFormatKnit1()
345
354
        target = self.make_branch('target', format=format)
346
 
        self.assertRaises(errors.IncompatibleRevision, install_bundle, 
 
355
        self.assertRaises(errors.IncompatibleRevision, install_bundle,
347
356
                          target.repository, read_bundle(text))
348
357
 
349
358
 
350
 
class V08BundleTester(TestCaseWithTransport):
351
 
 
352
 
    format = '0.8'
 
359
class BundleTester(object):
353
360
 
354
361
    def bzrdir_format(self):
355
362
        format = bzrdir.BzrDirMetaFormat1()
368
375
 
369
376
    def create_bundle_text(self, base_rev_id, rev_id):
370
377
        bundle_txt = StringIO()
371
 
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
378
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
372
379
                               bundle_txt, format=self.format)
373
380
        bundle_txt.seek(0)
374
 
        self.assertEqual(bundle_txt.readline(), 
 
381
        self.assertEqual(bundle_txt.readline(),
375
382
                         '# Bazaar revision bundle v%s\n' % self.format)
376
383
        self.assertEqual(bundle_txt.readline(), '#\n')
377
384
 
378
385
        rev = self.b1.repository.get_revision(rev_id)
379
386
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
380
387
                         u'# message:\n')
381
 
 
382
 
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
383
388
        bundle_txt.seek(0)
384
389
        return bundle_txt, rev_ids
385
390
 
387
392
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
388
393
        Make sure that the text generated is valid, and that it
389
394
        can be applied against the base, and generate the same information.
390
 
        
391
 
        :return: The in-memory bundle 
 
395
 
 
396
        :return: The in-memory bundle
392
397
        """
393
398
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
394
399
 
395
 
        # This should also validate the generated bundle 
 
400
        # This should also validate the generated bundle
396
401
        bundle = read_bundle(bundle_txt)
397
402
        repository = self.b1.repository
398
403
        for bundle_rev in bundle.real_revisions:
402
407
            # it
403
408
            branch_rev = repository.get_revision(bundle_rev.revision_id)
404
409
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
405
 
                      'timestamp', 'timezone', 'message', 'committer', 
 
410
                      'timestamp', 'timezone', 'message', 'committer',
406
411
                      'parent_ids', 'properties'):
407
 
                self.assertEqual(getattr(branch_rev, a), 
 
412
                self.assertEqual(getattr(branch_rev, a),
408
413
                                 getattr(bundle_rev, a))
409
 
            self.assertEqual(len(branch_rev.parent_ids), 
 
414
            self.assertEqual(len(branch_rev.parent_ids),
410
415
                             len(bundle_rev.parent_ids))
411
 
        self.assertEqual(rev_ids, 
 
416
        self.assertEqual(rev_ids,
412
417
                         [r.revision_id for r in bundle.real_revisions])
413
418
        self.valid_apply_bundle(base_rev_id, bundle,
414
419
                                   checkout_dir=checkout_dir)
418
423
    def get_invalid_bundle(self, base_rev_id, rev_id):
419
424
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
420
425
        Munge the text so that it's invalid.
421
 
        
 
426
 
422
427
        :return: The in-memory bundle
423
428
        """
424
429
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
425
 
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
430
        new_text = bundle_txt.getvalue().replace('executable:no',
426
431
                                               'executable:yes')
427
432
        bundle_txt = StringIO(new_text)
428
433
        bundle = read_bundle(bundle_txt)
429
434
        self.valid_apply_bundle(base_rev_id, bundle)
430
 
        return bundle 
 
435
        return bundle
431
436
 
432
437
    def test_non_bundle(self):
433
 
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
438
        self.assertRaises(errors.NotABundle,
 
439
                          read_bundle, StringIO('#!/bin/sh\n'))
434
440
 
435
441
    def test_malformed(self):
436
 
        self.assertRaises(BadBundle, read_bundle, 
 
442
        self.assertRaises(errors.BadBundle, read_bundle,
437
443
                          StringIO('# Bazaar revision bundle v'))
438
444
 
439
445
    def test_crlf_bundle(self):
440
446
        try:
441
447
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
442
 
        except BadBundle:
 
448
        except errors.BadBundle:
443
449
            # It is currently permitted for bundles with crlf line endings to
444
450
            # make read_bundle raise a BadBundle, but this should be fixed.
445
451
            # Anything else, especially NotABundle, is an error.
450
456
        """
451
457
 
452
458
        if checkout_dir is None:
453
 
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
459
            checkout_dir = osutils.mkdtemp(prefix='test-branch-', dir='.')
454
460
        else:
455
461
            if not os.path.exists(checkout_dir):
456
462
                os.mkdir(checkout_dir)
457
463
        tree = self.make_branch_and_tree(checkout_dir)
458
464
        s = StringIO()
459
 
        ancestors = write_bundle(self.b1.repository, rev_id, None, s,
 
465
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
460
466
                                 format=self.format)
461
467
        s.seek(0)
462
 
        assert isinstance(s.getvalue(), str), (
463
 
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
468
        self.assertIsInstance(s.getvalue(), str)
464
469
        install_bundle(tree.branch.repository, read_bundle(s))
465
470
        for ancestor in ancestors:
466
471
            old = self.b1.repository.revision_tree(ancestor)
467
472
            new = tree.branch.repository.revision_tree(ancestor)
468
 
 
469
 
            # Check that there aren't any inventory level changes
470
 
            delta = new.changes_from(old)
471
 
            self.assertFalse(delta.has_changed(),
472
 
                             'Revision %s not copied correctly.'
473
 
                             % (ancestor,))
474
 
 
475
 
            # Now check that the file contents are all correct
476
 
            for inventory_id in old:
477
 
                try:
478
 
                    old_file = old.get_file(inventory_id)
479
 
                except NoSuchFile:
480
 
                    continue
481
 
                if old_file is None:
482
 
                    continue
483
 
                self.assertEqual(old_file.read(),
484
 
                                 new.get_file(inventory_id).read())
485
 
        if rev_id is not None:
 
473
            old.lock_read()
 
474
            new.lock_read()
 
475
            try:
 
476
                # Check that there aren't any inventory level changes
 
477
                delta = new.changes_from(old)
 
478
                self.assertFalse(delta.has_changed(),
 
479
                                 'Revision %s not copied correctly.'
 
480
                                 % (ancestor,))
 
481
 
 
482
                # Now check that the file contents are all correct
 
483
                for inventory_id in old:
 
484
                    try:
 
485
                        old_file = old.get_file(inventory_id)
 
486
                    except errors.NoSuchFile:
 
487
                        continue
 
488
                    if old_file is None:
 
489
                        continue
 
490
                    self.assertEqual(old_file.read(),
 
491
                                     new.get_file(inventory_id).read())
 
492
            finally:
 
493
                new.unlock()
 
494
                old.unlock()
 
495
        if not _mod_revision.is_null(rev_id):
486
496
            rh = self.b1.revision_history()
487
497
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
488
498
            tree.update()
496
506
        sure everything matches the builtin branch.
497
507
        """
498
508
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
509
        to_tree.lock_write()
 
510
        try:
 
511
            self._valid_apply_bundle(base_rev_id, info, to_tree)
 
512
        finally:
 
513
            to_tree.unlock()
 
514
 
 
515
    def _valid_apply_bundle(self, base_rev_id, info, to_tree):
499
516
        original_parents = to_tree.get_parent_ids()
500
517
        repository = to_tree.branch.repository
501
518
        original_parents = to_tree.get_parent_ids()
502
519
        self.assertIs(repository.has_revision(base_rev_id), True)
503
520
        for rev in info.real_revisions:
504
521
            self.assert_(not repository.has_revision(rev.revision_id),
505
 
                'Revision {%s} present before applying bundle' 
 
522
                'Revision {%s} present before applying bundle'
506
523
                % rev.revision_id)
507
524
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
508
525
 
509
526
        for rev in info.real_revisions:
510
527
            self.assert_(repository.has_revision(rev.revision_id),
511
 
                'Missing revision {%s} after applying bundle' 
 
528
                'Missing revision {%s} after applying bundle'
512
529
                % rev.revision_id)
513
530
 
514
531
        self.assert_(to_tree.branch.repository.has_revision(info.target))
520
537
        rev = info.real_revisions[-1]
521
538
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
522
539
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
523
 
        
 
540
 
524
541
        # TODO: make sure the target tree is identical to base tree
525
542
        #       we might also check the working tree.
526
543
 
549
566
        self.tree1.add('one')
550
567
        self.tree1.commit('add one', rev_id='a@cset-0-1')
551
568
 
552
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
553
 
        # FIXME: The current write_bundle api no longer supports
554
 
        #        setting a custom summary message
555
 
        #        We should re-introduce the ability, and update
556
 
        #        the tests to make sure it works.
557
 
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
558
 
        #         message='With a specialized message')
 
569
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
559
570
 
560
571
        # Make sure we can handle files with spaces, tabs, other
561
572
        # bogus characters
574
585
        tt = TreeTransform(self.tree1)
575
586
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
576
587
        tt.apply()
 
588
        # have to fix length of file-id so that we can predictably rewrite
 
589
        # a (length-prefixed) record containing it later.
 
590
        self.tree1.add('with space.txt', 'withspace-id')
577
591
        self.tree1.add([
578
 
                'with space.txt'
579
 
                , 'dir'
 
592
                  'dir'
580
593
                , 'dir/filein subdir.c'
581
594
                , 'dir/WithCaps.txt'
582
595
                , 'dir/ pre space'
590
603
 
591
604
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
592
605
 
593
 
        # Check a rollup bundle 
594
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
606
        # Check a rollup bundle
 
607
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
595
608
 
596
609
        # Now delete entries
597
610
        self.tree1.remove(
604
617
        tt.set_executability(False, trans_id)
605
618
        tt.apply()
606
619
        self.tree1.commit('removed', rev_id='a@cset-0-3')
607
 
        
 
620
 
608
621
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
609
 
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
610
 
                          'a@cset-0-2', 'a@cset-0-3')
611
 
        # Check a rollup bundle 
612
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
 
622
        self.assertRaises((errors.TestamentMismatch,
 
623
            errors.VersionedFileInvalidChecksum), self.get_invalid_bundle,
 
624
            'a@cset-0-2', 'a@cset-0-3')
 
625
        # Check a rollup bundle
 
626
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
613
627
 
614
628
        # Now move the directory
615
629
        self.tree1.rename_one('dir', 'sub/dir')
616
630
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
617
631
 
618
632
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
619
 
        # Check a rollup bundle 
620
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
 
633
        # Check a rollup bundle
 
634
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
621
635
 
622
636
        # Modified files
623
637
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
624
 
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
638
        open('b1/sub/dir/ pre space', 'ab').write(
 
639
             '\r\nAdding some\r\nDOS format lines\r\n')
625
640
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
626
 
        self.tree1.rename_one('sub/dir/ pre space', 
 
641
        self.tree1.rename_one('sub/dir/ pre space',
627
642
                              'sub/ start space')
628
643
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
629
644
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
641
656
        self.assertEqualDiff(tree1_inv, tree2_inv)
642
657
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
643
658
        other.commit('rename file', rev_id='a@cset-0-6b')
644
 
        _merge_helper([other.basedir, -1], [None, None],
645
 
                      this_dir=self.tree1.basedir)
 
659
        self.tree1.merge_from_branch(other.branch)
646
660
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
647
661
                          verbose=False)
648
662
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
649
663
 
650
664
    def test_symlink_bundle(self):
651
 
        if not has_symlinks():
652
 
            raise TestSkipped("No symlink support")
 
665
        self.requireFeature(SymlinkFeature)
653
666
        self.tree1 = self.make_branch_and_tree('b1')
654
667
        self.b1 = self.tree1.branch
655
668
        tt = TreeTransform(self.tree1)
656
669
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
657
670
        tt.apply()
658
671
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
659
 
        self.get_valid_bundle(None, 'l@cset-0-1')
 
672
        self.get_valid_bundle('null:', 'l@cset-0-1')
660
673
        tt = TreeTransform(self.tree1)
661
674
        trans_id = tt.trans_id_tree_file_id('link-1')
662
675
        tt.adjust_path('link2', tt.root, trans_id)
683
696
        self.tree1 = self.make_branch_and_tree('b1')
684
697
        self.b1 = self.tree1.branch
685
698
        tt = TreeTransform(self.tree1)
686
 
        
 
699
 
687
700
        # Add
688
701
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
689
 
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
702
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
703
            'binary-2')
690
704
        tt.apply()
691
705
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
692
 
        self.get_valid_bundle(None, 'b@cset-0-1')
 
706
        self.get_valid_bundle('null:', 'b@cset-0-1')
693
707
 
694
708
        # Delete
695
709
        tt = TreeTransform(self.tree1)
719
733
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
720
734
 
721
735
        # Rollup
722
 
        self.get_valid_bundle(None, 'b@cset-0-4')
 
736
        self.get_valid_bundle('null:', 'b@cset-0-4')
723
737
 
724
738
    def test_last_modified(self):
725
739
        self.tree1 = self.make_branch_and_tree('b1')
743
757
        tt.create_file('file2', trans_id)
744
758
        tt.apply()
745
759
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
746
 
        _merge_helper([other.basedir, -1], [None, None],
747
 
                      this_dir=self.tree1.basedir)
 
760
        self.tree1.merge_from_branch(other.branch)
748
761
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
749
762
                          verbose=False)
750
763
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
765
778
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
766
779
                               'a@cset-0-1', bundle_file, format=self.format)
767
780
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
768
 
        self.assertContainsRe(bundle_file.getvalue(), 'one')
769
 
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
781
        self.assertContainsRe(self.get_raw(bundle_file), 'one')
 
782
        self.assertContainsRe(self.get_raw(bundle_file), 'three')
 
783
 
 
784
    def test_bundle_same_basis(self):
 
785
        """Ensure using the basis as the target doesn't cause an error"""
 
786
        self.tree1 = self.make_branch_and_tree('b1')
 
787
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
788
        bundle_file = StringIO()
 
789
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-1',
 
790
                               'a@cset-0-1', bundle_file)
 
791
 
 
792
    @staticmethod
 
793
    def get_raw(bundle_file):
 
794
        return bundle_file.getvalue()
770
795
 
771
796
    def test_unicode_bundle(self):
772
797
        # Handle international characters
773
798
        os.mkdir('b1')
774
799
        try:
775
 
            f = open(u'b1/with Dod\xe9', 'wb')
 
800
            f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
776
801
        except UnicodeEncodeError:
777
802
            raise TestSkipped("Filesystem doesn't support unicode")
778
803
 
784
809
            u'William Dod\xe9\n').encode('utf-8'))
785
810
        f.close()
786
811
 
787
 
        self.tree1.add([u'with Dod\xe9'], ['withdod-id'])
 
812
        self.tree1.add([u'with Dod\N{Euro Sign}'], ['withdod-id'])
788
813
        self.tree1.commit(u'i18n commit from William Dod\xe9',
789
814
                          rev_id='i18n-1', committer=u'William Dod\xe9')
790
815
 
791
 
        if sys.platform == 'darwin':
792
 
            # On Mac the '\xe9' gets changed to 'e\u0301'
793
 
            self.assertEqual([u'.bzr', u'with Dode\u0301'],
794
 
                             sorted(os.listdir(u'b1')))
795
 
            delta = self.tree1.changes_from(self.tree1.basis_tree())
796
 
            self.assertEqual([(u'with Dod\xe9', 'withdod-id', 'file')],
797
 
                             delta.removed)
798
 
            self.knownFailure("Mac OSX doesn't preserve unicode"
799
 
                              " combining characters.")
800
 
 
801
816
        # Add
802
 
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
817
        bundle = self.get_valid_bundle('null:', 'i18n-1')
803
818
 
804
819
        # Modified
805
 
        f = open(u'b1/with Dod\xe9', 'wb')
 
820
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
806
821
        f.write(u'Modified \xb5\n'.encode('utf8'))
807
822
        f.close()
808
823
        self.tree1.commit(u'modified', rev_id='i18n-2')
809
824
 
810
825
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
811
 
        
 
826
 
812
827
        # Renamed
813
 
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
828
        self.tree1.rename_one(u'with Dod\N{Euro Sign}', u'B\N{Euro Sign}gfors')
814
829
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
815
830
                          committer=u'Erik B\xe5gfors')
816
831
 
817
832
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
818
833
 
819
834
        # Removed
820
 
        self.tree1.remove([u'B\xe5gfors'])
 
835
        self.tree1.remove([u'B\N{Euro Sign}gfors'])
821
836
        self.tree1.commit(u'removed', rev_id='i18n-4')
822
837
 
823
838
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
824
839
 
825
840
        # Rollup
826
 
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
841
        bundle = self.get_valid_bundle('null:', 'i18n-4')
827
842
 
828
843
 
829
844
    def test_whitespace_bundle(self):
841
856
        # Added
842
857
        self.tree1.commit('funky whitespace', rev_id='white-1')
843
858
 
844
 
        bundle = self.get_valid_bundle(None, 'white-1')
 
859
        bundle = self.get_valid_bundle('null:', 'white-1')
845
860
 
846
861
        # Modified
847
862
        open('b1/trailing space ', 'ab').write('add some text\n')
860
875
        self.tree1.commit('removed', rev_id='white-4')
861
876
 
862
877
        bundle = self.get_valid_bundle('white-3', 'white-4')
863
 
        
 
878
 
864
879
        # Now test a complet roll-up
865
 
        bundle = self.get_valid_bundle(None, 'white-4')
 
880
        bundle = self.get_valid_bundle('null:', 'white-4')
866
881
 
867
882
    def test_alt_timezone_bundle(self):
868
883
        self.tree1 = self.make_branch_and_memory_tree('b1')
878
893
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
879
894
                          timezone=19800, timestamp=1152544886.0)
880
895
 
881
 
        bundle = self.get_valid_bundle(None, 'tz-1')
882
 
        
 
896
        bundle = self.get_valid_bundle('null:', 'tz-1')
 
897
 
883
898
        rev = bundle.revisions[0]
884
899
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
885
900
        self.assertEqual(19800, rev.timezone)
890
905
        self.tree1 = self.make_branch_and_tree('b1')
891
906
        self.b1 = self.tree1.branch
892
907
        self.tree1.commit('message', rev_id='revid1')
893
 
        bundle = self.get_valid_bundle(None, 'revid1')
894
 
        tree = bundle.revision_tree(self.b1.repository, 'revid1')
 
908
        bundle = self.get_valid_bundle('null:', 'revid1')
 
909
        tree = self.get_bundle_tree(bundle, 'revid1')
895
910
        self.assertEqual('revid1', tree.inventory.root.revision)
896
911
 
897
912
    def test_install_revisions(self):
898
913
        self.tree1 = self.make_branch_and_tree('b1')
899
914
        self.b1 = self.tree1.branch
900
915
        self.tree1.commit('message', rev_id='rev2a')
901
 
        bundle = self.get_valid_bundle(None, 'rev2a')
 
916
        bundle = self.get_valid_bundle('null:', 'rev2a')
902
917
        branch2 = self.make_branch('b2')
903
918
        self.assertFalse(branch2.repository.has_revision('rev2a'))
904
919
        target_revision = bundle.install_revisions(branch2.repository)
905
920
        self.assertTrue(branch2.repository.has_revision('rev2a'))
906
921
        self.assertEqual('rev2a', target_revision)
907
922
 
 
923
    def test_bundle_empty_property(self):
 
924
        """Test serializing revision properties with an empty value."""
 
925
        tree = self.make_branch_and_memory_tree('tree')
 
926
        tree.lock_write()
 
927
        self.addCleanup(tree.unlock)
 
928
        tree.add([''], ['TREE_ROOT'])
 
929
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
930
        self.b1 = tree.branch
 
931
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
932
        bundle = read_bundle(bundle_sio)
 
933
        revision_info = bundle.revisions[0]
 
934
        self.assertEqual('rev1', revision_info.revision_id)
 
935
        rev = revision_info.as_revision()
 
936
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
937
                         rev.properties)
 
938
 
 
939
    def test_bundle_sorted_properties(self):
 
940
        """For stability the writer should write properties in sorted order."""
 
941
        tree = self.make_branch_and_memory_tree('tree')
 
942
        tree.lock_write()
 
943
        self.addCleanup(tree.unlock)
 
944
 
 
945
        tree.add([''], ['TREE_ROOT'])
 
946
        tree.commit('One', rev_id='rev1',
 
947
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
948
        self.b1 = tree.branch
 
949
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
950
        bundle = read_bundle(bundle_sio)
 
951
        revision_info = bundle.revisions[0]
 
952
        self.assertEqual('rev1', revision_info.revision_id)
 
953
        rev = revision_info.as_revision()
 
954
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
955
                          'd':'1'}, rev.properties)
 
956
 
 
957
    def test_bundle_unicode_properties(self):
 
958
        """We should be able to round trip a non-ascii property."""
 
959
        tree = self.make_branch_and_memory_tree('tree')
 
960
        tree.lock_write()
 
961
        self.addCleanup(tree.unlock)
 
962
 
 
963
        tree.add([''], ['TREE_ROOT'])
 
964
        # Revisions themselves do not require anything about revision property
 
965
        # keys, other than that they are a basestring, and do not contain
 
966
        # whitespace.
 
967
        # However, Testaments assert than they are str(), and thus should not
 
968
        # be Unicode.
 
969
        tree.commit('One', rev_id='rev1',
 
970
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
971
        self.b1 = tree.branch
 
972
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
973
        bundle = read_bundle(bundle_sio)
 
974
        revision_info = bundle.revisions[0]
 
975
        self.assertEqual('rev1', revision_info.revision_id)
 
976
        rev = revision_info.as_revision()
 
977
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
978
                          'alpha':u'\u03b1'}, rev.properties)
 
979
 
 
980
    def test_bundle_with_ghosts(self):
 
981
        tree = self.make_branch_and_tree('tree')
 
982
        self.b1 = tree.branch
 
983
        self.build_tree_contents([('tree/file', 'content1')])
 
984
        tree.add(['file'])
 
985
        tree.commit('rev1')
 
986
        self.build_tree_contents([('tree/file', 'content2')])
 
987
        tree.add_parent_tree_id('ghost')
 
988
        tree.commit('rev2', rev_id='rev2')
 
989
        bundle = self.get_valid_bundle('null:', 'rev2')
 
990
 
 
991
    def make_simple_tree(self, format=None):
 
992
        tree = self.make_branch_and_tree('b1', format=format)
 
993
        self.b1 = tree.branch
 
994
        self.build_tree(['b1/file'])
 
995
        tree.add('file')
 
996
        return tree
 
997
 
 
998
    def test_across_serializers(self):
 
999
        tree = self.make_simple_tree('knit')
 
1000
        tree.commit('hello', rev_id='rev1')
 
1001
        tree.commit('hello', rev_id='rev2')
 
1002
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1003
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1004
        bundle.install_revisions(repo)
 
1005
        inv_text = repo.get_inventory_xml('rev2')
 
1006
        self.assertNotContainsRe(inv_text, 'format="5"')
 
1007
        self.assertContainsRe(inv_text, 'format="7"')
 
1008
 
 
1009
    def make_repo_with_installed_revisions(self):
 
1010
        tree = self.make_simple_tree('knit')
 
1011
        tree.commit('hello', rev_id='rev1')
 
1012
        tree.commit('hello', rev_id='rev2')
 
1013
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1014
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1015
        bundle.install_revisions(repo)
 
1016
        return repo
 
1017
 
 
1018
    def test_across_models(self):
 
1019
        repo = self.make_repo_with_installed_revisions()
 
1020
        inv = repo.get_inventory('rev2')
 
1021
        self.assertEqual('rev2', inv.root.revision)
 
1022
        root_id = inv.root.file_id
 
1023
        repo.lock_read()
 
1024
        self.addCleanup(repo.unlock)
 
1025
        self.assertEqual({(root_id, 'rev1'):(),
 
1026
            (root_id, 'rev2'):((root_id, 'rev1'),)},
 
1027
            repo.texts.get_parent_map([(root_id, 'rev1'), (root_id, 'rev2')]))
 
1028
 
 
1029
    def test_inv_hash_across_serializers(self):
 
1030
        repo = self.make_repo_with_installed_revisions()
 
1031
        recorded_inv_sha1 = repo.get_inventory_sha1('rev2')
 
1032
        xml = repo.get_inventory_xml('rev2')
 
1033
        self.assertEqual(sha_string(xml), recorded_inv_sha1)
 
1034
 
 
1035
    def test_across_models_incompatible(self):
 
1036
        tree = self.make_simple_tree('dirstate-with-subtree')
 
1037
        tree.commit('hello', rev_id='rev1')
 
1038
        tree.commit('hello', rev_id='rev2')
 
1039
        try:
 
1040
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1041
        except errors.IncompatibleBundleFormat:
 
1042
            raise TestSkipped("Format 0.8 doesn't work with knit3")
 
1043
        repo = self.make_repository('repo', format='knit')
 
1044
        bundle.install_revisions(repo)
 
1045
 
 
1046
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1047
        self.assertRaises(errors.IncompatibleRevision,
 
1048
                          bundle.install_revisions, repo)
 
1049
 
 
1050
    def test_get_merge_request(self):
 
1051
        tree = self.make_simple_tree()
 
1052
        tree.commit('hello', rev_id='rev1')
 
1053
        tree.commit('hello', rev_id='rev2')
 
1054
        bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1055
        result = bundle.get_merge_request(tree.branch.repository)
 
1056
        self.assertEqual((None, 'rev1', 'inapplicable'), result)
 
1057
 
 
1058
    def test_with_subtree(self):
 
1059
        tree = self.make_branch_and_tree('tree',
 
1060
                                         format='dirstate-with-subtree')
 
1061
        self.b1 = tree.branch
 
1062
        subtree = self.make_branch_and_tree('tree/subtree',
 
1063
                                            format='dirstate-with-subtree')
 
1064
        tree.add('subtree')
 
1065
        tree.commit('hello', rev_id='rev1')
 
1066
        try:
 
1067
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1068
        except errors.IncompatibleBundleFormat:
 
1069
            raise TestSkipped("Format 0.8 doesn't work with knit3")
 
1070
        if isinstance(bundle, v09.BundleInfo09):
 
1071
            raise TestSkipped("Format 0.9 doesn't work with subtrees")
 
1072
        repo = self.make_repository('repo', format='knit')
 
1073
        self.assertRaises(errors.IncompatibleRevision,
 
1074
                          bundle.install_revisions, repo)
 
1075
        repo2 = self.make_repository('repo2', format='dirstate-with-subtree')
 
1076
        bundle.install_revisions(repo2)
 
1077
 
 
1078
    def test_revision_id_with_slash(self):
 
1079
        self.tree1 = self.make_branch_and_tree('tree')
 
1080
        self.b1 = self.tree1.branch
 
1081
        try:
 
1082
            self.tree1.commit('Revision/id/with/slashes', rev_id='rev/id')
 
1083
        except ValueError:
 
1084
            raise TestSkipped("Repository doesn't support revision ids with"
 
1085
                              " slashes")
 
1086
        bundle = self.get_valid_bundle('null:', 'rev/id')
 
1087
 
 
1088
    def test_skip_file(self):
 
1089
        """Make sure we don't accidentally write to the wrong versionedfile"""
 
1090
        self.tree1 = self.make_branch_and_tree('tree')
 
1091
        self.b1 = self.tree1.branch
 
1092
        # rev1 is not present in bundle, done by fetch
 
1093
        self.build_tree_contents([('tree/file2', 'contents1')])
 
1094
        self.tree1.add('file2', 'file2-id')
 
1095
        self.tree1.commit('rev1', rev_id='reva')
 
1096
        self.build_tree_contents([('tree/file3', 'contents2')])
 
1097
        # rev2 is present in bundle, and done by fetch
 
1098
        # having file1 in the bunle causes file1's versionedfile to be opened.
 
1099
        self.tree1.add('file3', 'file3-id')
 
1100
        self.tree1.commit('rev2')
 
1101
        # Updating file2 should not cause an attempt to add to file1's vf
 
1102
        target = self.tree1.bzrdir.sprout('target').open_workingtree()
 
1103
        self.build_tree_contents([('tree/file2', 'contents3')])
 
1104
        self.tree1.commit('rev3', rev_id='rev3')
 
1105
        bundle = self.get_valid_bundle('reva', 'rev3')
 
1106
        if getattr(bundle, 'get_bundle_reader', None) is None:
 
1107
            raise TestSkipped('Bundle format cannot provide reader')
 
1108
        # be sure that file1 comes before file2
 
1109
        for b, m, k, r, f in bundle.get_bundle_reader().iter_records():
 
1110
            if f == 'file3-id':
 
1111
                break
 
1112
            self.assertNotEqual(f, 'file2-id')
 
1113
        bundle.install_revisions(target.branch.repository)
 
1114
 
 
1115
 
 
1116
class V08BundleTester(BundleTester, TestCaseWithTransport):
 
1117
 
 
1118
    format = '0.8'
 
1119
 
 
1120
    def test_bundle_empty_property(self):
 
1121
        """Test serializing revision properties with an empty value."""
 
1122
        tree = self.make_branch_and_memory_tree('tree')
 
1123
        tree.lock_write()
 
1124
        self.addCleanup(tree.unlock)
 
1125
        tree.add([''], ['TREE_ROOT'])
 
1126
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1127
        self.b1 = tree.branch
 
1128
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1129
        self.assertContainsRe(bundle_sio.getvalue(),
 
1130
                              '# properties:\n'
 
1131
                              '#   branch-nick: tree\n'
 
1132
                              '#   empty: \n'
 
1133
                              '#   one: two\n'
 
1134
                             )
 
1135
        bundle = read_bundle(bundle_sio)
 
1136
        revision_info = bundle.revisions[0]
 
1137
        self.assertEqual('rev1', revision_info.revision_id)
 
1138
        rev = revision_info.as_revision()
 
1139
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1140
                         rev.properties)
 
1141
 
 
1142
    def get_bundle_tree(self, bundle, revision_id):
 
1143
        repository = self.make_repository('repo')
 
1144
        return bundle.revision_tree(repository, 'revid1')
 
1145
 
 
1146
    def test_bundle_empty_property_alt(self):
 
1147
        """Test serializing revision properties with an empty value.
 
1148
 
 
1149
        Older readers had a bug when reading an empty property.
 
1150
        They assumed that all keys ended in ': \n'. However they would write an
 
1151
        empty value as ':\n'. This tests make sure that all newer bzr versions
 
1152
        can handle th second form.
 
1153
        """
 
1154
        tree = self.make_branch_and_memory_tree('tree')
 
1155
        tree.lock_write()
 
1156
        self.addCleanup(tree.unlock)
 
1157
        tree.add([''], ['TREE_ROOT'])
 
1158
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1159
        self.b1 = tree.branch
 
1160
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1161
        txt = bundle_sio.getvalue()
 
1162
        loc = txt.find('#   empty: ') + len('#   empty:')
 
1163
        # Create a new bundle, which strips the trailing space after empty
 
1164
        bundle_sio = StringIO(txt[:loc] + txt[loc+1:])
 
1165
 
 
1166
        self.assertContainsRe(bundle_sio.getvalue(),
 
1167
                              '# properties:\n'
 
1168
                              '#   branch-nick: tree\n'
 
1169
                              '#   empty:\n'
 
1170
                              '#   one: two\n'
 
1171
                             )
 
1172
        bundle = read_bundle(bundle_sio)
 
1173
        revision_info = bundle.revisions[0]
 
1174
        self.assertEqual('rev1', revision_info.revision_id)
 
1175
        rev = revision_info.as_revision()
 
1176
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1177
                         rev.properties)
 
1178
 
 
1179
    def test_bundle_sorted_properties(self):
 
1180
        """For stability the writer should write properties in sorted order."""
 
1181
        tree = self.make_branch_and_memory_tree('tree')
 
1182
        tree.lock_write()
 
1183
        self.addCleanup(tree.unlock)
 
1184
 
 
1185
        tree.add([''], ['TREE_ROOT'])
 
1186
        tree.commit('One', rev_id='rev1',
 
1187
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
1188
        self.b1 = tree.branch
 
1189
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1190
        self.assertContainsRe(bundle_sio.getvalue(),
 
1191
                              '# properties:\n'
 
1192
                              '#   a: 4\n'
 
1193
                              '#   b: 3\n'
 
1194
                              '#   branch-nick: tree\n'
 
1195
                              '#   c: 2\n'
 
1196
                              '#   d: 1\n'
 
1197
                             )
 
1198
        bundle = read_bundle(bundle_sio)
 
1199
        revision_info = bundle.revisions[0]
 
1200
        self.assertEqual('rev1', revision_info.revision_id)
 
1201
        rev = revision_info.as_revision()
 
1202
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
1203
                          'd':'1'}, rev.properties)
 
1204
 
 
1205
    def test_bundle_unicode_properties(self):
 
1206
        """We should be able to round trip a non-ascii property."""
 
1207
        tree = self.make_branch_and_memory_tree('tree')
 
1208
        tree.lock_write()
 
1209
        self.addCleanup(tree.unlock)
 
1210
 
 
1211
        tree.add([''], ['TREE_ROOT'])
 
1212
        # Revisions themselves do not require anything about revision property
 
1213
        # keys, other than that they are a basestring, and do not contain
 
1214
        # whitespace.
 
1215
        # However, Testaments assert than they are str(), and thus should not
 
1216
        # be Unicode.
 
1217
        tree.commit('One', rev_id='rev1',
 
1218
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1219
        self.b1 = tree.branch
 
1220
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1221
        self.assertContainsRe(bundle_sio.getvalue(),
 
1222
                              '# properties:\n'
 
1223
                              '#   alpha: \xce\xb1\n'
 
1224
                              '#   branch-nick: tree\n'
 
1225
                              '#   omega: \xce\xa9\n'
 
1226
                             )
 
1227
        bundle = read_bundle(bundle_sio)
 
1228
        revision_info = bundle.revisions[0]
 
1229
        self.assertEqual('rev1', revision_info.revision_id)
 
1230
        rev = revision_info.as_revision()
 
1231
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1232
                          'alpha':u'\u03b1'}, rev.properties)
 
1233
 
908
1234
 
909
1235
class V09BundleKnit2Tester(V08BundleTester):
910
1236
 
926
1252
        return format
927
1253
 
928
1254
 
929
 
class MungedBundleTester(TestCaseWithTransport):
 
1255
class V4BundleTester(BundleTester, TestCaseWithTransport):
 
1256
 
 
1257
    format = '4'
 
1258
 
 
1259
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
1260
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1261
        Make sure that the text generated is valid, and that it
 
1262
        can be applied against the base, and generate the same information.
 
1263
 
 
1264
        :return: The in-memory bundle
 
1265
        """
 
1266
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1267
 
 
1268
        # This should also validate the generated bundle
 
1269
        bundle = read_bundle(bundle_txt)
 
1270
        repository = self.b1.repository
 
1271
        for bundle_rev in bundle.real_revisions:
 
1272
            # These really should have already been checked when we read the
 
1273
            # bundle, since it computes the sha1 hash for the revision, which
 
1274
            # only will match if everything is okay, but lets be explicit about
 
1275
            # it
 
1276
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
1277
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
1278
                      'timestamp', 'timezone', 'message', 'committer',
 
1279
                      'parent_ids', 'properties'):
 
1280
                self.assertEqual(getattr(branch_rev, a),
 
1281
                                 getattr(bundle_rev, a))
 
1282
            self.assertEqual(len(branch_rev.parent_ids),
 
1283
                             len(bundle_rev.parent_ids))
 
1284
        self.assertEqual(set(rev_ids),
 
1285
                         set([r.revision_id for r in bundle.real_revisions]))
 
1286
        self.valid_apply_bundle(base_rev_id, bundle,
 
1287
                                   checkout_dir=checkout_dir)
 
1288
 
 
1289
        return bundle
 
1290
 
 
1291
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1292
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1293
        Munge the text so that it's invalid.
 
1294
 
 
1295
        :return: The in-memory bundle
 
1296
        """
 
1297
        from bzrlib.bundle import serializer
 
1298
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1299
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1300
        new_text = new_text.replace('<file file_id="exe-1"',
 
1301
                                    '<file executable="y" file_id="exe-1"')
 
1302
        new_text = new_text.replace('B222', 'B237')
 
1303
        bundle_txt = StringIO()
 
1304
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1305
        bundle_txt.write('\n')
 
1306
        bundle_txt.write(new_text.encode('bz2'))
 
1307
        bundle_txt.seek(0)
 
1308
        bundle = read_bundle(bundle_txt)
 
1309
        self.valid_apply_bundle(base_rev_id, bundle)
 
1310
        return bundle
 
1311
 
 
1312
    def create_bundle_text(self, base_rev_id, rev_id):
 
1313
        bundle_txt = StringIO()
 
1314
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
1315
                               bundle_txt, format=self.format)
 
1316
        bundle_txt.seek(0)
 
1317
        self.assertEqual(bundle_txt.readline(),
 
1318
                         '# Bazaar revision bundle v%s\n' % self.format)
 
1319
        self.assertEqual(bundle_txt.readline(), '#\n')
 
1320
        rev = self.b1.repository.get_revision(rev_id)
 
1321
        bundle_txt.seek(0)
 
1322
        return bundle_txt, rev_ids
 
1323
 
 
1324
    def get_bundle_tree(self, bundle, revision_id):
 
1325
        repository = self.make_repository('repo')
 
1326
        bundle.install_revisions(repository)
 
1327
        return repository.revision_tree(revision_id)
 
1328
 
 
1329
    def test_creation(self):
 
1330
        tree = self.make_branch_and_tree('tree')
 
1331
        self.build_tree_contents([('tree/file', 'contents1\nstatic\n')])
 
1332
        tree.add('file', 'fileid-2')
 
1333
        tree.commit('added file', rev_id='rev1')
 
1334
        self.build_tree_contents([('tree/file', 'contents2\nstatic\n')])
 
1335
        tree.commit('changed file', rev_id='rev2')
 
1336
        s = StringIO()
 
1337
        serializer = BundleSerializerV4('1.0')
 
1338
        serializer.write(tree.branch.repository, ['rev1', 'rev2'], {}, s)
 
1339
        s.seek(0)
 
1340
        tree2 = self.make_branch_and_tree('target')
 
1341
        target_repo = tree2.branch.repository
 
1342
        install_bundle(target_repo, serializer.read(s))
 
1343
        target_repo.lock_read()
 
1344
        self.addCleanup(target_repo.unlock)
 
1345
        # Turn the 'iterators_of_bytes' back into simple strings for comparison
 
1346
        repo_texts = dict((i, ''.join(content)) for i, content
 
1347
                          in target_repo.iter_files_bytes(
 
1348
                                [('fileid-2', 'rev1', '1'),
 
1349
                                 ('fileid-2', 'rev2', '2')]))
 
1350
        self.assertEqual({'1':'contents1\nstatic\n',
 
1351
                          '2':'contents2\nstatic\n'},
 
1352
                         repo_texts)
 
1353
        rtree = target_repo.revision_tree('rev2')
 
1354
        inventory_vf = target_repo.inventories
 
1355
        # If the inventory store has a graph, it must match the revision graph.
 
1356
        self.assertSubset(
 
1357
            [inventory_vf.get_parent_map([('rev2',)])[('rev2',)]],
 
1358
            [None, (('rev1',),)])
 
1359
        self.assertEqual('changed file',
 
1360
                         target_repo.get_revision('rev2').message)
 
1361
 
 
1362
    @staticmethod
 
1363
    def get_raw(bundle_file):
 
1364
        bundle_file.seek(0)
 
1365
        line = bundle_file.readline()
 
1366
        line = bundle_file.readline()
 
1367
        lines = bundle_file.readlines()
 
1368
        return ''.join(lines).decode('bz2')
 
1369
 
 
1370
    def test_copy_signatures(self):
 
1371
        tree_a = self.make_branch_and_tree('tree_a')
 
1372
        import bzrlib.gpg
 
1373
        import bzrlib.commit as commit
 
1374
        oldstrategy = bzrlib.gpg.GPGStrategy
 
1375
        branch = tree_a.branch
 
1376
        repo_a = branch.repository
 
1377
        tree_a.commit("base", allow_pointless=True, rev_id='A')
 
1378
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
1379
        try:
 
1380
            from bzrlib.testament import Testament
 
1381
            # monkey patch gpg signing mechanism
 
1382
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
1383
            new_config = test_commit.MustSignConfig(branch)
 
1384
            commit.Commit(config=new_config).commit(message="base",
 
1385
                                                    allow_pointless=True,
 
1386
                                                    rev_id='B',
 
1387
                                                    working_tree=tree_a)
 
1388
            def sign(text):
 
1389
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
1390
            self.assertTrue(repo_a.has_signature_for_revision_id('B'))
 
1391
        finally:
 
1392
            bzrlib.gpg.GPGStrategy = oldstrategy
 
1393
        tree_b = self.make_branch_and_tree('tree_b')
 
1394
        repo_b = tree_b.branch.repository
 
1395
        s = StringIO()
 
1396
        serializer = BundleSerializerV4('4')
 
1397
        serializer.write(tree_a.branch.repository, ['A', 'B'], {}, s)
 
1398
        s.seek(0)
 
1399
        install_bundle(repo_b, serializer.read(s))
 
1400
        self.assertTrue(repo_b.has_signature_for_revision_id('B'))
 
1401
        self.assertEqual(repo_b.get_signature_text('B'),
 
1402
                         repo_a.get_signature_text('B'))
 
1403
        s.seek(0)
 
1404
        # ensure repeat installs are harmless
 
1405
        install_bundle(repo_b, serializer.read(s))
 
1406
 
 
1407
 
 
1408
class V4WeaveBundleTester(V4BundleTester):
 
1409
 
 
1410
    def bzrdir_format(self):
 
1411
        return 'metaweave'
 
1412
 
 
1413
 
 
1414
class MungedBundleTester(object):
930
1415
 
931
1416
    def build_test_bundle(self):
932
1417
        wt = self.make_branch_and_tree('b1')
941
1426
 
942
1427
        bundle_txt = StringIO()
943
1428
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
944
 
                               'a@cset-0-1', bundle_txt)
945
 
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
1429
                               'a@cset-0-1', bundle_txt, self.format)
 
1430
        self.assertEqual(set(['a@cset-0-2']), set(rev_ids))
946
1431
        bundle_txt.seek(0, 0)
947
1432
        return bundle_txt
948
1433
 
977
1462
        bundle = read_bundle(bundle_txt)
978
1463
        self.check_valid(bundle)
979
1464
 
 
1465
 
 
1466
class MungedBundleTesterV09(TestCaseWithTransport, MungedBundleTester):
 
1467
 
 
1468
    format = '0.9'
 
1469
 
980
1470
    def test_missing_trailing_whitespace(self):
981
1471
        bundle_txt = self.build_test_bundle()
982
1472
 
1010
1500
        bundle = read_bundle(bundle_txt)
1011
1501
        self.check_valid(bundle)
1012
1502
 
 
1503
 
 
1504
class MungedBundleTesterV4(TestCaseWithTransport, MungedBundleTester):
 
1505
 
 
1506
    format = '4'
 
1507
 
 
1508
 
 
1509
class TestBundleWriterReader(TestCase):
 
1510
 
 
1511
    def test_roundtrip_record(self):
 
1512
        fileobj = StringIO()
 
1513
        writer = v4.BundleWriter(fileobj)
 
1514
        writer.begin()
 
1515
        writer.add_info_record(foo='bar')
 
1516
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1517
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1518
        writer.end()
 
1519
        fileobj.seek(0)
 
1520
        reader = v4.BundleReader(fileobj, stream_input=True)
 
1521
        record_iter = reader.iter_records()
 
1522
        record = record_iter.next()
 
1523
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1524
            'info', None, None), record)
 
1525
        record = record_iter.next()
 
1526
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1527
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1528
                          record)
 
1529
 
 
1530
    def test_roundtrip_record_memory_hungry(self):
 
1531
        fileobj = StringIO()
 
1532
        writer = v4.BundleWriter(fileobj)
 
1533
        writer.begin()
 
1534
        writer.add_info_record(foo='bar')
 
1535
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1536
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1537
        writer.end()
 
1538
        fileobj.seek(0)
 
1539
        reader = v4.BundleReader(fileobj, stream_input=False)
 
1540
        record_iter = reader.iter_records()
 
1541
        record = record_iter.next()
 
1542
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1543
            'info', None, None), record)
 
1544
        record = record_iter.next()
 
1545
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1546
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1547
                          record)
 
1548
 
 
1549
    def test_encode_name(self):
 
1550
        self.assertEqual('revision/rev1',
 
1551
            v4.BundleWriter.encode_name('revision', 'rev1'))
 
1552
        self.assertEqual('file/rev//1/file-id-1',
 
1553
            v4.BundleWriter.encode_name('file', 'rev/1', 'file-id-1'))
 
1554
        self.assertEqual('info',
 
1555
            v4.BundleWriter.encode_name('info', None, None))
 
1556
 
 
1557
    def test_decode_name(self):
 
1558
        self.assertEqual(('revision', 'rev1', None),
 
1559
            v4.BundleReader.decode_name('revision/rev1'))
 
1560
        self.assertEqual(('file', 'rev/1', 'file-id-1'),
 
1561
            v4.BundleReader.decode_name('file/rev//1/file-id-1'))
 
1562
        self.assertEqual(('info', None, None),
 
1563
                         v4.BundleReader.decode_name('info'))
 
1564
 
 
1565
    def test_too_many_names(self):
 
1566
        fileobj = StringIO()
 
1567
        writer = v4.BundleWriter(fileobj)
 
1568
        writer.begin()
 
1569
        writer.add_info_record(foo='bar')
 
1570
        writer._container.add_bytes_record('blah', ['two', 'names'])
 
1571
        writer.end()
 
1572
        fileobj.seek(0)
 
1573
        record_iter = v4.BundleReader(fileobj).iter_records()
 
1574
        record = record_iter.next()
 
1575
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1576
            'info', None, None), record)
 
1577
        self.assertRaises(errors.BadBundle, record_iter.next)
 
1578
 
 
1579
 
 
1580
class TestReadMergeableFromUrl(TestCaseWithTransport):
 
1581
 
 
1582
    def test_read_mergeable_skips_local(self):
 
1583
        """A local bundle named like the URL should not be read.
 
1584
        """
 
1585
        out, wt = test_read_bundle.create_bundle_file(self)
 
1586
        class FooService(object):
 
1587
            """A directory service that always returns source"""
 
1588
 
 
1589
            def look_up(self, name, url):
 
1590
                return 'source'
 
1591
        directories.register('foo:', FooService, 'Testing directory service')
 
1592
        self.addCleanup(lambda: directories.remove('foo:'))
 
1593
        self.build_tree_contents([('./foo:bar', out.getvalue())])
 
1594
        self.assertRaises(errors.NotABundle, read_mergeable_from_url,
 
1595
                          'foo:bar')
 
1596
 
 
1597
    def test_smart_server_connection_reset(self):
 
1598
        """If a smart server connection fails during the attempt to read a
 
1599
        bundle, then the ConnectionReset error should be propagated.
 
1600
        """
 
1601
        # Instantiate a server that will provoke a ConnectionReset
 
1602
        sock_server = _DisconnectingTCPServer()
 
1603
        sock_server.setUp()
 
1604
        self.addCleanup(sock_server.tearDown)
 
1605
        # We don't really care what the url is since the server will close the
 
1606
        # connection without interpreting it
 
1607
        url = sock_server.get_url()
 
1608
        self.assertRaises(errors.ConnectionReset, read_mergeable_from_url, url)
 
1609
 
 
1610
 
 
1611
class _DisconnectingTCPServer(object):
 
1612
    """A TCP server that immediately closes any connection made to it."""
 
1613
 
 
1614
    def setUp(self):
 
1615
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1616
        self.sock.bind(('127.0.0.1', 0))
 
1617
        self.sock.listen(1)
 
1618
        self.port = self.sock.getsockname()[1]
 
1619
        self.thread = threading.Thread(
 
1620
            name='%s (port %d)' % (self.__class__.__name__, self.port),
 
1621
            target=self.accept_and_close)
 
1622
        self.thread.start()
 
1623
 
 
1624
    def accept_and_close(self):
 
1625
        conn, addr = self.sock.accept()
 
1626
        conn.shutdown(socket.SHUT_RDWR)
 
1627
        conn.close()
 
1628
 
 
1629
    def get_url(self):
 
1630
        return 'bzr://127.0.0.1:%d/' % (self.port,)
 
1631
 
 
1632
    def tearDown(self):
 
1633
        try:
 
1634
            # make sure the thread dies by connecting to the listening socket,
 
1635
            # just in case the test failed to do so.
 
1636
            conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1637
            conn.connect(self.sock.getsockname())
 
1638
            conn.close()
 
1639
        except socket.error:
 
1640
            pass
 
1641
        self.sock.close()
 
1642
        self.thread.join()
 
1643