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(), """
37
# compatability - plugins import compare_trees from diff!!!
38
# deprecated as of 0.10
39
19
from bzrlib.delta import compare_trees
40
from bzrlib.symbol_versioning import (
44
from bzrlib.trace import mutter, warning
20
from bzrlib.errors import BzrError
21
import bzrlib.errors as errors
22
from bzrlib.patiencediff import unified_diff
23
import bzrlib.patiencediff
24
from bzrlib.symbol_versioning import *
25
from bzrlib.textfile import check_text_lines
26
from bzrlib.trace import mutter
47
29
# TODO: Rather than building a changeset object, we should probably
70
52
if allow_binary is False:
71
textfile.check_text_lines(oldlines)
72
textfile.check_text_lines(newlines)
53
check_text_lines(oldlines)
54
check_text_lines(newlines)
74
56
if sequence_matcher is None:
75
sequence_matcher = patiencediff.PatienceSequenceMatcher
76
ud = patiencediff.unified_diff(oldlines, newlines,
57
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
58
ud = unified_diff(oldlines, newlines,
77
59
fromfile=old_filename.encode(path_encoding),
78
60
tofile=new_filename.encode(path_encoding),
79
61
sequencematcher=sequence_matcher)
99
def _spawn_external_diff(diffcmd, capture_errors=True):
100
"""Spawn the externall diff process, and return the child handle.
102
:param diffcmd: The command list to spawn
103
:param capture_errors: Capture stderr as well as setting LANG=C
104
and LC_ALL=C. This lets us read and understand the output of diff,
105
and respond to any errors.
106
:return: A Popen object.
109
# construct minimal environment
111
path = os.environ.get('PATH')
114
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
117
stderr = subprocess.PIPE
123
pipe = subprocess.Popen(diffcmd,
124
stdin=subprocess.PIPE,
125
stdout=subprocess.PIPE,
129
if e.errno == errno.ENOENT:
130
raise errors.NoDiff(str(e))
136
81
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
138
83
"""Display a diff by calling out to the external diff program."""
86
if to_file != sys.stdout:
87
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
139
90
# make sure our own output is properly ordered before the diff
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
from tempfile import NamedTemporaryFile
96
oldtmpf = NamedTemporaryFile()
97
newtmpf = NamedTemporaryFile()
148
100
# TODO: perhaps a special case for comparing to or from the empty
193
143
diffcmd.extend(diff_opts)
195
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
out,err = pipe.communicate()
145
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
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 errors.BzrError(
216
'external diff failed with exit code 2'
217
' when run with LANG=C and LC_ALL=C,'
218
' but not when run natively: %r' % (diffcmd,))
220
first_line = lang_c_out.split('\n', 1)[0]
221
# Starting with diffutils 2.8.4 the word "binary" was dropped.
222
m = re.match('^(binary )?files.*differ$', first_line, re.I)
224
raise errors.BzrError('external diff failed with exit code 2;'
225
' command: %r' % (diffcmd,))
227
# Binary files differ, just return
230
# If we got to here, we haven't written out the output of diff
147
if rc != 0 and rc != 1:
234
148
# returns 1 if files differ; that's OK
236
150
msg = 'signal %d' % (-rc)
238
152
msg = 'exit code %d' % rc
240
raise errors.BzrError('external diff failed with %s; command: %r'
154
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
245
156
oldtmpf.close() # and delete
247
# Clean up. Warn in case the files couldn't be deleted
248
# (in case windows still holds the file open, but not
249
# if the files have already been deleted)
251
os.remove(old_abspath)
253
if e.errno not in (errno.ENOENT,):
254
warning('Failed to delete temporary file: %s %s',
257
os.remove(new_abspath)
259
if e.errno not in (errno.ENOENT,):
260
warning('Failed to delete temporary file: %s %s',
264
160
@deprecated_function(zero_eight)
302
199
def diff_cmd_helper(tree, specific_files, external_diff_options,
303
200
old_revision_spec=None, new_revision_spec=None,
305
201
old_label='a/', new_label='b/'):
306
202
"""Helper for cmd_diff.
311
:param specific_files:
312
208
The specific files to compare, or None
314
:param external_diff_options:
210
external_diff_options
315
211
If non-None, run an external diff, and pass it these options
317
:param old_revision_spec:
318
214
If None, use basis tree as old revision, otherwise use the tree for
319
215
the specified revision.
321
:param new_revision_spec:
322
218
If None, use working tree as new revision, otherwise use the tree for
323
219
the specified revision.
325
:param revision_specs:
326
Zero, one or two RevisionSpecs from the command line, saying what revisions
327
to compare. This can be passed as an alternative to the old_revision_spec
328
and new_revision_spec parameters.
330
221
The more general form is show_diff_trees(), where the caller
331
222
supplies any two trees.
334
# TODO: perhaps remove the old parameters old_revision_spec and
335
# new_revision_spec, since this is only really for use from cmd_diff and
336
# it now always passes through a sequence of revision_specs -- mbp
339
226
def spec_tree(spec):
341
revision = spec.in_store(tree.branch)
343
revision = spec.in_store(None)
344
revision_id = revision.rev_id
345
branch = revision.branch
346
return branch.repository.revision_tree(revision_id)
348
if revision_specs is not None:
349
assert (old_revision_spec is None
350
and new_revision_spec is None)
351
if len(revision_specs) > 0:
352
old_revision_spec = revision_specs[0]
353
if len(revision_specs) > 1:
354
new_revision_spec = revision_specs[1]
227
revision_id = spec.in_store(tree.branch).rev_id
228
return tree.branch.repository.revision_tree(revision_id)
356
229
if old_revision_spec is None:
357
230
old_tree = tree.basis_tree()
359
232
old_tree = spec_tree(old_revision_spec)
361
if (new_revision_spec is None
362
or new_revision_spec.spec is None):
234
if new_revision_spec is None:
365
237
new_tree = spec_tree(new_revision_spec)
367
if new_tree is not tree:
368
extra_trees = (tree,)
372
239
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
373
240
external_diff_options,
374
old_label=old_label, new_label=new_label,
375
extra_trees=extra_trees)
241
old_label=old_label, new_label=new_label)
378
244
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
379
245
external_diff_options=None,
380
old_label='a/', new_label='b/',
246
old_label='a/', new_label='b/'):
382
247
"""Show in text form the changes from one tree to another.
387
252
external_diff_options
388
253
If set, use an external GNU diff and pass these options.
391
If set, more Trees to use for looking up file ids
393
255
old_tree.lock_read()
395
if extra_trees is not None:
396
for tree in extra_trees:
398
257
new_tree.lock_read()
400
259
return _show_diff_trees(old_tree, new_tree, to_file,
401
260
specific_files, external_diff_options,
402
old_label=old_label, new_label=new_label,
403
extra_trees=extra_trees)
261
old_label=old_label, new_label=new_label)
405
263
new_tree.unlock()
406
if extra_trees is not None:
407
for tree in extra_trees:
410
265
old_tree.unlock()
413
268
def _show_diff_trees(old_tree, new_tree, to_file,
414
269
specific_files, external_diff_options,
415
old_label='a/', new_label='b/', extra_trees=None):
270
old_label='a/', new_label='b/' ):
417
272
# GNU Patch uses the epoch date to detect files that are being added
418
273
# or removed in a diff.
430
287
diff_file = internal_diff
432
delta = new_tree.changes_from(old_tree,
433
specific_files=specific_files,
434
extra_trees=extra_trees, require_versioned=True)
289
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
290
specific_files=specific_files)
437
293
for path, file_id, kind in delta.removed:
472
328
prop_str = get_prop_change(meta_modified)
473
329
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
474
# The file may be in a different location in the old tree (because
475
# the containing dir was renamed, but the file itself was not)
476
old_path = old_tree.id2path(file_id)
477
old_name = '%s%s\t%s' % (old_label, old_path,
478
_patch_header_date(old_tree, file_id, old_path))
330
old_name = '%s%s\t%s' % (old_label, path,
331
_patch_header_date(old_tree, file_id, path))
479
332
new_name = '%s%s\t%s' % (new_label, path,
480
333
_patch_header_date(new_tree, file_id, path))
481
334
if text_modified:
489
342
def _patch_header_date(tree, file_id, path):
490
343
"""Returns a timestamp suitable for use in a patch header."""
491
mtime = tree.get_file_mtime(file_id, path)
492
assert mtime is not None, \
493
"got an mtime of None for file-id %s, path %s in tree %s" % (
495
return timestamp.format_patch_date(mtime)
344
tm = time.gmtime(tree.get_file_mtime(file_id, path))
345
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
348
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
349
"""Complain if paths are not versioned in either tree."""
350
if not specific_files:
352
old_unversioned = old_tree.filter_unversioned_files(specific_files)
353
new_unversioned = new_tree.filter_unversioned_files(specific_files)
354
unversioned = old_unversioned.intersection(new_unversioned)
356
raise errors.PathsNotVersionedError(sorted(unversioned))
498
359
def _raise_if_nonexistent(paths, old_tree, new_tree):
499
360
"""Complain if paths are not in either inventory or tree.