~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Jelmer Vernooij
  • Date: 2011-08-19 22:34:02 UTC
  • mto: This revision was merged to the branch mainline in revision 6089.
  • Revision ID: jelmer@samba.org-20110819223402-wjywqb0fa1xxx522
Use get_transport_from_{url,path} in more places.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2011 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
23
from bzrlib import (
23
24
    bzrdir,
 
25
    diff,
24
26
    errors,
25
27
    inventory,
26
 
    repository,
 
28
    merge,
 
29
    osutils,
 
30
    revision as _mod_revision,
 
31
    symbol_versioning,
 
32
    tests,
27
33
    treebuilder,
28
34
    )
29
 
from bzrlib.builtins import _merge_helper
30
 
from bzrlib.bzrdir import BzrDir
 
35
from bzrlib.bundle import read_mergeable_from_url
31
36
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
32
37
from bzrlib.bundle.bundle_data import BundleTree
33
 
from bzrlib.bundle.serializer import write_bundle, read_bundle
 
38
from bzrlib.directory_service import directories
 
39
from bzrlib.bundle.serializer import write_bundle, read_bundle, v09, v4
34
40
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
35
41
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
36
 
from bzrlib.branch import Branch
37
 
from bzrlib.diff import internal_diff
38
 
from bzrlib.errors import (BzrError, TestamentMismatch, NotABundle, BadBundle, 
39
 
                           NoSuchFile,)
40
 
from bzrlib.merge import Merge3Merger
 
42
from bzrlib.bundle.serializer.v4 import BundleSerializerV4
41
43
from bzrlib.repofmt import knitrepo
42
 
from bzrlib.osutils import has_symlinks, sha_file
43
 
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
44
 
                          TestCase, TestSkipped)
 
44
from bzrlib.tests import (
 
45
    test_read_bundle,
 
46
    test_commit,
 
47
    )
45
48
from bzrlib.transform import TreeTransform
46
 
from bzrlib.workingtree import WorkingTree
 
49
from bzrlib.tests import (
 
50
    features,
 
51
    )
 
52
 
 
53
 
 
54
def get_text(vf, key):
 
55
    """Get the fulltext for a given revision id that is present in the vf"""
 
56
    stream = vf.get_record_stream([key], 'unordered', True)
 
57
    record = stream.next()
 
58
    return record.get_bytes_as('fulltext')
 
59
 
 
60
 
 
61
def get_inventory_text(repo, revision_id):
 
62
    """Get the fulltext for the inventory at revision id"""
 
63
    repo.lock_read()
 
64
    try:
 
65
        return get_text(repo.inventories, (revision_id,))
 
66
    finally:
 
67
        repo.unlock()
47
68
 
48
69
 
49
70
class MockTree(object):
 
71
 
50
72
    def __init__(self):
51
73
        from bzrlib.inventory import InventoryDirectory, ROOT_ID
52
74
        object.__init__(self)
57
79
 
58
80
    inventory = property(lambda x:x)
59
81
 
60
 
    def __iter__(self):
61
 
        return self.paths.iterkeys()
 
82
    def all_file_ids(self):
 
83
        return set(self.paths.keys())
62
84
 
63
85
    def __getitem__(self, file_id):
64
86
        if file_id == self.root.file_id:
94
116
            ie = InventoryDirectory(file_id, name, parent_id)
95
117
        elif kind == 'file':
96
118
            ie = InventoryFile(file_id, name, parent_id)
 
119
            ie.text_sha1 = text_sha_1
 
120
            ie.text_size = text_size
97
121
        elif kind == 'symlink':
98
122
            ie = InventoryLink(file_id, name, parent_id)
99
123
        else:
100
 
            raise BzrError('unknown kind %r' % kind)
101
 
        ie.text_sha1 = text_sha_1
102
 
        ie.text_size = text_size
 
124
            raise errors.BzrError('unknown kind %r' % kind)
103
125
        return ie
104
126
 
105
127
    def add_dir(self, file_id, path):
106
128
        self.paths[file_id] = path
107
129
        self.ids[path] = file_id
108
 
    
 
130
 
109
131
    def add_file(self, file_id, path, contents):
110
132
        self.add_dir(file_id, path)
111
133
        self.contents[file_id] = contents
125
147
        result.seek(0,0)
126
148
        return result
127
149
 
 
150
    def get_file_revision(self, file_id):
 
151
        return self.inventory[file_id].revision
 
152
 
128
153
    def contents_stats(self, file_id):
129
154
        if file_id not in self.contents:
130
155
            return None, None
131
 
        text_sha1 = sha_file(self.get_file(file_id))
 
156
        text_sha1 = osutils.sha_file(self.get_file(file_id))
132
157
        return text_sha1, len(self.contents[file_id])
133
158
 
134
159
 
135
 
class BTreeTester(TestCase):
 
160
class BTreeTester(tests.TestCase):
136
161
    """A simple unittest tester for the BundleTree class."""
137
162
 
138
163
    def make_tree_1(self):
142
167
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
143
168
        mtree.add_dir("d", "grandparent/alt_parent")
144
169
        return BundleTree(mtree, ''), mtree
145
 
        
 
170
 
146
171
    def test_renames(self):
147
172
        """Ensure that file renames have the proper effect on children"""
148
173
        btree = self.make_tree_1()[0]
149
174
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
150
 
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
175
        self.assertEqual(btree.old_path("grandparent/parent"),
151
176
                         "grandparent/parent")
152
177
        self.assertEqual(btree.old_path("grandparent/parent/file"),
153
178
                         "grandparent/parent/file")
160
185
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
161
186
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
162
187
 
163
 
        assert btree.path2id("grandparent2") is None
164
 
        assert btree.path2id("grandparent2/parent") is None
165
 
        assert btree.path2id("grandparent2/parent/file") is None
 
188
        self.assertTrue(btree.path2id("grandparent2") is None)
 
189
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
190
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
166
191
 
167
192
        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
 
193
        self.assertTrue(btree.old_path("grandparent") is None)
 
194
        self.assertTrue(btree.old_path("grandparent/parent") is None)
 
195
        self.assertTrue(btree.old_path("grandparent/parent/file") is None)
171
196
 
172
197
        self.assertEqual(btree.id2path("a"), "grandparent2")
173
198
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
177
202
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
178
203
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
179
204
 
180
 
        assert btree.path2id("grandparent") is None
181
 
        assert btree.path2id("grandparent/parent") is None
182
 
        assert btree.path2id("grandparent/parent/file") is None
 
205
        self.assertTrue(btree.path2id("grandparent") is None)
 
206
        self.assertTrue(btree.path2id("grandparent/parent") is None)
 
207
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
183
208
 
184
209
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
185
210
        self.assertEqual(btree.id2path("a"), "grandparent2")
190
215
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
191
216
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
192
217
 
193
 
        assert btree.path2id("grandparent2/parent") is None
194
 
        assert btree.path2id("grandparent2/parent/file") is None
 
218
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
219
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
195
220
 
