1
# Copyright (C) 2005 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
17
"""Tree classes, representing directory at point in time.
21
from cStringIO import StringIO
22
from warnings import warn
25
from bzrlib.errors import BzrError, BzrCheckError
26
from bzrlib import errors
27
from bzrlib.inventory import Inventory
28
from bzrlib.inter import InterObject
29
from bzrlib.osutils import fingerprint_file
30
import bzrlib.revision
31
from bzrlib.trace import mutter, note
35
"""Abstract file tree.
37
There are several subclasses:
39
* `WorkingTree` exists as files on disk editable by the user.
41
* `RevisionTree` is a tree as recorded at some point in the past.
43
Trees contain an `Inventory` object, and also know how to retrieve
44
file texts mentioned in the inventory, either from a working
45
directory or from a store.
47
It is possible for trees to contain files that are not described
48
in their inventory or vice versa; for this use `filenames()`.
50
Trees can be compared, etc, regardless of whether they are working
51
trees or versioned trees.
54
def compare(self, other):
55
"""Compare this tree with other.
57
:param other: A tre to compare with.
58
The comparison will be performed by an InterTree object looked up on
61
return InterTree.get(self, other).compare()
64
"""Get a list of the conflicts in the tree.
66
Each conflict is an instance of bzrlib.conflicts.Conflict.
70
def get_parent_ids(self):
71
"""Get the parent ids for this tree.
73
:return: a list of parent ids. [] is returned to indicate
74
a tree with no parents.
75
:raises: BzrError if the parents are not known.
77
raise NotImplementedError(self.get_parent_ids)
79
def has_filename(self, filename):
80
"""True if the tree has given filename."""
81
raise NotImplementedError()
83
def has_id(self, file_id):
84
return self.inventory.has_id(file_id)
88
def has_or_had_id(self, file_id):
89
if file_id == self.inventory.root.file_id:
91
return self.inventory.has_id(file_id)
94
return iter(self.inventory)
96
def id2path(self, file_id):
97
return self.inventory.id2path(file_id)
99
def iter_entries_by_dir(self):
100
"""Walk the tree in 'by_dir' order.
102
This will yield each entry in the tree as a (path, entry) tuple. The
103
order that they are yielded is: the contents of a directory are
104
preceeded by the parent of a directory, and all the contents of a
105
directory are grouped together.
107
return self.inventory.iter_entries_by_dir()
109
def kind(self, file_id):
110
raise NotImplementedError("subclasses must implement kind")
112
def _get_inventory(self):
113
return self._inventory
115
def get_file_by_path(self, path):
116
return self.get_file(self._inventory.path2id(path))
118
inventory = property(_get_inventory,
119
doc="Inventory of this Tree")
121
def _check_retrieved(self, ie, f):
124
fp = fingerprint_file(f)
127
if ie.text_size != None:
128
if ie.text_size != fp['size']:
129
raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store),
130
["inventory expects %d bytes" % ie.text_size,
131
"file is actually %d bytes" % fp['size'],
132
"store is probably damaged/corrupt"])
134
if ie.text_sha1 != fp['sha1']:
135
raise BzrError("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store),
136
["inventory expects %s" % ie.text_sha1,
137
"file is actually %s" % fp['sha1'],
138
"store is probably damaged/corrupt"])
141
def print_file(self, file_id):
142
"""Print file with id `file_id` to stdout."""
144
sys.stdout.write(self.get_file_text(file_id))
150
"""What files are present in this tree and unknown.
152
:return: an iterator over the unknown files.
159
def filter_unversioned_files(self, paths):
160
"""Filter out paths that are not versioned.
162
:return: set of paths.
164
# NB: we specifically *don't* call self.has_filename, because for
165
# WorkingTrees that can indicate files that exist on disk but that
167
pred = self.inventory.has_filename
168
return set((p for p in paths if not pred(p)))
172
from bzrlib.revisiontree import RevisionTree
175
class EmptyTree(Tree):
178
self._inventory = Inventory()
179
warn('EmptyTree is deprecated as of bzr 0.9 please use '
180
'repository.revision_tree instead.',
181
DeprecationWarning, stacklevel=2)
183
def get_parent_ids(self):
186
def get_symlink_target(self, file_id):
189
def has_filename(self, filename):
192
def kind(self, file_id):
193
assert self._inventory[file_id].kind == "root_directory"
194
return "root_directory"
196
def list_files(self):
199
def __contains__(self, file_id):
200
return file_id in self._inventory
202
def get_file_sha1(self, file_id, path=None):
203
assert self._inventory[file_id].kind == "root_directory"
207
######################################################################
210
# TODO: Merge these two functions into a single one that can operate
211
# on either a whole tree or a set of files.
213
# TODO: Return the diff in order by filename, not by category or in
214
# random order. Can probably be done by lock-stepping through the
215
# filenames from both trees.
218
def file_status(filename, old_tree, new_tree):
219
"""Return single-letter status, old and new names for a file.
221
The complexity here is in deciding how to represent renames;
222
many complex cases are possible.
224
old_inv = old_tree.inventory
225
new_inv = new_tree.inventory
226
new_id = new_inv.path2id(filename)
227
old_id = old_inv.path2id(filename)
229
if not new_id and not old_id:
230
# easy: doesn't exist in either; not versioned at all
231
if new_tree.is_ignored(filename):
232
return 'I', None, None
234
return '?', None, None
236
# There is now a file of this name, great.
239
# There is no longer a file of this name, but we can describe
240
# what happened to the file that used to have
241
# this name. There are two possibilities: either it was
242
# deleted entirely, or renamed.
244
if new_inv.has_id(old_id):
245
return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id)
247
return 'D', old_inv.id2path(old_id), None
249
# if the file_id is new in this revision, it is added
250
if new_id and not old_inv.has_id(new_id):
253
# if there used to be a file of this name, but that ID has now
254
# disappeared, it is deleted
255
if old_id and not new_inv.has_id(old_id):
262
def find_renames(old_inv, new_inv):
263
for file_id in old_inv:
264
if file_id not in new_inv:
266
old_name = old_inv.id2path(file_id)
267
new_name = new_inv.id2path(file_id)
268
if old_name != new_name:
269
yield (old_name, new_name)
272
def find_ids_across_trees(filenames, trees, require_versioned=True):
273
"""Find the ids corresponding to specified filenames.
275
All matches in all trees will be used, and all children of matched
276
directories will be used.
278
:param filenames: The filenames to find file_ids for
279
:param trees: The trees to find file_ids within
280
:param require_versioned: if true, all specified filenames must occur in
282
:return: a set of file ids for the specified filenames and their children.
286
specified_ids = _find_filename_ids_across_trees(filenames, trees,
288
return _find_children_across_trees(specified_ids, trees)
291
def _find_filename_ids_across_trees(filenames, trees, require_versioned):
292
"""Find the ids corresponding to specified filenames.
294
All matches in all trees will be used.
296
:param filenames: The filenames to find file_ids for
297
:param trees: The trees to find file_ids within
298
:param require_versioned: if true, all specified filenames must occur in
300
:return: a set of file ids for the specified filenames
303
interesting_ids = set()
304
for tree_path in filenames:
307
file_id = tree.inventory.path2id(tree_path)
308
if file_id is not None:
309
interesting_ids.add(file_id)
312
not_versioned.append(tree_path)
313
if len(not_versioned) > 0 and require_versioned:
314
raise errors.PathsNotVersionedError(not_versioned)
315
return interesting_ids
318
def _find_children_across_trees(specified_ids, trees):
319
"""Return a set including specified ids and their children
321
All matches in all trees will be used.
323
:param trees: The trees to find file_ids within
324
:return: a set containing all specified ids and their children
326
interesting_ids = set(specified_ids)
327
pending = interesting_ids
328
# now handle children of interesting ids
329
# we loop so that we handle all children of each id in both trees
330
while len(pending) > 0:
332
for file_id in pending:
334
if file_id not in tree:
336
entry = tree.inventory[file_id]
337
for child in getattr(entry, 'children', {}).itervalues():
338
if child.file_id not in interesting_ids:
339
new_pending.add(child.file_id)
340
interesting_ids.update(new_pending)
341
pending = new_pending
342
return interesting_ids
345
class InterTree(InterObject):
346
"""This class represents operations taking place between two Trees.
348
Its instances have methods like 'compare' and contain references to the
349
source and target trees these operations are to be carried out on.
351
clients of bzrlib should not need to use InterTree directly, rather they
352
should use the convenience methods on Tree such as 'Tree.compare()' which
353
will pass through to InterTree as appropriate.
359
"""Compare source and target.
361
:return: A TreeDelta.
363
# imported later to avoid circular imports
364
from bzrlib.delta import compare_trees
365
return compare_trees(self.source, self.target)