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

  • Committer: Jelmer Vernooij
  • Date: 2020-07-05 12:50:01 UTC
  • mfrom: (7490.40.46 work)
  • mto: (7490.40.48 work)
  • mto: This revision was merged to the branch mainline in revision 7519.
  • Revision ID: jelmer@jelmer.uk-20200705125001-7s3vo0p55szbbws7
Merge lp:brz/3.1.

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
from io import BytesIO
 
19
import os
 
20
import sys
 
21
import time
 
22
 
 
23
from ... import (
 
24
    errors,
 
25
    osutils,
 
26
    tests,
 
27
    trace,
 
28
    transform,
 
29
    urlutils,
 
30
    )
 
31
from ...conflicts import (
 
32
    DeletingParent,
 
33
    DuplicateEntry,
 
34
    DuplicateID,
 
35
    MissingParent,
 
36
    NonDirectoryParent,
 
37
    ParentLoop,
 
38
    UnversionedParent,
 
39
)
 
40
from ...errors import (
 
41
    DuplicateKey,
 
42
    ExistingLimbo,
 
43
    ExistingPendingDeletion,
 
44
    ImmortalPendingDeletion,
 
45
    LockError,
 
46
)
 
47
from ...osutils import (
 
48
    file_kind,
 
49
    pathjoin,
 
50
)
 
51
from ...sixish import (
 
52
    text_type,
 
53
    )
 
54
from .. import (
 
55
    features,
 
56
    TestSkipped,
 
57
    )
 
58
from ..features import (
 
59
    HardlinkFeature,
 
60
    SymlinkFeature,
 
61
    )
 
62
from ...transform import (
 
63
    create_from_tree,
 
64
    cook_conflicts,
 
65
    FinalPaths,
 
66
    resolve_conflicts,
 
67
    resolve_checkout,
 
68
    ROOT_PARENT,
 
69
    ImmortalLimbo,
 
70
    MalformedTransform,
 
71
    NoFinalPath,
 
72
    ReusingTransform,
 
73
    TransformRenameFailed,
 
74
)
 
75
 
 
76
 
 
77
 
 
78
class TestTreeTransform(tests.TestCaseWithTransport):
 
79
 
 
80
    def setUp(self):
 
81
        super(TestTreeTransform, self).setUp()
 
82
        self.wt = self.make_branch_and_tree('wt')
 
83
 
 
84
    def transform(self):
 
85
        transform = self.wt.transform()
 
86
        self.addCleanup(transform.finalize)
 
87
        return transform, transform.root
 
88
 
 
89
    def transform_for_sha1_test(self):
 
90
        trans, root = self.transform()
 
91
        self.wt.lock_tree_write()
 
92
        self.addCleanup(self.wt.unlock)
 
93
        contents = [b'just some content\n']
 
94
        sha1 = osutils.sha_strings(contents)
 
95
        # Roll back the clock
 
96
        trans._creation_mtime = time.time() - 20.0
 
97
        return trans, root, contents, sha1
 
98
 
 
99
    def test_existing_limbo(self):
 
100
        transform, root = self.transform()
 
101
        limbo_name = transform._limbodir
 
102
        deletion_path = transform._deletiondir
 
103
        os.mkdir(pathjoin(limbo_name, 'hehe'))
 
104
        self.assertRaises(ImmortalLimbo, transform.apply)
 
105
        self.assertRaises(LockError, self.wt.unlock)
 
106
        self.assertRaises(ExistingLimbo, self.transform)
 
107
        self.assertRaises(LockError, self.wt.unlock)
 
108
        os.rmdir(pathjoin(limbo_name, 'hehe'))
 
109
        os.rmdir(limbo_name)
 
110
        os.rmdir(deletion_path)
 
111
        transform, root = self.transform()
 
112
        transform.apply()
 
113
 
 
114
    def test_existing_pending_deletion(self):
 
115
        transform, root = self.transform()
 
116
        deletion_path = self._limbodir = urlutils.local_path_from_url(
 
117
            transform._tree._transport.abspath('pending-deletion'))
 
118
        os.mkdir(pathjoin(deletion_path, 'blocking-directory'))
 
119
        self.assertRaises(ImmortalPendingDeletion, transform.apply)
 
120
        self.assertRaises(LockError, self.wt.unlock)
 
121
        self.assertRaises(ExistingPendingDeletion, self.transform)
 
122
 
 
123
    def test_build(self):
 
124
        transform, root = self.transform()
 
125
        self.wt.lock_tree_write()
 
126
        self.addCleanup(self.wt.unlock)
 
127
        self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
 
128
        imaginary_id = transform.trans_id_tree_path('imaginary')
 
129
        imaginary_id2 = transform.trans_id_tree_path('imaginary/')
 
130
        self.assertEqual(imaginary_id, imaginary_id2)
 
131
        self.assertEqual(root, transform.get_tree_parent(imaginary_id))
 
132
        self.assertEqual('directory', transform.final_kind(root))
 
133
        self.assertEqual(self.wt.path2id(''), transform.final_file_id(root))
 
134
        trans_id = transform.create_path('name', root)
 
135
        self.assertIs(transform.final_file_id(trans_id), None)
 
136
        self.assertIs(None, transform.final_kind(trans_id))
 
137
        transform.create_file([b'contents'], trans_id)
 
138
        transform.set_executability(True, trans_id)
 
139
        transform.version_file(trans_id, file_id=b'my_pretties')
 
140
        self.assertRaises(DuplicateKey, transform.version_file,
 
141
                          trans_id, file_id=b'my_pretties')
 
142
        self.assertEqual(transform.final_file_id(trans_id), b'my_pretties')
 
143
        self.assertEqual(transform.final_parent(trans_id), root)
 
144
        self.assertIs(transform.final_parent(root), ROOT_PARENT)
 
145
        self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
 
146
        oz_id = transform.create_path('oz', root)
 
147
        transform.create_directory(oz_id)
 
148
        transform.version_file(oz_id, file_id=b'ozzie')
 
149
        trans_id2 = transform.create_path('name2', root)
 
150
        transform.create_file([b'contents'], trans_id2)
 
151
        transform.set_executability(False, trans_id2)
 
152
        transform.version_file(trans_id2, file_id=b'my_pretties2')
 
153
        modified_paths = transform.apply().modified_paths
 
154
        with self.wt.get_file('name') as f:
 
155
            self.assertEqual(b'contents', f.read())
 
156
        self.assertEqual(self.wt.path2id('name'), b'my_pretties')
 
157
        self.assertIs(self.wt.is_executable('name'), True)
 
158
        self.assertIs(self.wt.is_executable('name2'), False)
 
159
        self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
 
160
        self.assertEqual(len(modified_paths), 3)
 
161
        tree_mod_paths = [self.wt.abspath(self.wt.id2path(f)) for f in
 
162
                          (b'ozzie', b'my_pretties', b'my_pretties2')]
 
163
        self.assertSubset(tree_mod_paths, modified_paths)
 
164
        # is it safe to finalize repeatedly?
 
165
        transform.finalize()
 
166
        transform.finalize()
 
167
 
 
168
    def test_apply_informs_tree_of_observed_sha1(self):
 
169
        trans, root, contents, sha1 = self.transform_for_sha1_test()
 
170
        trans_id = trans.new_file('file1', root, contents, file_id=b'file1-id',
 
171
                                  sha1=sha1)
 
172
        calls = []
 
173
        orig = self.wt._observed_sha1
 
174
 
 
175
        def _observed_sha1(*args):
 
176
            calls.append(args)
 
177
            orig(*args)
 
178
        self.wt._observed_sha1 = _observed_sha1
 
179
        trans.apply()
 
180
        self.assertEqual([('file1', trans._observed_sha1s[trans_id])],
 
181
                         calls)
 
182
 
 
183
    def test_create_file_caches_sha1(self):
 
184
        trans, root, contents, sha1 = self.transform_for_sha1_test()
 
185
        trans_id = trans.create_path('file1', root)
 
186
        trans.create_file(contents, trans_id, sha1=sha1)
 
187
        st_val = osutils.lstat(trans._limbo_name(trans_id))
 
188
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
189
        self.assertEqual(o_sha1, sha1)
 
190
        self.assertEqualStat(o_st_val, st_val)
 
191
 
 
192
    def test__apply_insertions_updates_sha1(self):
 
193
        trans, root, contents, sha1 = self.transform_for_sha1_test()
 
194
        trans_id = trans.create_path('file1', root)
 
195
        trans.create_file(contents, trans_id, sha1=sha1)
 
196
        st_val = osutils.lstat(trans._limbo_name(trans_id))
 
197
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
198
        self.assertEqual(o_sha1, sha1)
 
199
        self.assertEqualStat(o_st_val, st_val)
 
200
        creation_mtime = trans._creation_mtime + 10.0
 
201
        # We fake a time difference from when the file was created until now it
 
202
        # is being renamed by using os.utime. Note that the change we actually
 
203
        # want to see is the real ctime change from 'os.rename()', but as long
 
204
        # as we observe a new stat value, we should be fine.
 
205
        os.utime(trans._limbo_name(trans_id), (creation_mtime, creation_mtime))
 
206
        trans.apply()
 
207
        new_st_val = osutils.lstat(self.wt.abspath('file1'))
 
208
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
209
        self.assertEqual(o_sha1, sha1)
 
210
        self.assertEqualStat(o_st_val, new_st_val)
 
211
        self.assertNotEqual(st_val.st_mtime, new_st_val.st_mtime)
 
212
 
 
213
    def test_new_file_caches_sha1(self):
 
214
        trans, root, contents, sha1 = self.transform_for_sha1_test()
 
215
        trans_id = trans.new_file('file1', root, contents, file_id=b'file1-id',
 
216
                                  sha1=sha1)
 
217
        st_val = osutils.lstat(trans._limbo_name(trans_id))
 
218
        o_sha1, o_st_val = trans._observed_sha1s[trans_id]
 
219
        self.assertEqual(o_sha1, sha1)
 
220
        self.assertEqualStat(o_st_val, st_val)
 
221
 
 
222
    def test_cancel_creation_removes_observed_sha1(self):
 
223
        trans, root, contents, sha1 = self.transform_for_sha1_test()
 
224
        trans_id = trans.new_file('file1', root, contents, file_id=b'file1-id',
 
225
                                  sha1=sha1)
 
226
        self.assertTrue(trans_id in trans._observed_sha1s)
 
227
        trans.cancel_creation(trans_id)
 
228
        self.assertFalse(trans_id in trans._observed_sha1s)
 
229
 
 
230
    def test_create_files_same_timestamp(self):
 
231
        transform, root = self.transform()
 
232
        self.wt.lock_tree_write()
 
233
        self.addCleanup(self.wt.unlock)
 
234
        # Roll back the clock, so that we know everything is being set to the
 
235
        # exact time
 
