/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
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
3
 
 
 
1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
 
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
6
5
# the Free Software Foundation; either version 2 of the License, or
7
6
# (at your option) any later version.
8
 
 
 
7
#
9
8
# This program is distributed in the hope that it will be useful,
10
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
11
# GNU General Public License for more details.
13
 
 
 
12
#
14
13
# You should have received a copy of the GNU General Public License
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 bzrlib.trace import mutter
19
 
from bzrlib.errors import BzrError
20
 
from bzrlib.delta import compare_trees
 
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
 
21
42
 
22
43
# TODO: Rather than building a changeset object, we should probably
23
44
# invoke callbacks on an object.  That object can either accumulate a
24
45
# list, write them out directly, etc etc.
25
46
 
26
 
def internal_diff(old_label, oldlines, new_label, newlines, to_file):
27
 
    import difflib
28
 
    
 
47
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
48
                  allow_binary=False, sequence_matcher=None,
 
49
                  path_encoding='utf8'):
29
50
    # FIXME: difflib is wrong if there is no trailing newline.
30
51
    # The syntax used by patch seems to be "\ No newline at
31
52
    # end of file" following the last diff line from that
41
62
    # both sequences are empty.
42
63
    if not oldlines and not newlines:
43
64
        return
44
 
 
45
 
    ud = difflib.unified_diff(oldlines, newlines,
46
 
                              fromfile=old_label, tofile=new_label)
47
 
 
 
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)
48
78
    # work-around for difflib being too smart for its own good
49
79
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
50
80
    if not oldlines:
51
 
        ud = list(ud)
52
81
        ud[2] = ud[2].replace('-1,0', '-0,0')
53
82
    elif not newlines:
54
 
        ud = list(ud)
55
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'
56
87
 
57
88
    for line in ud:
58
89
        to_file.write(line)
61
92
    print >>to_file
62
93
 
63
94
 
64
 
 
65
 
 
66
 
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
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,
67
133
                  diff_opts):
68
134
    """Display a diff by calling out to the external diff program."""
69
 
    import sys
70
 
    
71
 
    if to_file != sys.stdout:
72
 
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
73
 
                                  to_file)
74
 
 
75
135
    # make sure our own output is properly ordered before the diff
76
136
    to_file.flush()
77
137
 
78
 
    from tempfile import NamedTemporaryFile
79
 
    import os
80
 
 
81
 
    oldtmpf = NamedTemporaryFile()
82
 
    newtmpf = NamedTemporaryFile()
 
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')
83
142
 
84
143
    try:
85
144
        # TODO: perhaps a special case for comparing to or from the empty
92
151
        oldtmpf.writelines(oldlines)
93
152
        newtmpf.writelines(newlines)
94
153
 
95
 
        oldtmpf.flush()
96
 
        newtmpf.flush()
 
154
        oldtmpf.close()
 
155
        newtmpf.close()
97
156
 
98
157
        if not diff_opts:
99
158
            diff_opts = []
100
159
        diffcmd = ['diff',
101
 
                   '--label', old_label,
102
 
                   oldtmpf.name,
103
 
                   '--label', new_label,
104
 
                   newtmpf.name]
 
160
                   '--label', old_filename,
 
161
                   old_abspath,
 
162
                   '--label', new_filename,
 
163
                   new_abspath,
 
164
                   '--binary',
 
165
                  ]
105
166
 
106
167
        # diff only allows one style to be specified; they don't override.
107
168
        # note that some of these take optargs, and the optargs can be
127
188
        if diff_opts:
128
189
            diffcmd.extend(diff_opts)
129
190
 
130
 
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
 
191
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
 
192
        out,err = pipe.communicate()
 
193
        rc = pipe.returncode
131
194
        
132
 
        if rc != 0 and rc != 1:
 
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):
133
230
            # returns 1 if files differ; that's OK
134
231
            if rc < 0:
135
232
                msg = 'signal %d' % (-rc)
136
233
            else:
137
234
                msg = 'exit code %d' % rc
138
235
                
139
 
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
 
236
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
237
                                  % (rc, diffcmd))
 
238
 
 
239
 
140
240
    finally:
141
241
        oldtmpf.close()                 # and delete
142
242
        newtmpf.close()
143
 
    
144
 
 
145
 
 
146
 
def show_diff(b, revision, specific_files, external_diff_options=None):
147
 
    """Shortcut for showing the diff to the working tree.
148
 
 
149
 
    b
150
 
        Branch.
151
 
 
152
 
    revision
153
 
        None for each, or otherwise the old revision to compare against.
154
 
    
 
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
 
155
288
    The more general form is show_diff_trees(), where the caller
156
289
    supplies any two trees.
157
290
    """
158
 
    import sys
159
 
 
160
 
    if revision == None:
161
 
        old_tree = b.basis_tree()
162
 
    else:
163
 
        old_tree = b.revision_tree(b.lookup_revision(revision))
164
 
        
165
 
    new_tree = b.working_tree()
166
 
 
167
 
    show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
168
 
                    external_diff_options)
169
 
 
 
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)
170
334
 
171
335
 
172
336
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
173
 
                    external_diff_options=None):
 
337
                    external_diff_options=None,
 
338
                    old_label='a/', new_label='b/',
 
339
                    extra_trees=None):
174
340
    """Show in text form the changes from one tree to another.
175
341
 
176
342
    to_files
178
344
 
179
345
    external_diff_options
180
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
181
350
    """
182
 
 
183
 
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
184
 
    old_label = ''
185
 
    new_label = ''
186
 
 
187
 
    DEVNULL = '/dev/null'
188
 
    # Windows users, don't panic about this filename -- it is a
189
 
    # special signal to GNU patch that the file should be created or
190
 
    # deleted respectively.
 
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'
191
378
 
192
379
    # TODO: Generation of pseudo-diffs for added/deleted files could
193
380
    # be usefully made into a much faster special case.
200
387
    else:
201
388
        diff_file = internal_diff
202
389
    
203
 
 
204
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
205
 
                          specific_files=specific_files)
206
 
 
 
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
207
395
    for path, file_id, kind in delta.removed:
208
 
        print >>to_file, '*** removed %s %r' % (kind, path)
209
 
        if kind == 'file':
210
 
            diff_file(old_label + path,
211
 
                      old_tree.get_file(file_id).readlines(),
212
 
                      DEVNULL, 
213
 
                      [],
214
 
                      to_file)
215
 
 
 
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)
216
403
    for path, file_id, kind in delta.added:
217
 
        print >>to_file, '*** added %s %r' % (kind, path)
218
 
        if kind == 'file':
219
 
            diff_file(DEVNULL,
220
 
                      [],
221
 
                      new_label + path,
222
 
                      new_tree.get_file(file_id).readlines(),
223
 
                      to_file)
224
 
 
225
 
    for old_path, new_path, file_id, kind, text_modified in delta.renamed:
226
 
        print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
 
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))
227
440
        if text_modified:
228
 
            diff_file(old_label + old_path,
229
 
                      old_tree.get_file(file_id).readlines(),
230
 
                      new_label + new_path,
231
 
                      new_tree.get_file(file_id).readlines(),
232
 
                      to_file)
233
 
 
234
 
    for path, file_id, kind in delta.modified:
235
 
        print >>to_file, '*** modified %s %r' % (kind, path)
236
 
        if kind == 'file':
237
 
            diff_file(old_label + path,
238
 
                      old_tree.get_file(file_id).readlines(),
239
 
                      new_label + path,
240
 
                      new_tree.get_file(file_id).readlines(),
241
 
                      to_file)
242
 
 
243
 
 
244
 
 
245
 
 
246
 
 
 
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)