~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

Late bind to PatienceSequenceMatcher to allow plugin to override.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
15
14
# along with this program; if not, write to the Free Software
16
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
16
 
18
 
from sets import Set
19
 
 
20
 
from trace import mutter
21
 
 
22
 
 
23
 
 
24
 
 
25
 
 
26
 
 
27
 
def diff_trees(old_tree, new_tree):
28
 
    """Compute diff between two trees.
29
 
 
30
 
    They may be in different branches and may be working or historical
31
 
    trees.
32
 
 
33
 
    Yields a sequence of (state, id, old_name, new_name, kind).
34
 
    Each filename and each id is listed only once.
35
 
    """
36
 
 
37
 
    ## TODO: Compare files before diffing; only mention those that have changed
38
 
 
39
 
    ## TODO: Set nice names in the headers, maybe include diffstat
40
 
 
41
 
    ## TODO: Perhaps make this a generator rather than using
42
 
    ## a callback object?
43
 
 
44
 
    ## TODO: Allow specifying a list of files to compare, rather than
45
 
    ## doing the whole tree?  (Not urgent.)
46
 
 
47
 
    ## TODO: Allow diffing any two inventories, not just the
48
 
    ## current one against one.  We mgiht need to specify two
49
 
    ## stores to look for the files if diffing two branches.  That
50
 
    ## might imply this shouldn't be primarily a Branch method.
51
 
 
52
 
    ## XXX: This doesn't report on unknown files; that can be done
53
 
    ## from a separate method.
54
 
 
55
 
    old_it = old_tree.list_files()
56
 
    new_it = new_tree.list_files()
57
 
 
58
 
    def next(it):
 
17
from bzrlib.delta import compare_trees
 
18
from bzrlib.errors import BzrError
 
19
import bzrlib.errors as errors
 
20
from bzrlib.patiencediff import unified_diff
 
21
import bzrlib.patiencediff
 
22
from bzrlib.symbol_versioning import *
 
23
from bzrlib.textfile import check_text_lines
 
24
from bzrlib.trace import mutter
 
25
 
 
26
 
 
27
# TODO: Rather than building a changeset object, we should probably
 
28
# invoke callbacks on an object.  That object can either accumulate a
 
29
# list, write them out directly, etc etc.
 
30
 
 
31
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
32
                  allow_binary=False, sequence_matcher=None):
 
33
    # FIXME: difflib is wrong if there is no trailing newline.
 
34
    # The syntax used by patch seems to be "\ No newline at
 
35
    # end of file" following the last diff line from that
 
36
    # file.  This is not trivial to insert into the
 
37
    # unified_diff output and it might be better to just fix
 
38
    # or replace that function.
 
39
 
 
40
    # In the meantime we at least make sure the patch isn't
 
41
    # mangled.
 
42
 
 
43
 
 
44
    # Special workaround for Python2.3, where difflib fails if
 
45
    # both sequences are empty.
 
46
    if not oldlines and not newlines:
 
47
        return
 
48
    
 
49
    if allow_binary is False:
 
50
        check_text_lines(oldlines)
 
51
        check_text_lines(newlines)
 
52
 
 
53
    if sequence_matcher is None:
 
54
        sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
 
55
    ud = unified_diff(oldlines, newlines,
 
56
                      fromfile=old_filename+'\t', 
 
57
                      tofile=new_filename+'\t',
 
58
                      sequencematcher=sequence_matcher)
 
59
 
 
60
    ud = list(ud)
 
61
    # work-around for difflib being too smart for its own good
 
62
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
 
63
    if not oldlines:
 
64
        ud[2] = ud[2].replace('-1,0', '-0,0')
 
65
    elif not newlines:
 
66
        ud[2] = ud[2].replace('+1,0', '+0,0')
 
67
    # work around for difflib emitting random spaces after the label
 
68
    ud[0] = ud[0][:-2] + '\n'
 
69
    ud[1] = ud[1][:-2] + '\n'
 
70
 
 
71
    for line in ud:
 
72
        to_file.write(line)
 
73
        if not line.endswith('\n'):
 
74
            to_file.write("\n\\ No newline at end of file\n")
 
75
    print >>to_file
 
76
 
 
77
 
 
78
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
79
                  diff_opts):
 
80
    """Display a diff by calling out to the external diff program."""
 
81
    import sys
 
82
    
 
83
    if to_file != sys.stdout:
 
84
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
 
85
                                  to_file)
 
86
 
 
87
    # make sure our own output is properly ordered before the diff
 
88
    to_file.flush()
 
89
 
 
90
    from tempfile import NamedTemporaryFile
 
91
    import os
 
92
 
 
93
    oldtmpf = NamedTemporaryFile()
 
