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,
78
class TestTreeTransform(tests.TestCaseWithTransport):
81
super(TestTreeTransform, self).setUp()
82
self.wt = self.make_branch_and_tree('wt')
85
transform = self.wt.transform()
86
self.addCleanup(transform.finalize)
87
return transform, transform.root
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)
96
trans._creation_mtime = time.time() - 20.0
97
return trans, root, contents, sha1
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'))
110
os.rmdir(deletion_path)
111
transform, root = self.transform()
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)
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?
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',
173
orig = self.wt._observed_sha1
175
def _observed_sha1(*args):
178
self.wt._observed_sha1 = _observed_sha1
180
self.assertEqual([('file1', trans._observed_sha1s[trans_id])],
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)
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))
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)
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',
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)
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',
226
self.assertTrue(trans_id in trans._observed_sha1s)
227
trans.cancel_creation(trans_id)
228
self.assertFalse(trans_id in trans._observed_sha1s)
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
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))
243
fo, st1 = self.wt.get_file_with_stat('one', filtered=False)
245
fo, st2 = self.wt.get_file_with_stat('two', filtered=False)
247
# We only guarantee 2s resolution
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)
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()
262
self.assertEqual(b'new-root-id', self.wt.path2id(''))
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'],
270
transform.delete_contents(root)
271
transform.unversion_file(root)
272
transform.fixup_new_roots()
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'))
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)
284
def test_retain_existing_root(self):
285
tt, root = self.transform()
287
tt.new_directory('', ROOT_PARENT, b'new-root-id')
289
self.assertNotEqual(b'new-root-id', tt.final_file_id(tt.root))
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')
296
self.assertEqual(tt.root, tt.final_parent(child))
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)
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()
313
self.assertEqual(old_root_id, self.wt.path2id(''))
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)
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))
328
def test_apply_retains_root_directory(self):
329
# Do not attempt to delete the physical root directory, because that
331
transform, root = self.transform()
333
transform.delete_contents(root)
334
e = self.assertRaises(AssertionError, self.assertRaises,
335
TransformRenameFailed,
337
self.assertContainsRe('TransformRenameFailed not raised', str(e))
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)
344
self.assertEqual(old_root_id, self.wt.path2id(''))
346
def test_hardlink(self):
347
self.requireFeature(HardlinkFeature)
348
transform, root = self.transform()
349
transform.new_file('file1', root, [b'contents'])
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)
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',
371
self.assertEqual(len(transform.find_conflicts()), 0)
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')
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)
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)
393
self.addCleanup(tree.unlock)
396
tree.root_inventory.get_entry(b'subtree-id').reference_revision)
398
def test_conflicts(self):
399
transform, root = self.transform()
400
trans_id = transform.new_file('name', root, [b'contents'],
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')
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)
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
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)
486
# Force the tree to report that it is case insensitive, for conflict
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)
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
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)
509
# Force the tree to report that it is case insensitive, for conflict
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)
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
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)
530
self.assertPathExists('tree/file')
531
self.assertPathExists('tree/FiLe.moved')
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
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, []))
546
self.assertPathExists('tree/file')
547
self.assertPathExists('tree/FiLe.moved')
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)
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')
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')
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))
593
def test_add_del(self):
594
start, root = self.transform()
595
start.new_directory('a', root, b'a')
597
transform, root = self.transform()
598
transform.delete_versioned(transform.trans_id_tree_path('a'))
599
transform.new_directory('a', root, b'a')
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')
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)
617
def test_name_invariants(self):
618
create_tree, root = self.transform()
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')
629
mangle_tree, root = self.transform()
630
root = mangle_tree.root
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)
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)
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')
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'))
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')
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)
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'],
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)
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'],
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)
719
def test_move_dangling_ie(self):
720
create_tree, root = self.transform()
722
root = create_tree.root
723
create_tree.new_file('name1', root, [b'hello1'], b'name1')
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)
735
def test_replace_dangling_ie(self):
736
create_tree, root = self.transform()
738
root = create_tree.root
739
create_tree.new_file('name1', root, [b'hello1'], b'name1')
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)
756
def _test_symlinks(self, link_name1, link_target1,
757
link_name2, link_target2):
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)
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))))
782
def test_symlinks(self):
783
self._test_symlinks('wizard', 'wizard-target',
784
'wizard2', 'behind_curtain')
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')
793
def test_unsupported_symlink_no_conflict(self):
795
wt = self.make_branch_and_tree('.')
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)
807
os.symlink = os_symlink
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')
815
conflicts, root = self.transform()
816
# set up duplicate entry, duplicate id
817
new_dorothy = conflicts.new_file('dorothy', root, [b'dorothy'],
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')
829
conflicts.adjust_path('emeraldcity', emerald, emerald)
830
return conflicts, emerald, oz, old_dorothy, new_dorothy
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)
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,
853
self.assertEqual(cooked_conflicts[1], duplicate_id)
854
missing_parent = MissingParent('Created directory', 'munchkincity',
856
deleted_parent = DeletingParent('Not deleting', 'oz', b'oz-id')
857
self.assertEqual(cooked_conflicts[2], missing_parent)
858
unversioned_parent = UnversionedParent('Versioned directory',
861
unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
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)
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)
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 '
883
self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy. '
884
'Unversioned existing file '
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.')
900
def prepare_wrong_parent_kind(self):
901
tt, root = self.transform()
902
tt.new_file('parent', root, [b'contents'], b'parent-id')
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')
909
def test_find_conflicts_wrong_parent_kind(self):
910
tt = self.prepare_wrong_parent_kind()
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)
922
self.assertFalse(self.wt.is_versioned('parent'))
923
self.assertEqual(b'parent-id', self.wt.path2id('parent.new'))
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')
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)
938
self.assertFalse(self.wt.is_versioned('parent'))
939
self.assertEqual(b'parent-id', self.wt.path2id('parent.new'))
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'])
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)
952
self.assertFalse(self.wt.is_versioned('parent'))
953
self.assertFalse(self.wt.is_versioned('parent.new'))
955
def test_resolve_conflicts_missing_parent(self):
956
wt = self.make_branch_and_tree('.')
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'),
967
# apply fail since the missing directory doesn't exist
968
self.assertRaises(NoFinalPath, tt.apply)
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')
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)
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')
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')
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'],
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)
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')
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)
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")
1043
self.assertEqual(os.path.basename(e.from_path), "myfile")
1045
def test_set_executability_order(self):
1046
"""Ensure that executability behaves the same, no matter what order.
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
1052
declared should throw an exception (this may happen when a
1053
merge attempts to create a file with a duplicate ID)
1055
transform, root = self.transform()
1056
wt = transform._tree
1058
self.addCleanup(wt.unlock)
1059
transform.new_file('set_on_creation', root, [b'Set on creation'],
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'],
1066
self.assertRaises(KeyError, transform.set_executability, None, uws)
1068
self.assertTrue(wt.is_executable('set_on_creation'))
1069
self.assertTrue(wt.is_executable('set_after_creation'))
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)
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)
1086
self.assertTrue(self.wt.is_executable('file1'))
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')
1096
def instrumented_stat(path):
1097
stat_paths.append(path)
1098
return real_stat(path)
1100
transform, root = self.transform()
1102
bar1_id = transform.new_file('bar', root, [b'bar contents 1\n'],
1103
file_id=b'bar-id-1', executable=False)
1106
transform, root = self.transform()
1107
bar1_id = transform.trans_id_tree_path('bar')
1108
bar2_id = transform.trans_id_tree_path('bar2')
1110
os.stat = instrumented_stat
1111
transform.create_file([b'bar2 contents\n'],
1112
bar2_id, mode_id=bar1_id)
1115
transform.finalize()
1117
bar1_abspath = self.wt.abspath('bar')
1118
self.assertEqual([bar1_abspath], stat_paths)
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)
1125
transform, root = self.transform()
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()))
1142
transform.finalize()
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'])
1149
transform, root = self.transform()
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()))
1159
transform.finalize()
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')
1168
transform, root = self.transform()
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()))
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()))
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)
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()
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)
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 = {}
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 = {}
1244
transform.finalize()
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
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')
1257
transform, root = self.transform()
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'))
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()))
1271
transform.finalize()
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')
1280
transform, root = self.transform()
1281
transform.delete_contents(transform.trans_id_tree_path('floater'))
1283
transform, root = self.transform()
1284
floater = transform.trans_id_tree_path('floater')
1286
transform.adjust_path('flitter', root, floater)
1287
self.assertEqual([(b'floater-id', ('floater', 'flitter'), False,
1289
(b'toor_eert', b'toor_eert'),
1290
('floater', 'flitter'),
1291
(None, None), (False, False), False)],
1292
list(transform.iter_changes()))
1294
transform.finalize()
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')
1303
transform, root = self.transform()
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()))
1316
transform.finalize()
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)
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)
1329
self.assertEqual(transform2.rename_count, 2)
1331
def test_change_parent(self):
1332
"""Ensure that after we change a parent, the results are still right.
1334
Renames and parent changes on pending transforms can happen as part
1335
of conflict resolution, and are explicitly permitted by the
1338
This test ensures they work correctly with the rename-avoidance
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)
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)
1353
def test_cancel_parent(self):
1354
"""Cancelling a parent doesn't cause deletion of a non-empty directory
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.
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'])
1365
transform.cancel_creation(parent1)
1367
self.fail('Failed to move child1 before deleting parent1')
1368
transform.cancel_creation(child2)
1369
transform.create_directory(parent1)
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.
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)
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)
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)
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.
1398
self.fail('Transform still thinks child1 is a child of parent1')
1399
transform.finalize()
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')
1406
transform.create_directory(parent)
1408
self.fail("Can't handle contents with no name")
1409
transform.finalize()
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')
1416
transform.create_directory(parent)
1418
self.fail("Can't handle contents with no name")
1419
transform.new_directory('child', parent)
1420
transform.adjust_path('parent', root, parent)
1422
self.assertPathExists(self.wt.abspath('parent/child'))
1423
self.assertEqual(1, transform.rename_count)
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)
1431
child2 = transform.new_directory('child', parent)
1433
self.fail('Tranform tried to use the same limbo name twice')
1434
transform.adjust_path('child2', parent, child2)
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
1441
self.assertEqual(2, transform.rename_count)
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)
1451
# limbo/new-1 => parent
1452
self.assertEqual(1, transform.rename_count)
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)
1464
# limbo/new-1 => parent2, limbo/new-2 => parent2/child1
1465
self.assertEqual(2, transform.rename_count)
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)
1473
transform.finalize()
1475
self.fail('Tried to remove parent before child1')
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()
1485
def test_rollback_on_directory_clash(self):
1487
wt = self.make_branch_and_tree('.')
1488
tt = wt.transform() # TreeTransform obtains write lock
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:
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")
1506
def test_two_directories_clash(self):
1508
wt = self.make_branch_and_tree('.')
1509
tt = wt.transform() # TreeTransform obtains write lock
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:
1521
err = self.assertRaises(errors.FileExists, tt_helper)
1522
self.assertEndsWith(err.path, "/foo")
1524
def test_two_directories_clash_finalize(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_file_to_directory(self):
1543
wt = self.make_branch_and_tree('.')
1544
self.build_tree(['foo'])
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")
1556
self.assertPathExists("foo/bar")
1557
with wt.lock_read():
1558
self.assertEqual(wt.kind("foo"), "directory")
1560
changes = wt.changes_from(wt.basis_tree())
1561
self.assertFalse(changes.has_changed(), changes)
1563
def test_file_to_symlink(self):
1564
self.requireFeature(SymlinkFeature)
1565
wt = self.make_branch_and_tree('.')
1566
self.build_tree(['foo'])
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)
1575
self.assertPathExists("foo")
1577
self.addCleanup(wt.unlock)
1578
self.assertEqual(wt.kind("foo"), "symlink")
1580
def test_file_to_symlink_unsupported(self):
1581
wt = self.make_branch_and_tree('.')
1582
self.build_tree(['foo'])
1585
self.overrideAttr(osutils, 'supports_symlinks', lambda p: False)
1587
self.addCleanup(tt.finalize)
1588
foo_trans_id = tt.trans_id_tree_path("foo")
1589
tt.delete_contents(foo_trans_id)
1591
trace.push_log_file(log)
1592
tt.create_symlink("bar", foo_trans_id)
1594
self.assertContainsRe(
1596
b'Unable to create symlink "foo" on this filesystem')
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'])
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)
1611
self.assertPathExists("foo")
1613
self.addCleanup(wt.unlock)
1614
self.assertEqual(wt.kind("foo"), "file")
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'])
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)
1631
self.assertPathExists("foo")
1632
self.assertPathExists("baz")
1634
self.addCleanup(wt.unlock)
1635
self.assertEqual(wt.kind("foo"), "file")
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)
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')
1655
self.assertEqual('directory', osutils.file_kind('tree2/foo'))
1656
self.assertFileEqual(b'baz', 'tree2/bar')
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'])
1668
self.assertFileEqual(b'qux', 'tree2/foo')
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')
1679
self.assertEqual('bar', os.readlink('tree2/foo'))