/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

Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004-2006 by 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
from cStringIO import StringIO
 
18
import os
 
19
import sys
 
20
import tempfile
 
21
 
 
22
from bzrlib import inventory
 
23
from bzrlib.builtins import merge
 
24
from bzrlib.bzrdir import BzrDir
 
25
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
 
26
from bzrlib.bundle.bundle_data import BundleTree
 
27
from bzrlib.bundle.serializer import write_bundle, read_bundle
 
28
from bzrlib.branch import Branch
 
29
from bzrlib.diff import internal_diff
 
30
from bzrlib.delta import compare_trees
 
31
from bzrlib.errors import BzrError, TestamentMismatch, NotABundle, BadBundle
 
32
from bzrlib.merge import Merge3Merger
 
33
from bzrlib.osutils import has_symlinks, sha_file
 
34
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
 
35
                          TestCase, TestSkipped)
 
36
from bzrlib.transform import TreeTransform
 
37
from bzrlib.workingtree import WorkingTree
 
38
 
 
39
 
 
40
class MockTree(object):
 
41
    def __init__(self):
 
42
        from bzrlib.inventory import RootEntry, ROOT_ID
 
43
        object.__init__(self)
 
44
        self.paths = {ROOT_ID: ""}
 
45
        self.ids = {"": ROOT_ID}
 
46
        self.contents = {}
 
47
        self.root = RootEntry(ROOT_ID)
 
48
 
 
49
    inventory = property(lambda x:x)
 
50
 
 
51
    def __iter__(self):
 
52
        return self.paths.iterkeys()
 
53
 
 
54
    def __getitem__(self, file_id):
 
55
        if file_id == self.root.file_id:
 
56
            return self.root
 
57
        else:
 
58
            return self.make_entry(file_id, self.paths[file_id])
 
59
 
 
60
    def parent_id(self, file_id):
 
61
        parent_dir = os.path.dirname(self.paths[file_id])
 
62
        if parent_dir == "":
 
63
            return None
 
64
        return self.ids[parent_dir]
 
65
 
 
66
    def iter_entries(self):
 
67
        for path, file_id in self.ids.iteritems():
 
68
            yield path, self[file_id]
 
69
 
 
70
    def get_file_kind(self, file_id):
 
71
        if file_id in self.contents:
 
72
            kind = 'file'
 
73
        else:
 
74
            kind = 'directory'
 
75
        return kind
 
76
 
 
77
    def make_entry(self, file_id, path):
 
78
        from bzrlib.inventory import (InventoryEntry, InventoryFile
 
79
                                    , InventoryDirectory, InventoryLink)
 
80
        name = os.path.basename(path)
 
81
        kind = self.get_file_kind(file_id)
 
82
        parent_id = self.parent_id(file_id)
 
83
        text_sha_1, text_size = self.contents_stats(file_id)
 
84
        if kind == 'directory':
 
85
            ie = InventoryDirectory(file_id, name, parent_id)
 
86
        elif kind == 'file':
 
87
            ie = InventoryFile(file_id, name, parent_id)
 
88
        elif kind == 'symlink':
 
89
            ie = InventoryLink(file_id, name, parent_id)
 
90
        else:
 
91
            raise BzrError('unknown kind %r' % kind)
 
92
        ie.text_sha1 = text_sha_1
 
93
        ie.text_size = text_size
 
94
        return ie
 
95
 
 
96
    def add_dir(self, file_id, path):
 
97
        self.paths[file_id] = path
 
98
        self.ids[path] = file_id
 
99
    
 
100
    def add_file(self, file_id, path, contents):
 
101
        self.add_dir(file_id, path)
 
102
        self.contents[file_id] = contents
 
103
 
 
104
    def path2id(self, path):
 
105
        return self.ids.get(path)
 
106
 
 
107
    def id2path(self, file_id):
 
108
        return self.paths.get(file_id)
 
109
 
 
110
    def has_id(self, file_id):
 
111
        return self.id2path(file_id) is not None
 
112
 
 
113
    def get_file(self, file_id):
 
114
        result = StringIO()
 
115
        result.write(self.contents[file_id])
 
116
        result.seek(0,0)
 
117
        return result
 
118
 
 
119
    def contents_stats(self, file_id):
 
120
        if file_id not in self.contents:
 
121
            return None, None
 
122
        text_sha1 = sha_file(self.get_file(file_id))
 
123
        return text_sha1, len(self.contents[file_id])
 
124
 
 
125
 
 
126
class BTreeTester(TestCase):
 
