~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004-2006 by Canonical Ltd
 
1
# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
 
from bzrlib import inventory
23
 
from bzrlib.builtins import merge
24
 
from bzrlib.bzrdir import BzrDir
 
23
from bzrlib import (
 
24
    bzrdir,
 
25
    diff,
 
26
    errors,
 
27
    inventory,
 
28
    merge,
 
29
    osutils,
 
30
    repository,
 
31
    revision as _mod_revision,
 
32
    tests,
 
33
    treebuilder,
 
34
    )
 
35
from bzrlib.bundle import read_mergeable_from_url
25
36
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
26
37
from bzrlib.bundle.bundle_data import BundleTree
27
 
from bzrlib.bundle.serializer import write_bundle, read_bundle
 
38
from bzrlib.bzrdir import BzrDir
 
39
from bzrlib.directory_service import directories
 
40
from bzrlib.bundle.serializer import write_bundle, read_bundle, v09, v4
 
41
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
 
42
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
 
43
from bzrlib.bundle.serializer.v4 import BundleSerializerV4
28
44
from bzrlib.branch import Branch
29
 
from bzrlib.diff import internal_diff
30
 
from bzrlib.errors import BzrError, TestamentMismatch, NotABundle, BadBundle
31
 
from bzrlib.merge import Merge3Merger
32
 
from bzrlib.osutils import has_symlinks, sha_file
33
 
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
34
 
                          TestCase, TestSkipped)
 
45
from bzrlib.repofmt import knitrepo
 
46
from bzrlib.tests import (
 
47
    test_read_bundle,
 
48
    test_commit,
 
49
    )
35
50
from bzrlib.transform import TreeTransform
36
 
from bzrlib.workingtree import WorkingTree
 
51
 
 
52
 
 
53
def get_text(vf, key):
 
54
    """Get the fulltext for a given revision id that is present in the vf"""
 
55
    stream = vf.get_record_stream([key], 'unordered', True)
 
56
    record = stream.next()
 
57
    return record.get_bytes_as('fulltext')
 
58
 
 
59
 
 
60
def get_inventory_text(repo, revision_id):
 
61
    """Get the fulltext for the inventory at revision id"""
 
62
    repo.lock_read()
 
63
    try:
 
64
        return get_text(repo.inventories, (revision_id,))
 
65
    finally:
 
66
        repo.unlock()
37
67
 
38
68
 
39
69
class MockTree(object):
87
117
        elif kind == 'symlink':
88
118
            ie = InventoryLink(file_id, name, parent_id)
89
119
        else:
90
 
            raise BzrError('unknown kind %r' % kind)
 
120
            raise errors.BzrError('unknown kind %r' % kind)
91
121
        ie.text_sha1 = text_sha_1
92
122
        ie.text_size = text_size
93
123
        return ie
95
125
    def add_dir(self, file_id, path):
96
126
        self.paths[file_id] = path
97
127
        self.ids[path] = file_id
98
 
    
 
128
 
99
129
    def add_file(self, file_id, path, contents):
100
130
        self.add_dir(file_id, path)
101
131
        self.contents[file_id] = contents
118
148
    def contents_stats(self, file_id):
119
149
        if file_id not in self.contents:
120
150
            return None, None
121
 
        text_sha1 = sha_file(self.get_file(file_id))
 
151
        text_sha1 = osutils.sha_file(self.get_file(file_id))
122
152
        return text_sha1, len(self.contents[file_id])
123
153
 
124
154
 
125
 
class BTreeTester(TestCase):
 
155
class BTreeTester(tests.TestCase):
126
156
    """A simple unittest tester for the BundleTree class."""
127
157
 
128
158
    def make_tree_1(self):
132
162
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
133
163
        mtree.add_dir("d", "grandparent/alt_parent")
134
164
        return BundleTree(mtree, ''), mtree
135
 
        
 
165
 
136
166
    def test_renames(self):
137
167
        """Ensure that file renames have the proper effect on children"""
138
168
        btree = self.make_tree_1()[0]
139
169
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
140
 
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
170
        self.assertEqual(btree.old_path("grandparent/parent"),
141
171
                         "grandparent/parent")
142
172
        self.assertEqual(btree.old_path("grandparent/parent/file"),
143
173
                         "grandparent/parent/file")
150
180
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
151
181
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
152
182
 
153
 
        assert btree.path2id("grandparent2") is None
154
 
        assert btree.path2id("grandparent2/parent") is None
155
 
        assert btree.path2id("grandparent2/parent/file") is None
 
183
        self.assertTrue(btree.path2id("grandparent2") is None)
 
184
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
185
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
156
186
 
157
187
        btree.note_rename("grandparent", "grandparent2")
158
 
        assert btree.old_path("grandparent") is None
159
 
        assert btree.old_path("grandparent/parent") is None
160
 
        assert btree.old_path("grandparent/parent/file") is None
 
188
        self.assertTrue(btree.old_path("grandparent") is None)
 
189
        self.assertTrue(btree.old_path("grandparent/parent") is None)
 
190
        self.assertTrue(btree.old_path("grandparent/parent/file") is None)
161
191
 
162
192
        self.assertEqual(btree.id2path("a"), "grandparent2")
163
193
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
167
197
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
168
198
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
169
199
 
170
 
        assert btree.path2id("grandparent") is None
171
 
        assert btree.path2id("grandparent/parent") is None
172
 
        assert btree.path2id("grandparent/parent/file") is None
 
200
        self.assertTrue(btree.path2id("grandparent") is None)
 
201
        self.assertTrue(btree.path2id("grandparent/parent") is None)
 
202
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
173
203
 
174
204
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
175
205
        self.assertEqual(btree.id2path("a"), "grandparent2")
180
210
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
181
211
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
182
212
 
183
 
        assert btree.path2id("grandparent2/parent") is None
184
 
        assert btree.path2id("grandparent2/parent/file") is None
 
213
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
214
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
185
215
 
