/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 breezy/tests/test_transform.py

  • Committer: Jelmer Vernooij
  • Date: 2018-07-26 19:15:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7055.
  • Revision ID: jelmer@jelmer.uk-20180726191527-wniq205k6tzfo1xx
Install fastimport from git.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2012, 2016 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
import errno
 
18
import os
 
19
import sys
 
20
import time
 
21
 
 
22
from .. import (
 
23
    bencode,
 
24
    errors,
 
25
    filters,
 
26
    generate_ids,
 
27
    osutils,
 
28
    revision as _mod_revision,
 
29
    rules,
 
30
    tests,
 
31
    trace,
 
32
    transform,
 
33
    urlutils,
 
34
    )
 
35
from ..conflicts import (
 
36
    DeletingParent,
 
37
    DuplicateEntry,
 
38
    DuplicateID,
 
39
    MissingParent,
 
40
    NonDirectoryParent,
 
41
    ParentLoop,
 
42
    UnversionedParent,
 
43
)
 
44
from ..controldir import ControlDir
 
45
from ..diff import show_diff_trees
 
46
from ..errors import (
 
47
    DuplicateKey,
 
48
    ExistingLimbo,
 
49
    ExistingPendingDeletion,
 
50
    ImmortalLimbo,
 
51
    ImmortalPendingDeletion,
 
52
    LockError,
 
53
    MalformedTransform,
 
54
    ReusingTransform,
 
55
)
 
56
from ..osutils import (
 
57
    file_kind,
 
58
    pathjoin,
 
59
)
 
60
from ..merge import Merge3Merger, Merger
 
61
from ..mutabletree import MutableTree
 
62
from ..sixish import (
 
63
    BytesIO,
 
64
    text_type,
 
65
    )
 
66
from . import (
 
67
    features,
 
68
    TestCaseInTempDir,
 
69
    TestSkipped,
 
70
    )
 
71
from .features import (
 
72
    HardlinkFeature,
 
73
    SymlinkFeature,
 
74
    )
 
75
from ..transform import (
 
76
    build_tree,
 
77
    create_from_tree,
 
78
    cook_conflicts,
 
79
    _FileMover,
 
80
    FinalPaths,
 
81
    resolve_conflicts,
 
82
    resolve_checkout,
 
83
    ROOT_PARENT,
 
84
    TransformPreview,
 
85
    TreeTransform,
 
86
)
 
87
 
 
88
 
 
89
class TestTreeTransform(tests.TestCaseWithTransport):
 
90
 
 
91
    def setUp(self):
 
92
        super(TestTreeTransform, self).setUp()
 
93
        self.wt = self.make_branch_and_tree('.', format='development-subtree')
 
94
        os.chdir('..')
 
95
 
 
96
    def get_transform(self):
 
97
        transform = TreeTransform(self.wt)
 
98
        self.addCleanup(transform.finalize)
 
99
        return transform, transform.root
 
100
 
 
101
    def get_transform_for_sha1_test(self):
 
102
        trans, root = self.get_transform()
 
103
        self.wt.lock_tree_write()
 
104
        self.addCleanup(self.wt.unlock)
 
105
        contents = [b'just some content\n']
 
106
        sha1 = osutils.sha_strings(contents)
 
107
        # Roll back the clock
 
108
        trans._creation_mtime = time.time() - 20.0
 
109
        return trans, root, contents, sha1
 
110
 
 
111
    def test_existing_limbo(self):
 
112
        transform, root = self.get_transform()
 
113
        limbo_name = transform._limbodir
 
114
        deletion_path = transform._deletiondir
 
115
        os.mkdir(pathjoin(limbo_name, 'hehe'))
 
116
        self.assertRaises(ImmortalLimbo, transform.apply)
 
117
        self.assertRaises(LockError, self.wt.unlock)
 
118
        self.assertRaises(ExistingLimbo, self.get_transform)
 
119
        self.assertRaises(LockError, self.wt.unlock)
 
120
        os.rmdir(pathjoin(limbo_name, 'hehe'))
 
121
        os.rmdir(limbo_name)
 
122
        os.rmdir(deletion_path)
 
123
        transform, root = self.get_transform()
 
124
        transform.apply()
 
125
 
 
126
    def test_existing_pending_deletion(self):
 
127
        transform, root = self.get_transform()
 
128
        deletion_path = self._limbodir = urlutils.local_path_from_url(
 
129
            transform._tree._transport.abspath('pending-deletion'))
 
130
        os.mkdir(pathjoin(deletion_path, 'blocking-directory'))
 
131
        self.assertRaises(ImmortalPendingDeletion, transform.apply)
 
132
        self.assertRaises(LockError, self.wt.unlock)
 
133
        self.assertRaises(ExistingPendingDeletion, self.get_transform)
 
134
 
 
135
    def test_build(self):
 
136
        transform, root = self.get_transform()
 
137
        self.wt.lock_tree_write()
 
138
        self.addCleanup(self.wt.unlock)
 
139
        self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
 
140
        imaginary_id = transform.trans_id_tree_path('imaginary')
 
141
        imaginary_id2 = transform.trans_id_tree_path('imaginary/')
 
142
        self.assertEqual(imaginary_id, imaginary_id2)
 
143
        self.assertEqual(root, transform.get_tree_parent(imaginary_id))
 
144
        self.assertEqual('directory', transform.final_kind(root))
 
145
        self.assertEqual(self.wt.get_root_id(), transform.final_file_id(root))
 
146
        trans_id = transform.create_path('name', root)
 
147
        self.assertIs(transform.final_file_id(trans_id), None)
 
148
        self.assertIs(None, transform.final_kind(trans_id))
 
149
        transform.create_file([b'contents'], trans_id)
 
150
        transform.set_executability(True, trans_id)
 
151
        transform.version_file(b'my_pretties', trans_id)
 
152
        self.assertRaises(DuplicateKey, transform.version_file,
 
153
                          b'my_pretties', trans_id)
 
154
        self.assertEqual(transform.final_file_id(trans_id), b'my_pretties')
 
155
        self.assertEqual(transform.final_parent(trans_id), root)
 
156
        self.assertIs(transform.final_parent(root), ROOT_PARENT)
 
157
        self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
 
158
        oz_id = transform.create_path('oz', root)
 
159
        transform.create_directory(oz_id)
 
160
        transform.version_file(b'ozzie', oz_id)
 
161
        trans_id2 = transform.create_path('name2', root)
 
162
        transform.create_file([b'contents'], trans_id2)
 
163
        transform.set_executability(False, trans_id2)
 
164
        transform.version_file(b'my_pretties2', trans_id2)
 
165
        modified_paths = transform.apply().modified_paths
 
166
        with self.wt.get_file('name') as f:
 
167
            self.assertEqual(b'contents', f.read())
 
168
        self.assertEqual(self.wt.path2id('name'), b'my_pretties')
 
169
        self.assertIs(self.wt.is_executable('name'), True)
 
170
        self.assertIs(self.wt.is_executable('name2'), False)
 
171
        self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
 
172
        self.assertEqual(len(modified_paths), 3)
 
173
        tree_mod_paths = [self.wt.abspath(self.wt.id2path(f)) for f in
 
174
                          (b'ozzie', b'my_pretties', b'my_pretties2')]
 
175
        self.assertSubset(tree_mod_paths, modified_paths)
 
176
        # is it safe to finalize repeatedly?
 
177
        transform.finalize()
 
178
        transform.finalize()
 
179
 
 
180
    def test_apply_informs_tree_of_observed_sha1(self):
 
181
        trans, root, contents, sha1 = self.get_transform_for_sha1_test()
 
182
        trans_id = trans.new_file('file1', root, contents, file_id=b'file1-id',
 
183
                                  sha1=sha1)
 
184
        calls = []
 
185
        orig = self.wt._observed_sha1
 
186
        def _observed_sha1(*args):
 
187
            calls.append(args)
 
188
            orig(*args)
 
189
        self.wt._observed_sha1 = _observed_sha1
 
190
        trans.apply()
 
191
        self.assertEqual([(None, 'file1', trans._observed_sha1s[trans_id])],
 
192
                         calls)
 
193
 
 
194
    def test_create_file_caches_sha1(self):
 
195
        trans, root, contents, sha1 = self.get_transform_for_sha1_test()
 
196
        trans_id = trans.create_path('file1', root)
 
197
        trans.create_file(contents, trans_id, sha1=sha1)
 
198
        st_val = osutils.lstat(trans._limbo_name(trans_id))
 
199
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
200
        self.assertEqual(o_sha1, sha1)
 
201
        self.assertEqualStat(o_st_val, st_val)
 
202
 
 
203
    def test__apply_insertions_updates_sha1(self):
 
204
        trans, root, contents, sha1 = self.get_transform_for_sha1_test()
 
205
        trans_id = trans.create_path('file1', root)
 
206
        trans.create_file(contents, trans_id, sha1=sha1)
 
207
        st_val = osutils.lstat(trans._limbo_name(trans_id))
 
208
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
209
        self.assertEqual(o_sha1, sha1)
 
210
        self.assertEqualStat(o_st_val, st_val)
 
211
        creation_mtime = trans._creation_mtime + 10.0
 
212
        # We fake a time difference from when the file was created until now it
 
213
        # is being renamed by using os.utime. Note that the change we actually
 
214
        # want to see is the real ctime change from 'os.rename()', but as long
 
215
        # as we observe a new stat value, we should be fine.
 
216
        os.utime(trans._limbo_name(trans_id), (creation_mtime, creation_mtime))
 
217
        trans.apply()
 
218
        new_st_val = osutils.lstat(self.wt.abspath('file1'))
 
219
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
220
        self.assertEqual(o_sha1, sha1)
 
221
        self.assertEqualStat(o_st_val, new_st_val)
 
222
        self.assertNotEqual(st_val.st_mtime, new_st_val.st_mtime)
 
223
 
 
224
    def test_new_file_caches_sha1(self):
 
225
        trans, root, contents, sha1 = self.get_transform_for_sha1_test()
 
226
        trans_id = trans.new_file('file1', root, contents, file_id=b'file1-id',
 
227
                                  sha1=sha1)
 
228
        st_val = osutils.lstat(trans._limbo_name(trans_id))
 
229
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
230
        self.assertEqual(o_sha1, sha1)
 
231
        self.assertEqualStat(o_st_val, st_val)
 
232
 
 
233
    def test_cancel_creation_removes_observed_sha1(self):
 
234
        trans, root, contents, sha1 = self.get_transform_for_sha1_test()
 
235
        trans_id = trans.new_file('file1', root, contents, file_id=b'file1-id',
 
236
                                  sha1=sha1)
 
237
        self.assertTrue(trans_id in trans._observed_sha1s)
 
238
        trans.cancel_creation(trans_id)
 
239
        self.assertFalse(trans_id in trans._observed_sha1s)
 
240
 
 
241
    def test_create_files_same_timestamp(self):
 
242
        transform, root = self.get_transform()
 
243
        self.wt.lock_tree_write()
 
244
        self.addCleanup(self.wt.unlock)
 
245
        # Roll back the clock, so that we know everything is being set to the
 
246
        # exact time
 
247
        transform._creation_mtime = creation_mtime = time.time() - 20.0
 
248
        transform.create_file([b'content-one'],
 
249
                              transform.create_path('one', root))
 
250
        time.sleep(1) # *ugly*
 
251
        transform.create_file([b'content-two'],
 
252
                              transform.create_path('two', root))
 
253
        transform.apply()
 
254
        fo, st1 = self.wt.get_file_with_stat('one', filtered=False)
 
255
        fo.close()
 
256
        fo, st2 = self.wt.get_file_with_stat('two', filtered=False)
 
257
        fo.close()
 
258
        # We only guarantee 2s resolution
 
259
        self.assertTrue(abs(creation_mtime - st1.st_mtime) < 2.0,
 
260
            "%s != %s within 2 seconds" % (creation_mtime, st1.st_mtime))
 
261
        # But if we have more than that, all files should get the same result
 
262
        self.assertEqual(st1.st_mtime, st2.st_mtime)
 
263
 
 
264
    def test_change_root_id(self):
 
265
        transform, root = self.get_transform()
 
266
        self.assertNotEqual(b'new-root-id', self.wt.get_root_id())
 
267
        transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
268
        transform.delete_contents(root)
 
269
        transform.unversion_file(root)
 
270
        transform.fixup_new_roots()
 
271
        transform.apply()
 
272
        self.assertEqual(b'new-root-id', self.wt.get_root_id())
 
273
 
 
274
    def test_change_root_id_add_files(self):
 
275
        transform, root = self.get_transform()
 
276
        self.assertNotEqual(b'new-root-id', self.wt.get_root_id())
 
277
        new_trans_id = transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
278
        transform.new_file('file', new_trans_id, [b'new-contents\n'],
 
279
                           b'new-file-id')
 
280
        transform.delete_contents(root)
 
281
        transform.unversion_file(root)
 
282
        transform.fixup_new_roots()
 
283
        transform.apply()
 
284
        self.assertEqual(b'new-root-id', self.wt.get_root_id())
 
285
        self.assertEqual(b'new-file-id', self.wt.path2id('file'))
 
286
        self.assertFileEqual(b'new-contents\n', self.wt.abspath('file'))
 
287
 
 
288
    def test_add_two_roots(self):
 
289
        transform, root = self.get_transform()
 
290
        new_trans_id = transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
291
        new_trans_id = transform.new_directory('', ROOT_PARENT, b'alt-root-id')
 
292
        self.assertRaises(ValueError, transform.fixup_new_roots)
 
293
 
 
294
    def test_retain_existing_root(self):
 
295
        tt, root = self.get_transform()
 
296
        with tt:
 
297
            tt.new_directory('', ROOT_PARENT, b'new-root-id')
 
298
            tt.fixup_new_roots()
 
299
            self.assertNotEqual(b'new-root-id', tt.final_file_id(tt.root))
 
300
 
 
301
    def test_retain_existing_root_added_file(self):
 
302
        tt, root = self.get_transform()
 
303
        new_trans_id = tt.new_directory('', ROOT_PARENT, b'new-root-id')
 
304
        child = tt.new_directory('child', new_trans_id, b'child-id')
 
305
        tt.fixup_new_roots()
 
306
        self.assertEqual(tt.root, tt.final_parent(child))
 
307
 
 
308
    def test_add_unversioned_root(self):
 
309
        transform, root = self.get_transform()
 
310
        new_trans_id = transform.new_directory('', ROOT_PARENT, None)
 
311
        transform.delete_contents(transform.root)
 
312
        transform.fixup_new_roots()
 
313
        self.assertNotIn(transform.root, transform._new_id)
 
314
 
 
315
    def test_remove_root_fixup(self):
 
316
        transform, root = self.get_transform()
 
317
        old_root_id = self.wt.get_root_id()
 
318
        self.assertNotEqual(b'new-root-id', old_root_id)
 
319
        transform.delete_contents(root)
 
320
        transform.unversion_file(root)
 
321
        transform.fixup_new_roots()
 
322
        transform.apply()
 
323
        self.assertEqual(old_root_id, self.wt.get_root_id())
 
324
 
 
325
        transform, root = self.get_transform()
 
326
        new_trans_id = transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
327
        new_trans_id = transform.new_directory('', ROOT_PARENT, b'alt-root-id')
 
328
        self.assertRaises(ValueError, transform.fixup_new_roots)
 
329
 
 
330
    def test_fixup_new_roots_permits_empty_tree(self):
 
331
        transform, root = self.get_transform()
 
332
        transform.delete_contents(root)
 
333
        transform.unversion_file(root)
 
334
        transform.fixup_new_roots()
 
335
        self.assertIs(None, transform.final_kind(root))
 
336
        self.assertIs(None, transform.final_file_id(root))
 
337
 
 
338
    def test_apply_retains_root_directory(self):
 
339
        # Do not attempt to delete the physical root directory, because that
 
340
        # is impossible.
 
341
        transform, root = self.get_transform()
 
342
        with transform:
 
343
            transform.delete_contents(root)
 
344
            e = self.assertRaises(AssertionError, self.assertRaises,
 
345
                                  errors.TransformRenameFailed,
 
346
                                  transform.apply)
 
347
        self.assertContainsRe('TransformRenameFailed not raised', str(e))
 
348
 
 
349
    def test_apply_retains_file_id(self):
 
350
        transform, root = self.get_transform()
 
351
        old_root_id = transform.tree_file_id(root)
 
352
        transform.unversion_file(root)
 
353
        transform.apply()
 
354
        self.assertEqual(old_root_id, self.wt.get_root_id())
 
355
 
 
356
    def test_hardlink(self):
 
357
        self.requireFeature(HardlinkFeature)
 
358
        transform, root = self.get_transform()
 
359
        transform.new_file('file1', root, [b'contents'])
 
360
        transform.apply()
 
361
        target = self.make_branch_and_tree('target')
 
362
        target_transform = TreeTransform(target)
 
363
        trans_id = target_transform.create_path('file1', target_transform.root)
 
364
        target_transform.create_hardlink(self.wt.abspath('file1'), trans_id)
 
365
        target_transform.apply()
 
366
        self.assertPathExists('target/file1')
 
367
        source_stat = os.stat(self.wt.abspath('file1'))
 
368
        target_stat = os.stat('target/file1')
 
369
        self.assertEqual(source_stat, target_stat)
 
370
 
 
371
    def test_convenience(self):
 
372
        transform, root = self.get_transform()
 
373
        self.wt.lock_tree_write()
 
374
        self.addCleanup(self.wt.unlock)
 
375
        trans_id = transform.new_file('name', root, [b'contents'],
 
376
                                      b'my_pretties', True)
 
377
        oz = transform.new_directory('oz', root, b'oz-id')
 
378
        dorothy = transform.new_directory('dorothy', oz, b'dorothy-id')
 
379
        toto = transform.new_file('toto', dorothy, [b'toto-contents'],
 
380
                                  b'toto-id', False)
 
381
 
 
382
        self.assertEqual(len(transform.find_conflicts()), 0)
 
383
        transform.apply()
 
384
        self.assertRaises(ReusingTransform, transform.find_conflicts)
 
385
        with open(self.wt.abspath('name'), 'r') as f:
 
386
            self.assertEqual('contents', f.read())
 
387
        self.assertEqual(self.wt.path2id('name'), b'my_pretties')
 
388
        self.assertIs(self.wt.is_executable('name'), True)
 
389
        self.assertEqual(self.wt.path2id('oz'), b'oz-id')
 
390
        self.assertEqual(self.wt.path2id('oz/dorothy'), b'dorothy-id')
 
391
        self.assertEqual(self.wt.path2id('oz/dorothy/toto'), b'toto-id')
 
392
 
 
393
        self.assertEqual(b'toto-contents',
 
394
                         self.wt.get_file('oz/dorothy/toto').read())
 
395
        self.assertIs(self.wt.is_executable('oz/dorothy/toto'), False)
 
396
 
 
397
    def test_tree_reference(self):
 
398
        transform, root = self.get_transform()
 
399
        tree = transform._tree
 
400
        trans_id = transform.new_directory('reference', root, b'subtree-id')
 
401
        transform.set_tree_reference(b'subtree-revision', trans_id)
 
402
        transform.apply()
 
403
        tree.lock_read()
 
404
        self.addCleanup(tree.unlock)
 
405
        self.assertEqual(b'subtree-revision',
 
406
                         tree.root_inventory.get_entry(b'subtree-id').reference_revision)
 
407
 
 
408
    def test_conflicts(self):
 
409
        transform, root = self.get_transform()
 
410
        trans_id = transform.new_file('name', root, [b'contents'],
 
411
                                      b'my_pretties')
 
412
        self.assertEqual(len(transform.find_conflicts()), 0)
 
413
        trans_id2 = transform.new_file('name', root, [b'Crontents'], b'toto')
 
414
        self.assertEqual(transform.find_conflicts(),
 
415
                         [('duplicate', trans_id, trans_id2, 'name')])
 
416
        self.assertRaises(MalformedTransform, transform.apply)
 
417
        transform.adjust_path('name', trans_id, trans_id2)
 
418
        self.assertEqual(transform.find_conflicts(),
 
419
                         [('non-directory parent', trans_id)])
 
420
        tinman_id = transform.trans_id_tree_path('tinman')
 
421
        transform.adjust_path('name', tinman_id, trans_id2)
 
422
        self.assertEqual(transform.find_conflicts(),
 
423
                         [('unversioned parent', tinman_id),
 
424
                          ('missing parent', tinman_id)])
 
425
        lion_id = transform.create_path('lion', root)
 
426
        self.assertEqual(transform.find_conflicts(),
 
427
                         [('unversioned parent', tinman_id),
 
428
                          ('missing parent', tinman_id)])
 
429
        transform.adjust_path('name', lion_id, trans_id2)
 
430
        self.assertEqual(transform.find_conflicts(),
 
431
                         [('unversioned parent', lion_id),
 
432
                          ('missing parent', lion_id)])
 
433
        transform.version_file(b"Courage", lion_id)
 
434
        self.assertEqual(transform.find_conflicts(),
 
435
                         [('missing parent', lion_id),
 
436
                          ('versioning no contents', lion_id)])
 
437
        transform.adjust_path('name2', root, trans_id2)
 
438
        self.assertEqual(transform.find_conflicts(),
 
439
                         [('versioning no contents', lion_id)])
 
440
        transform.create_file([b'Contents, okay?'], lion_id)
 
441
        transform.adjust_path('name2', trans_id2, trans_id2)
 
442
        self.assertEqual(transform.find_conflicts(),
 
443
                         [('parent loop', trans_id2),
 
444
                          ('non-directory parent', trans_id2)])
 
445
        transform.adjust_path('name2', root, trans_id2)
 
446
        oz_id = transform.new_directory('oz', root)
 
447
        transform.set_executability(True, oz_id)
 
448
        self.assertEqual(transform.find_conflicts(),
 
449
                         [('unversioned executability', oz_id)])
 
450
        transform.version_file(b'oz-id', oz_id)
 
451
        self.assertEqual(transform.find_conflicts(),
 
452
                         [('non-file executability', oz_id)])
 
453
        transform.set_executability(None, oz_id)
 
454
        tip_id = transform.new_file('tip', oz_id, [b'ozma'], b'tip-id')
 
455
        transform.apply()
 
456
        self.assertEqual(self.wt.path2id('name'), b'my_pretties')
 
457
        with open(self.wt.abspath('name'), 'rb') as f:
 
458
            self.assertEqual(b'contents', f.read())
 
459
        transform2, root = self.get_transform()
 
460
        oz_id = transform2.trans_id_tree_path('oz')
 
461
        newtip = transform2.new_file('tip', oz_id, [b'other'], b'tip-id')
 
462
        result = transform2.find_conflicts()
 
463
        fp = FinalPaths(transform2)
 
464
        self.assertTrue('oz/tip' in transform2._tree_path_ids)
 
465
        self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
 
466
        self.assertEqual(len(result), 2)
 
467
        self.assertEqual((result[0][0], result[0][1]),
 
468
                         ('duplicate', newtip))
 
469
        self.assertEqual((result[1][0], result[1][2]),
 
470
                         ('duplicate id', newtip))
 
471
        transform2.finalize()
 
472
        transform3 = TreeTransform(self.wt)
 
473
        self.addCleanup(transform3.finalize)
 
474
        oz_id = transform3.trans_id_tree_path('oz')
 
475
        transform3.delete_contents(oz_id)
 
476
        self.assertEqual(transform3.find_conflicts(),
 
477
                         [('missing parent', oz_id)])
 
478
        root_id = transform3.root
 
479
        tip_id = transform3.trans_id_tree_path('oz/tip')
 
480
        transform3.adjust_path('tip', root_id, tip_id)
 
481
        transform3.apply()
 
482
 
 
483
    def test_conflict_on_case_insensitive(self):
 
484
        tree = self.make_branch_and_tree('tree')
 
485
        # Don't try this at home, kids!
 
486
        # Force the tree to report that it is case sensitive, for conflict
 
487
        # resolution tests
 
488
        tree.case_sensitive = True
 
489
        transform = TreeTransform(tree)
 
490
        self.addCleanup(transform.finalize)
 
491
        transform.new_file('file', transform.root, [b'content'])
 
492
        transform.new_file('FiLe', transform.root, [b'content'])
 
493
        result = transform.find_conflicts()
 
494
        self.assertEqual([], result)
 
495
        transform.finalize()
 
496
        # Force the tree to report that it is case insensitive, for conflict
 
497
        # generation tests
 
498
        tree.case_sensitive = False
 
499
        transform = TreeTransform(tree)
 
500
        self.addCleanup(transform.finalize)
 
501
        transform.new_file('file', transform.root, [b'content'])
 
502
        transform.new_file('FiLe', transform.root, [b'content'])
 
503
        result = transform.find_conflicts()
 
504
        self.assertEqual([('duplicate', 'new-1', 'new-2', 'file')], result)
 
505
 
 
506
    def test_conflict_on_case_insensitive_existing(self):
 
507
        tree = self.make_branch_and_tree('tree')
 
508
        self.build_tree(['tree/FiLe'])
 
509
        # Don't try this at home, kids!
 
510
        # Force the tree to report that it is case sensitive, for conflict
 
511
        # resolution tests
 
512
        tree.case_sensitive = True
 
513
        transform = TreeTransform(tree)
 
514
        self.addCleanup(transform.finalize)
 
