1
# Copyright (C) 2005-2014 Canonical Ltd.
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
25
from .lazy_import import lazy_import
26
lazy_import(globals(), """
40
from breezy.workingtree import WorkingTree
41
from breezy.i18n import gettext
47
from .registry import (
50
from .trace import mutter, note, warning
51
from .tree import FileTimestampUnavailable
54
DEFAULT_CONTEXT_AMOUNT = 3
57
# TODO: Rather than building a changeset object, we should probably
58
# invoke callbacks on an object. That object can either accumulate a
59
# list, write them out directly, etc etc.
62
class _PrematchedMatcher(difflib.SequenceMatcher):
63
"""Allow SequenceMatcher operations to use predetermined blocks"""
65
def __init__(self, matching_blocks):
66
difflib.SequenceMatcher(self, None, None)
67
self.matching_blocks = matching_blocks
71
def internal_diff(old_label, oldlines, new_label, newlines, to_file,
72
allow_binary=False, sequence_matcher=None,
73
path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
74
# FIXME: difflib is wrong if there is no trailing newline.
75
# The syntax used by patch seems to be "\ No newline at
76
# end of file" following the last diff line from that
77
# file. This is not trivial to insert into the
78
# unified_diff output and it might be better to just fix
79
# or replace that function.
81
# In the meantime we at least make sure the patch isn't
84
if allow_binary is False:
85
textfile.check_text_lines(oldlines)
86
textfile.check_text_lines(newlines)
88
if sequence_matcher is None:
89
sequence_matcher = patiencediff.PatienceSequenceMatcher
90
ud = unified_diff_bytes(
92
fromfile=old_label.encode(path_encoding, 'replace'),
93
tofile=new_label.encode(path_encoding, 'replace'),
94
n=context_lines, sequencematcher=sequence_matcher)
97
if len(ud) == 0: # Identical contents, nothing to do
99
# work-around for difflib being too smart for its own good
100
# if /dev/null is "1,0", patch won't recognize it as /dev/null
102
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
104
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
108
if not line.endswith(b'\n'):
109
to_file.write(b"\n\\ No newline at end of file\n")
113
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
114
tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
116
Compare two sequences of lines; generate the delta as a unified diff.
118
Unified diffs are a compact way of showing line changes and a few
119
lines of context. The number of context lines is set by 'n' which
122
By default, the diff control lines (those with ---, +++, or @@) are
123
created with a trailing newline. This is helpful so that inputs
124
created from file.readlines() result in diffs that are suitable for
125
file.writelines() since both the inputs and outputs have trailing
128
For inputs that do not have trailing newlines, set the lineterm
129
argument to "" so that the output will be uniformly newline free.
131
The unidiff format normally has a header for filenames and modification
132
times. Any or all of these may be specified using strings for
133
'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification
134
times are normally expressed in the format returned by time.ctime().
138
>>> for line in bytes_unified_diff(b'one two three four'.split(),
139
... b'zero one tree four'.split(), b'Original', b'Current',
140
... b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
143
--- Original Sat Jan 26 23:30:50 1991
144
+++ Current Fri Jun 06 10:20:52 2003
153
if sequencematcher is None:
154
sequencematcher = difflib.SequenceMatcher
157
fromfiledate = b'\t' + bytes(fromfiledate)
159
tofiledate = b'\t' + bytes(tofiledate)
162
for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
164
yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
165
yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
167
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
168
yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
169
for tag, i1, i2, j1, j2 in group:
171
for line in a[i1:i2]:
174
if tag == 'replace' or tag == 'delete':
175
for line in a[i1:i2]:
177
if tag == 'replace' or tag == 'insert':
178
for line in b[j1:j2]:
182
def _spawn_external_diff(diffcmd, capture_errors=True):
183
"""Spawn the external diff process, and return the child handle.
185
:param diffcmd: The command list to spawn
186
:param capture_errors: Capture stderr as well as setting LANG=C
187
and LC_ALL=C. This lets us read and understand the output of diff,
188
and respond to any errors.
189
:return: A Popen object.
192
# construct minimal environment
194
path = os.environ.get('PATH')
197
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
200
stderr = subprocess.PIPE
206
pipe = subprocess.Popen(diffcmd,
207
stdin=subprocess.PIPE,
208
stdout=subprocess.PIPE,
212
if e.errno == errno.ENOENT:
213
raise errors.NoDiff(str(e))
219
# diff style options as of GNU diff v3.2
220
style_option_list = ['-c', '-C', '--context',
222
'-f', '--forward-ed',
226
'-u', '-U', '--unified',
227
'-y', '--side-by-side',
231
def default_style_unified(diff_opts):
232
"""Default to unified diff style if alternative not specified in diff_opts.
234
diff only allows one style to be specified; they don't override.
235
Note that some of these take optargs, and the optargs can be
236
directly appended to the options.
237
This is only an approximate parser; it doesn't properly understand
240
:param diff_opts: List of options for external (GNU) diff.
241
:return: List of options with default style=='unified'.
243
for s in style_option_list:
251
diff_opts.append('-u')
255
def external_diff(old_label, oldlines, new_label, newlines, to_file,
257
"""Display a diff by calling out to the external diff program."""
258
# make sure our own output is properly ordered before the diff
261
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
262
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='brz-diff-new-')
263
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
264
newtmpf = os.fdopen(newtmp_fd, 'wb')
267
# TODO: perhaps a special case for comparing to or from the empty
268
# sequence; can just use /dev/null on Unix
270
# TODO: if either of the files being compared already exists as a
271
# regular named file (e.g. in the working directory) then we can
272
# compare directly to that, rather than copying it.
274
oldtmpf.writelines(oldlines)
275
newtmpf.writelines(newlines)
282
if sys.platform == 'win32':
283
# Popen doesn't do the proper encoding for external commands
284
# Since we are dealing with an ANSI api, use mbcs encoding
285
old_label = old_label.encode('mbcs')
286
new_label = new_label.encode('mbcs')
288
'--label', old_label,
290
'--label', new_label,
295
diff_opts = default_style_unified(diff_opts)
298
diffcmd.extend(diff_opts)
300
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
301
out, err = pipe.communicate()
304
# internal_diff() adds a trailing newline, add one here for consistency
307
# 'diff' gives retcode == 2 for all sorts of errors
308
# one of those is 'Binary files differ'.
309
# Bad options could also be the problem.
310
# 'Binary files' is not a real error, so we suppress that error.
313
# Since we got here, we want to make sure to give an i18n error
314
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
315
out, err = pipe.communicate()
317
# Write out the new i18n diff response
318
to_file.write(out + b'\n')
319
if pipe.returncode != 2:
320
raise errors.BzrError(
321
'external diff failed with exit code 2'
322
' when run with LANG=C and LC_ALL=C,'
323
' but not when run natively: %r' % (diffcmd,))
325
first_line = lang_c_out.split(b'\n', 1)[0]
326
# Starting with diffutils 2.8.4 the word "binary" was dropped.
327
m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
329
raise errors.BzrError('external diff failed with exit code 2;'
330
' command: %r' % (diffcmd,))
332
# Binary files differ, just return
335
# If we got to here, we haven't written out the output of diff
339
# returns 1 if files differ; that's OK
341
msg = 'signal %d' % (-rc)
343
msg = 'exit code %d' % rc
345
raise errors.BzrError('external diff failed with %s; command: %r'
349
oldtmpf.close() # and delete
353
# Warn in case the file couldn't be deleted (in case windows still
354
# holds the file open, but not if the files have already been
359
if e.errno not in (errno.ENOENT,):
360
warning('Failed to delete temporary file: %s %s', path, e)
366
def get_trees_and_branches_to_diff_locked(
367
path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
368
"""Get the trees and specific files to diff given a list of paths.
370
This method works out the trees to be diff'ed and the files of
371
interest within those trees.
374
the list of arguments passed to the diff command
375
:param revision_specs:
376
Zero, one or two RevisionSpecs from the diff command line,
377
saying what revisions to compare.
379
The url of the old branch or tree. If None, the tree to use is
380
taken from the first path, if any, or the current working tree.
382
The url of the new branch or tree. If None, the tree to use is
383
taken from the first path, if any, or the current working tree.
385
an ExitStack object. get_trees_and_branches_to_diff
386
will register cleanups that must be run to unlock the trees, etc.
388
if True and a view is set, apply the view or check that the paths
391
a tuple of (old_tree, new_tree, old_branch, new_branch,
392
specific_files, extra_trees) where extra_trees is a sequence of
393
additional trees to search in for file-ids. The trees and branches
394
will be read-locked until the cleanups registered via the exit_stack
397
# Get the old and new revision specs
398
old_revision_spec = None
399
new_revision_spec = None
400
if revision_specs is not None:
401
if len(revision_specs) > 0:
402
old_revision_spec = revision_specs[0]
404
old_url = old_revision_spec.get_branch()
405
if len(revision_specs) > 1:
406
new_revision_spec = revision_specs[1]
408
new_url = new_revision_spec.get_branch()
411
make_paths_wt_relative = True
412
consider_relpath = True
413
if path_list is None or len(path_list) == 0:
414
# If no path is given, the current working tree is used
415
default_location = u'.'
416
consider_relpath = False
417
elif old_url is not None and new_url is not None:
418
other_paths = path_list
419
make_paths_wt_relative = False
421
default_location = path_list[0]
422
other_paths = path_list[1:]
424
def lock_tree_or_branch(wt, br):
426
exit_stack.enter_context(wt.lock_read())
428
exit_stack.enter_context(br.lock_read())
430
# Get the old location
433
old_url = default_location
434
working_tree, branch, relpath = \
435
controldir.ControlDir.open_containing_tree_or_branch(old_url)
436
lock_tree_or_branch(working_tree, branch)
437
if consider_relpath and relpath != '':
438
if working_tree is not None and apply_view:
439
views.check_path_in_view(working_tree, relpath)
440
specific_files.append(relpath)
441
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
444
# Get the new location
446
new_url = default_location
447
if new_url != old_url:
448
working_tree, branch, relpath = \
449
controldir.ControlDir.open_containing_tree_or_branch(new_url)
450
lock_tree_or_branch(working_tree, branch)
451
if consider_relpath and relpath != '':
452
if working_tree is not None and apply_view:
453
views.check_path_in_view(working_tree, relpath)
454
specific_files.append(relpath)
455
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
456
basis_is_default=working_tree is None)
459
# Get the specific files (all files is None, no files is [])
460
if make_paths_wt_relative and working_tree is not None:
461
other_paths = working_tree.safe_relpath_files(
463
apply_view=apply_view)
464
specific_files.extend(other_paths)
465
if len(specific_files) == 0:
466
specific_files = None
467
if (working_tree is not None and working_tree.supports_views() and
469
view_files = working_tree.views.lookup_view()
471
specific_files = view_files
472
view_str = views.view_display_str(view_files)
473
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
475
# Get extra trees that ought to be searched for file-ids
477
if working_tree is not None and working_tree not in (old_tree, new_tree):
478
extra_trees = (working_tree,)
479
return (old_tree, new_tree, old_branch, new_branch,
480
specific_files, extra_trees)
483
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
484
if branch is None and tree is not None:
486
if spec is None or spec.spec is None:
489
return tree.basis_tree()
491
return branch.basis_tree()
494
return spec.as_tree(branch)
497
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
498
external_diff_options=None,
499
old_label='a/', new_label='b/',
501
path_encoding='utf8',
504
context=DEFAULT_CONTEXT_AMOUNT):
505
"""Show in text form the changes from one tree to another.
507
:param to_file: The output stream.
508
:param specific_files: Include only changes to these files - None for all
510
:param external_diff_options: If set, use an external GNU diff and pass
512
:param extra_trees: If set, more Trees to use for looking up file ids
513
:param path_encoding: If set, the path will be encoded as specified,
514
otherwise is supposed to be utf8
515
:param format_cls: Formatter class (DiffTree subclass)
518
context = DEFAULT_CONTEXT_AMOUNT
519
if format_cls is None:
520
format_cls = DiffTree
521
with contextlib.ExitStack() as exit_stack:
522
exit_stack.enter_context(old_tree.lock_read())
523
if extra_trees is not None:
524
for tree in extra_trees:
525
exit_stack.enter_context(tree.lock_read())
526
exit_stack.enter_context(new_tree.lock_read())
527
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
529
external_diff_options,
530
old_label, new_label, using,
531
context_lines=context)
532
return differ.show_diff(specific_files, extra_trees)
535
def _patch_header_date(tree, path):
536
"""Returns a timestamp suitable for use in a patch header."""
538
mtime = tree.get_file_mtime(path)
539
except FileTimestampUnavailable:
541
return timestamp.format_patch_date(mtime)
544
def get_executable_change(old_is_x, new_is_x):
545
descr = {True: b"+x", False: b"-x", None: b"??"}
546
if old_is_x != new_is_x:
547
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
552
class DiffPath(object):
553
"""Base type for command object that compare files"""
555
# The type or contents of the file were unsuitable for diffing
556
CANNOT_DIFF = 'CANNOT_DIFF'
557
# The file has changed in a semantic way
559
# The file content may have changed, but there is no semantic change
560
UNCHANGED = 'UNCHANGED'
562
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
565
:param old_tree: The tree to show as the old tree in the comparison
566
:param new_tree: The tree to show as new in the comparison
567
:param to_file: The file to write comparison data to
568
:param path_encoding: The character encoding to write paths in
570
self.old_tree = old_tree
571
self.new_tree = new_tree
572
self.to_file = to_file
573
self.path_encoding = path_encoding
579
def from_diff_tree(klass, diff_tree):
580
return klass(diff_tree.old_tree, diff_tree.new_tree,
581
diff_tree.to_file, diff_tree.path_encoding)
584
def _diff_many(differs, old_path, new_path, old_kind, new_kind):
585
for file_differ in differs:
586
result = file_differ.diff(old_path, new_path, old_kind, new_kind)
587
if result is not DiffPath.CANNOT_DIFF:
590
return DiffPath.CANNOT_DIFF
593
class DiffKindChange(object):
594
"""Special differ for file kind changes.
596
Represents kind change as deletion + creation. Uses the other differs
600
def __init__(self, differs):
601
self.differs = differs
607
def from_diff_tree(klass, diff_tree):
608
return klass(diff_tree.differs)
610
def diff(self, old_path, new_path, old_kind, new_kind):
611
"""Perform comparison
613
:param old_path: Path of the file in the old tree
614
:param new_path: Path of the file in the new tree
615
:param old_kind: Old file-kind of the file
616
:param new_kind: New file-kind of the file
618
if None in (old_kind, new_kind):
619
return DiffPath.CANNOT_DIFF
620
result = DiffPath._diff_many(
621
self.differs, old_path, new_path, old_kind, None)
622
if result is DiffPath.CANNOT_DIFF:
624
return DiffPath._diff_many(
625
self.differs, old_path, new_path, None, new_kind)
628
class DiffTreeReference(DiffPath):
630
def diff(self, old_path, new_path, old_kind, new_kind):
631
"""Perform comparison between two tree references. (dummy)
634
if 'tree-reference' not in (old_kind, new_kind):
635
return self.CANNOT_DIFF
636
if old_kind not in ('tree-reference', None):
637
return self.CANNOT_DIFF
638
if new_kind not in ('tree-reference', None):
639
return self.CANNOT_DIFF
643
class DiffDirectory(DiffPath):
645
def diff(self, old_path, new_path, old_kind, new_kind):
646
"""Perform comparison between two directories. (dummy)
649
if 'directory' not in (old_kind, new_kind):
650
return self.CANNOT_DIFF
651
if old_kind not in ('directory', None):
652
return self.CANNOT_DIFF
653
if new_kind not in ('directory', None):
654
return self.CANNOT_DIFF
658
class DiffSymlink(DiffPath):
660
def diff(self, old_path, new_path, old_kind, new_kind):
661
"""Perform comparison between two symlinks
663
:param old_path: Path of the file in the old tree
664
:param new_path: Path of the file in the new tree
665
:param old_kind: Old file-kind of the file
666
:param new_kind: New file-kind of the file
668
if 'symlink' not in (old_kind, new_kind):
669
return self.CANNOT_DIFF
670
if old_kind == 'symlink':
671
old_target = self.old_tree.get_symlink_target(old_path)
672
elif old_kind is None:
675
return self.CANNOT_DIFF
676
if new_kind == 'symlink':
677
new_target = self.new_tree.get_symlink_target(new_path)
678
elif new_kind is None:
681
return self.CANNOT_DIFF
682
return self.diff_symlink(old_target, new_target)
684
def diff_symlink(self, old_target, new_target):
685
if old_target is None:
686
self.to_file.write(b'=== target is \'%s\'\n' %
687
new_target.encode(self.path_encoding, 'replace'))
688
elif new_target is None:
689
self.to_file.write(b'=== target was \'%s\'\n' %
690
old_target.encode(self.path_encoding, 'replace'))
692
self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
693
(old_target.encode(self.path_encoding, 'replace'),
694
new_target.encode(self.path_encoding, 'replace')))
698
class DiffText(DiffPath):
700
# GNU Patch uses the epoch date to detect files that are being added
701
# or removed in a diff.
702
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
704
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
705
old_label='', new_label='', text_differ=internal_diff,
706
context_lines=DEFAULT_CONTEXT_AMOUNT):
707
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
708
self.text_differ = text_differ
709
self.old_label = old_label
710
self.new_label = new_label
711
self.path_encoding = path_encoding
712
self.context_lines = context_lines
714
def diff(self, old_path, new_path, old_kind, new_kind):
715
"""Compare two files in unified diff format
717
:param old_path: Path of the file in the old tree
718
:param new_path: Path of the file in the new tree
719
:param old_kind: Old file-kind of the file
720
:param new_kind: New file-kind of the file
722
if 'file' not in (old_kind, new_kind):
723
return self.CANNOT_DIFF
724
if old_kind == 'file':
725
old_date = _patch_header_date(self.old_tree, old_path)
726
elif old_kind is None:
727
old_date = self.EPOCH_DATE
729
return self.CANNOT_DIFF
730
if new_kind == 'file':
731
new_date = _patch_header_date(self.new_tree, new_path)
732
elif new_kind is None:
733
new_date = self.EPOCH_DATE
735
return self.CANNOT_DIFF
736
from_label = '%s%s\t%s' % (self.old_label, old_path,
738
to_label = '%s%s\t%s' % (self.new_label, new_path,
740
return self.diff_text(old_path, new_path, from_label, to_label)
742
def diff_text(self, from_path, to_path, from_label, to_label):
743
"""Diff the content of given files in two trees
745
:param from_path: The path in the from tree. If None,
746
the file is not present in the from tree.
747
:param to_path: The path in the to tree. This may refer
748
to a different file from from_path. If None,
749
the file is not present in the to tree.
751
def _get_text(tree, path):
755
return tree.get_file_lines(path)
756
except errors.NoSuchFile:
759
from_text = _get_text(self.old_tree, from_path)
760
to_text = _get_text(self.new_tree, to_path)
761
self.text_differ(from_label, from_text, to_label, to_text,
762
self.to_file, path_encoding=self.path_encoding,
763
context_lines=self.context_lines)
764
except errors.BinaryFile:
766
("Binary files %s%s and %s%s differ\n" %
767
(self.old_label, from_path, self.new_label, to_path)).encode(self.path_encoding, 'replace'))
771
class DiffFromTool(DiffPath):
773
def __init__(self, command_template, old_tree, new_tree, to_file,
774
path_encoding='utf-8'):
775
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
776
self.command_template = command_template
777
self._root = osutils.mkdtemp(prefix='brz-diff-')
780
def from_string(klass, command_template, old_tree, new_tree, to_file,
781
path_encoding='utf-8'):
782
return klass(command_template, old_tree, new_tree, to_file,
786
def make_from_diff_tree(klass, command_string, external_diff_options=None):
787
def from_diff_tree(diff_tree):
788
full_command_string = [command_string]
789
if external_diff_options is not None:
790
full_command_string += ' ' + external_diff_options
791
return klass.from_string(full_command_string, diff_tree.old_tree,
792
diff_tree.new_tree, diff_tree.to_file)
793
return from_diff_tree
795
def _get_command(self, old_path, new_path):
796
my_map = {'old_path': old_path, 'new_path': new_path}
797
command = [t.format(**my_map) for t in
798
self.command_template]
799
if command == self.command_template:
800
command += [old_path, new_path]
801
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
804
if isinstance(c, str):
805
command_encoded.append(c.encode('mbcs'))
807
command_encoded.append(c)
808
return command_encoded
812
def _execute(self, old_path, new_path):
813
command = self._get_command(old_path, new_path)
815
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
818
if e.errno == errno.ENOENT:
819
raise errors.ExecutableMissing(command[0])
822
self.to_file.write(proc.stdout.read())
826
def _try_symlink_root(self, tree, prefix):
827
if (getattr(tree, 'abspath', None) is None or
828
not osutils.host_os_dereferences_symlinks()):
831
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
833
if e.errno != errno.EEXIST:
839
"""Returns safe encoding for passing file path to diff tool"""
840
if sys.platform == 'win32':
843
# Don't fallback to 'utf-8' because subprocess may not be able to
844
# handle utf-8 correctly when locale is not utf-8.
845
return sys.getfilesystemencoding() or 'ascii'
847
def _is_safepath(self, path):
848
"""Return true if `path` may be able to pass to subprocess."""
851
return path == path.encode(fenc).decode(fenc)
855
def _safe_filename(self, prefix, relpath):
856
"""Replace unsafe character in `relpath` then join `self._root`,
857
`prefix` and `relpath`."""
859
# encoded_str.replace('?', '_') may break multibyte char.
860
# So we should encode, decode, then replace(u'?', u'_')
861
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
862
relpath_tmp = relpath_tmp.replace(u'?', u'_')
863
return osutils.pathjoin(self._root, prefix, relpath_tmp)
865
def _write_file(self, relpath, tree, prefix, force_temp=False,
867
if not force_temp and isinstance(tree, WorkingTree):
868
full_path = tree.abspath(relpath)
869
if self._is_safepath(full_path):
872
full_path = self._safe_filename(prefix, relpath)
873
if not force_temp and self._try_symlink_root(tree, prefix):
875
parent_dir = osutils.dirname(full_path)
877
os.makedirs(parent_dir)
879
if e.errno != errno.EEXIST:
881
with tree.get_file(relpath) as source, \
882
open(full_path, 'wb') as target:
883
osutils.pumpfile(source, target)
885
mtime = tree.get_file_mtime(relpath)
886
except FileTimestampUnavailable:
889
os.utime(full_path, (mtime, mtime))
891
osutils.make_readonly(full_path)
894
def _prepare_files(self, old_path, new_path, force_temp=False,
895
allow_write_new=False):
896
old_disk_path = self._write_file(
897
old_path, self.old_tree, 'old', force_temp)
898
new_disk_path = self._write_file(
899
new_path, self.new_tree, 'new', force_temp,
900
allow_write=allow_write_new)
901
return old_disk_path, new_disk_path
905
osutils.rmtree(self._root)
907
if e.errno != errno.ENOENT:
908
mutter("The temporary directory \"%s\" was not "
909
"cleanly removed: %s." % (self._root, e))
911
def diff(self, old_path, new_path, old_kind, new_kind):
912
if (old_kind, new_kind) != ('file', 'file'):
913
return DiffPath.CANNOT_DIFF
914
(old_disk_path, new_disk_path) = self._prepare_files(
916
self._execute(old_disk_path, new_disk_path)
918
def edit_file(self, old_path, new_path):
919
"""Use this tool to edit a file.
921
A temporary copy will be edited, and the new contents will be
924
:return: The new contents of the file.
926
old_abs_path, new_abs_path = self._prepare_files(
927
old_path, new_path, allow_write_new=True, force_temp=True)
928
command = self._get_command(old_abs_path, new_abs_path)
929
subprocess.call(command, cwd=self._root)
930
with open(new_abs_path, 'rb') as new_file:
931
return new_file.read()
934
class DiffTree(object):
935
"""Provides textual representations of the difference between two trees.
937
A DiffTree examines two trees and where a file-id has altered
938
between them, generates a textual representation of the difference.
939
DiffTree uses a sequence of DiffPath objects which are each
940
given the opportunity to handle a given altered fileid. The list
941
of DiffPath objects can be extended globally by appending to
942
DiffTree.diff_factories, or for a specific diff operation by
943
supplying the extra_factories option to the appropriate method.
946
# list of factories that can provide instances of DiffPath objects
947
# may be extended by plugins.
948
diff_factories = [DiffSymlink.from_diff_tree,
949
DiffDirectory.from_diff_tree,
950
DiffTreeReference.from_diff_tree]
952
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
953
diff_text=None, extra_factories=None):
956
:param old_tree: Tree to show as old in the comparison
957
:param new_tree: Tree to show as new in the comparison
958
:param to_file: File to write comparision to
959
:param path_encoding: Character encoding to write paths in
960
:param diff_text: DiffPath-type object to use as a last resort for
962
:param extra_factories: Factories of DiffPaths to try before any other
964
if diff_text is None:
965
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
966
'', '', internal_diff)
967
self.old_tree = old_tree
968
self.new_tree = new_tree
969
self.to_file = to_file
970
self.path_encoding = path_encoding
972
if extra_factories is not None:
973
self.differs.extend(f(self) for f in extra_factories)
974
self.differs.extend(f(self) for f in self.diff_factories)
975
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
978
def from_trees_options(klass, old_tree, new_tree, to_file,
979
path_encoding, external_diff_options, old_label,
980
new_label, using, context_lines):
981
"""Factory for producing a DiffTree.
983
Designed to accept options used by show_diff_trees.
985
:param old_tree: The tree to show as old in the comparison
986
:param new_tree: The tree to show as new in the comparison
987
:param to_file: File to write comparisons to
988
:param path_encoding: Character encoding to use for writing paths
989
:param external_diff_options: If supplied, use the installed diff
990
binary to perform file comparison, using supplied options.
991
:param old_label: Prefix to use for old file labels
992
:param new_label: Prefix to use for new file labels
993
:param using: Commandline to use to invoke an external diff tool
995
if using is not None:
996
extra_factories = [DiffFromTool.make_from_diff_tree(
997
using, external_diff_options)]
1000
if external_diff_options:
1001
opts = external_diff_options.split()
1003
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
1004
""":param path_encoding: not used but required
1005
to match the signature of internal_diff.
1007
external_diff(olab, olines, nlab, nlines, to_file, opts)
1009
diff_file = internal_diff
1010
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
1011
old_label, new_label, diff_file, context_lines=context_lines)
1012
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
1015
def show_diff(self, specific_files, extra_trees=None):
1016
"""Write tree diff to self.to_file
1018
:param specific_files: the specific files to compare (recursive)
1019
:param extra_trees: extra trees to use for mapping paths to file_ids
1022
return self._show_diff(specific_files, extra_trees)
1024
for differ in self.differs:
1027
def _show_diff(self, specific_files, extra_trees):
1028
# TODO: Generation of pseudo-diffs for added/deleted files could
1029
# be usefully made into a much faster special case.
1030
iterator = self.new_tree.iter_changes(self.old_tree,
1031
specific_files=specific_files,
1032
extra_trees=extra_trees,
1033
require_versioned=True)
1036
def changes_key(change):
1037
old_path, new_path = change.path
1043
def get_encoded_path(path):
1044
if path is not None:
1045
return path.encode(self.path_encoding, "replace")
1046
for change in sorted(iterator, key=changes_key):
1047
# The root does not get diffed, and items with no known kind (that
1048
# is, missing) in both trees are skipped as well.
1049
if change.parent_id == (None, None) or change.kind == (None, None):
1051
if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1053
'Ignoring "%s" as symlinks are not '
1054
'supported on this filesystem.' % (change.path[0],))
1056
oldpath, newpath = change.path
1057
oldpath_encoded = get_encoded_path(change.path[0])
1058
newpath_encoded = get_encoded_path(change.path[1])
1059
old_present = (change.kind[0] is not None and change.versioned[0])
1060
new_present = (change.kind[1] is not None and change.versioned[1])
1061
executable = change.executable
1063
renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
1065
properties_changed = []
1066
properties_changed.extend(
1067
get_executable_change(executable[0], executable[1]))
1069
if properties_changed:
1070
prop_str = b" (properties changed: %s)" % (
1071
b", ".join(properties_changed),)
1075
if (old_present, new_present) == (True, False):
1076
self.to_file.write(b"=== removed %s '%s'\n" %
1077
(kind[0].encode('ascii'), oldpath_encoded))
1079
elif (old_present, new_present) == (False, True):
1080
self.to_file.write(b"=== added %s '%s'\n" %
1081
(kind[1].encode('ascii'), newpath_encoded))
1084
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1085
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1087
# if it was produced by iter_changes, it must be
1088
# modified *somehow*, either content or execute bit.
1089
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1090
newpath_encoded, prop_str))
1091
if change.changed_content:
1092
self._diff(oldpath, newpath, kind[0], kind[1])
1098
def diff(self, old_path, new_path):
1099
"""Perform a diff of a single file
1101
:param old_path: The path of the file in the old tree
1102
:param new_path: The path of the file in the new tree
1104
if old_path is None:
1107
old_kind = self.old_tree.kind(old_path)
1108
if new_path is None:
1111
new_kind = self.new_tree.kind(new_path)
1112
self._diff(old_path, new_path, old_kind, new_kind)
1114
def _diff(self, old_path, new_path, old_kind, new_kind):
1115
result = DiffPath._diff_many(
1116
self.differs, old_path, new_path, old_kind, new_kind)
1117
if result is DiffPath.CANNOT_DIFF:
1118
error_path = new_path
1119
if error_path is None:
1120
error_path = old_path
1121
raise errors.NoDiffFound(error_path)
1124
format_registry = Registry()
1125
format_registry.register('default', DiffTree)