~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin Pool
  • Date: 2005-07-04 07:34:19 UTC
  • Revision ID: mbp@sourcefrog.net-20050704073419-44eb753d5556a4d0
- rename control file to pending-merges

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
from errors import BzrError
20
20
 
21
21
 
 
22
# TODO: Rather than building a changeset object, we should probably
 
23
# invoke callbacks on an object.  That object can either accumulate a
 
24
# list, write them out directly, etc etc.
22
25
 
23
 
def _diff_one(oldlines, newlines, to_file, **kw):
 
26
def internal_diff(old_label, oldlines, new_label, newlines, to_file):
24
27
    import difflib
25
28
    
26
29
    # FIXME: difflib is wrong if there is no trailing newline.
48
51
        newlines[-1] += '\n'
49
52
        nonl = True
50
53
 
51
 
    ud = difflib.unified_diff(oldlines, newlines, **kw)
 
54
    ud = difflib.unified_diff(oldlines, newlines,
 
55
                              fromfile=old_label, tofile=new_label)
52
56
 
53
57
    # work-around for difflib being too smart for its own good
54
58
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
59
63
        ud = list(ud)
60
64
        ud[2] = ud[2].replace('+1,0', '+0,0')
61
65
 
62
 
    to_file.writelines(ud)
 
66
    for line in ud:
 
67
        to_file.write(line)
63
68
    if nonl:
64
69
        print >>to_file, "\\ No newline at end of file"
65
70
    print >>to_file
66
71
 
67
72
 
68
73
 
69
 
def show_diff(b, revision, specific_files):
 
74
 
 
75
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
76
                  diff_opts):
 
77
    """Display a diff by calling out to the external diff program."""
 
78
    import sys
 
79
    
 
80
    if to_file != sys.stdout:
 
81
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
 
82
                                  to_file)
 
83
 
 
84
    # make sure our own output is properly ordered before the diff
 
85
    to_file.flush()
 
86
 
 
87
    from tempfile import NamedTemporaryFile
 
88
    import os
 
89
 
 
90
    oldtmpf = NamedTemporaryFile()
 
91
    newtmpf = NamedTemporaryFile()
 
92
 
 
93
    try:
 
94
        # TODO: perhaps a special case for comparing to or from the empty
 
95
        # sequence; can just use /dev/null on Unix
 
96
 
 
97
        # TODO: if either of the files being compared already exists as a
 
98
        # regular named file (e.g. in the working directory) then we can
 
99
        # compare directly to that, rather than copying it.
 
100
 
 
101
        oldtmpf.writelines(oldlines)
 
102
        newtmpf.writelines(newlines)
 
103
 
 
104
        oldtmpf.flush()
 
105
        newtmpf.flush()
 
106
 
 
107
        if not diff_opts:
 
108
            diff_opts = []
 
109
        diffcmd = ['diff',
 
110
                   '--label', old_label,
 
111
                   oldtmpf.name,
 
112
                   '--label', new_label,
 
113
                   newtmpf.name]
 
114
 
 
115
        # diff only allows one style to be specified; they don't override.
 
116
        # note that some of these take optargs, and the optargs can be
 
117
        # directly appended to the options.
 
118
        # this is only an approximate parser; it doesn't properly understand
 
119
        # the grammar.
 
120
        for s in ['-c', '-u', '-C', '-U',
 
121
                  '-e', '--ed',
 
122
                  '-q', '--brief',
 
123
                  '--normal',
 
124
                  '-n', '--rcs',
 
125
                  '-y', '--side-by-side',
 
126
                  '-D', '--ifdef']:
 
127
            for j in diff_opts:
 
128
                if j.startswith(s):
 
129
                    break
 
130
            else:
 
131
                continue
 
132
            break
 
133
        else:
 
134
            diffcmd.append('-u')
 
135
                  
 
136
        if diff_opts:
 
137
            diffcmd.extend(diff_opts)
 
138
 
 
139
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
 
