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