1
# Copyright (C) 2005, 2006 Canonical
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
from bzrlib.inventory import InventoryEntry
18
from bzrlib.osutils import pathjoin
19
from bzrlib.trace import mutter
22
class TreeDelta(object):
23
"""Describes changes from one tree to another.
32
(oldpath, newpath, id, kind, text_modified, meta_modified)
34
(path, id, kind, text_modified, meta_modified)
38
Each id is listed only once.
40
Files that are both modified and renamed are listed only in
41
renamed, with the text_modified flag true. The text_modified
42
applies either to the the content of the file or the target of the
43
symbolic link, depending of the kind of file.
45
Files are only considered renamed if their name has changed or
46
their parent directory has changed. Renaming a directory
47
does not count as renaming all its contents.
49
The lists are normally sorted when the delta is created.
58
def __eq__(self, other):
59
if not isinstance(other, TreeDelta):
61
return self.added == other.added \
62
and self.removed == other.removed \
63
and self.renamed == other.renamed \
64
and self.modified == other.modified \
65
and self.unchanged == other.unchanged
67
def __ne__(self, other):
68
return not (self == other)
71
return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
72
" unchanged=%r)" % (self.added, self.removed, self.renamed,
73
self.modified, self.unchanged)
75
def has_changed(self):
76
return bool(self.modified
81
def touches_file_id(self, file_id):
82
"""Return True if file_id is modified by this delta."""
83
for l in self.added, self.removed, self.modified:
87
for v in self.renamed:
93
def show(self, to_file, show_ids=False, show_unchanged=False):
96
path, fid, kind = item[:3]
98
if kind == 'directory':
100
elif kind == 'symlink':
103
if len(item) == 5 and item[4]:
107
print >>to_file, ' %-30s %s' % (path, fid)
109
print >>to_file, ' ', path
112
print >>to_file, 'removed:'
113
show_list(self.removed)
116
print >>to_file, 'added:'
117
show_list(self.added)
122
print >>to_file, 'renamed:'
123
for (oldpath, newpath, fid, kind,
124
text_modified, meta_modified) in self.renamed:
125
if text_modified or meta_modified:
126
extra_modified.append((newpath, fid, kind,
127
text_modified, meta_modified))
131
print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid)
133
print >>to_file, ' %s => %s' % (oldpath, newpath)
135
if self.modified or extra_modified:
136
print >>to_file, 'modified:'
137
show_list(self.modified)
138
show_list(extra_modified)
140
if show_unchanged and self.unchanged:
141
print >>to_file, 'unchanged:'
142
show_list(self.unchanged)
146
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
147
"""Describe changes from one tree to another.
149
Returns a TreeDelta with details of added, modified, renamed, and
152
The root entry is specifically exempt.
154
This only considers versioned files.
157
If true, also list files unchanged from one version to
161
If true, only check for changes to specified names or
162
files within them. Any unversioned files given have no effect
163
(but this might change in the future).
165
# NB: show_status depends on being able to pass in non-versioned files and
166
# report them as unknown
171
return _compare_trees(old_tree, new_tree, want_unchanged,
179
def _compare_trees(old_tree, new_tree, want_unchanged, specific_files):
181
from bzrlib.osutils import is_inside_any
183
old_inv = old_tree.inventory
184
new_inv = new_tree.inventory
186
mutter('start compare_trees')
188
# TODO: Rather than iterating over the whole tree and then filtering, we
189
# could diff just the specified files (if any) and their subtrees.
190
# Perhaps should take a list of file-ids instead? Need to indicate any
191
# ids or names which were not found in the trees.
193
# Map file_id to path and inventory entry
194
# We probably need to
195
new_id_to_path_map = {None:''}
197
def get_new_path(new_ie):
198
if new_ie.file_id in new_id_to_path_map:
199
return new_id_to_path_map[new_ie.file_id]
200
if new_ie.parent_id is None:
202
return pathjoin(get_new_path(new_inv[new_ie.parent_id]), new_ie.name)
204
for old_path, old_ie in old_inv.iter_entries():
205
if not old_tree.has_file_or_id(old_path, old_ie.file_id):
206
# In case old_tree is a WorkingTree, and the file
209
if new_inv.has_id(old_ie.file_id):
210
new_ie = new_inv[old_ie.file_id]
211
new_path = get_new_path(new_ie)
215
if new_path and new_tree.has_file_or_id(new_path, old_ie.file_id):
216
assert old_ie.kind == new_ie.kind
218
assert old_ie.kind in InventoryEntry.known_kinds, \
219
'invalid file kind %r' % old_ie.kind
221
if old_ie.kind == 'root_directory':
225
if (not is_inside_any(specific_files, old_path)
226
and not is_inside_any(specific_files, new_path)):
229
old_ie._read_tree_state(old_path, old_tree)
230
new_ie._read_tree_state(new_path, new_tree)
231
text_modified, meta_modified = new_ie.detect_changes(old_ie)
233
# TODO: Can possibly avoid calculating path strings if the
234
# two files are unchanged and their names and parents are
235
# the same and the parents are unchanged all the way up.
236
# May not be worthwhile.
238
if (old_ie.name != new_ie.name
239
or old_ie.parent_id != new_ie.parent_id):
240
delta.renamed.append((old_path,
242
old_ie.file_id, old_ie.kind,
243
text_modified, meta_modified))
244
elif text_modified or meta_modified:
245
delta.modified.append((new_path, old_ie.file_id, old_ie.kind,
246
text_modified, meta_modified))
248
delta.unchanged.append((new_path, old_ie.file_id, old_ie.kind))
250
if old_ie.kind == 'root_directory':
253
if not is_inside_any(specific_files, old_path):
255
delta.removed.append((old_path, old_ie.file_id, old_ie.kind))
257
mutter('start looking for new files')
258
for new_path, new_ie in new_inv.iter_entries():
259
if (new_ie.file_id in old_inv
260
or not new_tree.has_file_or_id(new_path, new_ie.file_id)):
262
if new_ie.kind == 'root_directory':
265
if not is_inside_any(specific_files, new_path):
267
delta.added.append((new_path, new_ie.file_id, new_ie.kind))
272
delta.modified.sort()
273
delta.unchanged.sort()