127
    """A simple unittest tester for the BundleTree class."""
 
128
 
 
129
    def make_tree_1(self):
 
130
        mtree = MockTree()
 
131
        mtree.add_dir("a", "grandparent")
 
132
        mtree.add_dir("b", "grandparent/parent")
 
133
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
 
134
        mtree.add_dir("d", "grandparent/alt_parent")
 
135
        return BundleTree(mtree, ''), mtree
 
136
        
 
137
    def test_renames(self):
 
138
        """Ensure that file renames have the proper effect on children"""
 
139
        btree = self.make_tree_1()[0]
 
140
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
 
141
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
142
                         "grandparent/parent")
 
143
        self.assertEqual(btree.old_path("grandparent/parent/file"),
 
144
                         "grandparent/parent/file")
 
145
 
 
146
        self.assertEqual(btree.id2path("a"), "grandparent")
 
147
        self.assertEqual(btree.id2path("b"), "grandparent/parent")
 
148
        self.assertEqual(btree.id2path("c"), "grandparent/parent/file")
 
149
 
 
150
        self.assertEqual(btree.path2id("grandparent"), "a")
 
151
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
 
152
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
 
153
 
 
154
        assert btree.path2id("grandparent2") is None
 
155
        assert btree.path2id("grandparent2/parent") is None
 
156
        assert btree.path2id("grandparent2/parent/file") is None
 
157
 
 
158
        btree.note_rename("grandparent", "grandparent2")
 
159
        assert btree.old_path("grandparent") is None
 
160
        assert btree.old_path("grandparent/parent") is None
 
161
        assert btree.old_path("grandparent/parent/file") is None
 
162
 
 
163
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
164
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
 
165
        self.assertEqual(btree.id2path("c"), "grandparent2/parent/file")
 
166
 
 
167
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
168
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
 
169
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
 
170
 
 
171
        assert btree.path2id("grandparent") is None
 
172
        assert btree.path2id("grandparent/parent") is None
 
173
        assert btree.path2id("grandparent/parent/file") is None
 
174
 
 
175
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
 
176
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
177
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
178
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file")
 
179
 
 
180
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
181
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
182
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
 
183
 
 
184
        assert btree.path2id("grandparent2/parent") is None
 
185
        assert btree.path2id("grandparent2/parent/file") is None
 
186
 
 
187
        btree.note_rename("grandparent/parent/file", 
 
188
                          "grandparent2/parent2/file2")
 
189
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
190
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
191
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file2")
 
192
 
 
193
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
194
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
195
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
 
196
 
 
197
        assert btree.path2id("grandparent2/parent2/file") is None
 
198
 
 
199
    def test_moves(self):
 
200
        """Ensure that file moves have the proper effect on children"""
 
201
        btree = self.make_tree_1()[0]
 
202
        btree.note_rename("grandparent/parent/file", 
 
203
                          "grandparent/alt_parent/file")
 
204
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
 
205
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
 
206
        assert btree.path2id("grandparent/parent/file") is None
 
207
 
 
208
    def unified_diff(self, old, new):
 
209
        out = StringIO()
 
210
        internal_diff("old", old, "new", new, out)
 
211
        out.seek(0,0)
 
212
        return out.read()
 
213
 
 
214
    def make_tree_2(self):
 
215
        btree = self.make_tree_1()[0]
 
216
        btree.note_rename("grandparent/parent/file", 
 
217
                          "grandparent/alt_parent/file")
 
218
        assert btree.id2path("e") is None
 
219
        assert btree.path2id("grandparent/parent/file") is None
 
220
        btree.note_id("e", "grandparent/parent/file")
 
221
        return btree
 
222
 
 
223
    def test_adds(self):
 
224
        """File/inventory adds"""
 
225
        btree = self.make_tree_2()
 
226
        add_patch = self.unified_diff([], ["Extra cheese\n"])
 
227
        btree.note_patch("grandparent/parent/file", add_patch)
 
228
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
229
        btree.note_target('grandparent/parent/symlink', 'venus')
 
230
        self.adds_test(btree)
 
231
 
 
232
    def adds_test(self, btree):
 
233
        self.assertEqual(btree.id2path("e"), "grandparent/parent/file")
 
234
        self.assertEqual(btree.path2id("grandparent/parent/file"), "e")
 
235
        self.assertEqual(btree.get_file("e").read(), "Extra cheese\n")
 
236
        self.assertEqual(btree.get_symlink_target('f'), 'venus')
 