236
        transform._creation_mtime = creation_mtime = time.time() - 20.0
 
237
        transform.create_file([b'content-one'],
 
238
                              transform.create_path('one', root))
 
239
        time.sleep(1)  # *ugly*
 
240
        transform.create_file([b'content-two'],
 
241
                              transform.create_path('two', root))
 
242
        transform.apply()
 
243
        fo, st1 = self.wt.get_file_with_stat('one', filtered=False)
 
244
        fo.close()
 
245
        fo, st2 = self.wt.get_file_with_stat('two', filtered=False)
 
246
        fo.close()
 
247
        # We only guarantee 2s resolution
 
248
        self.assertTrue(
 
249
            abs(creation_mtime - st1.st_mtime) < 2.0,
 
250
            "%s != %s within 2 seconds" % (creation_mtime, st1.st_mtime))
 
251
        # But if we have more than that, all files should get the same result
 
252
        self.assertEqual(st1.st_mtime, st2.st_mtime)
 
253
 
 
254
    def test_change_root_id(self):
 
255
        transform, root = self.transform()
 
256
        self.assertNotEqual(b'new-root-id', self.wt.path2id(''))
 
257
        transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
258
        transform.delete_contents(root)
 
259
        transform.unversion_file(root)
 
260
        transform.fixup_new_roots()
 
261
        transform.apply()
 
262
        self.assertEqual(b'new-root-id', self.wt.path2id(''))
 
263
 
 
264
    def test_change_root_id_add_files(self):
 
265
        transform, root = self.transform()
 
266
        self.assertNotEqual(b'new-root-id', self.wt.path2id(''))
 
267
        new_trans_id = transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
268
        transform.new_file('file', new_trans_id, [b'new-contents\n'],
 
269
                           b'new-file-id')
 
270
        transform.delete_contents(root)
 
271
        transform.unversion_file(root)
 
272
        transform.fixup_new_roots()
 
273
        transform.apply()
 
274
        self.assertEqual(b'new-root-id', self.wt.path2id(''))
 
275
        self.assertEqual(b'new-file-id', self.wt.path2id('file'))
 
276
        self.assertFileEqual(b'new-contents\n', self.wt.abspath('file'))
 
277
 
 
278
    def test_add_two_roots(self):
 
279
        transform, root = self.transform()
 
280
        transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
281
        transform.new_directory('', ROOT_PARENT, b'alt-root-id')
 
282
        self.assertRaises(ValueError, transform.fixup_new_roots)
 
283
 
 
284
    def test_retain_existing_root(self):
 
285
        tt, root = self.transform()
 
286
        with tt:
 
287
            tt.new_directory('', ROOT_PARENT, b'new-root-id')
 
288
            tt.fixup_new_roots()
 
289
            self.assertNotEqual(b'new-root-id', tt.final_file_id(tt.root))
 
290
 
 
291
    def test_retain_existing_root_added_file(self):
 
292
        tt, root = self.transform()
 
293
        new_trans_id = tt.new_directory('', ROOT_PARENT, b'new-root-id')
 
294
        child = tt.new_directory('child', new_trans_id, b'child-id')
 
295
        tt.fixup_new_roots()
 
296
        self.assertEqual(tt.root, tt.final_parent(child))
 
297
 
 
298
    def test_add_unversioned_root(self):
 
299
        transform, root = self.transform()
 
300
        transform.new_directory('', ROOT_PARENT, None)
 
301
        transform.delete_contents(transform.root)
 
302
        transform.fixup_new_roots()
 
303
        self.assertNotIn(transform.root, transform._new_id)
 
304
 
 
305
    def test_remove_root_fixup(self):
 
306
        transform, root = self.transform()
 
307
        old_root_id = self.wt.path2id('')
 
308
        self.assertNotEqual(b'new-root-id', old_root_id)
 
309
        transform.delete_contents(root)
 
310
        transform.unversion_file(root)
 
311
        transform.fixup_new_roots()
 
312
        transform.apply()
 
313
        self.assertEqual(old_root_id, self.wt.path2id(''))
 
314
 
 
315
        transform, root = self.transform()
 
316
        transform.new_directory('', ROOT_PARENT, b'new-root-id')
 
317
        transform.new_directory('', ROOT_PARENT, b'alt-root-id')
 
318
        self.assertRaises(ValueError, transform.fixup_new_roots)
 
319
 
 
320
    def test_fixup_new_roots_permits_empty_tree(self):
 
321
        transform, root = self.transform()
 
322
        transform.delete_contents(root)
 
323
        transform.unversion_file(root)
 
324
        transform.fixup_new_roots()
 
325
        self.assertIs(None, transform.final_kind(root))
 
326
        self.assertIs(None, transform.final_file_id(root))
 
327
 
 
328
    def test_apply_retains_root_directory(self):
 
329
        # Do not attempt to delete the physical root directory, because that
 
330
        # is impossible.
 
331
        transform, root = self.transform()
 
332
        with transform:
 
333
            transform.delete_contents(root)
 
334
            e = self.assertRaises(AssertionError, self.assertRaises,
 
335
                                  TransformRenameFailed,
 
336
                                  transform.apply)
 
337
        self.assertContainsRe('TransformRenameFailed not raised', str(e))
 
338
 
 
339
    def test_apply_retains_file_id(self):
 
340
        transform, root = self.transform()
 
341
        old_root_id = transform.tree_file_id(root)
 
342
        transform.unversion_file(root)
 
343
        transform.apply()
 
344
        self.assertEqual(old_root_id, self.wt.path2id(''))
 
345
 
 
346
    def test_hardlink(self):
 
347
        self.requireFeature(HardlinkFeature)
 
348
        transform, root = self.transform()
 
349
        transform.new_file('file1', root, [b'contents'])
 
350
        transform.apply()
 
351
        target = self.make_branch_and_tree('target')
 
352
        target_transform = target.transform()
 
353
        trans_id = target_transform.create_path('file1', target_transform.root)
 
354
        target_transform.create_hardlink(self.wt.abspath('file1'), trans_id)
 
355
        target_transform.apply()
 
356
        self.assertPathExists('target/file1')
 
357
        source_stat = os.stat(self.wt.abspath('file1'))
 
358
        target_stat = os.stat('target/file1')
 
359
        self.assertEqual(source_stat, target_stat)
 
360
 
 
361
    def test_convenience(self):
 
362
        transform, root = self.transform()
 
363
        self.wt.lock_tree_write()
 
364
        self.addCleanup(self.wt.unlock)
 
365
        transform.new_file('name', root, [b'contents'], b'my_pretties', True)
 
366
        oz = transform.new_directory('oz', root, b'oz-id')
 
367
        dorothy = transform.new_directory('dorothy', oz, b'dorothy-id')
 
368
        transform.new_file('toto', dorothy, [b'toto-contents'], b'toto-id',
 
369
                           False)
 
370
 
 
371
        self.assertEqual(len(transform.find_conflicts()), 0)
 
372
        transform.apply()
 
373
        self.assertRaises(ReusingTransform, transform.find_conflicts)
 
374
        with open(self.wt.abspath('name'), 'r') as f:
 
375
            self.assertEqual('contents', f.read())
 
376
        self.assertEqual(self.wt.path2id('name'), b'my_pretties')
 
377
        self.assertIs(self.wt.is_executable('name'), True)
 
378
        self.assertEqual(self.wt.path2id('oz'), b'oz-id')
 
379
        self.assertEqual(self.wt.path2id('oz/dorothy'), b'dorothy-id')
 
380
        self.assertEqual(self.wt.path2id('oz/dorothy/toto'), b'toto-id')
 
381
 
 
382
        self.assertEqual(b'toto-contents',
 
383
                         self.wt.get_file('oz/dorothy/toto').read())
 
384
        self.assertIs(self.wt.is_executable('oz/dorothy/toto'), False)
 
385
 
 
386
    def test_tree_reference(self):
 
387
        transform, root = self.transform()
 
388
        tree = transform._tree
 
389
        trans_id = transform.new_directory('reference', root, b'subtree-id')
 
390
        transform.set_tree_reference(b'subtree-revision', trans_id)
 
391
        transform.apply()
 
392
        tree.lock_read()
 
393
        self.addCleanup(tree.unlock)
 
394
        self.assertEqual(
 
395
            b'subtree-revision',
 
396
            tree.root_inventory.get_entry(b'subtree-id').reference_revision)
 
397
 
 
398
    def test_conflicts(self):
 
399
        transform, root = self.transform()
 
400
        trans_id = transform.new_file('name', root, [b'contents'],
 
401
                                      b'my_pretties')
 
402
        self.assertEqual(len(transform.find_conflicts()), 0)
 
403
        trans_id2 = transform.new_file('name', root, [b'Crontents'], b'toto')
 
404
        self.assertEqual(transform.find_conflicts(),
 
405
                         [('duplicate', trans_id, trans_id2, 'name')])
 
406
        self.assertRaises(MalformedTransform, transform.apply)
 
407
        transform.adjust_path('name', trans_id, trans_id2)
 
408
        self.assertEqual(transform.find_conflicts(),
 
409
                         [('non-directory parent', trans_id)])
 
410
        tinman_id = transform.trans_id_tree_path('tinman')
 
411
        transform.adjust_path('name', tinman_id, trans_id2)
 
412
        self.assertEqual(transform.find_conflicts(),
 
413
                         [('unversioned parent', tinman_id),
 
414
                          ('missing parent', tinman_id)])
 
415
        lion_id = transform.create_path('lion', root)
 
416
        self.assertEqual(transform.find_conflicts(),
 
417
                         [('unversioned parent', tinman_id),
 
418
                          ('missing parent', tinman_id)])
 
419
        transform.adjust_path('name', lion_id, trans_id2)
 
420
        self.assertEqual(transform.find_conflicts(),
 
421
                         [('unversioned parent', lion_id),
 
422
                          ('missing parent', lion_id)])
 
423
        transform.version_file(lion_id, file_id=b"Courage")
 
424
        self.assertEqual(transform.find_conflicts(),
 
425
                         [('missing parent', lion_id),
 
426
                          ('versioning no contents', lion_id)])
 
427
        transform.adjust_path('name2', root, trans_id2)
 
428
        self.assertEqual(transform.find_conflicts(),
 
429
                         [('versioning no contents', lion_id)])
 
430
        transform.create_file([b'Contents, okay?'], lion_id)
 
431
        transform.adjust_path('name2', trans_id2, trans_id2)
 
432
        self.assertEqual(transform.find_conflicts(),
 
433
                         [('parent loop', trans_id2),
 
434
                          ('non-directory parent', trans_id2)])
 
435
        transform.adjust_path('name2', root, trans_id2)
 
436
        oz_id = transform.new_directory('oz', root)
 
437
        transform.set_executability(True, oz_id)
 
438
        self.assertEqual(transform.find_conflicts(),
 
439
                         [('unversioned executability', oz_id)])
 
