~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/delta.py

first cut at merge from integration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical
 
1
# -*- coding: UTF-8 -*-
2
2
 
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
17
17
from bzrlib.inventory import InventoryEntry
18
18
from bzrlib.trace import mutter
19
19
 
20
 
 
21
20
class TreeDelta(object):
22
21
    """Describes changes from one tree to another.
23
22
 
158
157
 
159
158
    specific_files
160
159
        If true, only check for changes to specified names or
161
 
        files within them.  Any unversioned files given have no effect
162
 
        (but this might change in the future).
 
160
        files within them.
163
161
    """
164
 
    # NB: show_status depends on being able to pass in non-versioned files and
165
 
    # report them as unknown
 
162
 
166
163
    old_tree.lock_read()
167
164
    try:
168
165
        new_tree.lock_read()
184
181
    delta = TreeDelta()
185
182
    mutter('start compare_trees')
186
183
 
187
 
    # TODO: Rather than iterating over the whole tree and then filtering, we
188
 
    # could diff just the specified files (if any) and their subtrees.  
189
 
    # Perhaps should take a list of file-ids instead?   Need to indicate any
190
 
    # ids or names which were not found in the trees.
191
 
 
192
 
    old_files = old_tree.list_files()
193
 
    new_files = new_tree.list_files()
194
 
 
195
 
    more_old = True
196
 
    more_new = True
197
 
 
198
 
    added = {}
199
 
    removed = {}
200
 
 
201
 
    def get_next(iter):
202
 
        try:
203
 
            return iter.next()
204
 
        except StopIteration:
205
 
            return None, None, None, None, None
206
 
    old_path, old_class, old_kind, old_file_id, old_entry = get_next(old_files)
207
 
    new_path, new_class, new_kind, new_file_id, new_entry = get_next(new_files)
208
 
 
209
 
 
210
 
    def check_matching(old_path, old_entry, new_path, new_entry):
211
 
        """We have matched up 2 file_ids, check for changes."""
212
 
        assert old_entry.kind == new_entry.kind
213
 
 
214
 
        if old_entry.kind == 'root_directory':
215
 
            return
216
 
 
217
 
        if specific_files:
218
 
            if (not is_inside_any(specific_files, old_path)
219
 
                and not is_inside_any(specific_files, new_path)):
220
 
                return
221
 
 
222
 
        # temporary hack until all entries are populated before clients 
223
 
        # get them
224
 
        old_entry._read_tree_state(old_path, old_tree)
225
 
        new_entry._read_tree_state(new_path, new_tree)
226
 
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
227
 
        
228
 
        # If the name changes, or the parent_id changes, we have a rename
229
 
        # (if we move a parent, that doesn't count as a rename for the file)
230
 
        if (old_entry.name != new_entry.name 
231
 
            or old_entry.parent_id != new_entry.parent_id):
232
 
            delta.renamed.append((old_path,
233
 
                                  new_path,
234
 
                                  old_entry.file_id, old_entry.kind,
235
 
                                  text_modified, meta_modified))
236
 
        elif text_modified or meta_modified:
237
 
            delta.modified.append((new_path, new_entry.file_id, new_entry.kind,
238
 
                                   text_modified, meta_modified))
239
 
        elif want_unchanged:
240
 
            delta.unchanged.append((new_path, new_entry.file_id, new_entry.kind))
241
 
 
242
 
 
243
 
    def handle_old(path, entry):
244
 
        """old entry without a new entry match
245
 
 
246
 
        Check to see if a matching new entry was already seen as an
247
 
        added file, and switch the pair into being a rename.
248
 
        Otherwise just mark the old entry being removed.
249
 
        """
250
 
        if entry.file_id in added:
251
 
            # Actually this is a rename, we found a new file_id earlier
252
 
            # at a different location, so it is no-longer added
253
 
            x_new_path, x_new_entry = added.pop(entry.file_id)
254
 
            check_matching(path, entry, x_new_path, x_new_entry)
255
 
        else:
256
 
            # We have an old_file_id which doesn't line up with a new_file_id
257
 
            # So this file looks to be removed
258
 
            assert entry.file_id not in removed
259
 
            removed[entry.file_id] = path, entry
260
 
 
261
 
    def handle_new(path, entry):
262
 
        """new entry without an old entry match
263
 
        
264
 
        Check to see if a matching old entry was already seen as a
265
 
        removal, and change the pair into a rename.
266
 
        Otherwise just mark the new entry as an added file.
267
 
        """
268
 
        if entry.file_id in removed:
269
 
            # We saw this file_id earlier at an old different location
270
 
            # it is no longer removed, just renamed
271
 
            x_old_path, x_old_entry = removed.pop(entry.file_id)
272
 
            check_matching(x_old_path, x_old_entry, path, entry)
273
 
        else:
274
 
            # We have a new file which does not match an old file
275
 
            # mark it as added
276
 
            assert entry.file_id not in added
277
 
            added[entry.file_id] = path, entry
278
 
 
279
 
    while old_path or new_path:
280
 
        # list_files() returns files in alphabetical path sorted order
281
 
        if old_path == new_path:
282
 
            if old_file_id == new_file_id:
283
 
                # This is the common case, the files are in the same place
284
 
                # check if there were any content changes
285
 
 
286
 
                if old_file_id is None:
