13
13
# You should have received a copy of the GNU General Public License
14
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
23
from bzrlib.lazy_import import lazy_import
31
30
from bzrlib import (
31
branch as _mod_branch,
43
from bzrlib.workingtree import WorkingTree
44
from bzrlib.i18n import gettext
47
from bzrlib.registry import (
50
from bzrlib.trace import mutter, note, warning
52
DEFAULT_CONTEXT_AMOUNT = 3
54
class AtTemplate(string.Template):
55
"""Templating class that uses @ instead of $."""
42
from bzrlib.symbol_versioning import (
46
from bzrlib.trace import warning
60
49
# TODO: Rather than building a changeset object, we should probably
74
63
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
75
64
allow_binary=False, sequence_matcher=None,
76
path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
65
path_encoding='utf8'):
77
66
# FIXME: difflib is wrong if there is no trailing newline.
78
67
# The syntax used by patch seems to be "\ No newline at
79
68
# end of file" following the last diff line from that
97
86
if sequence_matcher is None:
98
87
sequence_matcher = patiencediff.PatienceSequenceMatcher
99
88
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)
89
fromfile=old_filename.encode(path_encoding),
90
tofile=new_filename.encode(path_encoding),
91
sequencematcher=sequence_matcher)
105
94
if len(ud) == 0: # Identical contents, nothing to do
291
def get_trees_and_branches_to_diff_locked(
292
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
275
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
293
276
"""Get the trees and specific files to diff given a list of paths.
295
278
This method works out the trees to be diff'ed and the files of
307
290
The url of the new branch or tree. If None, the tree to use is
308
291
taken from the first path, if any, or the current working tree.
310
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
311
will register cleanups that must be run to unlock the trees, etc.
313
if True and a view is set, apply the view or check that the paths
316
a tuple of (old_tree, new_tree, old_branch, new_branch,
317
specific_files, extra_trees) where extra_trees is a sequence of
318
additional trees to search in for file-ids. The trees and branches
319
will be read-locked until the cleanups registered via the add_cleanup
293
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
294
extra_trees is a sequence of additional trees to search in for
322
297
# Get the old and new revision specs
323
298
old_revision_spec = None
346
321
default_location = path_list[0]
347
322
other_paths = path_list[1:]
349
def lock_tree_or_branch(wt, br):
352
add_cleanup(wt.unlock)
355
add_cleanup(br.unlock)
357
324
# Get the old location
358
325
specific_files = []
359
326
if old_url is None:
360
327
old_url = default_location
361
328
working_tree, branch, relpath = \
362
controldir.ControlDir.open_containing_tree_or_branch(old_url)
363
lock_tree_or_branch(working_tree, branch)
329
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
364
330
if consider_relpath and relpath != '':
365
if working_tree is not None and apply_view:
366
views.check_path_in_view(working_tree, relpath)
367
331
specific_files.append(relpath)
368
332
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
371
334
# Get the new location
372
335
if new_url is None:
373
336
new_url = default_location
374
337
if new_url != old_url:
375
338
working_tree, branch, relpath = \
376
controldir.ControlDir.open_containing_tree_or_branch(new_url)
377
lock_tree_or_branch(working_tree, branch)
339
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
378
340
if consider_relpath and relpath != '':
379
if working_tree is not None and apply_view:
380
views.check_path_in_view(working_tree, relpath)
381
341
specific_files.append(relpath)
382
342
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
383
343
basis_is_default=working_tree is None)
386
345
# Get the specific files (all files is None, no files is [])
387
346
if make_paths_wt_relative and working_tree is not None:
388
other_paths = working_tree.safe_relpath_files(
390
apply_view=apply_view)
347
other_paths = _relative_paths_in_tree(working_tree, other_paths)
391
348
specific_files.extend(other_paths)
392
349
if len(specific_files) == 0:
393
350
specific_files = None
394
if (working_tree is not None and working_tree.supports_views()
396
view_files = working_tree.views.lookup_view()
398
specific_files = view_files
399
view_str = views.view_display_str(view_files)
400
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
402
352
# Get extra trees that ought to be searched for file-ids
403
353
extra_trees = None
404
354
if working_tree is not None and working_tree not in (old_tree, new_tree):
405
355
extra_trees = (working_tree,)
406
return (old_tree, new_tree, old_branch, new_branch,
407
specific_files, extra_trees)
356
return old_tree, new_tree, specific_files, extra_trees
410
359
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
421
370
return spec.as_tree(branch)
373
def _relative_paths_in_tree(tree, paths):
374
"""Get the relative paths within a working tree.
376
Each path may be either an absolute path or a path relative to the
377
current working directory.
380
for filename in paths:
382
result.append(tree.relpath(osutils.dereference_path(filename)))
383
except errors.PathNotChild:
384
raise errors.BzrCommandError("Files are in different branches")
424
388
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
425
389
external_diff_options=None,
426
390
old_label='a/', new_label='b/',
427
391
extra_trees=None,
428
392
path_encoding='utf8',
431
context=DEFAULT_CONTEXT_AMOUNT):
432
394
"""Show in text form the changes from one tree to another.
434
:param to_file: The output stream.
435
:param specific_files: Include only changes to these files - None for all
437
:param external_diff_options: If set, use an external GNU diff and pass
439
:param extra_trees: If set, more Trees to use for looking up file ids
440
:param path_encoding: If set, the path will be encoded as specified,
441
otherwise is supposed to be utf8
442
:param format_cls: Formatter class (DiffTree subclass)
400
Include only changes to these files - None for all changes.
402
external_diff_options
403
If set, use an external GNU diff and pass these options.
406
If set, more Trees to use for looking up file ids
409
If set, the path will be encoded as specified, otherwise is supposed
445
context = DEFAULT_CONTEXT_AMOUNT
446
if format_cls is None:
447
format_cls = DiffTree
448
412
old_tree.lock_read()
450
414
if extra_trees is not None:
453
417
new_tree.lock_read()
455
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
457
external_diff_options,
458
old_label, new_label, using,
459
context_lines=context)
419
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
421
external_diff_options,
422
old_label, new_label, using)
460
423
return differ.show_diff(specific_files, extra_trees)
462
425
new_tree.unlock()
470
433
def _patch_header_date(tree, file_id, path):
471
434
"""Returns a timestamp suitable for use in a patch header."""
473
mtime = tree.get_file_mtime(file_id, path)
474
except errors.FileTimestampUnavailable:
435
mtime = tree.get_file_mtime(file_id, path)
476
436
return timestamp.format_patch_date(mtime)
439
@deprecated_function(one_three)
440
def get_prop_change(meta_modified):
442
return " (properties changed)"
479
446
def get_executable_change(old_is_x, new_is_x):
480
447
descr = { True:"+x", False:"-x", None:"??" }
481
448
if old_is_x != new_is_x:
620
587
# or removed in a diff.
621
588
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
623
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
624
old_label='', new_label='', text_differ=internal_diff,
625
context_lines=DEFAULT_CONTEXT_AMOUNT):
590
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
591
old_label='', new_label='', text_differ=internal_diff):
626
592
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
627
593
self.text_differ = text_differ
628
594
self.old_label = old_label
629
595
self.new_label = new_label
630
596
self.path_encoding = path_encoding
631
self.context_lines = context_lines
633
598
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
634
599
"""Compare two files in unified diff format
658
623
return self.CANNOT_DIFF
659
624
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
660
625
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
661
return self.diff_text(from_file_id, to_file_id, from_label, to_label,
626
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
664
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
665
from_path=None, to_path=None):
628
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
666
629
"""Diff the content of given files in two trees
668
631
:param from_file_id: The id of the file in the from tree. If None,
670
633
:param to_file_id: The id of the file in the to tree. This may refer
671
634
to a different file from from_file_id. If None,
672
635
the file is not present in the to tree.
673
:param from_path: The path in the from tree or None if unknown.
674
:param to_path: The path in the to tree or None if unknown.
676
def _get_text(tree, file_id, path):
637
def _get_text(tree, file_id):
677
638
if file_id is not None:
678
return tree.get_file_lines(file_id, path)
639
return tree.get_file(file_id).readlines()
682
from_text = _get_text(self.old_tree, from_file_id, from_path)
683
to_text = _get_text(self.new_tree, to_file_id, to_path)
643
from_text = _get_text(self.old_tree, from_file_id)
644
to_text = _get_text(self.new_tree, to_file_id)
684
645
self.text_differ(from_label, from_text, to_label, to_text,
685
self.to_file, path_encoding=self.path_encoding,
686
context_lines=self.context_lines)
687
647
except errors.BinaryFile:
688
648
self.to_file.write(
689
649
("Binary files %s and %s differ\n" %
690
(from_label, to_label)).encode(self.path_encoding,'replace'))
650
(from_label, to_label)).encode(self.path_encoding))
691
651
return self.CHANGED
703
663
def from_string(klass, command_string, old_tree, new_tree, to_file,
704
664
path_encoding='utf-8'):
705
command_template = cmdline.split(command_string)
706
if '@' not in command_string:
707
command_template.extend(['@old_path', '@new_path'])
665
command_template = commands.shlex_split_unicode(command_string)
666
command_template.extend(['%(old_path)s', '%(new_path)s'])
708
667
return klass(command_template, old_tree, new_tree, to_file,
712
def make_from_diff_tree(klass, command_string, external_diff_options=None):
671
def make_from_diff_tree(klass, command_string):
713
672
def from_diff_tree(diff_tree):
714
full_command_string = [command_string]
715
if external_diff_options is not None:
716
full_command_string += ' ' + external_diff_options
717
return klass.from_string(full_command_string, diff_tree.old_tree,
673
return klass.from_string(command_string, diff_tree.old_tree,
718
674
diff_tree.new_tree, diff_tree.to_file)
719
675
return from_diff_tree
721
677
def _get_command(self, old_path, new_path):
722
678
my_map = {'old_path': old_path, 'new_path': new_path}
723
command = [AtTemplate(t).substitute(my_map) for t in
724
self.command_template]
725
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
728
if isinstance(c, unicode):
729
command_encoded.append(c.encode('mbcs'))
731
command_encoded.append(c)
732
return command_encoded
679
return [t % my_map for t in self.command_template]
736
681
def _execute(self, old_path, new_path):
737
682
command = self._get_command(old_path, new_path)
762
"""Returns safe encoding for passing file path to diff tool"""
763
if sys.platform == 'win32':
766
# Don't fallback to 'utf-8' because subprocess may not be able to
767
# handle utf-8 correctly when locale is not utf-8.
768
return sys.getfilesystemencoding() or 'ascii'
770
def _is_safepath(self, path):
771
"""Return true if `path` may be able to pass to subprocess."""
774
return path == path.encode(fenc).decode(fenc)
778
def _safe_filename(self, prefix, relpath):
779
"""Replace unsafe character in `relpath` then join `self._root`,
780
`prefix` and `relpath`."""
782
# encoded_str.replace('?', '_') may break multibyte char.
783
# So we should encode, decode, then replace(u'?', u'_')
784
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
785
relpath_tmp = relpath_tmp.replace(u'?', u'_')
786
return osutils.pathjoin(self._root, prefix, relpath_tmp)
788
def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
790
if not force_temp and isinstance(tree, WorkingTree):
791
full_path = tree.abspath(tree.id2path(file_id))
792
if self._is_safepath(full_path):
795
full_path = self._safe_filename(prefix, relpath)
796
if not force_temp and self._try_symlink_root(tree, prefix):
705
def _write_file(self, file_id, tree, prefix, relpath):
706
full_path = osutils.pathjoin(self._root, prefix, relpath)
707
if self._try_symlink_root(tree, prefix):
798
709
parent_dir = osutils.dirname(full_path)
814
mtime = tree.get_file_mtime(file_id)
815
except errors.FileTimestampUnavailable:
818
os.utime(full_path, (mtime, mtime))
820
osutils.make_readonly(full_path)
724
osutils.make_readonly(full_path)
725
mtime = tree.get_file_mtime(file_id)
726
os.utime(full_path, (mtime, mtime))
823
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
824
allow_write_new=False):
729
def _prepare_files(self, file_id, old_path, new_path):
825
730
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
826
old_path, force_temp)
827
732
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
828
new_path, force_temp,
829
allow_write=allow_write_new)
830
734
return old_disk_path, new_disk_path
832
736
def finish(self):
834
osutils.rmtree(self._root)
836
if e.errno != errno.ENOENT:
837
mutter("The temporary directory \"%s\" was not "
838
"cleanly removed: %s." % (self._root, e))
737
osutils.rmtree(self._root)
840
739
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
841
740
if (old_kind, new_kind) != ('file', 'file'):
842
741
return DiffPath.CANNOT_DIFF
843
(old_disk_path, new_disk_path) = self._prepare_files(
844
file_id, old_path, new_path)
845
self._execute(old_disk_path, new_disk_path)
847
def edit_file(self, file_id):
848
"""Use this tool to edit a file.
850
A temporary copy will be edited, and the new contents will be
853
:param file_id: The id of the file to edit.
854
:return: The new contents of the file.
856
old_path = self.old_tree.id2path(file_id)
857
new_path = self.new_tree.id2path(file_id)
858
old_abs_path, new_abs_path = self._prepare_files(
859
file_id, old_path, new_path,
860
allow_write_new=True,
862
command = self._get_command(old_abs_path, new_abs_path)
863
subprocess.call(command, cwd=self._root)
864
new_file = open(new_abs_path, 'rb')
866
return new_file.read()
742
self._prepare_files(file_id, old_path, new_path)
743
self._execute(osutils.pathjoin('old', old_path),
744
osutils.pathjoin('new', new_path))
871
747
class DiffTree(object):
914
790
def from_trees_options(klass, old_tree, new_tree, to_file,
915
791
path_encoding, external_diff_options, old_label,
916
new_label, using, context_lines):
917
793
"""Factory for producing a DiffTree.
919
795
Designed to accept options used by show_diff_trees.
921
796
:param old_tree: The tree to show as old in the comparison
922
797
:param new_tree: The tree to show as new in the comparison
923
798
:param to_file: File to write comparisons to
929
804
:param using: Commandline to use to invoke an external diff tool
931
806
if using is not None:
932
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
807
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
934
809
extra_factories = []
935
810
if external_diff_options:
936
811
opts = external_diff_options.split()
937
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
938
""":param path_encoding: not used but required
939
to match the signature of internal_diff.
812
def diff_file(olab, olines, nlab, nlines, to_file):
941
813
external_diff(olab, olines, nlab, nlines, to_file, opts)
943
815
diff_file = internal_diff
944
816
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
945
old_label, new_label, diff_file, context_lines=context_lines)
817
old_label, new_label, diff_file)
946
818
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
949
821
def show_diff(self, specific_files, extra_trees=None):
950
822
"""Write tree diff to self.to_file
952
:param specific_files: the specific files to compare (recursive)
824
:param sepecific_files: the specific files to compare (recursive)
953
825
:param extra_trees: extra trees to use for mapping paths to file_ids
1034
906
new_kind = self.new_tree.kind(file_id)
1035
907
except (errors.NoSuchId, errors.NoSuchFile):
1037
self._diff(file_id, old_path, new_path, old_kind, new_kind)
1040
def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
1041
910
result = DiffPath._diff_many(self.differs, file_id, old_path,
1042
911
new_path, old_kind, new_kind)
1043
912
if result is DiffPath.CANNOT_DIFF: