~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin Pool
  • Date: 2005-06-27 01:39:26 UTC
  • Revision ID: mbp@sourcefrog.net-20050627013926-49413d5928809350
- import effbot.org http client

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
17
 
18
 
from sets import Set
19
 
 
20
18
from trace import mutter
21
19
from errors import BzrError
22
20
 
23
21
 
24
 
def diff_trees(old_tree, new_tree):
25
 
    """Compute diff between two trees.
26
 
 
27
 
    They may be in different branches and may be working or historical
28
 
    trees.
29
 
 
30
 
    Yields a sequence of (state, id, old_name, new_name, kind).
31
 
    Each filename and each id is listed only once.
 
22
# TODO: Rather than building a changeset object, we should probably
 
23
# invoke callbacks on an object.  That object can either accumulate a
 
24
# list, write them out directly, etc etc.
 
25
 
 
26
def internal_diff(old_label, oldlines, new_label, newlines, to_file):
 
27
    import difflib
 
28
    
 
29
    # FIXME: difflib is wrong if there is no trailing newline.
 
30
    # The syntax used by patch seems to be "\ No newline at
 
31
    # end of file" following the last diff line from that
 
32
    # file.  This is not trivial to insert into the
 
33
    # unified_diff output and it might be better to just fix
 
34
    # or replace that function.
 
35
 
 
36
    # In the meantime we at least make sure the patch isn't
 
37
    # mangled.
 
38
 
 
39
 
 
40
    # Special workaround for Python2.3, where difflib fails if
 
41
    # both sequences are empty.
 
42
    if not oldlines and not newlines:
 
43
        return
 
44
 
 
45
    nonl = False
 
46
 
 
47
    if oldlines and (oldlines[-1][-1] != '\n'):
 
48
        oldlines[-1] += '\n'
 
49
        nonl = True
 
50
    if newlines and (newlines[-1][-1] != '\n'):
 
51
        newlines[-1] += '\n'
 
52
        nonl = True
 
53
 
 
54
    ud = difflib.unified_diff(oldlines, newlines,
 
55
                              fromfile=old_label, tofile=new_label)
 
56
 
 
57
    # work-around for difflib being too smart for its own good
 
58
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
 
59
    if not oldlines:
 
60
        ud = list(ud)
 
61
        ud[2] = ud[2].replace('-1,0', '-0,0')
 
62
    elif not newlines:
 
63
        ud = list(ud)
 
64
        ud[2] = ud[2].replace('+1,0', '+0,0')
 
65
 
 
66
    to_file.writelines(ud)
 
67
    if nonl:
 
68
        print >>to_file, "\\ No newline at end of file"
 
69
    print >>to_file
 
70
 
 
71
 
 
72
 
 
73
 
 
74
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
75
                  diff_opts):
 
76
    """Display a diff by calling out to the external diff program."""
 
77
    import sys
 
78
    
 
79
    if to_file != sys.stdout:
 
80
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
 
81
                                  to_file)
 
82
 
 
83
    # make sure our own output is properly ordered before the diff
 
84
    to_file.flush()
 
85
 
 
86
    from tempfile import NamedTemporaryFile
 
87
    import os
 
88
 
 
89
    oldtmpf = NamedTemporaryFile()
 
90
    newtmpf = NamedTemporaryFile()
 
91
 
 
92
    try:
 
93
        # TODO: perhaps a special case for comparing to or from the empty
 
94
        # sequence; can just use /dev/null on Unix
 
95
 
 
96
        # TODO: if either of the files being compared already exists as a
 
97
        # regular named file (e.g. in the working directory) then we can
 
98
        # compare directly to that, rather than copying it.
 
99
 
 
100
        oldtmpf.writelines(oldlines)
 
101
        newtmpf.writelines(newlines)
 
102
 
 
103
        oldtmpf.flush()
 
104
        newtmpf.flush()
 
105
 
 
106
        if not diff_opts:
 
107
            diff_opts = []
 
108
        diffcmd = ['diff',
 
109
                   '--label', old_label,
 
110
                   oldtmpf.name,
 
111
                   '--label', new_label,
 
112
                   newtmpf.name]
 
113
 
 
114
        # diff only allows one style to be specified; they don't override.
 
115
        # note that some of these take optargs, and the optargs can be
 
116
        # directly appended to the options.
 
117
        # this is only an approximate parser; it doesn't properly understand
 
118
        # the grammar.
 
119
        for s in ['-c', '-u', '-C', '-U',
 
120
                  '-e', '--ed',
 
121
                  '-q', '--brief',
 
122
                  '--normal',
 
123
                  '-n', '--rcs',
 
124
                  '-y', '--side-by-side',
 
125
                  '-D', '--ifdef']:
 
126
            for j in diff_opts:
 
127
                if j.startswith(s):
 
128
                    break
 
129
            else:
 
130
                continue
 
131
            break
 
132
        else:
 
133
            diffcmd.append('-u')
 
134
                  
 
135
        if diff_opts:
 
136
            diffcmd.extend(diff_opts)
 
137
 
 
138
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
 
139
        
 
140
        if rc != 0 and rc != 1:
 
141
            # returns 1 if files differ; that's OK
 
142
            if rc < 0:
 
143
                msg = 'signal %d' % (-rc)
 
144
            else:
 
145
                msg = 'exit code %d' % rc
 
146
                
 
147
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
 
148
    finally:
 
149
        oldtmpf.close()                 # and delete
 
150
        newtmpf.close()
 
151
    
 
152
 
 
153
 
 
154
def show_diff(b, revision, specific_files, external_diff_options=None):
 
155
    """Shortcut for showing the diff to the working tree.
 
