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
 
        def diffit(oldlines, newlines, **kw):
 
184
 
            # FIXME: difflib is wrong if there is no trailing newline.
 
185
 
            # The syntax used by patch seems to be "\ No newline at
 
186
 
            # end of file" following the last diff line from that
 
187
 
            # file.  This is not trivial to insert into the
 
188
 
            # unified_diff output and it might be better to just fix
 
189
 
            # or replace that function.
 
191
 
            # In the meantime we at least make sure the patch isn't
 
195
 
            # Special workaround for Python2.3, where difflib fails if
 
196
 
            # both sequences are empty.
 
197
 
            if not oldlines and not newlines:
 
202
 
            if oldlines and (oldlines[-1][-1] != '\n'):
 
205
 
            if newlines and (newlines[-1][-1] != '\n'):
 
209
 
            ud = difflib.unified_diff(oldlines, newlines, **kw)
 
211
 
            # work-around for difflib being too smart for its own good
 
212
 
            # if /dev/null is "1,0", patch won't recognize it as /dev/null
 
215
 
                ud[2] = ud[2].replace('-1,0', '-0,0')
 
218
 
                ud[2] = ud[2].replace('+1,0', '+0,0')
 
220
 
            sys.stdout.writelines(ud)
 
222
 
                print "\\ No newline at end of file"
 
223
 
            sys.stdout.write('\n')
 
225
 
        if file_state in ['.', '?', 'I']:
 
227
 
        elif file_state == 'A':
 
228
 
            print '*** added %s %r' % (kind, new_name)
 
231
 
                       new_tree.get_file(fid).readlines(),
 
233
 
                       tofile=new_label + new_name + idlabel)
 
234
 
        elif file_state == 'D':
 
235
 
            assert isinstance(old_name, types.StringTypes)
 
236
 
            print '*** deleted %s %r' % (kind, old_name)
 
238
 
                diffit(old_tree.get_file(fid).readlines(), [],
 
239
 
                       fromfile=old_label + old_name + idlabel,
 
241
 
        elif file_state in ['M', 'R']:
 
242
 
            if file_state == 'M':
 
243
 
                assert kind == 'file'
 
244
 
                assert old_name == new_name
 
245
 
                print '*** modified %s %r' % (kind, new_name)
 
246
 
            elif file_state == 'R':
 
247
 
                print '*** renamed %s %r => %r' % (kind, old_name, new_name)
 
250
 
                diffit(old_tree.get_file(fid).readlines(),
 
251
 
                       new_tree.get_file(fid).readlines(),
 
252
 
                       fromfile=old_label + old_name + idlabel,
 
253
 
                       tofile=new_label + new_name)
 
255
 
            raise BzrError("can't represent state %s {%s}" % (file_state, fid))
 
260
 
    """Describes changes from one tree to another.
 
269
 
        (oldpath, newpath, id)
 
273
 
    A path may occur in more than one list if it was e.g. deleted
 
274
 
    under an old id and renamed into place in a new id.
 
276
 
    Files are listed in either modified or renamed, not both.  In
 
277
 
    other words, renamed files may also be modified.
 
286
 
def compare_inventories(old_inv, new_inv):
 
287
 
    """Return a TreeDelta object describing changes between inventories.
 
289
 
    This only describes changes in the shape of the tree, not the
 
292
 
    This is an alternative to diff_trees() and should probably
 
293
 
    eventually replace it.
 
295
 
    old_ids = old_inv.id_set()
 
296
 
    new_ids = new_inv.id_set()
 
299
 
    delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)]
 
302
 
    delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)]
 
305
 
    for fid in old_ids & new_ids:
 
306
 
        old_ie = old_inv[fid]
 
307
 
        new_ie = new_inv[fid]
 
308
 
        old_path = old_inv.id2path(fid)
 
309
 
        new_path = new_inv.id2path(fid)
 
311
 
        if old_path != new_path:
 
312
 
            delta.renamed.append((old_path, new_path, fid))
 
313
 
        elif old_ie.text_sha1 != new_ie.text_sha1:
 
314
 
            delta.modified.append((new_path, fid))
 
316
 
    delta.modified.sort()