1
# Copyright (C) 2005-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23
revision as _mod_revision,
27
from bzrlib.tests import per_repository
30
class FileIdInvolvedBase(per_repository.TestCaseWithRepository):
32
def touch(self, tree, filename):
33
# use the trees transport to not depend on the tree's location or type.
34
tree.bzrdir.root_transport.append_bytes(filename, "appended line\n")
36
def compare_tree_fileids(self, branch, old_rev, new_rev):
37
old_tree = self.branch.repository.revision_tree(old_rev)
38
new_tree = self.branch.repository.revision_tree(new_rev)
39
delta = new_tree.changes_from(old_tree)
41
l2 = [id for path, id, kind in delta.added] + \
42
[id for oldpath, newpath, id, kind, text_modified, \
43
meta_modified in delta.renamed] + \
44
[id for path, id, kind, text_modified, meta_modified in \
49
class TestFileIdInvolved(FileIdInvolvedBase):
4
from bzrlib.tests import TestCaseInTempDir
6
from bzrlib.commit import commit
7
from bzrlib.add import smart_add
8
from bzrlib.branch import Branch
9
from bzrlib.clone import copy_branch
10
from bzrlib.merge import merge
11
from bzrlib.workingtree import WorkingTree
12
from bzrlib.delta import compare_trees
14
class TestFileIdInvolved(TestCaseInTempDir):
16
def touch(self,filename):
17
f = file(filename,"a")
18
f.write("appended line\n")
22
def merge( self, branch_from, force=False ):
23
from bzrlib.merge_core import ApplyMerge3
25
merge([branch_from,-1],[None,None], merge_type=ApplyMerge3,
26
check_clean=(not force) )
52
29
super(TestFileIdInvolved, self).setUp()
53
30
# create three branches, and merge it
55
# ,-->J------>K (branch2)
57
# A --->B --->C---->D-->G (main)
59
# '--->E---+---->F (branch1)
62
# B changes: 'a-file-id-2006-01-01-abcd'
63
# C changes: Nothing (perfect merge)
64
# D changes: 'b-file-id-2006-01-01-defg'
67
# G changes: 'b-file-id-2006-01-01-defg'
68
# J changes: 'b-file-id-2006-01-01-defg'
69
# K changes: 'c-funky<file-id>quiji%bo'
71
main_wt = self.make_branch_and_tree('main')
72
main_branch = main_wt.branch
73
self.build_tree(["main/a","main/b","main/c"])
75
main_wt.add(['a', 'b', 'c'], ['a-file-id-2006-01-01-abcd',
32
# /-->J ------>K (branch2)
34
# A ---> B --->C ---->D->G (main)
36
# \---> E---/----> F (branch1)
41
main_branch = Branch.initialize('.')
42
self.build_tree(["a","b","c"])
46
wt.add(['a', 'b', 'c'], ['a-file-id-2006-01-01-abcd',
76
47
'b-file-id-2006-01-01-defg',
77
'c-funky<file-id>quiji%bo'])
79
main_wt.commit("Commit one", rev_id="rev-A")
80
except errors.IllegalPath:
81
# TODO: jam 20060701 Consider raising a different exception
82
# newer formats do support this, and nothin can done to
83
# correct this test - its not a bug.
84
if sys.platform == 'win32':
85
raise tests.TestSkipped('Old repository formats do not'
86
' support file ids with <> on win32')
87
# This is not a known error condition
48
'c-funky<file-id> quiji%bo'])
49
commit(b, "Commit one", rev_id="rev-A")
90
51
#-------- end A -----------
92
bt1 = self.make_branch_and_tree('branch1')
95
self.build_tree(["branch1/d"])
96
bt1.add(['d'], ['file-d'])
97
bt1.commit("branch1, Commit one", rev_id="rev-E")
53
copy_branch(main_branch,"../branch1")
54
os.chdir("../branch1")
56
#branch1_branch = Branch.open(".")
57
self.build_tree(["d"])
59
commit(Branch.open("."), "branch1, Commit one", rev_id="rev-E")
99
61
#-------- end E -----------
101
self.touch(main_wt, "a")
102
main_wt.commit("Commit two", rev_id="rev-B")
65
commit(Branch.open("."), "Commit two", rev_id="rev-B")
104
67
#-------- end B -----------
106
bt2 = self.make_branch_and_tree('branch2')
107
bt2.pull(main_branch)
108
branch2_branch = bt2.branch
109
set_executability(bt2, 'b', True)
110
bt2.commit("branch2, Commit one", rev_id="rev-J")
69
copy_branch(Branch.open("."),"../branch2")
70
os.chdir("../branch2")
72
branch2_branch = Branch.open(".")
74
commit(Branch.open("."), "branch2, Commit one", rev_id="rev-J")
112
76
#-------- end J -----------
114
main_wt.merge_from_branch(b1)
115
main_wt.commit("merge branch1, rev-11", rev_id="rev-C")
80
self.merge("../branch1")
81
commit(Branch.open("."), "merge branch1, rev-11", rev_id="rev-C")
117
83
#-------- end C -----------
119
bt1.rename_one("d","e")
120
bt1.commit("branch1, commit two", rev_id="rev-F")
85
os.chdir("../branch1")
86
tree = WorkingTree('.', Branch.open("."))
87
tree.rename_one("d","e")
88
commit(Branch.open("."), "branch1, commit two", rev_id="rev-F")
122
91
#-------- end F -----------
125
bt2.commit("branch2, commit two", rev_id="rev-K")
93
os.chdir("../branch2")
96
commit(Branch.open("."), "branch2, commit two", rev_id="rev-K")
127
98
#-------- end K -----------
129
main_wt.merge_from_branch(b1)
130
self.touch(main_wt, "b")
103
self.merge("../branch1",force=True)
131
105
# D gets some funky characters to make sure the unescaping works
132
main_wt.commit("merge branch1, rev-12", rev_id="rev-<D>")
106
commit(Branch.open("."), "merge branch1, rev-12", rev_id="rev-<D>")
136
main_wt.merge_from_branch(branch2_branch)
137
main_wt.commit("merge branch1, rev-22", rev_id="rev-G")
110
self.merge("../branch2")
111
commit(Branch.open("."), "merge branch1, rev-22", rev_id="rev-G")
140
self.branch = main_branch
142
def test_fileids_altered_between_two_revs(self):
143
self.branch.lock_read()
144
self.addCleanup(self.branch.unlock)
145
self.branch.repository.fileids_altered_by_revision_ids(["rev-J","rev-K"])
147
{'b-file-id-2006-01-01-defg':set(['rev-J']),
148
'c-funky<file-id>quiji%bo':set(['rev-K'])
150
self.branch.repository.fileids_altered_by_revision_ids(["rev-J","rev-K"]))
153
{'b-file-id-2006-01-01-defg': set(['rev-<D>']),
154
'file-d': set(['rev-F']),
156
self.branch.repository.fileids_altered_by_revision_ids(['rev-<D>', 'rev-F']))
160
'b-file-id-2006-01-01-defg': set(['rev-<D>', 'rev-G', 'rev-J']),
161
'c-funky<file-id>quiji%bo': set(['rev-K']),
162
'file-d': set(['rev-F']),
164
self.branch.repository.fileids_altered_by_revision_ids(
165
['rev-<D>', 'rev-G', 'rev-F', 'rev-K', 'rev-J']))
168
{'a-file-id-2006-01-01-abcd': set(['rev-B']),
169
'b-file-id-2006-01-01-defg': set(['rev-<D>', 'rev-G', 'rev-J']),
170
'c-funky<file-id>quiji%bo': set(['rev-K']),
171
'file-d': set(['rev-F']),
173
self.branch.repository.fileids_altered_by_revision_ids(
174
['rev-G', 'rev-F', 'rev-C', 'rev-B', 'rev-<D>', 'rev-K', 'rev-J']))
176
def fileids_altered_by_revision_ids(self, revision_ids):
177
"""This is a wrapper to strip TREE_ROOT if it occurs"""
178
repo = self.branch.repository
179
root_id = self.branch.basis_tree().get_root_id()
180
result = repo.fileids_altered_by_revision_ids(revision_ids)
181
if root_id in result:
185
def test_fileids_altered_by_revision_ids(self):
186
self.branch.lock_read()
187
self.addCleanup(self.branch.unlock)
189
{'a-file-id-2006-01-01-abcd':set(['rev-A']),
190
'b-file-id-2006-01-01-defg': set(['rev-A']),
191
'c-funky<file-id>quiji%bo': set(['rev-A']),
193
self.fileids_altered_by_revision_ids(["rev-A"]))
195
{'a-file-id-2006-01-01-abcd':set(['rev-B'])
197
self.branch.repository.fileids_altered_by_revision_ids(["rev-B"]))
199
{'b-file-id-2006-01-01-defg':set(['rev-<D>'])
201
self.branch.repository.fileids_altered_by_revision_ids(["rev-<D>"]))
203
def test_fileids_involved_full_compare(self):
204
# this tests that the result of each fileid_involved calculation
205
# along a revision history selects only the fileids selected by
206
# comparing the trees - no less, and no more. This is correct
207
# because in our sample data we do not revert any file ids along
208
# the revision history.
209
self.branch.lock_read()
210
self.addCleanup(self.branch.unlock)
115
self.branch = Branch.open(".")
118
def test_fileid_involved_all_revs(self):
120
l = self.branch.fileid_involved( )
121
self.assertEquals( sorted(map( lambda x: x[0], l )), ["a","b","c","d"])
123
def test_fileid_involved_one_rev(self):
125
l = self.branch.fileid_involved("rev-B" )
126
self.assertEquals( sorted(map( lambda x: x[0], l )), ["a","b","c"])
128
def test_fileid_involved_two_revs(self):
130
l = self.branch.fileid_involved_between_revs("rev-B","rev-K" )
131
self.assertEquals( sorted(map( lambda x: x[0], l )), ["b","c"])
133
l = self.branch.fileid_involved_between_revs("rev-C","rev-<D>" )
134
self.assertEquals( sorted(map( lambda x: x[0], l )), ["b","d"])
136
l = self.branch.fileid_involved_between_revs("rev-C","rev-G" )
137
self.assertEquals( sorted(map( lambda x: x[0], l )), ["b","c","d"])
139
l = self.branch.fileid_involved_between_revs("rev-E","rev-G" )
140
self.assertEquals( sorted(map( lambda x: x[0], l )), ["a", "b","c","d"])
143
def test_fileid_involved_sets(self):
145
l = self.branch.fileid_involved_by_set(set(["rev-B"]))
146
self.assertEquals( sorted(map( lambda x: x[0], l )), ["a"])
148
l = self.branch.fileid_involved_by_set(set(["rev-<D>"]))
149
self.assertEquals( sorted(map( lambda x: x[0], l )), ["b"])
151
def test_fileid_involved_compare(self):
153
l1 = self.branch.fileid_involved_between_revs("rev-E", "rev-<D>")
154
l2 = self.branch.fileid_involved_by_set(set(["rev-<D>","rev-F","rev-C","rev-B"]))
155
self.assertEquals( l1, l2 )
157
l1 = self.branch.fileid_involved_between_revs("rev-C", "rev-G")
158
l2 = self.branch.fileid_involved_by_set(
159
set(["rev-G","rev-<D>","rev-F","rev-K","rev-J"]))
160
self.assertEquals( l1, l2 )
162
def test_fileid_involved_full_compare(self):
163
from bzrlib.tsort import topo_sort
212
165
history = self.branch.revision_history( )
214
167
if len(history) < 2: return
216
169
for start in range(0,len(history)-1):
217
start_id = history[start]
218
170
for end in range(start+1,len(history)):
219
end_id = history[end]
220
old_revs = set(self.branch.repository.get_ancestry(start_id))
221
new_revs = set(self.branch.repository.get_ancestry(end_id))
222
l1 = self.branch.repository.fileids_altered_by_revision_ids(
223
new_revs.difference(old_revs))
226
l2 = self.compare_tree_fileids(self.branch, start_id, end_id)
227
self.assertEquals(l1, l2)
230
class TestFileIdInvolvedNonAscii(FileIdInvolvedBase):
232
def test_utf8_file_ids_and_revision_ids(self):
233
main_wt = self.make_branch_and_tree('main')
234
main_branch = main_wt.branch
235
self.build_tree(["main/a"])
237
file_id = u'a-f\xedle-id'.encode('utf8')
238
main_wt.add(['a'], [file_id])
239
revision_id = u'r\xe9v-a'.encode('utf8')
241
main_wt.commit('a', rev_id=revision_id)
242
except errors.NonAsciiRevisionId:
243
raise tests.TestSkipped('non-ascii revision ids not supported by %s'
244
% self.repository_format)
246
repo = main_wt.branch.repository
248
self.addCleanup(repo.unlock)
249
file_ids = repo.fileids_altered_by_revision_ids([revision_id])
250
root_id = main_wt.basis_tree().get_root_id()
251
if root_id in file_ids:
252
self.assertEqual({file_id:set([revision_id]),
253
root_id:set([revision_id])
256
self.assertEqual({file_id:set([revision_id])}, file_ids)
259
class TestFileIdInvolvedSuperset(FileIdInvolvedBase):
262
super(TestFileIdInvolvedSuperset, self).setUp()
265
main_wt = self.make_branch_and_tree('main')
266
main_branch = main_wt.branch
267
self.build_tree(["main/a","main/b","main/c"])
269
main_wt.add(['a', 'b', 'c'], ['a-file-id-2006-01-01-abcd',
270
'b-file-id-2006-01-01-defg',
271
'c-funky<file-id>quiji\'"%bo'])
273
main_wt.commit("Commit one", rev_id="rev-A")
274
except errors.IllegalPath:
275
# TODO: jam 20060701 Consider raising a different exception
276
# newer formats do support this, and nothin can done to
277
# correct this test - its not a bug.
278
if sys.platform == 'win32':
279
raise tests.TestSkipped('Old repository formats do not'
280
' support file ids with <> on win32')
281
# This is not a known error condition
284
branch2_wt = self.make_branch_and_tree('branch2')
285
branch2_wt.pull(main_branch)
286
branch2_bzrdir = branch2_wt.bzrdir
287
branch2_branch = branch2_bzrdir.open_branch()
288
set_executability(branch2_wt, 'b', True)
289
branch2_wt.commit("branch2, Commit one", rev_id="rev-J")
291
main_wt.merge_from_branch(branch2_branch)
292
set_executability(main_wt, 'b', False)
293
main_wt.commit("merge branch1, rev-22", rev_id="rev-G")
296
self.branch = main_branch
298
def test_fileid_involved_full_compare2(self):
299
# this tests that fileids_altered_by_revision_ids returns
300
# more information than compare_tree can, because it
301
# sees each change rather than the aggregate delta.
302
self.branch.lock_read()
303
self.addCleanup(self.branch.unlock)
304
history = self.branch.revision_history()
307
old_revs = set(self.branch.repository.get_ancestry(old_rev))
308
new_revs = set(self.branch.repository.get_ancestry(new_rev))
310
l1 = self.branch.repository.fileids_altered_by_revision_ids(
311
new_revs.difference(old_revs))
314
l2 = self.compare_tree_fileids(self.branch, old_rev, new_rev)
315
self.assertNotEqual(l2, l1)
316
self.assertSubset(l2, l1)
319
class FileIdInvolvedWGhosts(per_repository.TestCaseWithRepository):
321
def create_branch_with_ghost_text(self):
322
builder = self.make_branch_builder('ghost')
323
builder.build_snapshot('A-id', None, [
324
('add', ('', 'root-id', 'directory', None)),
325
('add', ('a', 'a-file-id', 'file', 'some content\n'))])
326
b = builder.get_branch()
327
old_rt = b.repository.revision_tree('A-id')
328
new_inv = old_rt.inventory._get_mutable_inventory()
329
new_inv.revision_id = 'B-id'
330
new_inv['a-file-id'].revision = 'ghost-id'
331
new_rev = _mod_revision.Revision('B-id',
332
timestamp=time.time(),
334
message='Committing against a ghost',
335
committer='Joe Foo <joe@foo.com>',
337
parent_ids=('A-id', 'ghost-id'),
340
self.addCleanup(b.unlock)
341
b.repository.start_write_group()
342
b.repository.add_revision('B-id', new_rev, new_inv)
343
self.disable_commit_write_group_paranoia(b.repository)
344
b.repository.commit_write_group()
347
def disable_commit_write_group_paranoia(self, repo):
348
if isinstance(repo, remote.RemoteRepository):
349
# We can't easily disable the checks in a remote repo.
350
repo.abort_write_group()
351
raise tests.TestSkipped(
352
"repository format does not support storing revisions with "
354
pack_coll = getattr(repo, '_pack_collection', None)
355
if pack_coll is not None:
356
# Monkey-patch the pack collection instance to allow storing
357
# incomplete revisions.
358
pack_coll._check_new_inventories = lambda: []
360
def test_file_ids_include_ghosts(self):
361
b = self.create_branch_with_ghost_text()
364
{'a-file-id':set(['ghost-id'])},
365
repo.fileids_altered_by_revision_ids(['B-id']))
367
def test_file_ids_uses_fallbacks(self):
368
builder = self.make_branch_builder('source',
369
format=self.bzrdir_format)
370
repo = builder.get_branch().repository
371
if not repo._format.supports_external_lookups:
372
raise tests.TestNotApplicable('format does not support stacking')
373
builder.start_series()
374
builder.build_snapshot('A-id', None, [
375
('add', ('', 'root-id', 'directory', None)),
376
('add', ('file', 'file-id', 'file', 'contents\n'))])
377
builder.build_snapshot('B-id', ['A-id'], [
378
('modify', ('file-id', 'new-content\n'))])
379
builder.build_snapshot('C-id', ['B-id'], [
380
('modify', ('file-id', 'yet more content\n'))])
381
builder.finish_series()
382
source_b = builder.get_branch()
384
self.addCleanup(source_b.unlock)
385
base = self.make_branch('base')
386
base.pull(source_b, stop_revision='B-id')
387
stacked = self.make_branch('stacked')
388
stacked.set_stacked_on_url('../base')
389
stacked.pull(source_b, stop_revision='C-id')
392
self.addCleanup(stacked.unlock)
393
repo = stacked.repository
394
keys = {'file-id': set(['A-id'])}
395
if stacked.repository.supports_rich_root():
396
keys['root-id'] = set(['A-id'])
397
self.assertEqual(keys, repo.fileids_altered_by_revision_ids(['A-id']))
400
def set_executability(wt, path, executable=True):
401
"""Set the executable bit for the file at path in the working tree
403
os.chmod() doesn't work on windows. But TreeTransform can mark or
404
unmark a file as executable.
406
file_id = wt.path2id(path)
407
tt = transform.TreeTransform(wt)
409
tt.set_executability(executable, tt.trans_id_tree_file_id(file_id))
172
l1 = self.branch.fileid_involved_between_revs(
173
history[start], history[end])
175
old_tree = self.branch.revision_tree(history[start])
176
new_tree = self.branch.revision_tree(history[end])
177
delta = compare_trees(old_tree, new_tree )
179
l2 = [ id for path, id, kind in delta.added ] + \
180
[ id for oldpath, newpath, id, kind, text_modified, \
181
meta_modified in delta.renamed ] + \
182
[ id for path, id, kind, text_modified, meta_modified in \
185
self.assertEquals( l1, set(l2) )