156
 
 
157
    b
 
158
        Branch.
 
159
 
 
160
    revision
 
161
        None for each, or otherwise the old revision to compare against.
 
162
    
 
163
    The more general form is show_diff_trees(), where the caller
 
164
    supplies any two trees.
32
165
    """
33
 
 
34
 
    ## TODO: Compare files before diffing; only mention those that have changed
35
 
 
36
 
    ## TODO: Set nice names in the headers, maybe include diffstat
37
 
 
38
 
    ## TODO: Perhaps make this a generator rather than using
39
 
    ## a callback object?
40
 
 
41
 
    ## TODO: Allow specifying a list of files to compare, rather than
42
 
    ## doing the whole tree?  (Not urgent.)
43
 
 
44
 
    ## TODO: Allow diffing any two inventories, not just the
45
 
    ## current one against one.  We mgiht need to specify two
46
 
    ## stores to look for the files if diffing two branches.  That
47
 
    ## might imply this shouldn't be primarily a Branch method.
48
 
 
49
 
    ## XXX: This doesn't report on unknown files; that can be done
50
 
    ## from a separate method.
51
 
 
52
 
    old_it = old_tree.list_files()
53
 
    new_it = new_tree.list_files()
54
 
 
55
 
    def next(it):
56
 
        try:
57
 
            return it.next()
58
 
        except StopIteration:
59
 
            return None
60
 
 
61
 
    old_item = next(old_it)
62
 
    new_item = next(new_it)
63
 
 
64
 
    # We step through the two sorted iterators in parallel, trying to
65
 
    # keep them lined up.
66
 
 
67
 
    while (old_item != None) or (new_item != None):
68
 
        # OK, we still have some remaining on both, but they may be
69
 
        # out of step.        
70
 
        if old_item != None:
71
 
            old_name, old_class, old_kind, old_id = old_item
72
 
        else:
73
 
            old_name = None
74
 
            
75
 
        if new_item != None:
76
 
            new_name, new_class, new_kind, new_id = new_item
77
 
        else:
78
 
            new_name = None
79
 
 
80
 
        mutter("   diff pairwise %r" % (old_item,))
81
 
        mutter("                 %r" % (new_item,))
82
 
 
83
 
        if old_item:
84
 
            # can't handle the old tree being a WorkingTree
85
 
            assert old_class == 'V'
86
 
 
87
 
        if new_item and (new_class != 'V'):
88
 
            yield new_class, None, None, new_name, new_kind
89
 
            new_item = next(new_it)
90
 
        elif (not new_item) or (old_item and (old_name < new_name)):
91
 
            mutter("     extra entry in old-tree sequence")
92
 
            if new_tree.has_id(old_id):
93
 
                # will be mentioned as renamed under new name
94
 
                pass
95
 
            else:
96
 
                yield 'D', old_id, old_name, None, old_kind
97
 
            old_item = next(old_it)
98
 
        elif (not old_item) or (new_item and (new_name < old_name)):
99
 
            mutter("     extra entry in new-tree sequence")
100
 
            if old_tree.has_id(new_id):
101
 
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
102
 
            else:
103
 
                yield 'A', new_id, None, new_name, new_kind
104
 
            new_item = next(new_it)
105
 
        elif old_id != new_id:
106
 
            assert old_name == new_name
107
 
            # both trees have a file of this name, but it is not the
108
 
            # same file.  in other words, the old filename has been
109
 
            # overwritten by either a newly-added or a renamed file.
110
 
            # (should we return something about the overwritten file?)
111
 
            if old_tree.has_id(new_id):
112
 
                # renaming, overlying a deleted file
113
 
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
114
 
            else:
115
 
                yield 'A', new_id, None, new_name, new_kind
116
 
 
117
 
            new_item = next(new_it)
118
 
            old_item = next(old_it)
119
 
        else:
120
 
            assert old_id == new_id
121
 
            assert old_id != None
122
 
            assert old_name == new_name
123
 
            assert old_kind == new_kind
124
 
 
125
 
            if old_kind == 'directory':
126
 
                yield '.', new_id, old_name, new_name, new_kind
127
 
            elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id):
128
 
                mutter("    file size has changed, must be different")
129
 
                yield 'M', new_id, old_name, new_name, new_kind
130
 
            elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
131
 
                mutter("      SHA1 indicates they're identical")
132
 
                ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i))
133
 
                yield '.', new_id, old_name, new_name, new_kind
134
 
            else:
135
 
                mutter("      quick compare shows different")
136
 
                yield 'M', new_id, old_name, new_name, new_kind
137
 
 
138
 
            new_item = next(new_it)
139
 
            old_item = next(old_it)
140
 
 
141
 
 
142
 
 
143
 
def show_diff(b, revision, file_list):
144
 
    import difflib, sys, types
145
 
    
 
166
    import sys
 
167
 
146
168
    if revision == None:
147
169
        old_tree = b.basis_tree()
148
170
    else:
150
172
        
151
173
    new_tree = b.working_tree()
152
174
 
 
175
    show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
 
176
                    external_diff_options)
 
177
 
 
178
 
 
179
 
 
180
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
 
181
                    external_diff_options=None):
 
182
    """Show in text form the changes from one tree to another.
 
