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
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
25
# compatability - plugins import compare_trees from diff!!!
26
# deprecated as of 0.10
27
from bzrlib.delta import compare_trees
28
from bzrlib.errors import BzrError
29
import bzrlib.errors as errors
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
38
from bzrlib.symbol_versioning import (
36
41
from bzrlib.trace import mutter, warning
40
45
# invoke callbacks on an object. That object can either accumulate a
41
46
# list, write them out directly, etc etc.
49
class _PrematchedMatcher(difflib.SequenceMatcher):
50
"""Allow SequenceMatcher operations to use predetermined blocks"""
52
def __init__(self, matching_blocks):
53
difflib.SequenceMatcher(self, None, None)
54
self.matching_blocks = matching_blocks
43
58
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
44
59
allow_binary=False, sequence_matcher=None,
45
60
path_encoding='utf8'):
62
77
if allow_binary is False:
63
check_text_lines(oldlines)
64
check_text_lines(newlines)
78
textfile.check_text_lines(oldlines)
79
textfile.check_text_lines(newlines)
66
81
if sequence_matcher is None:
67
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
68
ud = unified_diff(oldlines, newlines,
82
sequence_matcher = patiencediff.PatienceSequenceMatcher
83
ud = patiencediff.unified_diff(oldlines, newlines,
69
84
fromfile=old_filename.encode(path_encoding),
70
85
tofile=new_filename.encode(path_encoding),
71
86
sequencematcher=sequence_matcher)
85
100
to_file.write(line)
86
101
if not line.endswith('\n'):
87
102
to_file.write("\n\\ No newline at end of file\n")
92
"""Set the env var LANG=C"""
93
os.environ['LANG'] = 'C'
96
106
def _spawn_external_diff(diffcmd, capture_errors=True):
97
107
"""Spawn the externall diff process, and return the child handle.
99
109
: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
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.
103
113
:return: A Popen object.
105
115
if capture_errors:
106
preexec_fn = _set_lang_C
116
# construct minimal environment
118
path = os.environ.get('PATH')
121
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
107
124
stderr = subprocess.PIPE
192
209
# 'diff' gives retcode == 2 for all sorts of errors
193
210
# one of those is 'Binary files differ'.
194
211
# Bad options could also be the problem.
195
# 'Binary files' is not a real error, so we suppress that error
212
# 'Binary files' is not a real error, so we suppress that error.
198
215
# Since we got here, we want to make sure to give an i18n error
202
219
# Write out the new i18n diff response
203
220
to_file.write(out+'\n')
204
221
if pipe.returncode != 2:
205
raise BzrError('external diff failed with exit code 2'
206
' when run with LANG=C, but not when run'
207
' natively: %r' % (diffcmd,))
222
raise errors.BzrError(
223
'external diff failed with exit code 2'
224
' when run with LANG=C and LC_ALL=C,'
225
' but not when run natively: %r' % (diffcmd,))
209
227
first_line = lang_c_out.split('\n', 1)[0]
210
m = re.match('^binary files.*differ$', first_line, re.I)
228
# Starting with diffutils 2.8.4 the word "binary" was dropped.
229
m = re.match('^(binary )?files.*differ$', first_line, re.I)
212
raise BzrError('external diff failed with exit code 2;'
213
' command: %r' % (diffcmd,))
231
raise errors.BzrError('external diff failed with exit code 2;'
232
' command: %r' % (diffcmd,))
215
234
# Binary files differ, just return
252
@deprecated_function(zero_eight)
253
def show_diff(b, from_spec, specific_files, external_diff_options=None,
254
revision2=None, output=None, b2=None):
255
"""Shortcut for showing the diff to the working tree.
257
Please use show_diff_trees instead.
263
None for 'basis tree', or otherwise the old revision to compare against.
265
The more general form is show_diff_trees(), where the caller
266
supplies any two trees.
271
if from_spec is None:
272
old_tree = b.bzrdir.open_workingtree()
274
old_tree = old_tree = old_tree.basis_tree()
276
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
278
if revision2 is None:
280
new_tree = b.bzrdir.open_workingtree()
282
new_tree = b2.bzrdir.open_workingtree()
284
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
286
return show_diff_trees(old_tree, new_tree, output, specific_files,
287
external_diff_options)
290
271
def diff_cmd_helper(tree, specific_files, external_diff_options,
291
272
old_revision_spec=None, new_revision_spec=None,
292
274
old_label='a/', new_label='b/'):
293
275
"""Helper for cmd_diff.
280
:param specific_files:
299
281
The specific files to compare, or None
301
external_diff_options
283
:param external_diff_options:
302
284
If non-None, run an external diff, and pass it these options
286
:param old_revision_spec:
305
287
If None, use basis tree as old revision, otherwise use the tree for
306
288
the specified revision.
290
:param new_revision_spec:
309
291
If None, use working tree as new revision, otherwise use the tree for
310
292
the specified revision.
294
:param revision_specs:
295
Zero, one or two RevisionSpecs from the command line, saying what revisions
296
to compare. This can be passed as an alternative to the old_revision_spec
297
and new_revision_spec parameters.
312
299
The more general form is show_diff_trees(), where the caller
313
300
supplies any two trees.
303
# TODO: perhaps remove the old parameters old_revision_spec and
304
# new_revision_spec, since this is only really for use from cmd_diff and
305
# it now always passes through a sequence of revision_specs -- mbp
315
308
def spec_tree(spec):
317
310
revision = spec.in_store(tree.branch)
320
313
revision_id = revision.rev_id
321
314
branch = revision.branch
322
315
return branch.repository.revision_tree(revision_id)
317
if revision_specs is not None:
318
assert (old_revision_spec is None
319
and new_revision_spec is None)
320
if len(revision_specs) > 0:
321
old_revision_spec = revision_specs[0]
322
if len(revision_specs) > 1:
323
new_revision_spec = revision_specs[1]
323
325
if old_revision_spec is None:
324
326
old_tree = tree.basis_tree()
326
328
old_tree = spec_tree(old_revision_spec)
328
if new_revision_spec is None:
330
if (new_revision_spec is None
331
or new_revision_spec.spec is None):
331
334
new_tree = spec_tree(new_revision_spec)
332
336
if new_tree is not tree:
333
337
extra_trees = (tree,)
343
347
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
344
348
external_diff_options=None,
345
349
old_label='a/', new_label='b/',
351
path_encoding='utf8'):
347
352
"""Show in text form the changes from one tree to another.
356
361
If set, more Trees to use for looking up file ids
364
If set, the path will be encoded as specified, otherwise is supposed
358
367
old_tree.lock_read()
369
if extra_trees is not None:
370
for tree in extra_trees:
360
372
new_tree.lock_read()
362
374
return _show_diff_trees(old_tree, new_tree, to_file,
363
375
specific_files, external_diff_options,
364
376
old_label=old_label, new_label=new_label,
365
extra_trees=extra_trees)
377
extra_trees=extra_trees,
378
path_encoding=path_encoding)
367
380
new_tree.unlock()
381
if extra_trees is not None:
382
for tree in extra_trees:
369
385
old_tree.unlock()
372
388
def _show_diff_trees(old_tree, new_tree, to_file,
373
specific_files, external_diff_options,
389
specific_files, external_diff_options, path_encoding,
374
390
old_label='a/', new_label='b/', extra_trees=None):
376
392
# GNU Patch uses the epoch date to detect files that are being added
396
412
for path, file_id, kind in delta.removed:
398
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
414
path_encoded = path.encode(path_encoding, "replace")
415
to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
399
416
old_name = '%s%s\t%s' % (old_label, path,
400
417
_patch_header_date(old_tree, file_id, path))
401
418
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
403
420
new_name, None, None, to_file)
404
421
for path, file_id, kind in delta.added:
406
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
423
path_encoded = path.encode(path_encoding, "replace")
424
to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
407
425
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
408
426
new_name = '%s%s\t%s' % (new_label, path,
409
427
_patch_header_date(new_tree, file_id, path))
414
432
text_modified, meta_modified) in delta.renamed:
416
434
prop_str = get_prop_change(meta_modified)
417
print >>to_file, '=== renamed %s %r => %r%s' % (
418
kind, old_path.encode('utf8'),
419
new_path.encode('utf8'), prop_str)
435
oldpath_encoded = old_path.encode(path_encoding, "replace")
436
newpath_encoded = new_path.encode(path_encoding, "replace")
437
to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
438
oldpath_encoded, newpath_encoded, prop_str))
420
439
old_name = '%s%s\t%s' % (old_label, old_path,
421
440
_patch_header_date(old_tree, file_id,
429
448
for path, file_id, kind, text_modified, meta_modified in delta.modified:
431
450
prop_str = get_prop_change(meta_modified)
432
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
433
old_name = '%s%s\t%s' % (old_label, path,
434
_patch_header_date(old_tree, file_id, path))
451
path_encoded = path.encode(path_encoding, "replace")
452
to_file.write("=== modified %s '%s'%s\n" % (kind,
453
path_encoded, prop_str))
454
# The file may be in a different location in the old tree (because
455
# the containing dir was renamed, but the file itself was not)
456
old_path = old_tree.id2path(file_id)
457
old_name = '%s%s\t%s' % (old_label, old_path,
458
_patch_header_date(old_tree, file_id, old_path))
435
459
new_name = '%s%s\t%s' % (new_label, path,
436
460
_patch_header_date(new_tree, file_id, path))
437
461
if text_modified:
445
469
def _patch_header_date(tree, file_id, path):
446
470
"""Returns a timestamp suitable for use in a patch header."""
447
tm = time.gmtime(tree.get_file_mtime(file_id, path))
448
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
471
mtime = tree.get_file_mtime(file_id, path)
472
assert mtime is not None, \
473
"got an mtime of None for file-id %s, path %s in tree %s" % (
475
return timestamp.format_patch_date(mtime)
451
478
def _raise_if_nonexistent(paths, old_tree, new_tree):