196
 
        btree.note_rename("grandparent/parent/file", 
 
221
        btree.note_rename("grandparent/parent/file",
197
222
                          "grandparent2/parent2/file2")
198
223
        self.assertEqual(btree.id2path("a"), "grandparent2")
199
224
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
203
228
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
204
229
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
205
230
 
206
 
        assert btree.path2id("grandparent2/parent2/file") is None
 
231
        self.assertTrue(btree.path2id("grandparent2/parent2/file") is None)
207
232
 
208
233
    def test_moves(self):
209
234
        """Ensure that file moves have the proper effect on children"""
210
235
        btree = self.make_tree_1()[0]
211
 
        btree.note_rename("grandparent/parent/file", 
 
236
        btree.note_rename("grandparent/parent/file",
212
237
                          "grandparent/alt_parent/file")
213
238
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
214
239
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
215
 
        assert btree.path2id("grandparent/parent/file") is None
 
240
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
216
241
 
217
242
    def unified_diff(self, old, new):
218
243
        out = StringIO()
219
 
        internal_diff("old", old, "new", new, out)
 
244
        diff.internal_diff("old", old, "new", new, out)
220
245
        out.seek(0,0)
221
246
        return out.read()
222
247
 
223
248
    def make_tree_2(self):
224
249
        btree = self.make_tree_1()[0]
225
 
        btree.note_rename("grandparent/parent/file", 
 
250
        btree.note_rename("grandparent/parent/file",
226
251
                          "grandparent/alt_parent/file")
227
 
        assert btree.id2path("e") is None
228
 
        assert btree.path2id("grandparent/parent/file") is None
 
252
        self.assertTrue(btree.id2path("e") is None)
 
253
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
229
254
        btree.note_id("e", "grandparent/parent/file")
230
255
        return btree
231
256
 
257
282
    def make_tree_3(self):
258
283
        btree, mtree = self.make_tree_1()
259
284
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
260
 
        btree.note_rename("grandparent/parent/file", 
 
285
        btree.note_rename("grandparent/parent/file",
261
286
                          "grandparent/alt_parent/file")
262
 
        btree.note_rename("grandparent/parent/topping", 
 
287
        btree.note_rename("grandparent/parent/topping",
263
288
                          "grandparent/alt_parent/stopping")
264
289
        return btree
265
290
 
289
314
        btree = self.make_tree_1()[0]
290
315
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
291
316
        btree.note_deletion("grandparent/parent/file")
292
 
        assert btree.id2path("c") is None
293
 
        assert btree.path2id("grandparent/parent/file") is None
 
317
        self.assertTrue(btree.id2path("c") is None)
 
318
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
294
319
 
295
320
    def sorted_ids(self, tree):
296
321
        ids = list(tree)
304
329
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
305
330
        btree.note_deletion("grandparent/parent/file")
306
331
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
307
 
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
332
        btree.note_last_changed("grandparent/alt_parent/fool",
308
333
                                "revisionidiguess")
309
334
        self.assertEqual(self.sorted_ids(btree),
310
335
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
311
336
 
312
337
 
313
 
class BundleTester1(TestCaseWithTransport):
 
338
class BundleTester1(tests.TestCaseWithTransport):
314
339
 
315
340
    def test_mismatched_bundle(self):
316
341
        format = bzrdir.BzrDirMetaFormat1()
317
 
        format.repository_format = knitrepo.RepositoryFormatKnit2()
 
342
        format.repository_format = knitrepo.RepositoryFormatKnit3()
318
343
        serializer = BundleSerializerV08('0.8')
319
344
        b = self.make_branch('.', format=format)
320
 
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write, 
 
345
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write,
321
346
                          b.repository, [], {}, StringIO())
322
347
 
323
348
    def test_matched_bundle(self):
324
349
        """Don't raise IncompatibleBundleFormat for knit2 and bundle0.9"""
325
350
        format = bzrdir.BzrDirMetaFormat1()
326
 
        format.repository_format = knitrepo.RepositoryFormatKnit2()
 
351
        format.repository_format = knitrepo.RepositoryFormatKnit3()
327
352
        serializer = BundleSerializerV09('0.9')
328
353
        b = self.make_branch('.', format=format)
329
354
        serializer.write(b.repository, [], {}, StringIO())
331
356
    def test_mismatched_model(self):
332
357
        """Try copying a bundle from knit2 to knit1"""
333
358
        format = bzrdir.BzrDirMetaFormat1()
334
 
        format.repository_format = knitrepo.RepositoryFormatKnit2()
 
359
        format.repository_format = knitrepo.RepositoryFormatKnit3()
335
360
        source = self.make_branch_and_tree('source', format=format)
336
361
        source.commit('one', rev_id='one-id')
337
362
        source.commit('two', rev_id='two-id')
338
363
        text = StringIO()
339
 
        write_bundle(source.branch.repository, 'two-id', None, text, 
 
364
        write_bundle(source.branch.repository, 'two-id', 'null:', text,
340
365
                     format='0.9')
341
366
        text.seek(0)
342
367
 
343
368
        format = bzrdir.BzrDirMetaFormat1()
344
369
        format.repository_format = knitrepo.RepositoryFormatKnit1()
345
370
        target = self.make_branch('target', format=format)
346
 
        self.assertRaises(errors.IncompatibleRevision, install_bundle, 
 
371
        self.assertRaises(errors.IncompatibleRevision, install_bundle,
347
372
                          target.repository, read_bundle(text))
348
373
 
349
374
 
350
 
class V08BundleTester(TestCaseWithTransport):
351
 
 
352
 
    format = '0.8'
 
375
class BundleTester(object):
353
376
 
354
377
    def bzrdir_format(self):
355
378
        format = bzrdir.BzrDirMetaFormat1()
359
382
    def make_branch_and_tree(self, path, format=None):
360
383
        if format is None:
361
384
            format = self.bzrdir_format()
362
 
        return TestCaseWithTransport.make_branch_and_tree(self, path, format)
 
385
        return tests.TestCaseWithTransport.make_branch_and_tree(
 
386
            self, path, format)
363
387
 
364
388
    def make_branch(self, path, format=None):
365
389
        if format is None:
366
390
            format = self.bzrdir_format()
367
 
        return TestCaseWithTransport.make_branch(self, path, format)
 
391
        return tests.TestCaseWithTransport.make_branch(self, path, format)
368
392
 
369
393
    def create_bundle_text(self, base_rev_id, rev_id):
370
394
        bundle_txt = StringIO()
371
 
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
395
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
372
396
                               bundle_txt, format=self.format)
373
397
        bundle_txt.seek(0)
374
 
        self.assertEqual(bundle_txt.readline(), 
 
398
        self.assertEqual(bundle_txt.readline(),
375
399
                         '# Bazaar revision bundle v%s\n' % self.format)
376
400
        self.assertEqual(bundle_txt.readline(), '#\n')
377
401
 
378
402
        rev = self.b1.repository.get_revision(rev_id)
379
403
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
380
404
                         u'# message:\n')
381
 
 
382
 
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
383
405
        bundle_txt.seek(0)
384
406
        return bundle_txt, rev_ids
385
407
 
387
409
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
388
410
        Make sure that the text generated is valid, and that it
389
411
        can be applied against the base, and generate the same information.
390
 
        
391
 
        :return: The in-memory bundle 
 
412
 
 
413
        :return: The in-memory bundle
392
414
        """
393
415
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
394
416
 
395
 
        # This should also validate the generated bundle 
 
417
        # This should also validate the generated bundle
396
418
        bundle = read_bundle(bundle_txt)
397
419
        repository = self.b1.repository
398
420
        for bundle_rev in bundle.real_revisions:
402
424
            # it
403
425
            branch_rev = repository.get_revision(bundle_rev.revision_id)
404
426
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
405
 
                      'timestamp', 'timezone', 'message', 'committer', 
 
427
                      'timestamp', 'timezone', 'message', 'committer',
406
428
                      'parent_ids', 'properties'):
407
 
                self.assertEqual(getattr(branch_rev, a), 
 
429
                self.assertEqual(getattr(branch_rev, a),
408
430
                                 getattr(bundle_rev, a))
409
 
            self.assertEqual(len(branch_rev.parent_ids), 
 
431
            self.assertEqual(len(branch_rev.parent_ids),
410
432
                             len(bundle_rev.parent_ids))
411
 
        self.assertEqual(rev_ids, 
 
433
        self.assertEqual(rev_ids,
412
434
                         [r.revision_id for r in bundle.real_revisions])
413
435
        self.valid_apply_bundle(base_rev_id, bundle,
414
436
                                   checkout_dir=checkout_dir)
418
440
    def get_invalid_bundle(self, base_rev_id, rev_id):
419
441
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
420
442
        Munge the text so that it's invalid.
421
 
        
 
443
 
422
444
        :return: The in-memory bundle
423
445
        """
424
446
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
425
 
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
447
        new_text = bundle_txt.getvalue().replace('executable:no',
426
448
                                               'executable:yes')
427
449
        bundle_txt = StringIO(new_text)
428
450
        bundle = read_bundle(bundle_txt)
429
451
        self.valid_apply_bundle(base_rev_id, bundle)
430
 
        return bundle 
 
452
        return bundle
431
453
 
432
454
    def test_non_bundle(self):
433
 
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
455
        self.assertRaises(errors.NotABundle,
 
456
                          read_bundle, StringIO('#!/bin/sh\n'))
434
457
 
435
458
    def test_malformed(self):
436
 
        self.assertRaises(BadBundle, read_bundle, 
 
459
        self.assertRaises(errors.BadBundle, read_bundle,
437
460
                          StringIO('# Bazaar revision bundle v'))
438
461
 
439
462
    def test_crlf_bundle(self):
440
463
        try:
441
464
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
442
 
        except BadBundle:
 
465
        except errors.BadBundle:
443
466
            # It is currently permitted for bundles with crlf line endings to
444
467
            # make read_bundle raise a BadBundle, but this should be fixed.
445
468
            # Anything else, especially NotABundle, is an error.
450
473
        """
451
474
 
452
475
        if checkout_dir is None:
453
 
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
476
            checkout_dir = osutils.mkdtemp(prefix='test-branch-', dir='.')
454
477
        else:
455
478
            if not os.path.exists(checkout_dir):
456
479
                os.mkdir(checkout_dir)
457
480
        tree = self.make_branch_and_tree(checkout_dir)
458
481
        s = StringIO()
459
 
        ancestors = write_bundle(self.b1.repository, rev_id, None, s,
 
482
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
460
483
                                 format=self.format)
461
484
        s.seek(0)
462
 
        assert isinstance(s.getvalue(), str), (
463
 
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
485
        self.assertIsInstance(s.getvalue(), str)
464
486
        install_bundle(tree.branch.repository, read_bundle(s))
465
487
        for ancestor in ancestors:
466
488
            old = self.b1.repository.revision_tree(ancestor)
467
489
            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:
 
490
            old.lock_read()
 
491
            new.lock_read()
 
492
            try:
 
493
                # Check that there aren't any inventory level changes
 
494
                delta = new.changes_from(old)
 
495
                self.assertFalse(delta.has_changed(),
 
496
                                 'Revision %s not copied correctly.'
 
497
                                 % (ancestor,))
 
498
 
 
499
                # Now check that the file contents are all correct
 
500
                for inventory_id in old.all_file_ids():
 
501
                    try:
 
502
                        old_file = old.get_file(inventory_id)
 
503
                    except errors.NoSuchFile:
 
504
                        continue
 
505
                    if old_file is None:
 
506
                        continue
 
507
                    self.assertEqual(old_file.read(),
 
508
                                     new.get_file(inventory_id).read())
 
509
            finally:
 
510
                new.unlock()
 
511
                old.unlock()
 
512
        if not _mod_revision.is_null(rev_id):
486
513
            rh = self.b1.revision_history()
487
 
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
 
514
            self.applyDeprecated(symbol_versioning.deprecated_in((2, 4, 0)),
 
515
                tree.branch.set_revision_history, rh[:rh.index(rev_id)+1])
488
516
            tree.update()
489
517
            delta = tree.changes_from(self.b1.repository.revision_tree(rev_id))
490
518
            self.assertFalse(delta.has_changed(),
491
 
                             'Working tree has modifications')
 
519
                             'Working tree has modifications: %s' % delta)
492
520
        return tree
493
521
 
494
522
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
496
524
        sure everything matches the builtin branch.
497
525
        """
498
526
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
527
        to_tree.lock_write()
 
528
        try:
 
529
            self._valid_apply_bundle(base_rev_id, info, to_tree)
 
530
        finally:
 
531
            to_tree.unlock()
 
532
 
 
533
    def _valid_apply_bundle(self, base_rev_id, info, to_tree):
499
534
        original_parents = to_tree.get_parent_ids()
500
535
        repository = to_tree.branch.repository
501
536
        original_parents = to_tree.get_parent_ids()
502
537
        self.assertIs(repository.has_revision(base_rev_id), True)
503
538
        for rev in info.real_revisions:
504
539
            self.assert_(not repository.has_revision(rev.revision_id),
505
 
                'Revision {%s} present before applying bundle' 
 
540
                'Revision {%s} present before applying bundle'
506
541
                % rev.revision_id)
507
 
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
542
        merge_bundle(info, to_tree, True, merge.Merge3Merger, False, False)
508
543
 
509
544
        for rev in info.real_revisions:
510
545
            self.assert_(repository.has_revision(rev.revision_id),
511
 
                'Missing revision {%s} after applying bundle' 
 
546
                'Missing revision {%s} after applying bundle'
512
547
                % rev.revision_id)
513
548
 
514
549
        self.assert_(to_tree.branch.repository.has_revision(info.target))
520
555
        rev = info.real_revisions[-1]
521
556
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
522
557
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
523
 
        
 
558
 
524
559
        # TODO: make sure the target tree is identical to base tree
525
560
        #       we might also check the working tree.
526
561
 
545
580
        self.tree1 = self.make_branch_and_tree('b1')
546
581
        self.b1 = self.tree1.branch
547
582
 
548
 
        open('b1/one', 'wb').write('one\n')
549
 
        self.tree1.add('one')
 
583
        self.build_tree_contents([('b1/one', 'one\n')])
 
584
        self.tree1.add('one', 'one-id')
 
585
        self.tree1.set_root_id('root-id')
550
586
        self.tree1.commit('add one', rev_id='a@cset-0-1')
551
587
 
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')
 
588
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
559
589
 
560
590
        # Make sure we can handle files with spaces, tabs, other
561
591
        # bogus characters
569
599
                , 'b1/sub/sub/'
570
600
                , 'b1/sub/sub/nonempty.txt'
571
601
                ])
572
 
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
573
 
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
602
        self.build_tree_contents([('b1/sub/sub/emptyfile.txt', ''),
 
603
                                  ('b1/dir/nolastnewline.txt', 'bloop')])
574
604
        tt = TreeTransform(self.tree1)
575
605
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
576
606
        tt.apply()
 
607
        # have to fix length of file-id so that we can predictably rewrite
 
608
        # a (length-prefixed) record containing it later.
 
609
        self.tree1.add('with space.txt', 'withspace-id')
577
610
        self.tree1.add([
578
 
                'with space.txt'
579
 
                , 'dir'
 
611
                  'dir'
580
612
                , 'dir/filein subdir.c'
581
613
                , 'dir/WithCaps.txt'
582
614
                , 'dir/ pre space'
590
622
 
591
623
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
592
624
 
593
 
        # Check a rollup bundle 
594
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
625
        # Check a rollup bundle
 
626
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
595
627
 
596
628
        # Now delete entries
597
629
        self.tree1.remove(
604
636
        tt.set_executability(False, trans_id)
605
637
        tt.apply()
606
638
        self.tree1.commit('removed', rev_id='a@cset-0-3')
607
 
        
 
639
 
608
640
        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')
 
641
        self.assertRaises((errors.TestamentMismatch,
 
642
            errors.VersionedFileInvalidChecksum,
 
643
            errors.BadBundle), self.get_invalid_bundle,
 
644
            'a@cset-0-2', 'a@cset-0-3')
 
645
        # Check a rollup bundle
 
646
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
613
647
 
614
648
        # Now move the directory
615
649
        self.tree1.rename_one('dir', 'sub/dir')
616
650
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
617
651
 
618
652
        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')
 
653
        # Check a rollup bundle
 
654
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
621
655
 
622
656
        # Modified files
623
657
        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')
 
658
        open('b1/sub/dir/ pre space', 'ab').write(
 
659
             '\r\nAdding some\r\nDOS format lines\r\n')
625
660
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
626
 
        self.tree1.rename_one('sub/dir/ pre space', 
 
661
        self.tree1.rename_one('sub/dir/ pre space',
627
662
                              'sub/ start space')
628
663
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
629
664
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
635
670
                          verbose=False)
636
671
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
637
672
        other = self.get_checkout('a@cset-0-5')
638
 
        tree1_inv = self.tree1.branch.repository.get_inventory_xml(
639
 
            'a@cset-0-5')
640
 
        tree2_inv = other.branch.repository.get_inventory_xml('a@cset-0-5')
 
673
        tree1_inv = get_inventory_text(self.tree1.branch.repository,
 
674
                                       'a@cset-0-5')
 
675
        tree2_inv = get_inventory_text(other.branch.repository,
 
676
                                       'a@cset-0-5')
641
677
        self.assertEqualDiff(tree1_inv, tree2_inv)
642
678
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
643
679
        other.commit('rename file', rev_id='a@cset-0-6b')
644
 
        _merge_helper([other.basedir, -1], [None, None],
645
 
                      this_dir=self.tree1.basedir)
 
680
        self.tree1.merge_from_branch(other.branch)
646
681
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
647
682
                          verbose=False)
648
683
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
649
684
 
650
 
    def test_symlink_bundle(self):
651
 
        if not has_symlinks():
652
 
            raise TestSkipped("No symlink support")
 
685
    def _test_symlink_bundle(self, link_name, link_target, new_link_target):
 
686
        link_id = 'link-1'
 
687
 
 
688
        self.requireFeature(features.SymlinkFeature)
653
689
        self.tree1 = self.make_branch_and_tree('b1')
654
690
        self.b1 = self.tree1.branch
 
691
 
655
692
        tt = TreeTransform(self.tree1)
656
 
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
693
        tt.new_symlink(link_name, tt.root, link_target, link_id)
657
694
        tt.apply()
658
695
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
659
 
        self.get_valid_bundle(None, 'l@cset-0-1')
 
696
        bundle = self.get_valid_bundle('null:', 'l@cset-0-1')
 
697
        if getattr(bundle ,'revision_tree', None) is not None:
 
698
            # Not all bundle formats supports revision_tree
 
699
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-1')
 
700
            self.assertEqual(link_target, bund_tree.get_symlink_target(link_id))
 
701
 
660
702
        tt = TreeTransform(self.tree1)
661
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
703
        trans_id = tt.trans_id_tree_file_id(link_id)
662
704
        tt.adjust_path('link2', tt.root, trans_id)
663
705
        tt.delete_contents(trans_id)
664
 
        tt.create_symlink('mars', trans_id)
 
706
        tt.create_symlink(new_link_target, trans_id)
665
707
        tt.apply()
666
708
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
667
 
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
709
        bundle = self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
710
        if getattr(bundle ,'revision_tree', None) is not None:
 
711
            # Not all bundle formats supports revision_tree
 
712
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-2')
 
713
            self.assertEqual(new_link_target,
 
714
                             bund_tree.get_symlink_target(link_id))
 
715
 
668
716
        tt = TreeTransform(self.tree1)
669
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
717
        trans_id = tt.trans_id_tree_file_id(link_id)
670
718
        tt.delete_contents(trans_id)
671
719
        tt.create_symlink('jupiter', trans_id)
672
720
        tt.apply()
673
721
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
674
 
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
722
        bundle = self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
723
 
675
724
        tt = TreeTransform(self.tree1)
676
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
725
        trans_id = tt.trans_id_tree_file_id(link_id)
677
726
        tt.delete_contents(trans_id)
678
727
        tt.apply()
679
728
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
680
 
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
729
        bundle = self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
730
 
 
731
    def test_symlink_bundle(self):
 
732
        self._test_symlink_bundle('link', 'bar/foo', 'mars')
 
733
 
 
734
    def test_unicode_symlink_bundle(self):
 
735
        self.requireFeature(features.UnicodeFilenameFeature)
 
736
        self._test_symlink_bundle(u'\N{Euro Sign}link',
 
737
                                  u'bar/\N{Euro Sign}foo',
 
738
                                  u'mars\N{Euro Sign}')
681
739
 
682
740
    def test_binary_bundle(self):
683
741
        self.tree1 = self.make_branch_and_tree('b1')
684
742
        self.b1 = self.tree1.branch
685
743
        tt = TreeTransform(self.tree1)
686
 
        
 
744
 
687
745
        # Add
688
746
        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')
 
747
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
748
            'binary-2')
690
749
        tt.apply()
691
750
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
692
 
        self.get_valid_bundle(None, 'b@cset-0-1')
 
751
        self.get_valid_bundle('null:', 'b@cset-0-1')
693
752
 
694
753
        # Delete
695
754
        tt = TreeTransform(self.tree1)
719
778
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
720
779
 
721
780
        # Rollup
722
 
        self.get_valid_bundle(None, 'b@cset-0-4')
 
781
        self.get_valid_bundle('null:', 'b@cset-0-4')
723
782
 
724
783
    def test_last_modified(self):
725
784
        self.tree1 = self.make_branch_and_tree('b1')
743
802
        tt.create_file('file2', trans_id)
744
803
        tt.apply()
745
804
        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)
 
805
        self.tree1.merge_from_branch(other.branch)
748
806
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
749
807
                          verbose=False)
750
808
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
764
822
        bundle_file = StringIO()
765
823
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
766
824
                               'a@cset-0-1', bundle_file, format=self.format)
767
 
        self.assertNotContainsRe(bundle_file.getvalue(), 'two')
768
 
        self.assertContainsRe(bundle_file.getvalue(), 'one')
769
 
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
825
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
 
826
        self.assertContainsRe(self.get_raw(bundle_file), 'one')
 
827
        self.assertContainsRe(self.get_raw(bundle_file), 'three')
 
828
 
 
829
    def test_bundle_same_basis(self):
 
830
        """Ensure using the basis as the target doesn't cause an error"""
 
831
        self.tree1 = self.make_branch_and_tree('b1')
 
832
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
833
        bundle_file = StringIO()
 
834
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-1',
 
835
                               'a@cset-0-1', bundle_file)
 
836
 
 
837
    @staticmethod
 
838
    def get_raw(bundle_file):
 
839
        return bundle_file.getvalue()
770
840
 
771
841
    def test_unicode_bundle(self):
 
842
        self.requireFeature(features.UnicodeFilenameFeature)
772
843
        # Handle international characters
773
844
        os.mkdir('b1')
774
 
        try:
775
 
            f = open(u'b1/with Dod\xe9', 'wb')
776
 
        except UnicodeEncodeError:
777
 
            raise TestSkipped("Filesystem doesn't support unicode")
 
845
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
778
846
 
779
847
        self.tree1 = self.make_branch_and_tree('b1')
780
848
        self.b1 = self.tree1.branch
784
852
            u'William Dod\xe9\n').encode('utf-8'))