440
        transform.version_file(oz_id, file_id=b'oz-id')
 
441
        self.assertEqual(transform.find_conflicts(),
 
442
                         [('non-file executability', oz_id)])
 
443
        transform.set_executability(None, oz_id)
 
444
        tip_id = transform.new_file('tip', oz_id, [b'ozma'], b'tip-id')
 
445
        transform.apply()
 
446
        self.assertEqual(self.wt.path2id('name'), b'my_pretties')
 
447
        with open(self.wt.abspath('name'), 'rb') as f:
 
448
            self.assertEqual(b'contents', f.read())
 
449
        transform2, root = self.transform()
 
450
        oz_id = transform2.trans_id_tree_path('oz')
 
451
        newtip = transform2.new_file('tip', oz_id, [b'other'], b'tip-id')
 
452
        result = transform2.find_conflicts()
 
453
        fp = FinalPaths(transform2)
 
454
        self.assertTrue('oz/tip' in transform2._tree_path_ids)
 
455
        self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
 
456
        self.assertEqual(len(result), 2)
 
457
        self.assertEqual((result[0][0], result[0][1]),
 
458
                         ('duplicate', newtip))
 
459
        self.assertEqual((result[1][0], result[1][2]),
 
460
                         ('duplicate id', newtip))
 
461
        transform2.finalize()
 
462
        transform3 = self.wt.transform()
 
463
        self.addCleanup(transform3.finalize)
 
464
        oz_id = transform3.trans_id_tree_path('oz')
 
465
        transform3.delete_contents(oz_id)
 
466
        self.assertEqual(transform3.find_conflicts(),
 
467
                         [('missing parent', oz_id)])
 
468
        root_id = transform3.root
 
469
        tip_id = transform3.trans_id_tree_path('oz/tip')
 
470
        transform3.adjust_path('tip', root_id, tip_id)
 
471
        transform3.apply()
 
472
 
 
473
    def test_conflict_on_case_insensitive(self):
 
474
        tree = self.make_branch_and_tree('tree')
 
475
        # Don't try this at home, kids!
 
476
        # Force the tree to report that it is case sensitive, for conflict
 
477
        # resolution tests
 
478
        tree.case_sensitive = True
 
479
        transform = tree.transform()
 
480
        self.addCleanup(transform.finalize)
 
481
        transform.new_file('file', transform.root, [b'content'])
 
482
        transform.new_file('FiLe', transform.root, [b'content'])
 
483
        result = transform.find_conflicts()
 
484
        self.assertEqual([], result)
 
485
        transform.finalize()
 
486
        # Force the tree to report that it is case insensitive, for conflict
 
487
        # generation tests
 
488
        tree.case_sensitive = False
 
489
        transform = tree.transform()
 
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([('duplicate', 'new-1', 'new-2', 'file')], result)
 
495
 
 
496
    def test_conflict_on_case_insensitive_existing(self):
 
497
        tree = self.make_branch_and_tree('tree')
 
498
        self.build_tree(['tree/FiLe'])
 
499
        # Don't try this at home, kids!
 
500
        # Force the tree to report that it is case sensitive, for conflict
 
501
        # resolution tests
 
502
        tree.case_sensitive = True
 
503
        transform = tree.transform()
 
504
        self.addCleanup(transform.finalize)
 
505
        transform.new_file('file', transform.root, [b'content'])
 
506
        result = transform.find_conflicts()
 
507
        self.assertEqual([], result)
 
508
        transform.finalize()
 
509
        # Force the tree to report that it is case insensitive, for conflict
 
510
        # generation tests
 
511
        tree.case_sensitive = False
 
512
        transform = tree.transform()
 
513
        self.addCleanup(transform.finalize)
 
514
        transform.new_file('file', transform.root, [b'content'])
 
515
        result = transform.find_conflicts()
 
516
        self.assertEqual([('duplicate', 'new-1', 'new-2', 'file')], result)
 
517
 
 
518
    def test_resolve_case_insensitive_conflict(self):
 
519
        tree = self.make_branch_and_tree('tree')
 
520
        # Don't try this at home, kids!
 
521
        # Force the tree to report that it is case insensitive, for conflict
 
522
        # resolution tests
 
523
        tree.case_sensitive = False
 
524
        transform = tree.transform()
 
525
        self.addCleanup(transform.finalize)
 
526
        transform.new_file('file', transform.root, [b'content'])
 
527
        transform.new_file('FiLe', transform.root, [b'content'])
 
528
        resolve_conflicts(transform)
 
529
        transform.apply()
 
530
        self.assertPathExists('tree/file')
 
531
        self.assertPathExists('tree/FiLe.moved')
 
532
 
 
533
    def test_resolve_checkout_case_conflict(self):
 
534
        tree = self.make_branch_and_tree('tree')
 
535
        # Don't try this at home, kids!
 
536
        # Force the tree to report that it is case insensitive, for conflict
 
537
        # resolution tests
 
538
        tree.case_sensitive = False
 
539
        transform = tree.transform()
 
540
        self.addCleanup(transform.finalize)
 
541
        transform.new_file('file', transform.root, [b'content'])
 
542
        transform.new_file('FiLe', transform.root, [b'content'])
 
543
        resolve_conflicts(transform,
 
544
                          pass_func=lambda t, c: resolve_checkout(t, c, []))
 
545
        transform.apply()
 
546
        self.assertPathExists('tree/file')
 
547
        self.assertPathExists('tree/FiLe.moved')
 
548
 
 
549
    def test_apply_case_conflict(self):
 
550
        """Ensure that a transform with case conflicts can always be applied"""
 
551
        tree = self.make_branch_and_tree('tree')
 
552
        transform = tree.transform()
 
553
        self.addCleanup(transform.finalize)
 
554
        transform.new_file('file', transform.root, [b'content'])
 
555
        transform.new_file('FiLe', transform.root, [b'content'])
 
556
        dir = transform.new_directory('dir', transform.root)
 
557
        transform.new_file('dirfile', dir, [b'content'])
 
558
        transform.new_file('dirFiLe', dir, [b'content'])
 
559
        resolve_conflicts(transform)
 
560
        transform.apply()
 
561
        self.assertPathExists('tree/file')
 
562
        if not os.path.exists('tree/FiLe.moved'):
 
563
            self.assertPathExists('tree/FiLe')
 
564
        self.assertPathExists('tree/dir/dirfile')
 
565
        if not os.path.exists('tree/dir/dirFiLe.moved'):
 
566
            self.assertPathExists('tree/dir/dirFiLe')
 
567
 
 
568
    def test_case_insensitive_limbo(self):
 
569
        tree = self.make_branch_and_tree('tree')
 
570
        # Don't try this at home, kids!
 
571
        # Force the tree to report that it is case insensitive
 
572
        tree.case_sensitive = False
 
573
        transform = tree.transform()
 
574
        self.addCleanup(transform.finalize)
 
575
        dir = transform.new_directory('dir', transform.root)
 
576
        first = transform.new_file('file', dir, [b'content'])
 
577
        second = transform.new_file('FiLe', dir, [b'content'])
 
578
        self.assertContainsRe(transform._limbo_name(first), 'new-1/file')
 
579
        self.assertNotContainsRe(transform._limbo_name(second), 'new-1/FiLe')
 
580
 
 
581
    def test_adjust_path_updates_child_limbo_names(self):
 
582
        tree = self.make_branch_and_tree('tree')
 
583
        transform = tree.transform()
 
584
        self.addCleanup(transform.finalize)
 
585
        foo_id = transform.new_directory('foo', transform.root)
 
586
        bar_id = transform.new_directory('bar', foo_id)
 
587
        baz_id = transform.new_directory('baz', bar_id)
 
588
        qux_id = transform.new_directory('qux', baz_id)
 
589
        transform.adjust_path('quxx', foo_id, bar_id)
 
590
        self.assertStartsWith(transform._limbo_name(qux_id),
 
591
                              transform._limbo_name(bar_id))
 
592
 
 
593
    def test_add_del(self):
 
594
        start, root = self.transform()
 
595
        start.new_directory('a', root, b'a')
 
596
        start.apply()
 
597
        transform, root = self.transform()
 
598
        transform.delete_versioned(transform.trans_id_tree_path('a'))
 
599
        transform.new_directory('a', root, b'a')
 
600
        transform.apply()
 
601
 
 
602
    def test_unversioning(self):
 
603
        create_tree, root = self.transform()
 
604
        parent_id = create_tree.new_directory('parent', root, b'parent-id')
 
605
        create_tree.new_file('child', parent_id, [b'child'], b'child-id')
 
606
        create_tree.apply()
 
607
        unversion = self.wt.transform()
 
608
        self.addCleanup(unversion.finalize)
 
609
        parent = unversion.trans_id_tree_path('parent')
 
610
        unversion.unversion_file(parent)
 
611
        self.assertEqual(unversion.find_conflicts(),
 
612
                         [('unversioned parent', parent_id)])
 
613
        file_id = unversion.trans_id_tree_path('parent/child')
 
614
        unversion.unversion_file(file_id)
 
615
        unversion.apply()
 
616
 
 
617
    def test_name_invariants(self):
 
618
        create_tree, root = self.transform()
 
619
        # prepare tree
 
620
        root = create_tree.root
 
621
        create_tree.new_file('name1', root, [b'hello1'], b'name1')
 
622
        create_tree.new_file('name2', root, [b'hello2'], b'name2')
 
623
        ddir = create_tree.new_directory('dying_directory', root, b'ddir')
 
624
        create_tree.new_file('dying_file', ddir, [b'goodbye1'], b'dfile')
 
625
        create_tree.new_file('moving_file', ddir, [b'later1'], b'mfile')
 
626
        create_tree.new_file('moving_file2', root, [b'later2'], b'mfile2')
 
627
        create_tree.apply()
 
628
 
 
629
        mangle_tree, root = self.transform()
 
630
        root = mangle_tree.root
 
631
        # swap names
 
632
        name1 = mangle_tree.trans_id_tree_path('name1')
 
633
        name2 = mangle_tree.trans_id_tree_path('name2')
 
634
        mangle_tree.adjust_path('name2', root, name1)
 
635
        mangle_tree.adjust_path('name1', root, name2)
 
636
 
 
637
        # tests for deleting parent directories
 
638
        ddir = mangle_tree.trans_id_tree_path('dying_directory')
 
639
        mangle_tree.delete_contents(ddir)
 
640
        dfile = mangle_tree.trans_id_tree_path('dying_directory/dying_file')
 
641
        mangle_tree.delete_versioned(dfile)
 
642
        mangle_tree.unversion_file(dfile)
 
643
        mfile = mangle_tree.trans_id_tree_path('dying_directory/moving_file')
 
644
        mangle_tree.adjust_path('mfile', root, mfile)
 
645
 
 
646
        # tests for adding parent directories
 
