1
1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
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
29
# compatability - plugins import compare_trees from diff!!!
30
# deprecated as of 0.10
17
31
from bzrlib.delta import compare_trees
18
32
from bzrlib.errors import BzrError
19
import bzrlib.errors as errors
20
from bzrlib.symbol_versioning import *
33
from bzrlib.patiencediff import unified_diff
34
import bzrlib.patiencediff
35
from bzrlib.symbol_versioning import (deprecated_function,
21
37
from bzrlib.textfile import check_text_lines
22
from bzrlib.trace import mutter
38
from bzrlib.trace import mutter, warning
24
41
# TODO: Rather than building a changeset object, we should probably
25
42
# invoke callbacks on an object. That object can either accumulate a
26
43
# list, write them out directly, etc etc.
28
45
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
46
allow_binary=False, sequence_matcher=None,
47
path_encoding='utf8'):
32
48
# FIXME: difflib is wrong if there is no trailing newline.
33
49
# The syntax used by patch seems to be "\ No newline at
34
50
# end of file" following the last diff line from that
94
"""Set the env var LANG=C"""
95
osutils.set_or_unset_env('LANG', 'C')
96
osutils.set_or_unset_env('LC_ALL', None)
97
osutils.set_or_unset_env('LC_CTYPE', None)
98
osutils.set_or_unset_env('LANGUAGE', None)
101
def _spawn_external_diff(diffcmd, capture_errors=True):
102
"""Spawn the externall diff process, and return the child handle.
104
:param diffcmd: The command list to spawn
105
:param capture_errors: Capture stderr as well as setting LANG=C.
106
This lets us read and understand the output of diff, and respond
108
:return: A Popen object.
111
if sys.platform == 'win32':
112
# Win32 doesn't support preexec_fn, but that is
113
# okay, because it doesn't support LANG either.
116
preexec_fn = _set_lang_C
117
stderr = subprocess.PIPE
123
pipe = subprocess.Popen(diffcmd,
124
stdin=subprocess.PIPE,
125
stdout=subprocess.PIPE,
127
preexec_fn=preexec_fn)
129
if e.errno == errno.ENOENT:
130
raise errors.NoDiff(str(e))
74
136
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
76
138
"""Display a diff by calling out to the external diff program."""
79
if to_file != sys.stdout:
80
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
83
139
# make sure our own output is properly ordered before the diff
86
from tempfile import NamedTemporaryFile
89
oldtmpf = NamedTemporaryFile()
90
newtmpf = NamedTemporaryFile()
142
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
143
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
144
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
145
newtmpf = os.fdopen(newtmp_fd, 'wb')
93
148
# TODO: perhaps a special case for comparing to or from the empty
136
193
diffcmd.extend(diff_opts)
138
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
195
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
out,err = pipe.communicate()
140
if rc != 0 and rc != 1:
199
# internal_diff() adds a trailing newline, add one here for consistency
202
# 'diff' gives retcode == 2 for all sorts of errors
203
# one of those is 'Binary files differ'.
204
# Bad options could also be the problem.
205
# 'Binary files' is not a real error, so we suppress that error.
208
# Since we got here, we want to make sure to give an i18n error
209
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
210
out, err = pipe.communicate()
212
# Write out the new i18n diff response
213
to_file.write(out+'\n')
214
if pipe.returncode != 2:
215
raise BzrError('external diff failed with exit code 2'
216
' when run with LANG=C, but not when run'
217
' natively: %r' % (diffcmd,))
219
first_line = lang_c_out.split('\n', 1)[0]
220
# Starting with diffutils 2.8.4 the word "binary" was dropped.
221
m = re.match('^(binary )?files.*differ$', first_line, re.I)
223
raise BzrError('external diff failed with exit code 2;'
224
' command: %r' % (diffcmd,))
226
# Binary files differ, just return
229
# If we got to here, we haven't written out the output of diff
141
233
# returns 1 if files differ; that's OK
143
235
msg = 'signal %d' % (-rc)
145
237
msg = 'exit code %d' % rc
147
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
239
raise BzrError('external diff failed with %s; command: %r'
149
244
oldtmpf.close() # and delete
246
# Clean up. Warn in case the files couldn't be deleted
247
# (in case windows still holds the file open, but not
248
# if the files have already been deleted)
250
os.remove(old_abspath)
252
if e.errno not in (errno.ENOENT,):
253
warning('Failed to delete temporary file: %s %s',
256
os.remove(new_abspath)
258
if e.errno not in (errno.ENOENT,):
259
warning('Failed to delete temporary file: %s %s',
153
263
@deprecated_function(zero_eight)
281
400
diff_file = internal_diff
283
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
284
specific_files=specific_files)
402
delta = new_tree.changes_from(old_tree,
403
specific_files=specific_files,
404
extra_trees=extra_trees, require_versioned=True)
287
407
for path, file_id, kind in delta.removed:
289
print >>to_file, '=== removed %s %r' % (kind, path)
290
old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
291
DEVNULL, None, None, to_file)
409
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
410
old_name = '%s%s\t%s' % (old_label, path,
411
_patch_header_date(old_tree, file_id, path))
412
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
413
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
414
new_name, None, None, to_file)
292
415
for path, file_id, kind in delta.added:
294
print >>to_file, '=== added %s %r' % (kind, path)
295
new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
296
DEVNULL, None, None, to_file,
417
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
418
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
419
new_name = '%s%s\t%s' % (new_label, path,
420
_patch_header_date(new_tree, file_id, path))
421
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
422
old_name, None, None, to_file,
298
424
for (old_path, new_path, file_id, kind,
299
425
text_modified, meta_modified) in delta.renamed:
301
427
prop_str = get_prop_change(meta_modified)
302
428
print >>to_file, '=== renamed %s %r => %r%s' % (
303
kind, old_path, new_path, prop_str)
304
_maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
305
new_label, new_path, new_tree,
429
kind, old_path.encode('utf8'),
430
new_path.encode('utf8'), prop_str)
431
old_name = '%s%s\t%s' % (old_label, old_path,
432
_patch_header_date(old_tree, file_id,
434
new_name = '%s%s\t%s' % (new_label, new_path,
435
_patch_header_date(new_tree, file_id,
437
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
306
439
text_modified, kind, to_file, diff_file)
307
440
for path, file_id, kind, text_modified, meta_modified in delta.modified:
309
442
prop_str = get_prop_change(meta_modified)
310
print >>to_file, '=== modified %s %r%s' % (kind, path, prop_str)
443
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
444
old_name = '%s%s\t%s' % (old_label, path,
445
_patch_header_date(old_tree, file_id, path))
446
new_name = '%s%s\t%s' % (new_label, path,
447
_patch_header_date(new_tree, file_id, path))
311
448
if text_modified:
312
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
313
new_label, path, new_tree,
449
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
314
451
True, kind, to_file, diff_file)
316
453
return has_changes
319
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
320
"""Complain if paths are not versioned in either tree."""
321
if not specific_files:
323
old_unversioned = old_tree.filter_unversioned_files(specific_files)
324
new_unversioned = new_tree.filter_unversioned_files(specific_files)
325
unversioned = old_unversioned.intersection(new_unversioned)
327
raise errors.PathsNotVersionedError(sorted(unversioned))
456
def _patch_header_date(tree, file_id, path):
457
"""Returns a timestamp suitable for use in a patch header."""
458
tm = time.gmtime(tree.get_file_mtime(file_id, path))
459
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
330
462
def _raise_if_nonexistent(paths, old_tree, new_tree):
331
463
"""Complain if paths are not in either inventory or tree.