~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Robert Collins
  • Date: 2005-10-02 22:47:02 UTC
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20051002224701-8a8b20b90de559a6
support ghosts in commits

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
 
                           NoSuchFile,)
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 InventoryDirectory, ROOT_ID
43
 
        object.__init__(self)
44
 
        self.paths = {ROOT_ID: ""}
45
 
        self.ids = {"": ROOT_ID}
46
 
        self.contents = {}
47
 
        self.root = InventoryDirectory(ROOT_ID, '', None)
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 = new.changes_from(old)
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 NoSuchFile:
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 = tree.changes_from(self.b1.repository.revision_tree(rev_id))
426
 
            self.assertFalse(delta.has_changed(),
427
 
                             'Working tree has modifications')
428
 
        return tree
429
 
 
430
 
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
431
 
        """Get the base revision, apply the changes, and make
432
 
        sure everything matches the builtin branch.
433
 
        """
434
 
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
435
 
        repository = to_tree.branch.repository
436
 
        self.assertIs(repository.has_revision(base_rev_id), True)
437
 
        for rev in info.real_revisions:
438
 
            self.assert_(not repository.has_revision(rev.revision_id),
439
 
                'Revision {%s} present before applying bundle' 
440
 
                % rev.revision_id)
441
 
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
442
 
 
443
 
        for rev in info.real_revisions:
444
 
            self.assert_(repository.has_revision(rev.revision_id),
445
 
                'Missing revision {%s} after applying bundle' 
446
 
                % rev.revision_id)
447
 
 
448
 
        self.assert_(to_tree.branch.repository.has_revision(info.target))
449
 
        # Do we also want to verify that all the texts have been added?
450
 
 
451
 
        self.assert_(info.target in to_tree.pending_merges())
452
 
 
453
 
 
454
 
        rev = info.real_revisions[-1]
455
 
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
456
 
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
457
 
        
458
 
        # TODO: make sure the target tree is identical to base tree
459
 
        #       we might also check the working tree.
460
 
 
461
 
        base_files = list(base_tree.list_files())
462
 
        to_files = list(to_tree.list_files())
463
 
        self.assertEqual(len(base_files), len(to_files))
464
 
        for base_file, to_file in zip(base_files, to_files):
465
 
            self.assertEqual(base_file, to_file)
466
 
 
467
 
        for path, status, kind, fileid, entry in base_files:
468
 
            # Check that the meta information is the same
469
 
            self.assertEqual(base_tree.get_file_size(fileid),
470
 
                    to_tree.get_file_size(fileid))
471
 
            self.assertEqual(base_tree.get_file_sha1(fileid),
472
 
                    to_tree.get_file_sha1(fileid))
473
 
            # Check that the contents are the same
474
 
            # This is pretty expensive
475
 
            # self.assertEqual(base_tree.get_file(fileid).read(),
476
 
            #         to_tree.get_file(fileid).read())
477
 
 
478
 
    def test_bundle(self):
479
 
        self.tree1 = self.make_branch_and_tree('b1')
480
 
        self.b1 = self.tree1.branch
481
 
 
482
 
        open('b1/one', 'wb').write('one\n')
483
 
        self.tree1.add('one')
484
 
        self.tree1.commit('add one', rev_id='a@cset-0-1')
485
 
 
486
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
487
 
        # FIXME: The current write_bundle api no longer supports
488
 
        #        setting a custom summary message
489
 
        #        We should re-introduce the ability, and update
490
 
        #        the tests to make sure it works.
491
 
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
492
 
        #         message='With a specialized message')
493
 
 
494
 
        # Make sure we can handle files with spaces, tabs, other
495
 
        # bogus characters
496
 
        self.build_tree([
497
 
                'b1/with space.txt'
498
 
                , 'b1/dir/'
499
 
                , 'b1/dir/filein subdir.c'
500
 
                , 'b1/dir/WithCaps.txt'
501
 
                , 'b1/dir/ pre space'
502
 
                , 'b1/sub/'
503
 
                , 'b1/sub/sub/'
504
 
                , 'b1/sub/sub/nonempty.txt'
505
 
                ])
506
 
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
507
 
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
508
 
        tt = TreeTransform(self.tree1)