785
853
        f.close()
786
854
 
787
 
        self.tree1.add([u'with Dod\xe9'])
788
 
        self.tree1.commit(u'i18n commit from William Dod\xe9', 
 
855
        self.tree1.add([u'with Dod\N{Euro Sign}'], ['withdod-id'])
 
856
        self.tree1.commit(u'i18n commit from William Dod\xe9',
789
857
                          rev_id='i18n-1', committer=u'William Dod\xe9')
790
858
 
791
859
        # Add
792
 
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
860
        bundle = self.get_valid_bundle('null:', 'i18n-1')
793
861
 
794
862
        # Modified
795
 
        f = open(u'b1/with Dod\xe9', 'wb')
 
863
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
796
864
        f.write(u'Modified \xb5\n'.encode('utf8'))
797
865
        f.close()
798
866
        self.tree1.commit(u'modified', rev_id='i18n-2')
799
867
 
800
868
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
801
 
        
 
869
 
802
870
        # Renamed
803
 
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
871
        self.tree1.rename_one(u'with Dod\N{Euro Sign}', u'B\N{Euro Sign}gfors')
804
872
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
805
873
                          committer=u'Erik B\xe5gfors')
806
874
 
807
875
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
808
876
 
809
877
        # Removed
810
 
        self.tree1.remove([u'B\xe5gfors'])
 