183
 
 
184
    to_files
 
185
        If set, include only changes to these files.
 
186
 
 
187
    external_diff_options
 
188
        If set, use an external GNU diff and pass these options.
 
189
    """
 
190
 
153
191
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
154
192
    old_label = ''
155
193
    new_label = ''
162
200
    # TODO: Generation of pseudo-diffs for added/deleted files could
163
201
    # be usefully made into a much faster special case.
164
202
 
165
 
    # TODO: Better to return them in sorted order I think.
166
 
 
167
 
    if file_list:
168
 
        file_list = [b.relpath(f) for f in file_list]
169
 
 
170
 
    # FIXME: If given a file list, compare only those files rather
171
 
    # than comparing everything and then throwing stuff away.
 
203
    if external_diff_options:
 
204
        assert isinstance(external_diff_options, basestring)
 
205
        opts = external_diff_options.split()
 
206
        def diff_file(olab, olines, nlab, nlines, to_file):
 
207
            external_diff(olab, olines, nlab, nlines, to_file, opts)
 
208
    else:
 
209
        diff_file = internal_diff
172
210
    
173
 
    for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
174
 
 
175
 
        if file_list and (new_name not in file_list):
176
 
            continue
177
 
        
178
 
        # Don't show this by default; maybe do it if an option is passed
179
 
        # idlabel = '      {%s}' % fid
180
 
        idlabel = ''
181
 
 
182
 
        # FIXME: Something about the diff format makes patch unhappy
183
 
        # with newly-added files.
184
 
 
185
 
        def diffit(oldlines, newlines, **kw):
186
 
            
187
 
            # FIXME: difflib is wrong if there is no trailing newline.
188
 
            # The syntax used by patch seems to be "\ No newline at
189
 
            # end of file" following the last diff line from that
190
 
            # file.  This is not trivial to insert into the
191
 
            # unified_diff output and it might be better to just fix
192
 
            # or replace that function.
193
 
 
194
 
            # In the meantime we at least make sure the patch isn't
195
 
            # mangled.
196
 
            
197
 
 
198
 
            # Special workaround for Python2.3, where difflib fails if
199
 
            # both sequences are empty.
200
 
            if not oldlines and not newlines:
201
 
                return
202
 
 
203
 
            nonl = False
204
 
 
205
 
            if oldlines and (oldlines[-1][-1] != '\n'):
206
 
                oldlines[-1] += '\n'
207
 
                nonl = True
208
 
            if newlines and (newlines[-1][-1] != '\n'):
209
 
                newlines[-1] += '\n'
210
 
                nonl = True
211
 
 
212
 
            ud = difflib.unified_diff(oldlines, newlines, **kw)
213
 
            sys.stdout.writelines(ud)
214
 
            if nonl:
215
 
                print "\\ No newline at end of file"
216
 
            sys.stdout.write('\n')
217
 
        
218
 
        if file_state in ['.', '?', 'I']:
219
 
            continue
220
 
        elif file_state == 'A':
221
 
            print '*** added %s %r' % (kind, new_name)
222
 
            if kind == 'file':
223
 
                diffit([],
224
 
                       new_tree.get_file(fid).readlines(),
225
 
                       fromfile=DEVNULL,
226
 
                       tofile=new_label + new_name + idlabel)
227
 
        elif file_state == 'D':
228
 
            assert isinstance(old_name, types.StringTypes)
229
 
            print '*** deleted %s %r' % (kind, old_name)
230
 
            if kind == 'file':
231
 
                diffit(old_tree.get_file(fid).readlines(), [],
232
 
                       fromfile=old_label + old_name + idlabel,
233
 
                       tofile=DEVNULL)
234
 
        elif file_state in ['M', 'R']:
235
 
            if file_state == 'M':
236
 
                assert kind == 'file'
237
 
                assert old_name == new_name
238
 
                print '*** modified %s %r' % (kind, new_name)
239
 
            elif file_state == 'R':
240
 
                print '*** renamed %s %r => %r' % (kind, old_name, new_name)
241
 
 
242
 
            if kind == 'file':
243
 
                diffit(old_tree.get_file(fid).readlines(),
244
 
                       new_tree.get_file(fid).readlines(),
245
 
                       fromfile=old_label + old_name + idlabel,
246
 
                       tofile=new_label + new_name)
247
 
        else:
248
 
            raise BzrError("can't represent state %s {%s}" % (file_state, fid))
249
 
 
250
 
 
251
 
 
252
 
class TreeDelta:
 
211
 
 
212
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
 
213
                          specific_files=specific_files)
 
214
 
 
215
    for path, file_id, kind in delta.removed:
 
216
        print >>to_file, '*** removed %s %r' % (kind, path)
 
217
        if kind == 'file':
 
218
            diff_file(old_label + path,
 
219
                      old_tree.get_file(file_id).readlines(),
 
220
                      DEVNULL, 
 
221
                      [],
 
222
                      to_file)
 
223
 
 
224
    for path, file_id, kind in delta.added:
 
225
        print >>to_file, '*** added %s %r' % (kind, path)
 
226
        if kind == 'file':
 
227
            diff_file(DEVNULL,
 
228
                      [],
 
229
                      new_label + path,
 
230
                      new_tree.get_file(file_id).readlines(),
 
231
                      to_file)
 
232
 
 
233
    for old_path, new_path, file_id, kind, text_modified in delta.renamed:
 
234
        print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
 
235
        if text_modified:
 
236
            diff_file(old_label + old_path,
 
237
                      old_tree.get_file(file_id).readlines(),
 
238
                      new_label + new_path,
 
239
                      new_tree.get_file(file_id).readlines(),
 
240
                      to_file)
 
241
 
 
242
    for path, file_id, kind in delta.modified:
 
243
        print >>to_file, '*** modified %s %r' % (kind, path)
 
244
        if kind == 'file':
 
245
            diff_file(old_label + path,
 
246
                      old_tree.get_file(file_id).readlines(),
 
247
                      new_label + path,
 
248
                      new_tree.get_file(file_id).readlines(),
 
249
                      to_file)
 
250
 
 
251
 
 
252
 
 
253
class TreeDelta(object):
253
254
    """Describes changes from one tree to another.