515
        transform.new_file('file', transform.root, [b'content'])
 
516
        result = transform.find_conflicts()
 
517
        self.assertEqual([], result)
 
518
        transform.finalize()
 
519
        # Force the tree to report that it is case insensitive, for conflict
 
520
        # generation tests
 
521
        tree.case_sensitive = False
 
522
        transform = TreeTransform(tree)
 
523
        self.addCleanup(transform.finalize)
 
524
        transform.new_file('file', transform.root, [b'content'])
 
525
        result = transform.find_conflicts()
 
526
        self.assertEqual([('duplicate', 'new-1', 'new-2', 'file')], result)
 
527
 
 
528
    def test_resolve_case_insensitive_conflict(self):
 
529
        tree = self.make_branch_and_tree('tree')
 
530
        # Don't try this at home, kids!
 
531
        # Force the tree to report that it is case insensitive, for conflict
 
532
        # resolution tests
 
533
        tree.case_sensitive = False
 
534
        transform = TreeTransform(tree)
 
535
        self.addCleanup(transform.finalize)
 
536
        transform.new_file('file', transform.root, [b'content'])
 
537
        transform.new_file('FiLe', transform.root, [b'content'])
 
538
        resolve_conflicts(transform)
 
539
        transform.apply()
 
540
        self.assertPathExists('tree/file')
 
541
        self.assertPathExists('tree/FiLe.moved')
 
542
 
 
543
    def test_resolve_checkout_case_conflict(self):
 
544
        tree = self.make_branch_and_tree('tree')
 
545
        # Don't try this at home, kids!
 
546
        # Force the tree to report that it is case insensitive, for conflict
 
547
        # resolution tests
 
548
        tree.case_sensitive = False
 
549
        transform = TreeTransform(tree)
 
550
        self.addCleanup(transform.finalize)
 
551
        transform.new_file('file', transform.root, [b'content'])
 
552
        transform.new_file('FiLe', transform.root, [b'content'])
 
553
        resolve_conflicts(transform,
 
554
                          pass_func=lambda t, c: resolve_checkout(t, c, []))
 
555
        transform.apply()
 
556
        self.assertPathExists('tree/file')
 
557
        self.assertPathExists('tree/FiLe.moved')
 
558
 
 
559
    def test_apply_case_conflict(self):
 
560
        """Ensure that a transform with case conflicts can always be applied"""
 
561
        tree = self.make_branch_and_tree('tree')
 
562
        transform = TreeTransform(tree)
 
563
        self.addCleanup(transform.finalize)
 
564
        transform.new_file('file', transform.root, [b'content'])
 
565
        transform.new_file('FiLe', transform.root, [b'content'])
 
566
        dir = transform.new_directory('dir', transform.root)
 
567
        transform.new_file('dirfile', dir, [b'content'])
 
568
        transform.new_file('dirFiLe', dir, [b'content'])
 
569
        resolve_conflicts(transform)
 
570
        transform.apply()
 
571
        self.assertPathExists('tree/file')
 
572
        if not os.path.exists('tree/FiLe.moved'):
 
573
            self.assertPathExists('tree/FiLe')
 
574
        self.assertPathExists('tree/dir/dirfile')
 
575
        if not os.path.exists('tree/dir/dirFiLe.moved'):
 
576
            self.assertPathExists('tree/dir/dirFiLe')
 
577
 
 
578
    def test_case_insensitive_limbo(self):
 
579
        tree = self.make_branch_and_tree('tree')
 
580
        # Don't try this at home, kids!
 
581
        # Force the tree to report that it is case insensitive
 
582
        tree.case_sensitive = False
 
583
        transform = TreeTransform(tree)
 
584
        self.addCleanup(transform.finalize)
 
585
        dir = transform.new_directory('dir', transform.root)
 
586
        first = transform.new_file('file', dir, [b'content'])
 
587
        second = transform.new_file('FiLe', dir, [b'content'])
 
588
        self.assertContainsRe(transform._limbo_name(first), 'new-1/file')
 
589
        self.assertNotContainsRe(transform._limbo_name(second), 'new-1/FiLe')
 
590
 
 
591
    def test_adjust_path_updates_child_limbo_names(self):
 
592
        tree = self.make_branch_and_tree('tree')
 
593
        transform = TreeTransform(tree)
 
594
        self.addCleanup(transform.finalize)
 
595
        foo_id = transform.new_directory('foo', transform.root)
 
596
        bar_id = transform.new_directory('bar', foo_id)
 
597
        baz_id = transform.new_directory('baz', bar_id)
 
598
        qux_id = transform.new_directory('qux', baz_id)
 
599
        transform.adjust_path('quxx', foo_id, bar_id)
 
600
        self.assertStartsWith(transform._limbo_name(qux_id),
 
601
                              transform._limbo_name(bar_id))
 
602
 
 
603
    def test_add_del(self):
 
604
        start, root = self.get_transform()
 
605
        start.new_directory('a', root, b'a')
 
606
        start.apply()
 
607
        transform, root = self.get_transform()
 
608
        transform.delete_versioned(transform.trans_id_tree_path('a'))
 
609
        transform.new_directory('a', root, b'a')
 
610
        transform.apply()
 
611
 
 
612
    def test_unversioning(self):
 
613
        create_tree, root = self.get_transform()
 
614
        parent_id = create_tree.new_directory('parent', root, b'parent-id')
 
615
        create_tree.new_file('child', parent_id, [b'child'], b'child-id')
 
616
        create_tree.apply()
 
617
        unversion = TreeTransform(self.wt)
 
618
        self.addCleanup(unversion.finalize)
 
619
        parent = unversion.trans_id_tree_path('parent')
 
620
        unversion.unversion_file(parent)
 
621
        self.assertEqual(unversion.find_conflicts(),
 
622
                         [('unversioned parent', parent_id)])
 
623
        file_id = unversion.trans_id_tree_path('parent/child')
 
624
        unversion.unversion_file(file_id)
 
625
        unversion.apply()
 
626
 
 
627
    def test_name_invariants(self):
 
628
        create_tree, root = self.get_transform()
 
629
        # prepare tree
 
630
        root = create_tree.root
 
631
        create_tree.new_file('name1', root, [b'hello1'], b'name1')
 
632
        create_tree.new_file('name2', root, [b'hello2'], b'name2')
 
633
        ddir = create_tree.new_directory('dying_directory', root, b'ddir')
 
634
        create_tree.new_file('dying_file', ddir, [b'goodbye1'], b'dfile')
 
635
        create_tree.new_file('moving_file', ddir, [b'later1'], b'mfile')
 
636
        create_tree.new_file('moving_file2', root, [b'later2'], b'mfile2')
 
637
        create_tree.apply()
 
638
 
 
639
        mangle_tree, root = self.get_transform()
 
640
        root = mangle_tree.root
 
641
        #swap names
 
642
        name1 = mangle_tree.trans_id_tree_path('name1')
 
643
        name2 = mangle_tree.trans_id_tree_path('name2')
 
644
        mangle_tree.adjust_path('name2', root, name1)
 
645
        mangle_tree.adjust_path('name1', root, name2)
 
646
 
 
647
        #tests for deleting parent directories
 
648
        ddir = mangle_tree.trans_id_tree_path('dying_directory')
 
649
        mangle_tree.delete_contents(ddir)
 
650
        dfile = mangle_tree.trans_id_tree_path('dying_directory/dying_file')
 
651
        mangle_tree.delete_versioned(dfile)
 
652
        mangle_tree.unversion_file(dfile)
 
653
        mfile = mangle_tree.trans_id_tree_path('dying_directory/moving_file')
 
654
        mangle_tree.adjust_path('mfile', root, mfile)
 
655
 
 
656
        #tests for adding parent directories
 
657
        newdir = mangle_tree.new_directory('new_directory', root, b'newdir')
 
658
        mfile2 = mangle_tree.trans_id_tree_path('moving_file2')
 
659
        mangle_tree.adjust_path('mfile2', newdir, mfile2)
 
660
        mangle_tree.new_file('newfile', newdir, [b'hello3'], b'dfile')
 
661
        self.assertEqual(mangle_tree.final_file_id(mfile2), b'mfile2')
 
662
        self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
 
663
        self.assertEqual(mangle_tree.final_file_id(mfile2), b'mfile2')
 
664
        mangle_tree.apply()
 
665
        with open(self.wt.abspath('name1'), 'r') as f:
 
666
            self.assertEqual(f.read(), 'hello2')
 
667
        with open(self.wt.abspath('name2'), 'r') as f:
 
668
            self.assertEqual(f.read(), 'hello1')
 
669
        mfile2_path = self.wt.abspath(pathjoin('new_directory', 'mfile2'))
 
670
        self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
 
671
        with open(mfile2_path, 'r') as f:
 
672
            self.assertEqual(f.read(), 'later2')
 
673
        self.assertEqual(self.wt.id2path(b'mfile2'), 'new_directory/mfile2')
 
674
        self.assertEqual(self.wt.path2id('new_directory/mfile2'), b'mfile2')
 
675
        newfile_path = self.wt.abspath(pathjoin('new_directory', 'newfile'))
 
676
        with open(newfile_path, 'r') as f:
 
677
            self.assertEqual(f.read(), 'hello3')
 
678
        self.assertEqual(self.wt.path2id('dying_directory'), b'ddir')
 
679
        self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
 
680
        mfile2_path = self.wt.abspath(pathjoin('new_directory', 'mfile2'))
 
681
 
 
682
    def test_both_rename(self):
 
683
        create_tree, root = self.get_transform()
 
684
        newdir = create_tree.new_directory('selftest', root, b'selftest-id')
 
685
        create_tree.new_file('blackbox.py', newdir, [b'hello1'], b'blackbox-id')
 
686
        create_tree.apply()
 
687
        mangle_tree, root = self.get_transform()
 
688
        selftest = mangle_tree.trans_id_tree_path('selftest')
 
689
        blackbox = mangle_tree.trans_id_tree_path('selftest/blackbox.py')
 
690
        mangle_tree.adjust_path('test', root, selftest)
 
691
        mangle_tree.adjust_path('test_too_much', root, selftest)
 
692
        mangle_tree.set_executability(True, blackbox)
 
693
        mangle_tree.apply()
 
694
 
 
695
    def test_both_rename2(self):
 
696
        create_tree, root = self.get_transform()
 
697
        breezy = create_tree.new_directory('breezy', root, b'breezy-id')
 
698
        tests = create_tree.new_directory('tests', breezy, b'tests-id')
 
699
        blackbox = create_tree.new_directory('blackbox', tests, b'blackbox-id')
 
700
        create_tree.new_file('test_too_much.py', blackbox, [b'hello1'],
 
701
                             b'test_too_much-id')
 
702
        create_tree.apply()
 
703
        mangle_tree, root = self.get_transform()
 
704
        breezy = mangle_tree.trans_id_tree_path('breezy')
 
705
        tests = mangle_tree.trans_id_tree_path('breezy/tests')
 
706
        test_too_much = mangle_tree.trans_id_tree_path('breezy/tests/blackbox/test_too_much.py')
 
707
        mangle_tree.adjust_path('selftest', breezy, tests)
 
708
        mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
 
709
        mangle_tree.set_executability(True, test_too_much)
 
710
        mangle_tree.apply()
 
711
 
 
712
    def test_both_rename3(self):
 
713
        create_tree, root = self.get_transform()
 
714
        tests = create_tree.new_directory('tests', root, b'tests-id')
 
715
        create_tree.new_file('test_too_much.py', tests, [b'hello1'],
 
716
                             b'test_too_much-id')
 
717
        create_tree.apply()
 
718
        mangle_tree, root = self.get_transform()
 
719
        tests = mangle_tree.trans_id_tree_path('tests')
 
720
        test_too_much = mangle_tree.trans_id_tree_path('tests/test_too_much.py')
 
721
        mangle_tree.adjust_path('selftest', root, tests)
 
722
        mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
 
723
        mangle_tree.set_executability(True, test_too_much)
 
724
        mangle_tree.apply()
 
725
 
 
726
    def test_move_dangling_ie(self):
 
727
        create_tree, root = self.get_transform()
 
728
        # prepare tree
 
729
        root = create_tree.root
 
730
        create_tree.new_file('name1', root, [b'hello1'], b'name1')
 
731
        create_tree.apply()
 
732
        delete_contents, root = self.get_transform()
 
733
        file = delete_contents.trans_id_tree_path('name1')
 
734
        delete_contents.delete_contents(file)
 
735
        delete_contents.apply()
 
736
        move_id, root = self.get_transform()
 
737
        name1 = move_id.trans_id_tree_path('name1')
 
738
        newdir = move_id.new_directory('dir', root, b'newdir')
 
739
        move_id.adjust_path('name2', newdir, name1)
 
740
        move_id.apply()
 
741
 
 
742
    def test_replace_dangling_ie(self):
 
743
        create_tree, root = self.get_transform()
 
744
        # prepare tree
 
745
        root = create_tree.root
 
746
        create_tree.new_file('name1', root, [b'hello1'], b'name1')
 
747
        create_tree.apply()
 
748
        delete_contents = TreeTransform(self.wt)
 
749
        self.addCleanup(delete_contents.finalize)
 
750
        file = delete_contents.trans_id_tree_path('name1')
 
751
        delete_contents.delete_contents(file)
 
752
        delete_contents.apply()
 
753
        delete_contents.finalize()
 
754
        replace = TreeTransform(self.wt)
 
755
        self.addCleanup(replace.finalize)
 
756
        name2 = replace.new_file('name2', root, [b'hello2'], b'name1')
 
757
        conflicts = replace.find_conflicts()
 
758
        name1 = replace.trans_id_tree_path('name1')
 
759
        self.assertEqual(conflicts, [('duplicate id', name1, name2)])
 
760
        resolve_conflicts(replace)
 
761
        replace.apply()
 
762
 
 
763
    def _test_symlinks(self, link_name1, link_target1,
 
764
                       link_name2, link_target2):
 
765
 
 
766
        def ozpath(p): return 'oz/' + p
 
767
 
 
768
        self.requireFeature(SymlinkFeature)
 
769
        transform, root = self.get_transform()
 
770
        oz_id = transform.new_directory('oz', root, b'oz-id')
 
771
        wizard = transform.new_symlink(link_name1, oz_id, link_target1,
 
772
                                       b'wizard-id')
 
773
        wiz_id = transform.create_path(link_name2, oz_id)
 
774
        transform.create_symlink(link_target2, wiz_id)
 
775
        transform.version_file(b'wiz-id2', wiz_id)
 
776
        transform.set_executability(True, wiz_id)
 
777
        self.assertEqual(transform.find_conflicts(),
 
778
                         [('non-file executability', wiz_id)])
 
779
        transform.set_executability(None, wiz_id)
 
780
        transform.apply()
 
781
        self.assertEqual(self.wt.path2id(ozpath(link_name1)), b'wizard-id')
 
782
        self.assertEqual('symlink',
 
783
                         file_kind(self.wt.abspath(ozpath(link_name1))))
 
784
        self.assertEqual(link_target2,
 
785
                         osutils.readlink(self.wt.abspath(ozpath(link_name2))))
 
786
        self.assertEqual(link_target1,
 
787
                         osutils.readlink(self.wt.abspath(ozpath(link_name1))))
 
788
 
 
789
    def test_symlinks(self):
 
790
        self._test_symlinks('wizard', 'wizard-target',
 
791
                            'wizard2', 'behind_curtain')
 
792
 
 
793
    def test_symlinks_unicode(self):
 
794
        self.requireFeature(features.UnicodeFilenameFeature)
 
795
        self._test_symlinks(u'\N{Euro Sign}wizard',
 
796
                            u'wizard-targ\N{Euro Sign}t',
 
797
                            u'\N{Euro Sign}wizard2',
 
798
                            u'b\N{Euro Sign}hind_curtain')
 
799
 
 
800
    def test_unable_create_symlink(self):
 
801
        def tt_helper():
 
802
            wt = self.make_branch_and_tree('.')
 
803
            tt = TreeTransform(wt)  # TreeTransform obtains write lock
 
804
            try:
 
805
                tt.new_symlink('foo', tt.root, 'bar')
 
806
                tt.apply()
 
807
            finally:
 
808
                wt.unlock()
 
809
        os_symlink = getattr(os, 'symlink', None)
 
810
        os.symlink = None
 
811
        try:
 
812
            err = self.assertRaises(errors.UnableCreateSymlink, tt_helper)
 
813
            self.assertEqual(
 
814
                "Unable to create symlink 'foo' on this platform",
 
815
                str(err))
 
816
        finally:
 
817
            if os_symlink:
 
818
                os.symlink = os_symlink
 
819
 
 
820
    def get_conflicted(self):
 
821
        create, root = self.get_transform()
 
822
        create.new_file('dorothy', root, [b'dorothy'], b'dorothy-id')
 
823
        oz = create.new_directory('oz', root, b'oz-id')
 
824
        create.new_directory('emeraldcity', oz, b'emerald-id')
 
825
        create.apply()
 
826
        conflicts, root = self.get_transform()
 
827
        # set up duplicate entry, duplicate id
 
828
        new_dorothy = conflicts.new_file('dorothy', root, [b'dorothy'],
 
829
                                         b'dorothy-id')
 
830
        old_dorothy = conflicts.trans_id_tree_path('dorothy')
 
831
        oz = conflicts.trans_id_tree_path('oz')
 
832
        # set up DeletedParent parent conflict
 
833
        conflicts.delete_versioned(oz)
 
834
        emerald = conflicts.trans_id_tree_path('oz/emeraldcity')
 
835
        # set up MissingParent conflict
 
836
        munchkincity = conflicts.trans_id_file_id(b'munchkincity-id')
 
837
        conflicts.adjust_path('munchkincity', root, munchkincity)
 
838
        conflicts.new_directory('auntem', munchkincity, b'auntem-id')
 
839
        # set up parent loop
 
840
        conflicts.adjust_path('emeraldcity', emerald, emerald)
 
841
        return conflicts, emerald, oz, old_dorothy, new_dorothy
 
842
 
 
843
    def test_conflict_resolution(self):
 
844
        conflicts, emerald, oz, old_dorothy, new_dorothy =\
 
845
            self.get_conflicted()
 
846
        resolve_conflicts(conflicts)
 
847
        self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
 
848
        self.assertIs(conflicts.final_file_id(old_dorothy), None)
 
849
        self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
 
850
        self.assertEqual(conflicts.final_file_id(new_dorothy), b'dorothy-id')
 
851
        self.assertEqual(conflicts.final_parent(emerald), oz)
 
852
        conflicts.apply()
 
853
 
 
854
    def test_cook_conflicts(self):
 
855
        tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
 
856
        raw_conflicts = resolve_conflicts(tt)
 
857
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
858
        duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved',
 
859
                                   'dorothy', None, b'dorothy-id')
 
860
        self.assertEqual(cooked_conflicts[0], duplicate)
 
861
        duplicate_id = DuplicateID('Unversioned existing file',
 
862
                                   'dorothy.moved', 'dorothy', None,
 
863
                                   b'dorothy-id')
 
864
        self.assertEqual(cooked_conflicts[1], duplicate_id)
 
865
        missing_parent = MissingParent('Created directory', 'munchkincity',
 
866
                                       b'munchkincity-id')
 
867
        deleted_parent = DeletingParent('Not deleting', 'oz', b'oz-id')
 
868
        self.assertEqual(cooked_conflicts[2], missing_parent)
 
869
        unversioned_parent = UnversionedParent('Versioned directory',
 
870
                                               'munchkincity',
 
871
                                               b'munchkincity-id')
 
872
        unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
 
873
                                               b'oz-id')
 
874
        self.assertEqual(cooked_conflicts[3], unversioned_parent)
 
875
        parent_loop = ParentLoop('Cancelled move', 'oz/emeraldcity',
 
876
                                 'oz/emeraldcity', b'emerald-id', b'emerald-id')
 
877
        self.assertEqual(cooked_conflicts[4], deleted_parent)
 
878
        self.assertEqual(cooked_conflicts[5], unversioned_parent2)
 
879
        self.assertEqual(cooked_conflicts[6], parent_loop)
 
880
        self.assertEqual(len(cooked_conflicts), 7)
 
881
        tt.finalize()
 
882
 
 
883
    def test_string_conflicts(self):
 
884
        tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
 
885
        raw_conflicts = resolve_conflicts(tt)
 
886
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
887
        tt.finalize()
 
888
        conflicts_s = [text_type(c) for c in cooked_conflicts]
 
889
        self.assertEqual(len(cooked_conflicts), len(conflicts_s))
 
890
        self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy.  '
 
891
                                         'Moved existing file to '
 
892
                                         'dorothy.moved.')
 
893
        self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy.  '
 
894
                                         'Unversioned existing file '
 
895
                                         'dorothy.moved.')
 
896
        self.assertEqual(conflicts_s[2], 'Conflict adding files to'
 
897
                                         ' munchkincity.  Created directory.')
 
898
        self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
 
899
                                         ' versioned, but has versioned'
 
900
                                         ' children.  Versioned directory.')
 
901
        self.assertEqualDiff(conflicts_s[4], "Conflict: can't delete oz because it"
 
902
                                         " is not empty.  Not deleting.")
 
903
        self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
 
904
                                         ' versioned, but has versioned'
 
905
                                         ' children.  Versioned directory.')
 
906
        self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
 
907
                                         ' oz/emeraldcity. Cancelled move.')
 
908
 
 
909
    def prepare_wrong_parent_kind(self):
 
910
        tt, root = self.get_transform()
 
911
        tt.new_file('parent', root, [b'contents'], b'parent-id')
 
912
        tt.apply()
 
913
        tt, root = self.get_transform()
 
914
        parent_id = tt.trans_id_file_id(b'parent-id')
 
915
        tt.new_file('child,', parent_id, [b'contents2'], b'file-id')
 
916
        return tt
 
917
 
 
918
    def test_find_conflicts_wrong_parent_kind(self):
 
919
        tt = self.prepare_wrong_parent_kind()
 
920
        tt.find_conflicts()
 
921
 
 
922
    def test_resolve_conflicts_wrong_existing_parent_kind(self):
 
923
        tt = self.prepare_wrong_parent_kind()
 
924
        raw_conflicts = resolve_conflicts(tt)
 
925
        self.assertEqual({('non-directory parent', 'Created directory',
 
926
                         'new-3')}, raw_conflicts)
 
927
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
928
        self.assertEqual([NonDirectoryParent('Created directory', 'parent.new',
 
929
        b'parent-id')], cooked_conflicts)
 
930
        tt.apply()
 
931
        self.assertFalse(self.wt.is_versioned('parent'))
 
932
        self.assertEqual(b'parent-id', self.wt.path2id('parent.new'))
 
933
 
 
934
    def test_resolve_conflicts_wrong_new_parent_kind(self):
 
935
        tt, root = self.get_transform()
 
936
        parent_id = tt.new_directory('parent', root, b'parent-id')
 
937
        tt.new_file('child,', parent_id, [b'contents2'], b'file-id')
 
938
        tt.apply()
 
939
        tt, root = self.get_transform()
 
940
        parent_id = tt.trans_id_file_id(b'parent-id')
 
941
        tt.delete_contents(parent_id)
 
942
        tt.create_file([b'contents'], parent_id)
 
943
        raw_conflicts = resolve_conflicts(tt)
 
944
        self.assertEqual({('non-directory parent', 'Created directory',
 
945
                         'new-3')}, raw_conflicts)
 
946
        tt.apply()
 
947
        self.assertFalse(self.wt.is_versioned('parent'))
 
948
        self.assertEqual(b'parent-id', self.wt.path2id('parent.new'))
 
949
 
 
950
    def test_resolve_conflicts_wrong_parent_kind_unversioned(self):
 
951
        tt, root = self.get_transform()
 
952
        parent_id = tt.new_directory('parent', root)
 
953
        tt.new_file('child,', parent_id, [b'contents2'])
 
954
        tt.apply()
 
955
        tt, root = self.get_transform()
 
956
        parent_id = tt.trans_id_tree_path('parent')
 
957
        tt.delete_contents(parent_id)
 
958
        tt.create_file([b'contents'], parent_id)
 
959
        resolve_conflicts(tt)
 
960
        tt.apply()
 
961
        self.assertFalse(self.wt.is_versioned('parent'))
 
962
        self.assertFalse(self.wt.is_versioned('parent.new'))
 
963
 
 
964
    def test_resolve_conflicts_missing_parent(self):
 
965
        wt = self.make_branch_and_tree('.')
 
966
        tt = TreeTransform(wt)
 
967
        self.addCleanup(tt.finalize)
 
968
        parent = tt.trans_id_file_id(b'parent-id')
 
969
        tt.new_file('file', parent, [b'Contents'])
 
970
        raw_conflicts = resolve_conflicts(tt)
 
971
        # Since the directory doesn't exist it's seen as 'missing'.  So
 
972
        # 'resolve_conflicts' create a conflict asking for it to be created.
 
973
        self.assertLength(1, raw_conflicts)
 
974
        self.assertEqual(('missing parent', 'Created directory', 'new-1'),
 
975
                         raw_conflicts.pop())
 
976
        # apply fail since the missing directory doesn't exist
 
977
        self.assertRaises(errors.NoFinalPath, tt.apply)
 
978
 
 
979
    def test_moving_versioned_directories(self):
 
980
        create, root = self.get_transform()
 
981
        kansas = create.new_directory('kansas', root, b'kansas-id')
 
