~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin Pool
  • Date: 2005-05-11 04:57:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050511045713-5175e3084bad6196
- New form 'file_id in tree' to check if the file is present
- Rewrite show_info to work on compare_trees (much faster)
- New form 'for file_id in tree' to iterate through files there.

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
from sets import Set
19
19
 
20
20
from trace import mutter
21
 
 
22
 
 
23
 
 
24
 
 
 
21
from errors import BzrError
25
22
 
26
23
 
27
24
def diff_trees(old_tree, new_tree):
30
27
    They may be in different branches and may be working or historical
31
28
    trees.
32
29
 
 
30
    This only compares the versioned files, paying no attention to
 
31
    files which are ignored or unknown.  Those can only be present in
 
32
    working trees and can be reported on separately.
 
33
 
33
34
    Yields a sequence of (state, id, old_name, new_name, kind).
34
35
    Each filename and each id is listed only once.
35
36
    """
36
 
 
37
 
    ## TODO: Compare files before diffing; only mention those that have changed
38
 
 
39
 
    ## TODO: Set nice names in the headers, maybe include diffstat
40
 
 
41
 
    ## TODO: Perhaps make this a generator rather than using
42
 
    ## a callback object?
43
 
 
44
37
    ## TODO: Allow specifying a list of files to compare, rather than
45
38
    ## doing the whole tree?  (Not urgent.)
46
39
 
49
42
    ## stores to look for the files if diffing two branches.  That
50
43
    ## might imply this shouldn't be primarily a Branch method.
51
44
 
52
 
    ## XXX: This doesn't report on unknown files; that can be done
53
 
    ## from a separate method.
 
45
    sha_match_cnt = modified_cnt = 0
54
46
 
55
47
    old_it = old_tree.list_files()
56
48
    new_it = new_tree.list_files()
80
72
        else:
81
73
            new_name = None
82
74
 
83
 
        mutter("   diff pairwise %r" % (old_item,))
84
 
        mutter("                 %r" % (new_item,))
85
 
 
86
75
        if old_item:
87
76
            # can't handle the old tree being a WorkingTree
88
77
            assert old_class == 'V'
91
80
            yield new_class, None, None, new_name, new_kind
92
81
            new_item = next(new_it)
93
82
        elif (not new_item) or (old_item and (old_name < new_name)):
94
 
            mutter("     extra entry in old-tree sequence")
95
83
            if new_tree.has_id(old_id):
96
84
                # will be mentioned as renamed under new name
97
85
                pass
99
87
                yield 'D', old_id, old_name, None, old_kind
100
88
            old_item = next(old_it)
101
89
        elif (not old_item) or (new_item and (new_name < old_name)):
102
 
            mutter("     extra entry in new-tree sequence")
103
90
            if old_tree.has_id(new_id):
104
91
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
105
92
            else:
121
108
            old_item = next(old_it)
122
109
        else:
123
110
            assert old_id == new_id
 
111
            assert old_id != None
124
112
            assert old_name == new_name
125
113
            assert old_kind == new_kind
126
114
 
127
115
            if old_kind == 'directory':
128
116
                yield '.', new_id, old_name, new_name, new_kind
129
 
            elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id):
130
 
                mutter("    file size has changed, must be different")
131
 
                yield 'M', new_id, old_name, new_name, new_kind
132
117
            elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
133
 
                mutter("      SHA1 indicates they're identical")
134
 
                ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i))
 
118
                sha_match_cnt += 1
135
119
                yield '.', new_id, old_name, new_name, new_kind
136
120
            else:
137
 
                mutter("      quick compare shows different")
 
121
                modified_cnt += 1
138
122
                yield 'M', new_id, old_name, new_name, new_kind
139
123
 
140
124
            new_item = next(new_it)
141
125
            old_item = next(old_it)
142
126
 
143
127
 
 
128
    mutter("diff finished: %d SHA matches, %d modified"
 
129
           % (sha_match_cnt, modified_cnt))
 
130
 
 
131
 
 
132
 
 
133
def show_diff(b, revision, file_list):
 
134
    import difflib, sys, types
 
135
    
 
136
    if revision == None:
 
137
        old_tree = b.basis_tree()
 
138
    else:
 
139
        old_tree = b.revision_tree(b.lookup_revision(revision))
 
140
        
 
141
    new_tree = b.working_tree()
 
142
 
 
143
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
 
144
    old_label = ''
 
145
    new_label = ''
 
146
 
 
147
    DEVNULL = '/dev/null'
 
148
    # Windows users, don't panic about this filename -- it is a
 
149
    # special signal to GNU patch that the file should be created or
 
150
    # deleted respectively.
 
151
 
 
152
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
153
    # be usefully made into a much faster special case.
 
154
 
 
155
    # TODO: Better to return them in sorted order I think.
 
156
 
 
157
    if file_list:
 
158
        file_list = [b.relpath(f) for f in file_list]
 
159
 
 
160
    # FIXME: If given a file list, compare only those files rather
 
161
    # than comparing everything and then throwing stuff away.
 
162
    
 
163
    for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
 
164
 
 
165
        if file_list and (new_name not in file_list):
 
166
            continue
 
167
        
 
168
        # Don't show this by default; maybe do it if an option is passed
 
169
        # idlabel = '      {%s}' % fid
 
170
        idlabel = ''
 
171
 
 
172
        def diffit(oldlines, newlines, **kw):
 
173
            
 
174
            # FIXME: difflib is wrong if there is no trailing newline.
 
175
            # The syntax used by patch seems to be "\ No newline at
 
176
            # end of file" following the last diff line from that
 
177
            # file.  This is not trivial to insert into the
 
178
            # unified_diff output and it might be better to just fix
 
179
            # or replace that function.
 
180
 
 
181
            # In the meantime we at least make sure the patch isn't
 
182
            # mangled.
 
183
            
 
184
 
 
185
            # Special workaround for Python2.3, where difflib fails if
 
186
            # both sequences are empty.
 
187
            if not oldlines and not newlines:
 
188
                return
 
189
 
 
190
            nonl = False
 
191
 
 
192
            if oldlines and (oldlines[-1][-1] != '\n'):
 
193
                oldlines[-1] += '\n'
 
194
                nonl = True
 
195
            if newlines and (newlines[-1][-1] != '\n'):
 
196
                newlines[-1] += '\n'
 
197
                nonl = True
 
198
 
 
199
            ud = difflib.unified_diff(oldlines, newlines, **kw)
 
200
 
 
201
            # work-around for difflib being too smart for its own good
 
202
            # if /dev/null is "1,0", patch won't recognize it as /dev/null
 
203
            if not oldlines:
 
204
                ud = list(ud)
 
205
                ud[2] = ud[2].replace('-1,0', '-0,0')
 
206
            elif not newlines:
 
207
                ud = list(ud)
 
208
                ud[2] = ud[2].replace('+1,0', '+0,0')
 
209
            
 
210
            sys.stdout.writelines(ud)
 
211
            if nonl:
 
212
                print "\\ No newline at end of file"
 
213
            sys.stdout.write('\n')
 
214
        
 
215
        if file_state in ['.', '?', 'I']:
 
216
            continue
 
217
        elif file_state == 'A':
 
218
            print '*** added %s %r' % (kind, new_name)
 
219
            if kind == 'file':
 
220
                diffit([],
 
221
                       new_tree.get_file(fid).readlines(),
 
222
                       fromfile=DEVNULL,
 
223
                       tofile=new_label + new_name + idlabel)
 
224
        elif file_state == 'D':
 
225
            assert isinstance(old_name, types.StringTypes)
 
226
            print '*** deleted %s %r' % (kind, old_name)
 
227
            if kind == 'file':
 
228
                diffit(old_tree.get_file(fid).readlines(), [],
 
229
                       fromfile=old_label + old_name + idlabel,
 
230
                       tofile=DEVNULL)
 
231
        elif file_state in ['M', 'R']:
 
232
            if file_state == 'M':
 
233
                assert kind == 'file'
 
234
                assert old_name == new_name
 
235
                print '*** modified %s %r' % (kind, new_name)
 
236
            elif file_state == 'R':
 
237
                print '*** renamed %s %r => %r' % (kind, old_name, new_name)
 
238
 
 
239
            if kind == 'file':
 
240
                diffit(old_tree.get_file(fid).readlines(),
 
241
                       new_tree.get_file(fid).readlines(),
 
242
                       fromfile=old_label + old_name + idlabel,
 
243
                       tofile=new_label + new_name)
 
244
        else:
 
245
            raise BzrError("can't represent state %s {%s}" % (file_state, fid))
 
246
 
 
247
 
 
248
 
 
249
class TreeDelta:
 
250
    """Describes changes from one tree to another.
 