237
 
 
238
    def test_adds2(self):
 
239
        """File/inventory adds, with patch-compatibile renames"""
 
240
        btree = self.make_tree_2()
 
241
        btree.contents_by_id = False
 
242
        add_patch = self.unified_diff(["Hello\n"], ["Extra cheese\n"])
 
243
        btree.note_patch("grandparent/parent/file", add_patch)
 
244
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
245
        btree.note_target('grandparent/parent/symlink', 'venus')
 
246
        self.adds_test(btree)
 
247
 
 
248
    def make_tree_3(self):
 
249
        btree, mtree = self.make_tree_1()
 
250
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
 
251
        btree.note_rename("grandparent/parent/file", 
 
252
                          "grandparent/alt_parent/file")
 
253
        btree.note_rename("grandparent/parent/topping", 
 
254
                          "grandparent/alt_parent/stopping")
 
255
        return btree
 
256
 
 
257
    def get_file_test(self, btree):
 
258
        self.assertEqual(btree.get_file("e").read(), "Lemon\n")
 
259
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
260
 
 
261
    def test_get_file(self):
 
262
        """Get file contents"""
 
263
        btree = self.make_tree_3()
 
264
        mod_patch = self.unified_diff(["Anchovies\n"], ["Lemon\n"])
 
265
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
266
        self.get_file_test(btree)
 
267
 
 
268
    def test_get_file2(self):
 
269
        """Get file contents, with patch-compatibile renames"""
 
270
        btree = self.make_tree_3()
 
271
        btree.contents_by_id = False
 
272
        mod_patch = self.unified_diff([], ["Lemon\n"])
 
273
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
274
        mod_patch = self.unified_diff([], ["Hello\n"])
 
275
        btree.note_patch("grandparent/alt_parent/file", mod_patch)
 
276
        self.get_file_test(btree)
 
277
 
 
278
    def test_delete(self):
 
279
        "Deletion by bundle"
 
280
        btree = self.make_tree_1()[0]
 
281
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
282
        btree.note_deletion("grandparent/parent/file")
 
283
        assert btree.id2path("c") is None
 
284
        assert btree.path2id("grandparent/parent/file") is None
 
285
 
 
286
    def sorted_ids(self, tree):
 
287
        ids = list(tree)
 
288
        ids.sort()
 
289
        return ids
 
290
 
 
291
    def test_iteration(self):
 
292
        """Ensure that iteration through ids works properly"""
 
293
        btree = self.make_tree_1()[0]
 
294
        self.assertEqual(self.sorted_ids(btree),
 
295
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
 
296
        btree.note_deletion("grandparent/parent/file")
 
297
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
 
298
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
299
                                "revisionidiguess")
 
300
        self.assertEqual(self.sorted_ids(btree),
 
301
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
 
302
 
 
303
 
 
304
class BundleTester(TestCaseWithTransport):
 
305
 
 
306
    def create_bundle_text(self, base_rev_id, rev_id):
 
307
        bundle_txt = StringIO()
 
308
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
309
                               bundle_txt)
 
310
        bundle_txt.seek(0)
 
311
        self.assertEqual(bundle_txt.readline(), 
 
312
                         '# Bazaar revision bundle v0.8\n')
 
313
        self.assertEqual(bundle_txt.readline(), '#\n')
 
314
 
 
315
        rev = self.b1.repository.get_revision(rev_id)
 
316
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
 
317
                         u'# message:\n')
 
318
 
 
319
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
 
320
        bundle_txt.seek(0)
 
321
        return bundle_txt, rev_ids
 
322
 
 
323
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
324
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
325
        Make sure that the text generated is valid, and that it
 
326
        can be applied against the base, and generate the same information.
 
327
        
 
328
        :return: The in-memory bundle 
 
329
        """
 
330
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
331
 
 
332
        # This should also validate the generated bundle 
 
333
        bundle = read_bundle(bundle_txt)
 
334
        repository = self.b1.repository
 
335
        for bundle_rev in bundle.real_revisions:
 
336
            # These really should have already been checked when we read the
 
337
            # bundle, since it computes the sha1 hash for the revision, which
 
338
            # only will match if everything is okay, but lets be explicit about
 
339
            # it
 
340
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
341
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
342
                      'timestamp', 'timezone', 'message', 'committer', 
 
343
                      'parent_ids', 'properties'):
 
344
                self.assertEqual(getattr(branch_rev, a), 
 
345
                                 getattr(bundle_rev, a))
 
346
            self.assertEqual(len(branch_rev.parent_ids), 
 
347
                             len(bundle_rev.parent_ids))
 
348
        self.assertEqual(rev_ids, 
 
349
                         [r.revision_id for r in bundle.real_revisions])
 
350
        self.valid_apply_bundle(base_rev_id, bundle,
 
351
                                   checkout_dir=checkout_dir)
 
352
 
 
353
        return bundle
 
354
 
 
355
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
356
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
357
        Munge the text so that it's invalid.
 
358
        
 
359
        :return: The in-memory bundle
 
360
        """
 
