~bzr-pqm/bzr/bzr.dev

974.1.26 by aaron.bentley at utoronto
merged mbp@sourcefrog.net-20050817233101-0939da1cf91f2472
1
# -*- coding: UTF-8 -*-
2
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.
7
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.
12
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
16
17
from bzrlib.trace import mutter
18
19
class TreeDelta(object):
20
    """Describes changes from one tree to another.
21
22
    Contains four lists:
23
24
    added
25
        (path, id, kind)
26
    removed
27
        (path, id, kind)
28
    renamed
29
        (oldpath, newpath, id, kind, text_modified)
30
    modified
31
        (path, id, kind)
32
    unchanged
33
        (path, id, kind)
34
35
    Each id is listed only once.
36
37
    Files that are both modified and renamed are listed only in
38
    renamed, with the text_modified flag true.
39
40
    Files are only considered renamed if their name has changed or
41
    their parent directory has changed.  Renaming a directory
42
    does not count as renaming all its contents.
43
44
    The lists are normally sorted when the delta is created.
45
    """
46
    def __init__(self):
47
        self.added = []
48
        self.removed = []
49
        self.renamed = []
50
        self.modified = []
51
        self.unchanged = []
52
53
    def __eq__(self, other):
54
        if not isinstance(other, TreeDelta):
55
            return False
56
        return self.added == other.added \
57
               and self.removed == other.removed \
58
               and self.renamed == other.renamed \
59
               and self.modified == other.modified \
60
               and self.unchanged == other.unchanged
61
62
    def __ne__(self, other):
63
        return not (self == other)
64
65
    def __repr__(self):
66
        return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
67
            " unchanged=%r)" % (self.added, self.removed, self.renamed,
68
            self.modified, self.unchanged)
69
70
    def has_changed(self):
71
        changes = len(self.added) + len(self.removed) + len(self.renamed)
72
        changes += len(self.modified) 
73
        return (changes != 0)
74
75
    def touches_file_id(self, file_id):
76
        """Return True if file_id is modified by this delta."""
77
        for l in self.added, self.removed, self.modified:
78
            for v in l:
79
                if v[1] == file_id:
80
                    return True
81
        for v in self.renamed:
82
            if v[2] == file_id:
83
                return True
84
        return False
85
            
86
87
    def show(self, to_file, show_ids=False, show_unchanged=False):
88
        def show_list(files):
89
            for path, fid, kind in files:
90
                if kind == 'directory':
91
                    path += '/'
92
                elif kind == 'symlink':
93
                    path += '@'
94
                    
95
                if show_ids:
96
                    print >>to_file, '  %-30s %s' % (path, fid)
97
                else:
98
                    print >>to_file, ' ', path
99
            
100
        if self.removed:
101
            print >>to_file, 'removed:'
102
            show_list(self.removed)
103
                
104
        if self.added:
105
            print >>to_file, 'added:'
106
            show_list(self.added)
107
108
        if self.renamed:
109
            print >>to_file, 'renamed:'
110
            for oldpath, newpath, fid, kind, text_modified in self.renamed:
111
                if show_ids:
112
                    print >>to_file, '  %s => %s %s' % (oldpath, newpath, fid)
113
                else:
114
                    print >>to_file, '  %s => %s' % (oldpath, newpath)
115
                    
116
        if self.modified:
117
            print >>to_file, 'modified:'
118
            show_list(self.modified)
119
            
120
        if show_unchanged and self.unchanged:
121
            print >>to_file, 'unchanged:'
122
            show_list(self.unchanged)
123
124
125
126
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
127
    """Describe changes from one tree to another.
128
129
    Returns a TreeDelta with details of added, modified, renamed, and
130
    deleted entries.
131
132
    The root entry is specifically exempt.
133
134
    This only considers versioned files.
135
136
    want_unchanged
137
        If true, also list files unchanged from one version to
138
        the next.
139
140
    specific_files
141
        If true, only check for changes to specified names or
142
        files within them.
143
    """
144
145
    from osutils import is_inside_any
146
    
147
    old_inv = old_tree.inventory
148
    new_inv = new_tree.inventory
149
    delta = TreeDelta()
150
    mutter('start compare_trees')
151
152
    # TODO: match for specific files can be rather smarter by finding
153
    # the IDs of those files up front and then considering only that.
154
155
    for file_id in old_tree:
156
        if file_id in new_tree:
157
            old_ie = old_inv[file_id]
158
            new_ie = new_inv[file_id]
159
160
            kind = old_ie.kind
161
            assert kind == new_ie.kind
162
            
163
            assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
164
                   'invalid file kind %r' % kind
165
166
            if kind == 'root_directory':
167
                continue
168
            
169
            if specific_files:
170
                if (not is_inside_any(specific_files, old_inv.id2path(file_id)) 
171
                    and not is_inside_any(specific_files, new_inv.id2path(file_id))):
172
                    continue
173
174
            if kind == 'file':
175
                old_sha1 = old_tree.get_file_sha1(file_id)
176
                new_sha1 = new_tree.get_file_sha1(file_id)
177
                text_modified = (old_sha1 != new_sha1)
178
            else:
179
                ## mutter("no text to check for %r %r" % (file_id, kind))
180
                text_modified = False
181
182
            # TODO: Can possibly avoid calculating path strings if the
183
            # two files are unchanged and their names and parents are
184
            # the same and the parents are unchanged all the way up.
185
            # May not be worthwhile.
186
            
187
            if (old_ie.name != new_ie.name
188
                or old_ie.parent_id != new_ie.parent_id):
189
                delta.renamed.append((old_inv.id2path(file_id),
190
                                      new_inv.id2path(file_id),
191
                                      file_id, kind,
192
                                      text_modified))
193
            elif text_modified:
194
                delta.modified.append((new_inv.id2path(file_id), file_id, kind))
195
            elif want_unchanged:
196
                delta.unchanged.append((new_inv.id2path(file_id), file_id, kind))
197
        else:
198
            kind = old_inv.get_file_kind(file_id)
199
            if kind == 'root_directory':
200
                continue
201
            old_path = old_inv.id2path(file_id)
202
            if specific_files:
203
                if not is_inside_any(specific_files, old_path):
204
                    continue
205
            delta.removed.append((old_path, file_id, kind))
206
207
    mutter('start looking for new files')
208
    for file_id in new_inv:
209
        if file_id in old_inv:
210
            continue
211
        kind = new_inv.get_file_kind(file_id)
212
        if kind == 'root_directory':
213
            continue
214
        new_path = new_inv.id2path(file_id)
215
        if specific_files:
216
            if not is_inside_any(specific_files, new_path):
217
                continue
218
        delta.added.append((new_path, file_id, kind))
219
            
220
    delta.removed.sort()
221
    delta.added.sort()
222
    delta.renamed.sort()
223
    delta.modified.sort()
224
    delta.unchanged.sort()
225
226
    return delta