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