361
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
362
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
363
                                               'executable:yes')
 
364
        bundle_txt = StringIO(new_text)
 
365
        bundle = read_bundle(bundle_txt)
 
366
        self.valid_apply_bundle(base_rev_id, bundle)
 
367
        return bundle 
 
368
 
 
369
    def test_non_bundle(self):
 
370
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
371
 
 
372
    def test_malformed(self):
 
373
        self.assertRaises(BadBundle, read_bundle, 
 
374
                          StringIO('# Bazaar revision bundle v'))
 
375
 
 
376
    def test_crlf_bundle(self):
 
377
        try:
 
378
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
 
379
        except BadBundle:
 
380
            # It is currently permitted for bundles with crlf line endings to
 
381
            # make read_bundle raise a BadBundle, but this should be fixed.
 
382
            # Anything else, especially NotABundle, is an error.
 
383
            pass
 
384
 
 
385
    def get_checkout(self, rev_id, checkout_dir=None):
 
386
        """Get a new tree, with the specified revision in it.
 
387
        """
 
388
 
 
389
        if checkout_dir is None:
 
390
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
391
        else:
 
392
            if not os.path.exists(checkout_dir):
 
393
                os.mkdir(checkout_dir)
 
394
        tree = BzrDir.create_standalone_workingtree(checkout_dir)
 
395
        s = StringIO()
 
396
        ancestors = write_bundle(self.b1.repository, rev_id, None, s)
 
397
        s.seek(0)
 
398
        assert isinstance(s.getvalue(), str), (
 
399
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
400
        install_bundle(tree.branch.repository, read_bundle(s))
 
401
        for ancestor in ancestors:
 
402
            old = self.b1.repository.revision_tree(ancestor)
 
403
            new = tree.branch.repository.revision_tree(ancestor)
 
404
 
 
405
            # Check that there aren't any inventory level changes
 
406
            delta = compare_trees(old, new)
 
407
            self.assertFalse(delta.has_changed(),
 
408
                             'Revision %s not copied correctly.'
 
409
                             % (ancestor,))
 
410
 
 
411
            # Now check that the file contents are all correct
 
412
            for inventory_id in old:
 
413
                try:
 
414
                    old_file = old.get_file(inventory_id)
 
415
                except:
 
416
                    continue
 
417
                if old_file is None:
 
418
                    continue
 
419
                self.assertEqual(old_file.read(),
 
420
                                 new.get_file(inventory_id).read())
 
421
        if rev_id is not None:
 
422
            rh = self.b1.revision_history()
 
423
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
 
424
            tree.update()
 
425
            delta = compare_trees(self.b1.repository.revision_tree(rev_id),
 
426
                                  tree)
 
427
            self.assertFalse(delta.has_changed(),
 
428
                             'Working tree has modifications')
 
429
        return tree
 
430
 
 
431
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
 
432
        """Get the base revision, apply the changes, and make
 
433
        sure everything matches the builtin branch.
 
434
        """
 
435
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
436
        repository = to_tree.branch.repository
 
437
        self.assertIs(repository.has_revision(base_rev_id), True)
 
438
        for rev in info.real_revisions:
 
439
            self.assert_(not repository.has_revision(rev.revision_id),
 
440
                'Revision {%s} present before applying bundle' 
 
441
                % rev.revision_id)
 
442
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
443
 
 
444
        for rev in info.real_revisions:
 
445
            self.assert_(repository.has_revision(rev.revision_id),
 
446
                'Missing revision {%s} after applying bundle' 
 
447
                % rev.revision_id)
 
448
 
 
449
        self.assert_(to_tree.branch.repository.has_revision(info.target))
 
450
        # Do we also want to verify that all the texts have been added?
 
451
 
 
452
        self.assert_(info.target in to_tree.pending_merges())
 
453
 
 
454
 
 
455
        rev = info.real_revisions[-1]
 
456
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
 
457
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
 
458
        
 
459
        # TODO: make sure the target tree is identical to base tree
 
460
        #       we might also check the working tree.
 
461
 
 
462
        base_files = list(base_tree.list_files())
 
463
        to_files = list(to_tree.list_files())
 
464
        self.assertEqual(len(base_files), len(to_files))
 
465
        for base_file, to_file in zip(base_files, to_files):
 
466
            self.assertEqual(base_file, to_file)
 
467
 
 
468
        for path, status, kind, fileid, entry in base_files:
 
469
            # Check that the meta information is the same
 
470
            self.assertEqual(base_tree.get_file_size(fileid),
 
471
                    to_tree.get_file_size(fileid))
 
472
            self.assertEqual(base_tree.get_file_sha1(fileid),
 
473
                    to_tree.get_file_sha1(fileid))
 
474
            # Check that the contents are the same
 
475
            # This is pretty expensive
 
476
            # self.assertEqual(base_tree.get_file(fileid).read(),
 
477
            #         to_tree.get_file(fileid).read())
 
478
 
 
479
    def test_bundle(self):
 
480
        self.tree1 = self.make_branch_and_tree('b1')
 
481
        self.b1 = self.tree1.branch
 
482
 
 
483
        open('b1/one', 'wb').write('one\n')
 
484
        self.tree1.add('one')
 
485
        self.tree1.commit('add one', rev_id='a@cset-0-1')
 
486
 
 
487
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
 
488
        # FIXME: The current write_bundle api no longer supports
 
489
        #        setting a custom summary message
 
490
        #        We should re-introduce the ability, and update
 
491
        #        the tests to make sure it works.
 
492
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
 
493
        #         message='With a specialized message')
 
494
 
 
495
        # Make sure we can handle files with spaces, tabs, other
 
496
        # bogus characters
 
497
        self.build_tree([
 
498
                'b1/with space.txt'
 
499
                , 'b1/dir/'
 
500
                , 'b1/dir/filein subdir.c'
 
501
                , 'b1/dir/WithCaps.txt'
 
502
                , 'b1/dir/ pre space'
 
503
                , 'b1/sub/'
 
504
                , 'b1/sub/sub/'
 
505
                , 'b1/sub/sub/nonempty.txt'
 
506
                ])
 
507
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
 
508
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
509
        tt = TreeTransform(self.tree1)
 
510
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
 
511
        tt.apply()
 
512
        self.tree1.add([
 
513
                'with space.txt'
 
514
                , 'dir'
 
515
                , 'dir/filein subdir.c'
 
516
                , 'dir/WithCaps.txt'
 
517
                , 'dir/ pre space'
 
518
                , 'dir/nolastnewline.txt'
 
519
                , 'sub'
 
520
                , 'sub/sub'
 
521
                , 'sub/sub/nonempty.txt'
 
522
                , 'sub/sub/emptyfile.txt'
 
523
                ])
 
524
        self.tree1.commit('add whitespace', rev_id='a@cset-0-2')
 
525
 
 
526
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
 
527
 
 
528
        # Check a rollup bundle 
 
529
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
530
 
 
531
        # Now delete entries
 
532
        self.tree1.remove(
 
533
                ['sub/sub/nonempty.txt'
 
534
                , 'sub/sub/emptyfile.txt'
 
535
                , 'sub/sub'
 
536
                ])
 
537
        tt = TreeTransform(self.tree1)
 
538
        trans_id = tt.trans_id_tree_file_id('exe-1')
 
539
        tt.set_executability(False, trans_id)
 
540
        tt.apply()
 
541
        self.tree1.commit('removed', rev_id='a@cset-0-3')
 
542
        
 
543
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
 
544
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
 
545
                          'a@cset-0-2', 'a@cset-0-3')
 
546
        # Check a rollup bundle 
 
547
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
 
548
 
 
549
        # Now move the directory
 
550
        self.tree1.rename_one('dir', 'sub/dir')
 
551
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
 
552
 
 
553
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
 
554
        # Check a rollup bundle 
 
555
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
 
556
 
 
557
        # Modified files
 
558
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
 
559
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
560
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
 
561
        self.tree1.rename_one('sub/dir/ pre space', 
 
562
                              'sub/ start space')
 
563
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
 
564
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
 
565
 
 
566
        self.tree1.rename_one('sub/dir/WithCaps.txt', 'temp')
 
567
        self.tree1.rename_one('with space.txt', 'WithCaps.txt')
 
