/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin Pool
  • Date: 2007-08-15 04:33:34 UTC
  • mto: (2701.1.2 remove-should-cache)
  • mto: This revision was merged to the branch mainline in revision 2710.
  • Revision ID: mbp@sourcefrog.net-20070815043334-01dx9emb0vjiy29v
Remove things deprecated in 0.11 and earlier

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
 
18
import re
 
19
import sys
 
20
 
 
21
from bzrlib.lazy_import import lazy_import
 
22
lazy_import(globals(), """
 
23
import errno
 
24
import subprocess
 
25
import tempfile
 
26
import time
 
27
 
 
28
from bzrlib import (
 
29
    errors,
 
30
    osutils,
 
31
    patiencediff,
 
32
    textfile,
 
33
    timestamp,
 
34
    )
 
35
""")
 
36
 
 
37
from bzrlib.symbol_versioning import (
 
38
        deprecated_function,
 
39
        )
 
40
from bzrlib.trace import mutter, warning
 
41
 
 
42
 
 
43
# TODO: Rather than building a changeset object, we should probably
 
44
# invoke callbacks on an object.  That object can either accumulate a
 
45
# list, write them out directly, etc etc.
 
46
 
 
47
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
48
                  allow_binary=False, sequence_matcher=None,
 
49
                  path_encoding='utf8'):
 
50
    # FIXME: difflib is wrong if there is no trailing newline.
 
51
    # The syntax used by patch seems to be "\ No newline at
 
52
    # end of file" following the last diff line from that
 
53
    # file.  This is not trivial to insert into the
 
54
    # unified_diff output and it might be better to just fix
 
55
    # or replace that function.
 
56
 
 
57
    # In the meantime we at least make sure the patch isn't
 
58
    # mangled.
 
59
 
 
60
 
 
61
    # Special workaround for Python2.3, where difflib fails if
 
62
    # both sequences are empty.
 
63
    if not oldlines and not newlines:
 
64
        return
 
65
    
 
66
    if allow_binary is False:
 
67
        textfile.check_text_lines(oldlines)
 
68
        textfile.check_text_lines(newlines)
 
69
 
 
70
    if sequence_matcher is None:
 
71
        sequence_matcher = patiencediff.PatienceSequenceMatcher
 
72
    ud = patiencediff.unified_diff(oldlines, newlines,
 
73
                      fromfile=old_filename.encode(path_encoding),
 
74
                      tofile=new_filename.encode(path_encoding),
 
75
                      sequencematcher=sequence_matcher)
 
76
 
 
77
    ud = list(ud)
 
78
    # work-around for difflib being too smart for its own good
 
79
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
 
80
    if not oldlines:
 
81
        ud[2] = ud[2].replace('-1,0', '-0,0')
 
82
    elif not newlines:
 
83
        ud[2] = ud[2].replace('+1,0', '+0,0')
 
84
    # work around for difflib emitting random spaces after the label
 
85
    ud[0] = ud[0][:-2] + '\n'
 
86
    ud[1] = ud[1][:-2] + '\n'
 
87
 
 
88
    for line in ud:
 
89
        to_file.write(line)
 
90
        if not line.endswith('\n'):
 
91
            to_file.write("\n\\ No newline at end of file\n")
 
92
    print >>to_file
 
93
 
 
94
 
 
95
def _spawn_external_diff(diffcmd, capture_errors=True):
 
96
    """Spawn the externall diff process, and return the child handle.
 
97
 
 
98
    :param diffcmd: The command list to spawn
 
99
    :param capture_errors: Capture stderr as well as setting LANG=C
 
100
        and LC_ALL=C. This lets us read and understand the output of diff,
 
101
        and respond to any errors.
 
102
    :return: A Popen object.
 
103
    """
 
104
    if capture_errors:
 
105
        # construct minimal environment
 
106
        env = {}
 
107
        path = os.environ.get('PATH')
 
108
        if path is not None:
 
109
            env['PATH'] = path
 
110
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
 
111
        env['LANG'] = 'C'
 
112
        env['LC_ALL'] = 'C'
 
113
        stderr = subprocess.PIPE
 
114
    else:
 
115
        env = None
 
116
        stderr = None
 
117
 
 
118
    try:
 
119
        pipe = subprocess.Popen(diffcmd,
 
120
                                stdin=subprocess.PIPE,
 
121
                                stdout=subprocess.PIPE,
 
122
                                stderr=stderr,
 
123
                                env=env)
 
124
    except OSError, e:
 
125
        if e.errno == errno.ENOENT:
 
126
            raise errors.NoDiff(str(e))
 
127
        raise
 
128
 
 
129
    return pipe
 
130
 
 
131
 
 
132
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
133
                  diff_opts):
 
134
    """Display a diff by calling out to the external diff program."""
 
135
    # make sure our own output is properly ordered before the diff
 
