1
# Copyright (C) 2005-2014 Canonical Ltd.
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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
43
43
from bzrlib.workingtree import WorkingTree
44
from bzrlib.i18n import gettext
47
from bzrlib.registry import (
46
from bzrlib.symbol_versioning import (
50
49
from bzrlib.trace import mutter, note, warning
52
DEFAULT_CONTEXT_AMOUNT = 3
54
52
class AtTemplate(string.Template):
55
53
"""Templating class that uses @ instead of $."""
74
72
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
75
73
allow_binary=False, sequence_matcher=None,
76
path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
74
path_encoding='utf8'):
77
75
# FIXME: difflib is wrong if there is no trailing newline.
78
76
# The syntax used by patch seems to be "\ No newline at
79
77
# end of file" following the last diff line from that
97
95
if sequence_matcher is None:
98
96
sequence_matcher = patiencediff.PatienceSequenceMatcher
99
97
ud = patiencediff.unified_diff(oldlines, newlines,
100
fromfile=old_filename.encode(path_encoding, 'replace'),
101
tofile=new_filename.encode(path_encoding, 'replace'),
102
n=context_lines, sequencematcher=sequence_matcher)
98
fromfile=old_filename.encode(path_encoding),
99
tofile=new_filename.encode(path_encoding),
100
sequencematcher=sequence_matcher)
105
103
if len(ud) == 0: # Identical contents, nothing to do
121
119
def _spawn_external_diff(diffcmd, capture_errors=True):
122
"""Spawn the external diff process, and return the child handle.
120
"""Spawn the externall diff process, and return the child handle.
124
122
:param diffcmd: The command list to spawn
125
123
:param capture_errors: Capture stderr as well as setting LANG=C
157
# diff style options as of GNU diff v3.2
158
style_option_list = ['-c', '-C', '--context',
160
'-f', '--forward-ed',
164
'-u', '-U', '--unified',
165
'-y', '--side-by-side',
168
def default_style_unified(diff_opts):
169
"""Default to unified diff style if alternative not specified in diff_opts.
171
diff only allows one style to be specified; they don't override.
172
Note that some of these take optargs, and the optargs can be
173
directly appended to the options.
174
This is only an approximate parser; it doesn't properly understand
177
:param diff_opts: List of options for external (GNU) diff.
178
:return: List of options with default style=='unified'.
180
for s in style_option_list:
188
diff_opts.append('-u')
192
156
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
232
diff_opts = default_style_unified(diff_opts)
196
# diff only allows one style to be specified; they don't override.
197
# note that some of these take optargs, and the optargs can be
198
# directly appended to the options.
199
# this is only an approximate parser; it doesn't properly understand
201
for s in ['-c', '-u', '-C', '-U',
206
'-y', '--side-by-side',
235
218
diffcmd.extend(diff_opts)
280
263
msg = 'exit code %d' % rc
282
265
raise errors.BzrError('external diff failed with %s; command: %r'
287
270
oldtmpf.close() # and delete
291
# Warn in case the file couldn't be deleted (in case windows still
292
# holds the file open, but not if the files have already been
297
if e.errno not in (errno.ENOENT,):
298
warning('Failed to delete temporary file: %s %s', path, e)
304
def get_trees_and_branches_to_diff_locked(
305
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
272
# Clean up. Warn in case the files couldn't be deleted
273
# (in case windows still holds the file open, but not
274
# if the files have already been deleted)
276
os.remove(old_abspath)
278
if e.errno not in (errno.ENOENT,):
279
warning('Failed to delete temporary file: %s %s',
282
os.remove(new_abspath)
284
if e.errno not in (errno.ENOENT,):
285
warning('Failed to delete temporary file: %s %s',
289
def get_trees_and_branches_to_diff(path_list, revision_specs, old_url, new_url,
306
291
"""Get the trees and specific files to diff given a list of paths.
308
293
This method works out the trees to be diff'ed and the files of
320
305
The url of the new branch or tree. If None, the tree to use is
321
306
taken from the first path, if any, or the current working tree.
323
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
324
will register cleanups that must be run to unlock the trees, etc.
325
307
:param apply_view:
326
308
if True and a view is set, apply the view or check that the paths
329
311
a tuple of (old_tree, new_tree, old_branch, new_branch,
330
312
specific_files, extra_trees) where extra_trees is a sequence of
331
additional trees to search in for file-ids. The trees and branches
332
will be read-locked until the cleanups registered via the add_cleanup
313
additional trees to search in for file-ids.
335
315
# Get the old and new revision specs
336
316
old_revision_spec = None
359
339
default_location = path_list[0]
360
340
other_paths = path_list[1:]
362
def lock_tree_or_branch(wt, br):
365
add_cleanup(wt.unlock)
368
add_cleanup(br.unlock)
370
342
# Get the old location
371
343
specific_files = []
372
344
if old_url is None:
373
345
old_url = default_location
374
346
working_tree, branch, relpath = \
375
controldir.ControlDir.open_containing_tree_or_branch(old_url)
376
lock_tree_or_branch(working_tree, branch)
347
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
377
348
if consider_relpath and relpath != '':
378
349
if working_tree is not None and apply_view:
379
350
views.check_path_in_view(working_tree, relpath)
386
357
new_url = default_location
387
358
if new_url != old_url:
388
359
working_tree, branch, relpath = \
389
controldir.ControlDir.open_containing_tree_or_branch(new_url)
390
lock_tree_or_branch(working_tree, branch)
360
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
391
361
if consider_relpath and relpath != '':
392
362
if working_tree is not None and apply_view:
393
363
views.check_path_in_view(working_tree, relpath)
399
369
# Get the specific files (all files is None, no files is [])
400
370
if make_paths_wt_relative and working_tree is not None:
401
other_paths = working_tree.safe_relpath_files(
372
from bzrlib.builtins import safe_relpath_files
373
other_paths = safe_relpath_files(working_tree, other_paths,
403
374
apply_view=apply_view)
375
except errors.FileInWrongBranch:
376
raise errors.BzrCommandError("Files are in different branches")
404
377
specific_files.extend(other_paths)
405
378
if len(specific_files) == 0:
406
379
specific_files = None
411
384
specific_files = view_files
412
385
view_str = views.view_display_str(view_files)
413
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
386
note("*** Ignoring files outside view. View is %s" % view_str)
415
388
# Get extra trees that ought to be searched for file-ids
416
389
extra_trees = None
417
390
if working_tree is not None and working_tree not in (old_tree, new_tree):
418
391
extra_trees = (working_tree,)
419
return (old_tree, new_tree, old_branch, new_branch,
420
specific_files, extra_trees)
392
return old_tree, new_tree, old_branch, new_branch, specific_files, extra_trees
423
395
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
439
411
old_label='a/', new_label='b/',
440
412
extra_trees=None,
441
413
path_encoding='utf8',
444
context=DEFAULT_CONTEXT_AMOUNT):
445
415
"""Show in text form the changes from one tree to another.
447
:param to_file: The output stream.
448
:param specific_files: Include only changes to these files - None for all
450
:param external_diff_options: If set, use an external GNU diff and pass
452
:param extra_trees: If set, more Trees to use for looking up file ids
453
:param path_encoding: If set, the path will be encoded as specified,
454
otherwise is supposed to be utf8
455
:param format_cls: Formatter class (DiffTree subclass)
421
Include only changes to these files - None for all changes.
423
external_diff_options
424
If set, use an external GNU diff and pass these options.
427
If set, more Trees to use for looking up file ids
430
If set, the path will be encoded as specified, otherwise is supposed
458
context = DEFAULT_CONTEXT_AMOUNT
459
if format_cls is None:
460
format_cls = DiffTree
461
433
old_tree.lock_read()
463
435
if extra_trees is not None:
466
438
new_tree.lock_read()
468
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
470
external_diff_options,
471
old_label, new_label, using,
472
context_lines=context)
440
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
442
external_diff_options,
443
old_label, new_label, using)
473
444
return differ.show_diff(specific_files, extra_trees)
475
446
new_tree.unlock()
633
604
# or removed in a diff.
634
605
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
636
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
637
old_label='', new_label='', text_differ=internal_diff,
638
context_lines=DEFAULT_CONTEXT_AMOUNT):
607
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
608
old_label='', new_label='', text_differ=internal_diff):
639
609
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
640
610
self.text_differ = text_differ
641
611
self.old_label = old_label
642
612
self.new_label = new_label
643
613
self.path_encoding = path_encoding
644
self.context_lines = context_lines
646
615
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
647
616
"""Compare two files in unified diff format
689
658
def _get_text(tree, file_id, path):
690
659
if file_id is not None:
691
return tree.get_file_lines(file_id, path)
660
return tree.get_file(file_id, path).readlines()
695
664
from_text = _get_text(self.old_tree, from_file_id, from_path)
696
665
to_text = _get_text(self.new_tree, to_file_id, to_path)
697
666
self.text_differ(from_label, from_text, to_label, to_text,
698
self.to_file, path_encoding=self.path_encoding,
699
context_lines=self.context_lines)
700
668
except errors.BinaryFile:
701
669
self.to_file.write(
702
670
("Binary files %s and %s differ\n" %
703
(from_label, to_label)).encode(self.path_encoding,'replace'))
671
(from_label, to_label)).encode(self.path_encoding))
704
672
return self.CHANGED
716
684
def from_string(klass, command_string, old_tree, new_tree, to_file,
717
685
path_encoding='utf-8'):
718
command_template = cmdline.split(command_string)
686
command_template = commands.shlex_split_unicode(command_string)
719
687
if '@' not in command_string:
720
688
command_template.extend(['@old_path', '@new_path'])
721
689
return klass(command_template, old_tree, new_tree, to_file,
725
def make_from_diff_tree(klass, command_string, external_diff_options=None):
693
def make_from_diff_tree(klass, command_string):
726
694
def from_diff_tree(diff_tree):
727
full_command_string = [command_string]
728
if external_diff_options is not None:
729
full_command_string += ' ' + external_diff_options
730
return klass.from_string(full_command_string, diff_tree.old_tree,
695
return klass.from_string(command_string, diff_tree.old_tree,
731
696
diff_tree.new_tree, diff_tree.to_file)
732
697
return from_diff_tree
734
699
def _get_command(self, old_path, new_path):
735
700
my_map = {'old_path': old_path, 'new_path': new_path}
736
command = [AtTemplate(t).substitute(my_map) for t in
737
self.command_template]
738
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
741
if isinstance(c, unicode):
742
command_encoded.append(c.encode('mbcs'))
744
command_encoded.append(c)
745
return command_encoded
701
return [AtTemplate(t).substitute(my_map) for t in
702
self.command_template]
749
704
def _execute(self, old_path, new_path):
750
705
command = self._get_command(old_path, new_path)
775
"""Returns safe encoding for passing file path to diff tool"""
776
if sys.platform == 'win32':
779
# Don't fallback to 'utf-8' because subprocess may not be able to
780
# handle utf-8 correctly when locale is not utf-8.
781
return sys.getfilesystemencoding() or 'ascii'
783
def _is_safepath(self, path):
784
"""Return true if `path` may be able to pass to subprocess."""
787
return path == path.encode(fenc).decode(fenc)
791
def _safe_filename(self, prefix, relpath):
792
"""Replace unsafe character in `relpath` then join `self._root`,
793
`prefix` and `relpath`."""
795
# encoded_str.replace('?', '_') may break multibyte char.
796
# So we should encode, decode, then replace(u'?', u'_')
797
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
798
relpath_tmp = relpath_tmp.replace(u'?', u'_')
799
return osutils.pathjoin(self._root, prefix, relpath_tmp)
801
728
def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
802
729
allow_write=False):
803
730
if not force_temp and isinstance(tree, WorkingTree):
804
full_path = tree.abspath(tree.id2path(file_id))
805
if self._is_safepath(full_path):
808
full_path = self._safe_filename(prefix, relpath)
731
return tree.abspath(tree.id2path(file_id))
733
full_path = osutils.pathjoin(self._root, prefix, relpath)
809
734
if not force_temp and self._try_symlink_root(tree, prefix):
811
736
parent_dir = osutils.dirname(full_path)
752
osutils.make_readonly(full_path)
827
754
mtime = tree.get_file_mtime(file_id)
828
755
except errors.FileTimestampUnavailable:
831
os.utime(full_path, (mtime, mtime))
833
osutils.make_readonly(full_path)
757
os.utime(full_path, (mtime, mtime))
836
760
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
869
793
old_path = self.old_tree.id2path(file_id)
870
794
new_path = self.new_tree.id2path(file_id)
871
old_abs_path, new_abs_path = self._prepare_files(
872
file_id, old_path, new_path,
873
allow_write_new=True,
875
command = self._get_command(old_abs_path, new_abs_path)
795
new_abs_path = self._prepare_files(file_id, old_path, new_path,
796
allow_write_new=True,
798
command = self._get_command(osutils.pathjoin('old', old_path),
799
osutils.pathjoin('new', new_path))
876
800
subprocess.call(command, cwd=self._root)
877
new_file = open(new_abs_path, 'rb')
801
new_file = open(new_abs_path, 'r')
879
803
return new_file.read()
927
851
def from_trees_options(klass, old_tree, new_tree, to_file,
928
852
path_encoding, external_diff_options, old_label,
929
new_label, using, context_lines):
930
854
"""Factory for producing a DiffTree.
932
856
Designed to accept options used by show_diff_trees.
934
857
:param old_tree: The tree to show as old in the comparison
935
858
:param new_tree: The tree to show as new in the comparison
936
859
:param to_file: File to write comparisons to
942
865
:param using: Commandline to use to invoke an external diff tool
944
867
if using is not None:
945
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
868
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
947
870
extra_factories = []
948
871
if external_diff_options:
949
872
opts = external_diff_options.split()
950
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
951
""":param path_encoding: not used but required
952
to match the signature of internal_diff.
873
def diff_file(olab, olines, nlab, nlines, to_file):
954
874
external_diff(olab, olines, nlab, nlines, to_file, opts)
956
876
diff_file = internal_diff
957
877
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
958
old_label, new_label, diff_file, context_lines=context_lines)
878
old_label, new_label, diff_file)
959
879
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
962
882
def show_diff(self, specific_files, extra_trees=None):
963
883
"""Write tree diff to self.to_file
965
:param specific_files: the specific files to compare (recursive)
885
:param sepecific_files: the specific files to compare (recursive)
966
886
:param extra_trees: extra trees to use for mapping paths to file_ids