568
        self.tree1.rename_one('temp', 'with space.txt')
 
569
        self.tree1.commit(u'swap filenames', rev_id='a@cset-0-6',
 
570
                          verbose=False)
 
571
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
 
572
        other = self.get_checkout('a@cset-0-5')
 
573
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
 
574
        other.commit('rename file', rev_id='a@cset-0-6b')
 
575
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
 
576
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
 
577
                          verbose=False)
 
578
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
 
579
 
 
580
    def test_symlink_bundle(self):
 
581
        if not has_symlinks():
 
582
            raise TestSkipped("No symlink support")
 
583
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
584
        self.b1 = self.tree1.branch
 
585
        tt = TreeTransform(self.tree1)
 
586
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
587
        tt.apply()
 
588
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
 
589
        self.get_valid_bundle(None, 'l@cset-0-1')
 
590
        tt = TreeTransform(self.tree1)
 
591
        trans_id = tt.trans_id_tree_file_id('link-1')
 
592
        tt.adjust_path('link2', tt.root, trans_id)
 
593
        tt.delete_contents(trans_id)
 
594
        tt.create_symlink('mars', trans_id)
 
595
        tt.apply()
 
596
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
 
597
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
598
        tt = TreeTransform(self.tree1)
 
599
        trans_id = tt.trans_id_tree_file_id('link-1')
 
600
        tt.delete_contents(trans_id)
 
601
        tt.create_symlink('jupiter', trans_id)
 
602
        tt.apply()
 
603
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
 
604
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
605
        tt = TreeTransform(self.tree1)
 
606
        trans_id = tt.trans_id_tree_file_id('link-1')
 
607
        tt.delete_contents(trans_id)
 
608
        tt.apply()
 
609
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
 
610
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
611
 
 
612
    def test_binary_bundle(self):
 
613
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
614
        self.b1 = self.tree1.branch
 
615
        tt = TreeTransform(self.tree1)
 
616
        
 
617
        # Add
 
618
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
 
619
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
620
        tt.apply()
 
621
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
 
622
        self.get_valid_bundle(None, 'b@cset-0-1')
 
623
 
 
624
        # Delete
 
625
        tt = TreeTransform(self.tree1)
 
626
        trans_id = tt.trans_id_tree_file_id('binary-1')
 
627
        tt.delete_contents(trans_id)
 
628
        tt.apply()
 
629
        self.tree1.commit('delete binary', rev_id='b@cset-0-2')
 
630
        self.get_valid_bundle('b@cset-0-1', 'b@cset-0-2')
 
631
 
 
632
        # Rename & modify
 
633
        tt = TreeTransform(self.tree1)
 
634
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
635
        tt.adjust_path('file3', tt.root, trans_id)
 
636
        tt.delete_contents(trans_id)
 
637
        tt.create_file('file\rcontents\x00\n\x00', trans_id)
 
638
        tt.apply()
 
639
        self.tree1.commit('rename and modify binary', rev_id='b@cset-0-3')
 
640
        self.get_valid_bundle('b@cset-0-2', 'b@cset-0-3')
 
641
 
 
642
        # Modify
 
643
        tt = TreeTransform(self.tree1)
 
644
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
645
        tt.delete_contents(trans_id)
 
646
        tt.create_file('\x00file\rcontents', trans_id)
 
647
        tt.apply()
 
648
        self.tree1.commit('just modify binary', rev_id='b@cset-0-4')
 
649
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
 
650
 
 
651
        # Rollup
 
652
        self.get_valid_bundle(None, 'b@cset-0-4')
 
653
 
 
654
    def test_last_modified(self):
 
655
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
656
        self.b1 = self.tree1.branch
 
657
        tt = TreeTransform(self.tree1)
 
658
        tt.new_file('file', tt.root, 'file', 'file')
 
659
        tt.apply()
 
660
        self.tree1.commit('create file', rev_id='a@lmod-0-1')
 
661
 
 
662
        tt = TreeTransform(self.tree1)
 
663
        trans_id = tt.trans_id_tree_file_id('file')
 
664
        tt.delete_contents(trans_id)
 
665
        tt.create_file('file2', trans_id)
 
666
        tt.apply()
 
667
        self.tree1.commit('modify text', rev_id='a@lmod-0-2a')
 
668
 
 
669
        other = self.get_checkout('a@lmod-0-1')
 
670
        tt = TreeTransform(other)
 
671
        trans_id = tt.trans_id_tree_file_id('file')
 
672
        tt.delete_contents(trans_id)
 