647
        newdir = mangle_tree.new_directory('new_directory', root, b'newdir')
 
648
        mfile2 = mangle_tree.trans_id_tree_path('moving_file2')
 
649
        mangle_tree.adjust_path('mfile2', newdir, mfile2)
 
650
        mangle_tree.new_file('newfile', newdir, [b'hello3'], b'dfile')
 
651
        self.assertEqual(mangle_tree.final_file_id(mfile2), b'mfile2')
 
652
        self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
 
653
        self.assertEqual(mangle_tree.final_file_id(mfile2), b'mfile2')
 
654
        mangle_tree.apply()
 
655
        with open(self.wt.abspath('name1'), 'r') as f:
 
656
            self.assertEqual(f.read(), 'hello2')
 
657
        with open(self.wt.abspath('name2'), 'r') as f:
 
658
            self.assertEqual(f.read(), 'hello1')
 
659
        mfile2_path = self.wt.abspath(pathjoin('new_directory', 'mfile2'))
 
660
        self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
 
661
        with open(mfile2_path, 'r') as f:
 
662
            self.assertEqual(f.read(), 'later2')
 
663
        self.assertEqual(self.wt.id2path(b'mfile2'), 'new_directory/mfile2')
 
664
        self.assertEqual(self.wt.path2id('new_directory/mfile2'), b'mfile2')
 
665
        newfile_path = self.wt.abspath(pathjoin('new_directory', 'newfile'))
 
666
        with open(newfile_path, 'r') as f:
 
667
            self.assertEqual(f.read(), 'hello3')
 
668
        self.assertEqual(self.wt.path2id('dying_directory'), b'ddir')
 
669
        self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
 
670
        mfile2_path = self.wt.abspath(pathjoin('new_directory', 'mfile2'))
 
671
 
 
672
    def test_both_rename(self):
 
673
        create_tree, root = self.transform()
 
674
        newdir = create_tree.new_directory('selftest', root, b'selftest-id')
 
675
        create_tree.new_file('blackbox.py', newdir, [
 
676
                             b'hello1'], b'blackbox-id')
 
677
        create_tree.apply()
 
678
        mangle_tree, root = self.transform()
 
679
        selftest = mangle_tree.trans_id_tree_path('selftest')
 
680
        blackbox = mangle_tree.trans_id_tree_path('selftest/blackbox.py')
 
681
        mangle_tree.adjust_path('test', root, selftest)
 
682
        mangle_tree.adjust_path('test_too_much', root, selftest)
 
683
        mangle_tree.set_executability(True, blackbox)
 
684
        mangle_tree.apply()
 
685
 
 
686
    def test_both_rename2(self):
 
687
        create_tree, root = self.transform()
 
688
        breezy = create_tree.new_directory('breezy', root, b'breezy-id')
 
689
        tests = create_tree.new_directory('tests', breezy, b'tests-id')
 
690
        blackbox = create_tree.new_directory('blackbox', tests, b'blackbox-id')
 
691
        create_tree.new_file('test_too_much.py', blackbox, [b'hello1'],
 
692
                             b'test_too_much-id')
 
693
        create_tree.apply()
 
694
        mangle_tree, root = self.transform()
 
695
        breezy = mangle_tree.trans_id_tree_path('breezy')
 
696
        tests = mangle_tree.trans_id_tree_path('breezy/tests')
 
697
        test_too_much = mangle_tree.trans_id_tree_path(
 
698
            'breezy/tests/blackbox/test_too_much.py')
 
699
        mangle_tree.adjust_path('selftest', breezy, tests)
 
700
        mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
 
701
        mangle_tree.set_executability(True, test_too_much)
 
702
        mangle_tree.apply()
 
703
 
 
704
    def test_both_rename3(self):
 
705
        create_tree, root = self.transform()
 
706
        tests = create_tree.new_directory('tests', root, b'tests-id')
 
707
        create_tree.new_file('test_too_much.py', tests, [b'hello1'],
 
708
                             b'test_too_much-id')
 
709
        create_tree.apply()
 
710
        mangle_tree, root = self.transform()
 
711
        tests = mangle_tree.trans_id_tree_path('tests')
 
712
        test_too_much = mangle_tree.trans_id_tree_path(
 
713
            'tests/test_too_much.py')
 
714
        mangle_tree.adjust_path('selftest', root, tests)
 
715
        mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
 
716
        mangle_tree.set_executability(True, test_too_much)
 
717
        mangle_tree.apply()
 
718
 
 
719
    def test_move_dangling_ie(self):
 
720
        create_tree, root = self.transform()
 
721
        # prepare tree
 
722
        root = create_tree.root
 
723
        create_tree.new_file('name1', root, [b'hello1'], b'name1')
 
724
        create_tree.apply()
 
725
        delete_contents, root = self.transform()
 
726
        file = delete_contents.trans_id_tree_path('name1')
 
727
        delete_contents.delete_contents(file)
 
728
        delete_contents.apply()
 
729
        move_id, root = self.transform()
 
730
        name1 = move_id.trans_id_tree_path('name1')
 
731
        newdir = move_id.new_directory('dir', root, b'newdir')
 
732
        move_id.adjust_path('name2', newdir, name1)
 
733
        move_id.apply()
 
734
 
 
735
    def test_replace_dangling_ie(self):
 
736
        create_tree, root = self.transform()
 
737
        # prepare tree
 
738
        root = create_tree.root
 
739
        create_tree.new_file('name1', root, [b'hello1'], b'name1')
 
740
        create_tree.apply()
 
741
        delete_contents = self.wt.transform()
 
742
        self.addCleanup(delete_contents.finalize)
 
743
        file = delete_contents.trans_id_tree_path('name1')
 
744
        delete_contents.delete_contents(file)
 
745
        delete_contents.apply()
 
746
        delete_contents.finalize()
 
747
        replace = self.wt.transform()
 
748
        self.addCleanup(replace.finalize)
 
749
        name2 = replace.new_file('name2', root, [b'hello2'], b'name1')
 
750
        conflicts = replace.find_conflicts()
 
751
        name1 = replace.trans_id_tree_path('name1')
 
752
        self.assertEqual(conflicts, [('duplicate id', name1, name2)])
 
753
        resolve_conflicts(replace)
 
754
        replace.apply()
 
755
 
 
756
    def _test_symlinks(self, link_name1, link_target1,
 
757
                       link_name2, link_target2):
 
758
 
 
759
        def ozpath(p):
 
760
            return 'oz/' + p
 
761
 
 
762
        self.requireFeature(SymlinkFeature)
 
763
        transform, root = self.transform()
 
764
        oz_id = transform.new_directory('oz', root, b'oz-id')
 
765
        transform.new_symlink(link_name1, oz_id, link_target1, b'wizard-id')
 
766
        wiz_id = transform.create_path(link_name2, oz_id)
 
767
        transform.create_symlink(link_target2, wiz_id)
 
768
        transform.version_file(wiz_id, file_id=b'wiz-id2')
 
769
        transform.set_executability(True, wiz_id)
 
770
        self.assertEqual(transform.find_conflicts(),
 
771
                         [('non-file executability', wiz_id)])
 
772
        transform.set_executability(None, wiz_id)
 
773
        transform.apply()
 
774
        self.assertEqual(self.wt.path2id(ozpath(link_name1)), b'wizard-id')
 
775
        self.assertEqual('symlink',
 
776
                         file_kind(self.wt.abspath(ozpath(link_name1))))
 
777
        self.assertEqual(link_target2,
 
778
                         osutils.readlink(self.wt.abspath(ozpath(link_name2))))
 
779
        self.assertEqual(link_target1,
 
780
                         osutils.readlink(self.wt.abspath(ozpath(link_name1))))
 
781
 
 
782
    def test_symlinks(self):
 
783
        self._test_symlinks('wizard', 'wizard-target',
 
784
                            'wizard2', 'behind_curtain')
 
785
 
 
786
    def test_symlinks_unicode(self):
 
787
        self.requireFeature(features.UnicodeFilenameFeature)
 
788
        self._test_symlinks(u'\N{Euro Sign}wizard',
 
789
                            u'wizard-targ\N{Euro Sign}t',
 
790
                            u'\N{Euro Sign}wizard2',
 
791
                            u'b\N{Euro Sign}hind_curtain')
 
792
 
 
793
    def test_unsupported_symlink_no_conflict(self):
 
794
        def tt_helper():
 
795
            wt = self.make_branch_and_tree('.')
 
796
            tt = wt.transform()
 
797
            self.addCleanup(tt.finalize)
 
798
            tt.new_symlink('foo', tt.root, 'bar')
 
799
            result = tt.find_conflicts()
 
800
            self.assertEqual([], result)
 
801
        os_symlink = getattr(os, 'symlink', None)
 
802
        os.symlink = None
 
803
        try:
 
804
            tt_helper()
 
805
        finally:
 
806
            if os_symlink:
 
807
                os.symlink = os_symlink
 
808
 
 
809
    def get_conflicted(self):
 
810
        create, root = self.transform()
 
811
        create.new_file('dorothy', root, [b'dorothy'], b'dorothy-id')
 
812
        oz = create.new_directory('oz', root, b'oz-id')
 
813
        create.new_directory('emeraldcity', oz, b'emerald-id')
 
814
        create.apply()
 
815
        conflicts, root = self.transform()
 
816
        # set up duplicate entry, duplicate id
 
817
        new_dorothy = conflicts.new_file('dorothy', root, [b'dorothy'],
 
818
                                         b'dorothy-id')
 
819
        old_dorothy = conflicts.trans_id_tree_path('dorothy')
 
820
        oz = conflicts.trans_id_tree_path('oz')
 
821
        # set up DeletedParent parent conflict
 
822
        conflicts.delete_versioned(oz)
 
823
        emerald = conflicts.trans_id_tree_path('oz/emeraldcity')
 
824
        # set up MissingParent conflict
 
825
        munchkincity = conflicts.trans_id_file_id(b'munchkincity-id')
 
826
        conflicts.adjust_path('munchkincity', root, munchkincity)
 
827
        conflicts.new_directory('auntem', munchkincity, b'auntem-id')
 
828
        # set up parent loop
 
829
        conflicts.adjust_path('emeraldcity', emerald, emerald)
 
830
        return conflicts, emerald, oz, old_dorothy, new_dorothy
 
831
 
 
832
    def test_conflict_resolution(self):
 
833
        conflicts, emerald, oz, old_dorothy, new_dorothy =\
 
834
            self.get_conflicted()
 
835
        resolve_conflicts(conflicts)
 
836
        self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
 
837
        self.assertIs(conflicts.final_file_id(old_dorothy), None)
 
838
        self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
 
839
        self.assertEqual(conflicts.final_file_id(new_dorothy), b'dorothy-id')
 
840
        self.assertEqual(conflicts.final_parent(emerald), oz)
 