509
 
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
510
 
        tt.apply()
511
 
        self.tree1.add([
512
 
                'with space.txt'
513
 
                , 'dir'
514
 
                , 'dir/filein subdir.c'
515
 
                , 'dir/WithCaps.txt'
516
 
                , 'dir/ pre space'
517
 
                , 'dir/nolastnewline.txt'
518
 
                , 'sub'
519
 
                , 'sub/sub'
520
 
                , 'sub/sub/nonempty.txt'
521
 
                , 'sub/sub/emptyfile.txt'
522
 
                ])
523
 
        self.tree1.commit('add whitespace', rev_id='a@cset-0-2')
524
 
 
525
 
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
526
 
 
527
 
        # Check a rollup bundle 
528
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
529
 
 
530
 
        # Now delete entries
531
 
        self.tree1.remove(
532
 
                ['sub/sub/nonempty.txt'
533
 
                , 'sub/sub/emptyfile.txt'
534
 
                , 'sub/sub'
535
 
                ])
536
 
        tt = TreeTransform(self.tree1)
537
 
        trans_id = tt.trans_id_tree_file_id('exe-1')
538
 
        tt.set_executability(False, trans_id)
539
 
        tt.apply()
540
 
        self.tree1.commit('removed', rev_id='a@cset-0-3')
541
 
        
542
 
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
543
 
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
544
 
                          'a@cset-0-2', 'a@cset-0-3')
545
 
        # Check a rollup bundle 
546
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
547
 
 
548
 
        # Now move the directory
549
 
        self.tree1.rename_one('dir', 'sub/dir')
550
 
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
551
 
 
552
 
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
553
 
        # Check a rollup bundle 
554
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
555
 
 
556
 
        # Modified files
557
 
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
558
 
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
559
 
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
560
 
        self.tree1.rename_one('sub/dir/ pre space', 
561
 
                              'sub/ start space')
562
 
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
563
 
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
564
 
 
565
 
        self.tree1.rename_one('sub/dir/WithCaps.txt', 'temp')
566
 
        self.tree1.rename_one('with space.txt', 'WithCaps.txt')
567
 
        self.tree1.rename_one('temp', 'with space.txt')
568
 
        self.tree1.commit(u'swap filenames', rev_id='a@cset-0-6',
569
 
                          verbose=False)
570
 
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
571
 
        other = self.get_checkout('a@cset-0-5')
572
 
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
573
 
        other.commit('rename file', rev_id='a@cset-0-6b')
574
 
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
575
 
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
576
 
                          verbose=False)
577
 
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
578
 
 
579
 
    def test_symlink_bundle(self):
580
 
        if not has_symlinks():
581
 
            raise TestSkipped("No symlink support")
582
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
583
 
        self.b1 = self.tree1.branch
584
 
        tt = TreeTransform(self.tree1)
585
 
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
586
 
        tt.apply()
587
 
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
588
 
        self.get_valid_bundle(None, 'l@cset-0-1')
589
 
        tt = TreeTransform(self.tree1)
590
 
        trans_id = tt.trans_id_tree_file_id('link-1')
591
 
        tt.adjust_path('link2', tt.root, trans_id)
592
 
        tt.delete_contents(trans_id)
593
 
        tt.create_symlink('mars', trans_id)
594
 
        tt.apply()
595
 
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
596
 
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
597
 
        tt = TreeTransform(self.tree1)
598
 
        trans_id = tt.trans_id_tree_file_id('link-1')
599
 
        tt.delete_contents(trans_id)
600
 
        tt.create_symlink('jupiter', trans_id)
601
 
        tt.apply()
602
 
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
603
 
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
604
 
        tt = TreeTransform(self.tree1)
605
 
        trans_id = tt.trans_id_tree_file_id('link-1')
606
 
        tt.delete_contents(trans_id)
607
 
        tt.apply()
608
 
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
609
 
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
610
 
 
611
 
    def test_binary_bundle(self):
612
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
613
 
        self.b1 = self.tree1.branch
614
 
        tt = TreeTransform(self.tree1)
615
 
        
616
 
        # Add
617
 
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
618
 
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
619
 
        tt.apply()
620
 
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
621
 
        self.get_valid_bundle(None, 'b@cset-0-1')
622
 
 
623
 
        # Delete
624
 
        tt = TreeTransform(self.tree1)
625
 
        trans_id = tt.trans_id_tree_file_id('binary-1')
626
 
        tt.delete_contents(trans_id)
627
 
        tt.apply()
628
 
        self.tree1.commit('delete binary', rev_id='b@cset-0-2')
629
 
        self.get_valid_bundle('b@cset-0-1', 'b@cset-0-2')
630
 
 
631
 
        # Rename & modify
632
 
        tt = TreeTransform(self.tree1)
633
 
        trans_id = tt.trans_id_tree_file_id('binary-2')
634
 
        tt.adjust_path('file3', tt.root, trans_id)
635
 
        tt.delete_contents(trans_id)
636
 
        tt.create_file('file\rcontents\x00\n\x00', trans_id)
637
 
        tt.apply()
638
 
        self.tree1.commit('rename and modify binary', rev_id='b@cset-0-3')
639
 
        self.get_valid_bundle('b@cset-0-2', 'b@cset-0-3')
640
 
 
641
 
        # Modify
642
 
        tt = TreeTransform(self.tree1)
643
 
        trans_id = tt.trans_id_tree_file_id('binary-2')
644
 
        tt.delete_contents(trans_id)
645
 
        tt.create_file('\x00file\rcontents', trans_id)
646
 
        tt.apply()
647
 
        self.tree1.commit('just modify binary', rev_id='b@cset-0-4')
648
 
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
649
 
 
650
 
        # Rollup
651
 
        self.get_valid_bundle(None, 'b@cset-0-4')
652
 
 
653
 
    def test_last_modified(self):
654
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
655
 
        self.b1 = self.tree1.branch
656
 
        tt = TreeTransform(self.tree1)
657
 
        tt.new_file('file', tt.root, 'file', 'file')
658
 
        tt.apply()
659
 
        self.tree1.commit('create file', rev_id='a@lmod-0-1')
660
 
 
661
 
        tt = TreeTransform(self.tree1)
662
 
        trans_id = tt.trans_id_tree_file_id('file')
663
 
        tt.delete_contents(trans_id)
664
 
        tt.create_file('file2', trans_id)
665
 
        tt.apply()
666
 
        self.tree1.commit('modify text', rev_id='a@lmod-0-2a')
667
 
 
668
 
        other = self.get_checkout('a@lmod-0-1')
669
 
        tt = TreeTransform(other)
670
 
        trans_id = tt.trans_id_tree_file_id('file')
671
 
        tt.delete_contents(trans_id)
672
 
        tt.create_file('file2', trans_id)
673
 
        tt.apply()
674
 
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
675
 
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
676
 
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
677
 
                          verbose=False)
678
 
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
679
 
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
680
 
 
681
 
    def test_hide_history(self):
682
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
683
 
        self.b1 = self.tree1.branch
684
 
 
685
 
        open('b1/one', 'wb').write('one\n')
686
 
        self.tree1.add('one')
687
 
        self.tree1.commit('add file', rev_id='a@cset-0-1')
688
 
        open('b1/one', 'wb').write('two\n')
689
 
        self.tree1.commit('modify', rev_id='a@cset-0-2')
690
 
        open('b1/one', 'wb').write('three\n')
691
 
        self.tree1.commit('modify', rev_id='a@cset-0-3')
692
 
        bundle_file = StringIO()
693
 
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
694
 
                               'a@cset-0-1', bundle_file)
695
 
        self.assertNotContainsRe(bundle_file.getvalue(), 'two')
696
 
        self.assertContainsRe(bundle_file.getvalue(), 'one')
697
 
        self.assertContainsRe(bundle_file.getvalue(), 'three')
698
 
 
699
 
    def test_unicode_bundle(self):
700
 
        # Handle international characters
701
 
        os.mkdir('b1')
702
 
        try:
703
 
            f = open(u'b1/with Dod\xe9', 'wb')
704
 
        except UnicodeEncodeError:
705
 
            raise TestSkipped("Filesystem doesn't support unicode")
706
 
 
707
 
        self.tree1 = self.make_branch_and_tree('b1')
708
 
        self.b1 = self.tree1.branch
709
 
 
710
 
        f.write((u'A file\n'
711
 
            u'With international man of mystery\n'
712
 
            u'William Dod\xe9\n').encode('utf-8'))
713
 
        f.close()
714
 
 
715
 
        self.tree1.add([u'with Dod\xe9'])
716
 
        self.tree1.commit(u'i18n commit from William Dod\xe9', 
717
 
                          rev_id='i18n-1', committer=u'William Dod\xe9')
718
 
 
719
 
        # Add
720
 
        bundle = self.get_valid_bundle(None, 'i18n-1')
721
 
 
722
 
        # Modified
723
 
        f = open(u'b1/with Dod\xe9', 'wb')
724
 
        f.write(u'Modified \xb5\n'.encode('utf8'))
725
 
        f.close()
726
 
        self.tree1.commit(u'modified', rev_id='i18n-2')
727
 
 
728
 
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
729
 
        
730
 
        # Renamed
731
 
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
732
 
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
733
 
                          committer=u'Erik B\xe5gfors')
734
 
 
735
 
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
736
 
 
737
 
        # Removed
738
 
        self.tree1.remove([u'B\xe5gfors'])
739
 
        self.tree1.commit(u'removed', rev_id='i18n-4')
740
 
 
741
 
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
742
 
 
743
 
        # Rollup
744
 
        bundle = self.get_valid_bundle(None, 'i18n-4')
745
 
 
746
 
 
747
 
    def test_whitespace_bundle(self):
748
 
        if sys.platform in ('win32', 'cygwin'):
749
 
            raise TestSkipped('Windows doesn\'t support filenames'
750
 
                              ' with tabs or trailing spaces')
751
 
        self.tree1 = self.make_branch_and_tree('b1')
752
 
        self.b1 = self.tree1.branch
753
 
 
754
 
        self.build_tree(['b1/trailing space '])
755
 
        self.tree1.add(['trailing space '])
756
 
        # TODO: jam 20060701 Check for handling files with '\t' characters
757
 
        #       once we actually support them
758
 
 
759
 
        # Added
760
 
        self.tree1.commit('funky whitespace', rev_id='white-1')
761
 
 
762
 
        bundle = self.get_valid_bundle(None, 'white-1')
763
 
 
764
 
        # Modified
765
 
        open('b1/trailing space ', 'ab').write('add some text\n')
766
 
        self.tree1.commit('add text', rev_id='white-2')
767
 
 
768
 
        bundle = self.get_valid_bundle('white-1', 'white-2')
769
 
 
770
 
        # Renamed
771
 
        self.tree1.rename_one('trailing space ', ' start and end space ')
772
 
        self.tree1.commit('rename', rev_id='white-3')
773
 
 
774
 
        bundle = self.get_valid_bundle('white-2', 'white-3')
775
 
 
776
 
        # Removed
777
 
        self.tree1.remove([' start and end space '])
778
 
        self.tree1.commit('removed', rev_id='white-4')
779
 
 
780
 
        bundle = self.get_valid_bundle('white-3', 'white-4')
781
 
        
782
 
        # Now test a complet roll-up
783
 
        bundle = self.get_valid_bundle(None, 'white-4')
784
 
 
785
 
    def test_alt_timezone_bundle(self):
786
 
        self.tree1 = self.make_branch_and_tree('b1')
787
 
        self.b1 = self.tree1.branch
788
 
 
789
 
        self.build_tree(['b1/newfile'])
790
 
        self.tree1.add(['newfile'])
791
 
 
792
 
        # Asia/Colombo offset = 5 hours 30 minutes
793
 
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
794
 
                          timezone=19800, timestamp=1152544886.0)
795
 
 
796
 
        bundle = self.get_valid_bundle(None, 'tz-1')
797
 
        
798
 
        rev = bundle.revisions[0]
799
 
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
800
 
        self.assertEqual(19800, rev.timezone)
801
 
        self.assertEqual(1152544886.0, rev.timestamp)
802
 
 
803
 
    def test_bundle_root_id(self):