94
    newtmpf = NamedTemporaryFile()
 
95
 
 
96
    try:
 
97
        # TODO: perhaps a special case for comparing to or from the empty
 
98
        # sequence; can just use /dev/null on Unix
 
99
 
 
100
        # TODO: if either of the files being compared already exists as a
 
101
        # regular named file (e.g. in the working directory) then we can
 
102
        # compare directly to that, rather than copying it.
 
103
 
 
104
        oldtmpf.writelines(oldlines)
 
105
        newtmpf.writelines(newlines)
 
106
 
 
107
        oldtmpf.flush()
 
108
        newtmpf.flush()
 
109
 
 
110
        if not diff_opts:
 
111
            diff_opts = []
 
112
        diffcmd = ['diff',
 
113
                   '--label', old_filename+'\t',
 
114
                   oldtmpf.name,
 
115
                   '--label', new_filename+'\t',
 
116
                   newtmpf.name]
 
117
 
 
118
        # diff only allows one style to be specified; they don't override.
 
119
        # note that some of these take optargs, and the optargs can be
 
120
        # directly appended to the options.
 
121
        # this is only an approximate parser; it doesn't properly understand
 
122
        # the grammar.
 
123
        for s in ['-c', '-u', '-C', '-U',
 
124
                  '-e', '--ed',
 
125
                  '-q', '--brief',
 
126
                  '--normal',
 
127
                  '-n', '--rcs',
 
128
                  '-y', '--side-by-side',
 
129
                  '-D', '--ifdef']:
 
130
            for j in diff_opts:
 
131
                if j.startswith(s):
 
132
                    break
 
133
            else:
 
134
                continue
 
135
            break
 
136
        else:
 
137
            diffcmd.append('-u')
 
138
                  
 
139
        if diff_opts:
 
140
            diffcmd.extend(diff_opts)
 
141
 
 
142
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
 
143
        
 
144
        if rc != 0 and rc != 1:
 
145
            # returns 1 if files differ; that's OK
 
146
            if rc < 0:
 
147
                msg = 'signal %d' % (-rc)
 
148
            else:
 
149
                msg = 'exit code %d' % rc
 
150
                
 
151
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
 
152
    finally:
 
153
        oldtmpf.close()                 # and delete
 
154
        newtmpf.close()
 
155
 
 
156
 
 
157
@deprecated_function(zero_eight)
 
158
def show_diff(b, from_spec, specific_files, external_diff_options=None,
 
159
              revision2=None, output=None, b2=None):
 
160
    """Shortcut for showing the diff to the working tree.
 
161
 
 
162
    Please use show_diff_trees instead.
 
163
 
 
164
    b
 
165
        Branch.
 
166
 
 
167
    revision
 
168
        None for 'basis tree', or otherwise the old revision to compare against.
 
169
    
 
170
    The more general form is show_diff_trees(), where the caller
 
171
    supplies any two trees.
 
172
    """
 
173
    if output is None:
 
174
        import sys
 
175
        output = sys.stdout
 
176
 
 
177
    if from_spec is None:
 
178
        old_tree = b.bzrdir.open_workingtree()
 
179
        if b2 is None:
 
180
            old_tree = old_tree = old_tree.basis_tree()
 
181
    else:
 
182
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
 
183
 
 
184
    if revision2 is None:
 
185
        if b2 is None:
 
186
            new_tree = b.bzrdir.open_workingtree()
 
187
        else:
 
188
            new_tree = b2.bzrdir.open_workingtree()
 
189
    else:
 
190
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
 
191
 
 
192
    return show_diff_trees(old_tree, new_tree, output, specific_files,
 
193
                           external_diff_options)
 
194
 
 
195
 
 
196
def diff_cmd_helper(tree, specific_files, external_diff_options, 
 
197
                    old_revision_spec=None, new_revision_spec=None,
 
198
                    old_label='a/', new_label='b/'):
 
199
    """Helper for cmd_diff.
 
200
 
 
201
   tree 
 
202
        A WorkingTree
 
203
 
 
204
    specific_files
 
205
        The specific files to compare, or None
 
206
 
 
207
    external_diff_options
 
208
        If non-None, run an external diff, and pass it these options
 
209
 
 
210
    old_revision_spec
 
211
        If None, use basis tree as old revision, otherwise use the tree for
 
212
        the specified revision. 
 
213
 
 
214
    new_revision_spec
 
215
        If None, use working tree as new revision, otherwise use the tree for
 
216
        the specified revision.
 
217
    
 
218
    The more general form is show_diff_trees(), where the caller
 
219
    supplies any two trees.
 
220
    """
 
221
    import sys
 
222
    output = sys.stdout
 