673
        tt.create_file('file2', trans_id)
 
674
        tt.apply()
 
675
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
 
676
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
 
677
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
 
678
                          verbose=False)
 
679
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
 
680
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
 
681
 
 
682
    def test_hide_history(self):
 
683
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
684
        self.b1 = self.tree1.branch
 
685
 
 
686
        open('b1/one', 'wb').write('one\n')
 
687
        self.tree1.add('one')
 
688
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
689
        open('b1/one', 'wb').write('two\n')
 
690
        self.tree1.commit('modify', rev_id='a@cset-0-2')
 
691
        open('b1/one', 'wb').write('three\n')
 
692
        self.tree1.commit('modify', rev_id='a@cset-0-3')
 
693
        bundle_file = StringIO()
 
694
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
695
                               'a@cset-0-1', bundle_file)
 
696
        self.assertNotContainsRe(bundle_file.getvalue(), 'two')
 
697
        self.assertContainsRe(bundle_file.getvalue(), 'one')
 
698
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
699
 
 
700
    def test_unicode_bundle(self):
 
701
        # Handle international characters
 
702
        os.mkdir('b1')
 
703
        try:
 
704
            f = open(u'b1/with Dod\xe9', 'wb')
 
705
        except UnicodeEncodeError:
 
706
            raise TestSkipped("Filesystem doesn't support unicode")
 
707
 
 
708
        self.tree1 = self.make_branch_and_tree('b1')
 
709
        self.b1 = self.tree1.branch
 
710
 
 
711
        f.write((u'A file\n'
 
712
            u'With international man of mystery\n'
 
713
            u'William Dod\xe9\n').encode('utf-8'))
 
714
        f.close()
 
715
 
 
716
        self.tree1.add([u'with Dod\xe9'])
 
717
        self.tree1.commit(u'i18n commit from William Dod\xe9', 
 
718
                          rev_id='i18n-1', committer=u'William Dod\xe9')
 
719
 
 
720
        # Add
 
721
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
722
 
 
723
        # Modified
 
724
        f = open(u'b1/with Dod\xe9', 'wb')
 
725
        f.write(u'Modified \xb5\n'.encode('utf8'))
 
726
        f.close()
 
727
        self.tree1.commit(u'modified', rev_id='i18n-2')
 
728
 
 
729
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
 
730
        
 
731
        # Renamed
 
732
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
733
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
 
734
                          committer=u'Erik B\xe5gfors')
 
735
 
 
736
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
 
737
 
 
738
        # Removed
 
739
        self.tree1.remove([u'B\xe5gfors'])
 
740
        self.tree1.commit(u'removed', rev_id='i18n-4')
 
741
 
 
742
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
 
743
 
 
744
        # Rollup
 
745
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
746
 
 
747
 
 
748
    def test_whitespace_bundle(self):
 
749
        if sys.platform in ('win32', 'cygwin'):
 
750
            raise TestSkipped('Windows doesn\'t support filenames'
 
751
                              ' with tabs or trailing spaces')
 
752
        self.tree1 = self.make_branch_and_tree('b1')
 
753
        self.b1 = self.tree1.branch
 
754
 
 
755
        self.build_tree(['b1/trailing space '])
 
756
        self.tree1.add(['trailing space '])
 
757
        # TODO: jam 20060701 Check for handling files with '\t' characters
 
758
        #       once we actually support them
 
759
 
 
760
        # Added
 
761
        self.tree1.commit('funky whitespace', rev_id='white-1')
 
762
 
 
763
        bundle = self.get_valid_bundle(None, 'white-1')
 
764
 
 
765
        # Modified
 
766
        open('b1/trailing space ', 'ab').write('add some text\n')
 
767
        self.tree1.commit('add text', rev_id='white-2')
 
768
 
 
769
        bundle = self.get_valid_bundle('white-1', 'white-2')
 
770
 
 
771
        # Renamed
 
772
        self.tree1.rename_one('trailing space ', ' start and end space ')
 
773
        self.tree1.commit('rename', rev_id='white-3')
 
774
 
 
775
        bundle = self.get_valid_bundle('white-2', 'white-3')
 
776
 
 
777
        # Removed
 
778
        self.tree1.remove([' start and end space '])
 
779
        self.tree1.commit('removed', rev_id='white-4')
 
780
 
 
781
        bundle = self.get_valid_bundle('white-3', 'white-4')
 
782
        
 
783
        # Now test a complet roll-up
 