186
 
        btree.note_rename("grandparent/parent/file", 
 
216
        btree.note_rename("grandparent/parent/file",
187
217
                          "grandparent2/parent2/file2")
188
218
        self.assertEqual(btree.id2path("a"), "grandparent2")
189
219
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
193
223
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
194
224
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
195
225
 
196
 
        assert btree.path2id("grandparent2/parent2/file") is None
 
226
        self.assertTrue(btree.path2id("grandparent2/parent2/file") is None)
197
227
 
198
228
    def test_moves(self):
199
229
        """Ensure that file moves have the proper effect on children"""
200
230
        btree = self.make_tree_1()[0]
201
 
        btree.note_rename("grandparent/parent/file", 
 
231
        btree.note_rename("grandparent/parent/file",
202
232
                          "grandparent/alt_parent/file")
203
233
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
204
234
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
205
 
        assert btree.path2id("grandparent/parent/file") is None
 
235
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
206
236
 
207
237
    def unified_diff(self, old, new):
208
238
        out = StringIO()
209
 
        internal_diff("old", old, "new", new, out)
 
239
        diff.internal_diff("old", old, "new", new, out)
210
240
        out.seek(0,0)
211
241
        return out.read()
212
242
 
213
243
    def make_tree_2(self):
214
244
        btree = self.make_tree_1()[0]
215
 
        btree.note_rename("grandparent/parent/file", 
 
245
        btree.note_rename("grandparent/parent/file",
216
246
                          "grandparent/alt_parent/file")
217
 
        assert btree.id2path("e") is None
218
 
        assert btree.path2id("grandparent/parent/file") is None
 
247
        self.assertTrue(btree.id2path("e") is None)
 
248
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
219
249
        btree.note_id("e", "grandparent/parent/file")
220
250
        return btree
221
251
 
247
277
    def make_tree_3(self):
248
278
        btree, mtree = self.make_tree_1()
249
279
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
250
 
        btree.note_rename("grandparent/parent/file", 
 
280
        btree.note_rename("grandparent/parent/file",
251
281
                          "grandparent/alt_parent/file")
252
 
        btree.note_rename("grandparent/parent/topping", 
 
282
        btree.note_rename("grandparent/parent/topping",
253
283
                          "grandparent/alt_parent/stopping")
254
284
        return btree
255
285
 
279
309
        btree = self.make_tree_1()[0]
280
310
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
281
311
        btree.note_deletion("grandparent/parent/file")
282
 
        assert btree.id2path("c") is None
283
 
        assert btree.path2id("grandparent/parent/file") is None
 
312
        self.assertTrue(btree.id2path("c") is None)
 
313
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
284
314
 
285
315
    def sorted_ids(self, tree):
286
316
        ids = list(tree)
294
324
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
295
325
        btree.note_deletion("grandparent/parent/file")
296
326
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
297
 
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
327
        btree.note_last_changed("grandparent/alt_parent/fool",
298
328
                                "revisionidiguess")
299
329
        self.assertEqual(self.sorted_ids(btree),
300
330
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
301
331
 
302
332
 
303
 
class BundleTester(TestCaseWithTransport):
 
333
class BundleTester1(tests.TestCaseWithTransport):
 
334
 
 
335
    def test_mismatched_bundle(self):
 
336
        format = bzrdir.BzrDirMetaFormat1()
 
337
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
338
        serializer = BundleSerializerV08('0.8')
 
339
        b = self.make_branch('.', format=format)
 
340
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write,
 
341
                          b.repository, [], {}, StringIO())
 
342
 
 
343
    def test_matched_bundle(self):
 
344
        """Don't raise IncompatibleBundleFormat for knit2 and bundle0.9"""
 
345
        format = bzrdir.BzrDirMetaFormat1()
 
346
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
347
        serializer = BundleSerializerV09('0.9')
 
348
        b = self.make_branch('.', format=format)
 
349
        serializer.write(b.repository, [], {}, StringIO())
 
350
 
 
351
    def test_mismatched_model(self):
 
352
        """Try copying a bundle from knit2 to knit1"""
 
353
        format = bzrdir.BzrDirMetaFormat1()
 
354
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
355
        source = self.make_branch_and_tree('source', format=format)
 
356
        source.commit('one', rev_id='one-id')
 
357
        source.commit('two', rev_id='two-id')
 
358
        text = StringIO()
 
359
        write_bundle(source.branch.repository, 'two-id', 'null:', text,
 
360
                     format='0.9')
 
361
        text.seek(0)
 
362
 
 
363
        format = bzrdir.BzrDirMetaFormat1()
 
364
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
365
        target = self.make_branch('target', format=format)
 
366
        self.assertRaises(errors.IncompatibleRevision, install_bundle,
 
367
                          target.repository, read_bundle(text))
 
368
 
 
369
 
 
370
class BundleTester(object):
 
371
 
 
372
    def bzrdir_format(self):
 
373
        format = bzrdir.BzrDirMetaFormat1()
 
374
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
375
        return format
 
376
 
 
377
    def make_branch_and_tree(self, path, format=None):
 
378
        if format is None:
 
379
            format = self.bzrdir_format()
 
380
        return tests.TestCaseWithTransport.make_branch_and_tree(
 
381
            self, path, format)
 
382
 
 
383
    def make_branch(self, path, format=None):
 
384
        if format is None:
 
385
            format = self.bzrdir_format()
 
386
        return tests.TestCaseWithTransport.make_branch(self, path, format)
304
387
 
305
388
    def create_bundle_text(self, base_rev_id, rev_id):
306
389
        bundle_txt = StringIO()
307
 
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
308
 
                               bundle_txt)
 
390
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
391
                               bundle_txt, format=self.format)
309
392
        bundle_txt.seek(0)
310
 
        self.assertEqual(bundle_txt.readline(), 
311
 
                         '# Bazaar revision bundle v0.8\n')
 
393
        self.assertEqual(bundle_txt.readline(),
 
394
                         '# Bazaar revision bundle v%s\n' % self.format)
312
395
        self.assertEqual(bundle_txt.readline(), '#\n')
313
396
 
314
397
        rev = self.b1.repository.get_revision(rev_id)
315
398
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
316
399
                         u'# message:\n')
317
 
 
318
 
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
319
400
        bundle_txt.seek(0)
320
401
        return bundle_txt, rev_ids
321
402
 
323
404
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
324
405
        Make sure that the text generated is valid, and that it
325
406
        can be applied against the base, and generate the same information.
326
 
        
327
 
        :return: The in-memory bundle 
 
407
 
 
408
        :return: The in-memory bundle
328
409
        """
329
410
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
330
411
 
331
 
        # This should also validate the generated bundle 
 
412
        # This should also validate the generated bundle
332
413
        bundle = read_bundle(bundle_txt)
333
414
        repository = self.b1.repository
334
415
        for bundle_rev in bundle.real_revisions:
338
419
            # it
339
420
            branch_rev = repository.get_revision(bundle_rev.revision_id)
340
421
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
341
 
                      'timestamp', 'timezone', 'message', 'committer', 
 
422
                      'timestamp', 'timezone', 'message', 'committer',
342
423
                      'parent_ids', 'properties'):
343
 
                self.assertEqual(getattr(branch_rev, a), 
 
424
                self.assertEqual(getattr(branch_rev, a),
344
425
                                 getattr(bundle_rev, a))
345
 
            self.assertEqual(len(branch_rev.parent_ids), 
 
426
            self.assertEqual(len(branch_rev.parent_ids),
346
427
                             len(bundle_rev.parent_ids))
347
 
        self.assertEqual(rev_ids, 
 
428
        self.assertEqual(rev_ids,
348
429
                         [r.revision_id for r in bundle.real_revisions])
349
430
        self.valid_apply_bundle(base_rev_id, bundle,
350
431
                                   checkout_dir=checkout_dir)
354
435
    def get_invalid_bundle(self, base_rev_id, rev_id):
355
436
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
356
437
        Munge the text so that it's invalid.
357
 
        
 
438
 
358
439
        :return: The in-memory bundle
359
440
        """
360
441
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
361
 
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
442
        new_text = bundle_txt.getvalue().replace('executable:no',
362
443
                                               'executable:yes')
363
444
        bundle_txt = StringIO(new_text)
364
445
        bundle = read_bundle(bundle_txt)
365
446
        self.valid_apply_bundle(base_rev_id, bundle)
366
 
        return bundle 
 
447
        return bundle
367
448
 
368
449
    def test_non_bundle(self):
369
 
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
450
        self.assertRaises(errors.NotABundle,
 
451
                          read_bundle, StringIO('#!/bin/sh\n'))
370
452
 
371
453
    def test_malformed(self):
372
 
        self.assertRaises(BadBundle, read_bundle, 
 
454
        self.assertRaises(errors.BadBundle, read_bundle,
373
455
                          StringIO('# Bazaar revision bundle v'))
374
456
 
375
457
    def test_crlf_bundle(self):
376
458
        try:
377
459
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
378
 
        except BadBundle:
 
460
        except errors.BadBundle:
379
461
            # It is currently permitted for bundles with crlf line endings to
380
462
            # make read_bundle raise a BadBundle, but this should be fixed.
381
463
            # Anything else, especially NotABundle, is an error.
386
468
        """
387
469
 
388
470
        if checkout_dir is None:
389
 
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
471
            checkout_dir = osutils.mkdtemp(prefix='test-branch-', dir='.')
390
472
        else:
391
473
            if not os.path.exists(checkout_dir):
392
474
                os.mkdir(checkout_dir)
393
 
        tree = BzrDir.create_standalone_workingtree(checkout_dir)
 
475
        tree = self.make_branch_and_tree(checkout_dir)
394
476
        s = StringIO()
395
 
        ancestors = write_bundle(self.b1.repository, rev_id, None, s)
 
477
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
 
478
                                 format=self.format)
396
479
        s.seek(0)
397
 
        assert isinstance(s.getvalue(), str), (
398
 
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
480
        self.assertIsInstance(s.getvalue(), str)
399
481
        install_bundle(tree.branch.repository, read_bundle(s))
400
482
        for ancestor in ancestors:
401
483
            old = self.b1.repository.revision_tree(ancestor)
402
484
            new = tree.branch.repository.revision_tree(ancestor)
403
 
 
404
 
            # Check that there aren't any inventory level changes
405
 
            delta = new.changes_from(old)
406
 
            self.assertFalse(delta.has_changed(),
407
 
                             'Revision %s not copied correctly.'
408
 
                             % (ancestor,))
409
 
 
410
 
            # Now check that the file contents are all correct
411
 
            for inventory_id in old:
412
 
                try:
413
 
                    old_file = old.get_file(inventory_id)
414
 
                except:
415
 
                    continue
416
 
                if old_file is None:
417
 
                    continue
418
 
                self.assertEqual(old_file.read(),
419
 
                                 new.get_file(inventory_id).read())
420
 
        if rev_id is not None:
 
485
            old.lock_read()
 
486
            new.lock_read()
 
487
            try:
 
488
                # Check that there aren't any inventory level changes
 
489
                delta = new.changes_from(old)
 
490
                self.assertFalse(delta.has_changed(),
 
491
                                 'Revision %s not copied correctly.'
 
492
                                 % (ancestor,))
 
493
 
 
494
                # Now check that the file contents are all correct
 
495
                for inventory_id in old:
 
496
                    try:
 
497
                        old_file = old.get_file(inventory_id)
 
498
                    except errors.NoSuchFile:
 
499
                        continue
 
500
                    if old_file is None:
 
501
                        continue
 
502
                    self.assertEqual(old_file.read(),
 
503
                                     new.get_file(inventory_id).read())
 
504
            finally:
 
505
                new.unlock()
 
506
                old.unlock()
 
507
        if not _mod_revision.is_null(rev_id):
421
508
            rh = self.b1.revision_history()
422
509
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
423
510
            tree.update()
424
511
            delta = tree.changes_from(self.b1.repository.revision_tree(rev_id))
425
512
            self.assertFalse(delta.has_changed(),
426
 
                             'Working tree has modifications')
 
513
                             'Working tree has modifications: %s' % delta)
427
514
        return tree
428
515
 
429
516
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
431
518
        sure everything matches the builtin branch.
432
519
        """
433
520
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
521
        to_tree.lock_write()
 
522
        try:
 
523
            self._valid_apply_bundle(base_rev_id, info, to_tree)
 
524
        finally:
 
525
            to_tree.unlock()
 
526
 
 
527
    def _valid_apply_bundle(self, base_rev_id, info, to_tree):
 
528
        original_parents = to_tree.get_parent_ids()
434
529
        repository = to_tree.branch.repository
 
530
        original_parents = to_tree.get_parent_ids()
435
531
        self.assertIs(repository.has_revision(base_rev_id), True)
436
532
        for rev in info.real_revisions:
437
533
            self.assert_(not repository.has_revision(rev.revision_id),
438
 
                'Revision {%s} present before applying bundle' 
 
534
                'Revision {%s} present before applying bundle'
439
535
                % rev.revision_id)
440
 
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
536
        merge_bundle(info, to_tree, True, merge.Merge3Merger, False, False)
441
537
 
442
538
        for rev in info.real_revisions:
443
539
            self.assert_(repository.has_revision(rev.revision_id),
444
 
                'Missing revision {%s} after applying bundle' 
 
540
                'Missing revision {%s} after applying bundle'
445
541
                % rev.revision_id)
446
542
 
447
543
        self.assert_(to_tree.branch.repository.has_revision(info.target))
448
544
        # Do we also want to verify that all the texts have been added?
449
545
 
450
 
        self.assert_(info.target in to_tree.pending_merges())
451
 
 
 
546
        self.assertEqual(original_parents + [info.target],
 
547
            to_tree.get_parent_ids())
452
548
 
453
549
        rev = info.real_revisions[-1]
454
550
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
455
551
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
456
 
        
 
552
 
457
553
        # TODO: make sure the target tree is identical to base tree
458
554
        #       we might also check the working tree.
459
555
 
478
574
        self.tree1 = self.make_branch_and_tree('b1')
479
575
        self.b1 = self.tree1.branch
480
576
 
481
 
        open('b1/one', 'wb').write('one\n')
482
 
        self.tree1.add('one')
 
577
        self.build_tree_contents([('b1/one', 'one\n')])
 
578
        self.tree1.add('one', 'one-id')
 
579
        self.tree1.set_root_id('root-id')
483
580
        self.tree1.commit('add one', rev_id='a@cset-0-1')
484
581
 
485
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
486
 
        # FIXME: The current write_bundle api no longer supports
487
 
        #        setting a custom summary message
488
 
        #        We should re-introduce the ability, and update
489
 
        #        the tests to make sure it works.
490
 
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
491
 
        #         message='With a specialized message')
 
582
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
492
583
 
493
584
        # Make sure we can handle files with spaces, tabs, other
494
585
        # bogus characters
502
593
                , 'b1/sub/sub/'
503
594
                , 'b1/sub/sub/nonempty.txt'
504
595
                ])
505
 
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
506
 
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
596
        self.build_tree_contents([('b1/sub/sub/emptyfile.txt', ''),
 
597
                                  ('b1/dir/nolastnewline.txt', 'bloop')])
507
598
        tt = TreeTransform(self.tree1)
508
599
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
509
600
        tt.apply()
 
601
        # have to fix length of file-id so that we can predictably rewrite
 
602
        # a (length-prefixed) record containing it later.
 
603
        self.tree1.add('with space.txt', 'withspace-id')
510
604
        self.tree1.add([
511
 
                'with space.txt'
512
 
                , 'dir'
 
605
                  'dir'
513
606
                , 'dir/filein subdir.c'
514
607
                , 'dir/WithCaps.txt'
515
608
                , 'dir/ pre space'
523
616
 
524
617
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
525
618
 
526
 
        # Check a rollup bundle 
527
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
619
        # Check a rollup bundle
 
620
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
528
621
 
529
622
        # Now delete entries
530
623
        self.tree1.remove(
537
630
        tt.set_executability(False, trans_id)
538
631
        tt.apply()
539
632
        self.tree1.commit('removed', rev_id='a@cset-0-3')
540
 
        
 
633
 
541
634
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
542
 
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
543
 
                          'a@cset-0-2', 'a@cset-0-3')
544
 
        # Check a rollup bundle 
545
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
 
635
        self.assertRaises((errors.TestamentMismatch,
 
636
            errors.VersionedFileInvalidChecksum,
 
637
            errors.BadBundle), self.get_invalid_bundle,
 
638
            'a@cset-0-2', 'a@cset-0-3')
 
639
        # Check a rollup bundle
 
640
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
546
641
 
547
642
        # Now move the directory
548
643
        self.tree1.rename_one('dir', 'sub/dir')
549
644
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
550
645
 
551
646
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
552
 
        # Check a rollup bundle 
553
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
 
647
        # Check a rollup bundle
 
648
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
554
649
 
555
650
        # Modified files
556
651
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
557
 
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
652
        open('b1/sub/dir/ pre space', 'ab').write(
 
653
             '\r\nAdding some\r\nDOS format lines\r\n')
558
654
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
559
 
        self.tree1.rename_one('sub/dir/ pre space', 
 
655
        self.tree1.rename_one('sub/dir/ pre space',
560
656
                              'sub/ start space')
561
657
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
562
658
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
568
664
                          verbose=False)
569
665
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
570
666
        other = self.get_checkout('a@cset-0-5')
 
667
        tree1_inv = get_inventory_text(self.tree1.branch.repository,
 
668
                                       'a@cset-0-5')
 
669
        tree2_inv = get_inventory_text(other.branch.repository,
 
670
                                       'a@cset-0-5')
 
671
        self.assertEqualDiff(tree1_inv, tree2_inv)
571
672
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
572
673
        other.commit('rename file', rev_id='a@cset-0-6b')
573
 
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
 
674
        self.tree1.merge_from_branch(other.branch)
574
675
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
575
676
                          verbose=False)
576
677
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
577
678
 
578
 
    def test_symlink_bundle(self):
579
 
        if not has_symlinks():
580
 
            raise TestSkipped("No symlink support")
581
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
679
    def _test_symlink_bundle(self, link_name, link_target, new_link_target):
 
680
        link_id = 'link-1'
 
681
 
 
682
        self.requireFeature(tests.SymlinkFeature)
 
683
        self.tree1 = self.make_branch_and_tree('b1')
582
684
        self.b1 = self.tree1.branch
 
685
 
583
686
        tt = TreeTransform(self.tree1)
584
 
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
687
        tt.new_symlink(link_name, tt.root, link_target, link_id)
585
688
        tt.apply()
586
689
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
587
 
        self.get_valid_bundle(None, 'l@cset-0-1')
 
690
        bundle = self.get_valid_bundle('null:', 'l@cset-0-1')
 
691
        if getattr(bundle ,'revision_tree', None) is not None:
 
692
            # Not all bundle formats supports revision_tree
 
693
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-1')
 
694
            self.assertEqual(link_target, bund_tree.get_symlink_target(link_id))
 
695
 
588
696
        tt = TreeTransform(self.tree1)
589
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
697
        trans_id = tt.trans_id_tree_file_id(link_id)
590
698
        tt.adjust_path('link2', tt.root, trans_id)
591
699
        tt.delete_contents(trans_id)
592
 
        tt.create_symlink('mars', trans_id)
 
700
        tt.create_symlink(new_link_target, trans_id)
593
701
        tt.apply()
594
702
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
595
 
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
703
        bundle = self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
704
        if getattr(bundle ,'revision_tree', None) is not None:
 
705
            # Not all bundle formats supports revision_tree
 
706
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-2')
 
707
            self.assertEqual(new_link_target,
 
708
                             bund_tree.get_symlink_target(link_id))
 
709
 
596
710
        tt = TreeTransform(self.tree1)
597
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
711
        trans_id = tt.trans_id_tree_file_id(link_id)
598
712
        tt.delete_contents(trans_id)
599
713
        tt.create_symlink('jupiter', trans_id)
600
714
        tt.apply()
601
715
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
602
 
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
716
        bundle = self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
717
 
603
718
        tt = TreeTransform(self.tree1)
604
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
719
        trans_id = tt.trans_id_tree_file_id(link_id)
605
720
        tt.delete_contents(trans_id)
606
721
        tt.apply()
607
722
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
608
 
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
723
        bundle = self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
724
 
 
725
    def test_symlink_bundle(self):
 
726
        self._test_symlink_bundle('link', 'bar/foo', 'mars')
 
727
 
 
728
    def test_unicode_symlink_bundle(self):
 
729
        self.requireFeature(tests.UnicodeFilenameFeature)
 
730
        self._test_symlink_bundle(u'\N{Euro Sign}link',
 
731
                                  u'bar/\N{Euro Sign}foo',
 
732
                                  u'mars\N{Euro Sign}')
609
733
 
610
734
    def test_binary_bundle(self):
611
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
735
        self.tree1 = self.make_branch_and_tree('b1')
612
736
        self.b1 = self.tree1.branch
613
737
        tt = TreeTransform(self.tree1)
614
 
        
 
738
 
615
739
        # Add
616
740
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
617
 
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
741
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
742
            'binary-2')
618
743
        tt.apply()
619
744
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
620
 
        self.get_valid_bundle(None, 'b@cset-0-1')
 
745
        self.get_valid_bundle('null:', 'b@cset-0-1')
621
746
 
622
747
        # Delete
623
748
        tt = TreeTransform(self.tree1)
647
772
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
648
773
 
649
774
        # Rollup
650
 
        self.get_valid_bundle(None, 'b@cset-0-4')
 
775
        self.get_valid_bundle('null:', 'b@cset-0-4')
651
776
 
652
777
    def test_last_modified(self):
653
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
778
        self.tree1 = self.make_branch_and_tree('b1')
654
779
        self.b1 = self.tree1.branch
655
780
        tt = TreeTransform(self.tree1)
656
781
        tt.new_file('file', tt.root, 'file', 'file')
671
796
        tt.create_file('file2', trans_id)
672
797
        tt.apply()
673
798
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
674
 
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
 
799
        self.tree1.merge_from_branch(other.branch)
675
800
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
676
801
                          verbose=False)