223
    def spec_tree(spec):
 
224
        revision_id = spec.in_store(tree.branch).rev_id
 
225
        return tree.branch.repository.revision_tree(revision_id)
 
226
    if old_revision_spec is None:
 
227
        old_tree = tree.basis_tree()
 
228
    else:
 
229
        old_tree = spec_tree(old_revision_spec)
 
230
 
 
231
    if new_revision_spec is None:
 
232
        new_tree = tree
 
233
    else:
 
234
        new_tree = spec_tree(new_revision_spec)
 
235
 
 
236
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
 
237
                           external_diff_options,
 
238
                           old_label=old_label, new_label=new_label)
 
239
 
 
240
 
 
241
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
 
242
                    external_diff_options=None,
 
243
                    old_label='a/', new_label='b/'):
 
244
    """Show in text form the changes from one tree to another.
 
245
 
 
246
    to_files
 
247
        If set, include only changes to these files.
 
248
 
 
249
    external_diff_options
 
250
        If set, use an external GNU diff and pass these options.
 
251
    """
 
252
    old_tree.lock_read()
 
253
    try:
 
254
        new_tree.lock_read()
59
255
        try:
60
 
            return it.next()
61
 
        except StopIteration:
62
 
            return None
63
 
 
64
 
    old_item = next(old_it)
65
 
    new_item = next(new_it)
66
 
 
67
 
    # We step through the two sorted iterators in parallel, trying to
68
 
    # keep them lined up.
69
 
 
70
 
    while (old_item != None) or (new_item != None):
71
 
        # OK, we still have some remaining on both, but they may be
72
 
        # out of step.        
73
 
        if old_item != None:
74
 
            old_name, old_class, old_kind, old_id = old_item
75
 
        else:
76
 
            old_name = None
77
 
            
78
 
        if new_item != None:
79
 
            new_name, new_class, new_kind, new_id = new_item
80
 
        else:
81
 
            new_name = None
82
 
 
83
 
        mutter("   diff pairwise %r" % (old_item,))
84
 
        mutter("                 %r" % (new_item,))
85
 
 
86
 
        if old_item:
87
 
            # can't handle the old tree being a WorkingTree
88
 
            assert old_class == 'V'
89
 
 
90
 
        if new_item and (new_class != 'V'):
91
 
            yield new_class, None, None, new_name, new_kind
92
 
            new_item = next(new_it)
93
 
        elif (not new_item) or (old_item and (old_name < new_name)):
94
 
            mutter("     extra entry in old-tree sequence")
95
 
            if new_tree.has_id(old_id):
96
 
                # will be mentioned as renamed under new name
97
 
                pass
98
 
            else:
99
 
                yield 'D', old_id, old_name, None, old_kind
100
 
            old_item = next(old_it)
101
 
        elif (not old_item) or (new_item and (new_name < old_name)):
102
 
            mutter("     extra entry in new-tree sequence")
103
 
            if old_tree.has_id(new_id):
104
 
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
105
 
            else:
106
 
                yield 'A', new_id, None, new_name, new_kind
107
 
            new_item = next(new_it)
108
 
        elif old_id != new_id:
109
 
            assert old_name == new_name
110
 
            # both trees have a file of this name, but it is not the
111
 
            # same file.  in other words, the old filename has been
112
 
            # overwritten by either a newly-added or a renamed file.
113
 
            # (should we return something about the overwritten file?)
114
 
            if old_tree.has_id(new_id):
115
 
                # renaming, overlying a deleted file
116
 
                yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
117
 
            else:
118
 
                yield 'A', new_id, None, new_name, new_kind
119
 
 
120
 
            new_item = next(new_it)
121
 
            old_item = next(old_it)
122
 
        else:
123
 
            assert old_id == new_id
124
 
            assert old_name == new_name
125
 
            assert old_kind == new_kind
126
 
 
127
 
            if old_kind == 'directory':
128
 
                yield '.', new_id, old_name, new_name, new_kind
129
 
            elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id):
130
 
                mutter("    file size has changed, must be different")
131
 
                yield 'M', new_id, old_name, new_name, new_kind
132
 
            elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
133
 
                mutter("      SHA1 indicates they're identical")
134
 
                ## assert compare_files(old_tree.get_file(i), new_tree.get_file(i))
135
 
                yield '.', new_id, old_name, new_name, new_kind
136
 
            else:
137
 
                mutter("      quick compare shows different")
138
 
                yield 'M', new_id, old_name, new_name, new_kind
139
 
 
140
 
            new_item = next(new_it)
141
 
            old_item = next(old_it)
142
 
 
143
 
 
 
