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
    Yields a sequence of (state, id, old_name, new_name, kind).
 
 
31
    Each filename and each id is listed only once.
 
 
34
    ## TODO: Compare files before diffing; only mention those that have changed
 
 
36
    ## TODO: Set nice names in the headers, maybe include diffstat
 
 
38
    ## TODO: Perhaps make this a generator rather than using
 
 
41
    ## TODO: Allow specifying a list of files to compare, rather than
 
 
42
    ## doing the whole tree?  (Not urgent.)
 
 
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.
 
 
49
    ## XXX: This doesn't report on unknown files; that can be done
 
 
50
    ## from a separate method.
 
 
52
    old_it = old_tree.list_files()
 
 
53
    new_it = new_tree.list_files()
 
 
61
    old_item = next(old_it)
 
 
62
    new_item = next(new_it)
 
 
64
    # We step through the two sorted iterators in parallel, trying to
 
 
67
    while (old_item != None) or (new_item != None):
 
 
68
        # OK, we still have some remaining on both, but they may be
 
 
71
            old_name, old_class, old_kind, old_id = old_item
 
 
76
            new_name, new_class, new_kind, new_id = new_item
 
 
80
        mutter("   diff pairwise %r" % (old_item,))
 
 
81
        mutter("                 %r" % (new_item,))
 
 
84
            # can't handle the old tree being a WorkingTree
 
 
85
            assert old_class == 'V'
 
 
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
 
 
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
 
 
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
 
 
115
                yield 'A', new_id, None, new_name, new_kind
 
 
117
            new_item = next(new_it)
 
 
118
            old_item = next(old_it)
 
 
120
            assert old_id == new_id
 
 
121
            assert old_id != None
 
 
122
            assert old_name == new_name
 
 
123
            assert old_kind == new_kind
 
 
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
 
 
135
                mutter("      quick compare shows different")
 
 
136
                yield 'M', new_id, old_name, new_name, new_kind
 
 
138
            new_item = next(new_it)
 
 
139
            old_item = next(old_it)
 
 
143
def show_diff(b, revision, file_list):
 
 
144
    import difflib, sys, types
 
 
147
        old_tree = b.basis_tree()
 
 
149
        old_tree = b.revision_tree(b.lookup_revision(revision))
 
 
151
    new_tree = b.working_tree()
 
 
153
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
 
 
157
    DEVNULL = '/dev/null'
 
 
158
    # Windows users, don't panic about this filename -- it is a
 
 
159
    # special signal to GNU patch that the file should be created or
 
 
160
    # deleted respectively.
 
 
162
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
 
163
    # be usefully made into a much faster special case.
 
 
165
    # TODO: Better to return them in sorted order I think.
 
 
168
        file_list = [b.relpath(f) for f in file_list]
 
 
170
    # FIXME: If given a file list, compare only those files rather
 
 
171
    # than comparing everything and then throwing stuff away.
 
 
173
    for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
 
 
175
        if file_list and (new_name not in file_list):
 
 
178
        # Don't show this by default; maybe do it if an option is passed
 
 
179
        # idlabel = '      {%s}' % fid
 
 
182
        # FIXME: Something about the diff format makes patch unhappy
 
 
183
        # with newly-added files.
 
 
185
        def diffit(oldlines, newlines, **kw):
 
 
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.
 
 
194
            # In the meantime we at least make sure the patch isn't
 
 
198
            # Special workaround for Python2.3, where difflib fails if
 
 
199
            # both sequences are empty.
 
 
200
            if not oldlines and not newlines:
 
 
205
            if oldlines and (oldlines[-1][-1] != '\n'):
 
 
208
            if newlines and (newlines[-1][-1] != '\n'):
 
 
212
            ud = difflib.unified_diff(oldlines, newlines, **kw)
 
 
213
            sys.stdout.writelines(ud)
 
 
215
                print "\\ No newline at end of file"
 
 
216
            sys.stdout.write('\n')
 
 
218
        if file_state in ['.', '?', 'I']:
 
 
220
        elif file_state == 'A':
 
 
221
            print '*** added %s %r' % (kind, new_name)
 
 
224
                       new_tree.get_file(fid).readlines(),
 
 
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)
 
 
231
                diffit(old_tree.get_file(fid).readlines(), [],
 
 
232
                       fromfile=old_label + old_name + idlabel,
 
 
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)
 
 
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)
 
 
248
            raise BzrError("can't represent state %s {%s}" % (file_state, fid))
 
 
253
    """Describes changes from one tree to another.
 
 
262
        (oldpath, newpath, id)
 
 
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.
 
 
269
    Files are listed in either modified or renamed, not both.  In
 
 
270
    other words, renamed files may also be modified.
 
 
279
def compare_inventories(old_inv, new_inv):
 
 
280
    """Return a TreeDelta object describing changes between inventories.
 
 
282
    This only describes changes in the shape of the tree, not the
 
 
285
    This is an alternative to diff_trees() and should probably
 
 
286
    eventually replace it.
 
 
288
    old_ids = old_inv.id_set()
 
 
289
    new_ids = new_inv.id_set()
 
 
292
    delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)]
 
 
295
    delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)]
 
 
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)
 
 
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))
 
 
309
    delta.modified.sort()