982
        create.new_directory('house', kansas, b'house-id')
 
983
        create.new_directory('oz', root, b'oz-id')
 
984
        create.apply()
 
985
        cyclone, root = self.get_transform()
 
986
        oz = cyclone.trans_id_tree_path('oz')
 
987
        house = cyclone.trans_id_tree_path('house')
 
988
        cyclone.adjust_path('house', oz, house)
 
989
        cyclone.apply()
 
990
 
 
991
    def test_moving_root(self):
 
992
        create, root = self.get_transform()
 
993
        fun = create.new_directory('fun', root, b'fun-id')
 
994
        create.new_directory('sun', root, b'sun-id')
 
995
        create.new_directory('moon', root, b'moon')
 
996
        create.apply()
 
997
        transform, root = self.get_transform()
 
998
        transform.adjust_root_path('oldroot', fun)
 
999
        new_root = transform.trans_id_tree_path('')
 
1000
        transform.version_file(b'new-root', new_root)
 
1001
        transform.apply()
 
1002
 
 
1003
    def test_renames(self):
 
1004
        create, root = self.get_transform()
 
1005
        old = create.new_directory('old-parent', root, b'old-id')
 
1006
        intermediate = create.new_directory('intermediate', old, b'im-id')
 
1007
        myfile = create.new_file('myfile', intermediate, [b'myfile-text'],
 
1008
                                 b'myfile-id')
 
1009
        create.apply()
 
1010
        rename, root = self.get_transform()
 
1011
        old = rename.trans_id_file_id(b'old-id')
 
1012
        rename.adjust_path('new', root, old)
 
1013
        myfile = rename.trans_id_file_id(b'myfile-id')
 
1014
        rename.set_executability(True, myfile)
 
1015
        rename.apply()
 
1016
 
 
1017
    def test_rename_fails(self):
 
1018
        self.requireFeature(features.not_running_as_root)
 
1019
        # see https://bugs.launchpad.net/bzr/+bug/491763
 
1020
        create, root_id = self.get_transform()
 
1021
        first_dir = create.new_directory('first-dir', root_id, b'first-id')
 
1022
        myfile = create.new_file('myfile', root_id, [b'myfile-text'],
 
1023
                                 b'myfile-id')
 
1024
        create.apply()
 
1025
        if os.name == "posix" and sys.platform != "cygwin":
 
1026
            # posix filesystems fail on renaming if the readonly bit is set
 
1027
            osutils.make_readonly(self.wt.abspath('first-dir'))
 
1028
        elif os.name == "nt":
 
1029
            # windows filesystems fail on renaming open files
 
1030
            self.addCleanup(open(self.wt.abspath('myfile')).close)
 
1031
        else:
 
1032
            self.skipTest("Can't force a permissions error on rename")
 
1033
        # now transform to rename
 
1034
        rename_transform, root_id = self.get_transform()
 
1035
        file_trans_id = rename_transform.trans_id_file_id(b'myfile-id')
 
1036
        dir_id = rename_transform.trans_id_file_id(b'first-id')
 
1037
        rename_transform.adjust_path('newname', dir_id, file_trans_id)
 
1038
        e = self.assertRaises(errors.TransformRenameFailed,
 
1039
            rename_transform.apply)
 
1040
        # On nix looks like: 
 
1041
        # "Failed to rename .../work/.bzr/checkout/limbo/new-1
 
1042
        # to .../first-dir/newname: [Errno 13] Permission denied"
 
1043
        # On windows looks like:
 
1044
        # "Failed to rename .../work/myfile to 
 
1045
        # .../work/.bzr/checkout/limbo/new-1: [Errno 13] Permission denied"
 
1046
        # This test isn't concerned with exactly what the error looks like,
 
1047
        # and the strerror will vary across OS and locales, but the assert
 
1048
        # that the exeception attributes are what we expect
 
1049
        self.assertEqual(e.errno, errno.EACCES)
 
1050
        if os.name == "posix":
 
1051
            self.assertEndsWith(e.to_path, "/first-dir/newname")
 
1052
        else:
 
1053
            self.assertEqual(os.path.basename(e.from_path), "myfile")
 
1054
 
 
1055
    def test_set_executability_order(self):
 
1056
        """Ensure that executability behaves the same, no matter what order.
 
1057
 
 
1058
        - create file and set executability simultaneously
 
1059
        - create file and set executability afterward
 
1060
        - unsetting the executability of a file whose executability has not been
 
1061
        declared should throw an exception (this may happen when a
 
1062
        merge attempts to create a file with a duplicate ID)
 
1063
        """
 
1064
        transform, root = self.get_transform()
 
1065
        wt = transform._tree
 
1066
        wt.lock_read()
 
1067
        self.addCleanup(wt.unlock)
 
1068
        transform.new_file('set_on_creation', root, [b'Set on creation'], b'soc',
 
1069
                           True)
 
1070
        sac = transform.new_file('set_after_creation', root,
 
1071
                                 [b'Set after creation'], b'sac')
 
1072
        transform.set_executability(True, sac)
 
1073
        uws = transform.new_file('unset_without_set', root, [b'Unset badly'],
 
1074
                                 b'uws')
 
1075
        self.assertRaises(KeyError, transform.set_executability, None, uws)
 
1076
        transform.apply()
 
1077
        self.assertTrue(wt.is_executable('set_on_creation'))
 
1078
        self.assertTrue(wt.is_executable('set_after_creation'))
 
1079
 
 
1080
    def test_preserve_mode(self):
 
1081
        """File mode is preserved when replacing content"""
 
1082
        if sys.platform == 'win32':
 
1083
            raise TestSkipped('chmod has no effect on win32')
 
1084
        transform, root = self.get_transform()
 
1085
        transform.new_file('file1', root, [b'contents'], b'file1-id', True)
 
1086
        transform.apply()
 
1087
        self.wt.lock_write()
 
1088
        self.addCleanup(self.wt.unlock)
 
1089
        self.assertTrue(self.wt.is_executable('file1'))
 
1090
        transform, root = self.get_transform()
 
1091
        file1_id = transform.trans_id_tree_path('file1')
 
1092
        transform.delete_contents(file1_id)
 
1093
        transform.create_file([b'contents2'], file1_id)
 
1094
        transform.apply()
 
1095
        self.assertTrue(self.wt.is_executable('file1'))
 
1096
 
 
1097
    def test__set_mode_stats_correctly(self):
 
1098
        """_set_mode stats to determine file mode."""
 
1099
        if sys.platform == 'win32':
 
1100
            raise TestSkipped('chmod has no effect on win32')
 
1101
 
 
1102
        stat_paths = []
 
1103
        real_stat = os.stat
 
1104
        def instrumented_stat(path):
 
1105
            stat_paths.append(path)
 
1106
            return real_stat(path)
 
1107
 
 
1108
        transform, root = self.get_transform()
 
1109
 
 
1110
        bar1_id = transform.new_file('bar', root, [b'bar contents 1\n'],
 
1111
                                     file_id=b'bar-id-1', executable=False)
 
1112
        transform.apply()
 
1113
 
 
1114
        transform, root = self.get_transform()
 
1115
        bar1_id = transform.trans_id_tree_path('bar')
 
1116
        bar2_id = transform.trans_id_tree_path('bar2')
 
1117
        try:
 
1118
            os.stat = instrumented_stat
 
1119
            transform.create_file([b'bar2 contents\n'], bar2_id, mode_id=bar1_id)
 
1120
        finally:
 
1121
            os.stat = real_stat
 
1122
            transform.finalize()
 
1123
 
 
1124
        bar1_abspath = self.wt.abspath('bar')
 
1125
        self.assertEqual([bar1_abspath], stat_paths)
 
1126
 
 
1127
    def test_iter_changes(self):
 
1128
        self.wt.set_root_id(b'eert_toor')
 
1129
        transform, root = self.get_transform()
 
1130
        transform.new_file('old', root, [b'blah'], b'id-1', True)
 
1131
        transform.apply()
 
1132
        transform, root = self.get_transform()
 
1133
        try:
 
1134
            self.assertEqual([], list(transform.iter_changes()))
 
1135
            old = transform.trans_id_tree_path('old')
 
1136
            transform.unversion_file(old)
 
1137
            self.assertEqual([(b'id-1', ('old', None), False, (True, False),
 
1138
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', 'file'),
 
1139
                (True, True))], list(transform.iter_changes()))
 
1140
            transform.new_directory('new', root, b'id-1')
 
1141
            self.assertEqual([(b'id-1', ('old', 'new'), True, (True, True),
 
1142
                (b'eert_toor', b'eert_toor'), ('old', 'new'),
 
1143
                ('file', 'directory'),
 
1144
                (True, False))], list(transform.iter_changes()))
 
1145
        finally:
 
1146
            transform.finalize()
 
1147
 
 
1148
    def test_iter_changes_new(self):
 
1149
        self.wt.set_root_id(b'eert_toor')
 
1150
        transform, root = self.get_transform()
 
1151
        transform.new_file('old', root, [b'blah'])
 
1152
        transform.apply()
 
1153
        transform, root = self.get_transform()
 
1154
        try:
 
1155
            old = transform.trans_id_tree_path('old')
 
1156
            transform.version_file(b'id-1', old)
 
1157
            self.assertEqual([(b'id-1', (None, 'old'), False, (False, True),
 
1158
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', 'file'),
 
1159
                (False, False))], list(transform.iter_changes()))
 
1160
        finally:
 
1161
            transform.finalize()
 
1162
 
 
1163
    def test_iter_changes_modifications(self):
 
1164
        self.wt.set_root_id(b'eert_toor')
 
1165
        transform, root = self.get_transform()
 
1166
        transform.new_file('old', root, [b'blah'], b'id-1')
 
1167
        transform.new_file('new', root, [b'blah'])
 
1168
        transform.new_directory('subdir', root, b'subdir-id')
 
1169
        transform.apply()
 
1170
        transform, root = self.get_transform()
 
1171
        try:
 
1172
            old = transform.trans_id_tree_path('old')
 
1173
            subdir = transform.trans_id_tree_path('subdir')
 
1174
            new = transform.trans_id_tree_path('new')
 
1175
            self.assertEqual([], list(transform.iter_changes()))
 
1176
 
 
1177
            #content deletion
 
1178
            transform.delete_contents(old)
 
1179
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1180
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', None),
 
1181
                (False, False))], list(transform.iter_changes()))
 
1182
 
 
1183
            #content change
 
1184
            transform.create_file([b'blah'], old)
 
1185
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1186
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', 'file'),
 
1187
                (False, False))], list(transform.iter_changes()))
 
1188
            transform.cancel_deletion(old)
 
1189
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1190
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', 'file'),
 
1191
                (False, False))], list(transform.iter_changes()))
 
1192
            transform.cancel_creation(old)
 
1193
 
 
1194
            # move file_id to a different file
 
1195
            self.assertEqual([], list(transform.iter_changes()))
 
1196
            transform.unversion_file(old)
 
1197
            transform.version_file(b'id-1', new)
 
1198
            transform.adjust_path('old', root, new)
 
1199
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1200
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', 'file'),
 
1201
                (False, False))], list(transform.iter_changes()))
 
1202
            transform.cancel_versioning(new)
 
1203
            transform._removed_id = set()
 
1204
 
 
1205
            #execute bit
 
1206
            self.assertEqual([], list(transform.iter_changes()))
 
1207
            transform.set_executability(True, old)
 
1208
            self.assertEqual([(b'id-1', ('old', 'old'), False, (True, True),
 
1209
                (b'eert_toor', b'eert_toor'), ('old', 'old'), ('file', 'file'),
 
1210
                (False, True))], list(transform.iter_changes()))
 
1211
            transform.set_executability(None, old)
 
1212
 
 
1213
            # filename
 
1214
            self.assertEqual([], list(transform.iter_changes()))
 
1215
            transform.adjust_path('new', root, old)
 
1216
            transform._new_parent = {}
 
1217
            self.assertEqual([(b'id-1', ('old', 'new'), False, (True, True),
 
1218
                (b'eert_toor', b'eert_toor'), ('old', 'new'), ('file', 'file'),
 
1219
                (False, False))], list(transform.iter_changes()))
 
1220
            transform._new_name = {}
 
1221
 
 
1222
            # parent directory
 
1223
            self.assertEqual([], list(transform.iter_changes()))
 
1224
            transform.adjust_path('new', subdir, old)
 
1225
            transform._new_name = {}
 
1226
            self.assertEqual([(b'id-1', ('old', 'subdir/old'), False,
 
1227
                (True, True), (b'eert_toor', b'subdir-id'), ('old', 'old'),
 
1228
                ('file', 'file'), (False, False))],
 
1229
                list(transform.iter_changes()))
 
1230
            transform._new_path = {}
 
1231
 
 
1232
        finally:
 
1233
            transform.finalize()
 
1234
 
 
1235
    def test_iter_changes_modified_bleed(self):
 
1236
        self.wt.set_root_id(b'eert_toor')
 
1237
        """Modified flag should not bleed from one change to another"""
 
1238
        # unfortunately, we have no guarantee that file1 (which is modified)
 
1239
        # will be applied before file2.  And if it's applied after file2, it
 
1240
        # obviously can't bleed into file2's change output.  But for now, it
 
1241
        # works.
 
1242
        transform, root = self.get_transform()
 
1243
        transform.new_file('file1', root, [b'blah'], b'id-1')
 
1244
        transform.new_file('file2', root, [b'blah'], b'id-2')
 
1245
        transform.apply()
 
1246
        transform, root = self.get_transform()
 
1247
        try:
 
1248
            transform.delete_contents(transform.trans_id_file_id(b'id-1'))
 
1249
            transform.set_executability(True,
 
1250
            transform.trans_id_file_id(b'id-2'))
 
1251
            self.assertEqual([(b'id-1', (u'file1', u'file1'), True, (True, True),
 
1252
                (b'eert_toor', b'eert_toor'), ('file1', u'file1'),
 
1253
                ('file', None), (False, False)),
 
1254
                (b'id-2', (u'file2', u'file2'), False, (True, True),
 
1255
                (b'eert_toor', b'eert_toor'), ('file2', u'file2'),
 
1256
                ('file', 'file'), (False, True))],
 
1257
                list(transform.iter_changes()))
 
1258
        finally:
 
1259
            transform.finalize()
 
1260
 
 
1261
    def test_iter_changes_move_missing(self):
 
1262
        """Test moving ids with no files around"""
 
1263
        self.wt.set_root_id(b'toor_eert')
 
1264
        # Need two steps because versioning a non-existant file is a conflict.
 
1265
        transform, root = self.get_transform()
 
1266
        transform.new_directory('floater', root, b'floater-id')
 
1267
        transform.apply()
 
1268
        transform, root = self.get_transform()
 
1269
        transform.delete_contents(transform.trans_id_tree_path('floater'))
 
1270
        transform.apply()
 
1271
        transform, root = self.get_transform()
 
1272
        floater = transform.trans_id_tree_path('floater')
 
1273
        try:
 
1274
            transform.adjust_path('flitter', root, floater)
 
1275
            self.assertEqual([(b'floater-id', ('floater', 'flitter'), False,
 
1276
            (True, True), (b'toor_eert', b'toor_eert'), ('floater', 'flitter'),
 
1277
            (None, None), (False, False))], list(transform.iter_changes()))
 
1278
        finally:
 
1279
            transform.finalize()
 
1280
 
 
1281
    def test_iter_changes_pointless(self):
 
1282
        """Ensure that no-ops are not treated as modifications"""
 
1283
        self.wt.set_root_id(b'eert_toor')
 
1284
        transform, root = self.get_transform()
 
1285
        transform.new_file('old', root, [b'blah'], b'id-1')
 
1286
        transform.new_directory('subdir', root, b'subdir-id')
 
1287
        transform.apply()
 
1288
        transform, root = self.get_transform()
 
1289
        try:
 
1290
            old = transform.trans_id_tree_path('old')
 
1291
            subdir = transform.trans_id_tree_path('subdir')
 
1292
            self.assertEqual([], list(transform.iter_changes()))
 
1293
            transform.delete_contents(subdir)
 
1294
            transform.create_directory(subdir)
 
1295
            transform.set_executability(False, old)
 
1296
            transform.unversion_file(old)
 
1297
            transform.version_file(b'id-1', old)
 
1298
            transform.adjust_path('old', root, old)
 
1299
            self.assertEqual([], list(transform.iter_changes()))
 
1300
        finally:
 
1301
            transform.finalize()
 
1302
 
 
1303
    def test_rename_count(self):
 
1304
        transform, root = self.get_transform()
 
1305
        transform.new_file('name1', root, [b'contents'])
 
1306
        self.assertEqual(transform.rename_count, 0)
 
1307
        transform.apply()
 
1308
        self.assertEqual(transform.rename_count, 1)
 
1309
        transform2, root = self.get_transform()
 
1310
        transform2.adjust_path('name2', root,
 
1311
                               transform2.trans_id_tree_path('name1'))
 
1312
        self.assertEqual(transform2.rename_count, 0)
 
1313
        transform2.apply()
 
1314
        self.assertEqual(transform2.rename_count, 2)
 
1315
 
 
1316
    def test_change_parent(self):
 
1317
        """Ensure that after we change a parent, the results are still right.
 
1318
 
 
1319
        Renames and parent changes on pending transforms can happen as part
 
1320
        of conflict resolution, and are explicitly permitted by the
 
1321
        TreeTransform API.
 
1322
 
 
1323
        This test ensures they work correctly with the rename-avoidance
 
1324
        optimization.
 
1325
        """
 
1326
        transform, root = self.get_transform()
 
1327
        parent1 = transform.new_directory('parent1', root)
 
1328
        child1 = transform.new_file('child1', parent1, [b'contents'])
 
1329
        parent2 = transform.new_directory('parent2', root)
 
1330
        transform.adjust_path('child1', parent2, child1)
 
1331
        transform.apply()
 
1332
        self.assertPathDoesNotExist(self.wt.abspath('parent1/child1'))
 
1333
        self.assertPathExists(self.wt.abspath('parent2/child1'))
 
1334
        # rename limbo/new-1 => parent1, rename limbo/new-3 => parent2
 
1335
        # no rename for child1 (counting only renames during apply)
 
1336
        self.assertEqual(2, transform.rename_count)
 
1337
 
 
1338
    def test_cancel_parent(self):
 
1339
        """Cancelling a parent doesn't cause deletion of a non-empty directory
 
1340
 
 
1341
        This is like the test_change_parent, except that we cancel the parent
 
1342
        before adjusting the path.  The transform must detect that the
 
1343
        directory is non-empty, and move children to safe locations.
 
1344
        """
 
1345
        transform, root = self.get_transform()
 
1346
        parent1 = transform.new_directory('parent1', root)
 
1347
        child1 = transform.new_file('child1', parent1, [b'contents'])
 
1348
        child2 = transform.new_file('child2', parent1, [b'contents'])
 
1349
        try:
 
1350
            transform.cancel_creation(parent1)
 
1351
        except OSError:
 
1352
            self.fail('Failed to move child1 before deleting parent1')
 
1353
        transform.cancel_creation(child2)
 
1354
        transform.create_directory(parent1)
 
1355
        try:
 
1356
            transform.cancel_creation(parent1)
 
1357
        # If the transform incorrectly believes that child2 is still in
 
1358
        # parent1's limbo directory, it will try to rename it and fail
 
1359
        # because was already moved by the first cancel_creation.
 
1360
        except OSError:
 
1361
            self.fail('Transform still thinks child2 is a child of parent1')
 
1362
        parent2 = transform.new_directory('parent2', root)
 
1363
        transform.adjust_path('child1', parent2, child1)
 
1364
        transform.apply()
 
1365
        self.assertPathDoesNotExist(self.wt.abspath('parent1'))
 
1366
        self.assertPathExists(self.wt.abspath('parent2/child1'))
 
1367
        # rename limbo/new-3 => parent2, rename limbo/new-2 => child1
 
1368
        self.assertEqual(2, transform.rename_count)
 
1369
 
 
1370
    def test_adjust_and_cancel(self):
 
1371
        """Make sure adjust_path keeps track of limbo children properly"""
 
1372
        transform, root = self.get_transform()
 
1373
        parent1 = transform.new_directory('parent1', root)
 
1374
        child1 = transform.new_file('child1', parent1, [b'contents'])
 
1375
        parent2 = transform.new_directory('parent2', root)
 
1376
        transform.adjust_path('child1', parent2, child1)
 
1377
        transform.cancel_creation(child1)
 
1378
        try:
 
1379
            transform.cancel_creation(parent1)
 
1380
        # if the transform thinks child1 is still in parent1's limbo
 
1381
        # directory, it will attempt to move it and fail.
 
1382
        except OSError:
 
1383
            self.fail('Transform still thinks child1 is a child of parent1')
 
1384
        transform.finalize()
 
1385
 
 
1386
    def test_noname_contents(self):
 
1387
        """TreeTransform should permit deferring naming files."""
 
1388
        transform, root = self.get_transform()
 
1389
        parent = transform.trans_id_file_id(b'parent-id')
 
1390
        try:
 
1391
            transform.create_directory(parent)
 
1392
        except KeyError:
 
1393
            self.fail("Can't handle contents with no name")
 
1394
        transform.finalize()
 
1395
 
 
1396
    def test_noname_contents_nested(self):
 
1397
        """TreeTransform should permit deferring naming files."""
 
1398
        transform, root = self.get_transform()
 
1399
        parent = transform.trans_id_file_id(b'parent-id')
 
1400
        try:
 
1401
            transform.create_directory(parent)
 
1402
        except KeyError:
 
1403
            self.fail("Can't handle contents with no name")
 
1404
        child = transform.new_directory('child', parent)
 
1405
        transform.adjust_path('parent', root, parent)
 
1406
        transform.apply()
 
1407
        self.assertPathExists(self.wt.abspath('parent/child'))
 
1408
        self.assertEqual(1, transform.rename_count)
 
1409
 
 
1410
    def test_reuse_name(self):
 
1411
        """Avoid reusing the same limbo name for different files"""
 
1412
        transform, root = self.get_transform()
 
1413
        parent = transform.new_directory('parent', root)
 
1414
        child1 = transform.new_directory('child', parent)
 
1415
        try:
 
1416
            child2 = transform.new_directory('child', parent)
 
1417
        except OSError:
 
1418
            self.fail('Tranform tried to use the same limbo name twice')
 
1419
        transform.adjust_path('child2', parent, child2)
 
1420
        transform.apply()
 
1421
        # limbo/new-1 => parent, limbo/new-3 => parent/child2
 
1422
        # child2 is put into top-level limbo because child1 has already
 
1423
        # claimed the direct limbo path when child2 is created.  There is no
 
1424
        # advantage in renaming files once they're in top-level limbo, except
 
1425
        # as part of apply.
 
1426
        self.assertEqual(2, transform.rename_count)
 
1427
 
 
1428
    def test_reuse_when_first_moved(self):
 
1429
        """Don't avoid direct paths when it is safe to use them"""
 
1430
        transform, root = self.get_transform()
 
1431
        parent = transform.new_directory('parent', root)
 
1432
        child1 = transform.new_directory('child', parent)
 
1433
        transform.adjust_path('child1', parent, child1)
 
1434
        child2 = transform.new_directory('child', parent)
 
1435
        transform.apply()
 
1436
        # limbo/new-1 => parent
 
1437
        self.assertEqual(1, transform.rename_count)
 
1438
 
 
1439
    def test_reuse_after_cancel(self):
 
1440
        """Don't avoid direct paths when it is safe to use them"""
 
1441
        transform, root = self.get_transform()
 
1442
        parent2 = transform.new_directory('parent2', root)
 
1443
        child1 = transform.new_directory('child1', parent2)
 
1444
        transform.cancel_creation(parent2)
 
1445
        transform.create_directory(parent2)
 
1446
        child2 = transform.new_directory('child1', parent2)
 
1447
        transform.adjust_path('child2', parent2, child1)
 
1448
        transform.apply()
 
1449
        # limbo/new-1 => parent2, limbo/new-2 => parent2/child1
 
1450
        self.assertEqual(2, transform.rename_count)
 
1451
 
 
1452
    def test_finalize_order(self):
 
1453
        """Finalize must be done in child-to-parent order"""
 
1454
        transform, root = self.get_transform()
 
1455
        parent = transform.new_directory('parent', root)
 
1456
        child = transform.new_directory('child', parent)
 
1457
        try:
 
1458
            transform.finalize()
 
1459
        except OSError:
 
1460
            self.fail('Tried to remove parent before child1')
 
1461
 
 
1462
    def test_cancel_with_cancelled_child_should_succeed(self):
 
1463
        transform, root = self.get_transform()
 
1464
        parent = transform.new_directory('parent', root)
 
1465
        child = transform.new_directory('child', parent)
 
1466
        transform.cancel_creation(child)
 
1467
        transform.cancel_creation(parent)
 
1468
        transform.finalize()
 
1469
 
 
1470
    def test_rollback_on_directory_clash(self):
 
1471
        def tt_helper():
 
1472
            wt = self.make_branch_and_tree('.')
 
1473
            tt = TreeTransform(wt)  # TreeTransform obtains write lock
 
1474
            try:
 
1475
                foo = tt.new_directory('foo', tt.root)
 
1476
                tt.new_file('bar', foo, [b'foobar'])
 
1477
                baz = tt.new_directory('baz', tt.root)
 
