1
# Copyright (C) 2006 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27
from bzrlib.bzrdir import BzrDir
28
from bzrlib.conflicts import (DuplicateEntry, DuplicateID, MissingParent,
29
UnversionedParent, ParentLoop, DeletingParent,)
30
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
31
ReusingTransform, CantMoveRoot,
32
PathsNotVersionedError, ExistingLimbo,
33
ImmortalLimbo, LockError)
34
from bzrlib.osutils import file_kind, has_symlinks, pathjoin
35
from bzrlib.merge import Merge3Merger
36
from bzrlib.tests import TestCaseInTempDir, TestSkipped, TestCase
37
from bzrlib.transform import (TreeTransform, ROOT_PARENT, FinalPaths,
38
resolve_conflicts, cook_conflicts,
39
find_interesting, build_tree, get_backup_name)
42
class TestTreeTransform(TestCaseInTempDir):
45
super(TestTreeTransform, self).setUp()
46
self.wt = BzrDir.create_standalone_workingtree('.')
49
def get_transform(self):
50
transform = TreeTransform(self.wt)
51
#self.addCleanup(transform.finalize)
52
return transform, transform.root
54
def test_existing_limbo(self):
55
limbo_name = urlutils.local_path_from_url(
56
self.wt._control_files.controlfilename('limbo'))
57
transform, root = self.get_transform()
58
os.mkdir(pathjoin(limbo_name, 'hehe'))
59
self.assertRaises(ImmortalLimbo, transform.apply)
60
self.assertRaises(LockError, self.wt.unlock)
61
self.assertRaises(ExistingLimbo, self.get_transform)
62
self.assertRaises(LockError, self.wt.unlock)
63
os.rmdir(pathjoin(limbo_name, 'hehe'))
65
transform, root = self.get_transform()
69
transform, root = self.get_transform()
70
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
71
imaginary_id = transform.trans_id_tree_path('imaginary')
72
imaginary_id2 = transform.trans_id_tree_path('imaginary/')
73
self.assertEqual(imaginary_id, imaginary_id2)
74
self.assertEqual(transform.get_tree_parent(imaginary_id), root)
75
self.assertEqual(transform.final_kind(root), 'directory')
76
self.assertEqual(transform.final_file_id(root), self.wt.get_root_id())
77
trans_id = transform.create_path('name', root)
78
self.assertIs(transform.final_file_id(trans_id), None)
79
self.assertRaises(NoSuchFile, transform.final_kind, trans_id)
80
transform.create_file('contents', trans_id)
81
transform.set_executability(True, trans_id)
82
transform.version_file('my_pretties', trans_id)
83
self.assertRaises(DuplicateKey, transform.version_file,
84
'my_pretties', trans_id)
85
self.assertEqual(transform.final_file_id(trans_id), 'my_pretties')
86
self.assertEqual(transform.final_parent(trans_id), root)
87
self.assertIs(transform.final_parent(root), ROOT_PARENT)
88
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
89
oz_id = transform.create_path('oz', root)
90
transform.create_directory(oz_id)
91
transform.version_file('ozzie', oz_id)
92
trans_id2 = transform.create_path('name2', root)
93
transform.create_file('contents', trans_id2)
94
transform.set_executability(False, trans_id2)
95
transform.version_file('my_pretties2', trans_id2)
96
modified_paths = transform.apply().modified_paths
97
self.assertEqual('contents', self.wt.get_file_byname('name').read())
98
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
99
self.assertIs(self.wt.is_executable('my_pretties'), True)
100
self.assertIs(self.wt.is_executable('my_pretties2'), False)
101
self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
102
self.assertEqual(len(modified_paths), 3)
103
tree_mod_paths = [self.wt.id2abspath(f) for f in
104
('ozzie', 'my_pretties', 'my_pretties2')]
105
self.assertSubset(tree_mod_paths, modified_paths)
106
# is it safe to finalize repeatedly?
110
def test_convenience(self):
111
transform, root = self.get_transform()
112
trans_id = transform.new_file('name', root, 'contents',
114
oz = transform.new_directory('oz', root, 'oz-id')
115
dorothy = transform.new_directory('dorothy', oz, 'dorothy-id')
116
toto = transform.new_file('toto', dorothy, 'toto-contents',
119
self.assertEqual(len(transform.find_conflicts()), 0)
121
self.assertRaises(ReusingTransform, transform.find_conflicts)
122
self.assertEqual('contents', file(self.wt.abspath('name')).read())
123
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
124
self.assertIs(self.wt.is_executable('my_pretties'), True)
125
self.assertEqual(self.wt.path2id('oz'), 'oz-id')
126
self.assertEqual(self.wt.path2id('oz/dorothy'), 'dorothy-id')
127
self.assertEqual(self.wt.path2id('oz/dorothy/toto'), 'toto-id')
129
self.assertEqual('toto-contents',
130
self.wt.get_file_byname('oz/dorothy/toto').read())
131
self.assertIs(self.wt.is_executable('toto-id'), False)
133
def test_conflicts(self):
134
transform, root = self.get_transform()
135
trans_id = transform.new_file('name', root, 'contents',
137
self.assertEqual(len(transform.find_conflicts()), 0)
138
trans_id2 = transform.new_file('name', root, 'Crontents', 'toto')
139
self.assertEqual(transform.find_conflicts(),
140
[('duplicate', trans_id, trans_id2, 'name')])
141
self.assertRaises(MalformedTransform, transform.apply)
142
transform.adjust_path('name', trans_id, trans_id2)
143
self.assertEqual(transform.find_conflicts(),
144
[('non-directory parent', trans_id)])
145
tinman_id = transform.trans_id_tree_path('tinman')
146
transform.adjust_path('name', tinman_id, trans_id2)
147
self.assertEqual(transform.find_conflicts(),
148
[('unversioned parent', tinman_id),
149
('missing parent', tinman_id)])
150
lion_id = transform.create_path('lion', root)
151
self.assertEqual(transform.find_conflicts(),
152
[('unversioned parent', tinman_id),
153
('missing parent', tinman_id)])
154
transform.adjust_path('name', lion_id, trans_id2)
155
self.assertEqual(transform.find_conflicts(),
156
[('unversioned parent', lion_id),
157
('missing parent', lion_id)])
158
transform.version_file("Courage", lion_id)
159
self.assertEqual(transform.find_conflicts(),
160
[('missing parent', lion_id),
161
('versioning no contents', lion_id)])
162
transform.adjust_path('name2', root, trans_id2)
163
self.assertEqual(transform.find_conflicts(),
164
[('versioning no contents', lion_id)])
165
transform.create_file('Contents, okay?', lion_id)
166
transform.adjust_path('name2', trans_id2, trans_id2)
167
self.assertEqual(transform.find_conflicts(),
168
[('parent loop', trans_id2),
169
('non-directory parent', trans_id2)])
170
transform.adjust_path('name2', root, trans_id2)
171
oz_id = transform.new_directory('oz', root)
172
transform.set_executability(True, oz_id)
173
self.assertEqual(transform.find_conflicts(),
174
[('unversioned executability', oz_id)])
175
transform.version_file('oz-id', oz_id)
176
self.assertEqual(transform.find_conflicts(),
177
[('non-file executability', oz_id)])
178
transform.set_executability(None, oz_id)
179
tip_id = transform.new_file('tip', oz_id, 'ozma', 'tip-id')
181
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
182
self.assertEqual('contents', file(self.wt.abspath('name')).read())
183
transform2, root = self.get_transform()
184
oz_id = transform2.trans_id_tree_file_id('oz-id')
185
newtip = transform2.new_file('tip', oz_id, 'other', 'tip-id')
186
result = transform2.find_conflicts()
187
fp = FinalPaths(transform2)
188
self.assert_('oz/tip' in transform2._tree_path_ids)
189
self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
190
self.assertEqual(len(result), 2)
191
self.assertEqual((result[0][0], result[0][1]),
192
('duplicate', newtip))
193
self.assertEqual((result[1][0], result[1][2]),
194
('duplicate id', newtip))
195
transform2.finalize()
196
transform3 = TreeTransform(self.wt)
197
self.addCleanup(transform3.finalize)
198
oz_id = transform3.trans_id_tree_file_id('oz-id')
199
transform3.delete_contents(oz_id)
200
self.assertEqual(transform3.find_conflicts(),
201
[('missing parent', oz_id)])
202
root_id = transform3.root
203
tip_id = transform3.trans_id_tree_file_id('tip-id')
204
transform3.adjust_path('tip', root_id, tip_id)
207
def test_add_del(self):
208
start, root = self.get_transform()
209
start.new_directory('a', root, 'a')
211
transform, root = self.get_transform()
212
transform.delete_versioned(transform.trans_id_tree_file_id('a'))
213
transform.new_directory('a', root, 'a')
216
def test_unversioning(self):
217
create_tree, root = self.get_transform()
218
parent_id = create_tree.new_directory('parent', root, 'parent-id')
219
create_tree.new_file('child', parent_id, 'child', 'child-id')
221
unversion = TreeTransform(self.wt)
222
self.addCleanup(unversion.finalize)
223
parent = unversion.trans_id_tree_path('parent')
224
unversion.unversion_file(parent)
225
self.assertEqual(unversion.find_conflicts(),
226
[('unversioned parent', parent_id)])
227
file_id = unversion.trans_id_tree_file_id('child-id')
228
unversion.unversion_file(file_id)
231
def test_name_invariants(self):
232
create_tree, root = self.get_transform()
234
root = create_tree.root
235
create_tree.new_file('name1', root, 'hello1', 'name1')
236
create_tree.new_file('name2', root, 'hello2', 'name2')
237
ddir = create_tree.new_directory('dying_directory', root, 'ddir')
238
create_tree.new_file('dying_file', ddir, 'goodbye1', 'dfile')
239
create_tree.new_file('moving_file', ddir, 'later1', 'mfile')
240
create_tree.new_file('moving_file2', root, 'later2', 'mfile2')
243
mangle_tree,root = self.get_transform()
244
root = mangle_tree.root
246
name1 = mangle_tree.trans_id_tree_file_id('name1')
247
name2 = mangle_tree.trans_id_tree_file_id('name2')
248
mangle_tree.adjust_path('name2', root, name1)
249
mangle_tree.adjust_path('name1', root, name2)
251
#tests for deleting parent directories
252
ddir = mangle_tree.trans_id_tree_file_id('ddir')
253
mangle_tree.delete_contents(ddir)
254
dfile = mangle_tree.trans_id_tree_file_id('dfile')
255
mangle_tree.delete_versioned(dfile)
256
mangle_tree.unversion_file(dfile)
257
mfile = mangle_tree.trans_id_tree_file_id('mfile')
258
mangle_tree.adjust_path('mfile', root, mfile)
260
#tests for adding parent directories
261
newdir = mangle_tree.new_directory('new_directory', root, 'newdir')
262
mfile2 = mangle_tree.trans_id_tree_file_id('mfile2')
263
mangle_tree.adjust_path('mfile2', newdir, mfile2)
264
mangle_tree.new_file('newfile', newdir, 'hello3', 'dfile')
265
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
266
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
267
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
269
self.assertEqual(file(self.wt.abspath('name1')).read(), 'hello2')
270
self.assertEqual(file(self.wt.abspath('name2')).read(), 'hello1')
271
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
272
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
273
self.assertEqual(file(mfile2_path).read(), 'later2')
274
self.assertEqual(self.wt.id2path('mfile2'), 'new_directory/mfile2')
275
self.assertEqual(self.wt.path2id('new_directory/mfile2'), 'mfile2')
276
newfile_path = self.wt.abspath(pathjoin('new_directory','newfile'))
277
self.assertEqual(file(newfile_path).read(), 'hello3')
278
self.assertEqual(self.wt.path2id('dying_directory'), 'ddir')
279
self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
280
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
282
def test_both_rename(self):
283
create_tree,root = self.get_transform()
284
newdir = create_tree.new_directory('selftest', root, 'selftest-id')
285
create_tree.new_file('blackbox.py', newdir, 'hello1', 'blackbox-id')
287
mangle_tree,root = self.get_transform()
288
selftest = mangle_tree.trans_id_tree_file_id('selftest-id')
289
blackbox = mangle_tree.trans_id_tree_file_id('blackbox-id')
290
mangle_tree.adjust_path('test', root, selftest)
291
mangle_tree.adjust_path('test_too_much', root, selftest)
292
mangle_tree.set_executability(True, blackbox)
295
def test_both_rename2(self):
296
create_tree,root = self.get_transform()
297
bzrlib = create_tree.new_directory('bzrlib', root, 'bzrlib-id')
298
tests = create_tree.new_directory('tests', bzrlib, 'tests-id')
299
blackbox = create_tree.new_directory('blackbox', tests, 'blackbox-id')
300
create_tree.new_file('test_too_much.py', blackbox, 'hello1',
303
mangle_tree,root = self.get_transform()
304
bzrlib = mangle_tree.trans_id_tree_file_id('bzrlib-id')
305
tests = mangle_tree.trans_id_tree_file_id('tests-id')
306
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
307
mangle_tree.adjust_path('selftest', bzrlib, tests)
308
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
309
mangle_tree.set_executability(True, test_too_much)
312
def test_both_rename3(self):
313
create_tree,root = self.get_transform()
314
tests = create_tree.new_directory('tests', root, 'tests-id')
315
create_tree.new_file('test_too_much.py', tests, 'hello1',
318
mangle_tree,root = self.get_transform()
319
tests = mangle_tree.trans_id_tree_file_id('tests-id')
320
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
321
mangle_tree.adjust_path('selftest', root, tests)
322
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
323
mangle_tree.set_executability(True, test_too_much)
326
def test_move_dangling_ie(self):
327
create_tree, root = self.get_transform()
329
root = create_tree.root
330
create_tree.new_file('name1', root, 'hello1', 'name1')
332
delete_contents, root = self.get_transform()
333
file = delete_contents.trans_id_tree_file_id('name1')
334
delete_contents.delete_contents(file)
335
delete_contents.apply()
336
move_id, root = self.get_transform()
337
name1 = move_id.trans_id_tree_file_id('name1')
338
newdir = move_id.new_directory('dir', root, 'newdir')
339
move_id.adjust_path('name2', newdir, name1)
342
def test_replace_dangling_ie(self):
343
create_tree, root = self.get_transform()
345
root = create_tree.root
346
create_tree.new_file('name1', root, 'hello1', 'name1')
348
delete_contents = TreeTransform(self.wt)
349
self.addCleanup(delete_contents.finalize)
350
file = delete_contents.trans_id_tree_file_id('name1')
351
delete_contents.delete_contents(file)
352
delete_contents.apply()
353
delete_contents.finalize()
354
replace = TreeTransform(self.wt)
355
self.addCleanup(replace.finalize)
356
name2 = replace.new_file('name2', root, 'hello2', 'name1')
357
conflicts = replace.find_conflicts()
358
name1 = replace.trans_id_tree_file_id('name1')
359
self.assertEqual(conflicts, [('duplicate id', name1, name2)])
360
resolve_conflicts(replace)
363
def test_symlinks(self):
364
if not has_symlinks():
365
raise TestSkipped('Symlinks are not supported on this platform')
366
transform,root = self.get_transform()
367
oz_id = transform.new_directory('oz', root, 'oz-id')
368
wizard = transform.new_symlink('wizard', oz_id, 'wizard-target',
370
wiz_id = transform.create_path('wizard2', oz_id)
371
transform.create_symlink('behind_curtain', wiz_id)
372
transform.version_file('wiz-id2', wiz_id)
373
transform.set_executability(True, wiz_id)
374
self.assertEqual(transform.find_conflicts(),
375
[('non-file executability', wiz_id)])
376
transform.set_executability(None, wiz_id)
378
self.assertEqual(self.wt.path2id('oz/wizard'), 'wizard-id')
379
self.assertEqual(file_kind(self.wt.abspath('oz/wizard')), 'symlink')
380
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard2')),
382
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard')),
386
def get_conflicted(self):
387
create,root = self.get_transform()
388
create.new_file('dorothy', root, 'dorothy', 'dorothy-id')
389
oz = create.new_directory('oz', root, 'oz-id')
390
create.new_directory('emeraldcity', oz, 'emerald-id')
392
conflicts,root = self.get_transform()
393
# set up duplicate entry, duplicate id
394
new_dorothy = conflicts.new_file('dorothy', root, 'dorothy',
396
old_dorothy = conflicts.trans_id_tree_file_id('dorothy-id')
397
oz = conflicts.trans_id_tree_file_id('oz-id')
398
# set up DeletedParent parent conflict
399
conflicts.delete_versioned(oz)
400
emerald = conflicts.trans_id_tree_file_id('emerald-id')
401
# set up MissingParent conflict
402
munchkincity = conflicts.trans_id_file_id('munchkincity-id')
403
conflicts.adjust_path('munchkincity', root, munchkincity)
404
conflicts.new_directory('auntem', munchkincity, 'auntem-id')
406
conflicts.adjust_path('emeraldcity', emerald, emerald)
407
return conflicts, emerald, oz, old_dorothy, new_dorothy
409
def test_conflict_resolution(self):
410
conflicts, emerald, oz, old_dorothy, new_dorothy =\
411
self.get_conflicted()
412
resolve_conflicts(conflicts)
413
self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
414
self.assertIs(conflicts.final_file_id(old_dorothy), None)
415
self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
416
self.assertEqual(conflicts.final_file_id(new_dorothy), 'dorothy-id')
417
self.assertEqual(conflicts.final_parent(emerald), oz)
420
def test_cook_conflicts(self):
421
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
422
raw_conflicts = resolve_conflicts(tt)
423
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
424
duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved',
425
'dorothy', None, 'dorothy-id')
426
self.assertEqual(cooked_conflicts[0], duplicate)
427
duplicate_id = DuplicateID('Unversioned existing file',
428
'dorothy.moved', 'dorothy', None,
430
self.assertEqual(cooked_conflicts[1], duplicate_id)
431
missing_parent = MissingParent('Created directory', 'munchkincity',
433
deleted_parent = DeletingParent('Not deleting', 'oz', 'oz-id')
434
self.assertEqual(cooked_conflicts[2], missing_parent)
435
unversioned_parent = UnversionedParent('Versioned directory',
438
unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
440
self.assertEqual(cooked_conflicts[3], unversioned_parent)
441
parent_loop = ParentLoop('Cancelled move', 'oz/emeraldcity',
442
'oz/emeraldcity', 'emerald-id', 'emerald-id')
443
self.assertEqual(cooked_conflicts[4], deleted_parent)
444
self.assertEqual(cooked_conflicts[5], unversioned_parent2)
445
self.assertEqual(cooked_conflicts[6], parent_loop)
446
self.assertEqual(len(cooked_conflicts), 7)
449
def test_string_conflicts(self):
450
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
451
raw_conflicts = resolve_conflicts(tt)
452
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
454
conflicts_s = [str(c) for c in cooked_conflicts]
455
self.assertEqual(len(cooked_conflicts), len(conflicts_s))
456
self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy. '
457
'Moved existing file to '
459
self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy. '
460
'Unversioned existing file '
462
self.assertEqual(conflicts_s[2], 'Conflict adding files to'
463
' munchkincity. Created directory.')
464
self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
465
' versioned, but has versioned'
466
' children. Versioned directory.')
467
self.assertEqualDiff(conflicts_s[4], "Conflict: can't delete oz because it"
468
" is not empty. Not deleting.")
469
self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
470
' versioned, but has versioned'
471
' children. Versioned directory.')
472
self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
473
' oz/emeraldcity. Cancelled move.')
475
def test_moving_versioned_directories(self):
476
create, root = self.get_transform()
477
kansas = create.new_directory('kansas', root, 'kansas-id')
478
create.new_directory('house', kansas, 'house-id')
479
create.new_directory('oz', root, 'oz-id')
481
cyclone, root = self.get_transform()
482
oz = cyclone.trans_id_tree_file_id('oz-id')
483
house = cyclone.trans_id_tree_file_id('house-id')
484
cyclone.adjust_path('house', oz, house)
487
def test_moving_root(self):
488
create, root = self.get_transform()
489
fun = create.new_directory('fun', root, 'fun-id')
490
create.new_directory('sun', root, 'sun-id')
491
create.new_directory('moon', root, 'moon')
493
transform, root = self.get_transform()
494
transform.adjust_root_path('oldroot', fun)
495
new_root=transform.trans_id_tree_path('')
496
transform.version_file('new-root', new_root)
499
def test_renames(self):
500
create, root = self.get_transform()
501
old = create.new_directory('old-parent', root, 'old-id')
502
intermediate = create.new_directory('intermediate', old, 'im-id')
503
myfile = create.new_file('myfile', intermediate, 'myfile-text',
506
rename, root = self.get_transform()
507
old = rename.trans_id_file_id('old-id')
508
rename.adjust_path('new', root, old)
509
myfile = rename.trans_id_file_id('myfile-id')
510
rename.set_executability(True, myfile)
513
def test_find_interesting(self):
514
create, root = self.get_transform()
516
create.new_file('vfile', root, 'myfile-text', 'myfile-id')
517
create.new_file('uvfile', root, 'othertext')
519
self.assertEqual(find_interesting(wt, wt, ['vfile']),
521
self.assertRaises(PathsNotVersionedError, find_interesting, wt, wt,
524
def test_set_executability_order(self):
525
"""Ensure that executability behaves the same, no matter what order.
527
- create file and set executability simultaneously
528
- create file and set executability afterward
529
- unsetting the executability of a file whose executability has not been
530
declared should throw an exception (this may happen when a
531
merge attempts to create a file with a duplicate ID)
533
transform, root = self.get_transform()
535
transform.new_file('set_on_creation', root, 'Set on creation', 'soc',
537
sac = transform.new_file('set_after_creation', root, 'Set after creation', 'sac')
538
transform.set_executability(True, sac)
539
uws = transform.new_file('unset_without_set', root, 'Unset badly', 'uws')
540
self.assertRaises(KeyError, transform.set_executability, None, uws)
542
self.assertTrue(wt.is_executable('soc'))
543
self.assertTrue(wt.is_executable('sac'))
545
def test_preserve_mode(self):
546
"""File mode is preserved when replacing content"""
547
if sys.platform == 'win32':
548
raise TestSkipped('chmod has no effect on win32')
549
transform, root = self.get_transform()
550
transform.new_file('file1', root, 'contents', 'file1-id', True)
552
self.assertTrue(self.wt.is_executable('file1-id'))
553
transform, root = self.get_transform()
554
file1_id = transform.trans_id_tree_file_id('file1-id')
555
transform.delete_contents(file1_id)
556
transform.create_file('contents2', file1_id)
558
self.assertTrue(self.wt.is_executable('file1-id'))
560
def test__set_mode_stats_correctly(self):
561
"""_set_mode stats to determine file mode."""
562
if sys.platform == 'win32':
563
raise TestSkipped('chmod has no effect on win32')
567
def instrumented_stat(path):
568
stat_paths.append(path)
569
return real_stat(path)
571
transform, root = self.get_transform()
573
bar1_id = transform.new_file('bar', root, 'bar contents 1\n',
574
file_id='bar-id-1', executable=False)
577
transform, root = self.get_transform()
578
bar1_id = transform.trans_id_tree_path('bar')
579
bar2_id = transform.trans_id_tree_path('bar2')
581
os.stat = instrumented_stat
582
transform.create_file('bar2 contents\n', bar2_id, mode_id=bar1_id)
587
bar1_abspath = self.wt.abspath('bar')
588
self.assertEqual([bar1_abspath], stat_paths)
590
def test_iter_changes(self):
591
transform, root = self.get_transform()
592
transform.new_file('old', root, 'blah', 'id-1', True)
594
transform, root = self.get_transform()
596
self.assertEqual([], list(transform._iter_changes()))
597
old = transform.trans_id_tree_file_id('id-1')
598
transform.unversion_file(old)
599
self.assertEqual([('id-1', 'old', False, (True, False),
600
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', 'file'),
601
(True, True))], list(transform._iter_changes()))
602
transform.new_directory('new', root, 'id-1')
603
self.assertEqual([('id-1', 'new', True, (True, True),
604
('TREE_ROOT', 'TREE_ROOT'), ('old', 'new'),
605
('file', 'directory'),
606
(True, False))], list(transform._iter_changes()))
610
def test_iter_changes_new(self):
611
transform, root = self.get_transform()
612
transform.new_file('old', root, 'blah')
614
transform, root = self.get_transform()
616
old = transform.trans_id_tree_path('old')
617
transform.version_file('id-1', old)
618
self.assertEqual([('id-1', 'old', False, (False, True),
619
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', 'file'),
620
(False, False))], list(transform._iter_changes()))
624
def test_iter_changes_modifications(self):
625
transform, root = self.get_transform()
626
transform.new_file('old', root, 'blah', 'id-1')
627
transform.new_file('new', root, 'blah')
628
transform.new_directory('subdir', root, 'subdir-id')
630
transform, root = self.get_transform()
632
old = transform.trans_id_tree_path('old')
633
subdir = transform.trans_id_tree_file_id('subdir-id')
634
new = transform.trans_id_tree_path('new')
635
self.assertEqual([], list(transform._iter_changes()))
638
transform.delete_contents(old)
639
self.assertEqual([('id-1', 'old', True, (True, True),
640
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', None),
641
(False, False))], list(transform._iter_changes()))
644
transform.create_file('blah', old)
645
self.assertEqual([('id-1', 'old', True, (True, True),
646
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', 'file'),
647
(False, False))], list(transform._iter_changes()))
648
transform.cancel_deletion(old)
649
self.assertEqual([('id-1', 'old', True, (True, True),
650
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', 'file'),
651
(False, False))], list(transform._iter_changes()))
652
transform.cancel_creation(old)
654
# move file_id to a different file
655
self.assertEqual([], list(transform._iter_changes()))
656
transform.unversion_file(old)
657
transform.version_file('id-1', new)
658
transform.adjust_path('old', root, new)
659
self.assertEqual([('id-1', 'old', True, (True, True),
660
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', 'file'),
661
(False, False))], list(transform._iter_changes()))
662
transform.cancel_versioning(new)
663
transform._removed_id = set()
666
self.assertEqual([], list(transform._iter_changes()))
667
transform.set_executability(True, old)
668
self.assertEqual([('id-1', 'old', False, (True, True),
669
('TREE_ROOT', 'TREE_ROOT'), ('old', 'old'), ('file', 'file'),
670
(False, True))], list(transform._iter_changes()))
671
transform.set_executability(None, old)
674
self.assertEqual([], list(transform._iter_changes()))
675
transform.adjust_path('new', root, old)
676
transform._new_parent = {}
677
self.assertEqual([('id-1', 'new', False, (True, True),
678
('TREE_ROOT', 'TREE_ROOT'), ('old', 'new'), ('file', 'file'),
679
(False, False))], list(transform._iter_changes()))
680
transform._new_name = {}
683
self.assertEqual([], list(transform._iter_changes()))
684
transform.adjust_path('new', subdir, old)
685
transform._new_name = {}
686
self.assertEqual([('id-1', 'subdir/old', False, (True, True),
687
('TREE_ROOT', 'subdir-id'), ('old', 'old'), ('file', 'file'),
688
(False, False))], list(transform._iter_changes()))
689
transform._new_path = {}
694
def test_iter_changes_modified_bleed(self):
695
"""Modified flag should not bleed from one change to another"""
696
# unfortunately, we have no guarantee that file1 (which is modified)
697
# will be applied before file2. And if it's applied after file2, it
698
# obviously can't bleed into file2's change output. But for now, it
700
transform, root = self.get_transform()
701
transform.new_file('file1', root, 'blah', 'id-1')
702
transform.new_file('file2', root, 'blah', 'id-2')
704
transform, root = self.get_transform()
706
transform.delete_contents(transform.trans_id_file_id('id-1'))
707
transform.set_executability(True,
708
transform.trans_id_file_id('id-2'))
709
self.assertEqual([('id-1', u'file1', True, (True, True),
710
('TREE_ROOT', 'TREE_ROOT'), ('file1', u'file1'),
711
('file', None), (False, False)),
712
('id-2', u'file2', False, (True, True),
713
('TREE_ROOT', 'TREE_ROOT'), ('file2', u'file2'),
714
('file', 'file'), (False, True))],
715
list(transform._iter_changes()))
719
def test_iter_changes_pointless(self):
720
"""Ensure that no-ops are not treated as modifications"""
721
transform, root = self.get_transform()
722
transform.new_file('old', root, 'blah', 'id-1')
723
transform.new_directory('subdir', root, 'subdir-id')
725
transform, root = self.get_transform()
727
old = transform.trans_id_tree_path('old')
728
subdir = transform.trans_id_tree_file_id('subdir-id')
729
self.assertEqual([], list(transform._iter_changes()))
730
transform.delete_contents(subdir)
731
transform.create_directory(subdir)
732
transform.set_executability(False, old)
733
transform.unversion_file(old)
734
transform.version_file('id-1', old)
735
transform.adjust_path('old', root, old)
736
self.assertEqual([], list(transform._iter_changes()))
740
class TransformGroup(object):
741
def __init__(self, dirname, root_id):
744
self.wt = BzrDir.create_standalone_workingtree(dirname)
745
self.wt.set_root_id(root_id)
746
self.b = self.wt.branch
747
self.tt = TreeTransform(self.wt)
748
self.root = self.tt.trans_id_tree_file_id(self.wt.get_root_id())
751
def conflict_text(tree, merge):
752
template = '%s TREE\n%s%s\n%s%s MERGE-SOURCE\n'
753
return template % ('<' * 7, tree, '=' * 7, merge, '>' * 7)
756
class TestTransformMerge(TestCaseInTempDir):
757
def test_text_merge(self):
758
root_id = generate_ids.gen_root_id()
759
base = TransformGroup("base", root_id)
760
base.tt.new_file('a', base.root, 'a\nb\nc\nd\be\n', 'a')
761
base.tt.new_file('b', base.root, 'b1', 'b')
762
base.tt.new_file('c', base.root, 'c', 'c')
763
base.tt.new_file('d', base.root, 'd', 'd')
764
base.tt.new_file('e', base.root, 'e', 'e')
765
base.tt.new_file('f', base.root, 'f', 'f')
766
base.tt.new_directory('g', base.root, 'g')
767
base.tt.new_directory('h', base.root, 'h')
769
other = TransformGroup("other", root_id)
770
other.tt.new_file('a', other.root, 'y\nb\nc\nd\be\n', 'a')
771
other.tt.new_file('b', other.root, 'b2', 'b')
772
other.tt.new_file('c', other.root, 'c2', 'c')
773
other.tt.new_file('d', other.root, 'd', 'd')
774
other.tt.new_file('e', other.root, 'e2', 'e')
775
other.tt.new_file('f', other.root, 'f', 'f')
776
other.tt.new_file('g', other.root, 'g', 'g')
777
other.tt.new_file('h', other.root, 'h\ni\nj\nk\n', 'h')
778
other.tt.new_file('i', other.root, 'h\ni\nj\nk\n', 'i')
780
this = TransformGroup("this", root_id)
781
this.tt.new_file('a', this.root, 'a\nb\nc\nd\bz\n', 'a')
782
this.tt.new_file('b', this.root, 'b', 'b')
783
this.tt.new_file('c', this.root, 'c', 'c')
784
this.tt.new_file('d', this.root, 'd2', 'd')
785
this.tt.new_file('e', this.root, 'e2', 'e')
786
this.tt.new_file('f', this.root, 'f', 'f')
787
this.tt.new_file('g', this.root, 'g', 'g')
788
this.tt.new_file('h', this.root, '1\n2\n3\n4\n', 'h')
789
this.tt.new_file('i', this.root, '1\n2\n3\n4\n', 'i')
791
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
793
self.assertEqual(this.wt.get_file('a').read(), 'y\nb\nc\nd\bz\n')
794
# three-way text conflict
795
self.assertEqual(this.wt.get_file('b').read(),
796
conflict_text('b', 'b2'))
798
self.assertEqual(this.wt.get_file('c').read(), 'c2')
800
self.assertEqual(this.wt.get_file('d').read(), 'd2')
801
# Ambigious clean merge
802
self.assertEqual(this.wt.get_file('e').read(), 'e2')
804
self.assertEqual(this.wt.get_file('f').read(), 'f')
805
# Correct correct results when THIS == OTHER
806
self.assertEqual(this.wt.get_file('g').read(), 'g')
807
# Text conflict when THIS & OTHER are text and BASE is dir
808
self.assertEqual(this.wt.get_file('h').read(),
809
conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
810
self.assertEqual(this.wt.get_file_byname('h.THIS').read(),
812
self.assertEqual(this.wt.get_file_byname('h.OTHER').read(),
814
self.assertEqual(file_kind(this.wt.abspath('h.BASE')), 'directory')
815
self.assertEqual(this.wt.get_file('i').read(),
816
conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
817
self.assertEqual(this.wt.get_file_byname('i.THIS').read(),
819
self.assertEqual(this.wt.get_file_byname('i.OTHER').read(),
821
self.assertEqual(os.path.exists(this.wt.abspath('i.BASE')), False)
822
modified = ['a', 'b', 'c', 'h', 'i']
823
merge_modified = this.wt.merge_modified()
824
self.assertSubset(merge_modified, modified)
825
self.assertEqual(len(merge_modified), len(modified))
826
file(this.wt.id2abspath('a'), 'wb').write('booga')
828
merge_modified = this.wt.merge_modified()
829
self.assertSubset(merge_modified, modified)
830
self.assertEqual(len(merge_modified), len(modified))
834
def test_file_merge(self):
835
if not has_symlinks():
836
raise TestSkipped('Symlinks are not supported on this platform')
837
root_id = generate_ids.gen_root_id()
838
base = TransformGroup("BASE", root_id)
839
this = TransformGroup("THIS", root_id)
840
other = TransformGroup("OTHER", root_id)
841
for tg in this, base, other:
842
tg.tt.new_directory('a', tg.root, 'a')
843
tg.tt.new_symlink('b', tg.root, 'b', 'b')
844
tg.tt.new_file('c', tg.root, 'c', 'c')
845
tg.tt.new_symlink('d', tg.root, tg.name, 'd')
846
targets = ((base, 'base-e', 'base-f', None, None),
847
(this, 'other-e', 'this-f', 'other-g', 'this-h'),
848
(other, 'other-e', None, 'other-g', 'other-h'))
849
for tg, e_target, f_target, g_target, h_target in targets:
850
for link, target in (('e', e_target), ('f', f_target),
851
('g', g_target), ('h', h_target)):
852
if target is not None:
853
tg.tt.new_symlink(link, tg.root, target, link)
855
for tg in this, base, other:
857
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
858
self.assertIs(os.path.isdir(this.wt.abspath('a')), True)
859
self.assertIs(os.path.islink(this.wt.abspath('b')), True)
860
self.assertIs(os.path.isfile(this.wt.abspath('c')), True)
861
for suffix in ('THIS', 'BASE', 'OTHER'):
862
self.assertEqual(os.readlink(this.wt.abspath('d.'+suffix)), suffix)
863
self.assertIs(os.path.lexists(this.wt.abspath('d')), False)
864
self.assertEqual(this.wt.id2path('d'), 'd.OTHER')
865
self.assertEqual(this.wt.id2path('f'), 'f.THIS')
866
self.assertEqual(os.readlink(this.wt.abspath('e')), 'other-e')
867
self.assertIs(os.path.lexists(this.wt.abspath('e.THIS')), False)
868
self.assertIs(os.path.lexists(this.wt.abspath('e.OTHER')), False)
869
self.assertIs(os.path.lexists(this.wt.abspath('e.BASE')), False)
870
self.assertIs(os.path.lexists(this.wt.abspath('g')), True)
871
self.assertIs(os.path.lexists(this.wt.abspath('g.BASE')), False)
872
self.assertIs(os.path.lexists(this.wt.abspath('h')), False)
873
self.assertIs(os.path.lexists(this.wt.abspath('h.BASE')), False)
874
self.assertIs(os.path.lexists(this.wt.abspath('h.THIS')), True)
875
self.assertIs(os.path.lexists(this.wt.abspath('h.OTHER')), True)
877
def test_filename_merge(self):
878
root_id = generate_ids.gen_root_id()
879
base = TransformGroup("BASE", root_id)
880
this = TransformGroup("THIS", root_id)
881
other = TransformGroup("OTHER", root_id)
882
base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a')
883
for t in [base, this, other]]
884
base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b')
885
for t in [base, this, other]]
886
base.tt.new_directory('c', base_a, 'c')
887
this.tt.new_directory('c1', this_a, 'c')
888
other.tt.new_directory('c', other_b, 'c')
890
base.tt.new_directory('d', base_a, 'd')
891
this.tt.new_directory('d1', this_b, 'd')
892
other.tt.new_directory('d', other_a, 'd')
894
base.tt.new_directory('e', base_a, 'e')
895
this.tt.new_directory('e', this_a, 'e')
896
other.tt.new_directory('e1', other_b, 'e')
898
base.tt.new_directory('f', base_a, 'f')
899
this.tt.new_directory('f1', this_b, 'f')
900
other.tt.new_directory('f1', other_b, 'f')
902
for tg in [this, base, other]:
904
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
905
self.assertEqual(this.wt.id2path('c'), pathjoin('b/c1'))
906
self.assertEqual(this.wt.id2path('d'), pathjoin('b/d1'))
907
self.assertEqual(this.wt.id2path('e'), pathjoin('b/e1'))
908
self.assertEqual(this.wt.id2path('f'), pathjoin('b/f1'))
910
def test_filename_merge_conflicts(self):
911
root_id = generate_ids.gen_root_id()
912
base = TransformGroup("BASE", root_id)
913
this = TransformGroup("THIS", root_id)
914
other = TransformGroup("OTHER", root_id)
915
base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a')
916
for t in [base, this, other]]
917
base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b')
918
for t in [base, this, other]]
920
base.tt.new_file('g', base_a, 'g', 'g')
921
other.tt.new_file('g1', other_b, 'g1', 'g')
923
base.tt.new_file('h', base_a, 'h', 'h')
924
this.tt.new_file('h1', this_b, 'h1', 'h')
926
base.tt.new_file('i', base.root, 'i', 'i')
927
other.tt.new_directory('i1', this_b, 'i')
929
for tg in [this, base, other]:
931
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
933
self.assertEqual(this.wt.id2path('g'), pathjoin('b/g1.OTHER'))
934
self.assertIs(os.path.lexists(this.wt.abspath('b/g1.BASE')), True)
935
self.assertIs(os.path.lexists(this.wt.abspath('b/g1.THIS')), False)
936
self.assertEqual(this.wt.id2path('h'), pathjoin('b/h1.THIS'))
937
self.assertIs(os.path.lexists(this.wt.abspath('b/h1.BASE')), True)
938
self.assertIs(os.path.lexists(this.wt.abspath('b/h1.OTHER')), False)
939
self.assertEqual(this.wt.id2path('i'), pathjoin('b/i1.OTHER'))
942
class TestBuildTree(tests.TestCaseWithTransport):
944
def test_build_tree(self):
945
if not has_symlinks():
946
raise TestSkipped('Test requires symlink support')
948
a = BzrDir.create_standalone_workingtree('a')
950
file('a/foo/bar', 'wb').write('contents')
951
os.symlink('a/foo/bar', 'a/foo/baz')
952
a.add(['foo', 'foo/bar', 'foo/baz'])
953
a.commit('initial commit')
954
b = BzrDir.create_standalone_workingtree('b')
955
build_tree(a.basis_tree(), b)
956
self.assertIs(os.path.isdir('b/foo'), True)
957
self.assertEqual(file('b/foo/bar', 'rb').read(), "contents")
958
self.assertEqual(os.readlink('b/foo/baz'), 'a/foo/bar')
960
def test_file_conflict_handling(self):
961
"""Ensure that when building trees, conflict handling is done"""
962
source = self.make_branch_and_tree('source')
963
target = self.make_branch_and_tree('target')
964
self.build_tree(['source/file', 'target/file'])
965
source.add('file', 'new-file')
966
source.commit('added file')
967
build_tree(source.basis_tree(), target)
968
self.assertEqual([DuplicateEntry('Moved existing file to',
969
'file.moved', 'file', None, 'new-file')],
971
target2 = self.make_branch_and_tree('target2')
972
target_file = file('target2/file', 'wb')
974
source_file = file('source/file', 'rb')
976
target_file.write(source_file.read())
981
build_tree(source.basis_tree(), target2)
982
self.assertEqual([], target2.conflicts())
984
def test_symlink_conflict_handling(self):
985
"""Ensure that when building trees, conflict handling is done"""
986
if not has_symlinks():
987
raise TestSkipped('Test requires symlink support')
988
source = self.make_branch_and_tree('source')
989
os.symlink('foo', 'source/symlink')
990
source.add('symlink', 'new-symlink')
991
source.commit('added file')
992
target = self.make_branch_and_tree('target')
993
os.symlink('bar', 'target/symlink')
994
build_tree(source.basis_tree(), target)
995
self.assertEqual([DuplicateEntry('Moved existing file to',
996
'symlink.moved', 'symlink', None, 'new-symlink')],
998
target = self.make_branch_and_tree('target2')
999
os.symlink('foo', 'target2/symlink')
1000
build_tree(source.basis_tree(), target)
1001
self.assertEqual([], target.conflicts())
1003
def test_directory_conflict_handling(self):
1004
"""Ensure that when building trees, conflict handling is done"""
1005
source = self.make_branch_and_tree('source')
1006
target = self.make_branch_and_tree('target')
1007
self.build_tree(['source/dir1/', 'source/dir1/file', 'target/dir1/'])
1008
source.add(['dir1', 'dir1/file'], ['new-dir1', 'new-file'])
1009
source.commit('added file')
1010
build_tree(source.basis_tree(), target)
1011
self.assertEqual([], target.conflicts())
1012
self.failUnlessExists('target/dir1/file')
1014
# Ensure contents are merged
1015
target = self.make_branch_and_tree('target2')
1016
self.build_tree(['target2/dir1/', 'target2/dir1/file2'])
1017
build_tree(source.basis_tree(), target)
1018
self.assertEqual([], target.conflicts())
1019
self.failUnlessExists('target2/dir1/file2')
1020
self.failUnlessExists('target2/dir1/file')
1022
# Ensure new contents are suppressed for existing branches
1023
target = self.make_branch_and_tree('target3')
1024
self.make_branch('target3/dir1')
1025
self.build_tree(['target3/dir1/file2'])
1026
build_tree(source.basis_tree(), target)
1027
self.failIfExists('target3/dir1/file')
1028
self.failUnlessExists('target3/dir1/file2')
1029
self.failUnlessExists('target3/dir1.diverted/file')
1030
self.assertEqual([DuplicateEntry('Diverted to',
1031
'dir1.diverted', 'dir1', 'new-dir1', None)],
1034
target = self.make_branch_and_tree('target4')
1035
self.build_tree(['target4/dir1/'])
1036
self.make_branch('target4/dir1/file')
1037
build_tree(source.basis_tree(), target)
1038
self.failUnlessExists('target4/dir1/file')
1039
self.assertEqual('directory', file_kind('target4/dir1/file'))
1040
self.failUnlessExists('target4/dir1/file.diverted')
1041
self.assertEqual([DuplicateEntry('Diverted to',
1042
'dir1/file.diverted', 'dir1/file', 'new-file', None)],
1045
def test_mixed_conflict_handling(self):
1046
"""Ensure that when building trees, conflict handling is done"""
1047
source = self.make_branch_and_tree('source')
1048
target = self.make_branch_and_tree('target')
1049
self.build_tree(['source/name', 'target/name/'])
1050
source.add('name', 'new-name')
1051
source.commit('added file')
1052
build_tree(source.basis_tree(), target)
1053
self.assertEqual([DuplicateEntry('Moved existing file to',
1054
'name.moved', 'name', None, 'new-name')], target.conflicts())
1056
def test_raises_in_populated(self):
1057
source = self.make_branch_and_tree('source')
1058
self.build_tree(['source/name'])
1060
source.commit('added name')
1061
target = self.make_branch_and_tree('target')
1062
self.build_tree(['target/name'])
1064
self.assertRaises(errors.WorkingTreeAlreadyPopulated,
1065
build_tree, source.basis_tree(), target)
1068
class MockTransform(object):
1070
def has_named_child(self, by_parent, parent_id, name):
1071
for child_id in by_parent[parent_id]:
1075
elif name == "name.~%s~" % child_id:
1079
class MockEntry(object):
1081
object.__init__(self)
1084
class TestGetBackupName(TestCase):
1085
def test_get_backup_name(self):
1086
tt = MockTransform()
1087
name = get_backup_name(MockEntry(), {'a':[]}, 'a', tt)
1088
self.assertEqual(name, 'name.~1~')
1089
name = get_backup_name(MockEntry(), {'a':['1']}, 'a', tt)
1090
self.assertEqual(name, 'name.~2~')
1091
name = get_backup_name(MockEntry(), {'a':['2']}, 'a', tt)
1092
self.assertEqual(name, 'name.~1~')
1093
name = get_backup_name(MockEntry(), {'a':['2'], 'b':[]}, 'b', tt)
1094
self.assertEqual(name, 'name.~1~')
1095
name = get_backup_name(MockEntry(), {'a':['1', '2', '3']}, 'a', tt)
1096
self.assertEqual(name, 'name.~4~')