~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Martin Pool
  • Date: 2005-08-24 08:59:32 UTC
  • Revision ID: mbp@sourcefrog.net-20050824085932-c61f1f1f1c930e13
- Add a simple UIFactory 

  The idea of this is to let a client of bzrlib set some 
  policy about how output is displayed.

  In this revision all that's done is that progress bars
  are constructed by a policy established by the application
  rather than being randomly constructed in the library 
  or passed down the calls.  This avoids progress bars
  popping up while running the test suite and cleans up
  some code.

Show diffs side-by-side

added added

removed removed

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