136
    to_file.flush()
 
137
 
 
138
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
 
139
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
 
140
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
 
141
    newtmpf = os.fdopen(newtmp_fd, 'wb')
 
142
 
 
143
    try:
 
144
        # TODO: perhaps a special case for comparing to or from the empty
 
145
        # sequence; can just use /dev/null on Unix
 
146
 
 
147
        # TODO: if either of the files being compared already exists as a
 
148
        # regular named file (e.g. in the working directory) then we can
 
149
        # compare directly to that, rather than copying it.
 
150
 
 
151
        oldtmpf.writelines(oldlines)
 
152
        newtmpf.writelines(newlines)
 
153
 
 
154
        oldtmpf.close()
 
155
        newtmpf.close()
 
156
 
 
157
        if not diff_opts:
 
158
            diff_opts = []
 
159
        diffcmd = ['diff',
 
160
                   '--label', old_filename,
 
161
                   old_abspath,
 
162
                   '--label', new_filename,
 
163
                   new_abspath,
 
164
                   '--binary',
 
165
                  ]
 
166
 
 
167
        # diff only allows one style to be specified; they don't override.
 
168
        # note that some of these take optargs, and the optargs can be
 
169
        # directly appended to the options.
 
170
        # this is only an approximate parser; it doesn't properly understand
 
171
        # the grammar.
 
172
        for s in ['-c', '-u', '-C', '-U',
 
173
                  '-e', '--ed',
 
174
                  '-q', '--brief',
 
175
                  '--normal',
 
176
                  '-n', '--rcs',
 
177
                  '-y', '--side-by-side',
 
178
                  '-D', '--ifdef']:
 
179
            for j in diff_opts:
 
180
                if j.startswith(s):
 
181
                    break
 
182
            else:
 
183
                continue
 
184
            break
 
185
        else:
 
186
            diffcmd.append('-u')
 
187
                  
 
188
        if diff_opts:
 
189
            diffcmd.extend(diff_opts)
 
190
 
 
191
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
 
192
        out,err = pipe.communicate()
 
193
        rc = pipe.returncode
 
194
        
 
195
        # internal_diff() adds a trailing newline, add one here for consistency
 
196
        out += '\n'
 
197
        if rc == 2:
 
198
            # 'diff' gives retcode == 2 for all sorts of errors
 
199
            # one of those is 'Binary files differ'.
 
200
            # Bad options could also be the problem.
 
201
            # 'Binary files' is not a real error, so we suppress that error.
 
202
            lang_c_out = out
 
203
 
 
204
            # Since we got here, we want to make sure to give an i18n error
 
205
            pipe = _spawn_external_diff(diffcmd, capture_errors=False)
 
206
            out, err = pipe.communicate()
 
207
 
 
208
            # Write out the new i18n diff response
 
209
            to_file.write(out+'\n')
 
210
            if pipe.returncode != 2:
 
211
                raise errors.BzrError(
 
212
                               'external diff failed with exit code 2'
 
213
                               ' when run with LANG=C and LC_ALL=C,'
 
214
                               ' but not when run natively: %r' % (diffcmd,))
 
215
 
 
216
            first_line = lang_c_out.split('\n', 1)[0]
 
217
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
 
218
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
 
219
            if m is None:
 
220
                raise errors.BzrError('external diff failed with exit code 2;'
 
221
                                      ' command: %r' % (diffcmd,))
 
222
            else:
 
223
                # Binary files differ, just return
 
224
                return
 
225
 
 
226
        # If we got to here, we haven't written out the output of diff
 
227
        # do so now
 
228
        to_file.write(out)
 
229
        if rc not in (0, 1):
 
230
            # returns 1 if files differ; that's OK
 
231
            if rc < 0:
 
232
                msg = 'signal %d' % (-rc)
 
233
            else:
 
234
                msg = 'exit code %d' % rc
 
235
                
 
236
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
237
                                  % (rc, diffcmd))
 
238
 
 
239
 
 
240
    finally:
 
241
        oldtmpf.close()                 # and delete
 
242
        newtmpf.close()
 
243
        # Clean up. Warn in case the files couldn't be deleted
 
244
        # (in case windows still holds the file open, but not
 
245
        # if the files have already been deleted)
 
246
        try:
 
247
            os.remove(old_abspath)
 
248
        except OSError, e:
 
249
            if e.errno not in (errno.ENOENT,):
 
250
                warning('Failed to delete temporary file: %s %s',
 
251
                        old_abspath, e)
 
252
        try:
 
253
            os.remove(new_abspath)
 
254
        except OSError:
 
255
            if e.errno not in (errno.ENOENT,):
 
256
                warning('Failed to delete temporary file: %s %s',
 
257
                        new_abspath, e)
 