1478
                tt.new_file('qux', baz, [b'quux'])
 
1479
                # Ask for a rename 'foo' -> 'baz'
 
1480
                tt.adjust_path('baz', tt.root, foo)
 
1481
                # Lie to tt that we've already resolved all conflicts.
 
1482
                tt.apply(no_conflicts=True)
 
1483
            except:
 
1484
                wt.unlock()
 
1485
                raise
 
1486
        # The rename will fail because the target directory is not empty (but
 
1487
        # raises FileExists anyway).
 
1488
        err = self.assertRaises(errors.FileExists, tt_helper)
 
1489
        self.assertEndsWith(err.path, "/baz")
 
1490
 
 
1491
    def test_two_directories_clash(self):
 
1492
        def tt_helper():
 
1493
            wt = self.make_branch_and_tree('.')
 
1494
            tt = TreeTransform(wt)  # TreeTransform obtains write lock
 
1495
            try:
 
1496
                foo_1 = tt.new_directory('foo', tt.root)
 
1497
                tt.new_directory('bar', foo_1)
 
1498
                # Adding the same directory with a different content
 
1499
                foo_2 = tt.new_directory('foo', tt.root)
 
1500
                tt.new_directory('baz', foo_2)
 
1501
                # Lie to tt that we've already resolved all conflicts.
 
1502
                tt.apply(no_conflicts=True)
 
1503
            except:
 
1504
                wt.unlock()
 
1505
                raise
 
1506
        err = self.assertRaises(errors.FileExists, tt_helper)
 
1507
        self.assertEndsWith(err.path, "/foo")
 
1508
 
 
1509
    def test_two_directories_clash_finalize(self):
 
1510
        def tt_helper():
 
1511
            wt = self.make_branch_and_tree('.')
 
1512
            tt = TreeTransform(wt)  # TreeTransform obtains write lock
 
1513
            try:
 
1514
                foo_1 = tt.new_directory('foo', tt.root)
 
1515
                tt.new_directory('bar', foo_1)
 
1516
                # Adding the same directory with a different content
 
1517
                foo_2 = tt.new_directory('foo', tt.root)
 
1518
                tt.new_directory('baz', foo_2)
 
1519
                # Lie to tt that we've already resolved all conflicts.
 
1520
                tt.apply(no_conflicts=True)
 
1521
            except:
 
1522
                tt.finalize()
 
1523
                raise
 
1524
        err = self.assertRaises(errors.FileExists, tt_helper)
 
1525
        self.assertEndsWith(err.path, "/foo")
 
1526
 
 
1527
    def test_file_to_directory(self):
 
1528
        wt = self.make_branch_and_tree('.')
 
1529
        self.build_tree(['foo'])
 
1530
        wt.add(['foo'])
 
1531
        wt.commit("one")
 
1532
        tt = TreeTransform(wt)
 
1533
        self.addCleanup(tt.finalize)
 
1534
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1535
        tt.delete_contents(foo_trans_id)
 
1536
        tt.create_directory(foo_trans_id)
 
1537
        bar_trans_id = tt.trans_id_tree_path("foo/bar")
 
1538
        tt.create_file([b"aa\n"], bar_trans_id)
 
1539
        tt.version_file(b"bar-1", bar_trans_id)
 
1540
        tt.apply()
 
1541
        self.assertPathExists("foo/bar")
 
1542
        wt.lock_read()
 
1543
        try:
 
1544
            self.assertEqual(wt.kind("foo"), "directory")
 
1545
        finally:
 
1546
            wt.unlock()
 
1547
        wt.commit("two")
 
1548
        changes = wt.changes_from(wt.basis_tree())
 
1549
        self.assertFalse(changes.has_changed(), changes)
 
1550
 
 
1551
    def test_file_to_symlink(self):
 
1552
        self.requireFeature(SymlinkFeature)
 
1553
        wt = self.make_branch_and_tree('.')
 
1554
        self.build_tree(['foo'])
 
1555
        wt.add(['foo'])
 
1556
        wt.commit("one")
 
1557
        tt = TreeTransform(wt)
 
1558
        self.addCleanup(tt.finalize)
 
1559
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1560
        tt.delete_contents(foo_trans_id)
 
1561
        tt.create_symlink("bar", foo_trans_id)
 
1562
        tt.apply()
 
1563
        self.assertPathExists("foo")
 
1564
        wt.lock_read()
 
1565
        self.addCleanup(wt.unlock)
 
1566
        self.assertEqual(wt.kind("foo"), "symlink")
 
1567
 
 
1568
    def test_dir_to_file(self):
 
1569
        wt = self.make_branch_and_tree('.')
 
1570
        self.build_tree(['foo/', 'foo/bar'])
 
1571
        wt.add(['foo', 'foo/bar'])
 
1572
        wt.commit("one")
 
1573
        tt = TreeTransform(wt)
 
1574
        self.addCleanup(tt.finalize)
 
1575
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1576
        bar_trans_id = tt.trans_id_tree_path("foo/bar")
 
1577
        tt.delete_contents(foo_trans_id)
 
1578
        tt.delete_versioned(bar_trans_id)
 
1579
        tt.create_file([b"aa\n"], foo_trans_id)
 
1580
        tt.apply()
 
1581
        self.assertPathExists("foo")
 
1582
        wt.lock_read()
 
1583
        self.addCleanup(wt.unlock)
 
1584
        self.assertEqual(wt.kind("foo"), "file")
 
1585
 
 
1586
    def test_dir_to_hardlink(self):
 
1587
        self.requireFeature(HardlinkFeature)
 
1588
        wt = self.make_branch_and_tree('.')
 
1589
        self.build_tree(['foo/', 'foo/bar'])
 
1590
        wt.add(['foo', 'foo/bar'])
 
1591
        wt.commit("one")
 
1592
        tt = TreeTransform(wt)
 
1593
        self.addCleanup(tt.finalize)
 
1594
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1595
        bar_trans_id = tt.trans_id_tree_path("foo/bar")
 
1596
        tt.delete_contents(foo_trans_id)
 
1597
        tt.delete_versioned(bar_trans_id)
 
1598
        self.build_tree(['baz'])
 
1599
        tt.create_hardlink("baz", foo_trans_id)
 
1600
        tt.apply()
 
1601
        self.assertPathExists("foo")
 
1602
        self.assertPathExists("baz")
 
1603
        wt.lock_read()
 
1604
        self.addCleanup(wt.unlock)
 
1605
        self.assertEqual(wt.kind("foo"), "file")
 
1606
 
 
1607
    def test_no_final_path(self):
 
1608
        transform, root = self.get_transform()
 
1609
        trans_id = transform.trans_id_file_id(b'foo')
 
1610
        transform.create_file([b'bar'], trans_id)
 
1611
        transform.cancel_creation(trans_id)
 
1612
        transform.apply()
 
1613
 
 
1614
    def test_create_from_tree(self):
 
1615
        tree1 = self.make_branch_and_tree('tree1')
 
1616
        self.build_tree_contents([('tree1/foo/',), ('tree1/bar', b'baz')])
 
1617
        tree1.add(['foo', 'bar'], [b'foo-id', b'bar-id'])
 
1618
        tree2 = self.make_branch_and_tree('tree2')
 
1619
        tt = TreeTransform(tree2)
 
1620
        foo_trans_id = tt.create_path('foo', tt.root)
 
1621
        create_from_tree(tt, foo_trans_id, tree1, 'foo', file_id=b'foo-id')
 
1622
        bar_trans_id = tt.create_path('bar', tt.root)
 
1623
        create_from_tree(tt, bar_trans_id, tree1, 'bar', file_id=b'bar-id')
 
1624
        tt.apply()
 
1625
        self.assertEqual('directory', osutils.file_kind('tree2/foo'))
 
1626
        self.assertFileEqual(b'baz', 'tree2/bar')
 
1627
 
 
1628
    def test_create_from_tree_bytes(self):
 
1629
        """Provided lines are used instead of tree content."""
 
1630
        tree1 = self.make_branch_and_tree('tree1')
 
1631
        self.build_tree_contents([('tree1/foo', b'bar'),])
 
1632
        tree1.add('foo', b'foo-id')
 
1633
        tree2 = self.make_branch_and_tree('tree2')
 
1634
        tt = TreeTransform(tree2)
 
1635
        foo_trans_id = tt.create_path('foo', tt.root)
 
1636
        create_from_tree(tt, foo_trans_id, tree1, 'foo', file_id=b'foo-id',
 
1637
                         chunks=[b'qux'])
 
1638
        tt.apply()
 
1639
        self.assertFileEqual(b'qux', 'tree2/foo')
 
1640
 
 
1641
    def test_create_from_tree_symlink(self):
 
1642
        self.requireFeature(SymlinkFeature)
 
1643
        tree1 = self.make_branch_and_tree('tree1')
 
1644
        os.symlink('bar', 'tree1/foo')
 
1645
        tree1.add('foo', b'foo-id')
 
1646
        tt = TreeTransform(self.make_branch_and_tree('tree2'))
 
1647
        foo_trans_id = tt.create_path('foo', tt.root)
 
1648
        create_from_tree(tt, foo_trans_id, tree1, 'foo', file_id=b'foo-id')
 
1649
        tt.apply()
 
1650
        self.assertEqual('bar', os.readlink('tree2/foo'))
 
1651
 
 
1652
 
 
1653
class TransformGroup(object):
 
1654
 
 
1655
    def __init__(self, dirname, root_id):
 
1656
        self.name = dirname
 
1657
        os.mkdir(dirname)
 
1658
        self.wt = ControlDir.create_standalone_workingtree(dirname)
 
1659
        self.wt.set_root_id(root_id)
 
1660
        self.b = self.wt.branch
 
1661
        self.tt = TreeTransform(self.wt)
 
1662
        self.root = self.tt.trans_id_tree_path('')
 
1663
 
 
1664
 
 
1665
def conflict_text(tree, merge):
 
1666
    template = b'%s TREE\n%s%s\n%s%s MERGE-SOURCE\n'
 
1667
    return template % (b'<' * 7, tree, b'=' * 7, merge, b'>' * 7)
 
1668
 
 
1669
 
 
1670
class TestInventoryAltered(tests.TestCaseWithTransport):
 
1671
 
 
1672
    def test_inventory_altered_unchanged(self):
 
1673
        tree = self.make_branch_and_tree('tree')
 
1674
        self.build_tree(['tree/foo'])
 
1675
        tree.add('foo', b'foo-id')
 
1676
        with TransformPreview(tree) as tt:
 
1677
            self.assertEqual([], tt._inventory_altered())
 
1678
 
 
1679
    def test_inventory_altered_changed_parent_id(self):
 
1680
        tree = self.make_branch_and_tree('tree')
 
1681
        self.build_tree(['tree/foo'])
 
1682
        tree.add('foo', b'foo-id')
 
1683
        with TransformPreview(tree) as tt:
 
1684
            tt.unversion_file(tt.root)
 
1685
            tt.version_file(b'new-id', tt.root)
 
1686
            foo_trans_id = tt.trans_id_tree_path('foo')
 
1687
            foo_tuple = ('foo', foo_trans_id)
 
1688
            root_tuple = ('', tt.root)
 
1689
            self.assertEqual([root_tuple, foo_tuple], tt._inventory_altered())
 
1690
 
 
1691
    def test_inventory_altered_noop_changed_parent_id(self):
 
1692
        tree = self.make_branch_and_tree('tree')
 
1693
        self.build_tree(['tree/foo'])
 
1694
        tree.add('foo', b'foo-id')
 
1695
        with TransformPreview(tree) as tt:
 
1696
            tt.unversion_file(tt.root)
 
1697
            tt.version_file(tree.get_root_id(), tt.root)
 
1698
            foo_trans_id = tt.trans_id_tree_path('foo')
 
1699
            self.assertEqual([], tt._inventory_altered())
 
1700
 
 
1701
 
 
1702
class TestTransformMerge(TestCaseInTempDir):
 
1703
 
 
1704
    def test_text_merge(self):
 
1705
        root_id = generate_ids.gen_root_id()
 
1706
        base = TransformGroup("base", root_id)
 
1707
        base.tt.new_file('a', base.root, [b'a\nb\nc\nd\be\n'], b'a')
 
1708
        base.tt.new_file('b', base.root, [b'b1'], b'b')
 
1709
        base.tt.new_file('c', base.root, [b'c'], b'c')
 
1710
        base.tt.new_file('d', base.root, [b'd'], b'd')
 
1711
        base.tt.new_file('e', base.root, [b'e'], b'e')
 
1712
        base.tt.new_file('f', base.root, [b'f'], b'f')
 
1713
        base.tt.new_directory('g', base.root, b'g')
 
1714
        base.tt.new_directory('h', base.root, b'h')
 
1715
        base.tt.apply()
 
1716
        other = TransformGroup("other", root_id)
 
1717
        other.tt.new_file('a', other.root, [b'y\nb\nc\nd\be\n'], b'a')
 
1718
        other.tt.new_file('b', other.root, [b'b2'], b'b')
 
1719
        other.tt.new_file('c', other.root, [b'c2'], b'c')
 
1720
        other.tt.new_file('d', other.root, [b'd'], b'd')
 
1721
        other.tt.new_file('e', other.root, [b'e2'], b'e')
 
1722
        other.tt.new_file('f', other.root, [b'f'], b'f')
 
1723
        other.tt.new_file('g', other.root, [b'g'], b'g')
 
1724
        other.tt.new_file('h', other.root, [b'h\ni\nj\nk\n'], b'h')
 
1725
        other.tt.new_file('i', other.root, [b'h\ni\nj\nk\n'], b'i')
 
1726
        other.tt.apply()
 
1727
        this = TransformGroup("this", root_id)
 
1728
        this.tt.new_file('a', this.root, [b'a\nb\nc\nd\bz\n'], b'a')
 
1729
        this.tt.new_file('b', this.root, [b'b'], b'b')
 
1730
        this.tt.new_file('c', this.root, [b'c'], b'c')
 
1731
        this.tt.new_file('d', this.root, [b'd2'], b'd')
 
1732
        this.tt.new_file('e', this.root, [b'e2'], b'e')
 
1733
        this.tt.new_file('f', this.root, [b'f'], b'f')
 
1734
        this.tt.new_file('g', this.root, [b'g'], b'g')
 
1735
        this.tt.new_file('h', this.root, [b'1\n2\n3\n4\n'], b'h')
 
1736
        this.tt.new_file('i', this.root, [b'1\n2\n3\n4\n'], b'i')
 
1737
        this.tt.apply()
 
1738
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1739
 
 
1740
        # textual merge
 
1741
        with this.wt.get_file(this.wt.id2path(b'a')) as f:
 
1742
            self.assertEqual(f.read(), b'y\nb\nc\nd\bz\n')
 
1743
        # three-way text conflict
 
1744
        with this.wt.get_file(this.wt.id2path(b'b')) as f:
 
1745
            self.assertEqual(f.read(), conflict_text(b'b', b'b2'))
 
1746
        # OTHER wins
 
1747
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'c')).read(), b'c2')
 
1748
        # THIS wins
 
1749
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'd')).read(), b'd2')
 
1750
        # Ambigious clean merge
 
1751
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'e')).read(), b'e2')
 
1752
        # No change
 
1753
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'f')).read(), b'f')
 
1754
        # Correct correct results when THIS == OTHER
 
1755
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'g')).read(), b'g')
 
1756
        # Text conflict when THIS & OTHER are text and BASE is dir
 
1757
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'h')).read(),
 
1758
                         conflict_text(b'1\n2\n3\n4\n', b'h\ni\nj\nk\n'))
 
1759
        self.assertEqual(this.wt.get_file('h.THIS').read(),
 
1760
                         b'1\n2\n3\n4\n')
 
1761
        self.assertEqual(this.wt.get_file('h.OTHER').read(),
 
1762
                         b'h\ni\nj\nk\n')
 
1763
        self.assertEqual(file_kind(this.wt.abspath('h.BASE')), 'directory')
 
1764
        self.assertEqual(this.wt.get_file(this.wt.id2path(b'i')).read(),
 
1765
                         conflict_text(b'1\n2\n3\n4\n', b'h\ni\nj\nk\n'))
 
1766
        self.assertEqual(this.wt.get_file('i.THIS').read(),
 
1767
                         b'1\n2\n3\n4\n')
 
1768
        self.assertEqual(this.wt.get_file('i.OTHER').read(),
 
1769
                         b'h\ni\nj\nk\n')
 
1770
        self.assertEqual(os.path.exists(this.wt.abspath('i.BASE')), False)
 
1771
        modified = ['a', 'b', 'c', 'h', 'i']
 
1772
        merge_modified = this.wt.merge_modified()
 
1773
        self.assertSubset(merge_modified, modified)
 
1774
        self.assertEqual(len(merge_modified), len(modified))
 
1775
        with open(this.wt.abspath(this.wt.id2path('a')), 'wb') as f: f.write(b'booga')
 
1776
        modified.pop(0)
 
1777
        merge_modified = this.wt.merge_modified()
 
1778
        self.assertSubset(merge_modified, modified)
 
1779
        self.assertEqual(len(merge_modified), len(modified))
 
1780
        this.wt.remove('b')
 
1781
        this.wt.revert()
 
1782
 
 
1783
    def test_file_merge(self):
 
1784
        self.requireFeature(SymlinkFeature)
 
1785
        root_id = generate_ids.gen_root_id()
 
1786
        base = TransformGroup("BASE", root_id)
 
1787
        this = TransformGroup("THIS", root_id)
 
1788
        other = TransformGroup("OTHER", root_id)
 
1789
        for tg in this, base, other:
 
1790
            tg.tt.new_directory('a', tg.root, b'a')
 
1791
            tg.tt.new_symlink('b', tg.root, 'b', b'b')
 
1792
            tg.tt.new_file('c', tg.root, [b'c'], b'c')
 
1793
            tg.tt.new_symlink('d', tg.root, tg.name, b'd')
 
1794
        targets = ((base, 'base-e', 'base-f', None, None),
 
1795
                   (this, 'other-e', 'this-f', 'other-g', 'this-h'),
 
1796
                   (other, 'other-e', None, 'other-g', 'other-h'))
 
1797
        for tg, e_target, f_target, g_target, h_target in targets:
 
1798
            for link, target in (('e', e_target), ('f', f_target),
 
1799
                                 ('g', g_target), ('h', h_target)):
 
1800
                if target is not None:
 
1801
                    tg.tt.new_symlink(link, tg.root, target, link.encode('ascii'))
 
1802
 
 
1803
        for tg in this, base, other:
 
1804
            tg.tt.apply()
 
1805
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1806
        self.assertIs(os.path.isdir(this.wt.abspath('a')), True)
 
1807
        self.assertIs(os.path.islink(this.wt.abspath('b')), True)
 
1808
        self.assertIs(os.path.isfile(this.wt.abspath('c')), True)
 
1809
        for suffix in ('THIS', 'BASE', 'OTHER'):
 
1810
            self.assertEqual(os.readlink(this.wt.abspath('d.'+suffix)), suffix)
 
1811
        self.assertIs(os.path.lexists(this.wt.abspath('d')), False)
 
1812
        self.assertEqual(this.wt.id2path(b'd'), 'd.OTHER')
 
1813
        self.assertEqual(this.wt.id2path(b'f'), 'f.THIS')
 
1814
        self.assertEqual(os.readlink(this.wt.abspath('e')), 'other-e')
 
1815
        self.assertIs(os.path.lexists(this.wt.abspath('e.THIS')), False)
 
1816
        self.assertIs(os.path.lexists(this.wt.abspath('e.OTHER')), False)
 
1817
        self.assertIs(os.path.lexists(this.wt.abspath('e.BASE')), False)
 
1818
        self.assertIs(os.path.lexists(this.wt.abspath('g')), True)
 
1819
        self.assertIs(os.path.lexists(this.wt.abspath('g.BASE')), False)
 
1820
        self.assertIs(os.path.lexists(this.wt.abspath('h')), False)
 
1821
        self.assertIs(os.path.lexists(this.wt.abspath('h.BASE')), False)
 
1822
        self.assertIs(os.path.lexists(this.wt.abspath('h.THIS')), True)
 
1823
        self.assertIs(os.path.lexists(this.wt.abspath('h.OTHER')), True)
 
1824
 
 
1825
    def test_filename_merge(self):
 
1826
        root_id = generate_ids.gen_root_id()
 
1827
        base = TransformGroup("BASE", root_id)
 
1828
        this = TransformGroup("THIS", root_id)
 
1829
        other = TransformGroup("OTHER", root_id)
 
1830
        base_a, this_a, other_a = [t.tt.new_directory('a', t.root, b'a')
 
1831
                                   for t in [base, this, other]]
 
1832
        base_b, this_b, other_b = [t.tt.new_directory('b', t.root, b'b')
 
1833
                                   for t in [base, this, other]]
 
1834
        base.tt.new_directory('c', base_a, b'c')
 
1835
        this.tt.new_directory('c1', this_a, b'c')
 
1836
        other.tt.new_directory('c', other_b, b'c')
 
1837
 
 
1838
        base.tt.new_directory('d', base_a, b'd')
 
1839
        this.tt.new_directory('d1', this_b, b'd')
 
1840
        other.tt.new_directory('d', other_a, b'd')
 
1841
 
 
1842
        base.tt.new_directory('e', base_a, b'e')
 
1843
        this.tt.new_directory('e', this_a, b'e')
 
1844
        other.tt.new_directory('e1', other_b, b'e')
 
1845
 
 
1846
        base.tt.new_directory('f', base_a, b'f')
 
1847
        this.tt.new_directory('f1', this_b, b'f')
 
1848
        other.tt.new_directory('f1', other_b, b'f')
 
1849
 
 
1850
        for tg in [this, base, other]:
 
1851
            tg.tt.apply()
 
1852
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1853
        self.assertEqual(this.wt.id2path(b'c'), pathjoin('b/c1'))
 
1854
        self.assertEqual(this.wt.id2path(b'd'), pathjoin('b/d1'))
 
1855
        self.assertEqual(this.wt.id2path(b'e'), pathjoin('b/e1'))
 
1856
        self.assertEqual(this.wt.id2path(b'f'), pathjoin('b/f1'))
 
1857
 
 
1858
    def test_filename_merge_conflicts(self):
 
1859
        root_id = generate_ids.gen_root_id()
 
1860
        base = TransformGroup("BASE", root_id)
 
1861
        this = TransformGroup("THIS", root_id)
 
1862
        other = TransformGroup("OTHER", root_id)
 
1863
        base_a, this_a, other_a = [t.tt.new_directory('a', t.root, b'a')
 
1864
                                   for t in [base, this, other]]
 
1865
        base_b, this_b, other_b = [t.tt.new_directory('b', t.root, b'b')
 
1866
                                   for t in [base, this, other]]
 
1867
 
 
1868
        base.tt.new_file('g', base_a, [b'g'], b'g')
 
1869
        other.tt.new_file('g1', other_b, [b'g1'], b'g')
 
1870
 
 
1871
        base.tt.new_file('h', base_a, [b'h'], b'h')
 
1872
        this.tt.new_file('h1', this_b, [b'h1'], b'h')
 
1873
 
 
1874
        base.tt.new_file('i', base.root, [b'i'], b'i')
 
1875
        other.tt.new_directory('i1', this_b, b'i')
 
1876
 
 
1877
        for tg in [this, base, other]:
 
1878
            tg.tt.apply()
 
1879
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1880
 
 
1881
        self.assertEqual(this.wt.id2path(b'g'), pathjoin('b/g1.OTHER'))
 
1882
        self.assertIs(os.path.lexists(this.wt.abspath('b/g1.BASE')), True)
 
1883
        self.assertIs(os.path.lexists(this.wt.abspath('b/g1.THIS')), False)
 
1884
        self.assertEqual(this.wt.id2path(b'h'), pathjoin('b/h1.THIS'))
 
1885
        self.assertIs(os.path.lexists(this.wt.abspath('b/h1.BASE')), True)
 
1886
        self.assertIs(os.path.lexists(this.wt.abspath('b/h1.OTHER')), False)
 
1887
        self.assertEqual(this.wt.id2path(b'i'), pathjoin('b/i1.OTHER'))
 
1888
 
 
1889
 
 
1890
class TestBuildTree(tests.TestCaseWithTransport):
 
1891
 
 
1892
    def test_build_tree_with_symlinks(self):
 
1893
        self.requireFeature(SymlinkFeature)
 
1894
        os.mkdir('a')
 
1895
        a = ControlDir.create_standalone_workingtree('a')
 
1896
        os.mkdir('a/foo')
 
1897
        with open('a/foo/bar', 'wb') as f: f.write(b'contents')
 
1898
        os.symlink('a/foo/bar', 'a/foo/baz')
 
1899
        a.add(['foo', 'foo/bar', 'foo/baz'])
 
1900
        a.commit('initial commit')
 
1901
        b = ControlDir.create_standalone_workingtree('b')
 
1902
        basis = a.basis_tree()
 
1903
        basis.lock_read()
 
1904
        self.addCleanup(basis.unlock)
 
1905
        build_tree(basis, b)
 
1906
        self.assertIs(os.path.isdir('b/foo'), True)
 
1907
        with open('b/foo/bar', 'rb') as f:
 
1908
            self.assertEqual(f.read(), b"contents")
 
1909
        self.assertEqual(os.readlink('b/foo/baz'), 'a/foo/bar')
 
