1
# Copyright (C) 2005 by Canonical Ltd
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
from cStringIO import StringIO
21
from bzrlib import errors, inventory, osutils
20
22
from bzrlib.branch import Branch
21
import bzrlib.errors as errors
22
23
from bzrlib.diff import internal_diff
23
from bzrlib.inventory import Inventory, ROOT_ID
24
import bzrlib.inventory as inventory
25
from bzrlib.osutils import has_symlinks, rename, pathjoin
26
from bzrlib.tests import TestCase, TestCaseInTempDir
24
from bzrlib.inventory import (Inventory, ROOT_ID, InventoryFile,
25
InventoryDirectory, InventoryEntry, TreeReference)
26
from bzrlib.osutils import (has_symlinks, rename, pathjoin, is_inside_any,
27
is_inside_or_parent_of_any)
28
from bzrlib.tests import TestCase, TestCaseWithTransport
29
from bzrlib.transform import TreeTransform
30
from bzrlib.uncommit import uncommit
29
33
class TestInventory(TestCase):
35
def test_add_path(self):
37
inv = Inventory(root_id=None)
38
self.assertIs(None, inv.root)
39
ie = inv.add_path("", "directory", "my-root")
40
self.assertEqual("my-root", ie.file_id)
41
self.assertIs(ie, inv.root)
31
43
def test_is_within(self):
32
from bzrlib.osutils import is_inside_any
34
45
SRC_FOO_C = pathjoin('src', 'foo.c')
35
46
for dirs, fn in [(['src', 'doc'], SRC_FOO_C),
59
84
self.assert_('src-id' in inv)
62
def test_version(self):
63
"""Inventory remembers the text's version."""
65
ie = inv.add_path('foo.txt', 'file')
86
def test_non_directory_children(self):
87
"""Test path2id when a parent directory has no children"""
88
inv = inventory.Inventory('tree_root')
89
inv.add(inventory.InventoryFile('file-id','file',
90
parent_id='tree_root'))
91
inv.add(inventory.InventoryLink('link-id','link',
92
parent_id='tree_root'))
93
self.assertIs(None, inv.path2id('file/subfile'))
94
self.assertIs(None, inv.path2id('link/subfile'))
96
def test_iter_entries(self):
99
for args in [('src', 'directory', 'src-id'),
100
('doc', 'directory', 'doc-id'),
101
('src/hello.c', 'file', 'hello-id'),
102
('src/bye.c', 'file', 'bye-id'),
103
('Makefile', 'file', 'makefile-id')]:
108
('Makefile', 'makefile-id'),
111
('src/bye.c', 'bye-id'),
112
('src/hello.c', 'hello-id'),
113
], [(path, ie.file_id) for path, ie in inv.iter_entries()])
115
def test_iter_entries_by_dir(self):
118
for args in [('src', 'directory', 'src-id'),
119
('doc', 'directory', 'doc-id'),
120
('src/hello.c', 'file', 'hello-id'),
121
('src/bye.c', 'file', 'bye-id'),
122
('zz', 'file', 'zz-id'),
123
('src/sub/', 'directory', 'sub-id'),
124
('src/zz.c', 'file', 'zzc-id'),
125
('src/sub/a', 'file', 'a-id'),
126
('Makefile', 'file', 'makefile-id')]:
131
('Makefile', 'makefile-id'),
135
('src/bye.c', 'bye-id'),
136
('src/hello.c', 'hello-id'),
137
('src/sub', 'sub-id'),
138
('src/zz.c', 'zzc-id'),
139
('src/sub/a', 'a-id'),
140
], [(path, ie.file_id) for path, ie in inv.iter_entries_by_dir()])
144
('Makefile', 'makefile-id'),
148
('src/bye.c', 'bye-id'),
149
('src/hello.c', 'hello-id'),
150
('src/sub', 'sub-id'),
151
('src/zz.c', 'zzc-id'),
152
('src/sub/a', 'a-id'),
153
], [(path, ie.file_id) for path, ie in inv.iter_entries_by_dir(
154
specific_file_ids=('a-id', 'zzc-id', 'doc-id', ROOT_ID,
155
'hello-id', 'bye-id', 'zz-id', 'src-id', 'makefile-id',
159
('Makefile', 'makefile-id'),
162
('src/bye.c', 'bye-id'),
163
('src/hello.c', 'hello-id'),
164
('src/zz.c', 'zzc-id'),
165
('src/sub/a', 'a-id'),
166
], [(path, ie.file_id) for path, ie in inv.iter_entries_by_dir(
167
specific_file_ids=('a-id', 'zzc-id', 'doc-id',
168
'hello-id', 'bye-id', 'zz-id', 'makefile-id'))])
171
('Makefile', 'makefile-id'),
172
('src/bye.c', 'bye-id'),
173
], [(path, ie.file_id) for path, ie in inv.iter_entries_by_dir(
174
specific_file_ids=('bye-id', 'makefile-id'))])
177
('Makefile', 'makefile-id'),
178
('src/bye.c', 'bye-id'),
179
], [(path, ie.file_id) for path, ie in inv.iter_entries_by_dir(
180
specific_file_ids=('bye-id', 'makefile-id'))])
183
('src/bye.c', 'bye-id'),
184
], [(path, ie.file_id) for path, ie in inv.iter_entries_by_dir(
185
specific_file_ids=('bye-id',))])
187
def test_add_recursive(self):
188
parent = InventoryDirectory('src-id', 'src', ROOT_ID)
189
child = InventoryFile('hello-id', 'hello.c', 'src-id')
190
parent.children[child.file_id] = child
193
self.assertEqual('src/hello.c', inv.id2path('hello-id'))
69
196
class TestInventoryEntry(TestCase):
131
258
link = inventory.InventoryLink('123', 'hello.c', ROOT_ID)
132
259
self.failIf(link.has_text())
135
class TestEntryDiffing(TestCaseInTempDir):
261
def test_make_entry(self):
262
self.assertIsInstance(inventory.make_entry("file", "name", ROOT_ID),
263
inventory.InventoryFile)
264
self.assertIsInstance(inventory.make_entry("symlink", "name", ROOT_ID),
265
inventory.InventoryLink)
266
self.assertIsInstance(inventory.make_entry("directory", "name", ROOT_ID),
267
inventory.InventoryDirectory)
269
def test_make_entry_non_normalized(self):
270
orig_normalized_filename = osutils.normalized_filename
273
osutils.normalized_filename = osutils._accessible_normalized_filename
274
entry = inventory.make_entry("file", u'a\u030a', ROOT_ID)
275
self.assertEqual(u'\xe5', entry.name)
276
self.assertIsInstance(entry, inventory.InventoryFile)
278
osutils.normalized_filename = osutils._inaccessible_normalized_filename
279
self.assertRaises(errors.InvalidNormalization,
280
inventory.make_entry, 'file', u'a\u030a', ROOT_ID)
282
osutils.normalized_filename = orig_normalized_filename
285
class TestEntryDiffing(TestCaseWithTransport):
138
288
super(TestEntryDiffing, self).setUp()
139
self.branch = Branch.initialize(u'.')
140
self.wt = self.branch.working_tree()
289
self.wt = self.make_branch_and_tree('.')
290
self.branch = self.wt.branch
141
291
print >> open('file', 'wb'), 'foo'
142
self.branch.working_tree().add(['file'], ['fileid'])
292
print >> open('binfile', 'wb'), 'foo'
293
self.wt.add(['file'], ['fileid'])
294
self.wt.add(['binfile'], ['binfileid'])
143
295
if has_symlinks():
144
296
os.symlink('target1', 'symlink')
145
self.branch.working_tree().add(['symlink'], ['linkid'])
297
self.wt.add(['symlink'], ['linkid'])
146
298
self.wt.commit('message_1', rev_id = '1')
147
299
print >> open('file', 'wb'), 'bar'
300
print >> open('binfile', 'wb'), 'x' * 1023 + '\x00'
148
301
if has_symlinks():
149
302
os.unlink('symlink')
150
303
os.symlink('target2', 'symlink')
151
304
self.tree_1 = self.branch.repository.revision_tree('1')
152
305
self.inv_1 = self.branch.repository.get_inventory('1')
153
306
self.file_1 = self.inv_1['fileid']
154
self.tree_2 = self.branch.working_tree()
307
self.file_1b = self.inv_1['binfileid']
308
self.tree_2 = self.wt
309
self.tree_2.lock_read()
310
self.addCleanup(self.tree_2.unlock)
155
311
self.inv_2 = self.tree_2.read_working_inventory()
156
312
self.file_2 = self.inv_2['fileid']
313
self.file_2b = self.inv_2['binfileid']
157
314
if has_symlinks():
158
315
self.link_1 = self.inv_1['linkid']
159
316
self.link_2 = self.inv_2['linkid']
239
404
# to change, and then test merge patterns
240
405
# with fake parent entries.
241
406
super(TestSnapshot, self).setUp()
242
self.branch = Branch.initialize(u'.')
407
self.wt = self.make_branch_and_tree('.')
408
self.branch = self.wt.branch
243
409
self.build_tree(['subdir/', 'subdir/file'], line_endings='binary')
244
self.branch.working_tree().add(['subdir', 'subdir/file'],
410
self.wt.add(['subdir', 'subdir/file'],
245
411
['dirid', 'fileid'])
246
412
if has_symlinks():
248
self.wt = self.branch.working_tree()
249
414
self.wt.commit('message_1', rev_id = '1')
250
415
self.tree_1 = self.branch.repository.revision_tree('1')
251
416
self.inv_1 = self.branch.repository.get_inventory('1')
252
417
self.file_1 = self.inv_1['fileid']
253
self.work_tree = self.branch.working_tree()
254
self.file_active = self.work_tree.inventory['fileid']
419
self.addCleanup(self.wt.unlock)
420
self.file_active = self.wt.inventory['fileid']
421
self.builder = self.branch.get_commit_builder([], timestamp=time.time(), revision_id='2')
256
423
def test_snapshot_new_revision(self):
257
424
# This tests that a simple commit with no parents makes a new
258
425
# revision value in the inventory entry
259
self.file_active.snapshot('2', 'subdir/file', {}, self.work_tree,
260
self.branch.repository.weave_store,
261
self.branch.get_transaction())
426
self.file_active.snapshot('2', 'subdir/file', {}, self.wt, self.builder)
262
427
# expected outcome - file_1 has a revision id of '2', and we can get
263
428
# its text of 'file contents' out of the weave.
264
429
self.assertEqual(self.file_1.revision, '1')
265
430
self.assertEqual(self.file_active.revision, '2')
266
431
# this should be a separate test probably, but lets check it once..
267
lines = self.branch.repository.weave_store.get_lines('fileid','2',
268
self.branch.get_transaction())
432
lines = self.branch.repository.weave_store.get_weave(
434
self.branch.get_transaction()).get_lines('2')
269
435
self.assertEqual(lines, ['contents of subdir/file\n'])
271
437
def test_snapshot_unchanged(self):
272
438
#This tests that a simple commit does not make a new entry for
273
439
# an unchanged inventory entry
274
440
self.file_active.snapshot('2', 'subdir/file', {'1':self.file_1},
276
self.branch.repository.weave_store,
277
self.branch.get_transaction())
441
self.wt, self.builder)
278
442
self.assertEqual(self.file_1.revision, '1')
279
443
self.assertEqual(self.file_active.revision, '1')
280
self.assertRaises(errors.WeaveError,
281
self.branch.repository.weave_store.get_lines,
282
'fileid', '2', self.branch.get_transaction())
444
vf = self.branch.repository.weave_store.get_weave(
446
self.branch.repository.get_transaction())
447
self.assertRaises(errors.RevisionNotPresent,
284
451
def test_snapshot_merge_identical_different_revid(self):
285
452
# This tests that a commit with two identical parents, one of which has
293
460
self.assertEqual(self.file_1, other_ie)
294
461
other_ie.revision = 'other'
295
462
self.assertNotEqual(self.file_1, other_ie)
296
self.branch.repository.weave_store.add_identical_text('fileid', '1',
297
'other', ['1'], self.branch.get_transaction())
463
versionfile = self.branch.repository.weave_store.get_weave(
464
'fileid', self.branch.repository.get_transaction())
465
versionfile.clone_text('other', '1', ['1'])
298
466
self.file_active.snapshot('2', 'subdir/file',
299
467
{'1':self.file_1, 'other':other_ie},
301
self.branch.repository.weave_store,
302
self.branch.get_transaction())
468
self.wt, self.builder)
303
469
self.assertEqual(self.file_active.revision, '2')
305
471
def test_snapshot_changed(self):
306
472
# This tests that a commit with one different parent results in a new
307
473
# revision id in the entry.
308
self.file_active.name='newname'
309
rename('subdir/file', 'subdir/newname')
474
self.wt.rename_one('subdir/file', 'subdir/newname')
475
self.file_active = self.wt.inventory['fileid']
310
476
self.file_active.snapshot('2', 'subdir/newname', {'1':self.file_1},
312
self.branch.repository.weave_store,
313
self.branch.get_transaction())
477
self.wt, self.builder)
314
478
# expected outcome - file_1 has a revision id of '2'
315
479
self.assertEqual(self.file_active.revision, '2')
318
class TestPreviousHeads(TestCaseInTempDir):
482
class TestPreviousHeads(TestCaseWithTransport):
321
485
# we want several inventories, that respectively
327
491
# D) fileid present in two inventories and one is
328
492
# a descendent of the other. (B, D)
329
493
super(TestPreviousHeads, self).setUp()
494
self.wt = self.make_branch_and_tree('.')
495
self.branch = self.wt.branch
330
496
self.build_tree(['file'])
331
self.branch = Branch.initialize(u'.')
332
self.wt = self.branch.working_tree()
333
497
self.wt.commit('new branch', allow_pointless=True, rev_id='A')
334
498
self.inv_A = self.branch.repository.get_inventory('A')
335
499
self.wt.add(['file'], ['fileid'])
336
500
self.wt.commit('add file', rev_id='B')
337
501
self.inv_B = self.branch.repository.get_inventory('B')
338
self.branch.lock_write()
340
self.branch.control_files.put_utf8('revision-history', 'A\n')
502
uncommit(self.branch, tree=self.wt)
343
503
self.assertEqual(self.branch.revision_history(), ['A'])
344
504
self.wt.commit('another add of file', rev_id='C')
345
505
self.inv_C = self.branch.repository.get_inventory('C')
346
self.wt.add_pending_merge('B')
506
self.wt.add_parent_tree_id('B')
347
507
self.wt.commit('merge in B', rev_id='D')
348
508
self.inv_D = self.branch.repository.get_inventory('D')
349
self.file_active = self.branch.working_tree().inventory['fileid']
510
self.addCleanup(self.wt.unlock)
511
self.file_active = self.wt.inventory['fileid']
350
512
self.weave = self.branch.repository.weave_store.get_weave('fileid',
351
self.branch.get_transaction())
513
self.branch.repository.get_transaction())
353
515
def get_previous_heads(self, inventories):
354
return self.file_active.find_previous_heads(inventories, self.weave)
516
return self.file_active.find_previous_heads(
518
self.branch.repository.weave_store,
519
self.branch.repository.get_transaction())
356
521
def test_fileid_in_no_inventory(self):
357
522
self.assertEqual({}, self.get_previous_heads([self.inv_A]))
381
546
# TODO: test two inventories with the same file revision
384
class TestExecutable(TestCaseInTempDir):
386
def test_stays_executable(self):
387
basic_inv = """<inventory format="5">
388
<file file_id="a-20051208024829-849e76f7968d7a86" name="a" executable="yes" />
389
<file file_id="b-20051208024829-849e76f7968d7a86" name="b" />
393
b = Branch.initialize('b1')
549
class TestDescribeChanges(TestCase):
551
def test_describe_change(self):
552
# we need to test the following change combinations:
558
# renamed/reparented and modified
559
# change kind (perhaps can't be done yet?)
560
# also, merged in combination with all of these?
561
old_a = InventoryFile('a-id', 'a_file', ROOT_ID)
562
old_a.text_sha1 = '123132'
564
new_a = InventoryFile('a-id', 'a_file', ROOT_ID)
565
new_a.text_sha1 = '123132'
568
self.assertChangeDescription('unchanged', old_a, new_a)
571
new_a.text_sha1 = 'abcabc'
572
self.assertChangeDescription('modified', old_a, new_a)
574
self.assertChangeDescription('added', None, new_a)
575
self.assertChangeDescription('removed', old_a, None)
576
# perhaps a bit questionable but seems like the most reasonable thing...
577
self.assertChangeDescription('unchanged', None, None)
579
# in this case it's both renamed and modified; show a rename and
581
new_a.name = 'newfilename'
582
self.assertChangeDescription('modified and renamed', old_a, new_a)
584
# reparenting is 'renaming'
585
new_a.name = old_a.name
586
new_a.parent_id = 'somedir-id'
587
self.assertChangeDescription('modified and renamed', old_a, new_a)
589
# reset the content values so its not modified
590
new_a.text_size = old_a.text_size
591
new_a.text_sha1 = old_a.text_sha1
592
new_a.name = old_a.name
594
new_a.name = 'newfilename'
595
self.assertChangeDescription('renamed', old_a, new_a)
597
# reparenting is 'renaming'
598
new_a.name = old_a.name
599
new_a.parent_id = 'somedir-id'
600
self.assertChangeDescription('renamed', old_a, new_a)
602
def assertChangeDescription(self, expected_change, old_ie, new_ie):
603
change = InventoryEntry.describe_change(old_ie, new_ie)
604
self.assertEqual(expected_change, change)
607
class TestRevert(TestCaseWithTransport):
609
def test_dangling_id(self):
610
wt = self.make_branch_and_tree('b1')
612
self.addCleanup(wt.unlock)
613
self.assertEqual(len(wt.inventory), 1)
394
614
open('b1/a', 'wb').write('a test\n')
395
open('b1/b', 'wb').write('b test\n')
396
os.chmod('b1/a', 0755)
397
os.chmod('b1/b', 0644)
398
# Manually writing the inventory, to ensure that
399
# the executable="yes" entry is set for 'a' and not for 'b'
400
open('b1/.bzr/inventory', 'wb').write(basic_inv)
402
a_id = "a-20051208024829-849e76f7968d7a86"
403
b_id = "b-20051208024829-849e76f7968d7a86"
405
self.assertEqual(['a', 'b'], [cn for cn,ie in t.inventory.iter_entries()])
407
self.failUnless(t.is_executable(a_id), "'a' lost the execute bit")
408
self.failIf(t.is_executable(b_id), "'b' gained an execute bit")
410
t.commit('adding a,b', rev_id='r1')
412
rev_tree = b.repository.revision_tree('r1')
413
self.failUnless(rev_tree.is_executable(a_id), "'a' lost the execute bit")
414
self.failIf(rev_tree.is_executable(b_id), "'b' gained an execute bit")
416
self.failUnless(rev_tree.inventory[a_id].executable)
417
self.failIf(rev_tree.inventory[b_id].executable)
419
# Make sure the entries are gone
422
self.failIf(t.has_id(a_id))
423
self.failIf(t.has_filename('a'))
424
self.failIf(t.has_id(b_id))
425
self.failIf(t.has_filename('b'))
427
# Make sure that revert is able to bring them back,
428
# and sets 'a' back to being executable
430
t.revert(['a', 'b'], rev_tree, backups=False)
431
self.assertEqual(['a', 'b'], [cn for cn,ie in t.inventory.iter_entries()])
433
self.failUnless(t.is_executable(a_id), "'a' lost the execute bit")
434
self.failIf(t.is_executable(b_id), "'b' gained an execute bit")
436
# Now remove them again, and make sure that after a
437
# commit, they are still marked correctly
440
t.commit('removed', rev_id='r2')
442
self.assertEqual([], [cn for cn,ie in t.inventory.iter_entries()])
443
self.failIf(t.has_id(a_id))
444
self.failIf(t.has_filename('a'))
445
self.failIf(t.has_id(b_id))
446
self.failIf(t.has_filename('b'))
448
# Now revert back to the previous commit
449
t.revert([], rev_tree, backups=False)
450
# TODO: FIXME: For some reason, after revert, the tree does not
451
# regenerate its working inventory, so we have to manually delete
452
# the working tree, and create a new one
453
# This seems to happen any time you do a merge operation on the
458
self.assertEqual(['a', 'b'], [cn for cn,ie in t.inventory.iter_entries()])
460
self.failUnless(t.is_executable(a_id), "'a' lost the execute bit")
461
self.failIf(t.is_executable(b_id), "'b' gained an execute bit")
463
# Now make sure that 'bzr branch' also preserves the
465
# TODO: Maybe this should be a blackbox test
466
b.clone('b2', revision='r1')
467
b2 = Branch.open('b2')
468
self.assertEquals('r1', b2.last_revision())
469
t2 = b2.working_tree()
471
self.assertEqual(['a', 'b'], [cn for cn,ie in t2.inventory.iter_entries()])
472
self.failUnless(t2.is_executable(a_id), "'a' lost the execute bit")
473
self.failIf(t2.is_executable(b_id), "'b' gained an execute bit")
475
# Make sure pull will delete the files
477
self.assertEquals('r2', b2.last_revision())
478
# FIXME: Same thing here, t2 needs to be recreated
480
t2 = b2.working_tree()
481
self.assertEqual([], [cn for cn,ie in t2.inventory.iter_entries()])
483
# Now commit the changes on the first branch
484
# so that the second branch can pull the changes
485
# and make sure that the executable bit has been copied
486
t.commit('resurrected', rev_id='r3')
491
t2 = b2.working_tree()
492
self.assertEquals('r3', b2.last_revision())
493
self.assertEqual(['a', 'b'], [cn for cn,ie in t2.inventory.iter_entries()])
495
self.failUnless(t2.is_executable(a_id), "'a' lost the execute bit")
496
self.failIf(t2.is_executable(b_id), "'b' gained an execute bit")
616
self.assertEqual(len(wt.inventory), 2)
617
wt.flush() # workaround revert doing wt._write_inventory for now.
620
self.assertEqual(len(wt.inventory), 1)
623
class TestIsRoot(TestCase):
624
"""Ensure our root-checking code is accurate."""
626
def test_is_root(self):
627
inv = Inventory('TREE_ROOT')
628
self.assertTrue(inv.is_root('TREE_ROOT'))
629
self.assertFalse(inv.is_root('booga'))
630
inv.root.file_id = 'booga'
631
self.assertFalse(inv.is_root('TREE_ROOT'))
632
self.assertTrue(inv.is_root('booga'))
633
# works properly even if no root is set
635
self.assertFalse(inv.is_root('TREE_ROOT'))
636
self.assertFalse(inv.is_root('booga'))
639
class TestTreeReference(TestCase):
641
def test_create(self):
642
inv = Inventory('tree-root-123')
643
inv.add(TreeReference('nested-id', 'nested', parent_id='tree-root-123',
644
revision='rev', reference_revision='rev2'))