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
25
from bzrlib.bzrdir import BzrDir
26
from bzrlib.conflicts import (DuplicateEntry, DuplicateID, MissingParent,
27
UnversionedParent, ParentLoop, DeletingParent,)
28
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
29
ReusingTransform, CantMoveRoot,
30
PathsNotVersionedError, ExistingLimbo,
31
ImmortalLimbo, LockError)
32
from bzrlib.osutils import file_kind, has_symlinks, pathjoin
33
from bzrlib.merge import Merge3Merger
34
from bzrlib.tests import TestCaseInTempDir, TestSkipped, TestCase
35
from bzrlib.transform import (TreeTransform, ROOT_PARENT, FinalPaths,
36
resolve_conflicts, cook_conflicts,
37
find_interesting, build_tree, get_backup_name)
38
from bzrlib.workingtree import gen_root_id
41
class TestTreeTransform(TestCaseInTempDir):
44
super(TestTreeTransform, self).setUp()
45
self.wt = BzrDir.create_standalone_workingtree('.')
48
def get_transform(self):
49
transform = TreeTransform(self.wt)
50
#self.addCleanup(transform.finalize)
51
return transform, transform.root
53
def test_existing_limbo(self):
54
limbo_name = urlutils.local_path_from_url(
55
self.wt._control_files.controlfilename('limbo'))
56
transform, root = self.get_transform()
57
os.mkdir(pathjoin(limbo_name, 'hehe'))
58
self.assertRaises(ImmortalLimbo, transform.apply)
59
self.assertRaises(LockError, self.wt.unlock)
60
self.assertRaises(ExistingLimbo, self.get_transform)
61
self.assertRaises(LockError, self.wt.unlock)
62
os.rmdir(pathjoin(limbo_name, 'hehe'))
64
transform, root = self.get_transform()
68
transform, root = self.get_transform()
69
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
70
imaginary_id = transform.trans_id_tree_path('imaginary')
71
imaginary_id2 = transform.trans_id_tree_path('imaginary/')
72
self.assertEqual(imaginary_id, imaginary_id2)
73
self.assertEqual(transform.get_tree_parent(imaginary_id), root)
74
self.assertEqual(transform.final_kind(root), 'directory')
75
self.assertEqual(transform.final_file_id(root), self.wt.get_root_id())
76
trans_id = transform.create_path('name', root)
77
self.assertIs(transform.final_file_id(trans_id), None)
78
self.assertRaises(NoSuchFile, transform.final_kind, trans_id)
79
transform.create_file('contents', trans_id)
80
transform.set_executability(True, trans_id)
81
transform.version_file('my_pretties', trans_id)
82
self.assertRaises(DuplicateKey, transform.version_file,
83
'my_pretties', trans_id)
84
self.assertEqual(transform.final_file_id(trans_id), 'my_pretties')
85
self.assertEqual(transform.final_parent(trans_id), root)
86
self.assertIs(transform.final_parent(root), ROOT_PARENT)
87
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
88
oz_id = transform.create_path('oz', root)
89
transform.create_directory(oz_id)
90
transform.version_file('ozzie', oz_id)
91
trans_id2 = transform.create_path('name2', root)
92
transform.create_file('contents', trans_id2)
93
transform.set_executability(False, trans_id2)
94
transform.version_file('my_pretties2', trans_id2)
95
modified_paths = transform.apply().modified_paths
96
self.assertEqual('contents', self.wt.get_file_byname('name').read())
97
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
98
self.assertIs(self.wt.is_executable('my_pretties'), True)
99
self.assertIs(self.wt.is_executable('my_pretties2'), False)
100
self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
101
self.assertEqual(len(modified_paths), 3)
102
tree_mod_paths = [self.wt.id2abspath(f) for f in
103
('ozzie', 'my_pretties', 'my_pretties2')]
104
self.assertSubset(tree_mod_paths, modified_paths)
105
# is it safe to finalize repeatedly?
109
def test_convenience(self):
110
transform, root = self.get_transform()
111
trans_id = transform.new_file('name', root, 'contents',
113
oz = transform.new_directory('oz', root, 'oz-id')
114
dorothy = transform.new_directory('dorothy', oz, 'dorothy-id')
115
toto = transform.new_file('toto', dorothy, 'toto-contents',
118
self.assertEqual(len(transform.find_conflicts()), 0)
120
self.assertRaises(ReusingTransform, transform.find_conflicts)
121
self.assertEqual('contents', file(self.wt.abspath('name')).read())
122
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
123
self.assertIs(self.wt.is_executable('my_pretties'), True)
124
self.assertEqual(self.wt.path2id('oz'), 'oz-id')
125
self.assertEqual(self.wt.path2id('oz/dorothy'), 'dorothy-id')
126
self.assertEqual(self.wt.path2id('oz/dorothy/toto'), 'toto-id')
128
self.assertEqual('toto-contents',
129
self.wt.get_file_byname('oz/dorothy/toto').read())
130
self.assertIs(self.wt.is_executable('toto-id'), False)
132
def test_conflicts(self):
133
transform, root = self.get_transform()
134
trans_id = transform.new_file('name', root, 'contents',
136
self.assertEqual(len(transform.find_conflicts()), 0)
137
trans_id2 = transform.new_file('name', root, 'Crontents', 'toto')
138
self.assertEqual(transform.find_conflicts(),
139
[('duplicate', trans_id, trans_id2, 'name')])
140
self.assertRaises(MalformedTransform, transform.apply)
141
transform.adjust_path('name', trans_id, trans_id2)
142
self.assertEqual(transform.find_conflicts(),
143
[('non-directory parent', trans_id)])
144
tinman_id = transform.trans_id_tree_path('tinman')
145
transform.adjust_path('name', tinman_id, trans_id2)
146
self.assertEqual(transform.find_conflicts(),
147
[('unversioned parent', tinman_id),
148
('missing parent', tinman_id)])
149
lion_id = transform.create_path('lion', root)
150
self.assertEqual(transform.find_conflicts(),
151
[('unversioned parent', tinman_id),
152
('missing parent', tinman_id)])
153
transform.adjust_path('name', lion_id, trans_id2)
154
self.assertEqual(transform.find_conflicts(),
155
[('unversioned parent', lion_id),
156
('missing parent', lion_id)])
157
transform.version_file("Courage", lion_id)
158
self.assertEqual(transform.find_conflicts(),
159
[('missing parent', lion_id),
160
('versioning no contents', lion_id)])
161
transform.adjust_path('name2', root, trans_id2)
162
self.assertEqual(transform.find_conflicts(),
163
[('versioning no contents', lion_id)])
164
transform.create_file('Contents, okay?', lion_id)
165
transform.adjust_path('name2', trans_id2, trans_id2)
166
self.assertEqual(transform.find_conflicts(),
167
[('parent loop', trans_id2),
168
('non-directory parent', trans_id2)])
169
transform.adjust_path('name2', root, trans_id2)
170
oz_id = transform.new_directory('oz', root)
171
transform.set_executability(True, oz_id)
172
self.assertEqual(transform.find_conflicts(),
173
[('unversioned executability', oz_id)])
174
transform.version_file('oz-id', oz_id)
175
self.assertEqual(transform.find_conflicts(),
176
[('non-file executability', oz_id)])
177
transform.set_executability(None, oz_id)
178
tip_id = transform.new_file('tip', oz_id, 'ozma', 'tip-id')
180
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
181
self.assertEqual('contents', file(self.wt.abspath('name')).read())
182
transform2, root = self.get_transform()
183
oz_id = transform2.trans_id_tree_file_id('oz-id')
184
newtip = transform2.new_file('tip', oz_id, 'other', 'tip-id')
185
result = transform2.find_conflicts()
186
fp = FinalPaths(transform2)
187
self.assert_('oz/tip' in transform2._tree_path_ids)
188
self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
189
self.assertEqual(len(result), 2)
190
self.assertEqual((result[0][0], result[0][1]),
191
('duplicate', newtip))
192
self.assertEqual((result[1][0], result[1][2]),
193
('duplicate id', newtip))
194
transform2.finalize()
195
transform3 = TreeTransform(self.wt)
196
self.addCleanup(transform3.finalize)
197
oz_id = transform3.trans_id_tree_file_id('oz-id')
198
transform3.delete_contents(oz_id)
199
self.assertEqual(transform3.find_conflicts(),
200
[('missing parent', oz_id)])
201
root_id = transform3.root
202
tip_id = transform3.trans_id_tree_file_id('tip-id')
203
transform3.adjust_path('tip', root_id, tip_id)
206
def test_add_del(self):
207
start, root = self.get_transform()
208
start.new_directory('a', root, 'a')
210
transform, root = self.get_transform()
211
transform.delete_versioned(transform.trans_id_tree_file_id('a'))
212
transform.new_directory('a', root, 'a')
215
def test_unversioning(self):
216
create_tree, root = self.get_transform()
217
parent_id = create_tree.new_directory('parent', root, 'parent-id')
218
create_tree.new_file('child', parent_id, 'child', 'child-id')
220
unversion = TreeTransform(self.wt)
221
self.addCleanup(unversion.finalize)
222
parent = unversion.trans_id_tree_path('parent')
223
unversion.unversion_file(parent)
224
self.assertEqual(unversion.find_conflicts(),
225
[('unversioned parent', parent_id)])
226
file_id = unversion.trans_id_tree_file_id('child-id')
227
unversion.unversion_file(file_id)
230
def test_name_invariants(self):
231
create_tree, root = self.get_transform()
233
root = create_tree.root
234
create_tree.new_file('name1', root, 'hello1', 'name1')
235
create_tree.new_file('name2', root, 'hello2', 'name2')
236
ddir = create_tree.new_directory('dying_directory', root, 'ddir')
237
create_tree.new_file('dying_file', ddir, 'goodbye1', 'dfile')
238
create_tree.new_file('moving_file', ddir, 'later1', 'mfile')
239
create_tree.new_file('moving_file2', root, 'later2', 'mfile2')
242
mangle_tree,root = self.get_transform()
243
root = mangle_tree.root
245
name1 = mangle_tree.trans_id_tree_file_id('name1')
246
name2 = mangle_tree.trans_id_tree_file_id('name2')
247
mangle_tree.adjust_path('name2', root, name1)
248
mangle_tree.adjust_path('name1', root, name2)
250
#tests for deleting parent directories
251
ddir = mangle_tree.trans_id_tree_file_id('ddir')
252
mangle_tree.delete_contents(ddir)
253
dfile = mangle_tree.trans_id_tree_file_id('dfile')
254
mangle_tree.delete_versioned(dfile)
255
mangle_tree.unversion_file(dfile)
256
mfile = mangle_tree.trans_id_tree_file_id('mfile')
257
mangle_tree.adjust_path('mfile', root, mfile)
259
#tests for adding parent directories
260
newdir = mangle_tree.new_directory('new_directory', root, 'newdir')
261
mfile2 = mangle_tree.trans_id_tree_file_id('mfile2')
262
mangle_tree.adjust_path('mfile2', newdir, mfile2)
263
mangle_tree.new_file('newfile', newdir, 'hello3', 'dfile')
264
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
265
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
266
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
268
self.assertEqual(file(self.wt.abspath('name1')).read(), 'hello2')
269
self.assertEqual(file(self.wt.abspath('name2')).read(), 'hello1')
270
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
271
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
272
self.assertEqual(file(mfile2_path).read(), 'later2')
273
self.assertEqual(self.wt.id2path('mfile2'), 'new_directory/mfile2')
274
self.assertEqual(self.wt.path2id('new_directory/mfile2'), 'mfile2')
275
newfile_path = self.wt.abspath(pathjoin('new_directory','newfile'))
276
self.assertEqual(file(newfile_path).read(), 'hello3')
277
self.assertEqual(self.wt.path2id('dying_directory'), 'ddir')
278
self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
279
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
281
def test_both_rename(self):
282
create_tree,root = self.get_transform()
283
newdir = create_tree.new_directory('selftest', root, 'selftest-id')
284
create_tree.new_file('blackbox.py', newdir, 'hello1', 'blackbox-id')
286
mangle_tree,root = self.get_transform()
287
selftest = mangle_tree.trans_id_tree_file_id('selftest-id')
288
blackbox = mangle_tree.trans_id_tree_file_id('blackbox-id')
289
mangle_tree.adjust_path('test', root, selftest)
290
mangle_tree.adjust_path('test_too_much', root, selftest)
291
mangle_tree.set_executability(True, blackbox)
294
def test_both_rename2(self):
295
create_tree,root = self.get_transform()
296
bzrlib = create_tree.new_directory('bzrlib', root, 'bzrlib-id')
297
tests = create_tree.new_directory('tests', bzrlib, 'tests-id')
298
blackbox = create_tree.new_directory('blackbox', tests, 'blackbox-id')
299
create_tree.new_file('test_too_much.py', blackbox, 'hello1',
302
mangle_tree,root = self.get_transform()
303
bzrlib = mangle_tree.trans_id_tree_file_id('bzrlib-id')
304
tests = mangle_tree.trans_id_tree_file_id('tests-id')
305
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
306
mangle_tree.adjust_path('selftest', bzrlib, tests)
307
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
308
mangle_tree.set_executability(True, test_too_much)
311
def test_both_rename3(self):
312
create_tree,root = self.get_transform()
313
tests = create_tree.new_directory('tests', root, 'tests-id')
314
create_tree.new_file('test_too_much.py', tests, 'hello1',
317
mangle_tree,root = self.get_transform()
318
tests = mangle_tree.trans_id_tree_file_id('tests-id')
319
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
320
mangle_tree.adjust_path('selftest', root, tests)
321
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
322
mangle_tree.set_executability(True, test_too_much)
325
def test_move_dangling_ie(self):
326
create_tree, root = self.get_transform()
328
root = create_tree.root
329
create_tree.new_file('name1', root, 'hello1', 'name1')
331
delete_contents, root = self.get_transform()
332
file = delete_contents.trans_id_tree_file_id('name1')
333
delete_contents.delete_contents(file)
334
delete_contents.apply()
335
move_id, root = self.get_transform()
336
name1 = move_id.trans_id_tree_file_id('name1')
337
newdir = move_id.new_directory('dir', root, 'newdir')
338
move_id.adjust_path('name2', newdir, name1)
341
def test_replace_dangling_ie(self):
342
create_tree, root = self.get_transform()
344
root = create_tree.root
345
create_tree.new_file('name1', root, 'hello1', 'name1')
347
delete_contents = TreeTransform(self.wt)
348
self.addCleanup(delete_contents.finalize)
349
file = delete_contents.trans_id_tree_file_id('name1')
350
delete_contents.delete_contents(file)
351
delete_contents.apply()
352
delete_contents.finalize()
353
replace = TreeTransform(self.wt)
354
self.addCleanup(replace.finalize)
355
name2 = replace.new_file('name2', root, 'hello2', 'name1')
356
conflicts = replace.find_conflicts()
357
name1 = replace.trans_id_tree_file_id('name1')
358
self.assertEqual(conflicts, [('duplicate id', name1, name2)])
359
resolve_conflicts(replace)
362
def test_symlinks(self):
363
if not has_symlinks():
364
raise TestSkipped('Symlinks are not supported on this platform')
365
transform,root = self.get_transform()
366
oz_id = transform.new_directory('oz', root, 'oz-id')
367
wizard = transform.new_symlink('wizard', oz_id, 'wizard-target',
369
wiz_id = transform.create_path('wizard2', oz_id)
370
transform.create_symlink('behind_curtain', wiz_id)
371
transform.version_file('wiz-id2', wiz_id)
372
transform.set_executability(True, wiz_id)
373
self.assertEqual(transform.find_conflicts(),
374
[('non-file executability', wiz_id)])
375
transform.set_executability(None, wiz_id)
377
self.assertEqual(self.wt.path2id('oz/wizard'), 'wizard-id')
378
self.assertEqual(file_kind(self.wt.abspath('oz/wizard')), 'symlink')
379
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard2')),
381
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard')),
385
def get_conflicted(self):
386
create,root = self.get_transform()
387
create.new_file('dorothy', root, 'dorothy', 'dorothy-id')
388
oz = create.new_directory('oz', root, 'oz-id')
389
create.new_directory('emeraldcity', oz, 'emerald-id')
391
conflicts,root = self.get_transform()
392
# set up duplicate entry, duplicate id
393
new_dorothy = conflicts.new_file('dorothy', root, 'dorothy',
395
old_dorothy = conflicts.trans_id_tree_file_id('dorothy-id')
396
oz = conflicts.trans_id_tree_file_id('oz-id')
397
# set up DeletedParent parent conflict
398
conflicts.delete_versioned(oz)
399
emerald = conflicts.trans_id_tree_file_id('emerald-id')
400
# set up MissingParent conflict
401
munchkincity = conflicts.trans_id_file_id('munchkincity-id')
402
conflicts.adjust_path('munchkincity', root, munchkincity)
403
conflicts.new_directory('auntem', munchkincity, 'auntem-id')
405
conflicts.adjust_path('emeraldcity', emerald, emerald)
406
return conflicts, emerald, oz, old_dorothy, new_dorothy
408
def test_conflict_resolution(self):
409
conflicts, emerald, oz, old_dorothy, new_dorothy =\
410
self.get_conflicted()
411
resolve_conflicts(conflicts)
412
self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
413
self.assertIs(conflicts.final_file_id(old_dorothy), None)
414
self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
415
self.assertEqual(conflicts.final_file_id(new_dorothy), 'dorothy-id')
416
self.assertEqual(conflicts.final_parent(emerald), oz)
419
def test_cook_conflicts(self):
420
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
421
raw_conflicts = resolve_conflicts(tt)
422
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
423
duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved',
424
'dorothy', None, 'dorothy-id')
425
self.assertEqual(cooked_conflicts[0], duplicate)
426
duplicate_id = DuplicateID('Unversioned existing file',
427
'dorothy.moved', 'dorothy', None,
429
self.assertEqual(cooked_conflicts[1], duplicate_id)
430
missing_parent = MissingParent('Created directory', 'munchkincity',
432
deleted_parent = DeletingParent('Not deleting', 'oz', 'oz-id')
433
self.assertEqual(cooked_conflicts[2], missing_parent)
434
unversioned_parent = UnversionedParent('Versioned directory',
437
unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
439
self.assertEqual(cooked_conflicts[3], unversioned_parent)
440
parent_loop = ParentLoop('Cancelled move', 'oz/emeraldcity',
441
'oz/emeraldcity', 'emerald-id', 'emerald-id')
442
self.assertEqual(cooked_conflicts[4], deleted_parent)
443
self.assertEqual(cooked_conflicts[5], unversioned_parent2)
444
self.assertEqual(cooked_conflicts[6], parent_loop)
445
self.assertEqual(len(cooked_conflicts), 7)
448
def test_string_conflicts(self):
449
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
450
raw_conflicts = resolve_conflicts(tt)
451
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
453
conflicts_s = [str(c) for c in cooked_conflicts]
454
self.assertEqual(len(cooked_conflicts), len(conflicts_s))
455
self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy. '
456
'Moved existing file to '
458
self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy. '
459
'Unversioned existing file '
461
self.assertEqual(conflicts_s[2], 'Conflict adding files to'
462
' munchkincity. Created directory.')
463
self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
464
' versioned, but has versioned'
465
' children. Versioned directory.')
466
self.assertEqualDiff(conflicts_s[4], "Conflict: can't delete oz because it"
467
" is not empty. Not deleting.")
468
self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
469
' versioned, but has versioned'
470
' children. Versioned directory.')
471
self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
472
' oz/emeraldcity. Cancelled move.')
474
def test_moving_versioned_directories(self):
475
create, root = self.get_transform()
476
kansas = create.new_directory('kansas', root, 'kansas-id')
477
create.new_directory('house', kansas, 'house-id')
478
create.new_directory('oz', root, 'oz-id')
480
cyclone, root = self.get_transform()
481
oz = cyclone.trans_id_tree_file_id('oz-id')
482
house = cyclone.trans_id_tree_file_id('house-id')
483
cyclone.adjust_path('house', oz, house)
486
def test_moving_root(self):
487
create, root = self.get_transform()
488
fun = create.new_directory('fun', root, 'fun-id')
489
create.new_directory('sun', root, 'sun-id')
490
create.new_directory('moon', root, 'moon')
492
transform, root = self.get_transform()
493
transform.adjust_root_path('oldroot', fun)
494
new_root=transform.trans_id_tree_path('')
495
transform.version_file('new-root', new_root)
498
def test_renames(self):
499
create, root = self.get_transform()
500
old = create.new_directory('old-parent', root, 'old-id')
501
intermediate = create.new_directory('intermediate', old, 'im-id')
502
myfile = create.new_file('myfile', intermediate, 'myfile-text',
505
rename, root = self.get_transform()
506
old = rename.trans_id_file_id('old-id')
507
rename.adjust_path('new', root, old)
508
myfile = rename.trans_id_file_id('myfile-id')
509
rename.set_executability(True, myfile)
512
def test_find_interesting(self):
513
create, root = self.get_transform()
515
create.new_file('vfile', root, 'myfile-text', 'myfile-id')
516
create.new_file('uvfile', root, 'othertext')
518
self.assertEqual(find_interesting(wt, wt, ['vfile']),
520
self.assertRaises(PathsNotVersionedError, find_interesting, wt, wt,
523
def test_set_executability_order(self):
524
"""Ensure that executability behaves the same, no matter what order.
526
- create file and set executability simultaneously
527
- create file and set executability afterward
528
- unsetting the executability of a file whose executability has not been
529
declared should throw an exception (this may happen when a
530
merge attempts to create a file with a duplicate ID)
532
transform, root = self.get_transform()
534
transform.new_file('set_on_creation', root, 'Set on creation', 'soc',
536
sac = transform.new_file('set_after_creation', root, 'Set after creation', 'sac')
537
transform.set_executability(True, sac)
538
uws = transform.new_file('unset_without_set', root, 'Unset badly', 'uws')
539
self.assertRaises(KeyError, transform.set_executability, None, uws)
541
self.assertTrue(wt.is_executable('soc'))
542
self.assertTrue(wt.is_executable('sac'))
544
def test_preserve_mode(self):
545
"""File mode is preserved when replacing content"""
546
if sys.platform == 'win32':
547
raise TestSkipped('chmod has no effect on win32')
548
transform, root = self.get_transform()
549
transform.new_file('file1', root, 'contents', 'file1-id', True)
551
self.assertTrue(self.wt.is_executable('file1-id'))
552
transform, root = self.get_transform()
553
file1_id = transform.trans_id_tree_file_id('file1-id')
554
transform.delete_contents(file1_id)
555
transform.create_file('contents2', file1_id)
557
self.assertTrue(self.wt.is_executable('file1-id'))
559
def test__set_mode_stats_correctly(self):
560
"""_set_mode stats to determine file mode."""
561
if sys.platform == 'win32':
562
raise TestSkipped('chmod has no effect on win32')
566
def instrumented_stat(path):
567
stat_paths.append(path)
568
return real_stat(path)
570
transform, root = self.get_transform()
572
bar1_id = transform.new_file('bar', root, 'bar contents 1\n',
573
file_id='bar-id-1', executable=False)
576
transform, root = self.get_transform()
577
bar1_id = transform.trans_id_tree_path('bar')
578
bar2_id = transform.trans_id_tree_path('bar2')
580
os.stat = instrumented_stat
581
transform.create_file('bar2 contents\n', bar2_id, mode_id=bar1_id)
586
bar1_abspath = self.wt.abspath('bar')
587
self.assertEqual([bar1_abspath], stat_paths)
590
class TransformGroup(object):
591
def __init__(self, dirname, root_id):
594
self.wt = BzrDir.create_standalone_workingtree(dirname)
595
self.wt.set_root_id(root_id)
596
self.b = self.wt.branch
597
self.tt = TreeTransform(self.wt)
598
self.root = self.tt.trans_id_tree_file_id(self.wt.get_root_id())
600
def conflict_text(tree, merge):
601
template = '%s TREE\n%s%s\n%s%s MERGE-SOURCE\n'
602
return template % ('<' * 7, tree, '=' * 7, merge, '>' * 7)
605
class TestTransformMerge(TestCaseInTempDir):
606
def test_text_merge(self):
607
root_id = gen_root_id()
608
base = TransformGroup("base", root_id)
609
base.tt.new_file('a', base.root, 'a\nb\nc\nd\be\n', 'a')
610
base.tt.new_file('b', base.root, 'b1', 'b')
611
base.tt.new_file('c', base.root, 'c', 'c')
612
base.tt.new_file('d', base.root, 'd', 'd')
613
base.tt.new_file('e', base.root, 'e', 'e')
614
base.tt.new_file('f', base.root, 'f', 'f')
615
base.tt.new_directory('g', base.root, 'g')
616
base.tt.new_directory('h', base.root, 'h')
618
other = TransformGroup("other", root_id)
619
other.tt.new_file('a', other.root, 'y\nb\nc\nd\be\n', 'a')
620
other.tt.new_file('b', other.root, 'b2', 'b')
621
other.tt.new_file('c', other.root, 'c2', 'c')
622
other.tt.new_file('d', other.root, 'd', 'd')
623
other.tt.new_file('e', other.root, 'e2', 'e')
624
other.tt.new_file('f', other.root, 'f', 'f')
625
other.tt.new_file('g', other.root, 'g', 'g')
626
other.tt.new_file('h', other.root, 'h\ni\nj\nk\n', 'h')
627
other.tt.new_file('i', other.root, 'h\ni\nj\nk\n', 'i')
629
this = TransformGroup("this", root_id)
630
this.tt.new_file('a', this.root, 'a\nb\nc\nd\bz\n', 'a')
631
this.tt.new_file('b', this.root, 'b', 'b')
632
this.tt.new_file('c', this.root, 'c', 'c')
633
this.tt.new_file('d', this.root, 'd2', 'd')
634
this.tt.new_file('e', this.root, 'e2', 'e')
635
this.tt.new_file('f', this.root, 'f', 'f')
636
this.tt.new_file('g', this.root, 'g', 'g')
637
this.tt.new_file('h', this.root, '1\n2\n3\n4\n', 'h')
638
this.tt.new_file('i', this.root, '1\n2\n3\n4\n', 'i')
640
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
642
self.assertEqual(this.wt.get_file('a').read(), 'y\nb\nc\nd\bz\n')
643
# three-way text conflict
644
self.assertEqual(this.wt.get_file('b').read(),
645
conflict_text('b', 'b2'))
647
self.assertEqual(this.wt.get_file('c').read(), 'c2')
649
self.assertEqual(this.wt.get_file('d').read(), 'd2')
650
# Ambigious clean merge
651
self.assertEqual(this.wt.get_file('e').read(), 'e2')
653
self.assertEqual(this.wt.get_file('f').read(), 'f')
654
# Correct correct results when THIS == OTHER
655
self.assertEqual(this.wt.get_file('g').read(), 'g')
656
# Text conflict when THIS & OTHER are text and BASE is dir
657
self.assertEqual(this.wt.get_file('h').read(),
658
conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
659
self.assertEqual(this.wt.get_file_byname('h.THIS').read(),
661
self.assertEqual(this.wt.get_file_byname('h.OTHER').read(),
663
self.assertEqual(file_kind(this.wt.abspath('h.BASE')), 'directory')
664
self.assertEqual(this.wt.get_file('i').read(),
665
conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
666
self.assertEqual(this.wt.get_file_byname('i.THIS').read(),
668
self.assertEqual(this.wt.get_file_byname('i.OTHER').read(),
670
self.assertEqual(os.path.exists(this.wt.abspath('i.BASE')), False)
671
modified = ['a', 'b', 'c', 'h', 'i']
672
merge_modified = this.wt.merge_modified()
673
self.assertSubset(merge_modified, modified)
674
self.assertEqual(len(merge_modified), len(modified))
675
file(this.wt.id2abspath('a'), 'wb').write('booga')
677
merge_modified = this.wt.merge_modified()
678
self.assertSubset(merge_modified, modified)
679
self.assertEqual(len(merge_modified), len(modified))
683
def test_file_merge(self):
684
if not has_symlinks():
685
raise TestSkipped('Symlinks are not supported on this platform')
686
root_id = gen_root_id()
687
base = TransformGroup("BASE", root_id)
688
this = TransformGroup("THIS", root_id)
689
other = TransformGroup("OTHER", root_id)
690
for tg in this, base, other:
691
tg.tt.new_directory('a', tg.root, 'a')
692
tg.tt.new_symlink('b', tg.root, 'b', 'b')
693
tg.tt.new_file('c', tg.root, 'c', 'c')
694
tg.tt.new_symlink('d', tg.root, tg.name, 'd')
695
targets = ((base, 'base-e', 'base-f', None, None),
696
(this, 'other-e', 'this-f', 'other-g', 'this-h'),
697
(other, 'other-e', None, 'other-g', 'other-h'))
698
for tg, e_target, f_target, g_target, h_target in targets:
699
for link, target in (('e', e_target), ('f', f_target),
700
('g', g_target), ('h', h_target)):
701
if target is not None:
702
tg.tt.new_symlink(link, tg.root, target, link)
704
for tg in this, base, other:
706
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
707
self.assertIs(os.path.isdir(this.wt.abspath('a')), True)
708
self.assertIs(os.path.islink(this.wt.abspath('b')), True)
709
self.assertIs(os.path.isfile(this.wt.abspath('c')), True)
710
for suffix in ('THIS', 'BASE', 'OTHER'):
711
self.assertEqual(os.readlink(this.wt.abspath('d.'+suffix)), suffix)
712
self.assertIs(os.path.lexists(this.wt.abspath('d')), False)
713
self.assertEqual(this.wt.id2path('d'), 'd.OTHER')
714
self.assertEqual(this.wt.id2path('f'), 'f.THIS')
715
self.assertEqual(os.readlink(this.wt.abspath('e')), 'other-e')
716
self.assertIs(os.path.lexists(this.wt.abspath('e.THIS')), False)
717
self.assertIs(os.path.lexists(this.wt.abspath('e.OTHER')), False)
718
self.assertIs(os.path.lexists(this.wt.abspath('e.BASE')), False)
719
self.assertIs(os.path.lexists(this.wt.abspath('g')), True)
720
self.assertIs(os.path.lexists(this.wt.abspath('g.BASE')), False)
721
self.assertIs(os.path.lexists(this.wt.abspath('h')), False)
722
self.assertIs(os.path.lexists(this.wt.abspath('h.BASE')), False)
723
self.assertIs(os.path.lexists(this.wt.abspath('h.THIS')), True)
724
self.assertIs(os.path.lexists(this.wt.abspath('h.OTHER')), True)
726
def test_filename_merge(self):
727
root_id = gen_root_id()
728
base = TransformGroup("BASE", root_id)
729
this = TransformGroup("THIS", root_id)
730
other = TransformGroup("OTHER", root_id)
731
base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a')
732
for t in [base, this, other]]
733
base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b')
734
for t in [base, this, other]]
735
base.tt.new_directory('c', base_a, 'c')
736
this.tt.new_directory('c1', this_a, 'c')
737
other.tt.new_directory('c', other_b, 'c')
739
base.tt.new_directory('d', base_a, 'd')
740
this.tt.new_directory('d1', this_b, 'd')
741
other.tt.new_directory('d', other_a, 'd')
743
base.tt.new_directory('e', base_a, 'e')
744
this.tt.new_directory('e', this_a, 'e')
745
other.tt.new_directory('e1', other_b, 'e')
747
base.tt.new_directory('f', base_a, 'f')
748
this.tt.new_directory('f1', this_b, 'f')
749
other.tt.new_directory('f1', other_b, 'f')
751
for tg in [this, base, other]:
753
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
754
self.assertEqual(this.wt.id2path('c'), pathjoin('b/c1'))
755
self.assertEqual(this.wt.id2path('d'), pathjoin('b/d1'))
756
self.assertEqual(this.wt.id2path('e'), pathjoin('b/e1'))
757
self.assertEqual(this.wt.id2path('f'), pathjoin('b/f1'))
759
def test_filename_merge_conflicts(self):
760
root_id = gen_root_id()
761
base = TransformGroup("BASE", root_id)
762
this = TransformGroup("THIS", root_id)
763
other = TransformGroup("OTHER", root_id)
764
base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a')
765
for t in [base, this, other]]
766
base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b')
767
for t in [base, this, other]]
769
base.tt.new_file('g', base_a, 'g', 'g')
770
other.tt.new_file('g1', other_b, 'g1', 'g')
772
base.tt.new_file('h', base_a, 'h', 'h')
773
this.tt.new_file('h1', this_b, 'h1', 'h')
775
base.tt.new_file('i', base.root, 'i', 'i')
776
other.tt.new_directory('i1', this_b, 'i')
778
for tg in [this, base, other]:
780
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
782
self.assertEqual(this.wt.id2path('g'), pathjoin('b/g1.OTHER'))
783
self.assertIs(os.path.lexists(this.wt.abspath('b/g1.BASE')), True)
784
self.assertIs(os.path.lexists(this.wt.abspath('b/g1.THIS')), False)
785
self.assertEqual(this.wt.id2path('h'), pathjoin('b/h1.THIS'))
786
self.assertIs(os.path.lexists(this.wt.abspath('b/h1.BASE')), True)
787
self.assertIs(os.path.lexists(this.wt.abspath('b/h1.OTHER')), False)
788
self.assertEqual(this.wt.id2path('i'), pathjoin('b/i1.OTHER'))
791
class TestBuildTree(tests.TestCaseWithTransport):
793
def test_build_tree(self):
794
if not has_symlinks():
795
raise TestSkipped('Test requires symlink support')
797
a = BzrDir.create_standalone_workingtree('a')
799
file('a/foo/bar', 'wb').write('contents')
800
os.symlink('a/foo/bar', 'a/foo/baz')
801
a.add(['foo', 'foo/bar', 'foo/baz'])
802
a.commit('initial commit')
803
b = BzrDir.create_standalone_workingtree('b')
804
build_tree(a.basis_tree(), b)
805
self.assertIs(os.path.isdir('b/foo'), True)
806
self.assertEqual(file('b/foo/bar', 'rb').read(), "contents")
807
self.assertEqual(os.readlink('b/foo/baz'), 'a/foo/bar')
809
def test_file_conflict_handling(self):
810
"""Ensure that when building trees, conflict handling is done"""
811
source = self.make_branch_and_tree('source')
812
target = self.make_branch_and_tree('target')
813
self.build_tree(['source/file', 'target/file'])
814
source.add('file', 'new-file')
815
source.commit('added file')
816
build_tree(source.basis_tree(), target)
817
self.assertEqual([DuplicateEntry('Moved existing file to',
818
'file.moved', 'file', None, 'new-file')],
820
target2 = self.make_branch_and_tree('target2')
821
target_file = file('target2/file', 'wb')
823
source_file = file('source/file', 'rb')
825
target_file.write(source_file.read())
830
build_tree(source.basis_tree(), target2)
831
self.assertEqual([], target2.conflicts())
833
def test_symlink_conflict_handling(self):
834
"""Ensure that when building trees, conflict handling is done"""
835
if not has_symlinks():
836
raise TestSkipped('Test requires symlink support')
837
source = self.make_branch_and_tree('source')
838
os.symlink('foo', 'source/symlink')
839
source.add('symlink', 'new-symlink')
840
source.commit('added file')
841
target = self.make_branch_and_tree('target')
842
os.symlink('bar', 'target/symlink')
843
build_tree(source.basis_tree(), target)
844
self.assertEqual([DuplicateEntry('Moved existing file to',
845
'symlink.moved', 'symlink', None, 'new-symlink')],
847
target = self.make_branch_and_tree('target2')
848
os.symlink('foo', 'target2/symlink')
849
build_tree(source.basis_tree(), target)
850
self.assertEqual([], target.conflicts())
852
def test_directory_conflict_handling(self):
853
"""Ensure that when building trees, conflict handling is done"""
854
source = self.make_branch_and_tree('source')
855
target = self.make_branch_and_tree('target')
856
self.build_tree(['source/dir1/', 'source/dir1/file', 'target/dir1/'])
857
source.add(['dir1', 'dir1/file'], ['new-dir1', 'new-file'])
858
source.commit('added file')
859
build_tree(source.basis_tree(), target)
860
self.assertEqual([], target.conflicts())
861
self.failUnlessExists('target/dir1/file')
863
# Ensure contents are merged
864
target = self.make_branch_and_tree('target2')
865
self.build_tree(['target2/dir1/', 'target2/dir1/file2'])
866
build_tree(source.basis_tree(), target)
867
self.assertEqual([], target.conflicts())
868
self.failUnlessExists('target2/dir1/file2')
869
self.failUnlessExists('target2/dir1/file')
871
# Ensure new contents are suppressed for existing branches
872
target = self.make_branch_and_tree('target3')
873
self.make_branch('target3/dir1')
874
self.build_tree(['target3/dir1/file2'])
875
build_tree(source.basis_tree(), target)
876
self.failIfExists('target3/dir1/file')
877
self.failUnlessExists('target3/dir1/file2')
878
self.failUnlessExists('target3/dir1.diverted/file')
879
self.assertEqual([DuplicateEntry('Diverted to',
880
'dir1.diverted', 'dir1', 'new-dir1', None)],
883
target = self.make_branch_and_tree('target4')
884
self.build_tree(['target4/dir1/'])
885
self.make_branch('target4/dir1/file')
886
build_tree(source.basis_tree(), target)
887
self.failUnlessExists('target4/dir1/file')
888
self.assertEqual('directory', file_kind('target4/dir1/file'))
889
self.failUnlessExists('target4/dir1/file.diverted')
890
self.assertEqual([DuplicateEntry('Diverted to',
891
'dir1/file.diverted', 'dir1/file', 'new-file', None)],
894
def test_mixed_conflict_handling(self):
895
"""Ensure that when building trees, conflict handling is done"""
896
source = self.make_branch_and_tree('source')
897
target = self.make_branch_and_tree('target')
898
self.build_tree(['source/name', 'target/name/'])
899
source.add('name', 'new-name')
900
source.commit('added file')
901
build_tree(source.basis_tree(), target)
902
self.assertEqual([DuplicateEntry('Moved existing file to',
903
'name.moved', 'name', None, 'new-name')], target.conflicts())
905
def test_raises_in_populated(self):
906
source = self.make_branch_and_tree('source')
907
self.build_tree(['source/name'])
909
source.commit('added name')
910
target = self.make_branch_and_tree('target')
911
self.build_tree(['target/name'])
913
self.assertRaises(AssertionError, build_tree, source.basis_tree(),
917
class MockTransform(object):
919
def has_named_child(self, by_parent, parent_id, name):
920
for child_id in by_parent[parent_id]:
924
elif name == "name.~%s~" % child_id:
928
class MockEntry(object):
930
object.__init__(self)
933
class TestGetBackupName(TestCase):
934
def test_get_backup_name(self):
936
name = get_backup_name(MockEntry(), {'a':[]}, 'a', tt)
937
self.assertEqual(name, 'name.~1~')
938
name = get_backup_name(MockEntry(), {'a':['1']}, 'a', tt)
939
self.assertEqual(name, 'name.~2~')
940
name = get_backup_name(MockEntry(), {'a':['2']}, 'a', tt)
941
self.assertEqual(name, 'name.~1~')
942
name = get_backup_name(MockEntry(), {'a':['2'], 'b':[]}, 'b', tt)
943
self.assertEqual(name, 'name.~1~')
944
name = get_backup_name(MockEntry(), {'a':['1', '2', '3']}, 'a', tt)
945
self.assertEqual(name, 'name.~4~')