878
        self.tree1.remove([u'B\N{Euro Sign}gfors'])
811
879
        self.tree1.commit(u'removed', rev_id='i18n-4')
812
880
 
813
881
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
814
882
 
815
883
        # Rollup
816
 
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
884
        bundle = self.get_valid_bundle('null:', 'i18n-4')
817
885
 
818
886
 
819
887
    def test_whitespace_bundle(self):
820
888
        if sys.platform in ('win32', 'cygwin'):
821
 
            raise TestSkipped('Windows doesn\'t support filenames'
822
 
                              ' with tabs or trailing spaces')
 
889
            raise tests.TestSkipped('Windows doesn\'t support filenames'
 
890
                                    ' with tabs or trailing spaces')
823
891
        self.tree1 = self.make_branch_and_tree('b1')
824
892
        self.b1 = self.tree1.branch
825
893
 
831
899
        # Added
832
900
        self.tree1.commit('funky whitespace', rev_id='white-1')
833
901
 
834
 
        bundle = self.get_valid_bundle(None, 'white-1')
 
902
        bundle = self.get_valid_bundle('null:', 'white-1')
835
903
 
836
904
        # Modified
837
905
        open('b1/trailing space ', 'ab').write('add some text\n')
850
918
        self.tree1.commit('removed', rev_id='white-4')