254
255
 
255
256
    Contains four lists:
256
257
 
257
258
    added
258
 
        (path, id)
 
259
        (path, id, kind)
259
260
    removed
260
 
        (path, id)
 
261
        (path, id, kind)
261
262
    renamed
262
 
        (oldpath, newpath, id)
 
263
        (oldpath, newpath, id, kind, text_modified)
263
264
    modified
264
 
        (path, id)
265
 
 
266
 
    A path may occur in more than one list if it was e.g. deleted
267
 
    under an old id and renamed into place in a new id.
268
 
 
269
 
    Files are listed in either modified or renamed, not both.  In
270
 
    other words, renamed files may also be modified.
 
265
        (path, id, kind)
 
266
    unchanged
 
267
        (path, id, kind)
 
268
 
 
269
    Each id is listed only once.
 
270
 
 
271
    Files that are both modified and renamed are listed only in
 
272
    renamed, with the text_modified flag true.
 
273
 
 
274
    Files are only considered renamed if their name has changed or
 
275
    their parent directory has changed.  Renaming a directory
 
276
    does not count as renaming all its contents.
 
277
 
 
278
    The lists are normally sorted when the delta is created.
271
279
    """
272
280
    def __init__(self):
273
281
        self.added = []
274
282
        self.removed = []
275
283
        self.renamed = []
276
284
        self.modified = []
277
 
 
278
 
 
279
 
def compare_inventories(old_inv, new_inv):
280
 
    """Return a TreeDelta object describing changes between inventories.
