~bzr-pqm/bzr/bzr.dev

1 by mbp at sourcefrog
import from baz patch-364
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
356 by Martin Pool
- pychecker fixes in bzrlib.diff
21
from errors import BzrError
1 by mbp at sourcefrog
import from baz patch-364
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
    Yields a sequence of (state, id, old_name, new_name, kind).
31
    Each filename and each id is listed only once.
32
    """
33
34
    ## TODO: Compare files before diffing; only mention those that have changed
35
36
    ## TODO: Set nice names in the headers, maybe include diffstat
37
38
    ## TODO: Perhaps make this a generator rather than using
39
    ## a callback object?
40
41
    ## TODO: Allow specifying a list of files to compare, rather than
42
    ## doing the whole tree?  (Not urgent.)
43
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.
48
49
    ## XXX: This doesn't report on unknown files; that can be done
50
    ## from a separate method.
51
52
    old_it = old_tree.list_files()
53
    new_it = new_tree.list_files()
54
55
    def next(it):
56
        try:
57
            return it.next()
58
        except StopIteration:
59
            return None
60
61
    old_item = next(old_it)
62
    new_item = next(new_it)
63
64
    # We step through the two sorted iterators in parallel, trying to
65
    # keep them lined up.
66
67
    while (old_item != None) or (new_item != None):
68
        # OK, we still have some remaining on both, but they may be
69
        # out of step.        
70
        if old_item != None:
71
            old_name, old_class, old_kind, old_id = old_item
72
        else:
73
            old_name = None
74
            
75
        if new_item != None:
76
            new_name, new_class, new_kind, new_id = new_item
77
        else:
78
            new_name = None
79
80
        mutter("   diff pairwise %r" % (old_item,))
81
        mutter("                 %r" % (new_item,))
82
83
        if old_item:
84
            # can't handle the old tree being a WorkingTree
85
            assert old_class == 'V'
86
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
94
                pass
95
            else:
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
102
            else:
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
114
            else:
115
                yield 'A', new_id, None, new_name, new_kind
116
117
            new_item = next(new_it)
118
            old_item = next(old_it)
119
        else:
120
            assert old_id == new_id
178 by mbp at sourcefrog
- Use a non-null file_id for the branch root directory. At the moment
121
            assert old_id != None
1 by mbp at sourcefrog
import from baz patch-364
122
            assert old_name == new_name
123
            assert old_kind == new_kind
124
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
134
            else:
135
                mutter("      quick compare shows different")
136
                yield 'M', new_id, old_name, new_name, new_kind
137
138
            new_item = next(new_it)
139
            old_item = next(old_it)
140
141
329 by Martin Pool
- refactor command functions into command classes
142
143
def show_diff(b, revision, file_list):
356 by Martin Pool
- pychecker fixes in bzrlib.diff
144
    import difflib, sys, types
329 by Martin Pool
- refactor command functions into command classes
145
    
146
    if revision == None:
147
        old_tree = b.basis_tree()
148
    else:
149
        old_tree = b.revision_tree(b.lookup_revision(revision))
150
        
151
    new_tree = b.working_tree()
152
153
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
154
    old_label = ''
155
    new_label = ''
156
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.
161
162
    # TODO: Generation of pseudo-diffs for added/deleted files could
163
    # be usefully made into a much faster special case.
164
165
    # TODO: Better to return them in sorted order I think.
166
167
    if file_list:
168
        file_list = [b.relpath(f) for f in file_list]
169
170
    # FIXME: If given a file list, compare only those files rather
171
    # than comparing everything and then throwing stuff away.
172
    
173
    for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
174
175
        if file_list and (new_name not in file_list):
176
            continue
177
        
178
        # Don't show this by default; maybe do it if an option is passed
179
        # idlabel = '      {%s}' % fid
180
        idlabel = ''
181
182
        # FIXME: Something about the diff format makes patch unhappy
183
        # with newly-added files.
184
185
        def diffit(oldlines, newlines, **kw):
186
            
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.
193
194
            # In the meantime we at least make sure the patch isn't
195
            # mangled.
196
            
197
198
            # Special workaround for Python2.3, where difflib fails if
199
            # both sequences are empty.
200
            if not oldlines and not newlines:
201
                return
202
203
            nonl = False
204
205
            if oldlines and (oldlines[-1][-1] != '\n'):
206
                oldlines[-1] += '\n'
207
                nonl = True
208
            if newlines and (newlines[-1][-1] != '\n'):
209
                newlines[-1] += '\n'
210
                nonl = True
211
212
            ud = difflib.unified_diff(oldlines, newlines, **kw)
213
            sys.stdout.writelines(ud)
214
            if nonl:
215
                print "\\ No newline at end of file"
216
            sys.stdout.write('\n')
217
        
218
        if file_state in ['.', '?', 'I']:
219
            continue
220
        elif file_state == 'A':
221
            print '*** added %s %r' % (kind, new_name)
222
            if kind == 'file':
223
                diffit([],
224
                       new_tree.get_file(fid).readlines(),
225
                       fromfile=DEVNULL,
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)
230
            if kind == 'file':
231
                diffit(old_tree.get_file(fid).readlines(), [],
232
                       fromfile=old_label + old_name + idlabel,
233
                       tofile=DEVNULL)
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)
241
242
            if kind == 'file':
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)
247
        else:
356 by Martin Pool
- pychecker fixes in bzrlib.diff
248
            raise BzrError("can't represent state %s {%s}" % (file_state, fid))
329 by Martin Pool
- refactor command functions into command classes
249
250