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
28
from bzrlib.bzrdir import BzrDir
29
from bzrlib.conflicts import (DuplicateEntry, DuplicateID, MissingParent,
30
UnversionedParent, ParentLoop, DeletingParent,)
31
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
32
ReusingTransform, CantMoveRoot,
33
PathsNotVersionedError, ExistingLimbo,
34
ImmortalLimbo, LockError)
35
from bzrlib.osutils import file_kind, has_symlinks, pathjoin
36
from bzrlib.merge import Merge3Merger
37
from bzrlib.tests import TestCaseInTempDir, TestSkipped, TestCase
38
from bzrlib.transform import (TreeTransform, ROOT_PARENT, FinalPaths,
39
resolve_conflicts, cook_conflicts,
40
find_interesting, build_tree, get_backup_name)
43
class TestTreeTransform(TestCaseInTempDir):
46
super(TestTreeTransform, self).setUp()
47
self.wt = BzrDir.create_standalone_workingtree('.')
50
def get_transform(self):
51
transform = TreeTransform(self.wt)
52
#self.addCleanup(transform.finalize)
53
return transform, transform.root
55
def test_existing_limbo(self):
56
limbo_name = urlutils.local_path_from_url(
57
self.wt._control_files.controlfilename('limbo'))
58
transform, root = self.get_transform()
59
os.mkdir(pathjoin(limbo_name, 'hehe'))
60
self.assertRaises(ImmortalLimbo, transform.apply)
61
self.assertRaises(LockError, self.wt.unlock)
62
self.assertRaises(ExistingLimbo, self.get_transform)
63
self.assertRaises(LockError, self.wt.unlock)
64
os.rmdir(pathjoin(limbo_name, 'hehe'))
66
transform, root = self.get_transform()
70
transform, root = self.get_transform()
71
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
72
imaginary_id = transform.trans_id_tree_path('imaginary')
73
imaginary_id2 = transform.trans_id_tree_path('imaginary/')
74
self.assertEqual(imaginary_id, imaginary_id2)
75
self.assertEqual(transform.get_tree_parent(imaginary_id), root)
76
self.assertEqual(transform.final_kind(root), 'directory')
77
self.assertEqual(transform.final_file_id(root), self.wt.get_root_id())
78
trans_id = transform.create_path('name', root)
79
self.assertIs(transform.final_file_id(trans_id), None)
80
self.assertRaises(NoSuchFile, transform.final_kind, trans_id)
81
transform.create_file('contents', trans_id)
82
transform.set_executability(True, trans_id)
83
transform.version_file('my_pretties', trans_id)
84
self.assertRaises(DuplicateKey, transform.version_file,
85
'my_pretties', trans_id)
86
self.assertEqual(transform.final_file_id(trans_id), 'my_pretties')
87
self.assertEqual(transform.final_parent(trans_id), root)
88
self.assertIs(transform.final_parent(root), ROOT_PARENT)
89
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
90
oz_id = transform.create_path('oz', root)
91
transform.create_directory(oz_id)
92
transform.version_file('ozzie', oz_id)
93
trans_id2 = transform.create_path('name2', root)
94
transform.create_file('contents', trans_id2)
95
transform.set_executability(False, trans_id2)
96
transform.version_file('my_pretties2', trans_id2)
97
modified_paths = transform.apply().modified_paths
98
self.assertEqual('contents', self.wt.get_file_byname('name').read())
99
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
100
self.assertIs(self.wt.is_executable('my_pretties'), True)
101
self.assertIs(self.wt.is_executable('my_pretties2'), False)
102
self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
103
self.assertEqual(len(modified_paths), 3)
104
tree_mod_paths = [self.wt.id2abspath(f) for f in
105
('ozzie', 'my_pretties', 'my_pretties2')]
106
self.assertSubset(tree_mod_paths, modified_paths)
107
# is it safe to finalize repeatedly?
111
def test_convenience(self):
112
transform, root = self.get_transform()
113
trans_id = transform.new_file('name', root, 'contents',
115
oz = transform.new_directory('oz', root, 'oz-id')
116
dorothy = transform.new_directory('dorothy', oz, 'dorothy-id')
117
toto = transform.new_file('toto', dorothy, 'toto-contents',
120
self.assertEqual(len(transform.find_conflicts()), 0)
122
self.assertRaises(ReusingTransform, transform.find_conflicts)
123
self.assertEqual('contents', file(self.wt.abspath('name')).read())
124
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
125
self.assertIs(self.wt.is_executable('my_pretties'), True)
126
self.assertEqual(self.wt.path2id('oz'), 'oz-id')
127
self.assertEqual(self.wt.path2id('oz/dorothy'), 'dorothy-id')
128
self.assertEqual(self.wt.path2id('oz/dorothy/toto'), 'toto-id')
130
self.assertEqual('toto-contents',
131
self.wt.get_file_byname('oz/dorothy/toto').read())
132
self.assertIs(self.wt.is_executable('toto-id'), False)
134
def test_conflicts(self):
135
transform, root = self.get_transform()
136
trans_id = transform.new_file('name', root, 'contents',
138
self.assertEqual(len(transform.find_conflicts()), 0)
139
trans_id2 = transform.new_file('name', root, 'Crontents', 'toto')
140
self.assertEqual(transform.find_conflicts(),
141
[('duplicate', trans_id, trans_id2, 'name')])
142
self.assertRaises(MalformedTransform, transform.apply)
143
transform.adjust_path('name', trans_id, trans_id2)
144
self.assertEqual(transform.find_conflicts(),
145
[('non-directory parent', trans_id)])
146
tinman_id = transform.trans_id_tree_path('tinman')
147
transform.adjust_path('name', tinman_id, trans_id2)
148
self.assertEqual(transform.find_conflicts(),
149
[('unversioned parent', tinman_id),
150
('missing parent', tinman_id)])
151
lion_id = transform.create_path('lion', root)
152
self.assertEqual(transform.find_conflicts(),
153
[('unversioned parent', tinman_id),
154
('missing parent', tinman_id)])
155
transform.adjust_path('name', lion_id, trans_id2)
156
self.assertEqual(transform.find_conflicts(),
157
[('unversioned parent', lion_id),
158
('missing parent', lion_id)])
159
transform.version_file("Courage", lion_id)
160
self.assertEqual(transform.find_conflicts(),
161
[('missing parent', lion_id),
162
('versioning no contents', lion_id)])
163
transform.adjust_path('name2', root, trans_id2)
164
self.assertEqual(transform.find_conflicts(),
165
[('versioning no contents', lion_id)])
166
transform.create_file('Contents, okay?', lion_id)
167
transform.adjust_path('name2', trans_id2, trans_id2)
168
self.assertEqual(transform.find_conflicts(),
169
[('parent loop', trans_id2),
170
('non-directory parent', trans_id2)])
171
transform.adjust_path('name2', root, trans_id2)
172
oz_id = transform.new_directory('oz', root)
173
transform.set_executability(True, oz_id)
174
self.assertEqual(transform.find_conflicts(),
175
[('unversioned executability', oz_id)])
176
transform.version_file('oz-id', oz_id)
177
self.assertEqual(transform.find_conflicts(),
178
[('non-file executability', oz_id)])
179
transform.set_executability(None, oz_id)
180
tip_id = transform.new_file('tip', oz_id, 'ozma', 'tip-id')
182
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
183
self.assertEqual('contents', file(self.wt.abspath('name')).read())
184
transform2, root = self.get_transform()
185
oz_id = transform2.trans_id_tree_file_id('oz-id')
186
newtip = transform2.new_file('tip', oz_id, 'other', 'tip-id')
187
result = transform2.find_conflicts()
188
fp = FinalPaths(transform2)
189
self.assert_('oz/tip' in transform2._tree_path_ids)
190
self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
191
self.assertEqual(len(result), 2)
192
self.assertEqual((result[0][0], result[0][1]),
193
('duplicate', newtip))
194
self.assertEqual((result[1][0], result[1][2]),
195
('duplicate id', newtip))
196
transform2.finalize()
197
transform3 = TreeTransform(self.wt)
198
self.addCleanup(transform3.finalize)
199
oz_id = transform3.trans_id_tree_file_id('oz-id')
200
transform3.delete_contents(oz_id)
201
self.assertEqual(transform3.find_conflicts(),
202
[('missing parent', oz_id)])
203
root_id = transform3.root
204
tip_id = transform3.trans_id_tree_file_id('tip-id')
205
transform3.adjust_path('tip', root_id, tip_id)
208
def test_add_del(self):
209
start, root = self.get_transform()
210
start.new_directory('a', root, 'a')
212
transform, root = self.get_transform()
213
transform.delete_versioned(transform.trans_id_tree_file_id('a'))
214
transform.new_directory('a', root, 'a')
217
def test_unversioning(self):
218
create_tree, root = self.get_transform()
219
parent_id = create_tree.new_directory('parent', root, 'parent-id')
220
create_tree.new_file('child', parent_id, 'child', 'child-id')
222
unversion = TreeTransform(self.wt)
223
self.addCleanup(unversion.finalize)
224
parent = unversion.trans_id_tree_path('parent')
225
unversion.unversion_file(parent)
226
self.assertEqual(unversion.find_conflicts(),
227
[('unversioned parent', parent_id)])
228
file_id = unversion.trans_id_tree_file_id('child-id')
229
unversion.unversion_file(file_id)
232
def test_name_invariants(self):
233
create_tree, root = self.get_transform()
235
root = create_tree.root
236
create_tree.new_file('name1', root, 'hello1', 'name1')
237
create_tree.new_file('name2', root, 'hello2', 'name2')
238
ddir = create_tree.new_directory('dying_directory', root, 'ddir')
239
create_tree.new_file('dying_file', ddir, 'goodbye1', 'dfile')
240
create_tree.new_file('moving_file', ddir, 'later1', 'mfile')
241
create_tree.new_file('moving_file2', root, 'later2', 'mfile2')
244
mangle_tree,root = self.get_transform()
245
root = mangle_tree.root
247
name1 = mangle_tree.trans_id_tree_file_id('name1')
248
name2 = mangle_tree.trans_id_tree_file_id('name2')
249
mangle_tree.adjust_path('name2', root, name1)
250
mangle_tree.adjust_path('name1', root, name2)
252
#tests for deleting parent directories
253
ddir = mangle_tree.trans_id_tree_file_id('ddir')
254
mangle_tree.delete_contents(ddir)
255
dfile = mangle_tree.trans_id_tree_file_id('dfile')
256
mangle_tree.delete_versioned(dfile)
257
mangle_tree.unversion_file(dfile)
258
mfile = mangle_tree.trans_id_tree_file_id('mfile')
259
mangle_tree.adjust_path('mfile', root, mfile)
261
#tests for adding parent directories
262
newdir = mangle_tree.new_directory('new_directory', root, 'newdir')
263
mfile2 = mangle_tree.trans_id_tree_file_id('mfile2')
264
mangle_tree.adjust_path('mfile2', newdir, mfile2)
265
mangle_tree.new_file('newfile', newdir, 'hello3', 'dfile')
266
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
267
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
268
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
270
self.assertEqual(file(self.wt.abspath('name1')).read(), 'hello2')
271
self.assertEqual(file(self.wt.abspath('name2')).read(), 'hello1')
272
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
273
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
274
self.assertEqual(file(mfile2_path).read(), 'later2')
275
self.assertEqual(self.wt.id2path('mfile2'), 'new_directory/mfile2')
276
self.assertEqual(self.wt.path2id('new_directory/mfile2'), 'mfile2')
277
newfile_path = self.wt.abspath(pathjoin('new_directory','newfile'))
278
self.assertEqual(file(newfile_path).read(), 'hello3')
279
self.assertEqual(self.wt.path2id('dying_directory'), 'ddir')
280
self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
281
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
283
def test_both_rename(self):
284
create_tree,root = self.get_transform()
285
newdir = create_tree.new_directory('selftest', root, 'selftest-id')
286
create_tree.new_file('blackbox.py', newdir, 'hello1', 'blackbox-id')
288
mangle_tree,root = self.get_transform()
289
selftest = mangle_tree.trans_id_tree_file_id('selftest-id')
290
blackbox = mangle_tree.trans_id_tree_file_id('blackbox-id')
291
mangle_tree.adjust_path('test', root, selftest)
292
mangle_tree.adjust_path('test_too_much', root, selftest)
293
mangle_tree.set_executability(True, blackbox)
296
def test_both_rename2(self):
297
create_tree,root = self.get_transform()
298
bzrlib = create_tree.new_directory('bzrlib', root, 'bzrlib-id')
299
tests = create_tree.new_directory('tests', bzrlib, 'tests-id')
300
blackbox = create_tree.new_directory('blackbox', tests, 'blackbox-id')
301
create_tree.new_file('test_too_much.py', blackbox, 'hello1',
304
mangle_tree,root = self.get_transform()
305
bzrlib = mangle_tree.trans_id_tree_file_id('bzrlib-id')
306
tests = mangle_tree.trans_id_tree_file_id('tests-id')
307
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
308
mangle_tree.adjust_path('selftest', bzrlib, tests)
309
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
310
mangle_tree.set_executability(True, test_too_much)
313
def test_both_rename3(self):
314
create_tree,root = self.get_transform()
315
tests = create_tree.new_directory('tests', root, 'tests-id')
316
create_tree.new_file('test_too_much.py', tests, 'hello1',
319
mangle_tree,root = self.get_transform()
320
tests = mangle_tree.trans_id_tree_file_id('tests-id')
321
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
322
mangle_tree.adjust_path('selftest', root, tests)
323
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
324
mangle_tree.set_executability(True, test_too_much)
327
def test_move_dangling_ie(self):
328
create_tree, root = self.get_transform()
330
root = create_tree.root
331
create_tree.new_file('name1', root, 'hello1', 'name1')
333
delete_contents, root = self.get_transform()
334
file = delete_contents.trans_id_tree_file_id('name1')
335
delete_contents.delete_contents(file)
336
delete_contents.apply()
337
move_id, root = self.get_transform()
338
name1 = move_id.trans_id_tree_file_id('name1')
339
newdir = move_id.new_directory('dir', root, 'newdir')
340
move_id.adjust_path('name2', newdir, name1)
343
def test_replace_dangling_ie(self):
344
create_tree, root = self.get_transform()
346
root = create_tree.root
347
create_tree.new_file('name1', root, 'hello1', 'name1')
349
delete_contents = TreeTransform(self.wt)
350
self.addCleanup(delete_contents.finalize)
351
file = delete_contents.trans_id_tree_file_id('name1')
352
delete_contents.delete_contents(file)
353
delete_contents.apply()
354
delete_contents.finalize()
355
replace = TreeTransform(self.wt)
356
self.addCleanup(replace.finalize)
357
name2 = replace.new_file('name2', root, 'hello2', 'name1')
358
conflicts = replace.find_conflicts()
359
name1 = replace.trans_id_tree_file_id('name1')
360
self.assertEqual(conflicts, [('duplicate id', name1, name2)])
361
resolve_conflicts(replace)
364
def test_symlinks(self):
365
if not has_symlinks():
366
raise TestSkipped('Symlinks are not supported on this platform')
367
transform,root = self.get_transform()
368
oz_id = transform.new_directory('oz', root, 'oz-id')
369
wizard = transform.new_symlink('wizard', oz_id, 'wizard-target',
371
wiz_id = transform.create_path('wizard2', oz_id)
372
transform.create_symlink('behind_curtain', wiz_id)
373
transform.version_file('wiz-id2', wiz_id)
374
transform.set_executability(True, wiz_id)
375
self.assertEqual(transform.find_conflicts(),
376
[('non-file executability', wiz_id)])
377
transform.set_executability(None, wiz_id)
379
self.assertEqual(self.wt.path2id('oz/wizard'), 'wizard-id')
380
self.assertEqual(file_kind(self.wt.abspath('oz/wizard')), 'symlink')
381
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard2')),
383
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard')),
387
def get_conflicted(self):
388
create,root = self.get_transform()
389
create.new_file('dorothy', root, 'dorothy', 'dorothy-id')
390
oz = create.new_directory('oz', root, 'oz-id')
391
create.new_directory('emeraldcity', oz, 'emerald-id')
393
conflicts,root = self.get_transform()
394
# set up duplicate entry, duplicate id
395
new_dorothy = conflicts.new_file('dorothy', root, 'dorothy',
397
old_dorothy = conflicts.trans_id_tree_file_id('dorothy-id')
398
oz = conflicts.trans_id_tree_file_id('oz-id')
399
# set up DeletedParent parent conflict
400
conflicts.delete_versioned(oz)
401
emerald = conflicts.trans_id_tree_file_id('emerald-id')
402
# set up MissingParent conflict
403
munchkincity = conflicts.trans_id_file_id('munchkincity-id')
404
conflicts.adjust_path('munchkincity', root, munchkincity)
405
conflicts.new_directory('auntem', munchkincity, 'auntem-id')
407
conflicts.adjust_path('emeraldcity', emerald, emerald)
408
return conflicts, emerald, oz, old_dorothy, new_dorothy
410
def test_conflict_resolution(self):
411
conflicts, emerald, oz, old_dorothy, new_dorothy =\
412
self.get_conflicted()
413
resolve_conflicts(conflicts)
414
self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
415
self.assertIs(conflicts.final_file_id(old_dorothy), None)
416
self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
417
self.assertEqual(conflicts.final_file_id(new_dorothy), 'dorothy-id')
418
self.assertEqual(conflicts.final_parent(emerald), oz)
421
def test_cook_conflicts(self):
422
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
423
raw_conflicts = resolve_conflicts(tt)
424
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
425
duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved',
426
'dorothy', None, 'dorothy-id')
427
self.assertEqual(cooked_conflicts[0], duplicate)
428
duplicate_id = DuplicateID('Unversioned existing file',
429
'dorothy.moved', 'dorothy', None,
431
self.assertEqual(cooked_conflicts[1], duplicate_id)
432
missing_parent = MissingParent('Created directory', 'munchkincity',
434
deleted_parent = DeletingParent('Not deleting', 'oz', 'oz-id')
435
self.assertEqual(cooked_conflicts[2], missing_parent)
436
unversioned_parent = UnversionedParent('Versioned directory',
439
unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
441
self.assertEqual(cooked_conflicts[3], unversioned_parent)
442
parent_loop = ParentLoop('Cancelled move', 'oz/emeraldcity',
443
'oz/emeraldcity', 'emerald-id', 'emerald-id')
444
self.assertEqual(cooked_conflicts[4], deleted_parent)
445
self.assertEqual(cooked_conflicts[5], unversioned_parent2)
446
self.assertEqual(cooked_conflicts[6], parent_loop)
447
self.assertEqual(len(cooked_conflicts), 7)
450
def test_string_conflicts(self):
451
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
452
raw_conflicts = resolve_conflicts(tt)
453
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
455
conflicts_s = [str(c) for c in cooked_conflicts]
456
self.assertEqual(len(cooked_conflicts), len(conflicts_s))
457
self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy. '
458
'Moved existing file to '
460
self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy. '
461
'Unversioned existing file '
463
self.assertEqual(conflicts_s[2], 'Conflict adding files to'
464
' munchkincity. Created directory.')
465
self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
466
' versioned, but has versioned'
467
' children. Versioned directory.')
468
self.assertEqualDiff(conflicts_s[4], "Conflict: can't delete oz because it"
469
" is not empty. Not deleting.")
470
self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
471
' versioned, but has versioned'
472
' children. Versioned directory.')
473
self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
474
' oz/emeraldcity. Cancelled move.')
476
def test_moving_versioned_directories(self):
477
create, root = self.get_transform()
478
kansas = create.new_directory('kansas', root, 'kansas-id')
479
create.new_directory('house', kansas, 'house-id')
480
create.new_directory('oz', root, 'oz-id')
482
cyclone, root = self.get_transform()
483
oz = cyclone.trans_id_tree_file_id('oz-id')
484
house = cyclone.trans_id_tree_file_id('house-id')
485
cyclone.adjust_path('house', oz, house)
488
def test_moving_root(self):
489
create, root = self.get_transform()
490
fun = create.new_directory('fun', root, 'fun-id')
491
create.new_directory('sun', root, 'sun-id')
492
create.new_directory('moon', root, 'moon')
494
transform, root = self.get_transform()
495
transform.adjust_root_path('oldroot', fun)
496
new_root=transform.trans_id_tree_path('')
497
transform.version_file('new-root', new_root)
500
def test_renames(self):
501
create, root = self.get_transform()
502
old = create.new_directory('old-parent', root, 'old-id')
503
intermediate = create.new_directory('intermediate', old, 'im-id')
504
myfile = create.new_file('myfile', intermediate, 'myfile-text',
507
rename, root = self.get_transform()
508
old = rename.trans_id_file_id('old-id')
509
rename.adjust_path('new', root, old)
510
myfile = rename.trans_id_file_id('myfile-id')
511
rename.set_executability(True, myfile)
514
def test_find_interesting(self):
515
create, root = self.get_transform()
517
create.new_file('vfile', root, 'myfile-text', 'myfile-id')
518
create.new_file('uvfile', root, 'othertext')
520
result = self.applyDeprecated(symbol_versioning.zero_fifteen,
521
find_interesting, wt, wt, ['vfile'])
522
self.assertEqual(result, set(['myfile-id']))
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~')