1910
 
 
1911
    def test_build_with_references(self):
 
1912
        tree = self.make_branch_and_tree('source',
 
1913
            format='development-subtree')
 
1914
        subtree = self.make_branch_and_tree('source/subtree',
 
1915
            format='development-subtree')
 
1916
        tree.add_reference(subtree)
 
1917
        tree.commit('a revision')
 
1918
        tree.branch.create_checkout('target')
 
1919
        self.assertPathExists('target')
 
1920
        self.assertPathExists('target/subtree')
 
1921
 
 
1922
    def test_file_conflict_handling(self):
 
1923
        """Ensure that when building trees, conflict handling is done"""
 
1924
        source = self.make_branch_and_tree('source')
 
1925
        target = self.make_branch_and_tree('target')
 
1926
        self.build_tree(['source/file', 'target/file'])
 
1927
        source.add('file', b'new-file')
 
1928
        source.commit('added file')
 
1929
        build_tree(source.basis_tree(), target)
 
1930
        self.assertEqual([DuplicateEntry('Moved existing file to',
 
1931
                          'file.moved', 'file', None, 'new-file')],
 
1932
                         target.conflicts())
 
1933
        target2 = self.make_branch_and_tree('target2')
 
1934
        with open('target2/file', 'wb') as target_file, \
 
1935
                open('source/file', 'rb') as source_file:
 
1936
            target_file.write(source_file.read())
 
1937
        build_tree(source.basis_tree(), target2)
 
1938
        self.assertEqual([], target2.conflicts())
 
1939
 
 
1940
    def test_symlink_conflict_handling(self):
 
1941
        """Ensure that when building trees, conflict handling is done"""
 
1942
        self.requireFeature(SymlinkFeature)
 
1943
        source = self.make_branch_and_tree('source')
 
1944
        os.symlink('foo', 'source/symlink')
 
1945
        source.add('symlink', b'new-symlink')
 
1946
        source.commit('added file')
 
1947
        target = self.make_branch_and_tree('target')
 
1948
        os.symlink('bar', 'target/symlink')
 
1949
        build_tree(source.basis_tree(), target)
 
1950
        self.assertEqual([DuplicateEntry('Moved existing file to',
 
1951
            'symlink.moved', 'symlink', None, 'new-symlink')],
 
1952
            target.conflicts())
 
1953
        target = self.make_branch_and_tree('target2')
 
1954
        os.symlink('foo', 'target2/symlink')
 
1955
        build_tree(source.basis_tree(), target)
 
1956
        self.assertEqual([], target.conflicts())
 
1957
 
 
1958
    def test_directory_conflict_handling(self):
 
1959
        """Ensure that when building trees, conflict handling is done"""
 
1960
        source = self.make_branch_and_tree('source')
 
1961
        target = self.make_branch_and_tree('target')
 
1962
        self.build_tree(['source/dir1/', 'source/dir1/file', 'target/dir1/'])
 
1963
        source.add(['dir1', 'dir1/file'], [b'new-dir1', b'new-file'])
 
1964
        source.commit('added file')
 
1965
        build_tree(source.basis_tree(), target)
 
1966
        self.assertEqual([], target.conflicts())
 
1967
        self.assertPathExists('target/dir1/file')
 
1968
 
 
1969
        # Ensure contents are merged
 
1970
        target = self.make_branch_and_tree('target2')
 
1971
        self.build_tree(['target2/dir1/', 'target2/dir1/file2'])
 
1972
        build_tree(source.basis_tree(), target)
 
1973
        self.assertEqual([], target.conflicts())
 
1974
        self.assertPathExists('target2/dir1/file2')
 
1975
        self.assertPathExists('target2/dir1/file')
 
1976
 
 
1977
        # Ensure new contents are suppressed for existing branches
 
1978
        target = self.make_branch_and_tree('target3')
 
1979
        self.make_branch('target3/dir1')
 
1980
        self.build_tree(['target3/dir1/file2'])
 
1981
        build_tree(source.basis_tree(), target)
 
1982
        self.assertPathDoesNotExist('target3/dir1/file')
 
1983
        self.assertPathExists('target3/dir1/file2')
 
1984
        self.assertPathExists('target3/dir1.diverted/file')
 
1985
        self.assertEqual([DuplicateEntry('Diverted to',
 
1986
            'dir1.diverted', 'dir1', 'new-dir1', None)],
 
1987
            target.conflicts())
 
1988
 
 
1989
        target = self.make_branch_and_tree('target4')
 
1990
        self.build_tree(['target4/dir1/'])
 
1991
        self.make_branch('target4/dir1/file')
 
1992
        build_tree(source.basis_tree(), target)
 
1993
        self.assertPathExists('target4/dir1/file')
 
1994
        self.assertEqual('directory', file_kind('target4/dir1/file'))
 
1995
        self.assertPathExists('target4/dir1/file.diverted')
 
1996
        self.assertEqual([DuplicateEntry('Diverted to',
 
1997
            'dir1/file.diverted', 'dir1/file', 'new-file', None)],
 
1998
            target.conflicts())
 
1999
 
 
2000
    def test_mixed_conflict_handling(self):
 
2001
        """Ensure that when building trees, conflict handling is done"""
 
2002
        source = self.make_branch_and_tree('source')
 
2003
        target = self.make_branch_and_tree('target')
 
2004
        self.build_tree(['source/name', 'target/name/'])
 
2005
        source.add('name', b'new-name')
 
2006
        source.commit('added file')
 
2007
        build_tree(source.basis_tree(), target)
 
2008
        self.assertEqual([DuplicateEntry('Moved existing file to',
 
2009
            'name.moved', 'name', None, 'new-name')], target.conflicts())
 
2010
 
 
2011
    def test_raises_in_populated(self):
 
2012
        source = self.make_branch_and_tree('source')
 
2013
        self.build_tree(['source/name'])
 
2014
        source.add('name')
 
2015
        source.commit('added name')
 
2016
        target = self.make_branch_and_tree('target')
 
2017
        self.build_tree(['target/name'])
 
2018
        target.add('name')
 
2019
        self.assertRaises(errors.WorkingTreeAlreadyPopulated,
 
2020
            build_tree, source.basis_tree(), target)
 
2021
 
 
2022
    def test_build_tree_rename_count(self):
 
2023
        source = self.make_branch_and_tree('source')
 
2024
        self.build_tree(['source/file1', 'source/dir1/'])
 
2025
        source.add(['file1', 'dir1'])
 
2026
        source.commit('add1')
 
2027
        target1 = self.make_branch_and_tree('target1')
 
2028
        transform_result = build_tree(source.basis_tree(), target1)
 
2029
        self.assertEqual(2, transform_result.rename_count)
 
2030
 
 
2031
        self.build_tree(['source/dir1/file2'])
 
2032
        source.add(['dir1/file2'])
 
2033
        source.commit('add3')
 
2034
        target2 = self.make_branch_and_tree('target2')
 
2035
        transform_result = build_tree(source.basis_tree(), target2)
 
2036
        # children of non-root directories should not be renamed
 
2037
        self.assertEqual(2, transform_result.rename_count)
 
2038
 
 
2039
    def create_ab_tree(self):
 
2040
        """Create a committed test tree with two files"""
 
2041
        source = self.make_branch_and_tree('source')
 
2042
        self.build_tree_contents([('source/file1', b'A')])
 
2043
        self.build_tree_contents([('source/file2', b'B')])
 
2044
        source.add(['file1', 'file2'], [b'file1-id', b'file2-id'])
 
2045
        source.commit('commit files')
 
2046
        source.lock_write()
 
2047
        self.addCleanup(source.unlock)
 
2048
        return source
 
2049
 
 
2050
    def test_build_tree_accelerator_tree(self):
 
2051
        source = self.create_ab_tree()
 
2052
        self.build_tree_contents([('source/file2', b'C')])
 
2053
        calls = []
 
2054
        real_source_get_file = source.get_file
 
2055
        def get_file(path, file_id=None):
 
2056
            calls.append(file_id)
 
2057
            return real_source_get_file(path, file_id)
 
2058
        source.get_file = get_file
 
2059
        target = self.make_branch_and_tree('target')
 
2060
        revision_tree = source.basis_tree()
 
2061
        revision_tree.lock_read()
 
2062
        self.addCleanup(revision_tree.unlock)
 
2063
        build_tree(revision_tree, target, source)
 
2064
        self.assertEqual([b'file1-id'], calls)
 
2065
        target.lock_read()
 
2066
        self.addCleanup(target.unlock)
 
2067
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2068
 
 
2069
    def test_build_tree_accelerator_tree_observes_sha1(self):
 
2070
        source = self.create_ab_tree()
 
2071
        sha1 = osutils.sha_string(b'A')
 
2072
        target = self.make_branch_and_tree('target')
 
2073
        target.lock_write()
 
2074
        self.addCleanup(target.unlock)
 
2075
        state = target.current_dirstate()
 
2076
        state._cutoff_time = time.time() + 60
 
2077
        build_tree(source.basis_tree(), target, source)
 
2078
        entry = state._get_entry(0, path_utf8=b'file1')
 
2079
        self.assertEqual(sha1, entry[1][0][1])
 
2080
 
 
2081
    def test_build_tree_accelerator_tree_missing_file(self):
 
2082
        source = self.create_ab_tree()
 
2083
        os.unlink('source/file1')
 
2084
        source.remove(['file2'])
 
2085
        target = self.make_branch_and_tree('target')
 
2086
        revision_tree = source.basis_tree()
 
2087
        revision_tree.lock_read()
 
2088
        self.addCleanup(revision_tree.unlock)
 
2089
        build_tree(revision_tree, target, source)
 
2090
        target.lock_read()
 
2091
        self.addCleanup(target.unlock)
 
2092
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2093
 
 
2094
    def test_build_tree_accelerator_wrong_kind(self):
 
2095
        self.requireFeature(SymlinkFeature)
 
2096
        source = self.make_branch_and_tree('source')
 
2097
        self.build_tree_contents([('source/file1', b'')])
 
2098
        self.build_tree_contents([('source/file2', b'')])
 
2099
        source.add(['file1', 'file2'], [b'file1-id', b'file2-id'])
 
2100
        source.commit('commit files')
 
2101
        os.unlink('source/file2')
 
2102
        self.build_tree_contents([('source/file2/', b'C')])
 
2103
        os.unlink('source/file1')
 
2104
        os.symlink('file2', 'source/file1')
 
2105
        calls = []
 
2106
        real_source_get_file = source.get_file
 
2107
        def get_file(path, file_id=None):
 
2108
            calls.append(file_id)
 
2109
            return real_source_get_file(path, file_id)
 
2110
        source.get_file = get_file
 
2111
        target = self.make_branch_and_tree('target')
 
2112
        revision_tree = source.basis_tree()
 
2113
        revision_tree.lock_read()
 
2114
        self.addCleanup(revision_tree.unlock)
 
2115
        build_tree(revision_tree, target, source)
 
2116
        self.assertEqual([], calls)
 
2117
        target.lock_read()
 
2118
        self.addCleanup(target.unlock)
 
2119
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2120
 
 
2121
    def test_build_tree_hardlink(self):
 
2122
        self.requireFeature(HardlinkFeature)
 
2123
        source = self.create_ab_tree()
 
2124
        target = self.make_branch_and_tree('target')
 
2125
        revision_tree = source.basis_tree()
 
2126
        revision_tree.lock_read()
 
2127
        self.addCleanup(revision_tree.unlock)
 
2128
        build_tree(revision_tree, target, source, hardlink=True)
 
2129
        target.lock_read()
 
2130
        self.addCleanup(target.unlock)
 
2131
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2132
        source_stat = os.stat('source/file1')
 
2133
        target_stat = os.stat('target/file1')
 
2134
        self.assertEqual(source_stat, target_stat)
 
2135
 
 
2136
        # Explicitly disallowing hardlinks should prevent them.
 
2137
        target2 = self.make_branch_and_tree('target2')
 
2138
        build_tree(revision_tree, target2, source, hardlink=False)
 
2139
        target2.lock_read()
 
2140
        self.addCleanup(target2.unlock)
 
2141
        self.assertEqual([], list(target2.iter_changes(revision_tree)))
 
2142
        source_stat = os.stat('source/file1')
 
2143
        target2_stat = os.stat('target2/file1')
 
2144
        self.assertNotEqual(source_stat, target2_stat)
 
2145
 
 
2146
    def test_build_tree_accelerator_tree_moved(self):
 
2147
        source = self.make_branch_and_tree('source')
 
2148
        self.build_tree_contents([('source/file1', b'A')])
 
2149
        source.add(['file1'], [b'file1-id'])
 
2150
        source.commit('commit files')
 
2151
        source.rename_one('file1', 'file2')
 
2152
        source.lock_read()
 
2153
        self.addCleanup(source.unlock)
 
2154
        target = self.make_branch_and_tree('target')
 
2155
        revision_tree = source.basis_tree()
 
2156
        revision_tree.lock_read()
 
2157
        self.addCleanup(revision_tree.unlock)
 
2158
        build_tree(revision_tree, target, source)
 
2159
        target.lock_read()
 
2160
        self.addCleanup(target.unlock)
 
2161
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2162
 
 
2163
    def test_build_tree_hardlinks_preserve_execute(self):
 
2164
        self.requireFeature(HardlinkFeature)
 
2165
        source = self.create_ab_tree()
 
2166
        tt = TreeTransform(source)
 
2167
        trans_id = tt.trans_id_tree_path('file1')
 
2168
        tt.set_executability(True, trans_id)
 
2169
        tt.apply()
 
2170
        self.assertTrue(source.is_executable('file1'))
 
2171
        target = self.make_branch_and_tree('target')
 
2172
        revision_tree = source.basis_tree()
 
2173
        revision_tree.lock_read()
 
2174
        self.addCleanup(revision_tree.unlock)
 
2175
        build_tree(revision_tree, target, source, hardlink=True)
 
2176
        target.lock_read()
 
2177
        self.addCleanup(target.unlock)
 
2178
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2179
        self.assertTrue(source.is_executable('file1'))
 
2180
 
 
2181
    def install_rot13_content_filter(self, pattern):
 
2182
        # We could use
 
2183
        # self.addCleanup(filters._reset_registry, filters._reset_registry())
 
2184
        # below, but that looks a bit... hard to read even if it's exactly
 
2185
        # the same thing.
 
2186
        original_registry = filters._reset_registry()
 
2187
        def restore_registry():
 
2188
            filters._reset_registry(original_registry)
 
2189
        self.addCleanup(restore_registry)
 
2190
        def rot13(chunks, context=None):
 
2191
            return [b''.join(chunks).encode('rot13')]
 
2192
        rot13filter = filters.ContentFilter(rot13, rot13)
 
2193
        filters.filter_stacks_registry.register(
 
2194
            'rot13', {'yes': [rot13filter]}.get)
 
2195
        os.mkdir(self.test_home_dir + '/.bazaar')
 
2196
        rules_filename = self.test_home_dir + '/.bazaar/rules'
 
2197
        with open(rules_filename, 'wb') as f:
 
2198
            f.write(b'[name %s]\nrot13=yes\n' % (pattern,))
 
2199
        def uninstall_rules():
 
2200
            os.remove(rules_filename)
 
2201
            rules.reset_rules()
 
2202
        self.addCleanup(uninstall_rules)
 
2203
        rules.reset_rules()
 
2204
 
 
2205
    def test_build_tree_content_filtered_files_are_not_hardlinked(self):
 
2206
        """build_tree will not hardlink files that have content filtering rules
 
2207
        applied to them (but will still hardlink other files from the same tree
 
2208
        if it can).
 
2209
        """
 
2210
        self.requireFeature(HardlinkFeature)
 
2211
        self.install_rot13_content_filter(b'file1')
 
2212
        source = self.create_ab_tree()
 
2213
        target = self.make_branch_and_tree('target')
 
2214
        revision_tree = source.basis_tree()
 
2215
        revision_tree.lock_read()
 
2216
        self.addCleanup(revision_tree.unlock)
 
2217
        build_tree(revision_tree, target, source, hardlink=True)
 
2218
        target.lock_read()
 
2219
        self.addCleanup(target.unlock)
 
2220
        self.assertEqual([], list(target.iter_changes(revision_tree)))
 
2221
        source_stat = os.stat('source/file1')
 
2222
        target_stat = os.stat('target/file1')
 
2223
        self.assertNotEqual(source_stat, target_stat)
 
2224
        source_stat = os.stat('source/file2')
 
2225
        target_stat = os.stat('target/file2')
 
2226
        self.assertEqualStat(source_stat, target_stat)
 
2227
 
 
2228
    def test_case_insensitive_build_tree_inventory(self):
 
2229
        if (features.CaseInsensitiveFilesystemFeature.available()
 
2230
            or features.CaseInsCasePresFilenameFeature.available()):
 
2231
            raise tests.UnavailableFeature('Fully case sensitive filesystem')
 
2232
        source = self.make_branch_and_tree('source')
 
2233
        self.build_tree(['source/file', 'source/FILE'])
 
2234
        source.add(['file', 'FILE'], [b'lower-id', b'upper-id'])
 
2235
        source.commit('added files')
 
2236
        # Don't try this at home, kids!
 
2237
        # Force the tree to report that it is case insensitive
 
2238
        target = self.make_branch_and_tree('target')
 
2239
        target.case_sensitive = False
 
2240
        build_tree(source.basis_tree(), target, source, delta_from_tree=True)
 
2241
        self.assertEqual('file.moved', target.id2path(b'lower-id'))
 
2242
        self.assertEqual('FILE', target.id2path(b'upper-id'))
 
2243
 
 
2244
    def test_build_tree_observes_sha(self):
 
2245
        source = self.make_branch_and_tree('source')
 
2246
        self.build_tree(['source/file1', 'source/dir/', 'source/dir/file2'])
 
2247
        source.add(['file1', 'dir', 'dir/file2'],
 
2248
                   [b'file1-id', b'dir-id', b'file2-id'])
 
2249
        source.commit('new files')
 
2250
        target = self.make_branch_and_tree('target')
 
2251
        target.lock_write()
 
2252
        self.addCleanup(target.unlock)
 
2253
        # We make use of the fact that DirState caches its cutoff time. So we
 
2254
        # set the 'safe' time to one minute in the future.
 
2255
        state = target.current_dirstate()
 
2256
        state._cutoff_time = time.time() + 60
 
2257
        build_tree(source.basis_tree(), target)
 
2258
        entry1_sha = osutils.sha_file_by_name('source/file1')
 
2259
        entry2_sha = osutils.sha_file_by_name('source/dir/file2')
 
2260
        # entry[1] is the state information, entry[1][0] is the state of the
 
2261
        # working tree, entry[1][0][1] is the sha value for the current working
 
2262
        # tree
 
2263
        entry1 = state._get_entry(0, path_utf8=b'file1')
 
2264
        self.assertEqual(entry1_sha, entry1[1][0][1])
 
2265
        # The 'size' field must also be set.
 
2266
        self.assertEqual(25, entry1[1][0][2])
 
2267
        entry1_state = entry1[1][0]
 
2268
        entry2 = state._get_entry(0, path_utf8=b'dir/file2')
 
2269
        self.assertEqual(entry2_sha, entry2[1][0][1])
 
2270
        self.assertEqual(29, entry2[1][0][2])
 
2271
        entry2_state = entry2[1][0]
 
2272
        # Now, make sure that we don't have to re-read the content. The
 
2273
        # packed_stat should match exactly.
 
2274
        self.assertEqual(entry1_sha, target.get_file_sha1('file1', b'file1-id'))
 
2275
        self.assertEqual(entry2_sha,
 
2276
                         target.get_file_sha1('dir/file2', b'file2-id'))
 
2277
        self.assertEqual(entry1_state, entry1[1][0])
 
2278
        self.assertEqual(entry2_state, entry2[1][0])
 
2279
 
 
2280
 
 
2281
class TestCommitTransform(tests.TestCaseWithTransport):
 
2282
 
 
2283
    def get_branch(self):
 
2284
        tree = self.make_branch_and_tree('tree')
 
2285
        tree.lock_write()
 
2286
        self.addCleanup(tree.unlock)
 
2287
        tree.commit('empty commit')
 
2288
        return tree.branch
 
2289
 
 
2290
    def get_branch_and_transform(self):
 
2291
        branch = self.get_branch()
 
2292
        tt = TransformPreview(branch.basis_tree())
 
2293
        self.addCleanup(tt.finalize)
 
2294
        return branch, tt
 
2295
 
 
2296
    def test_commit_wrong_basis(self):
 
2297
        branch = self.get_branch()
 
2298
        basis = branch.repository.revision_tree(
 
2299
            _mod_revision.NULL_REVISION)
 
2300
        tt = TransformPreview(basis)
 
2301
        self.addCleanup(tt.finalize)
 
2302
        e = self.assertRaises(ValueError, tt.commit, branch, '')
 
2303
        self.assertEqual('TreeTransform not based on branch basis: null:',
 
2304
                         str(e))
 
2305
 
 
2306
    def test_empy_commit(self):
 
2307
        branch, tt = self.get_branch_and_transform()
 
2308
        rev = tt.commit(branch, 'my message')
 
2309
        self.assertEqual(2, branch.revno())
 
2310
        repo = branch.repository
 
2311
        self.assertEqual('my message', repo.get_revision(rev).message)
 
2312
 
 
2313
    def test_merge_parents(self):
 
2314
        branch, tt = self.get_branch_and_transform()
 
2315
        rev = tt.commit(branch, 'my message', [b'rev1b', b'rev1c'])
 
2316
        self.assertEqual([b'rev1b', b'rev1c'],
 
2317
                         branch.basis_tree().get_parent_ids()[1:])
 
2318
 
 
2319
    def test_first_commit(self):
 
2320
        branch = self.make_branch('branch')
 
2321
        branch.lock_write()
 
2322
        self.addCleanup(branch.unlock)
 
2323
        tt = TransformPreview(branch.basis_tree())
 
2324
        self.addCleanup(tt.finalize)
 
2325
        tt.new_directory('', ROOT_PARENT, b'TREE_ROOT')
 
2326
        rev = tt.commit(branch, 'my message')
 
2327
        self.assertEqual([], branch.basis_tree().get_parent_ids())
 
2328
        self.assertNotEqual(_mod_revision.NULL_REVISION,
 
2329
                            branch.last_revision())
 
2330
 
 
2331
    def test_first_commit_with_merge_parents(self):
 
2332
        branch = self.make_branch('branch')
 
2333
        branch.lock_write()
 
2334
        self.addCleanup(branch.unlock)
 
2335
        tt = TransformPreview(branch.basis_tree())
 
2336
        self.addCleanup(tt.finalize)
 
2337
        e = self.assertRaises(ValueError, tt.commit, branch,
 
2338
                          'my message', [b'rev1b-id'])
 
2339
        self.assertEqual('Cannot supply merge parents for first commit.',
 
2340
                         str(e))
 
2341
        self.assertEqual(_mod_revision.NULL_REVISION, branch.last_revision())
 
2342
 
 
2343
    def test_add_files(self):
 
2344
        branch, tt = self.get_branch_and_transform()
 
2345
        tt.new_file('file', tt.root, [b'contents'], b'file-id')
 
2346
        trans_id = tt.new_directory('dir', tt.root, b'dir-id')
 
2347
        if SymlinkFeature.available():
 
2348
            tt.new_symlink('symlink', trans_id, 'target', b'symlink-id')
 
2349
        rev = tt.commit(branch, 'message')
 
2350
        tree = branch.basis_tree()
 
2351
        self.assertEqual('file', tree.id2path(b'file-id'))
 
2352
        self.assertEqual(b'contents', tree.get_file_text('file', b'file-id'))
 
2353
        self.assertEqual('dir', tree.id2path(b'dir-id'))
 
2354
        if SymlinkFeature.available():
 
2355
            self.assertEqual('dir/symlink', tree.id2path(b'symlink-id'))
 
2356
            self.assertEqual('target', tree.get_symlink_target('dir/symlink'))
 
2357
 
 
2358
    def test_add_unversioned(self):
 
2359
        branch, tt = self.get_branch_and_transform()
 
2360
        tt.new_file('file', tt.root, [b'contents'])
 
2361
        self.assertRaises(errors.StrictCommitFailed, tt.commit, branch,
 
2362
                          'message', strict=True)
 
2363
 
 
2364
    def test_modify_strict(self):
 
2365
        branch, tt = self.get_branch_and_transform()
 
2366
        tt.new_file('file', tt.root, [b'contents'], b'file-id')
 
2367
        tt.commit(branch, 'message', strict=True)
 
2368
        tt = TransformPreview(branch.basis_tree())
 
2369
        self.addCleanup(tt.finalize)
 
2370
        trans_id = tt.trans_id_file_id(b'file-id')
 
2371
        tt.delete_contents(trans_id)
 
2372
        tt.create_file([b'contents'], trans_id)
 
2373
        tt.commit(branch, 'message', strict=True)
 
2374
 
 
2375
    def test_commit_malformed(self):
 
2376
        """Committing a malformed transform should raise an exception.
 
2377
 
 
2378
        In this case, we are adding a file without adding its parent.
 
2379
        """
 
2380
        branch, tt = self.get_branch_and_transform()
 
2381
        parent_id = tt.trans_id_file_id(b'parent-id')
 
2382
        tt.new_file('file', parent_id, [b'contents'], b'file-id')
 
