~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-10-24 12:49:17 UTC
  • mfrom: (2935.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20071024124917-xb75eckyxx6vkrlg
Makefile fixes - hooks.html generation & allow python to be overridden (Ian Clatworthy)

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import errno
 
17
import difflib
18
18
import os
19
19
import re
 
20
import sys
 
21
 
 
22
from bzrlib.lazy_import import lazy_import
 
23
lazy_import(globals(), """
 
24
import errno
20
25
import subprocess
21
 
import sys
22
26
import tempfile
23
27
import time
24
28
 
25
 
# compatability - plugins import compare_trees from diff!!!
26
 
# deprecated as of 0.10
27
 
from bzrlib.delta import compare_trees
28
 
from bzrlib.errors import BzrError
29
 
import bzrlib.errors as errors
30
 
import bzrlib.osutils
31
 
from bzrlib.patiencediff import unified_diff
32
 
import bzrlib.patiencediff
33
 
from bzrlib.symbol_versioning import (deprecated_function,
34
 
        zero_eight)
35
 
from bzrlib.textfile import check_text_lines
 
29
from bzrlib import (
 
30
    errors,
 
31
    osutils,
 
32
    patiencediff,
 
33
    textfile,
 
34
    timestamp,
 
35
    )
 
36
""")
 
37
 
 
38
from bzrlib.symbol_versioning import (
 
39
        deprecated_function,
 
40
        )
36
41
from bzrlib.trace import mutter, warning
37
42
 
38
43
 
40
45
# invoke callbacks on an object.  That object can either accumulate a
41
46
# list, write them out directly, etc etc.
42
47
 
 
48
 
 
49
class _PrematchedMatcher(difflib.SequenceMatcher):
 
50
    """Allow SequenceMatcher operations to use predetermined blocks"""
 
51
 
 
52
    def __init__(self, matching_blocks):
 
53
        difflib.SequenceMatcher(self, None, None)
 
54
        self.matching_blocks = matching_blocks
 
55
        self.opcodes = None
 
56
 
 
57
 
43
58
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
44
59
                  allow_binary=False, sequence_matcher=None,
45
60
                  path_encoding='utf8'):
60
75
        return
61
76
    
62
77
    if allow_binary is False:
63
 
        check_text_lines(oldlines)
64
 
        check_text_lines(newlines)
 
78
        textfile.check_text_lines(oldlines)
 
79
        textfile.check_text_lines(newlines)
65
80
 
66
81
    if sequence_matcher is None:
67
 
        sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
68
 
    ud = unified_diff(oldlines, newlines,
 
82
        sequence_matcher = patiencediff.PatienceSequenceMatcher
 
83
    ud = patiencediff.unified_diff(oldlines, newlines,
69
84
                      fromfile=old_filename.encode(path_encoding),
70
85
                      tofile=new_filename.encode(path_encoding),
71
86
                      sequencematcher=sequence_matcher)
85
100
        to_file.write(line)
86
101
        if not line.endswith('\n'):
87
102
            to_file.write("\n\\ No newline at end of file\n")
88
 
    print >>to_file
89
 
 
90
 
 
91
 
def _set_lang_C():
92
 
    """Set the env var LANG=C"""
93
 
    os.environ['LANG'] = 'C'
 
103
    to_file.write('\n')
94
104
 
95
105
 
96
106
def _spawn_external_diff(diffcmd, capture_errors=True):
97
107
    """Spawn the externall diff process, and return the child handle.
98
108
 
99
109
    :param diffcmd: The command list to spawn
100
 
    :param capture_errors: Capture stderr as well as setting LANG=C.
101
 
        This lets us read and understand the output of diff, and respond 
102
 
        to any errors.
 
110
    :param capture_errors: Capture stderr as well as setting LANG=C
 
111
        and LC_ALL=C. This lets us read and understand the output of diff,
 
112
        and respond to any errors.
103
113
    :return: A Popen object.
104
114
    """
105
115
    if capture_errors:
106
 
        preexec_fn = _set_lang_C
 
116
        # construct minimal environment
 
117
        env = {}
 
118
        path = os.environ.get('PATH')
 
119
        if path is not None:
 
120
            env['PATH'] = path
 
121
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
 
122
        env['LANG'] = 'C'
 
123
        env['LC_ALL'] = 'C'
107
124
        stderr = subprocess.PIPE
108
125
    else:
109
 
        preexec_fn = None
 
126
        env = None
110
127
        stderr = None
111
128
 
112
129
    try:
114
131
                                stdin=subprocess.PIPE,
115
132
                                stdout=subprocess.PIPE,
116
133
                                stderr=stderr,
117
 
                                preexec_fn=preexec_fn)
 
