/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: John Arbash Meinel
  • Date: 2009-12-22 16:28:47 UTC
  • mto: This revision was merged to the branch mainline in revision 4922.
  • Revision ID: john@arbash-meinel.com-20091222162847-tvnsc69to4l4uf5r
Implement a permute_for_extension helper.

Use it for all of the 'simple' extension permutations.
It basically permutes all tests in the current module, by setting TestCase.module.
Which works well for most of our extension tests. Some had more advanced
handling of permutations (extra permutations, custom vars, etc.)

Show diffs side-by-side

added added

removed removed

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