784
        bundle = self.get_valid_bundle(None, 'white-4')
 
785
 
 
786
    def test_alt_timezone_bundle(self):
 
787
        self.tree1 = self.make_branch_and_tree('b1')
 
788
        self.b1 = self.tree1.branch
 
789
 
 
790
        self.build_tree(['b1/newfile'])
 
791
        self.tree1.add(['newfile'])
 
792
 
 
793
        # Asia/Colombo offset = 5 hours 30 minutes
 
794
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
 
795
                          timezone=19800, timestamp=1152544886.0)
 
796
 
 
797
        bundle = self.get_valid_bundle(None, 'tz-1')
 
798
        
 
799
        rev = bundle.revisions[0]
 
800
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
 
801
        self.assertEqual(19800, rev.timezone)
 
802
        self.assertEqual(1152544886.0, rev.timestamp)
 
803
 
 
804
 
 
805
class MungedBundleTester(TestCaseWithTransport):
 
806
 
 
807
    def build_test_bundle(self):
 
808
        wt = self.make_branch_and_tree('b1')
 
809
 
 
810
        self.build_tree(['b1/one'])
 
811
        wt.add('one')
 
812
        wt.commit('add one', rev_id='a@cset-0-1')
 
813
        self.build_tree(['b1/two'])
 
814
        wt.add('two')
 
815
        wt.commit('add two', rev_id='a@cset-0-2',
 
816
                  revprops={'branch-nick':'test'})
 
817
 
 
818
        bundle_txt = StringIO()
 
819
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
 
820
                               'a@cset-0-1', bundle_txt)
 
821
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
822
        bundle_txt.seek(0, 0)
 
823
        return bundle_txt
 
824
 
 
825
    def check_valid(self, bundle):
 
826
        """Check that after whatever munging, the final object is valid."""
 
827
        self.assertEqual(['a@cset-0-2'],
 
828
            [r.revision_id for r in bundle.real_revisions])
 
829
 
 
830
    def test_extra_whitespace(self):
 
831
        bundle_txt = self.build_test_bundle()
 
832
 
 
833
        # Seek to the end of the file
 
834
        # Adding one extra newline used to give us
 
835
        # TypeError: float() argument must be a string or a number
 
836
        bundle_txt.seek(0, 2)
 
837
        bundle_txt.write('\n')
 
838
        bundle_txt.seek(0)
 
839
 
 
840
        bundle = read_bundle(bundle_txt)
 
841
        self.check_valid(bundle)
 
842
 
 
843
    def test_extra_whitespace_2(self):
 
844
        bundle_txt = self.build_test_bundle()
 
845
 
 
846
        # Seek to the end of the file
 
847
        # Adding two extra newlines used to give us
 
848
        # MalformedPatches: The first line of all patches should be ...
 
849
        bundle_txt.seek(0, 2)
 
850
        bundle_txt.write('\n\n')
 
851
        bundle_txt.seek(0)
 
852
 
 
853
        bundle = read_bundle(bundle_txt)
 
854
        self.check_valid(bundle)
 
855
 
 
856
    def test_missing_trailing_whitespace(self):
 
857
        bundle_txt = self.build_test_bundle()
 
858
 
 
859
        # Remove a trailing newline, it shouldn't kill the parser
 
860
        raw = bundle_txt.getvalue()
 
861
        # The contents of the bundle don't have to be this, but this
 
862
        # test is concerned with the exact case where the serializer
 
863
        # creates a blank line at the end, and fails if that
 
864
        # line is stripped
 
865
        self.assertEqual('\n\n', raw[-2:])
 
866
        bundle_txt = StringIO(raw[:-1])
 
867
 
 
868
        bundle = read_bundle(bundle_txt)
 
869
        self.check_valid(bundle)
 
870
 
 
871
    def test_opening_text(self):
 
872
        bundle_txt = self.build_test_bundle()
 
873
 
 
874
        bundle_txt = StringIO("Some random\nemail comments\n"
 
875
                              + bundle_txt.getvalue())
 
876
 
 
877
        bundle = read_bundle(bundle_txt)
 
878
        self.check_valid(bundle)
 
879
 
 
880
    def test_trailing_text(self):
 
881
        bundle_txt = self.build_test_bundle()
 
882
 
 
883
        bundle_txt = StringIO(bundle_txt.getvalue() +
 
884
                              "Some trailing\nrandom\ntext\n")
 
885
 
 
886
        bundle = read_bundle(bundle_txt)
 
887
        self.check_valid(bundle)
 
888