134
                                env=env)
118
135
    except OSError, e:
119
136
        if e.errno == errno.ENOENT:
120
137
            raise errors.NoDiff(str(e))
192
209
            # 'diff' gives retcode == 2 for all sorts of errors
193
210
            # one of those is 'Binary files differ'.
194
211
            # Bad options could also be the problem.
195
 
            # 'Binary files' is not a real error, so we suppress that error
 
212
            # 'Binary files' is not a real error, so we suppress that error.
196
213
            lang_c_out = out
197
214
 
198
215
            # Since we got here, we want to make sure to give an i18n error
202
219
            # Write out the new i18n diff response
203
220
            to_file.write(out+'\n')
204
221
            if pipe.returncode != 2:
205
 
                raise BzrError('external diff failed with exit code 2'
206
 
                               ' when run with LANG=C, but not when run'
207
 
                               ' natively: %r' % (diffcmd,))
 
222
                raise errors.BzrError(
 
223
                               'external diff failed with exit code 2'
 
224
                               ' when run with LANG=C and LC_ALL=C,'
 
225
                               ' but not when run natively: %r' % (diffcmd,))
208
226
 
209
227
            first_line = lang_c_out.split('\n', 1)[0]
210
 
            m = re.match('^binary files.*differ$', first_line, re.I)
 
228
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
 
229
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
211
230
            if m is None:
212
 
                raise BzrError('external diff failed with exit code 2;'
213
 
                               ' command: %r' % (diffcmd,))
 
231
                raise errors.BzrError('external diff failed with exit code 2;'
 
232
                                      ' command: %r' % (diffcmd,))
214
233
            else:
215
234
                # Binary files differ, just return
216
235
                return
225
244
            else:
226
245
                msg = 'exit code %d' % rc
227
246
                
228
 
            raise BzrError('external diff failed with %s; command: %r' 
229
 
                           % (rc, diffcmd))
 
247
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
248
                                  % (rc, diffcmd))
230
249
 
231
250
 
232
251
    finally:
249
268
                        new_abspath, e)
250
269
 
251
270
 
252
 
@deprecated_function(zero_eight)
253
 
def show_diff(b, from_spec, specific_files, external_diff_options=None,
254
 
              revision2=None, output=None, b2=None):
255
 
    """Shortcut for showing the diff to the working tree.
256
 
 
257
 
    Please use show_diff_trees instead.
258
 
 
259
 
    b
260
 
        Branch.
261
 
 
262
 
    revision
263
 
        None for 'basis tree', or otherwise the old revision to compare against.
264
 
    
265
 
    The more general form is show_diff_trees(), where the caller
266
 
    supplies any two trees.
267
 
    """
268
 
    if output is None:
269
 
        output = sys.stdout
270
 
 
271
 
    if from_spec is None:
272
 
        old_tree = b.bzrdir.open_workingtree()
273
 
        if b2 is None:
274
 
            old_tree = old_tree = old_tree.basis_tree()
275
 
    else:
276
 
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
277
 
 
278
 
    if revision2 is None:
279
 
        if b2 is None:
280
 
            new_tree = b.bzrdir.open_workingtree()
281
 
        else:
282
 
            new_tree = b2.bzrdir.open_workingtree()