677
802
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
678
803
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
679
804
 
680
805
    def test_hide_history(self):
681
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
806
        self.tree1 = self.make_branch_and_tree('b1')
682
807
        self.b1 = self.tree1.branch
683
808
 
684
809
        open('b1/one', 'wb').write('one\n')
690
815
        self.tree1.commit('modify', rev_id='a@cset-0-3')
691
816
        bundle_file = StringIO()
692
817
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
818
                               'a@cset-0-1', bundle_file, format=self.format)
 
819
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
 
820
        self.assertContainsRe(self.get_raw(bundle_file), 'one')
 
821
        self.assertContainsRe(self.get_raw(bundle_file), 'three')
 
822
 
 
823
    def test_bundle_same_basis(self):
 
824
        """Ensure using the basis as the target doesn't cause an error"""
 
825
        self.tree1 = self.make_branch_and_tree('b1')
 
826
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
827
        bundle_file = StringIO()
 
828
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-1',
693
829
                               'a@cset-0-1', bundle_file)
694
 
        self.assertNotContainsRe(bundle_file.getvalue(), 'two')
695
 
        self.assertContainsRe(bundle_file.getvalue(), 'one')
696
 
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
830
 
 
831
    @staticmethod
 
832
    def get_raw(bundle_file):
 
833
        return bundle_file.getvalue()