251
 
 
252
    Contains four lists:
 
253
 
 
254
    added
 
255
        (path, id)
 
256
    removed
 
257
        (path, id)
 
258
    renamed
 
259
        (oldpath, newpath, id, text_modified)
 
260
    modified
 
261
        (path, id)
 
262
 
 
263
    Each id is listed only once.
 
264
 
 
265
    Files that are both modified and renamed are listed only in
 
266
    renamed, with the text_modified flag true.
 
267
    """
 
268
    def __init__(self):
 
269
        self.added = []
 
270
        self.removed = []
 
271
        self.renamed = []
 
272
        self.modified = []
 
273
 
 
274
    def show(self, to_file, show_ids):
 
275
        if self.removed:
 
276
            print >>to_file, 'removed files:'
 
277
            for path, fid in self.removed:
 
278
                if show_ids:
 
279
                    print >>to_file, '  %-30s %s' % (path, fid)
 
280
                else:
 
281
                    print >>to_file, ' ', path
 
282
        if self.added:
 
283
            print >>to_file, 'added files:'
 
284
            for path, fid in self.added:
 
285
                if show_ids:
 
286
                    print >>to_file, '  %-30s %s' % (path, fid)
 
287
                else:
 
288
                    print >>to_file, '  ' + path
 
289
        if self.renamed:
 
290
            print >>to_file, 'renamed files:'
 
291
            for oldpath, newpath, fid, text_modified in self.renamed:
 
292
                if show_ids:
 
293
                    print >>to_file, '  %s => %s %s' % (oldpath, newpath, fid)
 
294
                else:
 
295
                    print >>to_file, '  %s => %s' % (oldpath, newpath)
 
296
        if self.modified:
 
297
            print >>to_file, 'modified files:'
 
298
            for path, fid in self.modified:
 
299
                if show_ids:
 
300
                    print >>to_file, '  %-30s %s' % (path, fid)
 
301
                else:
 
302
                    print >>to_file, '  ' + path
 
303
 
 
304
 
 
305
 
 
306
def compare_trees(old_tree, new_tree):
 
307
    old_inv = old_tree.inventory
 
308
    new_inv = new_tree.inventory
 
309
    delta = TreeDelta()
 
310
    for file_id in old_tree:
 
311
        if file_id in new_tree:
 
312
            old_path = old_inv.id2path(file_id)
 
313
            new_path = new_inv.id2path(file_id)
 
314
 
 
315
            kind = old_inv.get_file_kind(file_id)
 
316
            assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
 
317
                   'invalid file kind %r' % kind
 
318
            if kind == 'file':
 
319
                old_sha1 = old_tree.get_file_sha1(file_id)
 
320
                new_sha1 = new_tree.get_file_sha1(file_id)
 
321
                text_modified = (old_sha1 != new_sha1)
 
322
            else:
 
323
                ## mutter("no text to check for %r %r" % (file_id, kind))
 
324
                text_modified = False
 
325
            
 
326
            if old_path != new_path:
 
327
                delta.renamed.append((old_path, new_path, file_id, text_modified))
 
328
            elif text_modified:
 
329
                delta.modified.append((new_path, file_id))
 
330
        else:
 
331
            delta.removed.append((old_inv.id2path(file_id), file_id))
 
332
    for file_id in new_inv:
 
333
        if file_id in old_inv:
 
334
            continue
 
335
        delta.added.append((new_inv.id2path(file_id), file_id))
 
336
            
 
337
    delta.removed.sort()
 
338
    delta.added.sort()
 
339
    delta.renamed.sort()
 
340
    delta.modified.sort()
 
341
 
 
342
    return delta