258
 
 
259
 
 
260
def diff_cmd_helper(tree, specific_files, external_diff_options, 
 
261
                    old_revision_spec=None, new_revision_spec=None,
 
262
                    revision_specs=None,
 
263
                    old_label='a/', new_label='b/'):
 
264
    """Helper for cmd_diff.
 
265
 
 
266
    :param tree:
 
267
        A WorkingTree
 
268
 
 
269
    :param specific_files:
 
270
        The specific files to compare, or None
 
271
 
 
272
    :param external_diff_options:
 
273
        If non-None, run an external diff, and pass it these options
 
274
 
 
275
    :param old_revision_spec:
 
276
        If None, use basis tree as old revision, otherwise use the tree for
 
277
        the specified revision. 
 
278
 
 
279
    :param new_revision_spec:
 
280
        If None, use working tree as new revision, otherwise use the tree for
 
281
        the specified revision.
 
282
    
 
283
    :param revision_specs: 
 
284
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
 
285
        to compare.  This can be passed as an alternative to the old_revision_spec 
 
286
        and new_revision_spec parameters.
 
287
 
 
288
    The more general form is show_diff_trees(), where the caller
 
289
    supplies any two trees.
 
290
    """
 
291
 
 
292
    # TODO: perhaps remove the old parameters old_revision_spec and
 
293
    # new_revision_spec, since this is only really for use from cmd_diff and
 
294
    # it now always passes through a sequence of revision_specs -- mbp
 
295
    # 20061221
 
296
 
 
297
    def spec_tree(spec):
 
298
        if tree:
 
299
            revision = spec.in_store(tree.branch)
 
300
        else:
 
301
            revision = spec.in_store(None)
 
302
        revision_id = revision.rev_id
 
303
        branch = revision.branch
 
304
        return branch.repository.revision_tree(revision_id)
 
305
 
 
306
    if revision_specs is not None:
 
307
        assert (old_revision_spec is None
 
308
                and new_revision_spec is None)
 
309
        if len(revision_specs) > 0:
 
310
            old_revision_spec = revision_specs[0]
 
311
        if len(revision_specs) > 1:
 
312
            new_revision_spec = revision_specs[1]
 
313
 
 
314
    if old_revision_spec is None:
 
315
        old_tree = tree.basis_tree()
 
316
    else:
 
317
        old_tree = spec_tree(old_revision_spec)
 
318
 
 
319
    if (new_revision_spec is None
 
320
        or new_revision_spec.spec is None):
 
321
        new_tree = tree
 
322
    else:
 
323
        new_tree = spec_tree(new_revision_spec)
 
324
 
 
325
    if new_tree is not tree:
 
326
        extra_trees = (tree,)
 
327
    else:
 
328
        extra_trees = None
 
329
 
 
330
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
 
331
                           external_diff_options,
 
332
                           old_label=old_label, new_label=new_label,
 
333
                           extra_trees=extra_trees)
 
334
 
 
335
 
 
336
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
 
337
                    external_diff_options=None,
 
338
                    old_label='a/', new_label='b/',
 
339
                    extra_trees=None):
 
340
    """Show in text form the changes from one tree to another.
 
341
 
 
342
    to_files
 
343
        If set, include only changes to these files.
 
344
 
 
345
    external_diff_options
 
346
        If set, use an external GNU diff and pass these options.
 
347
 
 
348
    extra_trees
 
349
        If set, more Trees to use for looking up file ids
 
350
    """
 
351
    old_tree.lock_read()
 
352
    try:
 
353
        if extra_trees is not None:
 
354
            for tree in extra_trees:
 
355
                tree.lock_read()
 
356
        new_tree.lock_read()
 
357
        try:
 
358
            return _show_diff_trees(old_tree, new_tree, to_file,
 
359
                                    specific_files, external_diff_options,
 
360
                                    old_label=old_label, new_label=new_label,
 
361
                                    extra_trees=extra_trees)
 
362
        finally:
 
363
            new_tree.unlock()
 
364
            if extra_trees is not None:
 
365
                for tree in extra_trees:
 
366
                    tree.unlock()
 
367
    finally:
 
368
        old_tree.unlock()
 
369
 
 
370
 
 
371
def _show_diff_trees(old_tree, new_tree, to_file,
 
372
                     specific_files, external_diff_options, 
 
373
                     old_label='a/', new_label='b/', extra_trees=None):
 
374
 
 
375
    # GNU Patch uses the epoch date to detect files that are being added
 
376
    # or removed in a diff.
 
377
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
378
 
 
379
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
380
    # be usefully made into a much faster special case.
 
381
 
 
382
    if external_diff_options:
 
383
        assert isinstance(external_diff_options, basestring)
 
384
        opts = external_diff_options.split()
 
385
        def diff_file(olab, olines, nlab, nlines, to_file):
 
386
            external_diff(olab, olines, nlab, nlines, to_file, opts)
 