851
919
 
852
920
        bundle = self.get_valid_bundle('white-3', 'white-4')
853
 
        
 
921
 
854
922
        # Now test a complet roll-up
855
 
        bundle = self.get_valid_bundle(None, 'white-4')
 
923
        bundle = self.get_valid_bundle('null:', 'white-4')
856
924
 
857
925
    def test_alt_timezone_bundle(self):
858
926
        self.tree1 = self.make_branch_and_memory_tree('b1')
868
936
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
869
937
                          timezone=19800, timestamp=1152544886.0)
870
938
 
871
 
        bundle = self.get_valid_bundle(None, 'tz-1')
872
 
        
 
939
        bundle = self.get_valid_bundle('null:', 'tz-1')
 
940
 
873
941
        rev = bundle.revisions[0]
874
942
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
875
943
        self.assertEqual(19800, rev.timezone)
880
948
        self.tree1 = self.make_branch_and_tree('b1')
881
949
        self.b1 = self.tree1.branch
882
950
        self.tree1.commit('message', rev_id='revid1')
883
 
        bundle = self.get_valid_bundle(None, 'revid1')
884
 
        tree = bundle.revision_tree(self.b1.repository, 'revid1')
 
951
        bundle = self.get_valid_bundle('null:', 'revid1')
 
952
        tree = self.get_bundle_tree(bundle, 'revid1')
885
953
        self.assertEqual('revid1', tree.inventory.root.revision)
886
954
 
 
955
    def test_install_revisions(self):
 
956
        self.tree1 = self.make_branch_and_tree('b1')
 
957
        self.b1 = self.tree1.branch
 
958
        self.tree1.commit('message', rev_id='rev2a')
 
959
        bundle = self.get_valid_bundle('null:', 'rev2a')
 
960
        branch2 = self.make_branch('b2')
 
961
        self.assertFalse(branch2.repository.has_revision('rev2a'))
 
962
        target_revision = bundle.install_revisions(branch2.repository)
 
963
        self.assertTrue(branch2.repository.has_revision('rev2a'))
 
964
        self.assertEqual('rev2a', target_revision)
 
965
 
 
966
    def test_bundle_empty_property(self):
 
967
        """Test serializing revision properties with an empty value."""
 
968
        tree = self.make_branch_and_memory_tree('tree')
 
969
        tree.lock_write()
 
970
        self.addCleanup(tree.unlock)
 
971
        tree.add([''], ['TREE_ROOT'])
 
972
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
973
        self.b1 = tree.branch
 
974
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
975
        bundle = read_bundle(bundle_sio)
 
976
        revision_info = bundle.revisions[0]
 
977
        self.assertEqual('rev1', revision_info.revision_id)
 
978
        rev = revision_info.as_revision()
 
979
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
980
                         rev.properties)
 
981
 
 
982
    def test_bundle_sorted_properties(self):
 
983
        """For stability the writer should write properties in sorted order."""
 
984
        tree = self.make_branch_and_memory_tree('tree')
 
985
        tree.lock_write()
 
986
        self.addCleanup(tree.unlock)
 
987
 
 
988
        tree.add([''], ['TREE_ROOT'])
 
989
        tree.commit('One', rev_id='rev1',
 
990
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
991
        self.b1 = tree.branch
 
992
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
993
        bundle = read_bundle(bundle_sio)
 
994
        revision_info = bundle.revisions[0]
 
995
        self.assertEqual('rev1', revision_info.revision_id)
 
996
        rev = revision_info.as_revision()
 
997
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
998
                          'd':'1'}, rev.properties)
 
999
 
 
1000
    def test_bundle_unicode_properties(self):
 
1001
        """We should be able to round trip a non-ascii property."""
 
1002
        tree = self.make_branch_and_memory_tree('tree')
 
1003
        tree.lock_write()
 
1004
        self.addCleanup(tree.unlock)
 
1005
 
 
1006
        tree.add([''], ['TREE_ROOT'])
 
1007
        # Revisions themselves do not require anything about revision property
 
1008
        # keys, other than that they are a basestring, and do not contain
 
1009
        # whitespace.
 
1010
        # However, Testaments assert than they are str(), and thus should not
 
1011
        # be Unicode.
 
1012
        tree.commit('One', rev_id='rev1',
 
1013
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1014
        self.b1 = tree.branch
 
1015
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1016
        bundle = read_bundle(bundle_sio)
 
1017
        revision_info = bundle.revisions[0]
 
1018
        self.assertEqual('rev1', revision_info.revision_id)
 
1019
        rev = revision_info.as_revision()
 
1020
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1021
                          'alpha':u'\u03b1'}, rev.properties)
 
1022
 
 
1023
    def test_bundle_with_ghosts(self):
 
1024
        tree = self.make_branch_and_tree('tree')
 
1025
        self.b1 = tree.branch
 
1026
        self.build_tree_contents([('tree/file', 'content1')])
 
1027
        tree.add(['file'])
 
1028
        tree.commit('rev1')
 
1029
        self.build_tree_contents([('tree/file', 'content2')])
 
1030
        tree.add_parent_tree_id('ghost')
 
1031
        tree.commit('rev2', rev_id='rev2')
 
1032
        bundle = self.get_valid_bundle('null:', 'rev2')
 
1033
 
 
1034
    def make_simple_tree(self, format=None):
 