281
 
 
282
 
    This only describes changes in the shape of the tree, not the
283
 
    actual texts.
284
 
 
285
 
    This is an alternative to diff_trees() and should probably
286
 
    eventually replace it.
 
285
        self.unchanged = []
 
286
 
 
287
    def __eq__(self, other):
 
288
        if not isinstance(other, TreeDelta):
 
289
            return False
 
290
        return self.added == other.added \
 
291
               and self.removed == other.removed \
 
292
               and self.renamed == other.renamed \
 
293
               and self.modified == other.modified \
 
294
               and self.unchanged == other.unchanged
 
295
 
 
296
    def __ne__(self, other):
 
297
        return not (self == other)
 
298
 
 
299
    def __repr__(self):
 
300
        return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
 
301
            " unchanged=%r)" % (self.added, self.removed, self.renamed,
 
302
            self.modified, self.unchanged)
 
303
 
 
304
    def has_changed(self):
 
305
        changes = len(self.added) + len(self.removed) + len(self.renamed)
 
306
        changes += len(self.modified) 
 
307
        return (changes != 0)
 
308
 
 
309
    def touches_file_id(self, file_id):
 
310
        """Return True if file_id is modified by this delta."""
 
311
        for l in self.added, self.removed, self.modified:
 
312
            for v in l:
 
313
                if v[1] == file_id:
 
314
                    return True
 
315
        for v in self.renamed:
 
316
            if v[2] == file_id:
 
317
                return True
 
318
        return False
 
319
            
 
320
 
 
321
    def show(self, to_file, show_ids=False, show_unchanged=False):
 
322
        def show_list(files):
 
323
            for path, fid, kind in files:
 
324
                if kind == 'directory':
 
325
                    path += '/'
 
326
                elif kind == 'symlink':
 
327
                    path += '@'
 
328
                    
 
329
                if show_ids:
 
330
                    print >>to_file, '  %-30s %s' % (path, fid)
 
331
                else:
 
332
                    print >>to_file, ' ', path
 
333
            
 
334
        if self.removed:
 
335
            print >>to_file, 'removed:'
 
336
            show_list(self.removed)
 
337
                
 
338
        if self.added:
 
339
            print >>to_file, 'added:'
 
340
            show_list(self.added)
 
341
 
 
342
        if self.renamed:
 
343
            print >>to_file, 'renamed:'
 
344
            for oldpath, newpath, fid, kind, text_modified in self.renamed:
 
345
                if show_ids:
 
346
                    print >>to_file, '  %s => %s %s' % (oldpath, newpath, fid)
 
347
                else:
 
348
                    print >>to_file, '  %s => %s' % (oldpath, newpath)
 
349
                    
 
350
        if self.modified:
 
351
            print >>to_file, 'modified:'
 
352
            show_list(self.modified)
 
353
            
 
354
        if show_unchanged and self.unchanged:
 
355
            print >>to_file, 'unchanged:'
 
356
            show_list(self.unchanged)
 
357
 
 
358
 
 
359
 
 
360
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
 
