~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-05-04 12:10:51 UTC
  • mfrom: (5819.1.4 777007-developer-doc)
  • Revision ID: pqm@pqm.ubuntu.com-20110504121051-aovlsmqiivjmc4fc
(jelmer) Small fixes to developer documentation. (Jonathan Riddell)

Show diffs side-by-side

added added

removed removed

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