287
 
                    # We have 2 unversioned files, no deltas possible???
288
 
                    pass
289
 
                else:
290
 
                    check_matching(old_path, old_entry, new_path, new_entry)
291
 
            else:
292
 
                # The ids don't match, so we have to handle them both
293
 
                # separately.
294
 
                if old_file_id is not None:
295
 
                    handle_old(old_path, old_entry)
296
 
 
297
 
                if new_file_id is not None:
298
 
                    handle_new(new_path, new_entry)
299
 
 
300
 
            # The two entries were at the same path, so increment both sides
301
 
            old_path, old_class, old_kind, old_file_id, old_entry = get_next(old_files)
302
 
            new_path, new_class, new_kind, new_file_id, new_entry = get_next(new_files)
303
 
        elif new_path is None or (old_path is not None and old_path < new_path):
304
 
            # Assume we don't match, only process old_path
305
 
            if old_file_id is not None:
306
 
                handle_old(old_path, old_entry)
307
 
            # old_path came first, so increment it, trying to match up
308
 
            old_path, old_class, old_kind, old_file_id, old_entry = get_next(old_files)
309
 
        elif new_path is not None:
310
 
            # new_path came first, so increment it, trying to match up
311
 
            if new_file_id is not None:
312
 
                handle_new(new_path, new_entry)
313
 
            new_path, new_class, new_kind, new_file_id, new_entry = get_next(new_files)
314
 
 
315
 
    # Now we have a set of added and removed files, mark them all
316
 
    for old_path, old_entry in removed.itervalues():
317
 
        if specific_files:
318
 
            if not is_inside_any(specific_files, old_path):
319
 
                continue
320
 
        delta.removed.append((old_path, old_entry.file_id, old_entry.kind))
321
 
    for new_path, new_entry in added.itervalues():
 
184
    # TODO: match for specific files can be rather smarter by finding
 
185
    # the IDs of those files up front and then considering only that.
 
186
 
 
187
    for file_id in old_tree:
 
188
        if file_id in new_tree:
 
189
            old_ie = old_inv[file_id]
 
190
            new_ie = new_inv[file_id]
 
191
 
 
192
            kind = old_ie.kind
 
193
            assert kind == new_ie.kind
 
194
            
 
195
            assert kind in InventoryEntry.known_kinds, \
 
196
                   'invalid file kind %r' % kind
 
197
 
 
198
            if kind == 'root_directory':
 
199
                continue
 
200
            
 
201
            if specific_files:
 
202
                if (not is_inside_any(specific_files, old_inv.id2path(file_id)) 
 
203
                    and not is_inside_any(specific_files, new_inv.id2path(file_id))):
 
204
                    continue
 
205
 
 
206
            # temporary hack until all entries are populated before clients 
 
207
            # get them
 
208
            old_path = old_inv.id2path(file_id)
 
209
            new_path = new_inv.id2path(file_id)
 
210
            old_ie._read_tree_state(old_path, old_tree)
 
211
            new_ie._read_tree_state(new_path, new_tree)
 
212
            text_modified, meta_modified = new_ie.detect_changes(old_ie)
 
213
 
 
214
            # TODO: Can possibly avoid calculating path strings if the
 
215
            # two files are unchanged and their names and parents are
 
216
            # the same and the parents are unchanged all the way up.
 
217
            # May not be worthwhile.
 
218
            
 
219
            if (old_ie.name != new_ie.name
 
220
                or old_ie.parent_id != new_ie.parent_id):
 
221
                delta.renamed.append((old_path,
 
222
                                      new_path,
 
223
                                      file_id, kind,
 
224
                                      text_modified, meta_modified))
 
225
            elif text_modified or meta_modified:
 
226
                delta.modified.append((new_path, file_id, kind,
 
227
                                       text_modified, meta_modified))
 
228
            elif want_unchanged:
 
229
                delta.unchanged.append((new_path, file_id, kind))
 
230
        else:
 
231
            kind = old_inv.get_file_kind(file_id)
 
232
            if kind == 'root_directory':
 
233
                continue
 
234
            old_path = old_inv.id2path(file_id)
 
235
            if specific_files:
 
236
                if not is_inside_any(specific_files, old_path):
 
237
                    continue
 
238
            delta.removed.append((old_path, file_id, kind))
 
239
 
 
240
    mutter('start looking for new files')
 
241
    for file_id in new_inv:
 
242
        if file_id in old_inv or file_id not in new_tree:
 
243
            continue
 
244
        kind = new_inv.get_file_kind(file_id)
 
245
        if kind == 'root_directory':
 
246
            continue
 
247
        new_path = new_inv.id2path(file_id)
322
248
        if specific_files:
323
249
            if not is_inside_any(specific_files, new_path):
324
250
                continue
325
 
        delta.added.append((new_path, new_entry.file_id, new_entry.kind))
326
 
 
 
251
        delta.added.append((new_path, file_id, kind))
 
252
            
327
253
    delta.removed.sort()
328
254
    delta.added.sort()
329
255
    delta.renamed.sort()
330
 
    # TODO: jam 20060529 These lists shouldn't need to be sorted
331
 
    #       since we added them in alphabetical order.
332
256
    delta.modified.sort()
333
257
    delta.unchanged.sort()
334
258