387
    else:
 
388
        diff_file = internal_diff
 
389
    
 
390
    delta = new_tree.changes_from(old_tree,
 
391
        specific_files=specific_files,
 
392
        extra_trees=extra_trees, require_versioned=True)
 
393
 
 
394
    has_changes = 0
 
395
    for path, file_id, kind in delta.removed:
 
396
        has_changes = 1
 
397
        print >>to_file, "=== removed %s '%s'" % (kind, path.encode('utf8'))
 
398
        old_name = '%s%s\t%s' % (old_label, path,
 
399
                                 _patch_header_date(old_tree, file_id, path))
 
400
        new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
 
401
        old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
 
402
                                         new_name, None, None, to_file)
 
403
    for path, file_id, kind in delta.added:
 
404
        has_changes = 1
 
405
        print >>to_file, "=== added %s '%s'" % (kind, path.encode('utf8'))
 
406
        old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
 
407
        new_name = '%s%s\t%s' % (new_label, path,
 
408
                                 _patch_header_date(new_tree, file_id, path))
 
409
        new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
 
410
                                         old_name, None, None, to_file, 
 
411
                                         reverse=True)
 
412
    for (old_path, new_path, file_id, kind,
 
413
         text_modified, meta_modified) in delta.renamed:
 
414
        has_changes = 1
 
415
        prop_str = get_prop_change(meta_modified)
 
416
        print >>to_file, "=== renamed %s '%s' => %r%s" % (
 
417
                    kind, old_path.encode('utf8'),
 
418
                    new_path.encode('utf8'), prop_str)
 
419
        old_name = '%s%s\t%s' % (old_label, old_path,
 
420
                                 _patch_header_date(old_tree, file_id,
 
421
                                                    old_path))
 
422
        new_name = '%s%s\t%s' % (new_label, new_path,
 
423
                                 _patch_header_date(new_tree, file_id,
 
424
                                                    new_path))
 
425
        _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
426
                                    new_name, new_tree,
 
427
                                    text_modified, kind, to_file, diff_file)
 
428
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
 
429
        has_changes = 1
 
430
        prop_str = get_prop_change(meta_modified)
 
431
        print >>to_file, "=== modified %s '%s'%s" % (kind, path.encode('utf8'),
 
432
                                                     prop_str)
 
433
        # The file may be in a different location in the old tree (because
 
434
        # the containing dir was renamed, but the file itself was not)
 
435
        old_path = old_tree.id2path(file_id)
 
436
        old_name = '%s%s\t%s' % (old_label, old_path,
 
437
                                 _patch_header_date(old_tree, file_id, old_path))
 
438
        new_name = '%s%s\t%s' % (new_label, path,
 
439
                                 _patch_header_date(new_tree, file_id, path))
 
440
        if text_modified:
 
441
            _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
442
                                        new_name, new_tree,
 
443
                                        True, kind, to_file, diff_file)
 
444
 
 
445
    return has_changes
 
446
 
 
447
 
 
448
def _patch_header_date(tree, file_id, path):
 
449
    """Returns a timestamp suitable for use in a patch header."""
 
450
    mtime = tree.get_file_mtime(file_id, path)
 
451
    assert mtime is not None, \
 
452
        "got an mtime of None for file-id %s, path %s in tree %s" % (
 
453
                file_id, path, tree)
 
454
    return timestamp.format_patch_date(mtime)
 
455
 
 
456
 
 
457
def _raise_if_nonexistent(paths, old_tree, new_tree):
 
458
    """Complain if paths are not in either inventory or tree.
 
459
 
 
460
    It's OK with the files exist in either tree's inventory, or 
 
461
    if they exist in the tree but are not versioned.
 
462
    
 
463
    This can be used by operations such as bzr status that can accept
 
464
    unknown or ignored files.
 
465
    """
 
466
    mutter("check paths: %r", paths)
 
467
    if not paths:
 
468
        return
 
469
    s = old_tree.filter_unversioned_files(paths)
 
470
    s = new_tree.filter_unversioned_files(s)
 
471
    s = [path for path in s if not new_tree.has_filename(path)]
 
472
    if s:
 
473
        raise errors.PathsDoNotExist(sorted(s))
 
474
 
 
475
 
 
476
def get_prop_change(meta_modified):
 
477
    if meta_modified:
 
478
        return " (properties changed)"
 
479
    else:
 
480
        return  ""
 
481
 
 
482
 
 
483
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
 
484
                                new_path, new_tree, text_modified,
 
485
                                kind, to_file, diff_file):
 
486
    if text_modified:
 
487
        new_entry = new_tree.inventory[file_id]
 
488
        old_tree.inventory[file_id].diff(diff_file,
 
489
                                         old_path, old_tree,
 
490
                                         new_path, new_entry, 
 
491
                                         new_tree, to_file)