/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin Pool
  • Date: 2005-05-11 04:18:51 UTC
  • Revision ID: mbp@sourcefrog.net-20050511041851-a0a7480c958ef86c
- new testing command compare-trees
- change operation of TreeDelta object a bit
  to specify which renamed files also have modified text
- new TreeDelta.show() factored out
- new compare_trees similar to compare_inventories
  but handling WorkingTrees that don't have a SHA-1 
  in the inventory but can get it from cache
- new Inventory.get_file_kind

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/env python
 
2
# -*- coding: UTF-8 -*-
 
3
 
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
 
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
 
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
from sets import Set
 
19
 
 
20
from trace import mutter
 
21
from errors import BzrError
 
22
 
 
23
 
 
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
    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
 
 
34
    Yields a sequence of (state, id, old_name, new_name, kind).
 
35
    Each filename and each id is listed only once.
 
36
    """
 
37
    ## TODO: Allow specifying a list of files to compare, rather than
 
38
    ## doing the whole tree?  (Not urgent.)
 
39
 
 
40
    ## TODO: Allow diffing any two inventories, not just the
 
41
    ## current one against one.  We mgiht need to specify two
 
42
    ## stores to look for the files if diffing two branches.  That
 
43
    ## might imply this shouldn't be primarily a Branch method.
 
44
 
 
45
    sha_match_cnt = modified_cnt = 0
 
46
 
 
47
    old_it = old_tree.list_files()
 
48
    new_it = new_tree.list_files()
 
49
 
 
50
    def next(it):
 
51
        try:
 
52
            return it.next()
 
53
        except StopIteration:
 
54
            return None
 
55
 
 
56
    old_item = next(old_it)
 
57
    new_item = next(new_it)
 
58
 
 
59
    # We step through the two sorted iterators in parallel, trying to
 
60
    # keep them lined up.
 
61
 
 
62
    while (old_item != None) or (new_item != None):
 
63
        # OK, we still have some remaining on both, but they may be
 
64
        # out of step.        
 
65
        if old_item != None:
 
66
            old_name, old_class, old_kind, old_id = old_item
 
67
        else:
 
68
            old_name = None
 
69
            
 
70
        if new_item != None:
 
71
            new_name, new_class, new_kind, new_id = new_item
 
72
        else:
 
73
            new_name = None
 
74
 
 
75
        if old_item:
 
76
            # can't handle the old tree being a WorkingTree
 
77
            assert old_class == 'V'
 
78
 
 
79
        if new_item and (new_class != 'V'):
 
80
            yield new_class, None, None, new_name, new_kind
 
81
            new_item = next(new_it)
 
82
        elif (not new_item) or (old_item and (old_name < new_name)):
 
83
            if new_tree.has_id(old_id):
 
84
                # will be mentioned as renamed under new name
 
85
                pass
 
86
            else:
 
87
                yield 'D', old_id, old_name, None, old_kind
 
88
            old_item = next(old_it)
 
89
        elif (not old_item) or (new_item and (new_name < old_name)):
 
90
            if old_tree.has_id(new_id):
 
91
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
 
92
            else:
 
93
                yield 'A', new_id, None, new_name, new_kind
 
94
            new_item = next(new_it)
 
95
        elif old_id != new_id:
 
96
            assert old_name == new_name
 
97
            # both trees have a file of this name, but it is not the
 
98
            # same file.  in other words, the old filename has been
 
99
            # overwritten by either a newly-added or a renamed file.
 
100
            # (should we return something about the overwritten file?)
 
101
            if old_tree.has_id(new_id):
 
102
                # renaming, overlying a deleted file
 
103
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
 
104
            else:
 
105
                yield 'A', new_id, None, new_name, new_kind
 
106
 
 
107
            new_item = next(new_it)
 
108
            old_item = next(old_it)
 
109
        else:
 
110
            assert old_id == new_id
 
111
            assert old_id != None
 
112
            assert old_name == new_name
 
113
            assert old_kind == new_kind
 
114
 
 
115
            if old_kind == 'directory':
 
116
                yield '.', new_id, old_name, new_name, new_kind
 
117
            elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
 
118
                sha_match_cnt += 1
 
119
                yield '.', new_id, old_name, new_name, new_kind
 
120
            else:
 
121
                modified_cnt += 1
 
122
                yield 'M', new_id, old_name, new_name, new_kind
 
123
 
 
124
            new_item = next(new_it)
 
125
            old_item = next(old_it)
 
126
 
 
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_inventories(old_inv, new_inv):
 
307
    """Return a TreeDelta object describing changes between inventories.
 
308
 
 
309
    This only describes changes in the shape of the tree, not the
 
310
    actual texts.
 
311
 
 
312
    This is an alternative to diff_trees() and should probably
 
313
    eventually replace it.
 
314
    """
 
315
    old_ids = old_inv.id_set()
 
316
    new_ids = new_inv.id_set()
 
317
    delta = TreeDelta()
 
318
 
 
319
    delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)]
 
320
    delta.removed.sort()
 
321
 
 
322
    delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)]
 
323
    delta.added.sort()
 
324
 
 
325
    for fid in old_ids & new_ids:
 
326
        old_ie = old_inv[fid]
 
327
        new_ie = new_inv[fid]
 
328
        old_path = old_inv.id2path(fid)
 
329
        new_path = new_inv.id2path(fid)
 
330
 
 
331
        text_modified = (old_ie.text_sha1 != new_ie.text_sha1)
 
332
 
 
333
        if old_path != new_path:
 
334
            delta.renamed.append((old_path, new_path, fid, text_modified))
 
335
        elif text_modified:
 
336
            delta.modified.append((new_path, fid))
 
337
 
 
338
    delta.modified.sort()
 
339
    delta.renamed.sort()    
 
340
 
 
341
    return delta
 
342
 
 
343
 
 
344
 
 
345
 
 
346
def compare_trees(old_tree, new_tree):
 
347
    old_inv = old_tree.inventory
 
348
    new_inv = new_tree.inventory
 
349
    delta = TreeDelta()
 
350
    for file_id in old_inv:
 
351
        if file_id in new_inv:
 
352
            old_path = old_inv.id2path(file_id)
 
353
            new_path = new_inv.id2path(file_id)
 
354
 
 
355
            kind = old_inv.get_file_kind(file_id)
 
356
            assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
 
357
                   'invalid file kind %r' % kind
 
358
            if kind == 'file':
 
359
                old_sha1 = old_tree.get_file_sha1(file_id)
 
360
                new_sha1 = new_tree.get_file_sha1(file_id)
 
361
                text_modified = (old_sha1 != new_sha1)
 
362
            else:
 
363
                ## mutter("no text to check for %r %r" % (file_id, kind))
 
364
                text_modified = False
 
365
            
 
366
            if old_path != new_path:
 
367
                delta.renamed.append((old_path, new_path, file_id, text_modified))
 
368
            elif text_modified:
 
369
                delta.modified.append((new_path, file_id))
 
370
        else:
 
371
            delta.removed.append((old_inv.id2path(file_id), file_id))
 
372
    for file_id in new_inv:
 
373
        if file_id in old_inv:
 
374
            continue
 
375
        delta.added.append((new_inv.id2path(file_id), file_id))
 
376
            
 
377
    delta.removed.sort()
 
378
    delta.added.sort()
 
379
    delta.renamed.sort()
 
380
    delta.modified.sort()
 
381
 
 
382
    return delta