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
25
# compatability - plugins import compare_trees from diff!!!
26
# deprecated as of 0.10
17
27
from bzrlib.delta import compare_trees
18
28
from bzrlib.errors import BzrError
19
29
import bzrlib.errors as errors
20
from bzrlib.symbol_versioning import *
21
from bzrlib.trace import mutter
31
from bzrlib.patiencediff import unified_diff
32
import bzrlib.patiencediff
33
from bzrlib.symbol_versioning import (deprecated_function,
35
from bzrlib.textfile import check_text_lines
36
from bzrlib.trace import mutter, warning
23
39
# TODO: Rather than building a changeset object, we should probably
24
40
# invoke callbacks on an object. That object can either accumulate a
25
41
# list, write them out directly, etc etc.
27
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file):
43
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
44
allow_binary=False, sequence_matcher=None,
45
path_encoding='utf8'):
30
46
# FIXME: difflib is wrong if there is no trailing newline.
31
47
# The syntax used by patch seems to be "\ No newline at
32
48
# end of file" following the last diff line from that
92
"""Set the env var LANG=C"""
93
os.environ['LANG'] = 'C'
96
def _spawn_external_diff(diffcmd, capture_errors=True):
97
"""Spawn the externall diff process, and return the child handle.
99
: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
103
:return: A Popen object.
106
if sys.platform == 'win32':
107
# Win32 doesn't support preexec_fn, but that is
108
# okay, because it doesn't support LANG either.
111
preexec_fn = _set_lang_C
112
stderr = subprocess.PIPE
118
pipe = subprocess.Popen(diffcmd,
119
stdin=subprocess.PIPE,
120
stdout=subprocess.PIPE,
122
preexec_fn=preexec_fn)
124
if e.errno == errno.ENOENT:
125
raise errors.NoDiff(str(e))
68
131
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
70
133
"""Display a diff by calling out to the external diff program."""
73
if to_file != sys.stdout:
74
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
77
134
# make sure our own output is properly ordered before the diff
80
from tempfile import NamedTemporaryFile
83
oldtmpf = NamedTemporaryFile()
84
newtmpf = NamedTemporaryFile()
137
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
138
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
139
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
140
newtmpf = os.fdopen(newtmp_fd, 'wb')
87
143
# TODO: perhaps a special case for comparing to or from the empty
130
188
diffcmd.extend(diff_opts)
132
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
190
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
191
out,err = pipe.communicate()
134
if rc != 0 and rc != 1:
194
# internal_diff() adds a trailing newline, add one here for consistency
197
# 'diff' gives retcode == 2 for all sorts of errors
198
# one of those is 'Binary files differ'.
199
# Bad options could also be the problem.
200
# 'Binary files' is not a real error, so we suppress that error.
203
# Since we got here, we want to make sure to give an i18n error
204
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
205
out, err = pipe.communicate()
207
# Write out the new i18n diff response
208
to_file.write(out+'\n')
209
if pipe.returncode != 2:
210
raise BzrError('external diff failed with exit code 2'
211
' when run with LANG=C, but not when run'
212
' natively: %r' % (diffcmd,))
214
first_line = lang_c_out.split('\n', 1)[0]
215
# Starting with diffutils 2.8.4 the word "binary" was dropped.
216
m = re.match('^(binary )?files.*differ$', first_line, re.I)
218
raise BzrError('external diff failed with exit code 2;'
219
' command: %r' % (diffcmd,))
221
# Binary files differ, just return
224
# If we got to here, we haven't written out the output of diff
135
228
# returns 1 if files differ; that's OK
137
230
msg = 'signal %d' % (-rc)
139
232
msg = 'exit code %d' % rc
141
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
234
raise BzrError('external diff failed with %s; command: %r'
143
239
oldtmpf.close() # and delete
241
# Clean up. Warn in case the files couldn't be deleted
242
# (in case windows still holds the file open, but not
243
# if the files have already been deleted)
245
os.remove(old_abspath)
247
if e.errno not in (errno.ENOENT,):
248
warning('Failed to delete temporary file: %s %s',
251
os.remove(new_abspath)
253
if e.errno not in (errno.ENOENT,):
254
warning('Failed to delete temporary file: %s %s',
147
258
@deprecated_function(zero_eight)
275
395
diff_file = internal_diff
277
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
278
specific_files=specific_files)
397
delta = new_tree.changes_from(old_tree,
398
specific_files=specific_files,
399
extra_trees=extra_trees, require_versioned=True)
281
402
for path, file_id, kind in delta.removed:
283
print >>to_file, '=== removed %s %r' % (kind, old_label + path)
284
old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
285
DEVNULL, None, None, to_file)
404
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
405
old_name = '%s%s\t%s' % (old_label, path,
406
_patch_header_date(old_tree, file_id, path))
407
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
408
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
409
new_name, None, None, to_file)
286
410
for path, file_id, kind in delta.added:
288
print >>to_file, '=== added %s %r' % (kind, new_label + path)
289
new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
290
DEVNULL, None, None, to_file,
412
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
413
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
414
new_name = '%s%s\t%s' % (new_label, path,
415
_patch_header_date(new_tree, file_id, path))
416
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
417
old_name, None, None, to_file,
292
419
for (old_path, new_path, file_id, kind,
293
420
text_modified, meta_modified) in delta.renamed:
295
422
prop_str = get_prop_change(meta_modified)
296
423
print >>to_file, '=== renamed %s %r => %r%s' % (
297
kind, old_label + old_path, new_label + new_path, prop_str)
298
_maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
299
new_label, new_path, new_tree,
424
kind, old_path.encode('utf8'),
425
new_path.encode('utf8'), prop_str)
426
old_name = '%s%s\t%s' % (old_label, old_path,
427
_patch_header_date(old_tree, file_id,
429
new_name = '%s%s\t%s' % (new_label, new_path,
430
_patch_header_date(new_tree, file_id,
432
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
300
434
text_modified, kind, to_file, diff_file)
301
435
for path, file_id, kind, text_modified, meta_modified in delta.modified:
303
437
prop_str = get_prop_change(meta_modified)
304
print >>to_file, '=== modified %s %r%s' % (kind, old_label + path,
438
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
439
old_name = '%s%s\t%s' % (old_label, path,
440
_patch_header_date(old_tree, file_id, path))
441
new_name = '%s%s\t%s' % (new_label, path,
442
_patch_header_date(new_tree, file_id, path))
306
443
if text_modified:
307
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
308
new_label, path, new_tree,
444
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
309
446
True, kind, to_file, diff_file)
311
448
return has_changes
314
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
315
"""Complain if paths are not versioned in either tree."""
316
if not specific_files:
451
def _patch_header_date(tree, file_id, path):
452
"""Returns a timestamp suitable for use in a patch header."""
453
tm = time.gmtime(tree.get_file_mtime(file_id, path))
454
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
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)
318
old_unversioned = old_tree.filter_unversioned_files(specific_files)
319
new_unversioned = new_tree.filter_unversioned_files(specific_files)
320
unversioned = old_unversioned.intersection(new_unversioned)
322
raise errors.PathsNotVersionedError(sorted(unversioned))
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))
325
476
def get_prop_change(meta_modified):
326
477
if meta_modified: