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
ExistingPendingDeletion, ImmortalLimbo,
35
ImmortalPendingDeletion, LockError)
36
from bzrlib.osutils import file_kind, pathjoin
37
from bzrlib.merge import Merge3Merger
38
from bzrlib.tests import (
39
CaseInsensitiveFilesystemFeature,
45
from bzrlib.transform import (TreeTransform, ROOT_PARENT, FinalPaths,
46
resolve_conflicts, cook_conflicts,
47
find_interesting, build_tree, get_backup_name,
48
change_entry, _FileMover, resolve_checkout)
51
class TestTreeTransform(tests.TestCaseWithTransport):
54
super(TestTreeTransform, self).setUp()
55
self.wt = self.make_branch_and_tree('.', format='dirstate-with-subtree')
58
def get_transform(self):
59
transform = TreeTransform(self.wt)
60
#self.addCleanup(transform.finalize)
61
return transform, transform.root
63
def test_existing_limbo(self):
64
transform, root = self.get_transform()
65
limbo_name = transform._limbodir
66
deletion_path = transform._deletiondir
67
os.mkdir(pathjoin(limbo_name, 'hehe'))
68
self.assertRaises(ImmortalLimbo, transform.apply)
69
self.assertRaises(LockError, self.wt.unlock)
70
self.assertRaises(ExistingLimbo, self.get_transform)
71
self.assertRaises(LockError, self.wt.unlock)
72
os.rmdir(pathjoin(limbo_name, 'hehe'))
74
os.rmdir(deletion_path)
75
transform, root = self.get_transform()
78
def test_existing_pending_deletion(self):
79
transform, root = self.get_transform()
80
deletion_path = self._limbodir = urlutils.local_path_from_url(
81
transform._tree._control_files.controlfilename('pending-deletion'))
82
os.mkdir(pathjoin(deletion_path, 'blocking-directory'))
83
self.assertRaises(ImmortalPendingDeletion, transform.apply)
84
self.assertRaises(LockError, self.wt.unlock)
85
self.assertRaises(ExistingPendingDeletion, self.get_transform)
88
transform, root = self.get_transform()
89
self.wt.lock_tree_write()
90
self.addCleanup(self.wt.unlock)
91
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
92
imaginary_id = transform.trans_id_tree_path('imaginary')
93
imaginary_id2 = transform.trans_id_tree_path('imaginary/')
94
self.assertEqual(imaginary_id, imaginary_id2)
95
self.assertEqual(transform.get_tree_parent(imaginary_id), root)
96
self.assertEqual(transform.final_kind(root), 'directory')
97
self.assertEqual(transform.final_file_id(root), self.wt.get_root_id())
98
trans_id = transform.create_path('name', root)
99
self.assertIs(transform.final_file_id(trans_id), None)
100
self.assertRaises(NoSuchFile, transform.final_kind, trans_id)
101
transform.create_file('contents', trans_id)
102
transform.set_executability(True, trans_id)
103
transform.version_file('my_pretties', trans_id)
104
self.assertRaises(DuplicateKey, transform.version_file,
105
'my_pretties', trans_id)
106
self.assertEqual(transform.final_file_id(trans_id), 'my_pretties')
107
self.assertEqual(transform.final_parent(trans_id), root)
108
self.assertIs(transform.final_parent(root), ROOT_PARENT)
109
self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
110
oz_id = transform.create_path('oz', root)
111
transform.create_directory(oz_id)
112
transform.version_file('ozzie', oz_id)
113
trans_id2 = transform.create_path('name2', root)
114
transform.create_file('contents', trans_id2)
115
transform.set_executability(False, trans_id2)
116
transform.version_file('my_pretties2', trans_id2)
117
modified_paths = transform.apply().modified_paths
118
self.assertEqual('contents', self.wt.get_file_byname('name').read())
119
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
120
self.assertIs(self.wt.is_executable('my_pretties'), True)
121
self.assertIs(self.wt.is_executable('my_pretties2'), False)
122
self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
123
self.assertEqual(len(modified_paths), 3)
124
tree_mod_paths = [self.wt.id2abspath(f) for f in
125
('ozzie', 'my_pretties', 'my_pretties2')]
126
self.assertSubset(tree_mod_paths, modified_paths)
127
# is it safe to finalize repeatedly?
131
def test_convenience(self):
132
transform, root = self.get_transform()
133
self.wt.lock_tree_write()
134
self.addCleanup(self.wt.unlock)
135
trans_id = transform.new_file('name', root, 'contents',
137
oz = transform.new_directory('oz', root, 'oz-id')
138
dorothy = transform.new_directory('dorothy', oz, 'dorothy-id')
139
toto = transform.new_file('toto', dorothy, 'toto-contents',
142
self.assertEqual(len(transform.find_conflicts()), 0)
144
self.assertRaises(ReusingTransform, transform.find_conflicts)
145
self.assertEqual('contents', file(self.wt.abspath('name')).read())
146
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
147
self.assertIs(self.wt.is_executable('my_pretties'), True)
148
self.assertEqual(self.wt.path2id('oz'), 'oz-id')
149
self.assertEqual(self.wt.path2id('oz/dorothy'), 'dorothy-id')
150
self.assertEqual(self.wt.path2id('oz/dorothy/toto'), 'toto-id')
152
self.assertEqual('toto-contents',
153
self.wt.get_file_byname('oz/dorothy/toto').read())
154
self.assertIs(self.wt.is_executable('toto-id'), False)
156
def test_tree_reference(self):
157
transform, root = self.get_transform()
158
tree = transform._tree
159
trans_id = transform.new_directory('reference', root, 'subtree-id')
160
transform.set_tree_reference('subtree-revision', trans_id)
163
self.addCleanup(tree.unlock)
164
self.assertEqual('subtree-revision',
165
tree.inventory['subtree-id'].reference_revision)
167
def test_conflicts(self):
168
transform, root = self.get_transform()
169
trans_id = transform.new_file('name', root, 'contents',
171
self.assertEqual(len(transform.find_conflicts()), 0)
172
trans_id2 = transform.new_file('name', root, 'Crontents', 'toto')
173
self.assertEqual(transform.find_conflicts(),
174
[('duplicate', trans_id, trans_id2, 'name')])
175
self.assertRaises(MalformedTransform, transform.apply)
176
transform.adjust_path('name', trans_id, trans_id2)
177
self.assertEqual(transform.find_conflicts(),
178
[('non-directory parent', trans_id)])
179
tinman_id = transform.trans_id_tree_path('tinman')
180
transform.adjust_path('name', tinman_id, trans_id2)
181
self.assertEqual(transform.find_conflicts(),
182
[('unversioned parent', tinman_id),
183
('missing parent', tinman_id)])
184
lion_id = transform.create_path('lion', root)
185
self.assertEqual(transform.find_conflicts(),
186
[('unversioned parent', tinman_id),
187
('missing parent', tinman_id)])
188
transform.adjust_path('name', lion_id, trans_id2)
189
self.assertEqual(transform.find_conflicts(),
190
[('unversioned parent', lion_id),
191
('missing parent', lion_id)])
192
transform.version_file("Courage", lion_id)
193
self.assertEqual(transform.find_conflicts(),
194
[('missing parent', lion_id),
195
('versioning no contents', lion_id)])
196
transform.adjust_path('name2', root, trans_id2)
197
self.assertEqual(transform.find_conflicts(),
198
[('versioning no contents', lion_id)])
199
transform.create_file('Contents, okay?', lion_id)
200
transform.adjust_path('name2', trans_id2, trans_id2)
201
self.assertEqual(transform.find_conflicts(),
202
[('parent loop', trans_id2),
203
('non-directory parent', trans_id2)])
204
transform.adjust_path('name2', root, trans_id2)
205
oz_id = transform.new_directory('oz', root)
206
transform.set_executability(True, oz_id)
207
self.assertEqual(transform.find_conflicts(),
208
[('unversioned executability', oz_id)])
209
transform.version_file('oz-id', oz_id)
210
self.assertEqual(transform.find_conflicts(),
211
[('non-file executability', oz_id)])
212
transform.set_executability(None, oz_id)
213
tip_id = transform.new_file('tip', oz_id, 'ozma', 'tip-id')
215
self.assertEqual(self.wt.path2id('name'), 'my_pretties')
216
self.assertEqual('contents', file(self.wt.abspath('name')).read())
217
transform2, root = self.get_transform()
218
oz_id = transform2.trans_id_tree_file_id('oz-id')
219
newtip = transform2.new_file('tip', oz_id, 'other', 'tip-id')
220
result = transform2.find_conflicts()
221
fp = FinalPaths(transform2)
222
self.assert_('oz/tip' in transform2._tree_path_ids)
223
self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
224
self.assertEqual(len(result), 2)
225
self.assertEqual((result[0][0], result[0][1]),
226
('duplicate', newtip))
227
self.assertEqual((result[1][0], result[1][2]),
228
('duplicate id', newtip))
229
transform2.finalize()
230
transform3 = TreeTransform(self.wt)
231
self.addCleanup(transform3.finalize)
232
oz_id = transform3.trans_id_tree_file_id('oz-id')
233
transform3.delete_contents(oz_id)
234
self.assertEqual(transform3.find_conflicts(),
235
[('missing parent', oz_id)])
236
root_id = transform3.root
237
tip_id = transform3.trans_id_tree_file_id('tip-id')
238
transform3.adjust_path('tip', root_id, tip_id)
241
def test_conflict_on_case_insensitive(self):
242
tree = self.make_branch_and_tree('tree')
243
# Don't try this at home, kids!
244
# Force the tree to report that it is case sensitive, for conflict
246
tree.case_sensitive = True
247
transform = TreeTransform(tree)
248
self.addCleanup(transform.finalize)
249
transform.new_file('file', transform.root, 'content')
250
transform.new_file('FiLe', transform.root, 'content')
251
result = transform.find_conflicts()
252
self.assertEqual([], result)
253
# Force the tree to report that it is case insensitive, for conflict
255
tree.case_sensitive = False
256
result = transform.find_conflicts()
257
self.assertEqual([('duplicate', 'new-1', 'new-2', 'file')], result)
259
def test_conflict_on_case_insensitive_existing(self):
260
tree = self.make_branch_and_tree('tree')
261
self.build_tree(['tree/FiLe'])
262
# Don't try this at home, kids!
263
# Force the tree to report that it is case sensitive, for conflict
265
tree.case_sensitive = True
266
transform = TreeTransform(tree)
267
self.addCleanup(transform.finalize)
268
transform.new_file('file', transform.root, 'content')
269
result = transform.find_conflicts()
270
self.assertEqual([], result)
271
# Force the tree to report that it is case insensitive, for conflict
273
tree.case_sensitive = False
274
result = transform.find_conflicts()
275
self.assertEqual([('duplicate', 'new-1', 'new-2', 'file')], result)
277
def test_resolve_case_insensitive_conflict(self):
278
tree = self.make_branch_and_tree('tree')
279
# Don't try this at home, kids!
280
# Force the tree to report that it is case insensitive, for conflict
282
tree.case_sensitive = False
283
transform = TreeTransform(tree)
284
self.addCleanup(transform.finalize)
285
transform.new_file('file', transform.root, 'content')
286
transform.new_file('FiLe', transform.root, 'content')
287
resolve_conflicts(transform)
289
self.failUnlessExists('tree/file')
290
self.failUnlessExists('tree/FiLe.moved')
292
def test_resolve_checkout_case_conflict(self):
293
tree = self.make_branch_and_tree('tree')
294
# Don't try this at home, kids!
295
# Force the tree to report that it is case insensitive, for conflict
297
tree.case_sensitive = False
298
transform = TreeTransform(tree)
299
self.addCleanup(transform.finalize)
300
transform.new_file('file', transform.root, 'content')
301
transform.new_file('FiLe', transform.root, 'content')
302
resolve_conflicts(transform,
303
pass_func=lambda t, c: resolve_checkout(t, c, []))
305
self.failUnlessExists('tree/file')
306
self.failUnlessExists('tree/FiLe.moved')
308
def test_apply_case_conflict(self):
309
"""Ensure that a transform with case conflicts can always be applied"""
310
tree = self.make_branch_and_tree('tree')
311
transform = TreeTransform(tree)
312
self.addCleanup(transform.finalize)
313
transform.new_file('file', transform.root, 'content')
314
transform.new_file('FiLe', transform.root, 'content')
315
dir = transform.new_directory('dir', transform.root)
316
transform.new_file('dirfile', dir, 'content')
317
transform.new_file('dirFiLe', dir, 'content')
318
resolve_conflicts(transform)
320
self.failUnlessExists('tree/file')
321
if not os.path.exists('tree/FiLe.moved'):
322
self.failUnlessExists('tree/FiLe')
323
self.failUnlessExists('tree/dir/dirfile')
324
if not os.path.exists('tree/dir/dirFiLe.moved'):
325
self.failUnlessExists('tree/dir/dirFiLe')
327
def test_case_insensitive_limbo(self):
328
tree = self.make_branch_and_tree('tree')
329
# Don't try this at home, kids!
330
# Force the tree to report that it is case insensitive
331
tree.case_sensitive = False
332
transform = TreeTransform(tree)
333
self.addCleanup(transform.finalize)
334
dir = transform.new_directory('dir', transform.root)
335
first = transform.new_file('file', dir, 'content')
336
second = transform.new_file('FiLe', dir, 'content')
337
self.assertContainsRe(transform._limbo_name(first), 'new-1/file')
338
self.assertNotContainsRe(transform._limbo_name(second), 'new-1/FiLe')
340
def test_add_del(self):
341
start, root = self.get_transform()
342
start.new_directory('a', root, 'a')
344
transform, root = self.get_transform()
345
transform.delete_versioned(transform.trans_id_tree_file_id('a'))
346
transform.new_directory('a', root, 'a')
349
def test_unversioning(self):
350
create_tree, root = self.get_transform()
351
parent_id = create_tree.new_directory('parent', root, 'parent-id')
352
create_tree.new_file('child', parent_id, 'child', 'child-id')
354
unversion = TreeTransform(self.wt)
355
self.addCleanup(unversion.finalize)
356
parent = unversion.trans_id_tree_path('parent')
357
unversion.unversion_file(parent)
358
self.assertEqual(unversion.find_conflicts(),
359
[('unversioned parent', parent_id)])
360
file_id = unversion.trans_id_tree_file_id('child-id')
361
unversion.unversion_file(file_id)
364
def test_name_invariants(self):
365
create_tree, root = self.get_transform()
367
root = create_tree.root
368
create_tree.new_file('name1', root, 'hello1', 'name1')
369
create_tree.new_file('name2', root, 'hello2', 'name2')
370
ddir = create_tree.new_directory('dying_directory', root, 'ddir')
371
create_tree.new_file('dying_file', ddir, 'goodbye1', 'dfile')
372
create_tree.new_file('moving_file', ddir, 'later1', 'mfile')
373
create_tree.new_file('moving_file2', root, 'later2', 'mfile2')
376
mangle_tree,root = self.get_transform()
377
root = mangle_tree.root
379
name1 = mangle_tree.trans_id_tree_file_id('name1')
380
name2 = mangle_tree.trans_id_tree_file_id('name2')
381
mangle_tree.adjust_path('name2', root, name1)
382
mangle_tree.adjust_path('name1', root, name2)
384
#tests for deleting parent directories
385
ddir = mangle_tree.trans_id_tree_file_id('ddir')
386
mangle_tree.delete_contents(ddir)
387
dfile = mangle_tree.trans_id_tree_file_id('dfile')
388
mangle_tree.delete_versioned(dfile)
389
mangle_tree.unversion_file(dfile)
390
mfile = mangle_tree.trans_id_tree_file_id('mfile')
391
mangle_tree.adjust_path('mfile', root, mfile)
393
#tests for adding parent directories
394
newdir = mangle_tree.new_directory('new_directory', root, 'newdir')
395
mfile2 = mangle_tree.trans_id_tree_file_id('mfile2')
396
mangle_tree.adjust_path('mfile2', newdir, mfile2)
397
mangle_tree.new_file('newfile', newdir, 'hello3', 'dfile')
398
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
399
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
400
self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
402
self.assertEqual(file(self.wt.abspath('name1')).read(), 'hello2')
403
self.assertEqual(file(self.wt.abspath('name2')).read(), 'hello1')
404
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
405
self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
406
self.assertEqual(file(mfile2_path).read(), 'later2')
407
self.assertEqual(self.wt.id2path('mfile2'), 'new_directory/mfile2')
408
self.assertEqual(self.wt.path2id('new_directory/mfile2'), 'mfile2')
409
newfile_path = self.wt.abspath(pathjoin('new_directory','newfile'))
410
self.assertEqual(file(newfile_path).read(), 'hello3')
411
self.assertEqual(self.wt.path2id('dying_directory'), 'ddir')
412
self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
413
mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
415
def test_both_rename(self):
416
create_tree,root = self.get_transform()
417
newdir = create_tree.new_directory('selftest', root, 'selftest-id')
418
create_tree.new_file('blackbox.py', newdir, 'hello1', 'blackbox-id')
420
mangle_tree,root = self.get_transform()
421
selftest = mangle_tree.trans_id_tree_file_id('selftest-id')
422
blackbox = mangle_tree.trans_id_tree_file_id('blackbox-id')
423
mangle_tree.adjust_path('test', root, selftest)
424
mangle_tree.adjust_path('test_too_much', root, selftest)
425
mangle_tree.set_executability(True, blackbox)
428
def test_both_rename2(self):
429
create_tree,root = self.get_transform()
430
bzrlib = create_tree.new_directory('bzrlib', root, 'bzrlib-id')
431
tests = create_tree.new_directory('tests', bzrlib, 'tests-id')
432
blackbox = create_tree.new_directory('blackbox', tests, 'blackbox-id')
433
create_tree.new_file('test_too_much.py', blackbox, 'hello1',
436
mangle_tree,root = self.get_transform()
437
bzrlib = mangle_tree.trans_id_tree_file_id('bzrlib-id')
438
tests = mangle_tree.trans_id_tree_file_id('tests-id')
439
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
440
mangle_tree.adjust_path('selftest', bzrlib, tests)
441
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
442
mangle_tree.set_executability(True, test_too_much)
445
def test_both_rename3(self):
446
create_tree,root = self.get_transform()
447
tests = create_tree.new_directory('tests', root, 'tests-id')
448
create_tree.new_file('test_too_much.py', tests, 'hello1',
451
mangle_tree,root = self.get_transform()
452
tests = mangle_tree.trans_id_tree_file_id('tests-id')
453
test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
454
mangle_tree.adjust_path('selftest', root, tests)
455
mangle_tree.adjust_path('blackbox.py', tests, test_too_much)
456
mangle_tree.set_executability(True, test_too_much)
459
def test_move_dangling_ie(self):
460
create_tree, root = self.get_transform()
462
root = create_tree.root
463
create_tree.new_file('name1', root, 'hello1', 'name1')
465
delete_contents, root = self.get_transform()
466
file = delete_contents.trans_id_tree_file_id('name1')
467
delete_contents.delete_contents(file)
468
delete_contents.apply()
469
move_id, root = self.get_transform()
470
name1 = move_id.trans_id_tree_file_id('name1')
471
newdir = move_id.new_directory('dir', root, 'newdir')
472
move_id.adjust_path('name2', newdir, name1)
475
def test_replace_dangling_ie(self):
476
create_tree, root = self.get_transform()
478
root = create_tree.root
479
create_tree.new_file('name1', root, 'hello1', 'name1')
481
delete_contents = TreeTransform(self.wt)
482
self.addCleanup(delete_contents.finalize)
483
file = delete_contents.trans_id_tree_file_id('name1')
484
delete_contents.delete_contents(file)
485
delete_contents.apply()
486
delete_contents.finalize()
487
replace = TreeTransform(self.wt)
488
self.addCleanup(replace.finalize)
489
name2 = replace.new_file('name2', root, 'hello2', 'name1')
490
conflicts = replace.find_conflicts()
491
name1 = replace.trans_id_tree_file_id('name1')
492
self.assertEqual(conflicts, [('duplicate id', name1, name2)])
493
resolve_conflicts(replace)
496
def test_symlinks(self):
497
self.requireFeature(SymlinkFeature)
498
transform,root = self.get_transform()
499
oz_id = transform.new_directory('oz', root, 'oz-id')
500
wizard = transform.new_symlink('wizard', oz_id, 'wizard-target',
502
wiz_id = transform.create_path('wizard2', oz_id)
503
transform.create_symlink('behind_curtain', wiz_id)
504
transform.version_file('wiz-id2', wiz_id)
505
transform.set_executability(True, wiz_id)
506
self.assertEqual(transform.find_conflicts(),
507
[('non-file executability', wiz_id)])
508
transform.set_executability(None, wiz_id)
510
self.assertEqual(self.wt.path2id('oz/wizard'), 'wizard-id')
511
self.assertEqual(file_kind(self.wt.abspath('oz/wizard')), 'symlink')
512
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard2')),
514
self.assertEqual(os.readlink(self.wt.abspath('oz/wizard')),
517
def test_unable_create_symlink(self):
519
wt = self.make_branch_and_tree('.')
520
tt = TreeTransform(wt) # TreeTransform obtains write lock
522
tt.new_symlink('foo', tt.root, 'bar')
526
os_symlink = getattr(os, 'symlink', None)
529
err = self.assertRaises(errors.UnableCreateSymlink, tt_helper)
531
"Unable to create symlink 'foo' on this platform",
535
os.symlink = os_symlink
537
def get_conflicted(self):
538
create,root = self.get_transform()
539
create.new_file('dorothy', root, 'dorothy', 'dorothy-id')
540
oz = create.new_directory('oz', root, 'oz-id')
541
create.new_directory('emeraldcity', oz, 'emerald-id')
543
conflicts,root = self.get_transform()
544
# set up duplicate entry, duplicate id
545
new_dorothy = conflicts.new_file('dorothy', root, 'dorothy',
547
old_dorothy = conflicts.trans_id_tree_file_id('dorothy-id')
548
oz = conflicts.trans_id_tree_file_id('oz-id')
549
# set up DeletedParent parent conflict
550
conflicts.delete_versioned(oz)
551
emerald = conflicts.trans_id_tree_file_id('emerald-id')
552
# set up MissingParent conflict
553
munchkincity = conflicts.trans_id_file_id('munchkincity-id')
554
conflicts.adjust_path('munchkincity', root, munchkincity)
555
conflicts.new_directory('auntem', munchkincity, 'auntem-id')
557
conflicts.adjust_path('emeraldcity', emerald, emerald)
558
return conflicts, emerald, oz, old_dorothy, new_dorothy
560
def test_conflict_resolution(self):
561
conflicts, emerald, oz, old_dorothy, new_dorothy =\
562
self.get_conflicted()
563
resolve_conflicts(conflicts)
564
self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
565
self.assertIs(conflicts.final_file_id(old_dorothy), None)
566
self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
567
self.assertEqual(conflicts.final_file_id(new_dorothy), 'dorothy-id')
568
self.assertEqual(conflicts.final_parent(emerald), oz)
571
def test_cook_conflicts(self):
572
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
573
raw_conflicts = resolve_conflicts(tt)
574
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
575
duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved',
576
'dorothy', None, 'dorothy-id')
577
self.assertEqual(cooked_conflicts[0], duplicate)
578
duplicate_id = DuplicateID('Unversioned existing file',
579
'dorothy.moved', 'dorothy', None,
581
self.assertEqual(cooked_conflicts[1], duplicate_id)
582
missing_parent = MissingParent('Created directory', 'munchkincity',
584
deleted_parent = DeletingParent('Not deleting', 'oz', 'oz-id')
585
self.assertEqual(cooked_conflicts[2], missing_parent)
586
unversioned_parent = UnversionedParent('Versioned directory',
589
unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
591
self.assertEqual(cooked_conflicts[3], unversioned_parent)
592
parent_loop = ParentLoop('Cancelled move', 'oz/emeraldcity',
593
'oz/emeraldcity', 'emerald-id', 'emerald-id')
594
self.assertEqual(cooked_conflicts[4], deleted_parent)
595
self.assertEqual(cooked_conflicts[5], unversioned_parent2)
596
self.assertEqual(cooked_conflicts[6], parent_loop)
597
self.assertEqual(len(cooked_conflicts), 7)
600
def test_string_conflicts(self):
601
tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
602
raw_conflicts = resolve_conflicts(tt)
603
cooked_conflicts = cook_conflicts(raw_conflicts, tt)
605
conflicts_s = [str(c) for c in cooked_conflicts]
606
self.assertEqual(len(cooked_conflicts), len(conflicts_s))
607
self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy. '
608
'Moved existing file to '
610
self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy. '
611
'Unversioned existing file '
613
self.assertEqual(conflicts_s[2], 'Conflict adding files to'
614
' munchkincity. Created directory.')
615
self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
616
' versioned, but has versioned'
617
' children. Versioned directory.')
618
self.assertEqualDiff(conflicts_s[4], "Conflict: can't delete oz because it"
619
" is not empty. Not deleting.")
620
self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
621
' versioned, but has versioned'
622
' children. Versioned directory.')
623
self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
624
' oz/emeraldcity. Cancelled move.')
626
def test_moving_versioned_directories(self):
627
create, root = self.get_transform()
628
kansas = create.new_directory('kansas', root, 'kansas-id')
629
create.new_directory('house', kansas, 'house-id')
630
create.new_directory('oz', root, 'oz-id')
632
cyclone, root = self.get_transform()
633
oz = cyclone.trans_id_tree_file_id('oz-id')
634
house = cyclone.trans_id_tree_file_id('house-id')
635
cyclone.adjust_path('house', oz, house)
638
def test_moving_root(self):
639
create, root = self.get_transform()
640
fun = create.new_directory('fun', root, 'fun-id')
641
create.new_directory('sun', root, 'sun-id')
642
create.new_directory('moon', root, 'moon')
644
transform, root = self.get_transform()
645
transform.adjust_root_path('oldroot', fun)
646
new_root=transform.trans_id_tree_path('')
647
transform.version_file('new-root', new_root)
650
def test_renames(self):
651
create, root = self.get_transform()
652
old = create.new_directory('old-parent', root, 'old-id')
653
intermediate = create.new_directory('intermediate', old, 'im-id')
654
myfile = create.new_file('myfile', intermediate, 'myfile-text',
657
rename, root = self.get_transform()
658
old = rename.trans_id_file_id('old-id')
659
rename.adjust_path('new', root, old)
660
myfile = rename.trans_id_file_id('myfile-id')
661
rename.set_executability(True, myfile)
664
def test_find_interesting(self):
665
create, root = self.get_transform()
667
create.new_file('vfile', root, 'myfile-text', 'myfile-id')
668
create.new_file('uvfile', root, 'othertext')
670
result = self.applyDeprecated(symbol_versioning.zero_fifteen,
671
find_interesting, wt, wt, ['vfile'])
672
self.assertEqual(result, set(['myfile-id']))
674
def test_set_executability_order(self):
675
"""Ensure that executability behaves the same, no matter what order.
677
- create file and set executability simultaneously
678
- create file and set executability afterward
679
- unsetting the executability of a file whose executability has not been
680
declared should throw an exception (this may happen when a
681
merge attempts to create a file with a duplicate ID)
683
transform, root = self.get_transform()
686
self.addCleanup(wt.unlock)
687
transform.new_file('set_on_creation', root, 'Set on creation', 'soc',
689
sac = transform.new_file('set_after_creation', root,
690
'Set after creation', 'sac')
691
transform.set_executability(True, sac)
692
uws = transform.new_file('unset_without_set', root, 'Unset badly',
694
self.assertRaises(KeyError, transform.set_executability, None, uws)
696
self.assertTrue(wt.is_executable('soc'))
697
self.assertTrue(wt.is_executable('sac'))
699
def test_preserve_mode(self):
700
"""File mode is preserved when replacing content"""
701
if sys.platform == 'win32':
702
raise TestSkipped('chmod has no effect on win32')
703
transform, root = self.get_transform()
704
transform.new_file('file1', root, 'contents', 'file1-id', True)
706
self.assertTrue(self.wt.is_executable('file1-id'))
707
transform, root = self.get_transform()
708
file1_id = transform.trans_id_tree_file_id('file1-id')
709
transform.delete_contents(file1_id)
710
transform.create_file('contents2', file1_id)
712
self.assertTrue(self.wt.is_executable('file1-id'))
714
def test__set_mode_stats_correctly(self):
715
"""_set_mode stats to determine file mode."""
716
if sys.platform == 'win32':
717
raise TestSkipped('chmod has no effect on win32')
721
def instrumented_stat(path):
722
stat_paths.append(path)
723
return real_stat(path)
725
transform, root = self.get_transform()
727
bar1_id = transform.new_file('bar', root, 'bar contents 1\n',
728
file_id='bar-id-1', executable=False)
731
transform, root = self.get_transform()
732
bar1_id = transform.trans_id_tree_path('bar')
733
bar2_id = transform.trans_id_tree_path('bar2')
735
os.stat = instrumented_stat
736
transform.create_file('bar2 contents\n', bar2_id, mode_id=bar1_id)
741
bar1_abspath = self.wt.abspath('bar')
742
self.assertEqual([bar1_abspath], stat_paths)
744
def test_iter_changes(self):
745
self.wt.set_root_id('eert_toor')
746
transform, root = self.get_transform()
747
transform.new_file('old', root, 'blah', 'id-1', True)
749
transform, root = self.get_transform()
751
self.assertEqual([], list(transform._iter_changes()))
752
old = transform.trans_id_tree_file_id('id-1')
753
transform.unversion_file(old)
754
self.assertEqual([('id-1', ('old', None), False, (True, False),
755
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
756
(True, True))], list(transform._iter_changes()))
757
transform.new_directory('new', root, 'id-1')
758
self.assertEqual([('id-1', ('old', 'new'), True, (True, True),
759
('eert_toor', 'eert_toor'), ('old', 'new'),
760
('file', 'directory'),
761
(True, False))], list(transform._iter_changes()))
765
def test_iter_changes_new(self):
766
self.wt.set_root_id('eert_toor')
767
transform, root = self.get_transform()
768
transform.new_file('old', root, 'blah')
770
transform, root = self.get_transform()
772
old = transform.trans_id_tree_path('old')
773
transform.version_file('id-1', old)
774
self.assertEqual([('id-1', (None, 'old'), False, (False, True),
775
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
776
(False, False))], list(transform._iter_changes()))
780
def test_iter_changes_modifications(self):
781
self.wt.set_root_id('eert_toor')
782
transform, root = self.get_transform()
783
transform.new_file('old', root, 'blah', 'id-1')
784
transform.new_file('new', root, 'blah')
785
transform.new_directory('subdir', root, 'subdir-id')
787
transform, root = self.get_transform()
789
old = transform.trans_id_tree_path('old')
790
subdir = transform.trans_id_tree_file_id('subdir-id')
791
new = transform.trans_id_tree_path('new')
792
self.assertEqual([], list(transform._iter_changes()))
795
transform.delete_contents(old)
796
self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
797
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', None),
798
(False, False))], list(transform._iter_changes()))
801
transform.create_file('blah', old)
802
self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
803
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
804
(False, False))], list(transform._iter_changes()))
805
transform.cancel_deletion(old)
806
self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
807
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
808
(False, False))], list(transform._iter_changes()))
809
transform.cancel_creation(old)
811
# move file_id to a different file
812
self.assertEqual([], list(transform._iter_changes()))
813
transform.unversion_file(old)
814
transform.version_file('id-1', new)
815
transform.adjust_path('old', root, new)
816
self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
817
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
818
(False, False))], list(transform._iter_changes()))
819
transform.cancel_versioning(new)
820
transform._removed_id = set()
823
self.assertEqual([], list(transform._iter_changes()))
824
transform.set_executability(True, old)
825
self.assertEqual([('id-1', ('old', 'old'), False, (True, True),
826
('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
827
(False, True))], list(transform._iter_changes()))
828
transform.set_executability(None, old)
831
self.assertEqual([], list(transform._iter_changes()))
832
transform.adjust_path('new', root, old)
833
transform._new_parent = {}
834
self.assertEqual([('id-1', ('old', 'new'), False, (True, True),
835
('eert_toor', 'eert_toor'), ('old', 'new'), ('file', 'file'),
836
(False, False))], list(transform._iter_changes()))
837
transform._new_name = {}
840
self.assertEqual([], list(transform._iter_changes()))
841
transform.adjust_path('new', subdir, old)
842
transform._new_name = {}
843
self.assertEqual([('id-1', ('old', 'subdir/old'), False,
844
(True, True), ('eert_toor', 'subdir-id'), ('old', 'old'),
845
('file', 'file'), (False, False))],
846
list(transform._iter_changes()))
847
transform._new_path = {}
852
def test_iter_changes_modified_bleed(self):
853
self.wt.set_root_id('eert_toor')
854
"""Modified flag should not bleed from one change to another"""
855
# unfortunately, we have no guarantee that file1 (which is modified)
856
# will be applied before file2. And if it's applied after file2, it
857
# obviously can't bleed into file2's change output. But for now, it
859
transform, root = self.get_transform()
860
transform.new_file('file1', root, 'blah', 'id-1')
861
transform.new_file('file2', root, 'blah', 'id-2')
863
transform, root = self.get_transform()
865
transform.delete_contents(transform.trans_id_file_id('id-1'))
866
transform.set_executability(True,
867
transform.trans_id_file_id('id-2'))
868
self.assertEqual([('id-1', (u'file1', u'file1'), True, (True, True),
869
('eert_toor', 'eert_toor'), ('file1', u'file1'),
870
('file', None), (False, False)),
871
('id-2', (u'file2', u'file2'), False, (True, True),
872
('eert_toor', 'eert_toor'), ('file2', u'file2'),
873
('file', 'file'), (False, True))],
874
list(transform._iter_changes()))
878
def test_iter_changes_move_missing(self):
879
"""Test moving ids with no files around"""
880
self.wt.set_root_id('toor_eert')
881
# Need two steps because versioning a non-existant file is a conflict.
882
transform, root = self.get_transform()
883
transform.new_directory('floater', root, 'floater-id')
885
transform, root = self.get_transform()
886
transform.delete_contents(transform.trans_id_tree_path('floater'))
888
transform, root = self.get_transform()
889
floater = transform.trans_id_tree_path('floater')
891
transform.adjust_path('flitter', root, floater)
892
self.assertEqual([('floater-id', ('floater', 'flitter'), False,
893
(True, True), ('toor_eert', 'toor_eert'), ('floater', 'flitter'),
894
(None, None), (False, False))], list(transform._iter_changes()))
898
def test_iter_changes_pointless(self):
899
"""Ensure that no-ops are not treated as modifications"""
900
self.wt.set_root_id('eert_toor')
901
transform, root = self.get_transform()
902
transform.new_file('old', root, 'blah', 'id-1')
903
transform.new_directory('subdir', root, 'subdir-id')
905
transform, root = self.get_transform()
907
old = transform.trans_id_tree_path('old')
908
subdir = transform.trans_id_tree_file_id('subdir-id')
909
self.assertEqual([], list(transform._iter_changes()))
910
transform.delete_contents(subdir)
911
transform.create_directory(subdir)
912
transform.set_executability(False, old)
913
transform.unversion_file(old)
914
transform.version_file('id-1', old)
915
transform.adjust_path('old', root, old)
916
self.assertEqual([], list(transform._iter_changes()))
920
def test_rename_count(self):
921
transform, root = self.get_transform()
922
transform.new_file('name1', root, 'contents')
923
self.assertEqual(transform.rename_count, 0)
925
self.assertEqual(transform.rename_count, 1)
926
transform2, root = self.get_transform()
927
transform2.adjust_path('name2', root,
928
transform2.trans_id_tree_path('name1'))
929
self.assertEqual(transform2.rename_count, 0)
931
self.assertEqual(transform2.rename_count, 2)
933
def test_change_parent(self):
934
"""Ensure that after we change a parent, the results are still right.
936
Renames and parent changes on pending transforms can happen as part
937
of conflict resolution, and are explicitly permitted by the
940
This test ensures they work correctly with the rename-avoidance
943
transform, root = self.get_transform()
944
parent1 = transform.new_directory('parent1', root)
945
child1 = transform.new_file('child1', parent1, 'contents')
946
parent2 = transform.new_directory('parent2', root)
947
transform.adjust_path('child1', parent2, child1)
949
self.failIfExists(self.wt.abspath('parent1/child1'))
950
self.failUnlessExists(self.wt.abspath('parent2/child1'))
951
# rename limbo/new-1 => parent1, rename limbo/new-3 => parent2
952
# no rename for child1 (counting only renames during apply)
953
self.failUnlessEqual(2, transform.rename_count)
955
def test_cancel_parent(self):
956
"""Cancelling a parent doesn't cause deletion of a non-empty directory
958
This is like the test_change_parent, except that we cancel the parent
959
before adjusting the path. The transform must detect that the
960
directory is non-empty, and move children to safe locations.
962
transform, root = self.get_transform()
963
parent1 = transform.new_directory('parent1', root)
964
child1 = transform.new_file('child1', parent1, 'contents')
965
child2 = transform.new_file('child2', parent1, 'contents')
967
transform.cancel_creation(parent1)
969
self.fail('Failed to move child1 before deleting parent1')
970
transform.cancel_creation(child2)
971
transform.create_directory(parent1)
973
transform.cancel_creation(parent1)
974
# If the transform incorrectly believes that child2 is still in
975
# parent1's limbo directory, it will try to rename it and fail
976
# because was already moved by the first cancel_creation.
978
self.fail('Transform still thinks child2 is a child of parent1')
979
parent2 = transform.new_directory('parent2', root)
980
transform.adjust_path('child1', parent2, child1)
982
self.failIfExists(self.wt.abspath('parent1'))
983
self.failUnlessExists(self.wt.abspath('parent2/child1'))
984
# rename limbo/new-3 => parent2, rename limbo/new-2 => child1
985
self.failUnlessEqual(2, transform.rename_count)
987
def test_adjust_and_cancel(self):
988
"""Make sure adjust_path keeps track of limbo children properly"""
989
transform, root = self.get_transform()
990
parent1 = transform.new_directory('parent1', root)
991
child1 = transform.new_file('child1', parent1, 'contents')
992
parent2 = transform.new_directory('parent2', root)
993
transform.adjust_path('child1', parent2, child1)
994
transform.cancel_creation(child1)
996
transform.cancel_creation(parent1)
997
# if the transform thinks child1 is still in parent1's limbo
998
# directory, it will attempt to move it and fail.
1000
self.fail('Transform still thinks child1 is a child of parent1')
1001
transform.finalize()
1003
def test_noname_contents(self):
1004
"""TreeTransform should permit deferring naming files."""
1005
transform, root = self.get_transform()
1006
parent = transform.trans_id_file_id('parent-id')
1008
transform.create_directory(parent)
1010
self.fail("Can't handle contents with no name")
1011
transform.finalize()
1013
def test_noname_contents_nested(self):
1014
"""TreeTransform should permit deferring naming files."""
1015
transform, root = self.get_transform()
1016
parent = transform.trans_id_file_id('parent-id')
1018
transform.create_directory(parent)
1020
self.fail("Can't handle contents with no name")
1021
child = transform.new_directory('child', parent)
1022
transform.adjust_path('parent', root, parent)
1024
self.failUnlessExists(self.wt.abspath('parent/child'))
1025
self.assertEqual(1, transform.rename_count)
1027
def test_reuse_name(self):
1028
"""Avoid reusing the same limbo name for different files"""
1029
transform, root = self.get_transform()
1030
parent = transform.new_directory('parent', root)
1031
child1 = transform.new_directory('child', parent)
1033
child2 = transform.new_directory('child', parent)
1035
self.fail('Tranform tried to use the same limbo name twice')
1036
transform.adjust_path('child2', parent, child2)
1038
# limbo/new-1 => parent, limbo/new-3 => parent/child2
1039
# child2 is put into top-level limbo because child1 has already
1040
# claimed the direct limbo path when child2 is created. There is no
1041
# advantage in renaming files once they're in top-level limbo, except
1043
self.assertEqual(2, transform.rename_count)
1045
def test_reuse_when_first_moved(self):
1046
"""Don't avoid direct paths when it is safe to use them"""
1047
transform, root = self.get_transform()
1048
parent = transform.new_directory('parent', root)
1049
child1 = transform.new_directory('child', parent)
1050
transform.adjust_path('child1', parent, child1)
1051
child2 = transform.new_directory('child', parent)
1053
# limbo/new-1 => parent
1054
self.assertEqual(1, transform.rename_count)
1056
def test_reuse_after_cancel(self):
1057
"""Don't avoid direct paths when it is safe to use them"""
1058
transform, root = self.get_transform()
1059
parent2 = transform.new_directory('parent2', root)
1060
child1 = transform.new_directory('child1', parent2)
1061
transform.cancel_creation(parent2)
1062
transform.create_directory(parent2)
1063
child2 = transform.new_directory('child1', parent2)
1064
transform.adjust_path('child2', parent2, child1)
1066
# limbo/new-1 => parent2, limbo/new-2 => parent2/child1
1067
self.assertEqual(2, transform.rename_count)
1069
def test_finalize_order(self):
1070
"""Finalize must be done in child-to-parent order"""
1071
transform, root = self.get_transform()
1072
parent = transform.new_directory('parent', root)
1073
child = transform.new_directory('child', parent)
1075
transform.finalize()
1077
self.fail('Tried to remove parent before child1')
1079
def test_cancel_with_cancelled_child_should_succeed(self):
1080
transform, root = self.get_transform()
1081
parent = transform.new_directory('parent', root)
1082
child = transform.new_directory('child', parent)
1083
transform.cancel_creation(child)
1084
transform.cancel_creation(parent)
1085
transform.finalize()
1087
def test_change_entry(self):
1088
txt = 'bzrlib.transform.change_entry was deprecated in version 0.90.'
1089
self.callDeprecated([txt], change_entry, None, None, None, None, None,
1092
def test_case_insensitive_clash(self):
1093
self.requireFeature(CaseInsensitiveFilesystemFeature)
1095
wt = self.make_branch_and_tree('.')
1096
tt = TreeTransform(wt) # TreeTransform obtains write lock
1098
tt.new_file('foo', tt.root, 'bar')
1099
tt.new_file('Foo', tt.root, 'spam')
1100
# Lie to tt that we've already resolved all conflicts.
1101
tt.apply(no_conflicts=True)
1105
err = self.assertRaises(errors.FileExists, tt_helper)
1106
self.assertContainsRe(str(err),
1107
"^File exists: .+/foo")
1109
def test_two_directories_clash(self):
1111
wt = self.make_branch_and_tree('.')
1112
tt = TreeTransform(wt) # TreeTransform obtains write lock
1114
foo_1 = tt.new_directory('foo', tt.root)
1115
tt.new_directory('bar', foo_1)
1116
foo_2 = tt.new_directory('foo', tt.root)
1117
tt.new_directory('baz', foo_2)
1118
# Lie to tt that we've already resolved all conflicts.
1119
tt.apply(no_conflicts=True)
1123
err = self.assertRaises(errors.FileExists, tt_helper)
1124
self.assertContainsRe(str(err),
1125
"^File exists: .+/foo")
1127
def test_two_directories_clash_finalize(self):
1129
wt = self.make_branch_and_tree('.')
1130
tt = TreeTransform(wt) # TreeTransform obtains write lock
1132
foo_1 = tt.new_directory('foo', tt.root)
1133
tt.new_directory('bar', foo_1)
1134
foo_2 = tt.new_directory('foo', tt.root)
1135
tt.new_directory('baz', foo_2)
1136
# Lie to tt that we've already resolved all conflicts.
1137
tt.apply(no_conflicts=True)
1141
err = self.assertRaises(errors.FileExists, tt_helper)
1142
self.assertContainsRe(str(err),
1143
"^File exists: .+/foo")
1146
class TransformGroup(object):
1148
def __init__(self, dirname, root_id):
1151
self.wt = BzrDir.create_standalone_workingtree(dirname)
1152
self.wt.set_root_id(root_id)
1153
self.b = self.wt.branch
1154
self.tt = TreeTransform(self.wt)
1155
self.root = self.tt.trans_id_tree_file_id(self.wt.get_root_id())
1158
def conflict_text(tree, merge):
1159
template = '%s TREE\n%s%s\n%s%s MERGE-SOURCE\n'
1160
return template % ('<' * 7, tree, '=' * 7, merge, '>' * 7)
1163
class TestTransformMerge(TestCaseInTempDir):
1164
def test_text_merge(self):
1165
root_id = generate_ids.gen_root_id()
1166
base = TransformGroup("base", root_id)
1167
base.tt.new_file('a', base.root, 'a\nb\nc\nd\be\n', 'a')
1168
base.tt.new_file('b', base.root, 'b1', 'b')
1169
base.tt.new_file('c', base.root, 'c', 'c')
1170
base.tt.new_file('d', base.root, 'd', 'd')
1171
base.tt.new_file('e', base.root, 'e', 'e')
1172
base.tt.new_file('f', base.root, 'f', 'f')
1173
base.tt.new_directory('g', base.root, 'g')
1174
base.tt.new_directory('h', base.root, 'h')
1176
other = TransformGroup("other", root_id)
1177
other.tt.new_file('a', other.root, 'y\nb\nc\nd\be\n', 'a')
1178
other.tt.new_file('b', other.root, 'b2', 'b')
1179
other.tt.new_file('c', other.root, 'c2', 'c')
1180
other.tt.new_file('d', other.root, 'd', 'd')
1181
other.tt.new_file('e', other.root, 'e2', 'e')
1182
other.tt.new_file('f', other.root, 'f', 'f')
1183
other.tt.new_file('g', other.root, 'g', 'g')
1184
other.tt.new_file('h', other.root, 'h\ni\nj\nk\n', 'h')
1185
other.tt.new_file('i', other.root, 'h\ni\nj\nk\n', 'i')
1187
this = TransformGroup("this", root_id)
1188
this.tt.new_file('a', this.root, 'a\nb\nc\nd\bz\n', 'a')
1189
this.tt.new_file('b', this.root, 'b', 'b')
1190
this.tt.new_file('c', this.root, 'c', 'c')
1191
this.tt.new_file('d', this.root, 'd2', 'd')
1192
this.tt.new_file('e', this.root, 'e2', 'e')
1193
this.tt.new_file('f', this.root, 'f', 'f')
1194
this.tt.new_file('g', this.root, 'g', 'g')
1195
this.tt.new_file('h', this.root, '1\n2\n3\n4\n', 'h')
1196
this.tt.new_file('i', this.root, '1\n2\n3\n4\n', 'i')
1198
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
1200
self.assertEqual(this.wt.get_file('a').read(), 'y\nb\nc\nd\bz\n')
1201
# three-way text conflict
1202
self.assertEqual(this.wt.get_file('b').read(),
1203
conflict_text('b', 'b2'))
1205
self.assertEqual(this.wt.get_file('c').read(), 'c2')
1207
self.assertEqual(this.wt.get_file('d').read(), 'd2')
1208
# Ambigious clean merge
1209
self.assertEqual(this.wt.get_file('e').read(), 'e2')
1211
self.assertEqual(this.wt.get_file('f').read(), 'f')
1212
# Correct correct results when THIS == OTHER
1213
self.assertEqual(this.wt.get_file('g').read(), 'g')
1214
# Text conflict when THIS & OTHER are text and BASE is dir
1215
self.assertEqual(this.wt.get_file('h').read(),
1216
conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
1217
self.assertEqual(this.wt.get_file_byname('h.THIS').read(),
1219
self.assertEqual(this.wt.get_file_byname('h.OTHER').read(),
1221
self.assertEqual(file_kind(this.wt.abspath('h.BASE')), 'directory')
1222
self.assertEqual(this.wt.get_file('i').read(),
1223
conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
1224
self.assertEqual(this.wt.get_file_byname('i.THIS').read(),
1226
self.assertEqual(this.wt.get_file_byname('i.OTHER').read(),
1228
self.assertEqual(os.path.exists(this.wt.abspath('i.BASE')), False)
1229
modified = ['a', 'b', 'c', 'h', 'i']
1230
merge_modified = this.wt.merge_modified()
1231
self.assertSubset(merge_modified, modified)
1232
self.assertEqual(len(merge_modified), len(modified))
1233
file(this.wt.id2abspath('a'), 'wb').write('booga')
1235
merge_modified = this.wt.merge_modified()
1236
self.assertSubset(merge_modified, modified)
1237
self.assertEqual(len(merge_modified), len(modified))
1241
def test_file_merge(self):
1242
self.requireFeature(SymlinkFeature)
1243
root_id = generate_ids.gen_root_id()
1244
base = TransformGroup("BASE", root_id)
1245
this = TransformGroup("THIS", root_id)
1246
other = TransformGroup("OTHER", root_id)
1247
for tg in this, base, other:
1248
tg.tt.new_directory('a', tg.root, 'a')
1249
tg.tt.new_symlink('b', tg.root, 'b', 'b')
1250
tg.tt.new_file('c', tg.root, 'c', 'c')
1251
tg.tt.new_symlink('d', tg.root, tg.name, 'd')
1252
targets = ((base, 'base-e', 'base-f', None, None),
1253
(this, 'other-e', 'this-f', 'other-g', 'this-h'),
1254
(other, 'other-e', None, 'other-g', 'other-h'))
1255
for tg, e_target, f_target, g_target, h_target in targets:
1256
for link, target in (('e', e_target), ('f', f_target),
1257
('g', g_target), ('h', h_target)):
1258
if target is not None:
1259
tg.tt.new_symlink(link, tg.root, target, link)
1261
for tg in this, base, other:
1263
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
1264
self.assertIs(os.path.isdir(this.wt.abspath('a')), True)
1265
self.assertIs(os.path.islink(this.wt.abspath('b')), True)
1266
self.assertIs(os.path.isfile(this.wt.abspath('c')), True)
1267
for suffix in ('THIS', 'BASE', 'OTHER'):
1268
self.assertEqual(os.readlink(this.wt.abspath('d.'+suffix)), suffix)
1269
self.assertIs(os.path.lexists(this.wt.abspath('d')), False)
1270
self.assertEqual(this.wt.id2path('d'), 'd.OTHER')
1271
self.assertEqual(this.wt.id2path('f'), 'f.THIS')
1272
self.assertEqual(os.readlink(this.wt.abspath('e')), 'other-e')
1273
self.assertIs(os.path.lexists(this.wt.abspath('e.THIS')), False)
1274
self.assertIs(os.path.lexists(this.wt.abspath('e.OTHER')), False)
1275
self.assertIs(os.path.lexists(this.wt.abspath('e.BASE')), False)
1276
self.assertIs(os.path.lexists(this.wt.abspath('g')), True)
1277
self.assertIs(os.path.lexists(this.wt.abspath('g.BASE')), False)
1278
self.assertIs(os.path.lexists(this.wt.abspath('h')), False)
1279
self.assertIs(os.path.lexists(this.wt.abspath('h.BASE')), False)
1280
self.assertIs(os.path.lexists(this.wt.abspath('h.THIS')), True)
1281
self.assertIs(os.path.lexists(this.wt.abspath('h.OTHER')), True)
1283
def test_filename_merge(self):
1284
root_id = generate_ids.gen_root_id()
1285
base = TransformGroup("BASE", root_id)
1286
this = TransformGroup("THIS", root_id)
1287
other = TransformGroup("OTHER", root_id)
1288
base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a')
1289
for t in [base, this, other]]
1290
base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b')
1291
for t in [base, this, other]]
1292
base.tt.new_directory('c', base_a, 'c')
1293
this.tt.new_directory('c1', this_a, 'c')
1294
other.tt.new_directory('c', other_b, 'c')
1296
base.tt.new_directory('d', base_a, 'd')
1297
this.tt.new_directory('d1', this_b, 'd')
1298
other.tt.new_directory('d', other_a, 'd')
1300
base.tt.new_directory('e', base_a, 'e')
1301
this.tt.new_directory('e', this_a, 'e')
1302
other.tt.new_directory('e1', other_b, 'e')
1304
base.tt.new_directory('f', base_a, 'f')
1305
this.tt.new_directory('f1', this_b, 'f')
1306
other.tt.new_directory('f1', other_b, 'f')
1308
for tg in [this, base, other]:
1310
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
1311
self.assertEqual(this.wt.id2path('c'), pathjoin('b/c1'))
1312
self.assertEqual(this.wt.id2path('d'), pathjoin('b/d1'))
1313
self.assertEqual(this.wt.id2path('e'), pathjoin('b/e1'))
1314
self.assertEqual(this.wt.id2path('f'), pathjoin('b/f1'))
1316
def test_filename_merge_conflicts(self):
1317
root_id = generate_ids.gen_root_id()
1318
base = TransformGroup("BASE", root_id)
1319
this = TransformGroup("THIS", root_id)
1320
other = TransformGroup("OTHER", root_id)
1321
base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a')
1322
for t in [base, this, other]]
1323
base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b')
1324
for t in [base, this, other]]
1326
base.tt.new_file('g', base_a, 'g', 'g')
1327
other.tt.new_file('g1', other_b, 'g1', 'g')
1329
base.tt.new_file('h', base_a, 'h', 'h')
1330
this.tt.new_file('h1', this_b, 'h1', 'h')
1332
base.tt.new_file('i', base.root, 'i', 'i')
1333
other.tt.new_directory('i1', this_b, 'i')
1335
for tg in [this, base, other]:
1337
Merge3Merger(this.wt, this.wt, base.wt, other.wt)
1339
self.assertEqual(this.wt.id2path('g'), pathjoin('b/g1.OTHER'))
1340
self.assertIs(os.path.lexists(this.wt.abspath('b/g1.BASE')), True)
1341
self.assertIs(os.path.lexists(this.wt.abspath('b/g1.THIS')), False)
1342
self.assertEqual(this.wt.id2path('h'), pathjoin('b/h1.THIS'))
1343
self.assertIs(os.path.lexists(this.wt.abspath('b/h1.BASE')), True)
1344
self.assertIs(os.path.lexists(this.wt.abspath('b/h1.OTHER')), False)
1345
self.assertEqual(this.wt.id2path('i'), pathjoin('b/i1.OTHER'))
1348
class TestBuildTree(tests.TestCaseWithTransport):
1350
def test_build_tree_with_symlinks(self):
1351
self.requireFeature(SymlinkFeature)
1353
a = BzrDir.create_standalone_workingtree('a')
1355
file('a/foo/bar', 'wb').write('contents')
1356
os.symlink('a/foo/bar', 'a/foo/baz')
1357
a.add(['foo', 'foo/bar', 'foo/baz'])
1358
a.commit('initial commit')
1359
b = BzrDir.create_standalone_workingtree('b')
1360
basis = a.basis_tree()
1362
self.addCleanup(basis.unlock)
1363
build_tree(basis, b)
1364
self.assertIs(os.path.isdir('b/foo'), True)
1365
self.assertEqual(file('b/foo/bar', 'rb').read(), "contents")
1366
self.assertEqual(os.readlink('b/foo/baz'), 'a/foo/bar')
1368
def test_build_with_references(self):
1369
tree = self.make_branch_and_tree('source',
1370
format='dirstate-with-subtree')
1371
subtree = self.make_branch_and_tree('source/subtree',
1372
format='dirstate-with-subtree')
1373
tree.add_reference(subtree)
1374
tree.commit('a revision')
1375
tree.branch.create_checkout('target')
1376
self.failUnlessExists('target')
1377
self.failUnlessExists('target/subtree')
1379
def test_file_conflict_handling(self):
1380
"""Ensure that when building trees, conflict handling is done"""
1381
source = self.make_branch_and_tree('source')
1382
target = self.make_branch_and_tree('target')
1383
self.build_tree(['source/file', 'target/file'])
1384
source.add('file', 'new-file')
1385
source.commit('added file')
1386
build_tree(source.basis_tree(), target)
1387
self.assertEqual([DuplicateEntry('Moved existing file to',
1388
'file.moved', 'file', None, 'new-file')],
1390
target2 = self.make_branch_and_tree('target2')
1391
target_file = file('target2/file', 'wb')
1393
source_file = file('source/file', 'rb')
1395
target_file.write(source_file.read())
1400
build_tree(source.basis_tree(), target2)
1401
self.assertEqual([], target2.conflicts())
1403
def test_symlink_conflict_handling(self):
1404
"""Ensure that when building trees, conflict handling is done"""
1405
self.requireFeature(SymlinkFeature)
1406
source = self.make_branch_and_tree('source')
1407
os.symlink('foo', 'source/symlink')
1408
source.add('symlink', 'new-symlink')
1409
source.commit('added file')
1410
target = self.make_branch_and_tree('target')
1411
os.symlink('bar', 'target/symlink')
1412
build_tree(source.basis_tree(), target)
1413
self.assertEqual([DuplicateEntry('Moved existing file to',
1414
'symlink.moved', 'symlink', None, 'new-symlink')],
1416
target = self.make_branch_and_tree('target2')
1417
os.symlink('foo', 'target2/symlink')
1418
build_tree(source.basis_tree(), target)
1419
self.assertEqual([], target.conflicts())
1421
def test_directory_conflict_handling(self):
1422
"""Ensure that when building trees, conflict handling is done"""
1423
source = self.make_branch_and_tree('source')
1424
target = self.make_branch_and_tree('target')
1425
self.build_tree(['source/dir1/', 'source/dir1/file', 'target/dir1/'])
1426
source.add(['dir1', 'dir1/file'], ['new-dir1', 'new-file'])
1427
source.commit('added file')
1428
build_tree(source.basis_tree(), target)
1429
self.assertEqual([], target.conflicts())
1430
self.failUnlessExists('target/dir1/file')
1432
# Ensure contents are merged
1433
target = self.make_branch_and_tree('target2')
1434
self.build_tree(['target2/dir1/', 'target2/dir1/file2'])
1435
build_tree(source.basis_tree(), target)
1436
self.assertEqual([], target.conflicts())
1437
self.failUnlessExists('target2/dir1/file2')
1438
self.failUnlessExists('target2/dir1/file')
1440
# Ensure new contents are suppressed for existing branches
1441
target = self.make_branch_and_tree('target3')
1442
self.make_branch('target3/dir1')
1443
self.build_tree(['target3/dir1/file2'])
1444
build_tree(source.basis_tree(), target)
1445
self.failIfExists('target3/dir1/file')
1446
self.failUnlessExists('target3/dir1/file2')
1447
self.failUnlessExists('target3/dir1.diverted/file')
1448
self.assertEqual([DuplicateEntry('Diverted to',
1449
'dir1.diverted', 'dir1', 'new-dir1', None)],
1452
target = self.make_branch_and_tree('target4')
1453
self.build_tree(['target4/dir1/'])
1454
self.make_branch('target4/dir1/file')
1455
build_tree(source.basis_tree(), target)
1456
self.failUnlessExists('target4/dir1/file')
1457
self.assertEqual('directory', file_kind('target4/dir1/file'))
1458
self.failUnlessExists('target4/dir1/file.diverted')
1459
self.assertEqual([DuplicateEntry('Diverted to',
1460
'dir1/file.diverted', 'dir1/file', 'new-file', None)],
1463
def test_mixed_conflict_handling(self):
1464
"""Ensure that when building trees, conflict handling is done"""
1465
source = self.make_branch_and_tree('source')
1466
target = self.make_branch_and_tree('target')
1467
self.build_tree(['source/name', 'target/name/'])
1468
source.add('name', 'new-name')
1469
source.commit('added file')
1470
build_tree(source.basis_tree(), target)
1471
self.assertEqual([DuplicateEntry('Moved existing file to',
1472
'name.moved', 'name', None, 'new-name')], target.conflicts())
1474
def test_raises_in_populated(self):
1475
source = self.make_branch_and_tree('source')
1476
self.build_tree(['source/name'])
1478
source.commit('added name')
1479
target = self.make_branch_and_tree('target')
1480
self.build_tree(['target/name'])
1482
self.assertRaises(errors.WorkingTreeAlreadyPopulated,
1483
build_tree, source.basis_tree(), target)
1485
def test_build_tree_rename_count(self):
1486
source = self.make_branch_and_tree('source')
1487
self.build_tree(['source/file1', 'source/dir1/'])
1488
source.add(['file1', 'dir1'])
1489
source.commit('add1')
1490
target1 = self.make_branch_and_tree('target1')
1491
transform_result = build_tree(source.basis_tree(), target1)
1492
self.assertEqual(2, transform_result.rename_count)
1494
self.build_tree(['source/dir1/file2'])
1495
source.add(['dir1/file2'])
1496
source.commit('add3')
1497
target2 = self.make_branch_and_tree('target2')
1498
transform_result = build_tree(source.basis_tree(), target2)
1499
# children of non-root directories should not be renamed
1500
self.assertEqual(2, transform_result.rename_count)
1502
def test_build_tree_accelerator_tree(self):
1503
source = self.make_branch_and_tree('source')
1504
self.build_tree_contents([('source/file1', 'A')])
1505
self.build_tree_contents([('source/file2', 'B')])
1506
source.add(['file1', 'file2'], ['file1-id', 'file2-id'])
1507
source.commit('commit files')
1508
self.build_tree_contents([('source/file2', 'C')])
1510
real_source_get_file = source.get_file
1511
def get_file(file_id, path=None):
1512
calls.append(file_id)
1513
return real_source_get_file(file_id, path)
1514
source.get_file = get_file
1516
self.addCleanup(source.unlock)
1517
target = self.make_branch_and_tree('target')
1518
revision_tree = source.basis_tree()
1519
revision_tree.lock_read()
1520
self.addCleanup(revision_tree.unlock)
1521
build_tree(revision_tree, target, source)
1522
self.assertEqual(['file1-id'], calls)
1524
self.addCleanup(target.unlock)
1525
self.assertEqual([], list(target._iter_changes(revision_tree)))
1527
def test_build_tree_accelerator_tree_missing_file(self):
1528
source = self.make_branch_and_tree('source')
1529
self.build_tree_contents([('source/file1', 'A')])
1530
self.build_tree_contents([('source/file2', 'B')])
1531
source.add(['file1', 'file2'])
1532
source.commit('commit files')
1533
os.unlink('source/file1')
1534
source.remove(['file2'])
1535
target = self.make_branch_and_tree('target')
1536
revision_tree = source.basis_tree()
1537
revision_tree.lock_read()
1538
self.addCleanup(revision_tree.unlock)
1539
build_tree(revision_tree, target, source)
1541
self.addCleanup(target.unlock)
1542
self.assertEqual([], list(target._iter_changes(revision_tree)))
1544
def test_build_tree_accelerator_wrong_kind(self):
1545
self.requireFeature(SymlinkFeature)
1546
source = self.make_branch_and_tree('source')
1547
self.build_tree_contents([('source/file1', '')])
1548
self.build_tree_contents([('source/file2', '')])
1549
source.add(['file1', 'file2'], ['file1-id', 'file2-id'])
1550
source.commit('commit files')
1551
os.unlink('source/file2')
1552
self.build_tree_contents([('source/file2/', 'C')])
1553
os.unlink('source/file1')
1554
os.symlink('file2', 'source/file1')
1556
real_source_get_file = source.get_file
1557
def get_file(file_id, path=None):
1558
calls.append(file_id)
1559
return real_source_get_file(file_id, path)
1560
source.get_file = get_file
1562
self.addCleanup(source.unlock)
1563
target = self.make_branch_and_tree('target')
1564
revision_tree = source.basis_tree()
1565
revision_tree.lock_read()
1566
self.addCleanup(revision_tree.unlock)
1567
build_tree(revision_tree, target, source)
1568
self.assertEqual([], calls)
1570
self.addCleanup(target.unlock)
1571
self.assertEqual([], list(target._iter_changes(revision_tree)))
1573
def test_build_tree_accelerator_tree_moved(self):
1574
source = self.make_branch_and_tree('source')
1575
self.build_tree_contents([('source/file1', 'A')])
1576
source.add(['file1'], ['file1-id'])
1577
source.commit('commit files')
1578
source.rename_one('file1', 'file2')
1580
self.addCleanup(source.unlock)
1581
target = self.make_branch_and_tree('target')
1582
revision_tree = source.basis_tree()
1583
revision_tree.lock_read()
1584
self.addCleanup(revision_tree.unlock)
1585
build_tree(revision_tree, target, source)
1587
self.addCleanup(target.unlock)
1588
self.assertEqual([], list(target._iter_changes(revision_tree)))
1591
class MockTransform(object):
1593
def has_named_child(self, by_parent, parent_id, name):
1594
for child_id in by_parent[parent_id]:
1598
elif name == "name.~%s~" % child_id:
1603
class MockEntry(object):
1605
object.__init__(self)
1609
class TestGetBackupName(TestCase):
1610
def test_get_backup_name(self):
1611
tt = MockTransform()
1612
name = get_backup_name(MockEntry(), {'a':[]}, 'a', tt)
1613
self.assertEqual(name, 'name.~1~')
1614
name = get_backup_name(MockEntry(), {'a':['1']}, 'a', tt)
1615
self.assertEqual(name, 'name.~2~')
1616
name = get_backup_name(MockEntry(), {'a':['2']}, 'a', tt)
1617
self.assertEqual(name, 'name.~1~')
1618
name = get_backup_name(MockEntry(), {'a':['2'], 'b':[]}, 'b', tt)
1619
self.assertEqual(name, 'name.~1~')
1620
name = get_backup_name(MockEntry(), {'a':['1', '2', '3']}, 'a', tt)
1621
self.assertEqual(name, 'name.~4~')
1624
class TestFileMover(tests.TestCaseWithTransport):
1626
def test_file_mover(self):
1627
self.build_tree(['a/', 'a/b', 'c/', 'c/d'])
1628
mover = _FileMover()
1629
mover.rename('a', 'q')
1630
self.failUnlessExists('q')
1631
self.failIfExists('a')
1632
self.failUnlessExists('q/b')
1633
self.failUnlessExists('c')
1634
self.failUnlessExists('c/d')
1636
def test_pre_delete_rollback(self):
1637
self.build_tree(['a/'])
1638
mover = _FileMover()
1639
mover.pre_delete('a', 'q')
1640
self.failUnlessExists('q')
1641
self.failIfExists('a')
1643
self.failIfExists('q')
1644
self.failUnlessExists('a')
1646
def test_apply_deletions(self):
1647
self.build_tree(['a/', 'b/'])
1648
mover = _FileMover()
1649
mover.pre_delete('a', 'q')
1650
mover.pre_delete('b', 'r')
1651
self.failUnlessExists('q')
1652
self.failUnlessExists('r')
1653
self.failIfExists('a')
1654
self.failIfExists('b')
1655
mover.apply_deletions()
1656
self.failIfExists('q')
1657
self.failIfExists('r')
1658
self.failIfExists('a')
1659
self.failIfExists('b')
1661
def test_file_mover_rollback(self):
1662
self.build_tree(['a/', 'a/b', 'c/', 'c/d/', 'c/e/'])
1663
mover = _FileMover()
1664
mover.rename('c/d', 'c/f')
1665
mover.rename('c/e', 'c/d')
1667
mover.rename('a', 'c')
1668
except errors.FileExists, e:
1670
self.failUnlessExists('a')
1671
self.failUnlessExists('c/d')
1674
class Bogus(Exception):
1678
class TestTransformRollback(tests.TestCaseWithTransport):
1680
class ExceptionFileMover(_FileMover):
1682
def __init__(self, bad_source=None, bad_target=None):
1683
_FileMover.__init__(self)
1684
self.bad_source = bad_source
1685
self.bad_target = bad_target
1687
def rename(self, source, target):
1688
if (self.bad_source is not None and
1689
source.endswith(self.bad_source)):
1691
elif (self.bad_target is not None and
1692
target.endswith(self.bad_target)):
1695
_FileMover.rename(self, source, target)
1697
def test_rollback_rename(self):
1698
tree = self.make_branch_and_tree('.')
1699
self.build_tree(['a/', 'a/b'])
1700
tt = TreeTransform(tree)
1701
self.addCleanup(tt.finalize)
1702
a_id = tt.trans_id_tree_path('a')
1703
tt.adjust_path('c', tt.root, a_id)
1704
tt.adjust_path('d', a_id, tt.trans_id_tree_path('a/b'))
1705
self.assertRaises(Bogus, tt.apply,
1706
_mover=self.ExceptionFileMover(bad_source='a'))
1707
self.failUnlessExists('a')
1708
self.failUnlessExists('a/b')
1710
self.failUnlessExists('c')
1711
self.failUnlessExists('c/d')
1713
def test_rollback_rename_into_place(self):
1714
tree = self.make_branch_and_tree('.')
1715
self.build_tree(['a/', 'a/b'])
1716
tt = TreeTransform(tree)
1717
self.addCleanup(tt.finalize)
1718
a_id = tt.trans_id_tree_path('a')
1719
tt.adjust_path('c', tt.root, a_id)
1720
tt.adjust_path('d', a_id, tt.trans_id_tree_path('a/b'))
1721
self.assertRaises(Bogus, tt.apply,
1722
_mover=self.ExceptionFileMover(bad_target='c/d'))
1723
self.failUnlessExists('a')
1724
self.failUnlessExists('a/b')
1726
self.failUnlessExists('c')
1727
self.failUnlessExists('c/d')
1729
def test_rollback_deletion(self):
1730
tree = self.make_branch_and_tree('.')
1731
self.build_tree(['a/', 'a/b'])
1732
tt = TreeTransform(tree)
1733
self.addCleanup(tt.finalize)
1734
a_id = tt.trans_id_tree_path('a')
1735
tt.delete_contents(a_id)
1736
tt.adjust_path('d', tt.root, tt.trans_id_tree_path('a/b'))
1737
self.assertRaises(Bogus, tt.apply,
1738
_mover=self.ExceptionFileMover(bad_target='d'))
1739
self.failUnlessExists('a')
1740
self.failUnlessExists('a/b')
1742
def test_resolve_no_parent(self):
1743
wt = self.make_branch_and_tree('.')
1744
tt = TreeTransform(wt)
1745
self.addCleanup(tt.finalize)
1746
parent = tt.trans_id_file_id('parent-id')
1747
tt.new_file('file', parent, 'Contents')
1748
resolve_conflicts(tt)