2383
        self.assertRaises(errors.MalformedTransform, tt.commit, branch,
 
2384
                          'message')
 
2385
 
 
2386
    def test_commit_rich_revision_data(self):
 
2387
        branch, tt = self.get_branch_and_transform()
 
2388
        rev_id = tt.commit(branch, 'message', timestamp=1, timezone=43201,
 
2389
                           committer='me <me@example.com>',
 
2390
                           revprops={u'foo': 'bar'}, revision_id=b'revid-1',
 
2391
                           authors=['Author1 <author1@example.com>',
 
2392
                              'Author2 <author2@example.com>',
 
2393
                               ])
 
2394
        self.assertEqual(b'revid-1', rev_id)
 
2395
        revision = branch.repository.get_revision(rev_id)
 
2396
        self.assertEqual(1, revision.timestamp)
 
2397
        self.assertEqual(43201, revision.timezone)
 
2398
        self.assertEqual('me <me@example.com>', revision.committer)
 
2399
        self.assertEqual(['Author1 <author1@example.com>',
 
2400
                          'Author2 <author2@example.com>'],
 
2401
                         revision.get_apparent_authors())
 
2402
        del revision.properties['authors']
 
2403
        self.assertEqual({'foo': 'bar',
 
2404
                          'branch-nick': 'tree'},
 
2405
                         revision.properties)
 
2406
 
 
2407
    def test_no_explicit_revprops(self):
 
2408
        branch, tt = self.get_branch_and_transform()
 
2409
        rev_id = tt.commit(branch, 'message', authors=[
 
2410
            'Author1 <author1@example.com>',
 
2411
            'Author2 <author2@example.com>', ])
 
2412
        revision = branch.repository.get_revision(rev_id)
 
2413
        self.assertEqual(['Author1 <author1@example.com>',
 
2414
                          'Author2 <author2@example.com>'],
 
2415
                         revision.get_apparent_authors())
 
2416
        self.assertEqual('tree', revision.properties['branch-nick'])
 
2417
 
 
2418
 
 
2419
class TestFileMover(tests.TestCaseWithTransport):
 
2420
 
 
2421
    def test_file_mover(self):
 
2422
        self.build_tree(['a/', 'a/b', 'c/', 'c/d'])
 
2423
        mover = _FileMover()
 
2424
        mover.rename('a', 'q')
 
2425
        self.assertPathExists('q')
 
2426
        self.assertPathDoesNotExist('a')
 
2427
        self.assertPathExists('q/b')
 
2428
        self.assertPathExists('c')
 
2429
        self.assertPathExists('c/d')
 
2430
 
 
2431
    def test_pre_delete_rollback(self):
 
2432
        self.build_tree(['a/'])
 
2433
        mover = _FileMover()
 
2434
        mover.pre_delete('a', 'q')
 
2435
        self.assertPathExists('q')
 
2436
        self.assertPathDoesNotExist('a')
 
2437
        mover.rollback()
 
2438
        self.assertPathDoesNotExist('q')
 
2439
        self.assertPathExists('a')
 
2440
 
 
2441
    def test_apply_deletions(self):
 
2442
        self.build_tree(['a/', 'b/'])
 
2443
        mover = _FileMover()
 
2444
        mover.pre_delete('a', 'q')
 
2445
        mover.pre_delete('b', 'r')
 
2446
        self.assertPathExists('q')
 
2447
        self.assertPathExists('r')
 
2448
        self.assertPathDoesNotExist('a')
 
2449
        self.assertPathDoesNotExist('b')
 
2450
        mover.apply_deletions()
 
2451
        self.assertPathDoesNotExist('q')
 
2452
        self.assertPathDoesNotExist('r')
 
2453
        self.assertPathDoesNotExist('a')
 
2454
        self.assertPathDoesNotExist('b')
 
2455
 
 
2456
    def test_file_mover_rollback(self):
 
2457
        self.build_tree(['a/', 'a/b', 'c/', 'c/d/', 'c/e/'])
 
2458
        mover = _FileMover()
 
2459
        mover.rename('c/d', 'c/f')
 
2460
        mover.rename('c/e', 'c/d')
 
2461
        try:
 
2462
            mover.rename('a', 'c')
 
2463
        except errors.FileExists as e:
 
2464
            mover.rollback()
 
2465
        self.assertPathExists('a')
 
2466
        self.assertPathExists('c/d')
 
2467
 
 
2468
 
 
2469
class Bogus(Exception):
 
2470
    pass
 
2471
 
 
2472
 
 
2473
class TestTransformRollback(tests.TestCaseWithTransport):
 
2474
 
 
2475
    class ExceptionFileMover(_FileMover):
 
2476
 
 
2477
        def __init__(self, bad_source=None, bad_target=None):
 
2478
            _FileMover.__init__(self)
 
2479
            self.bad_source = bad_source
 
2480
            self.bad_target = bad_target
 
2481
 
 
2482
        def rename(self, source, target):
 
2483
            if (self.bad_source is not None and
 
2484
                source.endswith(self.bad_source)):
 
2485
                raise Bogus
 
2486
            elif (self.bad_target is not None and
 
2487
                target.endswith(self.bad_target)):
 
2488
                raise Bogus
 
2489
            else:
 
2490
                _FileMover.rename(self, source, target)
 
2491
 
 
2492
    def test_rollback_rename(self):
 
2493
        tree = self.make_branch_and_tree('.')
 
2494
        self.build_tree(['a/', 'a/b'])
 
2495
        tt = TreeTransform(tree)
 
2496
        self.addCleanup(tt.finalize)
 
2497
        a_id = tt.trans_id_tree_path('a')
 
2498
        tt.adjust_path('c', tt.root, a_id)
 
2499
        tt.adjust_path('d', a_id, tt.trans_id_tree_path('a/b'))
 
2500
        self.assertRaises(Bogus, tt.apply,
 
2501
                          _mover=self.ExceptionFileMover(bad_source='a'))
 
2502
        self.assertPathExists('a')
 
2503
        self.assertPathExists('a/b')
 
2504
        tt.apply()
 
2505
        self.assertPathExists('c')
 
2506
        self.assertPathExists('c/d')
 
2507
 
 
2508
    def test_rollback_rename_into_place(self):
 
2509
        tree = self.make_branch_and_tree('.')
 
2510
        self.build_tree(['a/', 'a/b'])
 
2511
        tt = TreeTransform(tree)
 
2512
        self.addCleanup(tt.finalize)
 
2513
        a_id = tt.trans_id_tree_path('a')
 
2514
        tt.adjust_path('c', tt.root, a_id)
 
2515
        tt.adjust_path('d', a_id, tt.trans_id_tree_path('a/b'))
 
2516
        self.assertRaises(Bogus, tt.apply,
 
2517
                          _mover=self.ExceptionFileMover(bad_target='c/d'))
 
2518
        self.assertPathExists('a')
 
2519
        self.assertPathExists('a/b')
 
2520
        tt.apply()
 
2521
        self.assertPathExists('c')
 
2522
        self.assertPathExists('c/d')
 
2523
 
 
2524
    def test_rollback_deletion(self):
 
2525
        tree = self.make_branch_and_tree('.')
 
2526
        self.build_tree(['a/', 'a/b'])
 
2527
        tt = TreeTransform(tree)
 
2528
        self.addCleanup(tt.finalize)
 
2529
        a_id = tt.trans_id_tree_path('a')
 
2530
        tt.delete_contents(a_id)
 
2531
        tt.adjust_path('d', tt.root, tt.trans_id_tree_path('a/b'))
 
2532
        self.assertRaises(Bogus, tt.apply,
 
2533
                          _mover=self.ExceptionFileMover(bad_target='d'))
 
2534
        self.assertPathExists('a')
 
2535
        self.assertPathExists('a/b')
 
2536
 
 
2537
 
 
2538
class TestFinalizeRobustness(tests.TestCaseWithTransport):
 
2539
    """Ensure treetransform creation errors can be safely cleaned up after"""
 
2540
 
 
2541
    def _override_globals_in_method(self, instance, method_name, globals):
 
2542
        """Replace method on instance with one with updated globals"""
 
2543
        import types
 
2544
        func = getattr(instance, method_name).__func__
 
2545
        new_globals = dict(func.__globals__)
 
2546
        new_globals.update(globals)
 
2547
        new_func = types.FunctionType(func.__code__, new_globals,
 
2548
            func.__name__, func.__defaults__)
 
2549
        setattr(instance, method_name,
 
2550
            types.MethodType(new_func, instance, instance.__class__))
 
2551
        self.addCleanup(delattr, instance, method_name)
 
2552
 
 
2553
    @staticmethod
 
2554
    def _fake_open_raises_before(name, mode):
 
2555
        """Like open() but raises before doing anything"""
 
2556
        raise RuntimeError
 
2557
 
 
2558
    @staticmethod
 
2559
    def _fake_open_raises_after(name, mode):
 
2560
        """Like open() but raises after creating file without returning"""
 
2561
        open(name, mode).close()
 
2562
        raise RuntimeError
 
2563
 
 
2564
    def create_transform_and_root_trans_id(self):
 
2565
        """Setup a transform creating a file in limbo"""
 
2566
        tree = self.make_branch_and_tree('.')
 
2567
        tt = TreeTransform(tree)
 
2568
        return tt, tt.create_path("a", tt.root)
 
2569
 
 
2570
    def create_transform_and_subdir_trans_id(self):
 
2571
        """Setup a transform creating a directory containing a file in limbo"""
 
2572
        tree = self.make_branch_and_tree('.')
 
2573
        tt = TreeTransform(tree)
 
2574
        d_trans_id = tt.create_path("d", tt.root)
 
2575
        tt.create_directory(d_trans_id)
 
2576
        f_trans_id = tt.create_path("a", d_trans_id)
 
2577
        tt.adjust_path("a", d_trans_id, f_trans_id)
 
2578
        return tt, f_trans_id
 
2579
 
 
2580
    def test_root_create_file_open_raises_before_creation(self):
 
2581
        tt, trans_id = self.create_transform_and_root_trans_id()
 
2582
        self._override_globals_in_method(tt, "create_file",
 
2583
            {"open": self._fake_open_raises_before})
 
2584
        self.assertRaises(RuntimeError, tt.create_file, [b"contents"], trans_id)
 
2585
        path = tt._limbo_name(trans_id)
 
2586
        self.assertPathDoesNotExist(path)
 
2587
        tt.finalize()
 
2588
        self.assertPathDoesNotExist(tt._limbodir)
 
2589
 
 
2590
    def test_root_create_file_open_raises_after_creation(self):
 
2591
        tt, trans_id = self.create_transform_and_root_trans_id()
 
2592
        self._override_globals_in_method(tt, "create_file",
 
2593
            {"open": self._fake_open_raises_after})
 
2594
        self.assertRaises(RuntimeError, tt.create_file, [b"contents"], trans_id)
 
2595
        path = tt._limbo_name(trans_id)
 
2596
        self.assertPathExists(path)
 
2597
        tt.finalize()
 
2598
        self.assertPathDoesNotExist(path)
 
2599
        self.assertPathDoesNotExist(tt._limbodir)
 
2600
 
 
2601
    def test_subdir_create_file_open_raises_before_creation(self):
 
2602
        tt, trans_id = self.create_transform_and_subdir_trans_id()
 
2603
        self._override_globals_in_method(tt, "create_file",
 
2604
            {"open": self._fake_open_raises_before})
 
2605
        self.assertRaises(RuntimeError, tt.create_file, [b"contents"], trans_id)
 
2606
        path = tt._limbo_name(trans_id)
 
2607
        self.assertPathDoesNotExist(path)
 
2608
        tt.finalize()
 
2609
        self.assertPathDoesNotExist(tt._limbodir)
 
2610
 
 
2611
    def test_subdir_create_file_open_raises_after_creation(self):
 
2612
        tt, trans_id = self.create_transform_and_subdir_trans_id()
 
2613
        self._override_globals_in_method(tt, "create_file",
 
2614
            {"open": self._fake_open_raises_after})
 
2615
        self.assertRaises(RuntimeError, tt.create_file, [b"contents"], trans_id)
 
2616
        path = tt._limbo_name(trans_id)
 
2617
        self.assertPathExists(path)
 
2618
        tt.finalize()
 
2619
        self.assertPathDoesNotExist(path)
 
2620
        self.assertPathDoesNotExist(tt._limbodir)
 
2621
 
 
2622
    def test_rename_in_limbo_rename_raises_after_rename(self):
 
2623
        tt, trans_id = self.create_transform_and_root_trans_id()
 
2624
        parent1 = tt.new_directory('parent1', tt.root)
 
2625
        child1 = tt.new_file('child1', parent1, [b'contents'])
 
2626
        parent2 = tt.new_directory('parent2', tt.root)
 
2627
 
 
2628
        class FakeOSModule(object):
 
2629
            def rename(self, old, new):
 
2630
                os.rename(old, new)
 
2631
                raise RuntimeError
 
2632
        self._override_globals_in_method(tt, "_rename_in_limbo",
 
2633
            {"os": FakeOSModule()})
 
2634
        self.assertRaises(
 
2635
            RuntimeError, tt.adjust_path, "child1", parent2, child1)
 
2636
        path = osutils.pathjoin(tt._limbo_name(parent2), "child1")
 
2637
        self.assertPathExists(path)
 
2638
        tt.finalize()
 
2639
        self.assertPathDoesNotExist(path)
 
2640
        self.assertPathDoesNotExist(tt._limbodir)
 
2641
 
 
2642
    def test_rename_in_limbo_rename_raises_before_rename(self):
 
2643
        tt, trans_id = self.create_transform_and_root_trans_id()
 
2644
        parent1 = tt.new_directory('parent1', tt.root)
 
2645
        child1 = tt.new_file('child1', parent1, [b'contents'])
 
2646
        parent2 = tt.new_directory('parent2', tt.root)
 
2647
 
 
2648
        class FakeOSModule(object):
 
2649
            def rename(self, old, new):
 
2650
                raise RuntimeError
 
2651
        self._override_globals_in_method(tt, "_rename_in_limbo",
 
2652
            {"os": FakeOSModule()})
 
2653
        self.assertRaises(
 
2654
            RuntimeError, tt.adjust_path, "child1", parent2, child1)
 
2655
        path = osutils.pathjoin(tt._limbo_name(parent1), "child1")
 
2656
        self.assertPathExists(path)
 
2657
        tt.finalize()
 
2658
        self.assertPathDoesNotExist(path)
 
2659
        self.assertPathDoesNotExist(tt._limbodir)
 
2660
 
 
2661
 
 
2662
class TestTransformMissingParent(tests.TestCaseWithTransport):
 
2663
 
 
2664
    def make_tt_with_versioned_dir(self):
 
2665
        wt = self.make_branch_and_tree('.')
 
2666
        self.build_tree(['dir/',])
 
2667
        wt.add(['dir'], [b'dir-id'])
 
2668
        wt.commit('Create dir')
 
2669
        tt = TreeTransform(wt)
 
2670
        self.addCleanup(tt.finalize)
 
2671
        return wt, tt
 
2672
 
 
2673
    def test_resolve_create_parent_for_versioned_file(self):
 
2674
        wt, tt = self.make_tt_with_versioned_dir()
 
2675
        dir_tid = tt.trans_id_tree_path('dir')
 
2676
        file_tid = tt.new_file('file', dir_tid, [b'Contents'], file_id=b'file-id')
 
2677
        tt.delete_contents(dir_tid)
 
2678
        tt.unversion_file(dir_tid)
 
2679
        conflicts = resolve_conflicts(tt)
 
2680
        # one conflict for the missing directory, one for the unversioned
 
2681
        # parent
 
2682
        self.assertLength(2, conflicts)
 
2683
 
 
2684
    def test_non_versioned_file_create_conflict(self):
 
2685
        wt, tt = self.make_tt_with_versioned_dir()
 
2686
        dir_tid = tt.trans_id_tree_path('dir')
 
2687
        tt.new_file('file', dir_tid, [b'Contents'])
 
2688
        tt.delete_contents(dir_tid)
 
2689
        tt.unversion_file(dir_tid)
 
2690
        conflicts = resolve_conflicts(tt)
 
2691
        # no conflicts or rather: orphaning 'file' resolve the 'dir' conflict
 
2692
        self.assertLength(1, conflicts)
 
2693
        self.assertEqual(('deleting parent', 'Not deleting', 'new-1'),
 
2694
                         conflicts.pop())
 
2695
 
 
2696
 
 
2697
A_ENTRY = (b'a-id', ('a', 'a'), True, (True, True),
 
2698
                  (b'TREE_ROOT', b'TREE_ROOT'), ('a', 'a'), ('file', 'file'),
 
2699
                  (False, False))
 
2700
ROOT_ENTRY = (b'TREE_ROOT', ('', ''), False, (True, True), (None, None),
 
2701
              ('', ''), ('directory', 'directory'), (False, False))
 
2702
 
 
2703
 
 
2704
class TestTransformPreview(tests.TestCaseWithTransport):
 
2705
 
 
2706
    def create_tree(self):
 
2707
        tree = self.make_branch_and_tree('.')
 
2708
        self.build_tree_contents([('a', b'content 1')])
 
2709
        tree.set_root_id(b'TREE_ROOT')
 
2710
        tree.add('a', b'a-id')
 
2711
        tree.commit('rev1', rev_id=b'rev1')
 
2712
        return tree.branch.repository.revision_tree(b'rev1')
 
2713
 
 
2714
    def get_empty_preview(self):
 
2715
        repository = self.make_repository('repo')
 
2716
        tree = repository.revision_tree(_mod_revision.NULL_REVISION)
 
2717
        preview = TransformPreview(tree)
 
2718
        self.addCleanup(preview.finalize)
 
2719
        return preview
 
2720
 
 
2721
    def test_transform_preview(self):
 
2722
        revision_tree = self.create_tree()
 
2723
        preview = TransformPreview(revision_tree)
 
2724
        self.addCleanup(preview.finalize)
 
2725
 
 
2726
    def test_transform_preview_tree(self):
 
2727
        revision_tree = self.create_tree()
 
2728
        preview = TransformPreview(revision_tree)
 
2729
        self.addCleanup(preview.finalize)
 
2730
        preview.get_preview_tree()
 
2731
 
 
2732
    def test_transform_new_file(self):
 
2733
        revision_tree = self.create_tree()
 
2734
        preview = TransformPreview(revision_tree)
 
2735
        self.addCleanup(preview.finalize)
 
2736
        preview.new_file('file2', preview.root, [b'content B\n'], b'file2-id')
 
2737
        preview_tree = preview.get_preview_tree()
 
2738
        self.assertEqual(preview_tree.kind('file2'), 'file')
 
2739
        with preview_tree.get_file('file2', b'file2-id') as f:
 
2740
            self.assertEqual(f.read(), b'content B\n')
 
2741
 
 
2742
    def test_diff_preview_tree(self):
 
2743
        revision_tree = self.create_tree()
 
2744
        preview = TransformPreview(revision_tree)
 
2745
        self.addCleanup(preview.finalize)
 
2746
        preview.new_file('file2', preview.root, [b'content B\n'], b'file2-id')
 
2747
        preview_tree = preview.get_preview_tree()
 
2748
        out = BytesIO()
 
2749
        show_diff_trees(revision_tree, preview_tree, out)
 
2750
        lines = out.getvalue().splitlines()
 
2751
        self.assertEqual(lines[0], b"=== added file 'file2'")
 
2752
        # 3 lines of diff administrivia
 
2753
        self.assertEqual(lines[4], b"+content B")
 
2754
 
 
2755
    def test_transform_conflicts(self):
 
2756
        revision_tree = self.create_tree()
 
2757
        preview = TransformPreview(revision_tree)
 
2758
        self.addCleanup(preview.finalize)
 
2759
        preview.new_file('a', preview.root, [b'content 2'])
 
2760
        resolve_conflicts(preview)
 
2761
        trans_id = preview.trans_id_file_id(b'a-id')
 
2762
        self.assertEqual('a.moved', preview.final_name(trans_id))
 
2763
 
 
2764
    def get_tree_and_preview_tree(self):
 
2765
        revision_tree = self.create_tree()
 
2766
        preview = TransformPreview(revision_tree)
 
2767
        self.addCleanup(preview.finalize)
 
2768
        a_trans_id = preview.trans_id_file_id(b'a-id')
 
2769
        preview.delete_contents(a_trans_id)
 
2770
        preview.create_file([b'b content'], a_trans_id)
 
2771
        preview_tree = preview.get_preview_tree()
 
2772
        return revision_tree, preview_tree
 
2773
 
 
2774
    def test_iter_changes(self):
 
2775
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2776
        root = revision_tree.get_root_id()
 
2777
        self.assertEqual([(b'a-id', ('a', 'a'), True, (True, True),
 
2778
                          (root, root), ('a', 'a'), ('file', 'file'),
 
2779
                          (False, False))],
 
2780
                          list(preview_tree.iter_changes(revision_tree)))
 
2781
 
 
2782
    def test_include_unchanged_succeeds(self):
 
2783
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2784
        changes = preview_tree.iter_changes(revision_tree,
 
2785
                                            include_unchanged=True)
 
2786
        root = revision_tree.get_root_id()
 
2787
 
 
2788
        self.assertEqual([ROOT_ENTRY, A_ENTRY], list(changes))
 
2789
 
 
2790
    def test_specific_files(self):
 
2791
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2792
        changes = preview_tree.iter_changes(revision_tree,
 
2793
                                            specific_files=[''])
 
2794
        self.assertEqual([A_ENTRY], list(changes))
 
2795
 
 
2796
    def test_want_unversioned(self):
 
2797
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2798
        changes = preview_tree.iter_changes(revision_tree,
 
2799
                                            want_unversioned=True)
 
2800
        self.assertEqual([A_ENTRY], list(changes))
 
2801
 
 
2802
    def test_ignore_extra_trees_no_specific_files(self):
 
2803
        # extra_trees is harmless without specific_files, so we'll silently
 
2804
        # accept it, even though we won't use it.
 
2805
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2806
        preview_tree.iter_changes(revision_tree, extra_trees=[preview_tree])
 
2807
 
 
2808
    def test_ignore_require_versioned_no_specific_files(self):
 
2809
        # require_versioned is meaningless without specific_files.
 
2810
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2811
        preview_tree.iter_changes(revision_tree, require_versioned=False)
 
2812
 
 
2813
    def test_ignore_pb(self):
 
2814
        # pb could be supported, but TT.iter_changes doesn't support it.
 
2815
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
2816
        preview_tree.iter_changes(revision_tree)
 
2817
 
 
2818
    def test_kind(self):
 
2819
        revision_tree = self.create_tree()
 
2820
        preview = TransformPreview(revision_tree)
 
2821
        self.addCleanup(preview.finalize)
 
2822
        preview.new_file('file', preview.root, [b'contents'], b'file-id')
 
2823
        preview.new_directory('directory', preview.root, b'dir-id')
 
2824
        preview_tree = preview.get_preview_tree()
 
2825
        self.assertEqual('file', preview_tree.kind('file'))
 
2826
        self.assertEqual('directory', preview_tree.kind('directory'))
 
2827
 
 
2828
    def test_get_file_mtime(self):
 
2829
        preview = self.get_empty_preview()
 
2830
        file_trans_id = preview.new_file('file', preview.root, [b'contents'],
 
2831
                                         b'file-id')
 
2832
        limbo_path = preview._limbo_name(file_trans_id)
 
2833
        preview_tree = preview.get_preview_tree()
 
2834
        self.assertEqual(os.stat(limbo_path).st_mtime,
 
2835
                         preview_tree.get_file_mtime('file', b'file-id'))
 
2836
 
 
2837
    def test_get_file_mtime_renamed(self):
 
2838
        work_tree = self.make_branch_and_tree('tree')
 
2839
        self.build_tree(['tree/file'])
 
2840
        work_tree.add('file', b'file-id')
 
2841
        preview = TransformPreview(work_tree)
 
2842
        self.addCleanup(preview.finalize)
 
2843
        file_trans_id = preview.trans_id_tree_path('file')
 
2844
        preview.adjust_path('renamed', preview.root, file_trans_id)
 
2845
        preview_tree = preview.get_preview_tree()
 
2846
        preview_mtime = preview_tree.get_file_mtime('renamed', b'file-id')
 
2847
        work_mtime = work_tree.get_file_mtime('file', b'file-id')
 
2848
 
 
2849
    def test_get_file_size(self):
 
2850
        work_tree = self.make_branch_and_tree('tree')
 
2851
        self.build_tree_contents([('tree/old', b'old')])
 
2852
        work_tree.add('old', b'old-id')
 
2853
        preview = TransformPreview(work_tree)
 
2854
        self.addCleanup(preview.finalize)
 
2855
        new_id = preview.new_file('name', preview.root, [b'contents'], b'new-id',
 
2856
                                  'executable')
 
2857
        tree = preview.get_preview_tree()
 
2858
        self.assertEqual(len('old'), tree.get_file_size('old'))
 
2859
        self.assertEqual(len('contents'), tree.get_file_size('name'))
 
2860
 
 
2861
    def test_get_file(self):
 
2862
        preview = self.get_empty_preview()
 
2863
        preview.new_file('file', preview.root, [b'contents'], b'file-id')
 
2864
        preview_tree = preview.get_preview_tree()
 
2865
        with preview_tree.get_file('file') as tree_file:
 
