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
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
36
# compatability - plugins import compare_trees from diff!!!
37
# deprecated as of 0.10
17
38
from bzrlib.delta import compare_trees
18
from bzrlib.errors import BzrError
19
import bzrlib.errors as errors
20
from bzrlib.patiencediff import unified_diff
21
import bzrlib.patiencediff
22
from bzrlib.symbol_versioning import *
23
from bzrlib.textfile import check_text_lines
24
from bzrlib.trace import mutter
39
from bzrlib.symbol_versioning import (
43
from bzrlib.trace import mutter, warning
27
46
# TODO: Rather than building a changeset object, we should probably
50
69
if allow_binary is False:
51
check_text_lines(oldlines)
52
check_text_lines(newlines)
70
textfile.check_text_lines(oldlines)
71
textfile.check_text_lines(newlines)
54
73
if sequence_matcher is None:
55
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
56
ud = unified_diff(oldlines, newlines,
57
fromfile=old_filename.encode(path_encoding)+'\t',
58
tofile=new_filename.encode(path_encoding)+'\t',
74
sequence_matcher = patiencediff.PatienceSequenceMatcher
75
ud = patiencediff.unified_diff(oldlines, newlines,
76
fromfile=old_filename.encode(path_encoding),
77
tofile=new_filename.encode(path_encoding),
59
78
sequencematcher=sequence_matcher)
99
"""Set the env vars LANG=C and LC_ALL=C."""
100
osutils.set_or_unset_env('LANG', 'C')
101
osutils.set_or_unset_env('LC_ALL', 'C')
102
osutils.set_or_unset_env('LC_CTYPE', None)
103
osutils.set_or_unset_env('LANGUAGE', None)
106
def _spawn_external_diff(diffcmd, capture_errors=True):
107
"""Spawn the externall diff process, and return the child handle.
109
:param diffcmd: The command list to spawn
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.
113
:return: A Popen object.
116
if sys.platform == 'win32':
117
# Win32 doesn't support preexec_fn, but that is
118
# okay, because it doesn't support LANG and LC_ALL either.
121
preexec_fn = _set_lang_C
122
stderr = subprocess.PIPE
128
pipe = subprocess.Popen(diffcmd,
129
stdin=subprocess.PIPE,
130
stdout=subprocess.PIPE,
132
preexec_fn=preexec_fn)
134
if e.errno == errno.ENOENT:
135
raise errors.NoDiff(str(e))
79
141
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
81
143
"""Display a diff by calling out to the external diff program."""
84
if to_file != sys.stdout:
85
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
88
144
# make sure our own output is properly ordered before the diff
91
from tempfile import NamedTemporaryFile
94
oldtmpf = NamedTemporaryFile()
95
newtmpf = NamedTemporaryFile()
147
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
148
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
149
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
150
newtmpf = os.fdopen(newtmp_fd, 'wb')
98
153
# TODO: perhaps a special case for comparing to or from the empty
141
198
diffcmd.extend(diff_opts)
143
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
200
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
201
out,err = pipe.communicate()
145
if rc != 0 and rc != 1:
204
# internal_diff() adds a trailing newline, add one here for consistency
207
# 'diff' gives retcode == 2 for all sorts of errors
208
# one of those is 'Binary files differ'.
209
# Bad options could also be the problem.
210
# 'Binary files' is not a real error, so we suppress that error.
213
# Since we got here, we want to make sure to give an i18n error
214
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
215
out, err = pipe.communicate()
217
# Write out the new i18n diff response
218
to_file.write(out+'\n')
219
if pipe.returncode != 2:
220
raise errors.BzrError(
221
'external diff failed with exit code 2'
222
' when run with LANG=C and LC_ALL=C,'
223
' but not when run natively: %r' % (diffcmd,))
225
first_line = lang_c_out.split('\n', 1)[0]
226
# Starting with diffutils 2.8.4 the word "binary" was dropped.
227
m = re.match('^(binary )?files.*differ$', first_line, re.I)
229
raise errors.BzrError('external diff failed with exit code 2;'
230
' command: %r' % (diffcmd,))
232
# Binary files differ, just return
235
# If we got to here, we haven't written out the output of diff
146
239
# returns 1 if files differ; that's OK
148
241
msg = 'signal %d' % (-rc)
150
243
msg = 'exit code %d' % rc
152
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
245
raise errors.BzrError('external diff failed with %s; command: %r'
154
250
oldtmpf.close() # and delete
252
# Clean up. Warn in case the files couldn't be deleted
253
# (in case windows still holds the file open, but not
254
# if the files have already been deleted)
256
os.remove(old_abspath)
258
if e.errno not in (errno.ENOENT,):
259
warning('Failed to delete temporary file: %s %s',
262
os.remove(new_abspath)
264
if e.errno not in (errno.ENOENT,):
265
warning('Failed to delete temporary file: %s %s',
158
269
@deprecated_function(zero_eight)
197
307
def diff_cmd_helper(tree, specific_files, external_diff_options,
198
308
old_revision_spec=None, new_revision_spec=None,
199
310
old_label='a/', new_label='b/'):
200
311
"""Helper for cmd_diff.
316
:param specific_files:
206
317
The specific files to compare, or None
208
external_diff_options
319
:param external_diff_options:
209
320
If non-None, run an external diff, and pass it these options
322
:param old_revision_spec:
212
323
If None, use basis tree as old revision, otherwise use the tree for
213
324
the specified revision.
326
:param new_revision_spec:
216
327
If None, use working tree as new revision, otherwise use the tree for
217
328
the specified revision.
330
:param revision_specs:
331
Zero, one or two RevisionSpecs from the command line, saying what revisions
332
to compare. This can be passed as an alternative to the old_revision_spec
333
and new_revision_spec parameters.
219
335
The more general form is show_diff_trees(), where the caller
220
336
supplies any two trees.
339
# TODO: perhaps remove the old parameters old_revision_spec and
340
# new_revision_spec, since this is only really for use from cmd_diff and
341
# it now always passes through a sequence of revision_specs -- mbp
224
344
def spec_tree(spec):
225
revision_id = spec.in_store(tree.branch).rev_id
226
return tree.branch.repository.revision_tree(revision_id)
346
revision = spec.in_store(tree.branch)
348
revision = spec.in_store(None)
349
revision_id = revision.rev_id
350
branch = revision.branch
351
return branch.repository.revision_tree(revision_id)
353
if revision_specs is not None:
354
assert (old_revision_spec is None
355
and new_revision_spec is None)
356
if len(revision_specs) > 0:
357
old_revision_spec = revision_specs[0]
358
if len(revision_specs) > 1:
359
new_revision_spec = revision_specs[1]
227
361
if old_revision_spec is None:
228
362
old_tree = tree.basis_tree()
230
364
old_tree = spec_tree(old_revision_spec)
232
if new_revision_spec is None:
366
if (new_revision_spec is None
367
or new_revision_spec.spec is None):
235
370
new_tree = spec_tree(new_revision_spec)
372
if new_tree is not tree:
373
extra_trees = (tree,)
237
377
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
238
378
external_diff_options,
239
old_label=old_label, new_label=new_label)
379
old_label=old_label, new_label=new_label,
380
extra_trees=extra_trees)
242
383
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
243
384
external_diff_options=None,
244
old_label='a/', new_label='b/'):
385
old_label='a/', new_label='b/',
245
387
"""Show in text form the changes from one tree to another.
250
392
external_diff_options
251
393
If set, use an external GNU diff and pass these options.
396
If set, more Trees to use for looking up file ids
253
398
old_tree.lock_read()
400
if extra_trees is not None:
401
for tree in extra_trees:
255
403
new_tree.lock_read()
257
405
return _show_diff_trees(old_tree, new_tree, to_file,
258
406
specific_files, external_diff_options,
259
old_label=old_label, new_label=new_label)
407
old_label=old_label, new_label=new_label,
408
extra_trees=extra_trees)
261
410
new_tree.unlock()
411
if extra_trees is not None:
412
for tree in extra_trees:
263
415
old_tree.unlock()
266
418
def _show_diff_trees(old_tree, new_tree, to_file,
267
419
specific_files, external_diff_options,
268
old_label='a/', new_label='b/' ):
420
old_label='a/', new_label='b/', extra_trees=None):
270
DEVNULL = '/dev/null'
271
# Windows users, don't panic about this filename -- it is a
272
# special signal to GNU patch that the file should be created or
273
# deleted respectively.
422
# GNU Patch uses the epoch date to detect files that are being added
423
# or removed in a diff.
424
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
275
426
# TODO: Generation of pseudo-diffs for added/deleted files could
276
427
# be usefully made into a much faster special case.
278
_raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
280
429
if external_diff_options:
281
430
assert isinstance(external_diff_options, basestring)
282
431
opts = external_diff_options.split()
286
435
diff_file = internal_diff
288
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
289
specific_files=specific_files)
437
delta = new_tree.changes_from(old_tree,
438
specific_files=specific_files,
439
extra_trees=extra_trees, require_versioned=True)
292
442
for path, file_id, kind in delta.removed:
294
444
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
295
old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
296
DEVNULL, None, None, to_file)
445
old_name = '%s%s\t%s' % (old_label, path,
446
_patch_header_date(old_tree, file_id, path))
447
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
448
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
449
new_name, None, None, to_file)
297
450
for path, file_id, kind in delta.added:
299
452
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
300
new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
301
DEVNULL, None, None, to_file,
453
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
454
new_name = '%s%s\t%s' % (new_label, path,
455
_patch_header_date(new_tree, file_id, path))
456
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
457
old_name, None, None, to_file,
303
459
for (old_path, new_path, file_id, kind,
304
460
text_modified, meta_modified) in delta.renamed:
307
463
print >>to_file, '=== renamed %s %r => %r%s' % (
308
464
kind, old_path.encode('utf8'),
309
465
new_path.encode('utf8'), prop_str)
310
_maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
311
new_label, new_path, new_tree,
466
old_name = '%s%s\t%s' % (old_label, old_path,
467
_patch_header_date(old_tree, file_id,
469
new_name = '%s%s\t%s' % (new_label, new_path,
470
_patch_header_date(new_tree, file_id,
472
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
312
474
text_modified, kind, to_file, diff_file)
313
475
for path, file_id, kind, text_modified, meta_modified in delta.modified:
315
477
prop_str = get_prop_change(meta_modified)
316
478
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
479
old_name = '%s%s\t%s' % (old_label, path,
480
_patch_header_date(old_tree, file_id, path))
481
new_name = '%s%s\t%s' % (new_label, path,
482
_patch_header_date(new_tree, file_id, path))
317
483
if text_modified:
318
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
319
new_label, path, new_tree,
484
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
320
486
True, kind, to_file, diff_file)
322
488
return has_changes
325
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
326
"""Complain if paths are not versioned in either tree."""
327
if not specific_files:
329
old_unversioned = old_tree.filter_unversioned_files(specific_files)
330
new_unversioned = new_tree.filter_unversioned_files(specific_files)
331
unversioned = old_unversioned.intersection(new_unversioned)
333
raise errors.PathsNotVersionedError(sorted(unversioned))
491
def _patch_header_date(tree, file_id, path):
492
"""Returns a timestamp suitable for use in a patch header."""
493
tm = time.gmtime(tree.get_file_mtime(file_id, path))
494
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
336
497
def _raise_if_nonexistent(paths, old_tree, new_tree):
337
498
"""Complain if paths are not in either inventory or tree.
362
def _maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
363
new_label, new_path, new_tree, text_modified,
523
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
524
new_path, new_tree, text_modified,
364
525
kind, to_file, diff_file):
365
526
if text_modified:
366
527
new_entry = new_tree.inventory[file_id]
367
528
old_tree.inventory[file_id].diff(diff_file,
368
old_label + old_path, old_tree,
369
new_label + new_path, new_entry,
370
531
new_tree, to_file)