1035
        tree = self.make_branch_and_tree('b1', format=format)
 
1036
        self.b1 = tree.branch
 
1037
        self.build_tree(['b1/file'])
 
1038
        tree.add('file')
 
1039
        return tree
 
1040
 
 
1041
    def test_across_serializers(self):
 
1042
        tree = self.make_simple_tree('knit')
 
1043
        tree.commit('hello', rev_id='rev1')
 
1044
        tree.commit('hello', rev_id='rev2')
 
1045
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1046
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1047
        bundle.install_revisions(repo)
 
1048
        inv_text = repo._get_inventory_xml('rev2')
 
1049
        self.assertNotContainsRe(inv_text, 'format="5"')
 
1050
        self.assertContainsRe(inv_text, 'format="7"')
 
1051
 
 
1052
    def make_repo_with_installed_revisions(self):
 
1053
        tree = self.make_simple_tree('knit')
 
1054
        tree.commit('hello', rev_id='rev1')
 
1055
        tree.commit('hello', rev_id='rev2')
 
1056
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1057
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1058
        bundle.install_revisions(repo)
 
1059
        return repo
 
1060
 
 
1061
    def test_across_models(self):
 
1062
        repo = self.make_repo_with_installed_revisions()
 
1063
        inv = repo.get_inventory('rev2')
 
1064
        self.assertEqual('rev2', inv.root.revision)
 
1065
        root_id = inv.root.file_id
 
1066
        repo.lock_read()
 
1067
        self.addCleanup(repo.unlock)
 
1068
        self.assertEqual({(root_id, 'rev1'):(),
 
1069
            (root_id, 'rev2'):((root_id, 'rev1'),)},
 
1070
            repo.texts.get_parent_map([(root_id, 'rev1'), (root_id, 'rev2')]))
 
1071
 
 
1072
    def test_inv_hash_across_serializers(self):
 
1073
        repo = self.make_repo_with_installed_revisions()
 
1074
        recorded_inv_sha1 = repo.get_revision('rev2').inventory_sha1
 
1075
        xml = repo._get_inventory_xml('rev2')
 
1076
        self.assertEqual(osutils.sha_string(xml), recorded_inv_sha1)
 
1077
 
 
1078
    def test_across_models_incompatible(self):
 
1079
        tree = self.make_simple_tree('dirstate-with-subtree')
 
1080
        tree.commit('hello', rev_id='rev1')
 
1081
        tree.commit('hello', rev_id='rev2')
 
1082
        try:
 
1083
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1084
        except errors.IncompatibleBundleFormat:
 
1085
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1086
        repo = self.make_repository('repo', format='knit')
 
1087
        bundle.install_revisions(repo)
 
1088
 
 
1089
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1090
        self.assertRaises(errors.IncompatibleRevision,
 
1091
                          bundle.install_revisions, repo)
 
1092
 
 
1093
    def test_get_merge_request(self):
 
1094
        tree = self.make_simple_tree()
 
1095
        tree.commit('hello', rev_id='rev1')
 
1096
        tree.commit('hello', rev_id='rev2')
 
1097
        bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1098
        result = bundle.get_merge_request(tree.branch.repository)
 
1099
        self.assertEqual((None, 'rev1', 'inapplicable'), result)
 
1100
 
 
1101
    def test_with_subtree(self):
 
1102
        tree = self.make_branch_and_tree('tree',
 
1103
                                         format='dirstate-with-subtree')
 
1104
        self.b1 = tree.branch
 
1105
        subtree = self.make_branch_and_tree('tree/subtree',
 
1106
                                            format='dirstate-with-subtree')
 
1107
        tree.add('subtree')
 
1108
        tree.commit('hello', rev_id='rev1')
 
1109
        try:
 
1110
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1111
        except errors.IncompatibleBundleFormat:
 
1112
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1113
        if isinstance(bundle, v09.BundleInfo09):
 
1114
            raise tests.TestSkipped("Format 0.9 doesn't work with subtrees")
 
1115
        repo = self.make_repository('repo', format='knit')
 
1116
        self.assertRaises(errors.IncompatibleRevision,
 
1117
                          bundle.install_revisions, repo)
 
1118
        repo2 = self.make_repository('repo2', format='dirstate-with-subtree')
 
1119
        bundle.install_revisions(repo2)
 
1120
 
 
1121
    def test_revision_id_with_slash(self):
 
1122
        self.tree1 = self.make_branch_and_tree('tree')
 
1123
        self.b1 = self.tree1.branch
 
1124
        try:
 
1125
            self.tree1.commit('Revision/id/with/slashes', rev_id='rev/id')
 
1126
        except ValueError:
 
1127
            raise tests.TestSkipped(
 
1128
                "Repository doesn't support revision ids with slashes")
 
1129
        bundle = self.get_valid_bundle('null:', 'rev/id')
 
1130
 
 
1131
    def test_skip_file(self):
 
1132
        """Make sure we don't accidentally write to the wrong versionedfile"""
 
1133
        self.tree1 = self.make_branch_and_tree('tree')
 
1134
        self.b1 = self.tree1.branch
 
1135
        # rev1 is not present in bundle, done by fetch
 
1136
        self.build_tree_contents([('tree/file2', 'contents1')])
 
1137
        self.tree1.add('file2', 'file2-id')
 
1138
        self.tree1.commit('rev1', rev_id='reva')
 
1139
        self.build_tree_contents([('tree/file3', 'contents2')])
 
1140
        # rev2 is present in bundle, and done by fetch
 
1141
        # having file1 in the bunle causes file1's versionedfile to be opened.
 
1142
        self.tree1.add('file3', 'file3-id')
 
1143
        self.tree1.commit('rev2')
 
1144
        # Updating file2 should not cause an attempt to add to file1's vf
 
1145
        target = self.tree1.bzrdir.sprout('target').open_workingtree()
 
1146
        self.build_tree_contents([('tree/file2', 'contents3')])
 
1147
        self.tree1.commit('rev3', rev_id='rev3')
 
1148
        bundle = self.get_valid_bundle('reva', 'rev3')
 
1149
        if getattr(bundle, 'get_bundle_reader', None) is None:
 
1150
            raise tests.TestSkipped('Bundle format cannot provide reader')
 
1151
        # be sure that file1 comes before file2
 
1152
        for b, m, k, r, f in bundle.get_bundle_reader().iter_records():
 
1153
            if f == 'file3-id':
 
1154
                break
 
1155
            self.assertNotEqual(f, 'file2-id')
 
1156
        bundle.install_revisions(target.branch.repository)
 
1157
 
 
1158
 
 
1159
class V08BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1160
 
 
1161
    format = '0.8'
 
1162
 
 
1163
    def test_bundle_empty_property(self):
 
1164
        """Test serializing revision properties with an empty value."""
 
1165
        tree = self.make_branch_and_memory_tree('tree')
 
1166
        tree.lock_write()
 
1167
        self.addCleanup(tree.unlock)
 
1168
        tree.add([''], ['TREE_ROOT'])
 
1169
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1170
        self.b1 = tree.branch
 
1171
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1172
        self.assertContainsRe(bundle_sio.getvalue(),
 
1173
                              '# properties:\n'
 
1174
                              '#   branch-nick: tree\n'
 
1175
                              '#   empty: \n'
 
1176
                              '#   one: two\n'
 
1177
                             )
 
1178
        bundle = read_bundle(bundle_sio)
 
1179
        revision_info = bundle.revisions[0]
 
1180
        self.assertEqual('rev1', revision_info.revision_id)
 
1181
        rev = revision_info.as_revision()
 
1182
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1183
                         rev.properties)
 
1184
 
 
1185
    def get_bundle_tree(self, bundle, revision_id):
 
1186
        repository = self.make_repository('repo')
 
1187
        return bundle.revision_tree(repository, 'revid1')
 
1188
 
 
1189
    def test_bundle_empty_property_alt(self):
 
1190
        """Test serializing revision properties with an empty value.
 
1191
 
 
1192
        Older readers had a bug when reading an empty property.
 
1193
        They assumed that all keys ended in ': \n'. However they would write an
 
1194
        empty value as ':\n'. This tests make sure that all newer bzr versions
 
1195
        can handle th second form.
 
1196
        """
 
1197
        tree = self.make_branch_and_memory_tree('tree')
 
1198
        tree.lock_write()
 
1199
        self.addCleanup(tree.unlock)
 
1200
        tree.add([''], ['TREE_ROOT'])
 
1201
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1202
        self.b1 = tree.branch
 
1203
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1204
        txt = bundle_sio.getvalue()
 
1205
        loc = txt.find('#   empty: ') + len('#   empty:')
 
1206
        # Create a new bundle, which strips the trailing space after empty
 
1207
        bundle_sio = StringIO(txt[:loc] + txt[loc+1:])
 
1208
 
 
1209
        self.assertContainsRe(bundle_sio.getvalue(),
 
1210
                              '# properties:\n'
 
1211
                              '#   branch-nick: tree\n'
 
1212
                              '#   empty:\n'
 
1213
                              '#   one: two\n'
 
1214
                             )
 
1215
        bundle = read_bundle(bundle_sio)
 
1216
        revision_info = bundle.revisions[0]
 
1217
        self.assertEqual('rev1', revision_info.revision_id)
 
1218
        rev = revision_info.as_revision()
 
1219
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1220
                         rev.properties)
 
1221
 
 
1222
    def test_bundle_sorted_properties(self):
 
1223
        """For stability the writer should write properties in sorted order."""
 
1224
        tree = self.make_branch_and_memory_tree('tree')
 
1225
        tree.lock_write()
 
1226
        self.addCleanup(tree.unlock)
 
1227
 
 
1228
        tree.add([''], ['TREE_ROOT'])
 
1229
        tree.commit('One', rev_id='rev1',
 
1230
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
1231
        self.b1 = tree.branch
 
1232
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1233
        self.assertContainsRe(bundle_sio.getvalue(),
 
1234
                              '# properties:\n'
 
1235
                              '#   a: 4\n'
 
1236
                              '#   b: 3\n'
 
1237
                              '#   branch-nick: tree\n'
 
1238
                              '#   c: 2\n'
 
1239
                              '#   d: 1\n'
 
1240
                             )
 
1241
        bundle = read_bundle(bundle_sio)
 
1242
        revision_info = bundle.revisions[0]
 
1243
        self.assertEqual('rev1', revision_info.revision_id)
 
1244
        rev = revision_info.as_revision()
 
1245
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
1246
                          'd':'1'}, rev.properties)
 
1247
 
 
1248
    def test_bundle_unicode_properties(self):
 
1249
        """We should be able to round trip a non-ascii property."""
 
1250
        tree = self.make_branch_and_memory_tree('tree')
 
1251
        tree.lock_write()
 
1252
        self.addCleanup(tree.unlock)
 
1253
 
 
1254
        tree.add([''], ['TREE_ROOT'])
 
1255
        # Revisions themselves do not require anything about revision property
 
1256
        # keys, other than that they are a basestring, and do not contain
 
1257
        # whitespace.
 
1258
        # However, Testaments assert than they are str(), and thus should not
 
1259
        # be Unicode.
 
1260
        tree.commit('One', rev_id='rev1',
 
1261
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1262
        self.b1 = tree.branch
 
1263
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1264
        self.assertContainsRe(bundle_sio.getvalue(),
 
1265
                              '# properties:\n'
 
1266
                              '#   alpha: \xce\xb1\n'
 
1267
                              '#   branch-nick: tree\n'
 
1268
                              '#   omega: \xce\xa9\n'
 
1269
                             )
 
1270
        bundle = read_bundle(bundle_sio)
 
1271
        revision_info = bundle.revisions[0]
 
1272
        self.assertEqual('rev1', revision_info.revision_id)
 
1273
        rev = revision_info.as_revision()
 
1274
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1275
                          'alpha':u'\u03b1'}, rev.properties)
 
1276
 
887
1277
 
888
1278
class V09BundleKnit2Tester(V08BundleTester):
889
1279
 
891
1281
 
892
1282
    def bzrdir_format(self):
893
1283
        format = bzrdir.BzrDirMetaFormat1()
894
 
        format.repository_format = knitrepo.RepositoryFormatKnit2()
 
1284
        format.repository_format = knitrepo.RepositoryFormatKnit3()
895
1285
        return format
896
1286
 
897
1287
 
905
1295
        return format
906
1296
 
907
1297
 
908
 
class MungedBundleTester(TestCaseWithTransport):
 
1298
class V4BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1299
 
 
1300
    format = '4'
 
1301
 
 
1302
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
1303
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1304
        Make sure that the text generated is valid, and that it
 
1305
        can be applied against the base, and generate the same information.
 
1306
 
 
1307
        :return: The in-memory bundle
 
1308
        """
 
1309
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1310
 
 
1311
        # This should also validate the generated bundle
 
1312
        bundle = read_bundle(bundle_txt)
 
1313
        repository = self.b1.repository
 
1314
        for bundle_rev in bundle.real_revisions:
 
1315
            # These really should have already been checked when we read the
 
1316
            # bundle, since it computes the sha1 hash for the revision, which
 
1317
            # only will match if everything is okay, but lets be explicit about
 
1318
            # it
 
1319
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
1320
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
1321
                      'timestamp', 'timezone', 'message', 'committer',
 
1322
                      'parent_ids', 'properties'):
 
1323
                self.assertEqual(getattr(branch_rev, a),
 
1324
                                 getattr(bundle_rev, a))
 
1325
            self.assertEqual(len(branch_rev.parent_ids),
 
1326
                             len(bundle_rev.parent_ids))
 
1327
        self.assertEqual(set(rev_ids),
 
1328
                         set([r.revision_id for r in bundle.real_revisions]))
 
1329
        self.valid_apply_bundle(base_rev_id, bundle,
 
1330
                                   checkout_dir=checkout_dir)
 
1331
 
 
1332
        return bundle
 
1333
 
 
1334
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1335
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1336
        Munge the text so that it's invalid.
 
1337
 
 
1338
        :return: The in-memory bundle
 
1339
        """
 
1340
        from bzrlib.bundle import serializer
 
1341
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1342
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1343
        new_text = new_text.replace('<file file_id="exe-1"',
 
1344
                                    '<file executable="y" file_id="exe-1"')
 
1345
        new_text = new_text.replace('B260', 'B275')
 
1346
        bundle_txt = StringIO()
 
1347
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1348
        bundle_txt.write('\n')
 
1349
        bundle_txt.write(new_text.encode('bz2'))
 
1350
        bundle_txt.seek(0)
 
1351
        bundle = read_bundle(bundle_txt)
 
1352
        self.valid_apply_bundle(base_rev_id, bundle)
 
1353
        return bundle
 
1354
 
 
1355
    def create_bundle_text(self, base_rev_id, rev_id):
 
1356
        bundle_txt = StringIO()
 
1357
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
1358
                               bundle_txt, format=self.format)
 
1359
        bundle_txt.seek(0)
 
1360
        self.assertEqual(bundle_txt.readline(),
 
1361
                         '# Bazaar revision bundle v%s\n' % self.format)
 
1362
        self.assertEqual(bundle_txt.readline(), '#\n')
 
1363
        rev = self.b1.repository.get_revision(rev_id)
 
1364
        bundle_txt.seek(0)
 