140
        
 
141
        if rc != 0 and rc != 1:
 
142
            # returns 1 if files differ; that's OK
 
143
            if rc < 0:
 
144
                msg = 'signal %d' % (-rc)
 
145
            else:
 
146
                msg = 'exit code %d' % rc
 
147
                
 
148
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
 
149
    finally:
 
150
        oldtmpf.close()                 # and delete
 
151
        newtmpf.close()
 
152
    
 
153
 
 
154
 
 
155
def show_diff(b, revision, specific_files, external_diff_options=None):
 
156
    """Shortcut for showing the diff to the working tree.
 
157
 
 
158
    b
 
159
        Branch.
 
160
 
 
161
    revision
 
162
        None for each, or otherwise the old revision to compare against.
 
163
    
 
164
    The more general form is show_diff_trees(), where the caller
 
165
    supplies any two trees.
 
166
    """
70
167
    import sys
71
168
 
72
169
    if revision == None:
76
173
        
77
174
    new_tree = b.working_tree()
78
175
 
79
 
    show_diff_trees(old_tree, new_tree, sys.stdout, specific_files)
80
 
 
81
 
 
82
 
 
83
 
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None):
 
176
    show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
 
177
                    external_diff_options)
 
178
 
 
179
 
 
180
 
 
181
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
 
182
                    external_diff_options=None):
84
183
    """Show in text form the changes from one tree to another.
85
184
 
86
185
    to_files
87
186
        If set, include only changes to these files.
 
187
 
 
188
    external_diff_options
 
189
        If set, use an external GNU diff and pass these options.
88
190
    """
89
191
 
90
192
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
99
201
    # TODO: Generation of pseudo-diffs for added/deleted files could
100
202
    # be usefully made into a much faster special case.
101
203
 
 
204
    if external_diff_options:
 
205
        assert isinstance(external_diff_options, basestring)
 
206
        opts = external_diff_options.split()
 
207
        def diff_file(olab, olines, nlab, nlines, to_file):
 
208
            external_diff(olab, olines, nlab, nlines, to_file, opts)
 
209
    else:
 
210
        diff_file = internal_diff
 
211
    
 
212
 
102
213
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
103
214
                          specific_files=specific_files)
104
215
 
105
216
    for path, file_id, kind in delta.removed:
106
 
        print '*** removed %s %r' % (kind, path)
 
217
        print >>to_file, '*** removed %s %r' % (kind, path)
107
218
        if kind == 'file':
108
 
            _diff_one(old_tree.get_file(file_id).readlines(),
109
 
                   [],
110
 
                   to_file,
111
 
                   fromfile=old_label + path,
112
 
                   tofile=DEVNULL)
 
219
            diff_file(old_label + path,
 
220
                      old_tree.get_file(file_id).readlines(),
 
221
                      DEVNULL, 
 
222
                      [],
 
223
                      to_file)
113
224
 
114
225
    for path, file_id, kind in delta.added:
115
 
        print '*** added %s %r' % (kind, path)
 
226
        print >>to_file, '*** added %s %r' % (kind, path)
116
227
        if kind == 'file':
117
 
            _diff_one([],
118
 
                   new_tree.get_file(file_id).readlines(),
119
 
                   to_file,
120
 
                   fromfile=DEVNULL,
121
 
                   tofile=new_label + path)
 
228
            diff_file(DEVNULL,
 
229
                      [],
 
230
                      new_label + path,
 
231
                      new_tree.get_file(file_id).readlines(),
 
232
                      to_file)
122
233
 
123
234
    for old_path, new_path, file_id, kind, text_modified in delta.renamed:
124
 
        print '*** renamed %s %r => %r' % (kind, old_path, new_path)
 
235
        print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
125
236
        if text_modified:
126
 
            _diff_one(old_tree.get_file(file_id).readlines(),
127
 
                   new_tree.get_file(file_id).readlines(),
128
 
                   to_file,
129
 
                   fromfile=old_label + old_path,
130
 
                   tofile=new_label + new_path)
 