697
834
 
698
835
    def test_unicode_bundle(self):
 
836
        self.requireFeature(tests.UnicodeFilenameFeature)
699
837
        # Handle international characters
700
838
        os.mkdir('b1')
701
 
        try:
702
 
            f = open(u'b1/with Dod\xe9', 'wb')
703
 
        except UnicodeEncodeError:
704
 
            raise TestSkipped("Filesystem doesn't support unicode")
 
839
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
705
840
 
706
841
        self.tree1 = self.make_branch_and_tree('b1')
707
842
        self.b1 = self.tree1.branch
711
846
            u'William Dod\xe9\n').encode('utf-8'))
712
847
        f.close()
713
848
 
714
 
        self.tree1.add([u'with Dod\xe9'])
715
 
        self.tree1.commit(u'i18n commit from William Dod\xe9', 
 
849
        self.tree1.add([u'with Dod\N{Euro Sign}'], ['withdod-id'])
 
850
        self.tree1.commit(u'i18n commit from William Dod\xe9',
716
851
                          rev_id='i18n-1', committer=u'William Dod\xe9')
717
852
 
718
853
        # Add
719
 
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
854
        bundle = self.get_valid_bundle('null:', 'i18n-1')
720
855
 
721
856
        # Modified
722
 
        f = open(u'b1/with Dod\xe9', 'wb')
 
857
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
723
858
        f.write(u'Modified \xb5\n'.encode('utf8'))
724
859
        f.close()
725
860
        self.tree1.commit(u'modified', rev_id='i18n-2')
726
861
 
727
862
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
728
 
        
 
863
 
729
864
        # Renamed
730
 
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
865
        self.tree1.rename_one(u'with Dod\N{Euro Sign}', u'B\N{Euro Sign}gfors')
731
866
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
732
867
                          committer=u'Erik B\xe5gfors')
733
868
 
734
869
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
735
870
 
736
871
        # Removed
737
 
        self.tree1.remove([u'B\xe5gfors'])
 
872
        self.tree1.remove([u'B\N{Euro Sign}gfors'])
738
873
        self.tree1.commit(u'removed', rev_id='i18n-4')
739
874
 
740
875
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
741
876
 
742
877
        # Rollup
743
 
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
878
        bundle = self.get_valid_bundle('null:', 'i18n-4')
744
879
 
745
880
 
746
881
    def test_whitespace_bundle(self):
747
882
        if sys.platform in ('win32', 'cygwin'):
748
 
            raise TestSkipped('Windows doesn\'t support filenames'
749
 
                              ' with tabs or trailing spaces')
 
883
            raise tests.TestSkipped('Windows doesn\'t support filenames'
 
884
                                    ' with tabs or trailing spaces')
750
885
        self.tree1 = self.make_branch_and_tree('b1')
751
886
        self.b1 = self.tree1.branch
752
887
 
758
893
        # Added
759
894
        self.tree1.commit('funky whitespace', rev_id='white-1')
760
895
 
761
 
        bundle = self.get_valid_bundle(None, 'white-1')
 
896
        bundle = self.get_valid_bundle('null:', 'white-1')
762
897
 
763
898
        # Modified
764
899
        open('b1/trailing space ', 'ab').write('add some text\n')
777
912
        self.tree1.commit('removed', rev_id='white-4')
778
913
 
779
914
        bundle = self.get_valid_bundle('white-3', 'white-4')
780
 
        
 
915
 
781
916
        # Now test a complet roll-up
782
 
        bundle = self.get_valid_bundle(None, 'white-4')
 
917
        bundle = self.get_valid_bundle('null:', 'white-4')
783
918
 
784
919
    def test_alt_timezone_bundle(self):
785
 
        self.tree1 = self.make_branch_and_tree('b1')
 
920
        self.tree1 = self.make_branch_and_memory_tree('b1')
786
921
        self.b1 = self.tree1.branch
 
922
        builder = treebuilder.TreeBuilder()
787
923
 
788
 
        self.build_tree(['b1/newfile'])
789
 
        self.tree1.add(['newfile'])
 
924
        self.tree1.lock_write()
 
925
        builder.start_tree(self.tree1)
 
926
        builder.build(['newfile'])
 
927
        builder.finish_tree()
790
928
 
791
929
        # Asia/Colombo offset = 5 hours 30 minutes
792
930
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
793
931
                          timezone=19800, timestamp=1152544886.0)
794
932
 
795
 
        bundle = self.get_valid_bundle(None, 'tz-1')
796
 
        
 
933
        bundle = self.get_valid_bundle('null:', 'tz-1')
 
934
 
797
935
        rev = bundle.revisions[0]
798
936
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
799
937
        self.assertEqual(19800, rev.timezone)
800
938
        self.assertEqual(1152544886.0, rev.timestamp)
801
 
 
802
 
 
803
 
class MungedBundleTester(TestCaseWithTransport):
 
939
        self.tree1.unlock()
 
940
 
 
941
    def test_bundle_root_id(self):
 
942
        self.tree1 = self.make_branch_and_tree('b1')
 
943
        self.b1 = self.tree1.branch
 
944
        self.tree1.commit('message', rev_id='revid1')
 
945
        bundle = self.get_valid_bundle('null:', 'revid1')
 
946
        tree = self.get_bundle_tree(bundle, 'revid1')
 
947
        self.assertEqual('revid1', tree.inventory.root.revision)
 
948
 
 
949
    def test_install_revisions(self):
 
950
        self.tree1 = self.make_branch_and_tree('b1')
 
951
        self.b1 = self.tree1.branch
 
952
        self.tree1.commit('message', rev_id='rev2a')
 
953
        bundle = self.get_valid_bundle('null:', 'rev2a')
 
954
        branch2 = self.make_branch('b2')
 
955
        self.assertFalse(branch2.repository.has_revision('rev2a'))
 
956
        target_revision = bundle.install_revisions(branch2.repository)
 
957
        self.assertTrue(branch2.repository.has_revision('rev2a'))
 
958
        self.assertEqual('rev2a', target_revision)
 
959
 
 
960
    def test_bundle_empty_property(self):
 
961
        """Test serializing revision properties with an empty value."""
 
962
        tree = self.make_branch_and_memory_tree('tree')
 
963
        tree.lock_write()
 
964
        self.addCleanup(tree.unlock)
 
965
        tree.add([''], ['TREE_ROOT'])
 
966
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
967
        self.b1 = tree.branch
 
968
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
969
        bundle = read_bundle(bundle_sio)
 
970
        revision_info = bundle.revisions[0]
 
971
        self.assertEqual('rev1', revision_info.revision_id)
 
972
        rev = revision_info.as_revision()
 
973
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
974
                         rev.properties)
 
975
 
 
976
    def test_bundle_sorted_properties(self):
 
977
        """For stability the writer should write properties in sorted order."""
 
978
        tree = self.make_branch_and_memory_tree('tree')
 
979
        tree.lock_write()
 
980
        self.addCleanup(tree.unlock)
 
981
 
 
982
        tree.add([''], ['TREE_ROOT'])
 
983
        tree.commit('One', rev_id='rev1',
 
984
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
985
        self.b1 = tree.branch
 
986
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
987
        bundle = read_bundle(bundle_sio)
 
988
        revision_info = bundle.revisions[0]
 
989
        self.assertEqual('rev1', revision_info.revision_id)
 
990
        rev = revision_info.as_revision()
 
991
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
992
                          'd':'1'}, rev.properties)
 
993
 
 
994
    def test_bundle_unicode_properties(self):
 
995
        """We should be able to round trip a non-ascii property."""
 
996
        tree = self.make_branch_and_memory_tree('tree')
 
997
        tree.lock_write()
 
998
        self.addCleanup(tree.unlock)
 
999
 
 
1000
        tree.add([''], ['TREE_ROOT'])
 
1001
        # Revisions themselves do not require anything about revision property
 
1002
        # keys, other than that they are a basestring, and do not contain
 
1003
        # whitespace.
 
1004
        # However, Testaments assert than they are str(), and thus should not
 
1005
        # be Unicode.
 
1006
        tree.commit('One', rev_id='rev1',
 
1007
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1008
        self.b1 = tree.branch
 
1009
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1010
        bundle = read_bundle(bundle_sio)
 
1011
        revision_info = bundle.revisions[0]
 
1012
        self.assertEqual('rev1', revision_info.revision_id)
 
1013
        rev = revision_info.as_revision()
 
1014
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1015
                          'alpha':u'\u03b1'}, rev.properties)
 
1016
 
 
1017
    def test_bundle_with_ghosts(self):
 
1018
        tree = self.make_branch_and_tree('tree')
 
1019
        self.b1 = tree.branch
 
1020
        self.build_tree_contents([('tree/file', 'content1')])
 
1021
        tree.add(['file'])
 
1022
        tree.commit('rev1')
 
1023
        self.build_tree_contents([('tree/file', 'content2')])
 
1024
        tree.add_parent_tree_id('ghost')
 
1025
        tree.commit('rev2', rev_id='rev2')
 