1365
        return bundle_txt, rev_ids
 
1366
 
 
1367
    def get_bundle_tree(self, bundle, revision_id):
 
1368
        repository = self.make_repository('repo')
 
1369
        bundle.install_revisions(repository)
 
1370
        return repository.revision_tree(revision_id)
 
1371
 
 
1372
    def test_creation(self):
 
1373
        tree = self.make_branch_and_tree('tree')
 
1374
        self.build_tree_contents([('tree/file', 'contents1\nstatic\n')])
 
1375
        tree.add('file', 'fileid-2')
 
1376
        tree.commit('added file', rev_id='rev1')
 
1377
        self.build_tree_contents([('tree/file', 'contents2\nstatic\n')])
 
1378
        tree.commit('changed file', rev_id='rev2')
 
1379
        s = StringIO()
 
1380
        serializer = BundleSerializerV4('1.0')
 
1381
        serializer.write(tree.branch.repository, ['rev1', 'rev2'], {}, s)
 
1382
        s.seek(0)
 
1383
        tree2 = self.make_branch_and_tree('target')
 
1384
        target_repo = tree2.branch.repository
 
1385
        install_bundle(target_repo, serializer.read(s))
 
1386
        target_repo.lock_read()
 
1387
        self.addCleanup(target_repo.unlock)
 
1388
        # Turn the 'iterators_of_bytes' back into simple strings for comparison
 
1389
        repo_texts = dict((i, ''.join(content)) for i, content
 
1390
                          in target_repo.iter_files_bytes(
 
1391
                                [('fileid-2', 'rev1', '1'),
 
1392
                                 ('fileid-2', 'rev2', '2')]))
 
1393
        self.assertEqual({'1':'contents1\nstatic\n',
 
1394
                          '2':'contents2\nstatic\n'},
 
1395
                         repo_texts)
 
1396
        rtree = target_repo.revision_tree('rev2')
 
1397
        inventory_vf = target_repo.inventories
 
1398
        # If the inventory store has a graph, it must match the revision graph.
 
1399
        self.assertSubset(
 
1400
            [inventory_vf.get_parent_map([('rev2',)])[('rev2',)]],
 
1401
            [None, (('rev1',),)])
 
1402
        self.assertEqual('changed file',
 
1403
                         target_repo.get_revision('rev2').message)
 
1404
 
 
1405
    @staticmethod
 
1406
    def get_raw(bundle_file):
 
1407
        bundle_file.seek(0)
 
1408
        line = bundle_file.readline()
 
1409
        line = bundle_file.readline()
 
1410
        lines = bundle_file.readlines()
 
1411
        return ''.join(lines).decode('bz2')
 
1412
 
 
1413
    def test_copy_signatures(self):
 
1414
        tree_a = self.make_branch_and_tree('tree_a')
 
1415
        import bzrlib.gpg
 
1416
        import bzrlib.commit as commit
 
1417
        oldstrategy = bzrlib.gpg.GPGStrategy
 
1418
        branch = tree_a.branch
 
1419
        repo_a = branch.repository
 
1420
        tree_a.commit("base", allow_pointless=True, rev_id='A')
 
1421
        self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
 
1422
        try:
 
1423
            from bzrlib.testament import Testament
 
1424
            # monkey patch gpg signing mechanism
 
1425
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
1426
            new_config = test_commit.MustSignConfig(branch)
 
1427
            commit.Commit(config=new_config).commit(message="base",
 
1428
                                                    allow_pointless=True,
 
1429
                                                    rev_id='B',
 
1430
                                                    working_tree=tree_a)
 
1431
            def sign(text):
 
1432
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
1433
            self.assertTrue(repo_a.has_signature_for_revision_id('B'))
 
1434
        finally:
 
1435
            bzrlib.gpg.GPGStrategy = oldstrategy
 
1436
        tree_b = self.make_branch_and_tree('tree_b')
 
1437
        repo_b = tree_b.branch.repository
 
1438
        s = StringIO()
 
1439
        serializer = BundleSerializerV4('4')
 
1440
        serializer.write(tree_a.branch.repository, ['A', 'B'], {}, s)
 
1441
        s.seek(0)
 
1442
        install_bundle(repo_b, serializer.read(s))
 
1443
        self.assertTrue(repo_b.has_signature_for_revision_id('B'))
 
1444
        self.assertEqual(repo_b.get_signature_text('B'),
 
1445
                         repo_a.get_signature_text('B'))
 
1446
        s.seek(0)
 
1447
        # ensure repeat installs are harmless
 
1448
        install_bundle(repo_b, serializer.read(s))
 
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):
909
1646
 
910
1647
    def build_test_bundle(self):
911
1648
        wt = self.make_branch_and_tree('b1')
920
1657
 
921
1658
        bundle_txt = StringIO()
922
1659
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
923
 
                               'a@cset-0-1', bundle_txt)
924
 
        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))
925
1662
        bundle_txt.seek(0, 0)
926
1663
        return bundle_txt
927
1664
 
956
1693
        bundle = read_bundle(bundle_txt)
957
1694
        self.check_valid(bundle)
958
1695
 
 
1696
 
 
1697
class MungedBundleTesterV09(tests.TestCaseWithTransport, MungedBundleTester):
 
1698
 
 
1699
    format = '0.9'
 
1700
 
959
1701
    def test_missing_trailing_whitespace(self):
960
1702
        bundle_txt = self.build_test_bundle()
961
1703
 
989
1731
        bundle = read_bundle(bundle_txt)
990
1732
        self.check_valid(bundle)
991
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(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
        self.start_server(server)
 
1834
        url = server.get_url() + 'infinite-loop'
 
1835
        self.assertRaises(errors.NotABundle, read_mergeable_from_url, url)
 
1836
 
 
1837
    def test_smart_server_connection_reset(self):
 
1838
        """If a smart server connection fails during the attempt to read a
 
1839
        bundle, then the ConnectionReset error should be propagated.
 
1840
        """
 
1841
        # Instantiate a server that will provoke a ConnectionReset
 
1842
        sock_server = _DisconnectingTCPServer()
 
1843
        self.start_server(sock_server)
 
1844
        # We don't really care what the url is since the server will close the
 
1845
        # connection without interpreting it
 
1846
        url = sock_server.get_url()
 
1847
        self.assertRaises(errors.ConnectionReset, read_mergeable_from_url, url)
 
1848
 
 
1849
 
 
1850
class _DisconnectingTCPServer(object):
 
1851
    """A TCP server that immediately closes any connection made to it."""
 
1852
 
 
1853
    def start_server(self):
 
1854
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1855
        self.sock.bind(('127.0.0.1', 0))
 
1856
        self.sock.listen(1)
 
1857
        self.port = self.sock.getsockname()[1]
 
1858
        self.thread = threading.Thread(
 
1859
            name='%s (port %d)' % (self.__class__.__name__, self.port),
 
1860
            target=self.accept_and_close)
 
1861
        self.thread.start()
 
1862
 
 
1863
    def accept_and_close(self):
 
1864
        conn, addr = self.sock.accept()
 
1865
        conn.shutdown(socket.SHUT_RDWR)
 
1866
        conn.close()
 
1867
 
 
1868
    def get_url(self):
 
1869
        return 'bzr://127.0.0.1:%d/' % (self.port,)
 
1870
 
 
1871
    def stop_server(self):
 
1872
        try:
 
1873
            # make sure the thread dies by connecting to the listening socket,
 
1874
            # just in case the test failed to do so.
 
1875
            conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1876
            conn.connect(self.sock.getsockname())
 
1877
            conn.close()
 
1878
        except socket.error:
 
1879
            pass
 
1880
        self.sock.close()
 
1881
        self.thread.join()