237
            diff_file(old_label + old_path,
 
238
                      old_tree.get_file(file_id).readlines(),
 
239
                      new_label + new_path,
 
240
                      new_tree.get_file(file_id).readlines(),
 
241
                      to_file)
131
242
 
132
243
    for path, file_id, kind in delta.modified:
133
 
        print '*** modified %s %r' % (kind, path)
 
244
        print >>to_file, '*** modified %s %r' % (kind, path)
134
245
        if kind == 'file':
135
 
            _diff_one(old_tree.get_file(file_id).readlines(),
136
 
                   new_tree.get_file(file_id).readlines(),
137
 
                   to_file,
138
 
                   fromfile=old_label + path,
139
 
                   tofile=new_label + path)
 
246
            diff_file(old_label + path,
 
247
                      old_tree.get_file(file_id).readlines(),
 
248
                      new_label + path,
 
249
                      new_tree.get_file(file_id).readlines(),
 
250
                      to_file)
140
251
 
141
252
 
142
253
 
161
272
    Files that are both modified and renamed are listed only in
162
273
    renamed, with the text_modified flag true.
163
274
 
 
275
    Files are only considered renamed if their name has changed or
 
276
    their parent directory has changed.  Renaming a directory
 
277
    does not count as renaming all its contents.
 
278
 
164
279
    The lists are normally sorted when the delta is created.
165
280
    """
166
281
    def __init__(self):
170
285
        self.modified = []
171
286
        self.unchanged = []
172
287
 
 
288
    def __eq__(self, other):
 
289
        if not isinstance(other, TreeDelta):
 
290
            return False
 
291
        return self.added == other.added \
 
292
               and self.removed == other.removed \
 
293
               and self.renamed == other.renamed \
 
294
               and self.modified == other.modified \
 
295
               and self.unchanged == other.unchanged
 
296
 
 
297
    def __ne__(self, other):
 
298
        return not (self == other)
 
299
 
 
300
    def __repr__(self):
 
301
        return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
 
302
            " unchanged=%r)" % (self.added, self.removed, self.renamed,
 
303
            self.modified, self.unchanged)
 
304
 
 
305
    def has_changed(self):
 
306
        changes = len(self.added) + len(self.removed) + len(self.renamed)
 
307
        changes += len(self.modified) 
 
308
        return (changes != 0)
173
309
 
174
310
    def touches_file_id(self, file_id):
175
311
        """Return True if file_id is modified by this delta."""
222
358
 
223
359
 
224
360
 
225
 
def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None):
 
361
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
226
362
    """Describe changes from one tree to another.
227
363
 
228
364
    Returns a TreeDelta with details of added, modified, renamed, and
265
401
            old_path = old_inv.id2path(file_id)
266
402
            new_path = new_inv.id2path(file_id)
267
403
 
 
404
            old_ie = old_inv[file_id]
 
405
            new_ie = new_inv[file_id]
 
406
 
268
407
            if specific_files:
269
408
                if (not is_inside_any(specific_files, old_path) 
270
409
                    and not is_inside_any(specific_files, new_path)):
283
422
            # the same and the parents are unchanged all the way up.
284
423
            # May not be worthwhile.
285
424
            
286
 
            if old_path != new_path:
 
425
            if (old_ie.name != new_ie.name
 
426
                or old_ie.parent_id != new_ie.parent_id):
287
427
                delta.renamed.append((old_path, new_path, file_id, kind,
288
428
                                      text_modified))
289
429
            elif text_modified:
291
431
            elif want_unchanged:
292
432
                delta.unchanged.append((new_path, file_id, kind))
293
433
        else:
 
434
            kind = old_inv.get_file_kind(file_id)
294
435
            old_path = old_inv.id2path(file_id)
295
436
            if specific_files:
296
437
                if not is_inside_any(specific_files, old_path):