1026
        bundle = self.get_valid_bundle('null:', 'rev2')
 
1027
 
 
1028
    def make_simple_tree(self, format=None):
 
1029
        tree = self.make_branch_and_tree('b1', format=format)
 
1030
        self.b1 = tree.branch
 
1031
        self.build_tree(['b1/file'])
 
1032
        tree.add('file')
 
1033
        return tree
 
1034
 
 
1035
    def test_across_serializers(self):
 
1036
        tree = self.make_simple_tree('knit')
 
1037
        tree.commit('hello', rev_id='rev1')
 
1038
        tree.commit('hello', rev_id='rev2')
 
1039
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1040
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1041
        bundle.install_revisions(repo)
 
1042
        inv_text = repo.get_inventory_xml('rev2')
 
1043
        self.assertNotContainsRe(inv_text, 'format="5"')
 
1044
        self.assertContainsRe(inv_text, 'format="7"')
 
1045
 
 
1046
    def make_repo_with_installed_revisions(self):
 
1047
        tree = self.make_simple_tree('knit')
 
1048
        tree.commit('hello', rev_id='rev1')
 
1049
        tree.commit('hello', rev_id='rev2')
 
1050
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1051
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1052
        bundle.install_revisions(repo)
 
1053
        return repo
 
1054
 
 
1055
    def test_across_models(self):
 
1056
        repo = self.make_repo_with_installed_revisions()
 
1057
        inv = repo.get_inventory('rev2')
 
1058
        self.assertEqual('rev2', inv.root.revision)
 
1059
        root_id = inv.root.file_id
 
1060
        repo.lock_read()
 
1061
        self.addCleanup(repo.unlock)
 
1062
        self.assertEqual({(root_id, 'rev1'):(),
 
1063
            (root_id, 'rev2'):((root_id, 'rev1'),)},
 
1064
            repo.texts.get_parent_map([(root_id, 'rev1'), (root_id, 'rev2')]))
 
1065
 
 
1066
    def test_inv_hash_across_serializers(self):
 
1067
        repo = self.make_repo_with_installed_revisions()
 
1068
        recorded_inv_sha1 = repo.get_inventory_sha1('rev2')
 
1069
        xml = repo.get_inventory_xml('rev2')
 
1070
        self.assertEqual(osutils.sha_string(xml), recorded_inv_sha1)
 
1071
 
 
1072
    def test_across_models_incompatible(self):
 
1073
        tree = self.make_simple_tree('dirstate-with-subtree')
 
1074
        tree.commit('hello', rev_id='rev1')
 
1075
        tree.commit('hello', rev_id='rev2')
 
1076
        try:
 
1077
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1078
        except errors.IncompatibleBundleFormat:
 
1079
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1080
        repo = self.make_repository('repo', format='knit')
 
1081
        bundle.install_revisions(repo)
 
1082
 
 
1083
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1084
        self.assertRaises(errors.IncompatibleRevision,
 
1085
                          bundle.install_revisions, repo)
 
1086
 
 
1087
    def test_get_merge_request(self):
 
1088
        tree = self.make_simple_tree()
 
1089
        tree.commit('hello', rev_id='rev1')
 
1090
        tree.commit('hello', rev_id='rev2')
 
1091
        bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1092
        result = bundle.get_merge_request(tree.branch.repository)
 
1093
        self.assertEqual((None, 'rev1', 'inapplicable'), result)
 
1094
 
 
1095
    def test_with_subtree(self):
 
1096
        tree = self.make_branch_and_tree('tree',
 
1097
                                         format='dirstate-with-subtree')
 
1098
        self.b1 = tree.branch
 
1099
        subtree = self.make_branch_and_tree('tree/subtree',
 
1100
                                            format='dirstate-with-subtree')
 
1101
        tree.add('subtree')
 
1102
        tree.commit('hello', rev_id='rev1')
 
1103
        try:
 
1104
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1105
        except errors.IncompatibleBundleFormat:
 
1106
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1107
        if isinstance(bundle, v09.BundleInfo09):
 
1108
            raise tests.TestSkipped("Format 0.9 doesn't work with subtrees")
 
1109
        repo = self.make_repository('repo', format='knit')
 
1110
        self.assertRaises(errors.IncompatibleRevision,
 
1111
                          bundle.install_revisions, repo)
 
1112
        repo2 = self.make_repository('repo2', format='dirstate-with-subtree')
 
1113
        bundle.install_revisions(repo2)
 
1114
 
 
1115
    def test_revision_id_with_slash(self):
 
1116
        self.tree1 = self.make_branch_and_tree('tree')
 
1117
        self.b1 = self.tree1.branch
 
1118
        try:
 
1119
            self.tree1.commit('Revision/id/with/slashes', rev_id='rev/id')
 
1120
        except ValueError:
 
1121
            raise tests.TestSkipped(
 
1122
                "Repository doesn't support revision ids with slashes")
 
1123
        bundle = self.get_valid_bundle('null:', 'rev/id')
 
1124
 
 
1125
    def test_skip_file(self):
 
1126
        """Make sure we don't accidentally write to the wrong versionedfile"""
 
1127
        self.tree1 = self.make_branch_and_tree('tree')
 
1128
        self.b1 = self.tree1.branch
 
1129
        # rev1 is not present in bundle, done by fetch
 
1130
        self.build_tree_contents([('tree/file2', 'contents1')])
 
1131
        self.tree1.add('file2', 'file2-id')
 
1132
        self.tree1.commit('rev1', rev_id='reva')
 
1133
        self.build_tree_contents([('tree/file3', 'contents2')])
 
1134
        # rev2 is present in bundle, and done by fetch
 
1135
        # having file1 in the bunle causes file1's versionedfile to be opened.
 
1136
        self.tree1.add('file3', 'file3-id')
 
1137
        self.tree1.commit('rev2')
 
1138
        # Updating file2 should not cause an attempt to add to file1's vf
 
1139
        target = self.tree1.bzrdir.sprout('target').open_workingtree()
 
1140
        self.build_tree_contents([('tree/file2', 'contents3')])
 
1141
        self.tree1.commit('rev3', rev_id='rev3')
 
1142
        bundle = self.get_valid_bundle('reva', 'rev3')
 
1143
        if getattr(bundle, 'get_bundle_reader', None) is None:
 
1144
            raise tests.TestSkipped('Bundle format cannot provide reader')
 
1145
        # be sure that file1 comes before file2
 
1146
        for b, m, k, r, f in bundle.get_bundle_reader().iter_records():
 
1147
            if f == 'file3-id':
 
1148
                break
 
1149
            self.assertNotEqual(f, 'file2-id')
 
1150
        bundle.install_revisions(target.branch.repository)
 
1151
 
 
1152
 
 
1153
class V08BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1154
 
 
1155
    format = '0.8'
 
1156
 
 
1157
    def test_bundle_empty_property(self):
 
1158
        """Test serializing revision properties with an empty value."""
 
1159
        tree = self.make_branch_and_memory_tree('tree')
 
1160
        tree.lock_write()
 
1161
        self.addCleanup(tree.unlock)
 
1162
        tree.add([''], ['TREE_ROOT'])
 
1163
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1164
        self.b1 = tree.branch
 
1165
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
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 get_bundle_tree(self, bundle, revision_id):
 
1180
        repository = self.make_repository('repo')
 
1181
        return bundle.revision_tree(repository, 'revid1')
 
1182
 
 
1183
    def test_bundle_empty_property_alt(self):
 
1184
        """Test serializing revision properties with an empty value.
 
1185
 
 
1186
        Older readers had a bug when reading an empty property.
 
1187
        They assumed that all keys ended in ': \n'. However they would write an
 
1188
        empty value as ':\n'. This tests make sure that all newer bzr versions
 
1189
        can handle th second form.
 
1190
        """
 
1191
        tree = self.make_branch_and_memory_tree('tree')
 
1192
        tree.lock_write()
 
1193
        self.addCleanup(tree.unlock)
 
1194
        tree.add([''], ['TREE_ROOT'])
 
1195
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1196
        self.b1 = tree.branch
 
1197
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1198
        txt = bundle_sio.getvalue()
 
1199
        loc = txt.find('#   empty: ') + len('#   empty:')
 
1200
        # Create a new bundle, which strips the trailing space after empty
 
1201
        bundle_sio = StringIO(txt[:loc] + txt[loc+1:])
 
1202
 
 
1203
        self.assertContainsRe(bundle_sio.getvalue(),
 
1204
                              '# properties:\n'
 
1205
                              '#   branch-nick: tree\n'
 
1206
                              '#   empty:\n'
 
1207
                              '#   one: two\n'
 
1208
                             )
 
1209
        bundle = read_bundle(bundle_sio)
 
1210
        revision_info = bundle.revisions[0]
 
1211
        self.assertEqual('rev1', revision_info.revision_id)
 
1212
        rev = revision_info.as_revision()
 
1213
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1214
                         rev.properties)
 
1215
 
 
1216
    def test_bundle_sorted_properties(self):
 
1217
        """For stability the writer should write properties in sorted order."""
 
1218
        tree = self.make_branch_and_memory_tree('tree')
 
1219
        tree.lock_write()
 
1220
        self.addCleanup(tree.unlock)
 
1221
 
 
1222
        tree.add([''], ['TREE_ROOT'])
 