256
            return _show_diff_trees(old_tree, new_tree, to_file,
 
257
                                    specific_files, external_diff_options,
 
258
                                    old_label=old_label, new_label=new_label)
 
259
        finally:
 
260
            new_tree.unlock()
 
261
    finally:
 
262
        old_tree.unlock()
 
263
 
 
264
 
 
265
def _show_diff_trees(old_tree, new_tree, to_file,
 
266
                     specific_files, external_diff_options, 
 
267
                     old_label='a/', new_label='b/' ):
 
268
 
 
269
    DEVNULL = '/dev/null'
 
270
    # Windows users, don't panic about this filename -- it is a
 
271
    # special signal to GNU patch that the file should be created or
 
272
    # deleted respectively.
 
273
 
 
274
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
275
    # be usefully made into a much faster special case.
 
276
 
 
277
    _raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
 
278
 
 
279
    if external_diff_options:
 
280
        assert isinstance(external_diff_options, basestring)
 
281
        opts = external_diff_options.split()
 
282
        def diff_file(olab, olines, nlab, nlines, to_file):
 
283
            external_diff(olab, olines, nlab, nlines, to_file, opts)
 
284
    else:
 
285
        diff_file = internal_diff
 
286
    
 
287
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
 
288
                          specific_files=specific_files)
 
289
 
 
290
    has_changes = 0
 
291
    for path, file_id, kind in delta.removed:
 
292
        has_changes = 1
 
293
        print >>to_file, '=== removed %s %r' % (kind, path)
 
294
        old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
 
295
                                         DEVNULL, None, None, to_file)
 
296
    for path, file_id, kind in delta.added:
 
297
        has_changes = 1
 
298
        print >>to_file, '=== added %s %r' % (kind, path)
 
299
        new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
 
300
                                         DEVNULL, None, None, to_file, 
 
301
                                         reverse=True)
 
302
    for (old_path, new_path, file_id, kind,
 
303
         text_modified, meta_modified) in delta.renamed:
 
304
        has_changes = 1
 
305
        prop_str = get_prop_change(meta_modified)
 
306
        print >>to_file, '=== renamed %s %r => %r%s' % (
 
307
                    kind, old_path, new_path, prop_str)
 
308
        _maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
 
309
                                    new_label, new_path, new_tree,
 
310
                                    text_modified, kind, to_file, diff_file)
 
311
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
 
312
        has_changes = 1
 
313
        prop_str = get_prop_change(meta_modified)
 
314
        print >>to_file, '=== modified %s %r%s' % (kind, path, prop_str)
 
315
        if text_modified:
 
316
            _maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
 
317
                                        new_label, path, new_tree,
 
318
                                        True, kind, to_file, diff_file)
 
319
 
 
320
    return has_changes
 
321
 
 
322
 
 
323
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
 
324
    """Complain if paths are not versioned in either tree."""
 
325
    if not specific_files:
 
326
        return
 
327
    old_unversioned = old_tree.filter_unversioned_files(specific_files)
 
328
    new_unversioned = new_tree.filter_unversioned_files(specific_files)
 
329
    unversioned = old_unversioned.intersection(new_unversioned)
 
330
    if unversioned:
 
331
        raise errors.PathsNotVersionedError(sorted(unversioned))
 
332
    
 
333
 
 
334
def _raise_if_nonexistent(paths, old_tree, new_tree):
 
335
    """Complain if paths are not in either inventory or tree.
 
336
 
 
337
    It's OK with the files exist in either tree's inventory, or 
 
338
    if they exist in the tree but are not versioned.
 
339
    
 
340
    This can be used by operations such as bzr status that can accept
 
341
    unknown or ignored files.
 
342
    """
 
343
    mutter("check paths: %r", paths)
 
344
    if not paths:
 
345
        return
 
346
    s = old_tree.filter_unversioned_files(paths)
 
347
    s = new_tree.filter_unversioned_files(s)
 
348
    s = [path for path in s if not new_tree.has_filename(path)]
 
349
    if s:
 
350
        raise errors.PathsDoNotExist(sorted(s))
 
351
 
 
352
 
 
353
def get_prop_change(meta_modified):
 
354
    if meta_modified:
 
355
        return " (properties changed)"
 
356
    else:
 
357
        return  ""
 
358
 
 
359
 
 
360
def _maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
 
361
                                new_label, new_path, new_tree, text_modified,
 
362
                                kind, to_file, diff_file):
 
363
    if text_modified:
 
364
        new_entry = new_tree.inventory[file_id]
 
365
        old_tree.inventory[file_id].diff(diff_file,
 
366
                                         old_label + old_path, old_tree,
 
367
                                         new_label + new_path, new_entry, 
 
368
                                         new_tree, to_file)