~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

Deprecate compare_trees and move its body to InterTree.changes_from.

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