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(), """
38
from bzrlib.symbol_versioning import (
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
41
36
from bzrlib.trace import mutter, warning
45
40
# invoke callbacks on an object. That object can either accumulate a
46
41
# 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
58
43
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
59
44
allow_binary=False, sequence_matcher=None,
60
45
path_encoding='utf8'):
77
62
if allow_binary is False:
78
textfile.check_text_lines(oldlines)
79
textfile.check_text_lines(newlines)
63
check_text_lines(oldlines)
64
check_text_lines(newlines)
81
66
if sequence_matcher is None:
82
sequence_matcher = patiencediff.PatienceSequenceMatcher
83
ud = patiencediff.unified_diff(oldlines, newlines,
67
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
68
ud = unified_diff(oldlines, newlines,
84
69
fromfile=old_filename.encode(path_encoding),
85
70
tofile=new_filename.encode(path_encoding),
86
71
sequencematcher=sequence_matcher)
100
85
to_file.write(line)
101
86
if not line.endswith('\n'):
102
87
to_file.write("\n\\ No newline at end of file\n")
92
"""Set the env var LANG=C"""
93
os.environ['LANG'] = 'C'
106
96
def _spawn_external_diff(diffcmd, capture_errors=True):
107
97
"""Spawn the externall diff process, and return the child handle.
109
99
: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.
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
113
103
:return: A Popen object.
115
105
if capture_errors:
116
# construct minimal environment
118
path = os.environ.get('PATH')
121
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
106
preexec_fn = _set_lang_C
124
107
stderr = subprocess.PIPE
209
192
# 'diff' gives retcode == 2 for all sorts of errors
210
193
# one of those is 'Binary files differ'.
211
194
# Bad options could also be the problem.
212
# 'Binary files' is not a real error, so we suppress that error.
195
# 'Binary files' is not a real error, so we suppress that error
215
198
# Since we got here, we want to make sure to give an i18n error
219
202
# Write out the new i18n diff response
220
203
to_file.write(out+'\n')
221
204
if pipe.returncode != 2:
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,))
205
raise BzrError('external diff failed with exit code 2'
206
' when run with LANG=C, but not when run'
207
' natively: %r' % (diffcmd,))
227
209
first_line = lang_c_out.split('\n', 1)[0]
228
# Starting with diffutils 2.8.4 the word "binary" was dropped.
229
m = re.match('^(binary )?files.*differ$', first_line, re.I)
210
m = re.match('^binary files.*differ$', first_line, re.I)
231
raise errors.BzrError('external diff failed with exit code 2;'
232
' command: %r' % (diffcmd,))
212
raise BzrError('external diff failed with exit code 2;'
213
' command: %r' % (diffcmd,))
234
215
# 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)
271
290
def diff_cmd_helper(tree, specific_files, external_diff_options,
272
291
old_revision_spec=None, new_revision_spec=None,
274
292
old_label='a/', new_label='b/'):
275
293
"""Helper for cmd_diff.
280
:param specific_files:
281
299
The specific files to compare, or None
283
:param external_diff_options:
301
external_diff_options
284
302
If non-None, run an external diff, and pass it these options
286
:param old_revision_spec:
287
305
If None, use basis tree as old revision, otherwise use the tree for
288
306
the specified revision.
290
:param new_revision_spec:
291
309
If None, use working tree as new revision, otherwise use the tree for
292
310
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.
299
312
The more general form is show_diff_trees(), where the caller
300
313
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
308
315
def spec_tree(spec):
310
317
revision = spec.in_store(tree.branch)
313
320
revision_id = revision.rev_id
314
321
branch = revision.branch
315
322
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]
325
323
if old_revision_spec is None:
326
324
old_tree = tree.basis_tree()
328
326
old_tree = spec_tree(old_revision_spec)
330
if (new_revision_spec is None
331
or new_revision_spec.spec is None):
328
if new_revision_spec is None:
334
331
new_tree = spec_tree(new_revision_spec)
336
332
if new_tree is not tree:
337
333
extra_trees = (tree,)
347
343
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
348
344
external_diff_options=None,
349
345
old_label='a/', new_label='b/',
351
path_encoding='utf8'):
352
347
"""Show in text form the changes from one tree to another.
361
356
If set, more Trees to use for looking up file ids
364
If set, the path will be encoded as specified, otherwise is supposed
367
358
old_tree.lock_read()
369
if extra_trees is not None:
370
for tree in extra_trees:
372
360
new_tree.lock_read()
374
362
return _show_diff_trees(old_tree, new_tree, to_file,
375
363
specific_files, external_diff_options,
376
364
old_label=old_label, new_label=new_label,
377
extra_trees=extra_trees,
378
path_encoding=path_encoding)
365
extra_trees=extra_trees)
380
367
new_tree.unlock()
381
if extra_trees is not None:
382
for tree in extra_trees:
385
369
old_tree.unlock()
388
372
def _show_diff_trees(old_tree, new_tree, to_file,
389
specific_files, external_diff_options, path_encoding,
373
specific_files, external_diff_options,
390
374
old_label='a/', new_label='b/', extra_trees=None):
392
376
# GNU Patch uses the epoch date to detect files that are being added
412
396
for path, file_id, kind in delta.removed:
414
path_encoded = path.encode(path_encoding, "replace")
415
to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
398
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
416
399
old_name = '%s%s\t%s' % (old_label, path,
417
400
_patch_header_date(old_tree, file_id, path))
418
401
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
420
403
new_name, None, None, to_file)
421
404
for path, file_id, kind in delta.added:
423
path_encoded = path.encode(path_encoding, "replace")
424
to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
406
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
425
407
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
426
408
new_name = '%s%s\t%s' % (new_label, path,
427
409
_patch_header_date(new_tree, file_id, path))
432
414
text_modified, meta_modified) in delta.renamed:
434
416
prop_str = get_prop_change(meta_modified)
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))
417
print >>to_file, '=== renamed %s %r => %r%s' % (
418
kind, old_path.encode('utf8'),
419
new_path.encode('utf8'), prop_str)
439
420
old_name = '%s%s\t%s' % (old_label, old_path,
440
421
_patch_header_date(old_tree, file_id,
448
429
for path, file_id, kind, text_modified, meta_modified in delta.modified:
450
431
prop_str = get_prop_change(meta_modified)
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))
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))
459
435
new_name = '%s%s\t%s' % (new_label, path,
460
436
_patch_header_date(new_tree, file_id, path))
461
437
if text_modified:
469
445
def _patch_header_date(tree, file_id, path):
470
446
"""Returns a timestamp suitable for use in a patch header."""
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)
447
tm = time.gmtime(tree.get_file_mtime(file_id, path))
448
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
478
451
def _raise_if_nonexistent(paths, old_tree, new_tree):