283
 
    else:
284
 
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
285
 
 
286
 
    return show_diff_trees(old_tree, new_tree, output, specific_files,
287
 
                           external_diff_options)
288
 
 
289
 
 
290
271
def diff_cmd_helper(tree, specific_files, external_diff_options, 
291
272
                    old_revision_spec=None, new_revision_spec=None,
 
273
                    revision_specs=None,
292
274
                    old_label='a/', new_label='b/'):
293
275
    """Helper for cmd_diff.
294
276
 
295
 
   tree 
 
277
    :param tree:
296
278
        A WorkingTree
297
279
 
298
 
    specific_files
 
280
    :param specific_files:
299
281
        The specific files to compare, or None
300
282
 
301
 
    external_diff_options
 
283
    :param external_diff_options:
302
284
        If non-None, run an external diff, and pass it these options
303
285
 
304
 
    old_revision_spec
 
286
    :param old_revision_spec:
305
287
        If None, use basis tree as old revision, otherwise use the tree for
306
288
        the specified revision. 
307
289
 
308
 
    new_revision_spec
 
290
    :param new_revision_spec:
309
291
        If None, use working tree as new revision, otherwise use the tree for
310
292
        the specified revision.
311
293
    
 
294
    :param revision_specs: 
 
295
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
 
296
        to compare.  This can be passed as an alternative to the old_revision_spec 
 
297
        and new_revision_spec parameters.
 
298
 
312
299
    The more general form is show_diff_trees(), where the caller
313
300
    supplies any two trees.
314
301
    """
 
302
 
 
303
    # TODO: perhaps remove the old parameters old_revision_spec and
 
304
    # new_revision_spec, since this is only really for use from cmd_diff and
 
305
    # it now always passes through a sequence of revision_specs -- mbp
 
306
    # 20061221
 
307
 
315
308
    def spec_tree(spec):
316
309
        if tree:
317
310
            revision = spec.in_store(tree.branch)
320
313
        revision_id = revision.rev_id
321
314
        branch = revision.branch
322
315
        return branch.repository.revision_tree(revision_id)
 
316
 
 
317
    if revision_specs is not None:
 
318
        assert (old_revision_spec is None
 
319
                and new_revision_spec is None)
 
320
        if len(revision_specs) > 0:
 
321
            old_revision_spec = revision_specs[0]
 
322
        if len(revision_specs) > 1:
 
323
            new_revision_spec = revision_specs[1]
 
324
 
323
325
    if old_revision_spec is None:
324
326
        old_tree = tree.basis_tree()
325
327
    else:
326
328
        old_tree = spec_tree(old_revision_spec)
327
329
 
328
 
    if new_revision_spec is None:
 
330
    if (new_revision_spec is None
 
331
        or new_revision_spec.spec is None):
329
332
        new_tree = tree
330
333
    else:
331
334
        new_tree = spec_tree(new_revision_spec)
 
335
 
332
336
    if new_tree is not tree:
333
337
        extra_trees = (tree,)
334
338
    else:
343
347
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
344
348
                    external_diff_options=None,
345
349
                    old_label='a/', new_label='b/',
346
 
                    extra_trees=None):
 
350
                    extra_trees=None,
 
351
                    path_encoding='utf8'):
347
352
    """Show in text form the changes from one tree to another.
348
353
 
349
354
    to_files
354
359
 
355
360
    extra_trees
356
361
        If set, more Trees to use for looking up file ids
 
362
 
 
363
    path_encoding
 
364
        If set, the path will be encoded as specified, otherwise is supposed
 
365
        to be utf8
357
366
    """
358
367
    old_tree.lock_read()
359
368
    try:
 
369
        if extra_trees is not None:
 
370
            for tree in extra_trees:
 
371
                tree.lock_read()
360
372
        new_tree.lock_read()
361
373
        try:
362
374
            return _show_diff_trees(old_tree, new_tree, to_file,
363
375
                                    specific_files, external_diff_options,
364
376
                                    old_label=old_label, new_label=new_label,
365
 
                                    extra_trees=extra_trees)
 