1223
        tree.commit('One', rev_id='rev1',
 
1224
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
1225
        self.b1 = tree.branch
 
1226
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1227
        self.assertContainsRe(bundle_sio.getvalue(),
 
1228
                              '# properties:\n'
 
1229
                              '#   a: 4\n'
 
1230
                              '#   b: 3\n'
 
1231
                              '#   branch-nick: tree\n'
 
1232
                              '#   c: 2\n'
 
1233
                              '#   d: 1\n'
 
1234
                             )
 
1235
        bundle = read_bundle(bundle_sio)
 
1236
        revision_info = bundle.revisions[0]
 
1237
        self.assertEqual('rev1', revision_info.revision_id)
 
1238
        rev = revision_info.as_revision()
 
1239
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
1240
                          'd':'1'}, rev.properties)
 
1241
 
 
1242
    def test_bundle_unicode_properties(self):
 
1243
        """We should be able to round trip a non-ascii property."""
 
1244
        tree = self.make_branch_and_memory_tree('tree')
 
1245
        tree.lock_write()
 
1246
        self.addCleanup(tree.unlock)
 
1247
 
 
1248
        tree.add([''], ['TREE_ROOT'])
 
1249
        # Revisions themselves do not require anything about revision property
 
1250
        # keys, other than that they are a basestring, and do not contain
 
1251
        # whitespace.
 
1252
        # However, Testaments assert than they are str(), and thus should not
 
1253
        # be Unicode.
 
1254
        tree.commit('One', rev_id='rev1',
 
1255
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1256
        self.b1 = tree.branch
 
1257
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1258
        self.assertContainsRe(bundle_sio.getvalue(),
 
1259
                              '# properties:\n'
 
1260
                              '#   alpha: \xce\xb1\n'
 
1261
                              '#   branch-nick: tree\n'
 
1262
                              '#   omega: \xce\xa9\n'
 
1263
                             )
 
1264
        bundle = read_bundle(bundle_sio)
 
1265
        revision_info = bundle.revisions[0]
 
1266
        self.assertEqual('rev1', revision_info.revision_id)
 
1267
        rev = revision_info.as_revision()
 
1268
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1269
                          'alpha':u'\u03b1'}, rev.properties)
 
1270
 
 
1271
 
 
1272
class V09BundleKnit2Tester(V08BundleTester):
 
1273
 
 
1274
    format = '0.9'
 
1275
 
 
1276
    def bzrdir_format(self):
 
1277
        format = bzrdir.BzrDirMetaFormat1()
 
1278
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
1279
        return format
 
1280
 
 
1281
 
 
1282
class V09BundleKnit1Tester(V08BundleTester):
 
1283
 
 
1284
    format = '0.9'
 
1285
 
 
1286
    def bzrdir_format(self):
 
1287
        format = bzrdir.BzrDirMetaFormat1()
 
1288
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
1289
        return format
 
1290
 
 
1291
 
 
1292
class V4BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1293
 
 
1294
    format = '4'
 
1295
 
 
1296
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
1297
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1298
        Make sure that the text generated is valid, and that it
 
1299
        can be applied against the base, and generate the same information.
 
1300
 
 
1301
        :return: The in-memory bundle
 
1302
        """
 
1303
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1304
 
 
1305
        # This should also validate the generated bundle
 
1306
        bundle = read_bundle(bundle_txt)
 
1307
        repository = self.b1.repository
 
1308
        for bundle_rev in bundle.real_revisions:
 
1309
            # These really should have already been checked when we read the
 
1310
            # bundle, since it computes the sha1 hash for the revision, which
 
1311
            # only will match if everything is okay, but lets be explicit about
 
1312
            # it
 
1313
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
1314
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
1315
                      'timestamp', 'timezone', 'message', 'committer',
 
1316
                      'parent_ids', 'properties'):
 
1317
                self.assertEqual(getattr(branch_rev, a),
 
1318
                                 getattr(bundle_rev, a))
 
1319
            self.assertEqual(len(branch_rev.parent_ids),
 
1320
                             len(bundle_rev.parent_ids))
 
1321
        self.assertEqual(set(rev_ids),
 
1322
                         set([r.revision_id for r in bundle.real_revisions]))
 
1323
        self.valid_apply_bundle(base_rev_id, bundle,
 
1324
                                   checkout_dir=checkout_dir)
 
1325
 
 
1326
        return bundle
 
1327
 
 
1328
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1329
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1330
        Munge the text so that it's invalid.
 
1331
 
 
1332
        :return: The in-memory bundle
 
1333
        """
 
1334
        from bzrlib.bundle import serializer
 
1335
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1336
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1337
        new_text = new_text.replace('<file file_id="exe-1"',
 
1338
                                    '<file executable="y" file_id="exe-1"')
 
1339
        new_text = new_text.replace('B260', 'B275')
 
1340
        bundle_txt = StringIO()
 
1341
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1342
        bundle_txt.write('\n')
 
1343
        bundle_txt.write(new_text.encode('bz2'))
 
1344
        bundle_txt.seek(0)
 
1345
        bundle = read_bundle(bundle_txt)
 
1346
        self.valid_apply_bundle(base_rev_id, bundle)
 
1347
        return bundle
 
1348
 
 
1349
    def create_bundle_text(self, base_rev_id, rev_id):
 
1350
        bundle_txt = StringIO()
 
1351
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
1352
                               bundle_txt, format=self.format)
 
1353
        bundle_txt.seek(0)
 
1354
        self.assertEqual(bundle_txt.readline(),
 
1355
                         '# Bazaar revision bundle v%s\n' % self.format)
 
1356
        self.assertEqual(bundle_txt.readline(), '#\n')
 
1357
        rev = self.b1.repository.get_revision(rev_id)
 
1358
        bundle_txt.seek(0)
 
1359
        return bundle_txt, rev_ids
 
1360
 
 
1361
    def get_bundle_tree(self, bundle, revision_id):
 
1362
        repository = self.make_repository('repo')
 
1363
        bundle.install_revisions(repository)
 
1364
        return repository.revision_tree(revision_id)
 
1365
 
 
1366
    def test_creation(self):
 
1367
        tree = self.make_branch_and_tree('tree')
 
1368
        self.build_tree_contents([('tree/file', 'contents1\nstatic\n')])
 
1369
        tree.add('file', 'fileid-2')
 
1370
        tree.commit('added file', rev_id='rev1')
 
1371
        self.build_tree_contents([('tree/file', 'contents2\nstatic\n')])
 
1372
        tree.commit('changed file', rev_id='rev2')
 
1373
        s = StringIO()
 
1374
        serializer = BundleSerializerV4('1.0')
 
1375
        serializer.write(tree.branch.repository, ['rev1', 'rev2'], {}, s)
 
1376
        s.seek(0)
 
1377
        tree2 = self.make_branch_and_tree('target')
 
1378
        target_repo = tree2.branch.repository
 
1379
        install_bundle(target_repo, serializer.read(s))
 
1380
        target_repo.lock_read()
 
1381
        self.addCleanup(target_repo.unlock)
 
1382
        # Turn the 'iterators_of_bytes' back into simple strings for comparison
 
1383
        repo_texts = dict((i, ''.join(content)) for i, content
 
1384
                          in target_repo.iter_files_bytes(
 
1385
                                [('fileid-2', 'rev1', '1'),
 
1386
                                 ('fileid-2', 'rev2', '2')]))
 
1387
        self.assertEqual({'1':'contents1\nstatic\n',
 
1388
                          '2':'contents2\nstatic\n'},
 
1389
                         repo_texts)
 
1390
        rtree = target_repo.revision_tree('rev2')
 
1391
        inventory_vf = target_repo.inventories
 
1392
        # If the inventory store has a graph, it must match the revision graph.
 
1393
        self.assertSubset(
 
1394
            [inventory_vf.get_parent_map([('rev2',)])[('rev2',)]],
 
1395
            [None, (('rev1',),)])
 
1396
        self.assertEqual('changed file',
 
1397
                         target_repo.get_revision('rev2').message)
 
1398
 
 
1399
    @staticmethod
 
1400
    def get_raw(bundle_file):
 
1401
        bundle_file.seek(0)
 
1402
        line = bundle_file.readline()
 
1403
        line = bundle_file.readline()
 
1404
        lines = bundle_file.readlines()
 
1405
        return ''.join(lines).decode('bz2')
 
1406
 
 
1407
    def test_copy_signatures(self):
 
1408
        tree_a = self.make_branch_and_tree('tree_a')
 
1409
        import bzrlib.gpg
 
1410
        import bzrlib.commit as commit
 
1411
        oldstrategy = bzrlib.gpg.GPGStrategy
 
1412
        branch = tree_a.branch
 
1413
        repo_a = branch.repository
 
1414
        tree_a.commit("base", allow_pointless=True, rev_id='A')
 
1415
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
1416
        try:
 
1417
            from bzrlib.testament import Testament
 
1418
            # monkey patch gpg signing mechanism
 
1419
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
1420
            new_config = test_commit.MustSignConfig(branch)
 
1421
            commit.Commit(config=new_config).commit(message="base",
 
1422
                                                    allow_pointless=True,
 
1423
                                                    rev_id='B',
 
1424
                                                    working_tree=tree_a)
 
1425
            def sign(text):
 
1426
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
1427
            self.assertTrue(repo_a.has_signature_for_revision_id('B'))
 
1428
        finally:
 
1429
            bzrlib.gpg.GPGStrategy = oldstrategy
 
1430
        tree_b = self.make_branch_and_tree('tree_b')
 
1431
        repo_b = tree_b.branch.repository
 
1432
        s = StringIO()
 
1433
        serializer = BundleSerializerV4('4')
 
1434
        serializer.write(tree_a.branch.repository, ['A', 'B'], {}, s)
 
1435
        s.seek(0)
 
1436
        install_bundle(repo_b, serializer.read(s))
 
1437
        self.assertTrue(repo_b.has_signature_for_revision_id('B'))
 
1438
        self.assertEqual(repo_b.get_signature_text('B'),
 
1439
                         repo_a.get_signature_text('B'))
 
1440
        s.seek(0)
 
1441
        # ensure repeat installs are harmless
 
1442
        install_bundle(repo_b, serializer.read(s))
 
1443
 
 
1444
 
 
1445
class V4WeaveBundleTester(V4BundleTester):
 
1446
 
 
1447
    def bzrdir_format(self):
 
1448
        return 'metaweave'
 
1449
 
 
1450
 
 
1451
class V4_2aBundleTester(V4BundleTester):
 
1452
 
 
1453
    def bzrdir_format(self):
 
1454
        return '2a'
 
1455
 
 
1456
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1457
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1458
        Munge the text so that it's invalid.
 
1459
 
 
1460
        :return: The in-memory bundle
 
1461
        """
 
1462
        from bzrlib.bundle import serializer
 
1463
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1464
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1465
        # We are going to be replacing some text to set the executable bit on a
 
1466
        # file. Make sure the text replacement actually works correctly.
 
1467
        self.assertContainsRe(new_text, '(?m)B244\n\ni 1\n<inventory')
 
1468
        new_text = new_text.replace('<file file_id="exe-1"',
 
1469
                                    '<file executable="y" file_id="exe-1"')
 
1470
        new_text = new_text.replace('B244', 'B259')
 
1471
        bundle_txt = StringIO()
 
1472
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1473
        bundle_txt.write('\n')
 
1474
        bundle_txt.write(new_text.encode('bz2'))
 
1475
        bundle_txt.seek(0)
 
1476
        bundle = read_bundle(bundle_txt)
 
1477
        self.valid_apply_bundle(base_rev_id, bundle)
 
1478
        return bundle
 
1479
 
 
1480
    def make_merged_branch(self):
 
1481
        builder = self.make_branch_builder('source')
 
1482
        builder.start_series()
 
1483
        builder.build_snapshot('a@cset-0-1', None, [
 
1484
            ('add', ('', 'root-id', 'directory', None)),
 
1485
            ('add', ('file', 'file-id', 'file', 'original content\n')),
 
1486
            ])
 
1487
        builder.build_snapshot('a@cset-0-2a', ['a@cset-0-1'], [
 
1488
            ('modify', ('file-id', 'new-content\n')),
 
1489
            ])
 
1490
        builder.build_snapshot('a@cset-0-2b', ['a@cset-0-1'], [
 
1491
            ('add', ('other-file', 'file2-id', 'file', 'file2-content\n')),
 
1492
            ])
 
1493
        builder.build_snapshot('a@cset-0-3', ['a@cset-0-2a', 'a@cset-0-2b'], [
 
1494
            ('add', ('other-file', 'file2-id', 'file', 'file2-content\n')),
 
1495
            ])
 
1496
        builder.finish_series()
 
1497
        self.b1 = builder.get_branch()
 
1498
        self.b1.lock_read()
 
1499
        self.addCleanup(self.b1.unlock)
 
1500
 
 
1501
    def make_bundle_just_inventories(self, base_revision_id,
 
1502
                                     target_revision_id,
 
1503
                                     revision_ids):
 
1504
        sio = StringIO()
 
1505
        writer = v4.BundleWriteOperation(base_revision_id, target_revision_id,
 
1506
                                         self.b1.repository, sio)
 
1507
        writer.bundle.begin()
 
1508
        writer._add_inventory_mpdiffs_from_serializer(revision_ids)
 
1509
        writer.bundle.end()
 
1510
        sio.seek(0)
 
1511
        return sio
 
1512
 
 
1513
    def test_single_inventory_multiple_parents_as_xml(self):
 
1514
        self.make_merged_branch()
 
1515
        sio = self.make_bundle_just_inventories('a@cset-0-1', 'a@cset-0-3',
 
1516
                                                ['a@cset-0-3'])
 
1517
        reader = v4.BundleReader(sio, stream_input=False)
 
1518
        records = list(reader.iter_records())
 
1519
        self.assertEqual(1, len(records))
 
1520
        (bytes, metadata, repo_kind, revision_id,
 
1521
         file_id) = records[0]
 
1522
        self.assertIs(None, file_id)
 
1523
        self.assertEqual('a@cset-0-3', revision_id)
 
1524
        self.assertEqual('inventory', repo_kind)
 
1525
        self.assertEqual({'parents': ['a@cset-0-2a', 'a@cset-0-2b'],
 
1526
                          'sha1': '09c53b0c4de0895e11a2aacc34fef60a6e70865c',
 
1527
                          'storage_kind': 'mpdiff',
 
1528
                         }, metadata)
 
1529
        # We should have an mpdiff that takes some lines from both parents.
 
1530
        self.assertEqualDiff(
 
1531
            'i 1\n'
 
1532
            '<inventory format="10" revision_id="a@cset-0-3">\n'
 
1533
            '\n'
 
1534
            'c 0 1 1 2\n'
 
1535
            'c 1 3 3 2\n', bytes)
 
1536
 
 
1537
    def test_single_inv_no_parents_as_xml(self):
 
1538
        self.make_merged_branch()
 
1539
        sio = self.make_bundle_just_inventories('null:', 'a@cset-0-1',
 
1540
                                                ['a@cset-0-1'])
 
1541
        reader = v4.BundleReader(sio, stream_input=False)
 
1542
        records = list(reader.iter_records())
 
1543
        self.assertEqual(1, len(records))
 
1544
        (bytes, metadata, repo_kind, revision_id,
 
1545
         file_id) = records[0]
 
1546
        self.assertIs(None, file_id)
 
1547
        self.assertEqual('a@cset-0-1', revision_id)
 
1548
        self.assertEqual('inventory', repo_kind)
 
1549
        self.assertEqual({'parents': [],
 
1550
                          'sha1': 'a13f42b142d544aac9b085c42595d304150e31a2',
 
1551
                          'storage_kind': 'mpdiff',
 
1552
                         }, metadata)
 
1553
        # We should have an mpdiff that takes some lines from both parents.
 
1554
        self.assertEqualDiff(
 
1555
            'i 4\n'
 
1556
            '<inventory format="10" revision_id="a@cset-0-1">\n'
 
1557
            '<directory file_id="root-id" name=""'
 
1558
                ' revision="a@cset-0-1" />\n'
 
1559
            '<file file_id="file-id" name="file" parent_id="root-id"'
 
1560
                ' revision="a@cset-0-1"'
 
1561
                ' text_sha1="09c2f8647e14e49e922b955c194102070597c2d1"'
 
1562
                ' text_size="17" />\n'
 
1563
            '</inventory>\n'
 
1564
            '\n', bytes)
 
1565
 
 
1566
    def test_multiple_inventories_as_xml(self):
 
1567
        self.make_merged_branch()
 
1568
        sio = self.make_bundle_just_inventories('a@cset-0-1', 'a@cset-0-3',
 
1569
            ['a@cset-0-2a', 'a@cset-0-2b', 'a@cset-0-3'])
 
1570
        reader = v4.BundleReader(sio, stream_input=False)
 
1571
        records = list(reader.iter_records())
 
1572
        self.assertEqual(3, len(records))
 
1573
        revision_ids = [rev_id for b, m, k, rev_id, f in records]
 
1574
        self.assertEqual(['a@cset-0-2a', 'a@cset-0-2b', 'a@cset-0-3'],
 
1575
                         revision_ids)
 
1576
        metadata_2a = records[0][1]
 
1577
        self.assertEqual({'parents': ['a@cset-0-1'],
 
1578
                          'sha1': '1e105886d62d510763e22885eec733b66f5f09bf',
 
1579
                          'storage_kind': 'mpdiff',
 
1580
                         }, metadata_2a)
 
1581
        metadata_2b = records[1][1]
 
1582
        self.assertEqual({'parents': ['a@cset-0-1'],
 
1583
                          'sha1': 'f03f12574bdb5ed2204c28636c98a8547544ccd8',
 
1584
                          'storage_kind': 'mpdiff',
 
1585
                         }, metadata_2b)
 
1586
        metadata_3 = records[2][1]
 
1587
        self.assertEqual({'parents': ['a@cset-0-2a', 'a@cset-0-2b'],
 
1588
                          'sha1': '09c53b0c4de0895e11a2aacc34fef60a6e70865c',
 
1589
                          'storage_kind': 'mpdiff',
 
1590
                         }, metadata_3)
 
1591
        bytes_2a = records[0][0]
 
1592
        self.assertEqualDiff(
 
1593
            'i 1\n'
 
1594
            '<inventory format="10" revision_id="a@cset-0-2a">\n'
 
1595
            '\n'
 
1596
            'c 0 1 1 1\n'
 
1597
            'i 1\n'
 
1598
            '<file file_id="file-id" name="file" parent_id="root-id"'
 
1599
                ' revision="a@cset-0-2a"'
 
1600
                ' text_sha1="50f545ff40e57b6924b1f3174b267ffc4576e9a9"'
 
1601
                ' text_size="12" />\n'
 
1602
            '\n'
 
1603
            'c 0 3 3 1\n', bytes_2a)
 
1604
        bytes_2b = records[1][0]
 
1605
        self.assertEqualDiff(
 
1606
            'i 1\n'
 
1607
            '<inventory format="10" revision_id="a@cset-0-2b">\n'
 
1608
            '\n'
 
1609
            'c 0 1 1 2\n'
 
1610
            'i 1\n'
 
1611
            '<file file_id="file2-id" name="other-file" parent_id="root-id"'
 
1612
                ' revision="a@cset-0-2b"'
 
1613
                ' text_sha1="b46c0c8ea1e5ef8e46fc8894bfd4752a88ec939e"'
 
1614
                ' text_size="14" />\n'
 
1615
            '\n'
 
1616
            'c 0 3 4 1\n', bytes_2b)
 
1617
        bytes_3 = records[2][0]
 
1618
        self.assertEqualDiff(
 
1619
            'i 1\n'
 
1620
            '<inventory format="10" revision_id="a@cset-0-3">\n'
 
1621
            '\n'
 
1622
            'c 0 1 1 2\n'
 
1623
            'c 1 3 3 2\n', bytes_3)
 
1624
 
 
1625
    def test_creating_bundle_preserves_chk_pages(self):
 
1626
        self.make_merged_branch()
 
1627
        target = self.b1.bzrdir.sprout('target',
 
1628
                                       revision_id='a@cset-0-2a').open_branch()
 
1629
        bundle_txt, rev_ids = self.create_bundle_text('a@cset-0-2a',
 
1630
                                                      'a@cset-0-3')
 
1631
        self.assertEqual(['a@cset-0-2b', 'a@cset-0-3'], rev_ids)
 
1632
        bundle = read_bundle(bundle_txt)
 
1633
        target.lock_write()
 
1634
        self.addCleanup(target.unlock)
 
1635
        install_bundle(target.repository, bundle)
 
1636
        inv1 = self.b1.repository.inventories.get_record_stream([
 
1637
            ('a@cset-0-3',)], 'unordered',
 
1638
            True).next().get_bytes_as('fulltext')
 
1639
        inv2 = target.repository.inventories.get_record_stream([
 
1640
            ('a@cset-0-3',)], 'unordered',
 
1641
            True).next().get_bytes_as('fulltext')
 
1642
        self.assertEqualDiff(inv1, inv2)
 
1643
 
 
1644
 
 
1645
class MungedBundleTester(object):
804
1646
 
805
1647
    def build_test_bundle(self):
806
1648
        wt = self.make_branch_and_tree('b1')
815
1657
 
816
1658
        bundle_txt = StringIO()
817
1659
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
818
 
                               'a@cset-0-1', bundle_txt)
819
 
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
1660
                               'a@cset-0-1', bundle_txt, self.format)
 
1661
        self.assertEqual(set(['a@cset-0-2']), set(rev_ids))
820
1662
        bundle_txt.seek(0, 0)
821
1663
        return bundle_txt
822
1664
 
851
1693
        bundle = read_bundle(bundle_txt)
852
1694
        self.check_valid(bundle)
853
1695
 
 
1696
 
 
1697
class MungedBundleTesterV09(tests.TestCaseWithTransport, MungedBundleTester):
 
1698
 
 
1699
    format = '0.9'
 
1700
 
854
1701
    def test_missing_trailing_whitespace(self):
855
1702
        bundle_txt = self.build_test_bundle()
856
1703
 
884
1731
        bundle = read_bundle(bundle_txt)
885
1732
        self.check_valid(bundle)
886
1733
 
 
1734
 
 
1735
class MungedBundleTesterV4(tests.TestCaseWithTransport, MungedBundleTester):
 
1736
 
 
1737
    format = '4'
 
1738
 
 
1739
 
 
1740
class TestBundleWriterReader(tests.TestCase):
 
1741
 
 
1742
    def test_roundtrip_record(self):
 
1743
        fileobj = StringIO()
 
1744
        writer = v4.BundleWriter(fileobj)
 
1745
        writer.begin()
 
1746
        writer.add_info_record(foo='bar')
 
1747
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1748
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1749
        writer.end()
 
1750
        fileobj.seek(0)
 
1751
        reader = v4.BundleReader(fileobj, stream_input=True)
 
1752
        record_iter = reader.iter_records()
 
1753
        record = record_iter.next()
 
1754
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1755
            'info', None, None), record)
 
1756
        record = record_iter.next()
 
1757
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1758
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1759
                          record)
 
1760
 
 
1761
    def test_roundtrip_record_memory_hungry(self):
 
1762
        fileobj = StringIO()
 
1763
        writer = v4.BundleWriter(fileobj)
 
1764
        writer.begin()
 
1765
        writer.add_info_record(foo='bar')
 
1766
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1767
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1768
        writer.end()
 
1769
        fileobj.seek(0)
 
1770
        reader = v4.BundleReader(fileobj, stream_input=False)
 
1771
        record_iter = reader.iter_records()
 
1772
        record = record_iter.next()
 
1773
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1774
            'info', None, None), record)
 
1775
        record = record_iter.next()
 
1776
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1777
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1778
                          record)
 
1779
 
 
1780
    def test_encode_name(self):
 
1781
        self.assertEqual('revision/rev1',
 
1782
            v4.BundleWriter.encode_name('revision', 'rev1'))
 
1783
        self.assertEqual('file/rev//1/file-id-1',
 
1784
            v4.BundleWriter.encode_name('file', 'rev/1', 'file-id-1'))
 
1785
        self.assertEqual('info',
 
1786
            v4.BundleWriter.encode_name('info', None, None))
 
1787
 
 
1788
    def test_decode_name(self):
 
1789
        self.assertEqual(('revision', 'rev1', None),
 
1790
            v4.BundleReader.decode_name('revision/rev1'))
 
1791
        self.assertEqual(('file', 'rev/1', 'file-id-1'),
 
1792
            v4.BundleReader.decode_name('file/rev//1/file-id-1'))
 
1793
        self.assertEqual(('info', None, None),
 
1794
                         v4.BundleReader.decode_name('info'))
 
1795
 
 
1796
    def test_too_many_names(self):
 
1797
        fileobj = StringIO()
 
1798
        writer = v4.BundleWriter(fileobj)
 
1799
        writer.begin()
 
1800
        writer.add_info_record(foo='bar')
 
1801
        writer._container.add_bytes_record('blah', ['two', 'names'])
 
1802
        writer.end()
 
1803
        fileobj.seek(0)
 
1804
        record_iter = v4.BundleReader(fileobj).iter_records()
 
1805
        record = record_iter.next()
 
1806
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1807
            'info', None, None), record)
 
1808
        self.assertRaises(errors.BadBundle, record_iter.next)
 
1809
 
 
1810
 
 
1811
class TestReadMergeableFromUrl(tests.TestCaseWithTransport):
 
1812
 
 
1813
    def test_read_mergeable_skips_local(self):
 
1814
        """A local bundle named like the URL should not be read.
 