2866
            self.assertEqual(b'contents', tree_file.read())
 
2867
 
 
2868
    def test_get_symlink_target(self):
 
2869
        self.requireFeature(SymlinkFeature)
 
2870
        preview = self.get_empty_preview()
 
2871
        preview.new_symlink('symlink', preview.root, 'target', b'symlink-id')
 
2872
        preview_tree = preview.get_preview_tree()
 
2873
        self.assertEqual('target',
 
2874
                         preview_tree.get_symlink_target('symlink'))
 
2875
 
 
2876
    def test_all_file_ids(self):
 
2877
        tree = self.make_branch_and_tree('tree')
 
2878
        self.build_tree(['tree/a', 'tree/b', 'tree/c'])
 
2879
        tree.add(['a', 'b', 'c'], [b'a-id', b'b-id', b'c-id'])
 
2880
        preview = TransformPreview(tree)
 
2881
        self.addCleanup(preview.finalize)
 
2882
        preview.unversion_file(preview.trans_id_file_id(b'b-id'))
 
2883
        c_trans_id = preview.trans_id_file_id(b'c-id')
 
2884
        preview.unversion_file(c_trans_id)
 
2885
        preview.version_file(b'c-id', c_trans_id)
 
2886
        preview_tree = preview.get_preview_tree()
 
2887
        self.assertEqual({b'a-id', b'c-id', tree.get_root_id()},
 
2888
                         preview_tree.all_file_ids())
 
2889
 
 
2890
    def test_path2id_deleted_unchanged(self):
 
2891
        tree = self.make_branch_and_tree('tree')
 
2892
        self.build_tree(['tree/unchanged', 'tree/deleted'])
 
2893
        tree.add(['unchanged', 'deleted'], [b'unchanged-id', b'deleted-id'])
 
2894
        preview = TransformPreview(tree)
 
2895
        self.addCleanup(preview.finalize)
 
2896
        preview.unversion_file(preview.trans_id_file_id(b'deleted-id'))
 
2897
        preview_tree = preview.get_preview_tree()
 
2898
        self.assertEqual(b'unchanged-id', preview_tree.path2id('unchanged'))
 
2899
        self.assertFalse(preview_tree.is_versioned('deleted'))
 
2900
 
 
2901
    def test_path2id_created(self):
 
2902
        tree = self.make_branch_and_tree('tree')
 
2903
        self.build_tree(['tree/unchanged'])
 
2904
        tree.add(['unchanged'], [b'unchanged-id'])
 
2905
        preview = TransformPreview(tree)
 
2906
        self.addCleanup(preview.finalize)
 
2907
        preview.new_file('new', preview.trans_id_file_id(b'unchanged-id'),
 
2908
            [b'contents'], b'new-id')
 
2909
        preview_tree = preview.get_preview_tree()
 
2910
        self.assertEqual(b'new-id', preview_tree.path2id('unchanged/new'))
 
2911
 
 
2912
    def test_path2id_moved(self):
 
2913
        tree = self.make_branch_and_tree('tree')
 
2914
        self.build_tree(['tree/old_parent/', 'tree/old_parent/child'])
 
2915
        tree.add(['old_parent', 'old_parent/child'],
 
2916
                 [b'old_parent-id', b'child-id'])
 
2917
        preview = TransformPreview(tree)
 
2918
        self.addCleanup(preview.finalize)
 
2919
        new_parent = preview.new_directory('new_parent', preview.root,
 
2920
                                           b'new_parent-id')
 
2921
        preview.adjust_path('child', new_parent,
 
2922
                            preview.trans_id_file_id(b'child-id'))
 
2923
        preview_tree = preview.get_preview_tree()
 
2924
        self.assertFalse(preview_tree.is_versioned('old_parent/child'))
 
2925
        self.assertEqual(b'child-id', preview_tree.path2id('new_parent/child'))
 
2926
 
 
2927
    def test_path2id_renamed_parent(self):
 
2928
        tree = self.make_branch_and_tree('tree')
 
2929
        self.build_tree(['tree/old_name/', 'tree/old_name/child'])
 
2930
        tree.add(['old_name', 'old_name/child'],
 
2931
                 [b'parent-id', b'child-id'])
 
2932
        preview = TransformPreview(tree)
 
2933
        self.addCleanup(preview.finalize)
 
2934
        preview.adjust_path('new_name', preview.root,
 
2935
                            preview.trans_id_file_id(b'parent-id'))
 
2936
        preview_tree = preview.get_preview_tree()
 
2937
        self.assertFalse(preview_tree.is_versioned('old_name/child'))
 
2938
        self.assertEqual(b'child-id', preview_tree.path2id('new_name/child'))
 
2939
 
 
2940
    def assertMatchingIterEntries(self, tt, specific_files=None):
 
2941
        preview_tree = tt.get_preview_tree()
 
2942
        preview_result = list(preview_tree.iter_entries_by_dir(
 
2943
                              specific_files=specific_files))
 
2944
        tree = tt._tree
 
2945
        tt.apply()
 
2946
        actual_result = list(tree.iter_entries_by_dir(
 
2947
            specific_files=specific_files))
 
2948
        self.assertEqual(actual_result, preview_result)
 
2949
 
 
2950
    def test_iter_entries_by_dir_new(self):
 
2951
        tree = self.make_branch_and_tree('tree')
 
2952
        tt = TreeTransform(tree)
 
2953
        tt.new_file('new', tt.root, [b'contents'], b'new-id')
 
2954
        self.assertMatchingIterEntries(tt)
 
2955
 
 
2956
    def test_iter_entries_by_dir_deleted(self):
 
2957
        tree = self.make_branch_and_tree('tree')
 
2958
        self.build_tree(['tree/deleted'])
 
2959
        tree.add('deleted', b'deleted-id')
 
2960
        tt = TreeTransform(tree)
 
2961
        tt.delete_contents(tt.trans_id_file_id(b'deleted-id'))
 
2962
        self.assertMatchingIterEntries(tt)
 
2963
 
 
2964
    def test_iter_entries_by_dir_unversioned(self):
 
2965
        tree = self.make_branch_and_tree('tree')
 
2966
        self.build_tree(['tree/removed'])
 
2967
        tree.add('removed', b'removed-id')
 
2968
        tt = TreeTransform(tree)
 
2969
        tt.unversion_file(tt.trans_id_file_id(b'removed-id'))
 
2970
        self.assertMatchingIterEntries(tt)
 
2971
 
 
2972
    def test_iter_entries_by_dir_moved(self):
 
2973
        tree = self.make_branch_and_tree('tree')
 
2974
        self.build_tree(['tree/moved', 'tree/new_parent/'])
 
2975
        tree.add(['moved', 'new_parent'], [b'moved-id', b'new_parent-id'])
 
2976
        tt = TreeTransform(tree)
 
2977
        tt.adjust_path('moved', tt.trans_id_file_id(b'new_parent-id'),
 
2978
                       tt.trans_id_file_id(b'moved-id'))
 
2979
        self.assertMatchingIterEntries(tt)
 
2980
 
 
2981
    def test_iter_entries_by_dir_specific_files(self):
 
2982
        tree = self.make_branch_and_tree('tree')
 
2983
        tree.set_root_id(b'tree-root-id')
 
2984
        self.build_tree(['tree/parent/', 'tree/parent/child'])
 
2985
        tree.add(['parent', 'parent/child'], [b'parent-id', b'child-id'])
 
2986
        tt = TreeTransform(tree)
 
2987
        self.assertMatchingIterEntries(tt, ['', 'parent/child'])
 
2988
 
 
2989
    def test_symlink_content_summary(self):
 
2990
        self.requireFeature(SymlinkFeature)
 
2991
        preview = self.get_empty_preview()
 
2992
        preview.new_symlink('path', preview.root, 'target', b'path-id')
 
2993
        summary = preview.get_preview_tree().path_content_summary('path')
 
2994
        self.assertEqual(('symlink', None, None, 'target'), summary)
 
2995
 
 
2996
    def test_missing_content_summary(self):
 
2997
        preview = self.get_empty_preview()
 
2998
        summary = preview.get_preview_tree().path_content_summary('path')
 
2999
        self.assertEqual(('missing', None, None, None), summary)
 
3000
 
 
3001
    def test_deleted_content_summary(self):
 
3002
        tree = self.make_branch_and_tree('tree')
 
3003
        self.build_tree(['tree/path/'])
 
3004
        tree.add('path')
 
3005
        preview = TransformPreview(tree)
 
3006
        self.addCleanup(preview.finalize)
 
3007
        preview.delete_contents(preview.trans_id_tree_path('path'))
 
3008
        summary = preview.get_preview_tree().path_content_summary('path')
 
3009
        self.assertEqual(('missing', None, None, None), summary)
 
3010
 
 
3011
    def test_file_content_summary_executable(self):
 
3012
        preview = self.get_empty_preview()
 
3013
        path_id = preview.new_file('path', preview.root, [b'contents'], b'path-id')
 
3014
        preview.set_executability(True, path_id)
 
3015
        summary = preview.get_preview_tree().path_content_summary('path')
 
3016
        self.assertEqual(4, len(summary))
 
3017
        self.assertEqual('file', summary[0])
 
3018
        # size must be known
 
3019
        self.assertEqual(len('contents'), summary[1])
 
3020
        # executable
 
3021
        self.assertEqual(True, summary[2])
 
3022
        # will not have hash (not cheap to determine)
 
3023
        self.assertIs(None, summary[3])
 
3024
 
 
3025
    def test_change_executability(self):
 
3026
        tree = self.make_branch_and_tree('tree')
 
3027
        self.build_tree(['tree/path'])
 
3028
        tree.add('path')
 
3029
        preview = TransformPreview(tree)
 
3030
        self.addCleanup(preview.finalize)
 
3031
        path_id = preview.trans_id_tree_path('path')
 
3032
        preview.set_executability(True, path_id)
 
3033
        summary = preview.get_preview_tree().path_content_summary('path')
 
3034
        self.assertEqual(True, summary[2])
 
3035
 
 
3036
    def test_file_content_summary_non_exec(self):
 
3037
        preview = self.get_empty_preview()
 
3038
        preview.new_file('path', preview.root, [b'contents'], b'path-id')
 
3039
        summary = preview.get_preview_tree().path_content_summary('path')
 
3040
        self.assertEqual(4, len(summary))
 
3041
        self.assertEqual('file', summary[0])
 
3042
        # size must be known
 
3043
        self.assertEqual(len('contents'), summary[1])
 
3044
        # not executable
 
3045
        self.assertEqual(False, summary[2])
 
3046
        # will not have hash (not cheap to determine)
 
3047
        self.assertIs(None, summary[3])
 
3048
 
 
3049
    def test_dir_content_summary(self):
 
3050
        preview = self.get_empty_preview()
 
3051
        preview.new_directory('path', preview.root, b'path-id')
 
3052
        summary = preview.get_preview_tree().path_content_summary('path')
 
3053
        self.assertEqual(('directory', None, None, None), summary)
 
3054
 
 
3055
    def test_tree_content_summary(self):
 
3056
        preview = self.get_empty_preview()
 
3057
        path = preview.new_directory('path', preview.root, b'path-id')
 
3058
        preview.set_tree_reference(b'rev-1', path)
 
3059
        summary = preview.get_preview_tree().path_content_summary('path')
 
3060
        self.assertEqual(4, len(summary))
 
3061
        self.assertEqual('tree-reference', summary[0])
 
3062
 
 
3063
    def test_annotate(self):
 
3064
        tree = self.make_branch_and_tree('tree')
 
3065
        self.build_tree_contents([('tree/file', b'a\n')])
 
3066
        tree.add('file', b'file-id')
 
3067
        tree.commit('a', rev_id=b'one')
 
3068
        self.build_tree_contents([('tree/file', b'a\nb\n')])
 
3069
        preview = TransformPreview(tree)
 
3070
        self.addCleanup(preview.finalize)
 
3071
        file_trans_id = preview.trans_id_file_id(b'file-id')
 
3072
        preview.delete_contents(file_trans_id)
 
3073
        preview.create_file([b'a\nb\nc\n'], file_trans_id)
 
3074
        preview_tree = preview.get_preview_tree()
 
3075
        expected = [
 
3076
            (b'one', b'a\n'),
 
3077
            (b'me:', b'b\n'),
 
3078
            (b'me:', b'c\n'),
 
3079
        ]
 
3080
        annotation = preview_tree.annotate_iter('file', default_revision=b'me:')
 
3081
        self.assertEqual(expected, annotation)
 
3082
 
 
3083
    def test_annotate_missing(self):
 
3084
        preview = self.get_empty_preview()
 
3085
        preview.new_file('file', preview.root, [b'a\nb\nc\n'], b'file-id')
 
3086
        preview_tree = preview.get_preview_tree()
 
3087
        expected = [
 
3088
            (b'me:', b'a\n'),
 
3089
            (b'me:', b'b\n'),
 
3090
            (b'me:', b'c\n'),
 
3091
         ]
 
3092
        annotation = preview_tree.annotate_iter('file', default_revision=b'me:')
 
3093
        self.assertEqual(expected, annotation)
 
3094
 
 
3095
    def test_annotate_rename(self):
 
3096
        tree = self.make_branch_and_tree('tree')
 
3097
        self.build_tree_contents([('tree/file', b'a\n')])
 
3098
        tree.add('file', b'file-id')
 
3099
        tree.commit('a', rev_id=b'one')
 
3100
        preview = TransformPreview(tree)
 
3101
        self.addCleanup(preview.finalize)
 
3102
        file_trans_id = preview.trans_id_file_id(b'file-id')
 
3103
        preview.adjust_path('newname', preview.root, file_trans_id)
 
3104
        preview_tree = preview.get_preview_tree()
 
3105
        expected = [
 
3106
            (b'one', b'a\n'),
 
3107
        ]
 
3108
        annotation = preview_tree.annotate_iter('file', default_revision=b'me:')
 
3109
        self.assertEqual(expected, annotation)
 
3110
 
 
3111
    def test_annotate_deleted(self):
 
3112
        tree = self.make_branch_and_tree('tree')
 
3113
        self.build_tree_contents([('tree/file', b'a\n')])
 
3114
        tree.add('file', b'file-id')
 
3115
        tree.commit('a', rev_id=b'one')
 
3116
        self.build_tree_contents([('tree/file', b'a\nb\n')])
 
3117
        preview = TransformPreview(tree)
 
3118
        self.addCleanup(preview.finalize)
 
3119
        file_trans_id = preview.trans_id_file_id(b'file-id')
 
3120
        preview.delete_contents(file_trans_id)
 
3121
        preview_tree = preview.get_preview_tree()
 
3122
        annotation = preview_tree.annotate_iter('file', default_revision=b'me:')
 
3123
        self.assertIs(None, annotation)
 
3124
 
 
3125
    def test_stored_kind(self):
 
3126
        preview = self.get_empty_preview()
 
3127
        preview.new_file('file', preview.root, [b'a\nb\nc\n'], b'file-id')
 
3128
        preview_tree = preview.get_preview_tree()
 
3129
        self.assertEqual('file', preview_tree.stored_kind('file'))
 
3130
 
 
3131
    def test_is_executable(self):
 
3132
        preview = self.get_empty_preview()
 
3133
        preview.new_file('file', preview.root, [b'a\nb\nc\n'], b'file-id')
 
3134
        preview.set_executability(True, preview.trans_id_file_id(b'file-id'))
 
3135
        preview_tree = preview.get_preview_tree()
 
3136
        self.assertEqual(True, preview_tree.is_executable('file'))
 
3137
 
 
3138
    def test_get_set_parent_ids(self):
 
3139
        revision_tree, preview_tree = self.get_tree_and_preview_tree()
 
3140
        self.assertEqual([], preview_tree.get_parent_ids())
 
3141
        preview_tree.set_parent_ids([b'rev-1'])
 
3142
        self.assertEqual([b'rev-1'], preview_tree.get_parent_ids())
 
3143
 
 
3144
    def test_plan_file_merge(self):
 
3145
        work_a = self.make_branch_and_tree('wta')
 
3146
        self.build_tree_contents([('wta/file', b'a\nb\nc\nd\n')])
 
3147
        work_a.add('file', b'file-id')
 
3148
        base_id = work_a.commit('base version')
 
3149
        tree_b = work_a.controldir.sprout('wtb').open_workingtree()
 
3150
        preview = TransformPreview(work_a)
 
3151
        self.addCleanup(preview.finalize)
 
3152
        trans_id = preview.trans_id_file_id(b'file-id')
 
3153
        preview.delete_contents(trans_id)
 
3154
        preview.create_file([b'b\nc\nd\ne\n'], trans_id)
 
3155
        self.build_tree_contents([('wtb/file', b'a\nc\nd\nf\n')])
 
3156
        tree_a = preview.get_preview_tree()
 
3157
        tree_a.set_parent_ids([base_id])
 
3158
        self.assertEqual([
 
3159
            ('killed-a', b'a\n'),
 
3160
            ('killed-b', b'b\n'),
 
3161
            ('unchanged', b'c\n'),
 
3162
            ('unchanged', b'd\n'),
 
3163
            ('new-a', b'e\n'),
 
3164
            ('new-b', b'f\n'),
 
3165
        ], list(tree_a.plan_file_merge(b'file-id', tree_b)))
 
3166
 
 
3167
    def test_plan_file_merge_revision_tree(self):
 
3168
        work_a = self.make_branch_and_tree('wta')
 
3169
        self.build_tree_contents([('wta/file', b'a\nb\nc\nd\n')])
 
3170
        work_a.add('file', b'file-id')
 
3171
        base_id = work_a.commit('base version')
 
3172
        tree_b = work_a.controldir.sprout('wtb').open_workingtree()
 
3173
        preview = TransformPreview(work_a.basis_tree())
 
3174
        self.addCleanup(preview.finalize)
 
3175
        trans_id = preview.trans_id_file_id(b'file-id')
 
3176
        preview.delete_contents(trans_id)
 
3177
        preview.create_file([b'b\nc\nd\ne\n'], trans_id)
 
3178
        self.build_tree_contents([('wtb/file', b'a\nc\nd\nf\n')])
 
3179
        tree_a = preview.get_preview_tree()
 
3180
        tree_a.set_parent_ids([base_id])
 
3181
        self.assertEqual([
 
3182
            ('killed-a', b'a\n'),
 
3183
            ('killed-b', b'b\n'),
 
3184
            ('unchanged', b'c\n'),
 
3185
            ('unchanged', b'd\n'),
 
3186
            ('new-a', b'e\n'),
 
3187
            ('new-b', b'f\n'),
 
3188
        ], list(tree_a.plan_file_merge(b'file-id', tree_b)))
 
3189
 
 
3190
    def test_walkdirs(self):
 
3191
        preview = self.get_empty_preview()
 
3192
        root = preview.new_directory('', ROOT_PARENT, b'tree-root')
 
3193
        # FIXME: new_directory should mark root.
 
3194
        preview.fixup_new_roots()
 
3195
        preview_tree = preview.get_preview_tree()
 
3196
        file_trans_id = preview.new_file('a', preview.root, [b'contents'],
 
3197
                                         b'a-id')
 
3198
        expected = [(('', b'tree-root'),
 
3199
                    [('a', 'a', 'file', None, b'a-id', 'file')])]
 
3200
        self.assertEqual(expected, list(preview_tree.walkdirs()))
 
3201
 
 
3202
    def test_extras(self):
 
3203
        work_tree = self.make_branch_and_tree('tree')
 
3204
        self.build_tree(['tree/removed-file', 'tree/existing-file',
 
3205
                         'tree/not-removed-file'])
 
3206
        work_tree.add(['removed-file', 'not-removed-file'])
 
3207
        preview = TransformPreview(work_tree)
 
3208
        self.addCleanup(preview.finalize)
 
3209
        preview.new_file('new-file', preview.root, [b'contents'])
 
3210
        preview.new_file('new-versioned-file', preview.root, [b'contents'],
 
3211
                         b'new-versioned-id')
 
3212
        tree = preview.get_preview_tree()
 
3213
        preview.unversion_file(preview.trans_id_tree_path('removed-file'))
 
3214
        self.assertEqual({'new-file', 'removed-file', 'existing-file'},
 
3215
                         set(tree.extras()))
 
3216
 
 
3217
    def test_merge_into_preview(self):
 
3218
        work_tree = self.make_branch_and_tree('tree')
 
3219
        self.build_tree_contents([('tree/file', b'b\n')])
 
3220
        work_tree.add('file', b'file-id')
 
3221
        work_tree.commit('first commit')
 
3222
        child_tree = work_tree.controldir.sprout('child').open_workingtree()
 
3223
        self.build_tree_contents([('child/file', b'b\nc\n')])
 
3224
        child_tree.commit('child commit')
 
3225
        child_tree.lock_write()
 
3226
        self.addCleanup(child_tree.unlock)
 
3227
        work_tree.lock_write()
 
3228
        self.addCleanup(work_tree.unlock)
 
3229
        preview = TransformPreview(work_tree)
 
3230
        self.addCleanup(preview.finalize)
 
3231
        file_trans_id = preview.trans_id_file_id(b'file-id')
 
3232
        preview.delete_contents(file_trans_id)
 
3233
        preview.create_file([b'a\nb\n'], file_trans_id)
 
3234
        preview_tree = preview.get_preview_tree()
 
3235
        merger = Merger.from_revision_ids(preview_tree,
 
3236
                                          child_tree.branch.last_revision(),
 
3237
                                          other_branch=child_tree.branch,
 
3238
                                          tree_branch=work_tree.branch)
 
3239
        merger.merge_type = Merge3Merger
 
3240
        tt = merger.make_merger().make_preview_transform()
 
3241
        self.addCleanup(tt.finalize)
 
3242
        final_tree = tt.get_preview_tree()
 
3243
        self.assertEqual(
 
3244
                b'a\nb\nc\n',
 
3245
                final_tree.get_file_text(final_tree.id2path(b'file-id')))
 
3246
 
 
3247
    def test_merge_preview_into_workingtree(self):
 
3248
        tree = self.make_branch_and_tree('tree')
 
3249
        tree.set_root_id(b'TREE_ROOT')
 
3250
        tt = TransformPreview(tree)
 
3251
        self.addCleanup(tt.finalize)
 
3252
        tt.new_file('name', tt.root, [b'content'], b'file-id')
 
3253
        tree2 = self.make_branch_and_tree('tree2')
 
3254
        tree2.set_root_id(b'TREE_ROOT')
 
3255
        merger = Merger.from_uncommitted(tree2, tt.get_preview_tree(),
 
3256
                                         tree.basis_tree())
 
3257
        merger.merge_type = Merge3Merger
 
3258
        merger.do_merge()
 
3259
 
 
3260
    def test_merge_preview_into_workingtree_handles_conflicts(self):
 
3261
        tree = self.make_branch_and_tree('tree')
 
3262
        self.build_tree_contents([('tree/foo', b'bar')])
 
3263
        tree.add('foo', b'foo-id')
 
3264
        tree.commit('foo')
 
3265
        tt = TransformPreview(tree)
 
3266
        self.addCleanup(tt.finalize)
 
3267
        trans_id = tt.trans_id_file_id(b'foo-id')
 
3268
        tt.delete_contents(trans_id)
 
3269
        tt.create_file([b'baz'], trans_id)
 
3270
        tree2 = tree.controldir.sprout('tree2').open_workingtree()
 
3271
        self.build_tree_contents([('tree2/foo', b'qux')])
 
3272
        merger = Merger.from_uncommitted(tree2, tt.get_preview_tree(),
 
3273
                                         tree.basis_tree())
 
3274
        merger.merge_type = Merge3Merger
 
3275
        merger.do_merge()
 
3276
 
 
3277
    def test_has_filename(self):
 
3278
        wt = self.make_branch_and_tree('tree')
 
3279
        self.build_tree(['tree/unmodified', 'tree/removed', 'tree/modified'])
 
3280
        tt = TransformPreview(wt)
 
3281
        removed_id = tt.trans_id_tree_path('removed')
 
3282
        tt.delete_contents(removed_id)
 
3283
        tt.new_file('new', tt.root, [b'contents'])
 
3284
        modified_id = tt.trans_id_tree_path('modified')
 
3285
        tt.delete_contents(modified_id)
 
3286
        tt.create_file([b'modified-contents'], modified_id)
 
3287
        self.addCleanup(tt.finalize)
 
3288
        tree = tt.get_preview_tree()
 
3289
        self.assertTrue(tree.has_filename('unmodified'))
 
3290
        self.assertFalse(tree.has_filename('not-present'))
 
3291
        self.assertFalse(tree.has_filename('removed'))
 
3292
        self.assertTrue(tree.has_filename('new'))
 
3293
        self.assertTrue(tree.has_filename('modified'))
 
3294
 
 
3295
    def test_is_executable(self):
 
3296
        tree = self.make_branch_and_tree('tree')
 
3297
        preview = TransformPreview(tree)
 
3298
        self.addCleanup(preview.finalize)
 
3299
        preview.new_file('foo', preview.root, [b'bar'], b'baz-id')
 
3300
        preview_tree = preview.get_preview_tree()
 
3301
        self.assertEqual(False, preview_tree.is_executable('tree/foo', b'baz-id'))
 
3302
        self.assertEqual(False, preview_tree.is_executable('tree/foo'))
 
3303
 
 
3304
    def test_commit_preview_tree(self):
 
3305
        tree = self.make_branch_and_tree('tree')
 