377
                                    extra_trees=extra_trees,
 
378
                                    path_encoding=path_encoding)
366
379
        finally:
367
380
            new_tree.unlock()
 
381
            if extra_trees is not None:
 
382
                for tree in extra_trees:
 
383
                    tree.unlock()
368
384
    finally:
369
385
        old_tree.unlock()
370
386
 
371
387
 
372
388
def _show_diff_trees(old_tree, new_tree, to_file,
373
 
                     specific_files, external_diff_options, 
 
389
                     specific_files, external_diff_options, path_encoding,
374
390
                     old_label='a/', new_label='b/', extra_trees=None):
375
391
 
376
392
    # GNU Patch uses the epoch date to detect files that are being added
395
411
    has_changes = 0
396
412
    for path, file_id, kind in delta.removed:
397
413
        has_changes = 1
398
 
        print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
 
414
        path_encoded = path.encode(path_encoding, "replace")
 
415
        to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
399
416
        old_name = '%s%s\t%s' % (old_label, path,
400
417
                                 _patch_header_date(old_tree, file_id, path))
401
418
        new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
403
420
                                         new_name, None, None, to_file)
404
421
    for path, file_id, kind in delta.added:
405
422
        has_changes = 1
406
 
        print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
 
423
        path_encoded = path.encode(path_encoding, "replace")
 
424
        to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
407
425
        old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
408
426
        new_name = '%s%s\t%s' % (new_label, path,
409
427
                                 _patch_header_date(new_tree, file_id, path))
414
432
         text_modified, meta_modified) in delta.renamed:
415
433
        has_changes = 1
416
434
        prop_str = get_prop_change(meta_modified)
417
 
        print >>to_file, '=== renamed %s %r => %r%s' % (
418
 
                    kind, old_path.encode('utf8'),
419
 
                    new_path.encode('utf8'), prop_str)
 
435
        oldpath_encoded = old_path.encode(path_encoding, "replace")
 
436
        newpath_encoded = new_path.encode(path_encoding, "replace")
 
437
        to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
 
438
                            oldpath_encoded, newpath_encoded, prop_str))
420
439
        old_name = '%s%s\t%s' % (old_label, old_path,
421
440
                                 _patch_header_date(old_tree, file_id,
422
441
                                                    old_path))
429
448
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
430
449
        has_changes = 1
431
450
        prop_str = get_prop_change(meta_modified)
432
 
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
433
 
        old_name = '%s%s\t%s' % (old_label, path,
434
 
                                 _patch_header_date(old_tree, file_id, path))
 
451
        path_encoded = path.encode(path_encoding, "replace")
 
452
        to_file.write("=== modified %s '%s'%s\n" % (kind,
 
453
                            path_encoded, prop_str))
 
454
        # The file may be in a different location in the old tree (because
 
455
        # the containing dir was renamed, but the file itself was not)
 
456
        old_path = old_tree.id2path(file_id)
 
457
        old_name = '%s%s\t%s' % (old_label, old_path,
 
458
                                 _patch_header_date(old_tree, file_id, old_path))
435
459
        new_name = '%s%s\t%s' % (new_label, path,
436
460
                                 _patch_header_date(new_tree, file_id, path))
437
461
        if text_modified:
444
468
 
445
469
def _patch_header_date(tree, file_id, path):
446
470
    """Returns a timestamp suitable for use in a patch header."""
447
 
    tm = time.gmtime(tree.get_file_mtime(file_id, path))
448
 
    return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
 
471
    mtime = tree.get_file_mtime(file_id, path)
 
472
    assert mtime is not None, \
 
473
        "got an mtime of None for file-id %s, path %s in tree %s" % (
 
474
                file_id, path, tree)
 
475
    return timestamp.format_patch_date(mtime)
449
476
 
450
477
 
451
478
def _raise_if_nonexistent(paths, old_tree, new_tree):