841
        conflicts.apply()
 
842
 
 
843
    def test_cook_conflicts(self):
 
844
        tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
 
845
        raw_conflicts = resolve_conflicts(tt)
 
846
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
847
        duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved',
 
848
                                   'dorothy', None, b'dorothy-id')
 
849
        self.assertEqual(cooked_conflicts[0], duplicate)
 
850
        duplicate_id = DuplicateID('Unversioned existing file',
 
851
                                   'dorothy.moved', 'dorothy', None,
 
852
                                   b'dorothy-id')
 
853
        self.assertEqual(cooked_conflicts[1], duplicate_id)
 
854
        missing_parent = MissingParent('Created directory', 'munchkincity',
 
855
                                       b'munchkincity-id')
 
856
        deleted_parent = DeletingParent('Not deleting', 'oz', b'oz-id')
 
857
        self.assertEqual(cooked_conflicts[2], missing_parent)
 
858
        unversioned_parent = UnversionedParent('Versioned directory',
 
859
                                               'munchkincity',
 
860
                                               b'munchkincity-id')
 
861
        unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
 
862
                                                b'oz-id')
 
863
        self.assertEqual(cooked_conflicts[3], unversioned_parent)
 
864
        parent_loop = ParentLoop(
 
865
            'Cancelled move', 'oz/emeraldcity',
 
866
            'oz/emeraldcity', b'emerald-id', b'emerald-id')
 
867
        self.assertEqual(cooked_conflicts[4], deleted_parent)
 
868
        self.assertEqual(cooked_conflicts[5], unversioned_parent2)
 
869
        self.assertEqual(cooked_conflicts[6], parent_loop)
 
870
        self.assertEqual(len(cooked_conflicts), 7)
 
871
        tt.finalize()
 
872
 
 
873
    def test_string_conflicts(self):
 
874
        tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
 
875
        raw_conflicts = resolve_conflicts(tt)
 
876
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
877
        tt.finalize()
 
878
        conflicts_s = [text_type(c) for c in cooked_conflicts]
 
879
        self.assertEqual(len(cooked_conflicts), len(conflicts_s))
 
880
        self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy.  '
 
881
                                         'Moved existing file to '
 
882
                                         'dorothy.moved.')
 
883
        self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy.  '
 
884
                                         'Unversioned existing file '
 
885
                                         'dorothy.moved.')
 
886
        self.assertEqual(conflicts_s[2], 'Conflict adding files to'
 
887
                                         ' munchkincity.  Created directory.')
 
888
        self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
 
889
                                         ' versioned, but has versioned'
 
890
                                         ' children.  Versioned directory.')
 
891
        self.assertEqualDiff(
 
892
            conflicts_s[4], "Conflict: can't delete oz because it"
 
893
                            " is not empty.  Not deleting.")
 
894
        self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
 
895
                                         ' versioned, but has versioned'
 
896
                                         ' children.  Versioned directory.')
 
897
        self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
 
898
                                         ' oz/emeraldcity. Cancelled move.')
 
899
 
 
900
    def prepare_wrong_parent_kind(self):
 
901
        tt, root = self.transform()
 
902
        tt.new_file('parent', root, [b'contents'], b'parent-id')
 
903
        tt.apply()
 
904
        tt, root = self.transform()
 
905
        parent_id = tt.trans_id_file_id(b'parent-id')
 
906
        tt.new_file('child,', parent_id, [b'contents2'], b'file-id')
 
907
        return tt
 
908
 
 
909
    def test_find_conflicts_wrong_parent_kind(self):
 
910
        tt = self.prepare_wrong_parent_kind()
 
911
        tt.find_conflicts()
 
912
 
 
913
    def test_resolve_conflicts_wrong_existing_parent_kind(self):
 
914
        tt = self.prepare_wrong_parent_kind()
 
915
        raw_conflicts = resolve_conflicts(tt)
 
916
        self.assertEqual({('non-directory parent', 'Created directory',
 
917
                           'new-3')}, raw_conflicts)
 
918
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
919
        self.assertEqual([NonDirectoryParent('Created directory', 'parent.new',
 
920
                                             b'parent-id')], cooked_conflicts)
 
921
        tt.apply()
 
922
        self.assertFalse(self.wt.is_versioned('parent'))
 
923
        self.assertEqual(b'parent-id', self.wt.path2id('parent.new'))
 
924
 
 
925
    def test_resolve_conflicts_wrong_new_parent_kind(self):
 
926
        tt, root = self.transform()
 
927
        parent_id = tt.new_directory('parent', root, b'parent-id')
 
928
        tt.new_file('child,', parent_id, [b'contents2'], b'file-id')
 
929
        tt.apply()
 
930
        tt, root = self.transform()
 
931
        parent_id = tt.trans_id_file_id(b'parent-id')
 
932
        tt.delete_contents(parent_id)
 
933
        tt.create_file([b'contents'], parent_id)
 
934
        raw_conflicts = resolve_conflicts(tt)
 
935
        self.assertEqual({('non-directory parent', 'Created directory',
 
936
                           'new-3')}, raw_conflicts)
 
937
        tt.apply()
 
938
        self.assertFalse(self.wt.is_versioned('parent'))
 
939
        self.assertEqual(b'parent-id', self.wt.path2id('parent.new'))
 
940
 
 
941
    def test_resolve_conflicts_wrong_parent_kind_unversioned(self):
 
942
        tt, root = self.transform()
 
943
        parent_id = tt.new_directory('parent', root)
 
944
        tt.new_file('child,', parent_id, [b'contents2'])
 
945
        tt.apply()
 
946
        tt, root = self.transform()
 
947
        parent_id = tt.trans_id_tree_path('parent')
 
948
        tt.delete_contents(parent_id)
 
949
        tt.create_file([b'contents'], parent_id)
 
950
        resolve_conflicts(tt)
 
951
        tt.apply()
 
952
        self.assertFalse(self.wt.is_versioned('parent'))
 
953
        self.assertFalse(self.wt.is_versioned('parent.new'))
 
954
 
 
955
    def test_resolve_conflicts_missing_parent(self):
 
956
        wt = self.make_branch_and_tree('.')
 
957
        tt = wt.transform()
 
958
        self.addCleanup(tt.finalize)
 
959
        parent = tt.trans_id_file_id(b'parent-id')
 
960
        tt.new_file('file', parent, [b'Contents'])
 
961
        raw_conflicts = resolve_conflicts(tt)
 
962
        # Since the directory doesn't exist it's seen as 'missing'.  So
 
963
        # 'resolve_conflicts' create a conflict asking for it to be created.
 
964
        self.assertLength(1, raw_conflicts)
 
965
        self.assertEqual(('missing parent', 'Created directory', 'new-1'),
 
966
                         raw_conflicts.pop())
 
967
        # apply fail since the missing directory doesn't exist
 
968
        self.assertRaises(NoFinalPath, tt.apply)
 
969
 
 
970
    def test_moving_versioned_directories(self):
 
971
        create, root = self.transform()
 
972
        kansas = create.new_directory('kansas', root, b'kansas-id')
 
973
        create.new_directory('house', kansas, b'house-id')
 
974
        create.new_directory('oz', root, b'oz-id')
 
975
        create.apply()
 
976
        cyclone, root = self.transform()
 
977
        oz = cyclone.trans_id_tree_path('oz')
 
978
        house = cyclone.trans_id_tree_path('house')
 
979
        cyclone.adjust_path('house', oz, house)
 
980
        cyclone.apply()
 
981
 
 
982
    def test_moving_root(self):
 
983
        create, root = self.transform()
 
984
        fun = create.new_directory('fun', root, b'fun-id')
 
985
        create.new_directory('sun', root, b'sun-id')
 
986
        create.new_directory('moon', root, b'moon')
 
987
        create.apply()
 
988
        transform, root = self.transform()
 
989
        transform.adjust_root_path('oldroot', fun)
 
990
        new_root = transform.trans_id_tree_path('')
 
991
        transform.version_file(new_root, file_id=b'new-root')
 
992
        transform.apply()
 
993
 
 
994
    def test_renames(self):
 
995
        create, root = self.transform()
 
996
        old = create.new_directory('old-parent', root, b'old-id')
 
997
        intermediate = create.new_directory('intermediate', old, b'im-id')
 
998
        myfile = create.new_file('myfile', intermediate, [b'myfile-text'],
 
999
                                 b'myfile-id')
 
1000
        create.apply()
 
1001
        rename, root = self.transform()
 
1002
        old = rename.trans_id_file_id(b'old-id')
 
1003
        rename.adjust_path('new', root, old)
 
1004
        myfile = rename.trans_id_file_id(b'myfile-id')
 
1005
        rename.set_executability(True, myfile)
 
1006
        rename.apply()
 
1007
 
 
1008
    def test_rename_fails(self):
 
1009
        self.requireFeature(features.not_running_as_root)
 
1010
        # see https://bugs.launchpad.net/bzr/+bug/491763
 
1011
        create, root_id = self.transform()
 
1012
        create.new_directory('first-dir', root_id, b'first-id')
 
1013
        create.new_file('myfile', root_id, [b'myfile-text'], b'myfile-id')
 
1014
        create.apply()
 
1015
        if os.name == "posix" and sys.platform != "cygwin":
 
1016
            # posix filesystems fail on renaming if the readonly bit is set
 
1017
            osutils.make_readonly(self.wt.abspath('first-dir'))
 
1018
        elif os.name == "nt":
 
1019
            # windows filesystems fail on renaming open files
 
1020
            self.addCleanup(open(self.wt.abspath('myfile')).close)
 
1021
        else:
 
1022
            self.skipTest("Can't force a permissions error on rename")
 
1023
        # now transform to rename
 
1024
        rename_transform, root_id = self.transform()
 
1025
        file_trans_id = rename_transform.trans_id_file_id(b'myfile-id')
 
1026
        dir_id = rename_transform.trans_id_file_id(b'first-id')
 
1027
        rename_transform.adjust_path('newname', dir_id, file_trans_id)
 
1028
        e = self.assertRaises(TransformRenameFailed,
 
1029
                              rename_transform.apply)
 
1030
        # On nix looks like:
 
1031
        # "Failed to rename .../work/.bzr/checkout/limbo/new-1
 
1032
        # to .../first-dir/newname: [Errno 13] Permission denied"
 
1033
        # On windows looks like:
 
1034
        # "Failed to rename .../work/myfile to
 
1035
        # .../work/.bzr/checkout/limbo/new-1: [Errno 13] Permission denied"
 
1036
        # This test isn't concerned with exactly what the error looks like,
 
1037
        # and the strerror will vary across OS and locales, but the assert
 
1038
        # that the exeception attributes are what we expect
 
1039
        self.assertEqual(e.errno, errno.EACCES)
 
1040
        if os.name == "posix":
 