3306
        rev_id = tree.commit('rev1')
 
3307
        tree.branch.lock_write()
 
3308
        self.addCleanup(tree.branch.unlock)
 
3309
        tt = TransformPreview(tree)
 
3310
        tt.new_file('file', tt.root, [b'contents'], b'file_id')
 
3311
        self.addCleanup(tt.finalize)
 
3312
        preview = tt.get_preview_tree()
 
3313
        preview.set_parent_ids([rev_id])
 
3314
        builder = tree.branch.get_commit_builder([rev_id])
 
3315
        list(builder.record_iter_changes(preview, rev_id, tt.iter_changes()))
 
3316
        builder.finish_inventory()
 
3317
        rev2_id = builder.commit('rev2')
 
3318
        rev2_tree = tree.branch.repository.revision_tree(rev2_id)
 
3319
        self.assertEqual(b'contents', rev2_tree.get_file_text('file'))
 
3320
 
 
3321
    def test_ascii_limbo_paths(self):
 
3322
        self.requireFeature(features.UnicodeFilenameFeature)
 
3323
        branch = self.make_branch('any')
 
3324
        tree = branch.repository.revision_tree(_mod_revision.NULL_REVISION)
 
3325
        tt = TransformPreview(tree)
 
3326
        self.addCleanup(tt.finalize)
 
3327
        foo_id = tt.new_directory('', ROOT_PARENT)
 
3328
        bar_id = tt.new_file(u'\u1234bar', foo_id, [b'contents'])
 
3329
        limbo_path = tt._limbo_name(bar_id)
 
3330
        self.assertEqual(limbo_path, limbo_path)
 
3331
 
 
3332
 
 
3333
class FakeSerializer(object):
 
3334
    """Serializer implementation that simply returns the input.
 
3335
 
 
3336
    The input is returned in the order used by pack.ContainerPushParser.
 
3337
    """
 
3338
    @staticmethod
 
3339
    def bytes_record(bytes, names):
 
3340
        return names, bytes
 
3341
 
 
3342
 
 
3343
class TestSerializeTransform(tests.TestCaseWithTransport):
 
3344
 
 
3345
    _test_needs_features = [features.UnicodeFilenameFeature]
 
3346
 
 
3347
    def get_preview(self, tree=None):
 
3348
        if tree is None:
 
3349
            tree = self.make_branch_and_tree('tree')
 
3350
        tt = TransformPreview(tree)
 
3351
        self.addCleanup(tt.finalize)
 
3352
        return tt
 
3353
 
 
3354
    def assertSerializesTo(self, expected, tt):
 
3355
        records = list(tt.serialize(FakeSerializer()))
 
3356
        self.assertEqual(expected, records)
 
3357
 
 
3358
    @staticmethod
 
3359
    def default_attribs():
 
3360
        return {
 
3361
            b'_id_number': 1,
 
3362
            b'_new_name': {},
 
3363
            b'_new_parent': {},
 
3364
            b'_new_executability': {},
 
3365
            b'_new_id': {},
 
3366
            b'_tree_path_ids': {b'': b'new-0'},
 
3367
            b'_removed_id': [],
 
3368
            b'_removed_contents': [],
 
3369
            b'_non_present_ids': {},
 
3370
            }
 
3371
 
 
3372
    def make_records(self, attribs, contents):
 
3373
        records = [
 
3374
            ((((b'attribs'),),), bencode.bencode(attribs))]
 
3375
        records.extend([(((n, k),), c) for n, k, c in contents])
 
3376
        return records
 
3377
 
 
3378
    def creation_records(self):
 
3379
        attribs = self.default_attribs()
 
3380
        attribs[b'_id_number'] = 3
 
3381
        attribs[b'_new_name'] = {
 
3382
            b'new-1': u'foo\u1234'.encode('utf-8'), b'new-2': b'qux'}
 
3383
        attribs[b'_new_id'] = {b'new-1': b'baz', b'new-2': b'quxx'}
 
3384
        attribs[b'_new_parent'] = {b'new-1': b'new-0', b'new-2': b'new-0'}
 
3385
        attribs[b'_new_executability'] = {b'new-1': 1}
 
3386
        contents = [
 
3387
            (b'new-1', b'file', b'i 1\nbar\n'),
 
3388
            (b'new-2', b'directory', b''),
 
3389
            ]
 
3390
        return self.make_records(attribs, contents)
 
3391
 
 
3392
    def test_serialize_creation(self):
 
3393
        tt = self.get_preview()
 
3394
        tt.new_file(u'foo\u1234', tt.root, [b'bar'], b'baz', True)
 
3395
        tt.new_directory('qux', tt.root, b'quxx')
 
3396
        self.assertSerializesTo(self.creation_records(), tt)
 
3397
 
 
3398
    def test_deserialize_creation(self):
 
3399
        tt = self.get_preview()
 
3400
        tt.deserialize(iter(self.creation_records()))
 
3401
        self.assertEqual(3, tt._id_number)
 
3402
        self.assertEqual({'new-1': u'foo\u1234',
 
3403
                          'new-2': 'qux'}, tt._new_name)
 
3404
        self.assertEqual({'new-1': b'baz', 'new-2': b'quxx'}, tt._new_id)
 
3405
        self.assertEqual({'new-1': tt.root, 'new-2': tt.root}, tt._new_parent)
 
3406
        self.assertEqual({b'baz': 'new-1', b'quxx': 'new-2'}, tt._r_new_id)
 
3407
        self.assertEqual({'new-1': True}, tt._new_executability)
 
3408
        self.assertEqual({'new-1': 'file',
 
3409
                          'new-2': 'directory'}, tt._new_contents)
 
3410
        foo_limbo = open(tt._limbo_name('new-1'), 'rb')
 
3411
        try:
 
3412
            foo_content = foo_limbo.read()
 
3413
        finally:
 
3414
            foo_limbo.close()
 
3415
        self.assertEqual(b'bar', foo_content)
 
3416
 
 
3417
    def symlink_creation_records(self):
 
3418
        attribs = self.default_attribs()
 
3419
        attribs[b'_id_number'] = 2
 
3420
        attribs[b'_new_name'] = {b'new-1': u'foo\u1234'.encode('utf-8')}
 
3421
        attribs[b'_new_parent'] = {b'new-1': b'new-0'}
 
3422
        contents = [(b'new-1', b'symlink', u'bar\u1234'.encode('utf-8'))]
 
3423
        return self.make_records(attribs, contents)
 
3424
 
 
3425
    def test_serialize_symlink_creation(self):
 
3426
        self.requireFeature(features.SymlinkFeature)
 
3427
        tt = self.get_preview()
 
3428
        tt.new_symlink(u'foo\u1234', tt.root, u'bar\u1234')
 
3429
        self.assertSerializesTo(self.symlink_creation_records(), tt)
 
3430
 
 
3431
    def test_deserialize_symlink_creation(self):
 
3432
        self.requireFeature(features.SymlinkFeature)
 
3433
        tt = self.get_preview()
 
3434
        tt.deserialize(iter(self.symlink_creation_records()))
 
3435
        abspath = tt._limbo_name('new-1')
 
3436
        foo_content = osutils.readlink(abspath)
 
3437
        self.assertEqual(u'bar\u1234', foo_content)
 
3438
 
 
3439
    def make_destruction_preview(self):
 
3440
        tree = self.make_branch_and_tree('.')
 
3441
        self.build_tree([u'foo\u1234', 'bar'])
 
3442
        tree.add([u'foo\u1234', 'bar'], [b'foo-id', b'bar-id'])
 
3443
        return self.get_preview(tree)
 
3444
 
 
3445
    def destruction_records(self):
 
3446
        attribs = self.default_attribs()
 
3447
        attribs[b'_id_number'] = 3
 
3448
        attribs[b'_removed_id'] = [b'new-1']
 
3449
        attribs[b'_removed_contents'] = [b'new-2']
 
3450
        attribs[b'_tree_path_ids'] = {
 
3451
            b'': b'new-0',
 
3452
            u'foo\u1234'.encode('utf-8'): b'new-1',
 
3453
            b'bar': b'new-2',
 
3454
            }
 
3455
        return self.make_records(attribs, [])
 
3456
 
 
3457
    def test_serialize_destruction(self):
 
3458
        tt = self.make_destruction_preview()
 
3459
        foo_trans_id = tt.trans_id_tree_path(u'foo\u1234')
 
3460
        tt.unversion_file(foo_trans_id)
 
3461
        bar_trans_id = tt.trans_id_tree_path('bar')
 
3462
        tt.delete_contents(bar_trans_id)
 
3463
        self.assertSerializesTo(self.destruction_records(), tt)
 
3464
 
 
3465
    def test_deserialize_destruction(self):
 
3466
        tt = self.make_destruction_preview()
 
3467
        tt.deserialize(iter(self.destruction_records()))
 
3468
        self.assertEqual({u'foo\u1234': 'new-1',
 
3469
                          'bar': 'new-2',
 
3470
                          '': tt.root}, tt._tree_path_ids)
 
3471
        self.assertEqual({'new-1': u'foo\u1234',
 
3472
                          'new-2': 'bar',
 
3473
                          tt.root: ''}, tt._tree_id_paths)
 
3474
        self.assertEqual({'new-1'}, tt._removed_id)
 
3475
        self.assertEqual({'new-2'}, tt._removed_contents)
 
3476
 
 
3477
    def missing_records(self):
 
3478
        attribs = self.default_attribs()
 
3479
        attribs[b'_id_number'] = 2
 
3480
        attribs[b'_non_present_ids'] = {
 
3481
            b'boo': b'new-1',}
 
3482
        return self.make_records(attribs, [])
 
3483
 
 
3484
    def test_serialize_missing(self):
 
3485
        tt = self.get_preview()
 
3486
        boo_trans_id = tt.trans_id_file_id(b'boo')
 
3487
        self.assertSerializesTo(self.missing_records(), tt)
 
3488
 
 
3489
    def test_deserialize_missing(self):
 
3490
        tt = self.get_preview()
 
3491
        tt.deserialize(iter(self.missing_records()))
 
3492
        self.assertEqual({b'boo': 'new-1'}, tt._non_present_ids)
 
3493
 
 
3494
    def make_modification_preview(self):
 
3495
        LINES_ONE = b'aa\nbb\ncc\ndd\n'
 
3496
        LINES_TWO = b'z\nbb\nx\ndd\n'
 
3497
        tree = self.make_branch_and_tree('tree')
 
3498
        self.build_tree_contents([('tree/file', LINES_ONE)])
 
3499
        tree.add('file', b'file-id')
 
3500
        return self.get_preview(tree), [LINES_TWO]
 
3501
 
 
3502
    def modification_records(self):
 
3503
        attribs = self.default_attribs()
 
3504
        attribs[b'_id_number'] = 2
 
3505
        attribs[b'_tree_path_ids'] = {
 
3506
            b'file': b'new-1',
 
3507
            b'': b'new-0',}
 
3508
        attribs[b'_removed_contents'] = [b'new-1']
 
3509
        contents = [(b'new-1', b'file',
 
3510
                     b'i 1\nz\n\nc 0 1 1 1\ni 1\nx\n\nc 0 3 3 1\n')]
 
3511
        return self.make_records(attribs, contents)
 
3512
 
 
3513
    def test_serialize_modification(self):
 
3514
        tt, LINES = self.make_modification_preview()
 
3515
        trans_id = tt.trans_id_file_id(b'file-id')
 
3516
        tt.delete_contents(trans_id)
 
3517
        tt.create_file(LINES, trans_id)
 
3518
        self.assertSerializesTo(self.modification_records(), tt)
 
3519
 
 
3520
    def test_deserialize_modification(self):
 
3521
        tt, LINES = self.make_modification_preview()
 
3522
        tt.deserialize(iter(self.modification_records()))
 
3523
        self.assertFileEqual(b''.join(LINES), tt._limbo_name('new-1'))
 
3524
 
 
3525
    def make_kind_change_preview(self):
 
3526
        LINES = b'a\nb\nc\nd\n'
 
3527
        tree = self.make_branch_and_tree('tree')
 
3528
        self.build_tree(['tree/foo/'])
 
3529
        tree.add('foo', b'foo-id')
 
3530
        return self.get_preview(tree), [LINES]
 
3531
 
 
3532
    def kind_change_records(self):
 
3533
        attribs = self.default_attribs()
 
3534
        attribs[b'_id_number'] = 2
 
3535
        attribs[b'_tree_path_ids'] = {
 
3536
            b'foo': b'new-1',
 
3537
            b'': b'new-0',}
 
3538
        attribs[b'_removed_contents'] = [b'new-1']
 
3539
        contents = [(b'new-1', b'file',
 
3540
                     b'i 4\na\nb\nc\nd\n\n')]
 
3541
        return self.make_records(attribs, contents)
 
3542
 
 
3543
    def test_serialize_kind_change(self):
 
3544
        tt, LINES = self.make_kind_change_preview()
 
3545
        trans_id = tt.trans_id_file_id(b'foo-id')
 
3546
        tt.delete_contents(trans_id)
 
3547
        tt.create_file(LINES, trans_id)
 
3548
        self.assertSerializesTo(self.kind_change_records(), tt)
 
3549
 
 
3550
    def test_deserialize_kind_change(self):
 
3551
        tt, LINES = self.make_kind_change_preview()
 
3552
        tt.deserialize(iter(self.kind_change_records()))
 
3553
        self.assertFileEqual(b''.join(LINES), tt._limbo_name('new-1'))
 
3554
 
 
3555
    def make_add_contents_preview(self):
 
3556
        LINES = b'a\nb\nc\nd\n'
 
3557
        tree = self.make_branch_and_tree('tree')
 
3558
        self.build_tree(['tree/foo'])
 
3559
        tree.add('foo')
 
3560
        os.unlink('tree/foo')
 
3561
        return self.get_preview(tree), LINES
 
3562
 
 
3563
    def add_contents_records(self):
 
3564
        attribs = self.default_attribs()
 
3565
        attribs[b'_id_number'] = 2
 
3566
        attribs[b'_tree_path_ids'] = {
 
3567
            b'foo': b'new-1',
 
3568
            b'': b'new-0',}
 
3569
        contents = [(b'new-1', b'file',
 
3570
                     b'i 4\na\nb\nc\nd\n\n')]
 
3571
        return self.make_records(attribs, contents)
 
3572
 
 
3573
    def test_serialize_add_contents(self):
 
3574
        tt, LINES = self.make_add_contents_preview()
 
3575
        trans_id = tt.trans_id_tree_path('foo')
 
3576
        tt.create_file([LINES], trans_id)
 
3577
        self.assertSerializesTo(self.add_contents_records(), tt)
 
3578
 
 
3579
    def test_deserialize_add_contents(self):
 
3580
        tt, LINES = self.make_add_contents_preview()
 
3581
        tt.deserialize(iter(self.add_contents_records()))
 
3582
        self.assertFileEqual(LINES, tt._limbo_name('new-1'))
 
3583
 
 
3584
    def test_get_parents_lines(self):
 
3585
        LINES_ONE = b'aa\nbb\ncc\ndd\n'
 
3586
        LINES_TWO = b'z\nbb\nx\ndd\n'
 
3587
        tree = self.make_branch_and_tree('tree')
 
3588
        self.build_tree_contents([('tree/file', LINES_ONE)])
 
3589
        tree.add('file', b'file-id')
 
3590
        tt = self.get_preview(tree)
 
3591
        trans_id = tt.trans_id_tree_path('file')
 
3592
        self.assertEqual(([b'aa\n', b'bb\n', b'cc\n', b'dd\n'],),
 
3593
            tt._get_parents_lines(trans_id))
 
3594
 
 
3595
    def test_get_parents_texts(self):
 
3596
        LINES_ONE = b'aa\nbb\ncc\ndd\n'
 
3597
        LINES_TWO = b'z\nbb\nx\ndd\n'
 
3598
        tree = self.make_branch_and_tree('tree')
 
3599
        self.build_tree_contents([('tree/file', LINES_ONE)])
 
3600
        tree.add('file', b'file-id')
 
3601
        tt = self.get_preview(tree)
 
3602
        trans_id = tt.trans_id_tree_path('file')
 
3603
        self.assertEqual((LINES_ONE,),
 
3604
            tt._get_parents_texts(trans_id))
 
3605
 
 
3606
 
 
3607
class TestOrphan(tests.TestCaseWithTransport):
 
3608
 
 
3609
    def test_no_orphan_for_transform_preview(self):
 
3610
        tree = self.make_branch_and_tree('tree')
 
3611
        tt = transform.TransformPreview(tree)
 
3612
        self.addCleanup(tt.finalize)
 
3613
        self.assertRaises(NotImplementedError, tt.new_orphan, 'foo', 'bar')
 
3614
 
 
3615
    def _set_orphan_policy(self, wt, policy):
 
3616
        wt.branch.get_config_stack().set('transform.orphan_policy',
 
3617
                                               policy)
 
3618
 
 
3619
    def _prepare_orphan(self, wt):
 
3620
        self.build_tree(['dir/', 'dir/file', 'dir/foo'])
 
3621
        wt.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
 
3622
        wt.commit('add dir and file ignoring foo')
 
3623
        tt = transform.TreeTransform(wt)
 
3624
        self.addCleanup(tt.finalize)
 
3625
        # dir and bar are deleted
 
3626
        dir_tid = tt.trans_id_tree_path('dir')
 
3627
        file_tid = tt.trans_id_tree_path('dir/file')
 
3628
        orphan_tid = tt.trans_id_tree_path('dir/foo')
 
3629
        tt.delete_contents(file_tid)
 
3630
        tt.unversion_file(file_tid)
 
3631
        tt.delete_contents(dir_tid)
 
3632
        tt.unversion_file(dir_tid)
 
3633
        # There should be a conflict because dir still contain foo
 
3634
        raw_conflicts = tt.find_conflicts()
 
3635
        self.assertLength(1, raw_conflicts)
 
3636
        self.assertEqual(('missing parent', 'new-1'), raw_conflicts[0])
 
3637
        return tt, orphan_tid
 
3638
 
 
3639
    def test_new_orphan_created(self):
 
3640
        wt = self.make_branch_and_tree('.')
 
3641
        self._set_orphan_policy(wt, 'move')
 
3642
        tt, orphan_tid = self._prepare_orphan(wt)
 
3643
        warnings = []
 
3644
        def warning(*args):
 
3645
            warnings.append(args[0] % args[1:])
 
3646
        self.overrideAttr(trace, 'warning', warning)
 
3647
        remaining_conflicts = resolve_conflicts(tt)
 
3648
        self.assertEqual(['dir/foo has been orphaned in brz-orphans'],
 
3649
                          warnings)
 
3650
        # Yeah for resolved conflicts !
 
3651
        self.assertLength(0, remaining_conflicts)
 
3652
        # We have a new orphan
 
3653
        self.assertEqual('foo.~1~', tt.final_name(orphan_tid))
 
3654
        self.assertEqual('brz-orphans',
 
3655
                          tt.final_name(tt.final_parent(orphan_tid)))
 
3656
 
 
3657
    def test_never_orphan(self):
 
3658
        wt = self.make_branch_and_tree('.')
 
3659
        self._set_orphan_policy(wt, 'conflict')
 
3660
        tt, orphan_tid = self._prepare_orphan(wt)
 
3661
        remaining_conflicts = resolve_conflicts(tt)
 
3662
        self.assertLength(1, remaining_conflicts)
 
3663
        self.assertEqual(('deleting parent', 'Not deleting', 'new-1'),
 
3664
                         remaining_conflicts.pop())
 
3665
 
 
3666
    def test_orphan_error(self):
 
3667
        def bogus_orphan(tt, orphan_id, parent_id):
 
3668
            raise transform.OrphaningError(tt.final_name(orphan_id),
 
3669
                                           tt.final_name(parent_id))
 
3670
        transform.orphaning_registry.register('bogus', bogus_orphan,
 
3671
                                              'Raise an error when orphaning')
 
3672
        wt = self.make_branch_and_tree('.')
 
3673
        self._set_orphan_policy(wt, 'bogus')
 
3674
        tt, orphan_tid = self._prepare_orphan(wt)
 
3675
        remaining_conflicts = resolve_conflicts(tt)
 
3676
        self.assertLength(1, remaining_conflicts)
 
3677
        self.assertEqual(('deleting parent', 'Not deleting', 'new-1'),
 
3678
                         remaining_conflicts.pop())
 
3679
 
 
3680
    def test_unknown_orphan_policy(self):
 
3681
        wt = self.make_branch_and_tree('.')
 
3682
        # Set a fictional policy nobody ever implemented
 
3683
        self._set_orphan_policy(wt, 'donttouchmypreciouuus')
 
3684
        tt, orphan_tid = self._prepare_orphan(wt)
 
3685
        warnings = []
 
3686
        def warning(*args):
 
3687
            warnings.append(args[0] % args[1:])
 
3688
        self.overrideAttr(trace, 'warning', warning)
 
3689
        remaining_conflicts = resolve_conflicts(tt)
 
3690
        # We fallback to the default policy which create a conflict
 
3691
        self.assertLength(1, remaining_conflicts)
 
3692
        self.assertEqual(('deleting parent', 'Not deleting', 'new-1'),
 
3693
                         remaining_conflicts.pop())
 
3694
        self.assertLength(1, warnings)
 
3695
        self.assertStartsWith(warnings[0], 'Value "donttouchmypreciouuus" ')
 
3696
 
 
3697
 
 
3698
class TestTransformHooks(tests.TestCaseWithTransport):
 
3699
 
 
3700
    def setUp(self):
 
3701
        super(TestTransformHooks, self).setUp()
 
3702
        self.wt = self.make_branch_and_tree('.')
 
3703
        os.chdir('..')
 
3704
 
 
3705
    def get_transform(self):
 
3706
        transform = TreeTransform(self.wt)
 
3707
        self.addCleanup(transform.finalize)
 
3708
        return transform, transform.root
 
3709
 
 
3710
    def test_pre_commit_hooks(self):
 
3711
        calls = []
 
3712
        def record_pre_transform(tree, tt):
 
3713
            calls.append((tree, tt))
 
3714
        MutableTree.hooks.install_named_hook('pre_transform',
 
3715
            record_pre_transform, "Pre transform")
 
3716
        transform, root = self.get_transform()
 
3717
        old_root_id = transform.tree_file_id(root)
 
3718
        transform.apply()
 
3719
        self.assertEqual(old_root_id, self.wt.get_root_id())
 
3720
        self.assertEqual([(self.wt, transform)], calls)
 
3721
 
 
3722
    def test_post_commit_hooks(self):
 
3723
        calls = []
 
3724
        def record_post_transform(tree, tt):
 
3725
            calls.append((tree, tt))
 
3726
        MutableTree.hooks.install_named_hook('post_transform',
 
3727
            record_post_transform, "Post transform")
 
3728
        transform, root = self.get_transform()
 
3729
        old_root_id = transform.tree_file_id(root)
 
3730
        transform.apply()
 
3731
        self.assertEqual(old_root_id, self.wt.get_root_id())
 
3732
        self.assertEqual([(self.wt, transform)], calls)
 
3733
 
 
3734
 
 
3735
class TestLinkTree(tests.TestCaseWithTransport):
 
3736
 
 
3737
    _test_needs_features = [HardlinkFeature]
 
3738
 
 
3739
    def setUp(self):
 
3740
        tests.TestCaseWithTransport.setUp(self)
 
3741
        self.parent_tree = self.make_branch_and_tree('parent')
 
3742
        self.parent_tree.lock_write()
 
3743
        self.addCleanup(self.parent_tree.unlock)
 
3744
        self.build_tree_contents([('parent/foo', b'bar')])
 
3745
        self.parent_tree.add('foo')
 
3746
        self.parent_tree.commit('added foo')
 
3747
        child_controldir = self.parent_tree.controldir.sprout('child')
 
3748
        self.child_tree = child_controldir.open_workingtree()
 
3749
 
 
3750
    def hardlinked(self):
 
3751
        parent_stat = os.lstat(self.parent_tree.abspath('foo'))
 
3752
        child_stat = os.lstat(self.child_tree.abspath('foo'))
 
3753
        return parent_stat.st_ino == child_stat.st_ino
 
3754
 
 
3755
    def test_link_fails_if_modified(self):
 
3756
        """If the file to be linked has modified text, don't link."""
 
3757
        self.build_tree_contents([('child/foo', b'baz')])
 
3758
        transform.link_tree(self.child_tree, self.parent_tree)
 
3759
        self.assertFalse(self.hardlinked())
 
3760
 
 
3761
    def test_link_fails_if_execute_bit_changed(self):
 
3762
        """If the file to be linked has modified execute bit, don't link."""
 
3763
        tt = TreeTransform(self.child_tree)
 
3764
        try:
 
3765
            trans_id = tt.trans_id_tree_path('foo')
 
3766
            tt.set_executability(True, trans_id)
 
3767
            tt.apply()
 
3768
        finally:
 
3769
            tt.finalize()
 
3770
        transform.link_tree(self.child_tree, self.parent_tree)
 
3771
        self.assertFalse(self.hardlinked())
 
3772
 
 
3773
    def test_link_succeeds_if_unmodified(self):
 
3774
        """If the file to be linked is unmodified, link"""
 
3775
        transform.link_tree(self.child_tree, self.parent_tree)
 
3776
        self.assertTrue(self.hardlinked())