1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
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.
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.
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
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
38
# compatability - plugins import compare_trees from diff!!!
39
# deprecated as of 0.10
40
from bzrlib.delta import compare_trees
41
from bzrlib.symbol_versioning import (
45
from bzrlib.trace import mutter, warning
48
# TODO: Rather than building a changeset object, we should probably
49
# invoke callbacks on an object. That object can either accumulate a
50
# list, write them out directly, etc etc.
53
class _PrematchedMatcher(difflib.SequenceMatcher):
54
"""Allow SequenceMatcher operations to use predetermined blocks"""
56
def __init__(self, matching_blocks):
57
difflib.SequenceMatcher(self, None, None)
58
self.matching_blocks = matching_blocks
62
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
63
allow_binary=False, sequence_matcher=None,
64
path_encoding='utf8'):
65
# FIXME: difflib is wrong if there is no trailing newline.
66
# The syntax used by patch seems to be "\ No newline at
67
# end of file" following the last diff line from that
68
# file. This is not trivial to insert into the
69
# unified_diff output and it might be better to just fix
70
# or replace that function.
72
# In the meantime we at least make sure the patch isn't
76
# Special workaround for Python2.3, where difflib fails if
77
# both sequences are empty.
78
if not oldlines and not newlines:
81
if allow_binary is False:
82
textfile.check_text_lines(oldlines)
83
textfile.check_text_lines(newlines)
85
if sequence_matcher is None:
86
sequence_matcher = patiencediff.PatienceSequenceMatcher
87
ud = patiencediff.unified_diff(oldlines, newlines,
88
fromfile=old_filename.encode(path_encoding),
89
tofile=new_filename.encode(path_encoding),
90
sequencematcher=sequence_matcher)
93
# work-around for difflib being too smart for its own good
94
# if /dev/null is "1,0", patch won't recognize it as /dev/null
96
ud[2] = ud[2].replace('-1,0', '-0,0')
98
ud[2] = ud[2].replace('+1,0', '+0,0')
99
# work around for difflib emitting random spaces after the label
100
ud[0] = ud[0][:-2] + '\n'
101
ud[1] = ud[1][:-2] + '\n'
105
if not line.endswith('\n'):
106
to_file.write("\n\\ No newline at end of file\n")
110
def _spawn_external_diff(diffcmd, capture_errors=True):
111
"""Spawn the externall diff process, and return the child handle.
113
:param diffcmd: The command list to spawn
114
:param capture_errors: Capture stderr as well as setting LANG=C
115
and LC_ALL=C. This lets us read and understand the output of diff,
116
and respond to any errors.
117
:return: A Popen object.
120
# construct minimal environment
122
path = os.environ.get('PATH')
125
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
128
stderr = subprocess.PIPE
134
pipe = subprocess.Popen(diffcmd,
135
stdin=subprocess.PIPE,
136
stdout=subprocess.PIPE,
140
if e.errno == errno.ENOENT:
141
raise errors.NoDiff(str(e))
147
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
149
"""Display a diff by calling out to the external diff program."""
150
# make sure our own output is properly ordered before the diff
153
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
154
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
155
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
156
newtmpf = os.fdopen(newtmp_fd, 'wb')
159
# TODO: perhaps a special case for comparing to or from the empty
160
# sequence; can just use /dev/null on Unix
162
# TODO: if either of the files being compared already exists as a
163
# regular named file (e.g. in the working directory) then we can
164
# compare directly to that, rather than copying it.
166
oldtmpf.writelines(oldlines)
167
newtmpf.writelines(newlines)
175
'--label', old_filename,
177
'--label', new_filename,
182
# diff only allows one style to be specified; they don't override.
183
# note that some of these take optargs, and the optargs can be
184
# directly appended to the options.
185
# this is only an approximate parser; it doesn't properly understand
187
for s in ['-c', '-u', '-C', '-U',
192
'-y', '--side-by-side',
204
diffcmd.extend(diff_opts)
206
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
207
out,err = pipe.communicate()
210
# internal_diff() adds a trailing newline, add one here for consistency
213
# 'diff' gives retcode == 2 for all sorts of errors
214
# one of those is 'Binary files differ'.
215
# Bad options could also be the problem.
216
# 'Binary files' is not a real error, so we suppress that error.
219
# Since we got here, we want to make sure to give an i18n error
220
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
221
out, err = pipe.communicate()
223
# Write out the new i18n diff response
224
to_file.write(out+'\n')
225
if pipe.returncode != 2:
226
raise errors.BzrError(
227
'external diff failed with exit code 2'
228
' when run with LANG=C and LC_ALL=C,'
229
' but not when run natively: %r' % (diffcmd,))
231
first_line = lang_c_out.split('\n', 1)[0]
232
# Starting with diffutils 2.8.4 the word "binary" was dropped.
233
m = re.match('^(binary )?files.*differ$', first_line, re.I)
235
raise errors.BzrError('external diff failed with exit code 2;'
236
' command: %r' % (diffcmd,))
238
# Binary files differ, just return
241
# If we got to here, we haven't written out the output of diff
245
# returns 1 if files differ; that's OK
247
msg = 'signal %d' % (-rc)
249
msg = 'exit code %d' % rc
251
raise errors.BzrError('external diff failed with %s; command: %r'
256
oldtmpf.close() # and delete
258
# Clean up. Warn in case the files couldn't be deleted
259
# (in case windows still holds the file open, but not
260
# if the files have already been deleted)
262
os.remove(old_abspath)
264
if e.errno not in (errno.ENOENT,):
265
warning('Failed to delete temporary file: %s %s',
268
os.remove(new_abspath)
270
if e.errno not in (errno.ENOENT,):
271
warning('Failed to delete temporary file: %s %s',
275
@deprecated_function(zero_eight)
276
def show_diff(b, from_spec, specific_files, external_diff_options=None,
277
revision2=None, output=None, b2=None):
278
"""Shortcut for showing the diff to the working tree.
280
Please use show_diff_trees instead.
286
None for 'basis tree', or otherwise the old revision to compare against.
288
The more general form is show_diff_trees(), where the caller
289
supplies any two trees.
294
if from_spec is None:
295
old_tree = b.bzrdir.open_workingtree()
297
old_tree = old_tree = old_tree.basis_tree()
299
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
301
if revision2 is None:
303
new_tree = b.bzrdir.open_workingtree()
305
new_tree = b2.bzrdir.open_workingtree()
307
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
309
return show_diff_trees(old_tree, new_tree, output, specific_files,
310
external_diff_options)
313
def diff_cmd_helper(tree, specific_files, external_diff_options,
314
old_revision_spec=None, new_revision_spec=None,
316
old_label='a/', new_label='b/'):
317
"""Helper for cmd_diff.
322
:param specific_files:
323
The specific files to compare, or None
325
:param external_diff_options:
326
If non-None, run an external diff, and pass it these options
328
:param old_revision_spec:
329
If None, use basis tree as old revision, otherwise use the tree for
330
the specified revision.
332
:param new_revision_spec:
333
If None, use working tree as new revision, otherwise use the tree for
334
the specified revision.
336
:param revision_specs:
337
Zero, one or two RevisionSpecs from the command line, saying what revisions
338
to compare. This can be passed as an alternative to the old_revision_spec
339
and new_revision_spec parameters.
341
The more general form is show_diff_trees(), where the caller
342
supplies any two trees.
345
# TODO: perhaps remove the old parameters old_revision_spec and
346
# new_revision_spec, since this is only really for use from cmd_diff and
347
# it now always passes through a sequence of revision_specs -- mbp
352
revision = spec.in_store(tree.branch)
354
revision = spec.in_store(None)
355
revision_id = revision.rev_id
356
branch = revision.branch
357
return branch.repository.revision_tree(revision_id)
359
if revision_specs is not None:
360
assert (old_revision_spec is None
361
and new_revision_spec is None)
362
if len(revision_specs) > 0:
363
old_revision_spec = revision_specs[0]
364
if len(revision_specs) > 1:
365
new_revision_spec = revision_specs[1]
367
if old_revision_spec is None:
368
old_tree = tree.basis_tree()
370
old_tree = spec_tree(old_revision_spec)
372
if (new_revision_spec is None
373
or new_revision_spec.spec is None):
376
new_tree = spec_tree(new_revision_spec)
378
if new_tree is not tree:
379
extra_trees = (tree,)
383
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
384
external_diff_options,
385
old_label=old_label, new_label=new_label,
386
extra_trees=extra_trees)
389
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
390
external_diff_options=None,
391
old_label='a/', new_label='b/',
393
"""Show in text form the changes from one tree to another.
396
If set, include only changes to these files.
398
external_diff_options
399
If set, use an external GNU diff and pass these options.
402
If set, more Trees to use for looking up file ids
406
if extra_trees is not None:
407
for tree in extra_trees:
411
return _show_diff_trees(old_tree, new_tree, to_file,
412
specific_files, external_diff_options,
413
old_label=old_label, new_label=new_label,
414
extra_trees=extra_trees)
417
if extra_trees is not None:
418
for tree in extra_trees:
424
def _show_diff_trees(old_tree, new_tree, to_file,
425
specific_files, external_diff_options,
426
old_label='a/', new_label='b/', extra_trees=None):
428
# GNU Patch uses the epoch date to detect files that are being added
429
# or removed in a diff.
430
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
432
# TODO: Generation of pseudo-diffs for added/deleted files could
433
# be usefully made into a much faster special case.
435
if external_diff_options:
436
assert isinstance(external_diff_options, basestring)
437
opts = external_diff_options.split()
438
def diff_file(olab, olines, nlab, nlines, to_file):
439
external_diff(olab, olines, nlab, nlines, to_file, opts)
441
diff_file = internal_diff
443
delta = new_tree.changes_from(old_tree,
444
specific_files=specific_files,
445
extra_trees=extra_trees, require_versioned=True)
448
for path, file_id, kind in delta.removed:
450
print >>to_file, "=== removed %s '%s'" % (kind, path.encode('utf8'))
451
old_name = '%s%s\t%s' % (old_label, path,
452
_patch_header_date(old_tree, file_id, path))
453
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
454
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
455
new_name, None, None, to_file)
456
for path, file_id, kind in delta.added:
458
print >>to_file, "=== added %s '%s'" % (kind, path.encode('utf8'))
459
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
460
new_name = '%s%s\t%s' % (new_label, path,
461
_patch_header_date(new_tree, file_id, path))
462
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
463
old_name, None, None, to_file,
465
for (old_path, new_path, file_id, kind,
466
text_modified, meta_modified) in delta.renamed:
468
prop_str = get_prop_change(meta_modified)
469
print >>to_file, "=== renamed %s '%s' => %r%s" % (
470
kind, old_path.encode('utf8'),
471
new_path.encode('utf8'), prop_str)
472
old_name = '%s%s\t%s' % (old_label, old_path,
473
_patch_header_date(old_tree, file_id,
475
new_name = '%s%s\t%s' % (new_label, new_path,
476
_patch_header_date(new_tree, file_id,
478
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
480
text_modified, kind, to_file, diff_file)
481
for path, file_id, kind, text_modified, meta_modified in delta.modified:
483
prop_str = get_prop_change(meta_modified)
484
print >>to_file, "=== modified %s '%s'%s" % (kind, path.encode('utf8'),
486
# The file may be in a different location in the old tree (because
487
# the containing dir was renamed, but the file itself was not)
488
old_path = old_tree.id2path(file_id)
489
old_name = '%s%s\t%s' % (old_label, old_path,
490
_patch_header_date(old_tree, file_id, old_path))
491
new_name = '%s%s\t%s' % (new_label, path,
492
_patch_header_date(new_tree, file_id, path))
494
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
496
True, kind, to_file, diff_file)
501
def _patch_header_date(tree, file_id, path):
502
"""Returns a timestamp suitable for use in a patch header."""
503
mtime = tree.get_file_mtime(file_id, path)
504
assert mtime is not None, \
505
"got an mtime of None for file-id %s, path %s in tree %s" % (
507
return timestamp.format_patch_date(mtime)
510
def _raise_if_nonexistent(paths, old_tree, new_tree):
511
"""Complain if paths are not in either inventory or tree.
513
It's OK with the files exist in either tree's inventory, or
514
if they exist in the tree but are not versioned.
516
This can be used by operations such as bzr status that can accept
517
unknown or ignored files.
519
mutter("check paths: %r", paths)
522
s = old_tree.filter_unversioned_files(paths)
523
s = new_tree.filter_unversioned_files(s)
524
s = [path for path in s if not new_tree.has_filename(path)]
526
raise errors.PathsDoNotExist(sorted(s))
529
def get_prop_change(meta_modified):
531
return " (properties changed)"
536
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
537
new_path, new_tree, text_modified,
538
kind, to_file, diff_file):
540
new_entry = new_tree.inventory[file_id]
541
old_tree.inventory[file_id].diff(diff_file,