/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

Show diffs side-by-side

added added

removed removed

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