804
 
        self.tree1 = self.make_branch_and_tree('b1')
805
 
        self.b1 = self.tree1.branch
806
 
        self.tree1.commit('message', rev_id='revid1')
807
 
        bundle = self.get_valid_bundle(None, 'revid1')
808
 
        tree = bundle.revision_tree(self.b1.repository, 'revid1')
809
 
        self.assertEqual('revid1', tree.inventory.root.revision)
810
 
 
811
 
 
812
 
class MungedBundleTester(TestCaseWithTransport):
813
 
 
814
 
    def build_test_bundle(self):
815
 
        wt = self.make_branch_and_tree('b1')
816
 
 
817
 
        self.build_tree(['b1/one'])
818
 
        wt.add('one')
819
 
        wt.commit('add one', rev_id='a@cset-0-1')
820
 
        self.build_tree(['b1/two'])
821
 
        wt.add('two')
822
 
        wt.commit('add two', rev_id='a@cset-0-2',
823
 
                  revprops={'branch-nick':'test'})
824
 
 
825
 
        bundle_txt = StringIO()
826
 
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
827
 
                               'a@cset-0-1', bundle_txt)
828
 
        self.assertEqual(['a@cset-0-2'], rev_ids)
829
 
        bundle_txt.seek(0, 0)
830
 
        return bundle_txt
831
 
 
832
 
    def check_valid(self, bundle):
833
 
        """Check that after whatever munging, the final object is valid."""
834
 
        self.assertEqual(['a@cset-0-2'],
835
 
            [r.revision_id for r in bundle.real_revisions])
836
 
 
837
 
    def test_extra_whitespace(self):
838
 
        bundle_txt = self.build_test_bundle()
839
 
 
840
 
        # Seek to the end of the file
841
 
        # Adding one extra newline used to give us
842
 
        # TypeError: float() argument must be a string or a number
843
 
        bundle_txt.seek(0, 2)
844
 
        bundle_txt.write('\n')
845
 
        bundle_txt.seek(0)
846
 
 
847
 
        bundle = read_bundle(bundle_txt)
848
 
        self.check_valid(bundle)
849
 
 
850
 
    def test_extra_whitespace_2(self):
851
 
        bundle_txt = self.build_test_bundle()
852
 
 
853
 
        # Seek to the end of the file
854
 
        # Adding two extra newlines used to give us
855
 
        # MalformedPatches: The first line of all patches should be ...
856
 
        bundle_txt.seek(0, 2)
857
 
        bundle_txt.write('\n\n')
858
 
        bundle_txt.seek(0)
859
 
 
860
 
        bundle = read_bundle(bundle_txt)
861
 
        self.check_valid(bundle)
862
 
 
863
 
    def test_missing_trailing_whitespace(self):
864
 
        bundle_txt = self.build_test_bundle()
865
 
 
866
 
        # Remove a trailing newline, it shouldn't kill the parser
867
 
        raw = bundle_txt.getvalue()
868
 
        # The contents of the bundle don't have to be this, but this
869
 
        # test is concerned with the exact case where the serializer
870
 
        # creates a blank line at the end, and fails if that
871
 
        # line is stripped
872
 
        self.assertEqual('\n\n', raw[-2:])
873
 
        bundle_txt = StringIO(raw[:-1])
874
 
 
875
 
        bundle = read_bundle(bundle_txt)
876
 
        self.check_valid(bundle)
877
 
 
878
 
    def test_opening_text(self):
879
 
        bundle_txt = self.build_test_bundle()
880
 
 
881
 
        bundle_txt = StringIO("Some random\nemail comments\n"
882
 
                              + bundle_txt.getvalue())
883
 
 
884
 
        bundle = read_bundle(bundle_txt)
885
 
        self.check_valid(bundle)
886
 
 
887
 
    def test_trailing_text(self):
888
 
        bundle_txt = self.build_test_bundle()
889
 
 
890
 
        bundle_txt = StringIO(bundle_txt.getvalue() +
891
 
                              "Some trailing\nrandom\ntext\n")
892
 
 
893
 
        bundle = read_bundle(bundle_txt)
894
 
        self.check_valid(bundle)
895