1041
            self.assertEndsWith(e.to_path, "/first-dir/newname")
 
1042
        else:
 
1043
            self.assertEqual(os.path.basename(e.from_path), "myfile")
 
1044
 
 
1045
    def test_set_executability_order(self):
 
1046
        """Ensure that executability behaves the same, no matter what order.
 
1047
 
 
1048
        - create file and set executability simultaneously
 
1049
        - create file and set executability afterward
 
1050
        - unsetting the executability of a file whose executability has not
 
1051
          been
 
1052
        declared should throw an exception (this may happen when a
 
1053
        merge attempts to create a file with a duplicate ID)
 
1054
        """
 
1055
        transform, root = self.transform()
 
1056
        wt = transform._tree
 
1057
        wt.lock_read()
 
1058
        self.addCleanup(wt.unlock)
 
1059
        transform.new_file('set_on_creation', root, [b'Set on creation'],
 
1060
                           b'soc', True)
 
1061
        sac = transform.new_file('set_after_creation', root,
 
1062
                                 [b'Set after creation'], b'sac')
 
1063
        transform.set_executability(True, sac)
 
1064
        uws = transform.new_file('unset_without_set', root, [b'Unset badly'],
 
1065
                                 b'uws')
 
1066
        self.assertRaises(KeyError, transform.set_executability, None, uws)
 
1067
        transform.apply()
 
1068
        self.assertTrue(wt.is_executable('set_on_creation'))
 
1069
        self.assertTrue(wt.is_executable('set_after_creation'))
 
1070
 
 
1071
    def test_preserve_mode(self):
 
1072
        """File mode is preserved when replacing content"""
 
1073
        if sys.platform == 'win32':
 
1074
            raise TestSkipped('chmod has no effect on win32')
 
1075
        transform, root = self.transform()
 
1076
        transform.new_file('file1', root, [b'contents'], b'file1-id', True)
 
1077
        transform.apply()
 
1078
        self.wt.lock_write()
 
1079
        self.addCleanup(self.wt.unlock)
 
1080
        self.assertTrue(self.wt.is_executable('file1'))
 
1081
        transform, root = self.transform()
 
1082
        file1_id = transform.trans_id_tree_path('file1')
 
1083
        transform.delete_contents(file1_id)
 
1084
        transform.create_file([b'contents2'], file1_id)
 
1085
        transform.apply()
 
1086
        self.assertTrue(self.wt.is_executable('file1'))
 
1087
 
 
1088
    def test__set_mode_stats_correctly(self):
 
1089
        """_set_mode stats to determine file mode."""
 
1090
        if sys.platform == 'win32':
 
1091
            raise TestSkipped('chmod has no effect on win32')
 
1092
 
 
1093
        stat_paths = []
 
1094
        real_stat = os.stat
 
1095
 
 
1096
        def instrumented_stat(path):
 
1097
            stat_paths.append(path)
 
1098
            return real_stat(path)
 
1099
 
 
1100
        transform, root = self.transform()
 
1101
 
 
1102
        bar1_id = transform.new_file('bar', root, [b'bar contents 1\n'],
 
1103
                                     file_id=b'bar-id-1', executable=False)
 
1104
        transform.apply()
 
1105
 
 
1106
        transform, root = self.transform()
 
1107
        bar1_id = transform.trans_id_tree_path('bar')
 
1108
        bar2_id = transform.trans_id_tree_path('bar2')
 
1109
        try:
 
1110
            os.stat = instrumented_stat
 
1111
            transform.create_file([b'bar2 contents\n'],
 
1112
                                  bar2_id, mode_id=bar1_id)
 
1113
        finally:
 
1114
            os.stat = real_stat
 
1115
            transform.finalize()
 
1116
 
 
1117
        bar1_abspath = self.wt.abspath('bar')
 
1118
        self.assertEqual([bar1_abspath], stat_paths)
 
1119
 
 
1120
    def test_iter_changes(self):
 
1121
        self.wt.set_root_id(b'eert_toor')
 
1122
        transform, root = self.transform()
 
1123
        transform.new_file('old', root, [b'blah'], b'id-1', True)
 
1124
        transform.apply()
 
1125
        transform, root = self.transform()
 
1126
        try:
 
1127
            self.assertEqual([], list(transform.iter_changes()))
 
1128
            old = transform.trans_id_tree_path('old')
 
1129
            transform.unversion_file(old)
 
1130
            self.assertEqual([(b'id-1', ('old', None), False, (True, False),
 
1131
                               (b'eert_toor', b'eert_toor'),
 
1132
                               ('old', 'old'), ('file', 'file'),
 
1133
                               (True, True), False)],
 
1134
                             list(transform.iter_changes()))
 
1135
            transform.new_directory('new', root, b'id-1')
 
1136
            self.assertEqual([(b'id-1', ('old', 'new'), True, (True, True),
 
1137
                               (b'eert_toor', b'eert_toor'), ('old', 'new'),
 
1138
                               ('file', 'directory'),
 
1139
                               (True, False), False)],
 
1140
                             list(transform.iter_changes()))
 
1141
        finally:
 
1142
            transform.finalize()
 
1143
 
 
1144
    def test_iter_changes_new(self):
 
1145
        self.wt.set_root_id(b'eert_toor')
 
1146
        transform, root = self.transform()
 
1147
        transform.new_file('old', root, [b'blah'])
 
1148
        transform.apply()
 
1149
        transform, root = self.transform()
 
1150
        try:
 
1151
            old = transform.trans_id_tree_path('old')
 
1152
            transform.version_file(old, file_id=b'id-1')
 
1153
            self.assertEqual([(b'id-1', (None, 'old'), False, (False, True),
 
1154
                               (b'eert_toor', b'eert_toor'),
 
1155
                               ('old', 'old'), ('file', 'file'),
 
1156
                               (False, False), False)],
 
1157
                             list(transform.iter_changes()))
 
1158
        finally:
 
1159
            transform.finalize()
 
1160
 
 
1161
    def test_iter_changes_modifications(self):
 
1162
        self.wt.set_root_id(b'eert_toor')
 
1163
        transform, root = self.transform()
 
1164
        transform.new_file('old', root, [b'blah'], b'id-1')
 
1165
        transform.new_file('new', root, [b'blah'])
 
1166
        transform.new_directory('subdir', root, b'subdir-id')
 
1167
        transform.apply()
 
1168
        transform, root = self.transform()
 
1169
        try:
 
1170
            old = transform.trans_id_tree_path('old')
 
1171
            subdir = transform.trans_id_tree_path('subdir')
 
1172
            new = transform.trans_id_tree_path('new')
 
1173
            self.assertEqual([], list(transform.iter_changes()))
 
1174
 
 
1175
            # content deletion
 
1176
            transform.delete_contents(old)
 
1177
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1178
                               (b'eert_toor', b'eert_toor'),
 
1179
                               ('old', 'old'), ('file', None),
 
1180
                               (False, False), False)],
 
1181
                             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'),
 
1187
                               ('old', 'old'), ('file', 'file'),
 
1188
                               (False, False), False)],
 
1189
                             list(transform.iter_changes()))
 
1190
            transform.cancel_deletion(old)
 
1191
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1192
                               (b'eert_toor', b'eert_toor'),
 
1193
                               ('old', 'old'), ('file', 'file'),
 
1194
                               (False, False), False)],
 
1195
                             list(transform.iter_changes()))
 
1196
            transform.cancel_creation(old)
 
1197
 
 
1198
            # move file_id to a different file
 
1199
            self.assertEqual([], list(transform.iter_changes()))
 
1200
            transform.unversion_file(old)
 
1201
            transform.version_file(new, file_id=b'id-1')
 
1202
            transform.adjust_path('old', root, new)
 
1203
            self.assertEqual([(b'id-1', ('old', 'old'), True, (True, True),
 
1204
                               (b'eert_toor', b'eert_toor'),
 
1205
                               ('old', 'old'), ('file', 'file'),
 
1206
                               (False, False), False)],
 
1207
                             list(transform.iter_changes()))
 
1208
            transform.cancel_versioning(new)
 
1209
            transform._removed_id = set()
 
1210
 
 
1211
            # execute bit
 
1212
            self.assertEqual([], list(transform.iter_changes()))
 
1213
            transform.set_executability(True, old)
 
1214
            self.assertEqual([(b'id-1', ('old', 'old'), False, (True, True),
 
1215
                               (b'eert_toor', b'eert_toor'),
 
1216
                               ('old', 'old'), ('file', 'file'),
 
1217
                               (False, True), False)],
 
1218
                             list(transform.iter_changes()))
 
1219
            transform.set_executability(None, old)
 
1220
 
 
1221
            # filename
 
1222
            self.assertEqual([], list(transform.iter_changes()))
 
1223
            transform.adjust_path('new', root, old)
 
1224
            transform._new_parent = {}
 
1225
            self.assertEqual([(b'id-1', ('old', 'new'), False, (True, True),
 
1226
                               (b'eert_toor', b'eert_toor'),
 
1227
                               ('old', 'new'), ('file', 'file'),
 
1228
                               (False, False), False)],
 
1229
                             list(transform.iter_changes()))
 
1230
            transform._new_name = {}
 
1231
 
 
1232
            # parent directory
 
1233
            self.assertEqual([], list(transform.iter_changes()))
 
1234
            transform.adjust_path('new', subdir, old)
 
1235
            transform._new_name = {}
 
1236
            self.assertEqual([(b'id-1', ('old', 'subdir/old'), False,
 
1237
                               (True, True), (b'eert_toor',
 
1238
                                              b'subdir-id'), ('old', 'old'),
 
1239
                               ('file', 'file'), (False, False), False)],
 
1240
                             list(transform.iter_changes()))
 
1241
            transform._new_path = {}
 
1242
 
 
1243
        finally:
 
1244
            transform.finalize()
 
1245
 
 
1246
    def test_iter_changes_modified_bleed(self):
 
1247
        self.wt.set_root_id(b'eert_toor')
 
1248
        """Modified flag should not bleed from one change to another"""
 
1249
        # unfortunately, we have no guarantee that file1 (which is modified)
 
1250
        # will be applied before file2.  And if it's applied after file2, it
 
1251
        # obviously can't bleed into file2's change output.  But for now, it
 
1252
        # works.
 
1253
        transform, root = self.transform()
 
1254
        transform.new_file('file1', root, [b'blah'], b'id-1')
 
1255
        transform.new_file('file2', root, [b'blah'], b'id-2')
 
1256
        transform.apply()
 
1257
        transform, root = self.transform()
 
1258
        try:
 
1259
            transform.delete_contents(transform.trans_id_file_id(b'id-1'))
 
1260
            transform.set_executability(True,
 
1261
                                        transform.trans_id_file_id(b'id-2'))
 
