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