1815
        """
 
1816
        out, wt = test_read_bundle.create_bundle_file(self)
 
1817
        class FooService(object):
 
1818
            """A directory service that always returns source"""
 
1819
 
 
1820
            def look_up(self, name, url):
 
1821
                return 'source'
 
1822
        directories.register('foo:', FooService, 'Testing directory service')
 
1823
        self.addCleanup(lambda: directories.remove('foo:'))
 
1824
        self.build_tree_contents([('./foo:bar', out.getvalue())])
 
1825
        self.assertRaises(errors.NotABundle, read_mergeable_from_url,
 
1826
                          'foo:bar')
 
1827
 
 
1828
    def test_infinite_redirects_are_not_a_bundle(self):
 
1829
        """If a URL causes TooManyRedirections then NotABundle is raised.
 
1830
        """
 
1831
        from bzrlib.tests.blackbox.test_push import RedirectingMemoryServer
 
1832
        server = RedirectingMemoryServer()
 
1833
        server.setUp()
 
1834
        url = server.get_url() + 'infinite-loop'
 
1835
        self.addCleanup(server.tearDown)
 
1836
        self.assertRaises(errors.NotABundle, read_mergeable_from_url, url)
 
1837
 
 
1838
    def test_smart_server_connection_reset(self):
 
1839
        """If a smart server connection fails during the attempt to read a
 
1840
        bundle, then the ConnectionReset error should be propagated.
 
1841
        """
 
1842
        # Instantiate a server that will provoke a ConnectionReset
 
1843
        sock_server = _DisconnectingTCPServer()
 
1844
        sock_server.setUp()
 
1845
        self.addCleanup(sock_server.tearDown)
 
1846
        # We don't really care what the url is since the server will close the
 
1847
        # connection without interpreting it
 
1848
        url = sock_server.get_url()
 
1849
        self.assertRaises(errors.ConnectionReset, read_mergeable_from_url, url)
 
1850
 
 
1851
 
 
1852
class _DisconnectingTCPServer(object):
 
1853
    """A TCP server that immediately closes any connection made to it."""
 
1854
 
 
1855
    def setUp(self):
 
1856
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1857
        self.sock.bind(('127.0.0.1', 0))
 
1858
        self.sock.listen(1)
 
1859
        self.port = self.sock.getsockname()[1]
 
1860
        self.thread = threading.Thread(
 
1861
            name='%s (port %d)' % (self.__class__.__name__, self.port),
 
1862
            target=self.accept_and_close)
 
1863
        self.thread.start()
 
1864
 
 
1865
    def accept_and_close(self):
 
1866
        conn, addr = self.sock.accept()
 
1867
        conn.shutdown(socket.SHUT_RDWR)
 
1868
        conn.close()
 
1869
 
 
1870
    def get_url(self):
 
1871
        return 'bzr://127.0.0.1:%d/' % (self.port,)
 
1872
 
 
1873
    def tearDown(self):
 
1874
        try:
 
1875
            # make sure the thread dies by connecting to the listening socket,
 
1876
            # just in case the test failed to do so.
 
1877
            conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1878
            conn.connect(self.sock.getsockname())
 
1879
            conn.close()
 
1880
        except socket.error:
 
1881
            pass
 
1882
        self.sock.close()
 
1883
        self.thread.join()
 
1884