1262
            self.assertEqual(
 
1263
                [(b'id-1', (u'file1', u'file1'), True, (True, True),
 
1264
                 (b'eert_toor', b'eert_toor'), ('file1', u'file1'),
 
1265
                 ('file', None), (False, False), False),
 
1266
                 (b'id-2', (u'file2', u'file2'), False, (True, True),
 
1267
                 (b'eert_toor', b'eert_toor'), ('file2', u'file2'),
 
1268
                 ('file', 'file'), (False, True), False)],
 
1269
                list(transform.iter_changes()))
 
1270
        finally:
 
1271
            transform.finalize()
 
1272
 
 
1273
    def test_iter_changes_move_missing(self):
 
1274
        """Test moving ids with no files around"""
 
1275
        self.wt.set_root_id(b'toor_eert')
 
1276
        # Need two steps because versioning a non-existant file is a conflict.
 
1277
        transform, root = self.transform()
 
1278
        transform.new_directory('floater', root, b'floater-id')
 
1279
        transform.apply()
 
1280
        transform, root = self.transform()
 
1281
        transform.delete_contents(transform.trans_id_tree_path('floater'))
 
1282
        transform.apply()
 
1283
        transform, root = self.transform()
 
1284
        floater = transform.trans_id_tree_path('floater')
 
1285
        try:
 
1286
            transform.adjust_path('flitter', root, floater)
 
1287
            self.assertEqual([(b'floater-id', ('floater', 'flitter'), False,
 
1288
                               (True, True),
 
1289
                               (b'toor_eert', b'toor_eert'),
 
1290
                               ('floater', 'flitter'),
 
1291
                               (None, None), (False, False), False)],
 
1292
                             list(transform.iter_changes()))
 
1293
        finally:
 
1294
            transform.finalize()
 
1295
 
 
1296
    def test_iter_changes_pointless(self):
 
1297
        """Ensure that no-ops are not treated as modifications"""
 
1298
        self.wt.set_root_id(b'eert_toor')
 
1299
        transform, root = self.transform()
 
1300
        transform.new_file('old', root, [b'blah'], b'id-1')
 
1301
        transform.new_directory('subdir', root, b'subdir-id')
 
1302
        transform.apply()
 
1303
        transform, root = self.transform()
 
1304
        try:
 
1305
            old = transform.trans_id_tree_path('old')
 
1306
            subdir = transform.trans_id_tree_path('subdir')
 
1307
            self.assertEqual([], list(transform.iter_changes()))
 
1308
            transform.delete_contents(subdir)
 
1309
            transform.create_directory(subdir)
 
1310
            transform.set_executability(False, old)
 
1311
            transform.unversion_file(old)
 
1312
            transform.version_file(old, file_id=b'id-1')
 
1313
            transform.adjust_path('old', root, old)
 
1314
            self.assertEqual([], list(transform.iter_changes()))
 
1315
        finally:
 
1316
            transform.finalize()
 
1317
 
 
1318
    def test_rename_count(self):
 
1319
        transform, root = self.transform()
 
1320
        transform.new_file('name1', root, [b'contents'])
 
1321
        self.assertEqual(transform.rename_count, 0)
 
1322
        transform.apply()
 
1323
        self.assertEqual(transform.rename_count, 1)
 
1324
        transform2, root = self.transform()
 
1325
        transform2.adjust_path('name2', root,
 
1326
                               transform2.trans_id_tree_path('name1'))
 
1327
        self.assertEqual(transform2.rename_count, 0)
 
1328
        transform2.apply()
 
1329
        self.assertEqual(transform2.rename_count, 2)
 
1330
 
 
1331
    def test_change_parent(self):
 
1332
        """Ensure that after we change a parent, the results are still right.
 
1333
 
 
1334
        Renames and parent changes on pending transforms can happen as part
 
1335
        of conflict resolution, and are explicitly permitted by the
 
1336
        TreeTransform API.
 
1337
 
 
1338
        This test ensures they work correctly with the rename-avoidance
 
1339
        optimization.
 
1340
        """
 
1341
        transform, root = self.transform()
 
1342
        parent1 = transform.new_directory('parent1', root)
 
1343
        child1 = transform.new_file('child1', parent1, [b'contents'])
 
1344
        parent2 = transform.new_directory('parent2', root)
 
1345
        transform.adjust_path('child1', parent2, child1)
 
1346
        transform.apply()
 
1347
        self.assertPathDoesNotExist(self.wt.abspath('parent1/child1'))
 
1348
        self.assertPathExists(self.wt.abspath('parent2/child1'))
 
1349
        # rename limbo/new-1 => parent1, rename limbo/new-3 => parent2
 
1350
        # no rename for child1 (counting only renames during apply)
 
1351
        self.assertEqual(2, transform.rename_count)
 
1352
 
 
1353
    def test_cancel_parent(self):
 
1354
        """Cancelling a parent doesn't cause deletion of a non-empty directory
 
1355
 
 
1356
        This is like the test_change_parent, except that we cancel the parent
 
1357
        before adjusting the path.  The transform must detect that the
 
1358
        directory is non-empty, and move children to safe locations.
 
1359
        """
 
1360
        transform, root = self.transform()
 
1361
        parent1 = transform.new_directory('parent1', root)
 
1362
        child1 = transform.new_file('child1', parent1, [b'contents'])
 
1363
        child2 = transform.new_file('child2', parent1, [b'contents'])
 
1364
        try:
 
1365
            transform.cancel_creation(parent1)
 
1366
        except OSError:
 
1367
            self.fail('Failed to move child1 before deleting parent1')
 
1368
        transform.cancel_creation(child2)
 
1369
        transform.create_directory(parent1)
 
1370
        try:
 
1371
            transform.cancel_creation(parent1)
 
1372
        # If the transform incorrectly believes that child2 is still in
 
1373
        # parent1's limbo directory, it will try to rename it and fail
 
1374
        # because was already moved by the first cancel_creation.
 
1375
        except OSError:
 
1376
            self.fail('Transform still thinks child2 is a child of parent1')
 
1377
        parent2 = transform.new_directory('parent2', root)
 
1378
        transform.adjust_path('child1', parent2, child1)
 
1379
        transform.apply()
 
1380
        self.assertPathDoesNotExist(self.wt.abspath('parent1'))
 
1381
        self.assertPathExists(self.wt.abspath('parent2/child1'))
 
1382
        # rename limbo/new-3 => parent2, rename limbo/new-2 => child1
 
1383
        self.assertEqual(2, transform.rename_count)
 
1384
 
 
1385
    def test_adjust_and_cancel(self):
 
1386
        """Make sure adjust_path keeps track of limbo children properly"""
 
1387
        transform, root = self.transform()
 
1388
        parent1 = transform.new_directory('parent1', root)
 
1389
        child1 = transform.new_file('child1', parent1, [b'contents'])
 
1390
        parent2 = transform.new_directory('parent2', root)
 
1391
        transform.adjust_path('child1', parent2, child1)
 
1392
        transform.cancel_creation(child1)
 
1393
        try:
 
1394
            transform.cancel_creation(parent1)
 
1395
        # if the transform thinks child1 is still in parent1's limbo
 
1396
        # directory, it will attempt to move it and fail.
 
1397
        except OSError:
 
1398
            self.fail('Transform still thinks child1 is a child of parent1')
 
1399
        transform.finalize()
 
1400
 
 
1401
    def test_noname_contents(self):
 
1402
        """TreeTransform should permit deferring naming files."""
 
1403
        transform, root = self.transform()
 
1404
        parent = transform.trans_id_file_id(b'parent-id')
 
1405
        try:
 
1406
            transform.create_directory(parent)
 
1407
        except KeyError:
 
1408
            self.fail("Can't handle contents with no name")
 
1409
        transform.finalize()
 
1410
 
 
1411
    def test_noname_contents_nested(self):
 
1412
        """TreeTransform should permit deferring naming files."""
 
1413
        transform, root = self.transform()
 
1414
        parent = transform.trans_id_file_id(b'parent-id')
 
1415
        try:
 
1416
            transform.create_directory(parent)
 
1417
        except KeyError:
 
1418
            self.fail("Can't handle contents with no name")
 
1419
        transform.new_directory('child', parent)
 
1420
        transform.adjust_path('parent', root, parent)
 
1421
        transform.apply()
 
1422
        self.assertPathExists(self.wt.abspath('parent/child'))
 
1423
        self.assertEqual(1, transform.rename_count)
 
1424
 
 
1425
    def test_reuse_name(self):
 
1426
        """Avoid reusing the same limbo name for different files"""
 
1427
        transform, root = self.transform()
 
1428
        parent = transform.new_directory('parent', root)
 
1429
        transform.new_directory('child', parent)
 
1430
        try:
 
1431
            child2 = transform.new_directory('child', parent)
 
1432
        except OSError:
 
1433
            self.fail('Tranform tried to use the same limbo name twice')
 
1434
        transform.adjust_path('child2', parent, child2)
 
1435
        transform.apply()
 
1436
        # limbo/new-1 => parent, limbo/new-3 => parent/child2
 
1437
        # child2 is put into top-level limbo because child1 has already
 
1438
        # claimed the direct limbo path when child2 is created.  There is no
 
1439
        # advantage in renaming files once they're in top-level limbo, except
 
1440
        # as part of apply.
 
1441
        self.assertEqual(2, transform.rename_count)
 
1442
 
 
1443
    def test_reuse_when_first_moved(self):
 
1444
        """Don't avoid direct paths when it is safe to use them"""
 
1445
        transform, root = self.transform()
 
1446
        parent = transform.new_directory('parent', root)
 
1447
        child1 = transform.new_directory('child', parent)
 
1448
        transform.adjust_path('child1', parent, child1)
 
1449
        transform.new_directory('child', parent)
 
1450
        transform.apply()
 
1451
        # limbo/new-1 => parent
 
1452
        self.assertEqual(1, transform.rename_count)
 
1453
 
 
1454
    def test_reuse_after_cancel(self):
 
1455
        """Don't avoid direct paths when it is safe to use them"""
 
1456
        transform, root = self.transform()
 
1457
        parent2 = transform.new_directory('parent2', root)
 
1458
        child1 = transform.new_directory('child1', parent2)
 
1459
        transform.cancel_creation(parent2)
 
1460
        transform.create_directory(parent2)
 
1461
        transform.new_directory('child1', parent2)
 
1462
        transform.adjust_path('child2', parent2, child1)
 
1463
        transform.apply()
 
1464
        # limbo/new-1 => parent2, limbo/new-2 => parent2/child1
 
1465
        self.assertEqual(2, transform.rename_count)
 
1466
 
 
1467
    def test_finalize_order(self):
 
1468
        """Finalize must be done in child-to-parent order"""
 
1469
        transform, root = self.transform()
 
1470
        parent = transform.new_directory('parent', root)
 
1471
        transform.new_directory('child', parent)
 
1472
        try:
 
1473
            transform.finalize()
 
1474
        except OSError:
 
