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.symbol_versioning import *
21
from bzrlib.trace import mutter
39
from bzrlib.symbol_versioning import (
43
from bzrlib.trace import mutter, warning
23
46
# TODO: Rather than building a changeset object, we should probably
24
47
# invoke callbacks on an object. That object can either accumulate a
25
48
# list, write them out directly, etc etc.
27
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file):
50
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
51
allow_binary=False, sequence_matcher=None,
52
path_encoding='utf8'):
30
53
# FIXME: difflib is wrong if there is no trailing newline.
31
54
# The syntax used by patch seems to be "\ No newline at
32
55
# end of file" following the last diff line from that
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))
68
141
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
70
143
"""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
144
# make sure our own output is properly ordered before the diff
80
from tempfile import NamedTemporaryFile
83
oldtmpf = NamedTemporaryFile()
84
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')
87
153
# TODO: perhaps a special case for comparing to or from the empty
130
198
diffcmd.extend(diff_opts)
132
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
200
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
201
out,err = pipe.communicate()
134
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
135
239
# returns 1 if files differ; that's OK
137
241
msg = 'signal %d' % (-rc)
139
243
msg = 'exit code %d' % rc
141
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
245
raise errors.BzrError('external diff failed with %s; command: %r'
143
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',
147
269
@deprecated_function(zero_eight)
186
307
def diff_cmd_helper(tree, specific_files, external_diff_options,
187
old_revision_spec=None, new_revision_spec=None):
308
old_revision_spec=None, new_revision_spec=None,
310
old_label='a/', new_label='b/'):
188
311
"""Helper for cmd_diff.
316
:param specific_files:
194
317
The specific files to compare, or None
196
external_diff_options
319
:param external_diff_options:
197
320
If non-None, run an external diff, and pass it these options
322
:param old_revision_spec:
200
323
If None, use basis tree as old revision, otherwise use the tree for
201
324
the specified revision.
326
:param new_revision_spec:
204
327
If None, use working tree as new revision, otherwise use the tree for
205
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.
207
335
The more general form is show_diff_trees(), where the caller
208
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
212
344
def spec_tree(spec):
213
revision_id = spec.in_store(tree.branch).rev_id
214
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]
215
361
if old_revision_spec is None:
216
362
old_tree = tree.basis_tree()
218
364
old_tree = spec_tree(old_revision_spec)
220
if new_revision_spec is None:
366
if (new_revision_spec is None
367
or new_revision_spec.spec is None):
223
370
new_tree = spec_tree(new_revision_spec)
372
if new_tree is not tree:
373
extra_trees = (tree,)
225
377
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
226
external_diff_options)
378
external_diff_options,
379
old_label=old_label, new_label=new_label,
380
extra_trees=extra_trees)
229
383
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
230
external_diff_options=None):
384
external_diff_options=None,
385
old_label='a/', new_label='b/',
231
387
"""Show in text form the changes from one tree to another.
236
392
external_diff_options
237
393
If set, use an external GNU diff and pass these options.
396
If set, more Trees to use for looking up file ids
239
398
old_tree.lock_read()
400
if extra_trees is not None:
401
for tree in extra_trees:
241
403
new_tree.lock_read()
243
405
return _show_diff_trees(old_tree, new_tree, to_file,
244
specific_files, external_diff_options)
406
specific_files, external_diff_options,
407
old_label=old_label, new_label=new_label,
408
extra_trees=extra_trees)
246
410
new_tree.unlock()
411
if extra_trees is not None:
412
for tree in extra_trees:
248
415
old_tree.unlock()
251
418
def _show_diff_trees(old_tree, new_tree, to_file,
252
specific_files, external_diff_options):
254
# TODO: Options to control putting on a prefix or suffix, perhaps
255
# as a format string?
259
DEVNULL = '/dev/null'
260
# Windows users, don't panic about this filename -- it is a
261
# special signal to GNU patch that the file should be created or
262
# deleted respectively.
419
specific_files, external_diff_options,
420
old_label='a/', new_label='b/', extra_trees=None):
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'
264
426
# TODO: Generation of pseudo-diffs for added/deleted files could
265
427
# be usefully made into a much faster special case.
267
_raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
269
429
if external_diff_options:
270
430
assert isinstance(external_diff_options, basestring)
271
431
opts = external_diff_options.split()
275
435
diff_file = internal_diff
277
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
278
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)
281
442
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)
444
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
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)
286
450
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,
452
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
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,
292
459
for (old_path, new_path, file_id, kind,
293
460
text_modified, meta_modified) in delta.renamed:
295
462
prop_str = get_prop_change(meta_modified)
296
463
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,
464
kind, old_path.encode('utf8'),
465
new_path.encode('utf8'), prop_str)
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,
300
474
text_modified, kind, to_file, diff_file)
301
475
for path, file_id, kind, text_modified, meta_modified in delta.modified:
303
477
prop_str = get_prop_change(meta_modified)
304
print >>to_file, '=== modified %s %r%s' % (kind, old_label + path,
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))
306
483
if text_modified:
307
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
308
new_label, path, new_tree,
484
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
309
486
True, kind, to_file, diff_file)
311
488
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:
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)
497
def _raise_if_nonexistent(paths, old_tree, new_tree):
498
"""Complain if paths are not in either inventory or tree.
500
It's OK with the files exist in either tree's inventory, or
501
if they exist in the tree but are not versioned.
503
This can be used by operations such as bzr status that can accept
504
unknown or ignored files.
506
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))
509
s = old_tree.filter_unversioned_files(paths)
510
s = new_tree.filter_unversioned_files(s)
511
s = [path for path in s if not new_tree.has_filename(path)]
513
raise errors.PathsDoNotExist(sorted(s))
325
516
def get_prop_change(meta_modified):
326
517
if meta_modified: