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
29
from bzrlib import (
29
# compatability - plugins import compare_trees from diff!!!
30
# deprecated as of 0.10
31
from bzrlib.delta import compare_trees
32
from bzrlib.errors import BzrError
33
from bzrlib.patiencediff import unified_diff
34
import bzrlib.patiencediff
35
from bzrlib.symbol_versioning import (deprecated_function,
37
from bzrlib.textfile import check_text_lines
38
from bzrlib.symbol_versioning import (
38
41
from bzrlib.trace import mutter, warning
42
45
# invoke callbacks on an object. That object can either accumulate a
43
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
45
58
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
46
59
allow_binary=False, sequence_matcher=None,
47
60
path_encoding='utf8'):
64
77
if allow_binary is False:
65
check_text_lines(oldlines)
66
check_text_lines(newlines)
78
textfile.check_text_lines(oldlines)
79
textfile.check_text_lines(newlines)
68
81
if sequence_matcher is None:
69
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
70
ud = unified_diff(oldlines, newlines,
82
sequence_matcher = patiencediff.PatienceSequenceMatcher
83
ud = patiencediff.unified_diff(oldlines, newlines,
71
84
fromfile=old_filename.encode(path_encoding),
72
85
tofile=new_filename.encode(path_encoding),
73
86
sequencematcher=sequence_matcher)
87
100
to_file.write(line)
88
101
if not line.endswith('\n'):
89
102
to_file.write("\n\\ No newline at end of file\n")
94
"""Set the env var LANG=C"""
95
osutils.set_or_unset_env('LANG', 'C')
96
osutils.set_or_unset_env('LC_ALL', None)
97
osutils.set_or_unset_env('LC_CTYPE', None)
98
osutils.set_or_unset_env('LANGUAGE', None)
101
106
def _spawn_external_diff(diffcmd, capture_errors=True):
102
107
"""Spawn the externall diff process, and return the child handle.
104
109
:param diffcmd: The command list to spawn
105
:param capture_errors: Capture stderr as well as setting LANG=C.
106
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.
108
113
:return: A Popen object.
110
115
if capture_errors:
111
if sys.platform == 'win32':
112
# Win32 doesn't support preexec_fn, but that is
113
# okay, because it doesn't support LANG either.
116
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
117
124
stderr = subprocess.PIPE
212
219
# Write out the new i18n diff response
213
220
to_file.write(out+'\n')
214
221
if pipe.returncode != 2:
215
raise BzrError('external diff failed with exit code 2'
216
' when run with LANG=C, but not when run'
217
' 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,))
219
227
first_line = lang_c_out.split('\n', 1)[0]
220
228
# Starting with diffutils 2.8.4 the word "binary" was dropped.
221
229
m = re.match('^(binary )?files.*differ$', first_line, re.I)
223
raise BzrError('external diff failed with exit code 2;'
224
' command: %r' % (diffcmd,))
231
raise errors.BzrError('external diff failed with exit code 2;'
232
' command: %r' % (diffcmd,))
226
234
# Binary files differ, just return
263
@deprecated_function(zero_eight)
264
def show_diff(b, from_spec, specific_files, external_diff_options=None,
265
revision2=None, output=None, b2=None):
266
"""Shortcut for showing the diff to the working tree.
268
Please use show_diff_trees instead.
274
None for 'basis tree', or otherwise the old revision to compare against.
276
The more general form is show_diff_trees(), where the caller
277
supplies any two trees.
282
if from_spec is None:
283
old_tree = b.bzrdir.open_workingtree()
285
old_tree = old_tree = old_tree.basis_tree()
287
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
289
if revision2 is None:
291
new_tree = b.bzrdir.open_workingtree()
293
new_tree = b2.bzrdir.open_workingtree()
295
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
297
return show_diff_trees(old_tree, new_tree, output, specific_files,
298
external_diff_options)
301
271
def diff_cmd_helper(tree, specific_files, external_diff_options,
302
272
old_revision_spec=None, new_revision_spec=None,
303
274
old_label='a/', new_label='b/'):
304
275
"""Helper for cmd_diff.
280
:param specific_files:
310
281
The specific files to compare, or None
312
external_diff_options
283
:param external_diff_options:
313
284
If non-None, run an external diff, and pass it these options
286
:param old_revision_spec:
316
287
If None, use basis tree as old revision, otherwise use the tree for
317
288
the specified revision.
290
:param new_revision_spec:
320
291
If None, use working tree as new revision, otherwise use the tree for
321
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.
323
299
The more general form is show_diff_trees(), where the caller
324
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
326
308
def spec_tree(spec):
328
310
revision = spec.in_store(tree.branch)
331
313
revision_id = revision.rev_id
332
314
branch = revision.branch
333
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]
334
325
if old_revision_spec is None:
335
326
old_tree = tree.basis_tree()
337
328
old_tree = spec_tree(old_revision_spec)
339
if new_revision_spec is None:
330
if (new_revision_spec is None
331
or new_revision_spec.spec is None):
342
334
new_tree = spec_tree(new_revision_spec)
343
336
if new_tree is not tree:
344
337
extra_trees = (tree,)
354
347
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
355
348
external_diff_options=None,
356
349
old_label='a/', new_label='b/',
351
path_encoding='utf8'):
358
352
"""Show in text form the changes from one tree to another.
367
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
369
367
old_tree.lock_read()
369
if extra_trees is not None:
370
for tree in extra_trees:
371
372
new_tree.lock_read()
373
374
return _show_diff_trees(old_tree, new_tree, to_file,
374
375
specific_files, external_diff_options,
375
376
old_label=old_label, new_label=new_label,
376
extra_trees=extra_trees)
377
extra_trees=extra_trees,
378
path_encoding=path_encoding)
378
380
new_tree.unlock()
381
if extra_trees is not None:
382
for tree in extra_trees:
380
385
old_tree.unlock()
383
388
def _show_diff_trees(old_tree, new_tree, to_file,
384
specific_files, external_diff_options,
389
specific_files, external_diff_options, path_encoding,
385
390
old_label='a/', new_label='b/', extra_trees=None):
387
392
# GNU Patch uses the epoch date to detect files that are being added
407
412
for path, file_id, kind in delta.removed:
409
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))
410
416
old_name = '%s%s\t%s' % (old_label, path,
411
417
_patch_header_date(old_tree, file_id, path))
412
418
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
414
420
new_name, None, None, to_file)
415
421
for path, file_id, kind in delta.added:
417
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))
418
425
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
419
426
new_name = '%s%s\t%s' % (new_label, path,
420
427
_patch_header_date(new_tree, file_id, path))
425
432
text_modified, meta_modified) in delta.renamed:
427
434
prop_str = get_prop_change(meta_modified)
428
print >>to_file, '=== renamed %s %r => %r%s' % (
429
kind, old_path.encode('utf8'),
430
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))
431
439
old_name = '%s%s\t%s' % (old_label, old_path,
432
440
_patch_header_date(old_tree, file_id,
440
448
for path, file_id, kind, text_modified, meta_modified in delta.modified:
442
450
prop_str = get_prop_change(meta_modified)
443
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
444
old_name = '%s%s\t%s' % (old_label, path,
445
_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))
446
459
new_name = '%s%s\t%s' % (new_label, path,
447
460
_patch_header_date(new_tree, file_id, path))
448
461
if text_modified:
456
469
def _patch_header_date(tree, file_id, path):
457
470
"""Returns a timestamp suitable for use in a patch header."""
458
tm = time.gmtime(tree.get_file_mtime(file_id, path))
459
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)
462
478
def _raise_if_nonexistent(paths, old_tree, new_tree):