361
    """Describe changes from one tree to another.
 
362
 
 
363
    Returns a TreeDelta with details of added, modified, renamed, and
 
364
    deleted entries.
 
365
 
 
366
    The root entry is specifically exempt.
 
367
 
 
368
    This only considers versioned files.
 
369
 
 
370
    want_unchanged
 
371
        If true, also list files unchanged from one version to
 
372
        the next.
 
373
 
 
374
    specific_files
 
375
        If true, only check for changes to specified names or
 
376
        files within them.
287
377
    """
288
 
    old_ids = old_inv.id_set()
289
 
    new_ids = new_inv.id_set()
 
378
 
 
379
    from osutils import is_inside_any
 
380
    
 
381
    old_inv = old_tree.inventory
 
382
    new_inv = new_tree.inventory
290
383
    delta = TreeDelta()
291
 
 
292
 
    delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)]
 
384
    mutter('start compare_trees')
 
385
 
 
386
    # TODO: match for specific files can be rather smarter by finding
 
387
    # the IDs of those files up front and then considering only that.
 
388
 
 
389
    for file_id in old_tree:
 
390
        if file_id in new_tree:
 
391
            kind = old_inv.get_file_kind(file_id)
 
392
            assert kind == new_inv.get_file_kind(file_id)
 
393
            
 
394
            assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
 
395
                   'invalid file kind %r' % kind
 
396
 
 
397
            if kind == 'root_directory':
 
398
                continue
 
399
            
 
400
            old_path = old_inv.id2path(file_id)
 
401
            new_path = new_inv.id2path(file_id)
 
402
 
 
403
            old_ie = old_inv[file_id]
 
404
            new_ie = new_inv[file_id]
 
405
 
 
406
            if specific_files:
 
407
                if (not is_inside_any(specific_files, old_path) 
 
408
                    and not is_inside_any(specific_files, new_path)):
 
409
                    continue
 
410
 
 
411
            if kind == 'file':
 
412
                old_sha1 = old_tree.get_file_sha1(file_id)
 
413
                new_sha1 = new_tree.get_file_sha1(file_id)
 
414
                text_modified = (old_sha1 != new_sha1)
 
415
            else:
 
416
                ## mutter("no text to check for %r %r" % (file_id, kind))
 
417
                text_modified = False
 
418
 
 
419
            # TODO: Can possibly avoid calculating path strings if the
 
420
            # two files are unchanged and their names and parents are
 
421
            # the same and the parents are unchanged all the way up.
 
422
            # May not be worthwhile.
 
423
            
 
424
            if (old_ie.name != new_ie.name
 
425
                or old_ie.parent_id != new_ie.parent_id):
 
426
                delta.renamed.append((old_path, new_path, file_id, kind,
 
427
                                      text_modified))
 
428
            elif text_modified:
 
429
                delta.modified.append((new_path, file_id, kind))
 
430
            elif want_unchanged:
 
431
                delta.unchanged.append((new_path, file_id, kind))
 
432
        else:
 
433
            kind = old_inv.get_file_kind(file_id)
 
434
            old_path = old_inv.id2path(file_id)
 
435
            if specific_files:
 
436
                if not is_inside_any(specific_files, old_path):
 
437
                    continue
 
438
            delta.removed.append((old_path, file_id, kind))
 
439
 
 
440
    mutter('start looking for new files')
 
441
    for file_id in new_inv:
 
442
        if file_id in old_inv:
 
443
            continue
 
444
        new_path = new_inv.id2path(file_id)
 
445
        if specific_files:
 
446
            if not is_inside_any(specific_files, new_path):
 
447
                continue
 
448
        kind = new_inv.get_file_kind(file_id)
 
449
        delta.added.append((new_path, file_id, kind))
 
450
            
293
451
    delta.removed.sort()
294
 
 
295
 
    delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)]
296
452
    delta.added.sort()
297
 
 
298
 
    for fid in old_ids & new_ids:
299
 
        old_ie = old_inv[fid]
300
 
        new_ie = new_inv[fid]
301
 
        old_path = old_inv.id2path(fid)
302
 
        new_path = new_inv.id2path(fid)
303
 
 
304
 
        if old_path != new_path:
305
 
            delta.renamed.append((old_path, new_path, fid))
306
 
        elif old_ie.text_sha1 != new_ie.text_sha1:
307
 
            delta.modified.append((new_path, fid))
308
 
 
 
453
    delta.renamed.sort()
309
454
    delta.modified.sort()
310
 
    delta.renamed.sort()    
 
455
    delta.unchanged.sort()
311
456
 
312
457
    return delta