2
# -*- coding: UTF-8 -*-
 
 
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.
 
 
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.
 
 
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
 
 
20
from trace import mutter
 
 
21
from errors import BzrError
 
 
24
def diff_trees(old_tree, new_tree):
 
 
25
    """Compute diff between two trees.
 
 
27
    They may be in different branches and may be working or historical
 
 
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.
 
 
34
    Yields a sequence of (state, id, old_name, new_name, kind).
 
 
35
    Each filename and each id is listed only once.
 
 
37
    ## TODO: Allow specifying a list of files to compare, rather than
 
 
38
    ## doing the whole tree?  (Not urgent.)
 
 
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.
 
 
45
    sha_match_cnt = modified_cnt = 0
 
 
47
    old_it = old_tree.list_files()
 
 
48
    new_it = new_tree.list_files()
 
 
56
    old_item = next(old_it)
 
 
57
    new_item = next(new_it)
 
 
59
    # We step through the two sorted iterators in parallel, trying to
 
 
62
    while (old_item != None) or (new_item != None):
 
 
63
        # OK, we still have some remaining on both, but they may be
 
 
66
            old_name, old_class, old_kind, old_id = old_item
 
 
71
            new_name, new_class, new_kind, new_id = new_item
 
 
76
            # can't handle the old tree being a WorkingTree
 
 
77
            assert old_class == 'V'
 
 
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
 
 
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
 
 
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
 
 
105
                yield 'A', new_id, None, new_name, new_kind
 
 
107
            new_item = next(new_it)
 
 
108
            old_item = next(old_it)
 
 
110
            assert old_id == new_id
 
 
111
            assert old_id != None
 
 
112
            assert old_name == new_name
 
 
113
            assert old_kind == new_kind
 
 
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):
 
 
119
                yield '.', new_id, old_name, new_name, new_kind
 
 
122
                yield 'M', new_id, old_name, new_name, new_kind
 
 
124
            new_item = next(new_it)
 
 
125
            old_item = next(old_it)
 
 
128
    mutter("diff finished: %d SHA matches, %d modified"
 
 
129
           % (sha_match_cnt, modified_cnt))
 
 
133
def show_diff(b, revision, file_list):
 
 
134
    import difflib, sys, types
 
 
137
        old_tree = b.basis_tree()
 
 
139
        old_tree = b.revision_tree(b.lookup_revision(revision))
 
 
141
    new_tree = b.working_tree()
 
 
143
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
 
 
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.
 
 
152
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
 
153
    # be usefully made into a much faster special case.
 
 
155
    # TODO: Better to return them in sorted order I think.
 
 
158
        file_list = [b.relpath(f) for f in file_list]
 
 
160
    # FIXME: If given a file list, compare only those files rather
 
 
161
    # than comparing everything and then throwing stuff away.
 
 
163
    for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
 
 
165
        if file_list and (new_name not in file_list):
 
 
168
        # Don't show this by default; maybe do it if an option is passed
 
 
169
        # idlabel = '      {%s}' % fid
 
 
172
        def diffit(oldlines, newlines, **kw):
 
 
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.
 
 
181
            # In the meantime we at least make sure the patch isn't
 
 
185
            # Special workaround for Python2.3, where difflib fails if
 
 
186
            # both sequences are empty.
 
 
187
            if not oldlines and not newlines:
 
 
192
            if oldlines and (oldlines[-1][-1] != '\n'):
 
 
195
            if newlines and (newlines[-1][-1] != '\n'):
 
 
199
            ud = difflib.unified_diff(oldlines, newlines, **kw)
 
 
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
 
 
205
                ud[2] = ud[2].replace('-1,0', '-0,0')
 
 
208
                ud[2] = ud[2].replace('+1,0', '+0,0')
 
 
210
            sys.stdout.writelines(ud)
 
 
212
                print "\\ No newline at end of file"
 
 
213
            sys.stdout.write('\n')
 
 
215
        if file_state in ['.', '?', 'I']:
 
 
217
        elif file_state == 'A':
 
 
218
            print '*** added %s %r' % (kind, new_name)
 
 
221
                       new_tree.get_file(fid).readlines(),
 
 
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)
 
 
228
                diffit(old_tree.get_file(fid).readlines(), [],
 
 
229
                       fromfile=old_label + old_name + idlabel,
 
 
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)
 
 
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)
 
 
245
            raise BzrError("can't represent state %s {%s}" % (file_state, fid))
 
 
250
    """Describes changes from one tree to another.
 
 
259
        (oldpath, newpath, id, text_modified)
 
 
265
    Each id is listed only once.
 
 
267
    Files that are both modified and renamed are listed only in
 
 
268
    renamed, with the text_modified flag true.
 
 
270
    The lists are normally sorted when the delta is created.
 
 
279
    def show(self, to_file, show_ids=False, show_unchanged=False):
 
 
280
        def show_list(files):
 
 
281
            for path, fid in files:
 
 
283
                    print >>to_file, '  %-30s %s' % (path, fid)
 
 
285
                    print >>to_file, ' ', path
 
 
288
            print >>to_file, 'removed files:'
 
 
289
            show_list(self.removed)
 
 
292
            print >>to_file, 'added files:'
 
 
293
            show_list(self.added)
 
 
296
            print >>to_file, 'renamed files:'
 
 
297
            for oldpath, newpath, fid, text_modified in self.renamed:
 
 
299
                    print >>to_file, '  %s => %s %s' % (oldpath, newpath, fid)
 
 
301
                    print >>to_file, '  %s => %s' % (oldpath, newpath)
 
 
304
            print >>to_file, 'modified files:'
 
 
305
            show_list(self.modified)
 
 
307
        if show_unchanged and self.unchanged:
 
 
308
            print >>to_file, 'unchanged files:'
 
 
309
            show_list(self.unchanged)
 
 
313
def compare_trees(old_tree, new_tree, want_unchanged):
 
 
314
    old_inv = old_tree.inventory
 
 
315
    new_inv = new_tree.inventory
 
 
317
    for file_id in old_tree:
 
 
318
        if file_id in new_tree:
 
 
319
            old_path = old_inv.id2path(file_id)
 
 
320
            new_path = new_inv.id2path(file_id)
 
 
322
            kind = old_inv.get_file_kind(file_id)
 
 
323
            assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
 
 
324
                   'invalid file kind %r' % kind
 
 
326
                old_sha1 = old_tree.get_file_sha1(file_id)
 
 
327
                new_sha1 = new_tree.get_file_sha1(file_id)
 
 
328
                text_modified = (old_sha1 != new_sha1)
 
 
330
                ## mutter("no text to check for %r %r" % (file_id, kind))
 
 
331
                text_modified = False
 
 
333
            # TODO: Can possibly avoid calculating path strings if the
 
 
334
            # two files are unchanged and their names and parents are
 
 
335
            # the same and the parents are unchanged all the way up.
 
 
336
            # May not be worthwhile.
 
 
338
            if old_path != new_path:
 
 
339
                delta.renamed.append((old_path, new_path, file_id, text_modified))
 
 
341
                delta.modified.append((new_path, file_id))
 
 
343
                delta.unchanged.append((new_path, file_id))
 
 
345
            delta.removed.append((old_inv.id2path(file_id), file_id))
 
 
346
    for file_id in new_inv:
 
 
347
        if file_id in old_inv:
 
 
349
        delta.added.append((new_inv.id2path(file_id), file_id))
 
 
354
    delta.modified.sort()