1475
            self.fail('Tried to remove parent before child1')
 
1476
 
 
1477
    def test_cancel_with_cancelled_child_should_succeed(self):
 
1478
        transform, root = self.transform()
 
1479
        parent = transform.new_directory('parent', root)
 
1480
        child = transform.new_directory('child', parent)
 
1481
        transform.cancel_creation(child)
 
1482
        transform.cancel_creation(parent)
 
1483
        transform.finalize()
 
1484
 
 
1485
    def test_rollback_on_directory_clash(self):
 
1486
        def tt_helper():
 
1487
            wt = self.make_branch_and_tree('.')
 
1488
            tt = wt.transform()  # TreeTransform obtains write lock
 
1489
            try:
 
1490
                foo = tt.new_directory('foo', tt.root)
 
1491
                tt.new_file('bar', foo, [b'foobar'])
 
1492
                baz = tt.new_directory('baz', tt.root)
 
1493
                tt.new_file('qux', baz, [b'quux'])
 
1494
                # Ask for a rename 'foo' -> 'baz'
 
1495
                tt.adjust_path('baz', tt.root, foo)
 
1496
                # Lie to tt that we've already resolved all conflicts.
 
1497
                tt.apply(no_conflicts=True)
 
1498
            except BaseException:
 
1499
                wt.unlock()
 
1500
                raise
 
1501
        # The rename will fail because the target directory is not empty (but
 
1502
        # raises FileExists anyway).
 
1503
        err = self.assertRaises(errors.FileExists, tt_helper)
 
1504
        self.assertEndsWith(err.path, "/baz")
 
1505
 
 
1506
    def test_two_directories_clash(self):
 
1507
        def tt_helper():
 
1508
            wt = self.make_branch_and_tree('.')
 
1509
            tt = wt.transform()  # TreeTransform obtains write lock
 
1510
            try:
 
1511
                foo_1 = tt.new_directory('foo', tt.root)
 
1512
                tt.new_directory('bar', foo_1)
 
1513
                # Adding the same directory with a different content
 
1514
                foo_2 = tt.new_directory('foo', tt.root)
 
1515
                tt.new_directory('baz', foo_2)
 
1516
                # Lie to tt that we've already resolved all conflicts.
 
1517
                tt.apply(no_conflicts=True)
 
1518
            except BaseException:
 
1519
                wt.unlock()
 
1520
                raise
 
1521
        err = self.assertRaises(errors.FileExists, tt_helper)
 
1522
        self.assertEndsWith(err.path, "/foo")
 
1523
 
 
1524
    def test_two_directories_clash_finalize(self):
 
1525
        def tt_helper():
 
1526
            wt = self.make_branch_and_tree('.')
 
1527
            tt = wt.transform()  # TreeTransform obtains write lock
 
1528
            try:
 
1529
                foo_1 = tt.new_directory('foo', tt.root)
 
1530
                tt.new_directory('bar', foo_1)
 
1531
                # Adding the same directory with a different content
 
1532
                foo_2 = tt.new_directory('foo', tt.root)
 
1533
                tt.new_directory('baz', foo_2)
 
1534
                # Lie to tt that we've already resolved all conflicts.
 
1535
                tt.apply(no_conflicts=True)
 
1536
            except BaseException:
 
1537
                tt.finalize()
 
1538
                raise
 
1539
        err = self.assertRaises(errors.FileExists, tt_helper)
 
1540
        self.assertEndsWith(err.path, "/foo")
 
1541
 
 
1542
    def test_file_to_directory(self):
 
1543
        wt = self.make_branch_and_tree('.')
 
1544
        self.build_tree(['foo'])
 
1545
        wt.add(['foo'])
 
1546
        wt.commit("one")
 
1547
        tt = wt.transform()
 
1548
        self.addCleanup(tt.finalize)
 
1549
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1550
        tt.delete_contents(foo_trans_id)
 
1551
        tt.create_directory(foo_trans_id)
 
1552
        bar_trans_id = tt.trans_id_tree_path("foo/bar")
 
1553
        tt.create_file([b"aa\n"], bar_trans_id)
 
1554
        tt.version_file(bar_trans_id, file_id=b"bar-1")
 
1555
        tt.apply()
 
1556
        self.assertPathExists("foo/bar")
 
1557
        with wt.lock_read():
 
1558
            self.assertEqual(wt.kind("foo"), "directory")
 
1559
        wt.commit("two")
 
1560
        changes = wt.changes_from(wt.basis_tree())
 
1561
        self.assertFalse(changes.has_changed(), changes)
 
1562
 
 
1563
    def test_file_to_symlink(self):
 
1564
        self.requireFeature(SymlinkFeature)
 
1565
        wt = self.make_branch_and_tree('.')
 
1566
        self.build_tree(['foo'])
 
1567
        wt.add(['foo'])
 
1568
        wt.commit("one")
 
1569
        tt = wt.transform()
 
1570
        self.addCleanup(tt.finalize)
 
1571
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1572
        tt.delete_contents(foo_trans_id)
 
1573
        tt.create_symlink("bar", foo_trans_id)
 
1574
        tt.apply()
 
1575
        self.assertPathExists("foo")
 
1576
        wt.lock_read()
 
1577
        self.addCleanup(wt.unlock)
 
1578
        self.assertEqual(wt.kind("foo"), "symlink")
 
1579
 
 
1580
    def test_file_to_symlink_unsupported(self):
 
1581
        wt = self.make_branch_and_tree('.')
 
1582
        self.build_tree(['foo'])
 
1583
        wt.add(['foo'])
 
1584
        wt.commit("one")
 
1585
        self.overrideAttr(osutils, 'supports_symlinks', lambda p: False)
 
1586
        tt = wt.transform()
 
1587
        self.addCleanup(tt.finalize)
 
1588
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1589
        tt.delete_contents(foo_trans_id)
 
1590
        log = BytesIO()
 
1591
        trace.push_log_file(log)
 
1592
        tt.create_symlink("bar", foo_trans_id)
 
1593
        tt.apply()
 
1594
        self.assertContainsRe(
 
1595
            log.getvalue(),
 
1596
            b'Unable to create symlink "foo" on this filesystem')
 
1597
 
 
1598
    def test_dir_to_file(self):
 
1599
        wt = self.make_branch_and_tree('.')
 
1600
        self.build_tree(['foo/', 'foo/bar'])
 
1601
        wt.add(['foo', 'foo/bar'])
 
1602
        wt.commit("one")
 
1603
        tt = wt.transform()
 
1604
        self.addCleanup(tt.finalize)
 
1605
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1606
        bar_trans_id = tt.trans_id_tree_path("foo/bar")
 
1607
        tt.delete_contents(foo_trans_id)
 
1608
        tt.delete_versioned(bar_trans_id)
 
1609
        tt.create_file([b"aa\n"], foo_trans_id)
 
1610
        tt.apply()
 
1611
        self.assertPathExists("foo")
 
1612
        wt.lock_read()
 
1613
        self.addCleanup(wt.unlock)
 
1614
        self.assertEqual(wt.kind("foo"), "file")
 
1615
 
 
1616
    def test_dir_to_hardlink(self):
 
1617
        self.requireFeature(HardlinkFeature)
 
1618
        wt = self.make_branch_and_tree('.')
 
1619
        self.build_tree(['foo/', 'foo/bar'])
 
1620
        wt.add(['foo', 'foo/bar'])
 
1621
        wt.commit("one")
 
1622
        tt = wt.transform()
 
1623
        self.addCleanup(tt.finalize)
 
1624
        foo_trans_id = tt.trans_id_tree_path("foo")
 
1625
        bar_trans_id = tt.trans_id_tree_path("foo/bar")
 
1626
        tt.delete_contents(foo_trans_id)
 
1627
        tt.delete_versioned(bar_trans_id)
 
1628
        self.build_tree(['baz'])
 
1629
        tt.create_hardlink("baz", foo_trans_id)
 
1630
        tt.apply()
 
1631
        self.assertPathExists("foo")
 
1632
        self.assertPathExists("baz")
 
1633
        wt.lock_read()
 
1634
        self.addCleanup(wt.unlock)
 
1635
        self.assertEqual(wt.kind("foo"), "file")
 
1636
 
 
1637
    def test_no_final_path(self):
 
1638
        transform, root = self.transform()
 
1639
        trans_id = transform.trans_id_file_id(b'foo')
 
1640
        transform.create_file([b'bar'], trans_id)
 
1641
        transform.cancel_creation(trans_id)
 
1642
        transform.apply()
 
1643
 
 
1644
    def test_create_from_tree(self):
 
1645
        tree1 = self.make_branch_and_tree('tree1')
 
1646
        self.build_tree_contents([('tree1/foo/',), ('tree1/bar', b'baz')])
 
1647
        tree1.add(['foo', 'bar'], [b'foo-id', b'bar-id'])
 
1648
        tree2 = self.make_branch_and_tree('tree2')
 
1649
        tt = tree2.transform()
 
1650
        foo_trans_id = tt.create_path('foo', tt.root)
 
1651
        create_from_tree(tt, foo_trans_id, tree1, 'foo')
 
1652
        bar_trans_id = tt.create_path('bar', tt.root)
 
1653
        create_from_tree(tt, bar_trans_id, tree1, 'bar')
 
1654
        tt.apply()
 
1655
        self.assertEqual('directory', osutils.file_kind('tree2/foo'))
 
1656
        self.assertFileEqual(b'baz', 'tree2/bar')
 
1657
 
 
1658
    def test_create_from_tree_bytes(self):
 
1659
        """Provided lines are used instead of tree content."""
 
1660
        tree1 = self.make_branch_and_tree('tree1')
 
1661
        self.build_tree_contents([('tree1/foo', b'bar'), ])
 
1662
        tree1.add('foo', b'foo-id')
 
1663
        tree2 = self.make_branch_and_tree('tree2')
 
1664
        tt = tree2.transform()
 
1665
        foo_trans_id = tt.create_path('foo', tt.root)
 
1666
        create_from_tree(tt, foo_trans_id, tree1, 'foo', chunks=[b'qux'])
 
1667
        tt.apply()
 
1668
        self.assertFileEqual(b'qux', 'tree2/foo')
 
1669
 
 
1670
    def test_create_from_tree_symlink(self):
 
1671
        self.requireFeature(SymlinkFeature)
 
1672
        tree1 = self.make_branch_and_tree('tree1')
 
1673
        os.symlink('bar', 'tree1/foo')
 
1674
        tree1.add('foo', b'foo-id')
 
1675
        tt = self.make_branch_and_tree('tree2').transform()
 
1676
        foo_trans_id = tt.create_path('foo', tt.root)
 
1677
        create_from_tree(tt, foo_trans_id, tree1, 'foo')
 
1678
        tt.apply()
 
1679
        self.assertEqual('bar', os.readlink('tree2/foo'))