~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Aaron Bentley
  • Date: 2006-09-21 20:21:36 UTC
  • mto: (2027.1.2 revert-subpath-56549)
  • mto: This revision was merged to the branch mainline in revision 2031.
  • Revision ID: abentley@panoramicfeedback.com-20060921202136-e5a8deaadfa00021
Added test for preserving file mode

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