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
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
37
from bzrlib.symbol_versioning import (
40
from bzrlib.trace import mutter, warning
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.
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.
57
# In the meantime we at least make sure the patch isn't
61
# Special workaround for Python2.3, where difflib fails if
62
# both sequences are empty.
63
if not oldlines and not newlines:
66
if allow_binary is False:
67
textfile.check_text_lines(oldlines)
68
textfile.check_text_lines(newlines)
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)
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
81
ud[2] = ud[2].replace('-1,0', '-0,0')
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'
90
if not line.endswith('\n'):
91
to_file.write("\n\\ No newline at end of file\n")
95
def _spawn_external_diff(diffcmd, capture_errors=True):
96
"""Spawn the externall diff process, and return the child handle.
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.
105
# construct minimal environment
107
path = os.environ.get('PATH')
110
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
113
stderr = subprocess.PIPE
119
pipe = subprocess.Popen(diffcmd,
120
stdin=subprocess.PIPE,
121
stdout=subprocess.PIPE,
125
if e.errno == errno.ENOENT:
126
raise errors.NoDiff(str(e))
132
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
134
"""Display a diff by calling out to the external diff program."""
135
# make sure our own output is properly ordered before the diff
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')
144
# TODO: perhaps a special case for comparing to or from the empty
145
# sequence; can just use /dev/null on Unix
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.
151
oldtmpf.writelines(oldlines)
152
newtmpf.writelines(newlines)
160
'--label', old_filename,
162
'--label', new_filename,
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
172
for s in ['-c', '-u', '-C', '-U',
177
'-y', '--side-by-side',
189
diffcmd.extend(diff_opts)
191
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
192
out,err = pipe.communicate()
195
# internal_diff() adds a trailing newline, add one here for consistency
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.
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()
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,))
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)
220
raise errors.BzrError('external diff failed with exit code 2;'
221
' command: %r' % (diffcmd,))
223
# Binary files differ, just return
226
# If we got to here, we haven't written out the output of diff
230
# returns 1 if files differ; that's OK
232
msg = 'signal %d' % (-rc)
234
msg = 'exit code %d' % rc
236
raise errors.BzrError('external diff failed with %s; command: %r'
241
oldtmpf.close() # and delete
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)
247
os.remove(old_abspath)
249
if e.errno not in (errno.ENOENT,):
250
warning('Failed to delete temporary file: %s %s',
253
os.remove(new_abspath)
255
if e.errno not in (errno.ENOENT,):
256
warning('Failed to delete temporary file: %s %s',
260
def diff_cmd_helper(tree, specific_files, external_diff_options,
261
old_revision_spec=None, new_revision_spec=None,
263
old_label='a/', new_label='b/'):
264
"""Helper for cmd_diff.
269
:param specific_files:
270
The specific files to compare, or None
272
:param external_diff_options:
273
If non-None, run an external diff, and pass it these options
275
:param old_revision_spec:
276
If None, use basis tree as old revision, otherwise use the tree for
277
the specified revision.
279
:param new_revision_spec:
280
If None, use working tree as new revision, otherwise use the tree for
281
the specified revision.
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.
288
The more general form is show_diff_trees(), where the caller
289
supplies any two trees.
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
299
revision = spec.in_store(tree.branch)
301
revision = spec.in_store(None)
302
revision_id = revision.rev_id
303
branch = revision.branch
304
return branch.repository.revision_tree(revision_id)
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]
314
if old_revision_spec is None:
315
old_tree = tree.basis_tree()
317
old_tree = spec_tree(old_revision_spec)
319
if (new_revision_spec is None
320
or new_revision_spec.spec is None):
323
new_tree = spec_tree(new_revision_spec)
325
if new_tree is not tree:
326
extra_trees = (tree,)
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)
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/',
340
"""Show in text form the changes from one tree to another.
343
If set, include only changes to these files.
345
external_diff_options
346
If set, use an external GNU diff and pass these options.
349
If set, more Trees to use for looking up file ids
353
if extra_trees is not None:
354
for tree in extra_trees:
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)
364
if extra_trees is not None:
365
for tree in extra_trees:
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):
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'
379
# TODO: Generation of pseudo-diffs for added/deleted files could
380
# be usefully made into a much faster special case.
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)
388
diff_file = internal_diff
390
delta = new_tree.changes_from(old_tree,
391
specific_files=specific_files,
392
extra_trees=extra_trees, require_versioned=True)
395
for path, file_id, kind in delta.removed:
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:
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,
412
for (old_path, new_path, file_id, kind,
413
text_modified, meta_modified) in delta.renamed:
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,
422
new_name = '%s%s\t%s' % (new_label, new_path,
423
_patch_header_date(new_tree, file_id,
425
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
427
text_modified, kind, to_file, diff_file)
428
for path, file_id, kind, text_modified, meta_modified in delta.modified:
430
prop_str = get_prop_change(meta_modified)
431
print >>to_file, "=== modified %s '%s'%s" % (kind, path.encode('utf8'),
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))
441
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
443
True, kind, to_file, diff_file)
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" % (
454
return timestamp.format_patch_date(mtime)
457
def _raise_if_nonexistent(paths, old_tree, new_tree):
458
"""Complain if paths are not in either inventory or tree.
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.
463
This can be used by operations such as bzr status that can accept
464
unknown or ignored files.
466
mutter("check paths: %r", paths)
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)]
473
raise errors.PathsDoNotExist(sorted(s))
476
def get_prop_change(meta_modified):
478
return " (properties changed)"
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):
487
new_entry = new_tree.inventory[file_id]
488
old_tree.inventory[file_id].diff(diff_file,