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
from .lazy_import import lazy_import
23
from bzrlib.lazy_import import lazy_import
26
24
lazy_import(globals(), """
31
branch as _mod_branch,
43
from breezy.workingtree import WorkingTree
44
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
56
class AtTemplate(string.Template):
57
"""Templating class that uses @ instead of $."""
42
from bzrlib.symbol_versioning import (
46
from bzrlib.trace import mutter, warning
62
49
# TODO: Rather than building a changeset object, we should probably
274
250
msg = 'signal %d' % (-rc)
276
252
msg = 'exit code %d' % rc
278
raise errors.BzrError('external diff failed with %s; command: %r'
254
raise errors.BzrError('external diff failed with %s; command: %r'
283
259
oldtmpf.close() # and delete
287
# Warn in case the file couldn't be deleted (in case windows still
288
# holds the file open, but not if the files have already been
293
if e.errno not in (errno.ENOENT,):
294
warning('Failed to delete temporary file: %s %s', path, e)
300
def get_trees_and_branches_to_diff_locked(
301
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
261
# Clean up. Warn in case the files couldn't be deleted
262
# (in case windows still holds the file open, but not
263
# if the files have already been deleted)
265
os.remove(old_abspath)
267
if e.errno not in (errno.ENOENT,):
268
warning('Failed to delete temporary file: %s %s',
271
os.remove(new_abspath)
273
if e.errno not in (errno.ENOENT,):
274
warning('Failed to delete temporary file: %s %s',
278
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
302
279
"""Get the trees and specific files to diff given a list of paths.
304
281
This method works out the trees to be diff'ed and the files of
355
324
default_location = path_list[0]
356
325
other_paths = path_list[1:]
358
def lock_tree_or_branch(wt, br):
361
add_cleanup(wt.unlock)
364
add_cleanup(br.unlock)
366
327
# Get the old location
367
328
specific_files = []
368
329
if old_url is None:
369
330
old_url = default_location
370
331
working_tree, branch, relpath = \
371
controldir.ControlDir.open_containing_tree_or_branch(old_url)
372
lock_tree_or_branch(working_tree, branch)
332
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
373
333
if consider_relpath and relpath != '':
374
if working_tree is not None and apply_view:
375
views.check_path_in_view(working_tree, relpath)
376
334
specific_files.append(relpath)
377
335
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
380
337
# Get the new location
381
338
if new_url is None:
382
339
new_url = default_location
383
340
if new_url != old_url:
384
341
working_tree, branch, relpath = \
385
controldir.ControlDir.open_containing_tree_or_branch(new_url)
386
lock_tree_or_branch(working_tree, branch)
342
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
387
343
if consider_relpath and relpath != '':
388
if working_tree is not None and apply_view:
389
views.check_path_in_view(working_tree, relpath)
390
344
specific_files.append(relpath)
391
345
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
392
346
basis_is_default=working_tree is None)
395
348
# Get the specific files (all files is None, no files is [])
396
349
if make_paths_wt_relative and working_tree is not None:
397
other_paths = working_tree.safe_relpath_files(
399
apply_view=apply_view)
350
other_paths = _relative_paths_in_tree(working_tree, other_paths)
400
351
specific_files.extend(other_paths)
401
352
if len(specific_files) == 0:
402
353
specific_files = None
403
if (working_tree is not None and working_tree.supports_views()
405
view_files = working_tree.views.lookup_view()
407
specific_files = view_files
408
view_str = views.view_display_str(view_files)
409
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
411
355
# Get extra trees that ought to be searched for file-ids
412
356
extra_trees = None
413
357
if working_tree is not None and working_tree not in (old_tree, new_tree):
414
358
extra_trees = (working_tree,)
415
return (old_tree, new_tree, old_branch, new_branch,
416
specific_files, extra_trees)
359
return old_tree, new_tree, specific_files, extra_trees
419
362
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
435
396
old_label='a/', new_label='b/',
436
397
extra_trees=None,
437
398
path_encoding='utf8',
440
context=DEFAULT_CONTEXT_AMOUNT):
441
400
"""Show in text form the changes from one tree to another.
443
:param to_file: The output stream.
444
:param specific_files: Include only changes to these files - None for all
446
:param external_diff_options: If set, use an external GNU diff and pass
448
:param extra_trees: If set, more Trees to use for looking up file ids
449
:param path_encoding: If set, the path will be encoded as specified,
450
otherwise is supposed to be utf8
451
:param format_cls: Formatter class (DiffTree subclass)
406
Include only changes to these files - None for all changes.
408
external_diff_options
409
If set, use an external GNU diff and pass these options.
412
If set, more Trees to use for looking up file ids
415
If set, the path will be encoded as specified, otherwise is supposed
454
context = DEFAULT_CONTEXT_AMOUNT
455
if format_cls is None:
456
format_cls = DiffTree
457
with old_tree.lock_read():
458
420
if extra_trees is not None:
459
421
for tree in extra_trees:
461
423
new_tree.lock_read()
463
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
465
external_diff_options,
466
old_label, new_label, using,
467
context_lines=context)
425
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
427
external_diff_options,
428
old_label, new_label, using)
468
429
return differ.show_diff(specific_files, extra_trees)
470
431
new_tree.unlock()
471
432
if extra_trees is not None:
472
433
for tree in extra_trees:
476
439
def _patch_header_date(tree, file_id, path):
477
440
"""Returns a timestamp suitable for use in a patch header."""
479
mtime = tree.get_file_mtime(path, file_id)
480
except FileTimestampUnavailable:
441
mtime = tree.get_file_mtime(file_id, path)
482
442
return timestamp.format_patch_date(mtime)
445
def _raise_if_nonexistent(paths, old_tree, new_tree):
446
"""Complain if paths are not in either inventory or tree.
448
It's OK with the files exist in either tree's inventory, or
449
if they exist in the tree but are not versioned.
451
This can be used by operations such as bzr status that can accept
452
unknown or ignored files.
454
mutter("check paths: %r", paths)
457
s = old_tree.filter_unversioned_files(paths)
458
s = new_tree.filter_unversioned_files(s)
459
s = [path for path in s if not new_tree.has_filename(path)]
461
raise errors.PathsDoNotExist(sorted(s))
464
@deprecated_function(one_three)
465
def get_prop_change(meta_modified):
467
return " (properties changed)"
485
471
def get_executable_change(old_is_x, new_is_x):
486
472
descr = { True:"+x", False:"-x", None:"??" }
487
473
if old_is_x != new_is_x:
626
612
# or removed in a diff.
627
613
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
629
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
630
old_label='', new_label='', text_differ=internal_diff,
631
context_lines=DEFAULT_CONTEXT_AMOUNT):
615
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
616
old_label='', new_label='', text_differ=internal_diff):
632
617
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
633
618
self.text_differ = text_differ
634
619
self.old_label = old_label
635
620
self.new_label = new_label
636
621
self.path_encoding = path_encoding
637
self.context_lines = context_lines
639
623
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
640
624
"""Compare two files in unified diff format
664
648
return self.CANNOT_DIFF
665
649
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
666
650
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
667
return self.diff_text(old_path, new_path, from_label, to_label,
668
from_file_id, to_file_id)
651
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
670
def diff_text(self, from_path, to_path, from_label, to_label,
671
from_file_id=None, to_file_id=None):
653
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
672
654
"""Diff the content of given files in two trees
674
:param from_path: The path in the from tree. If None,
656
:param from_file_id: The id of the file in the from tree. If None,
675
657
the file is not present in the from tree.
676
:param to_path: The path in the to tree. This may refer
677
to a different file from from_path. If None,
658
:param to_file_id: The id of the file in the to tree. This may refer
659
to a different file from from_file_id. If None,
678
660
the file is not present in the to tree.
679
:param from_file_id: The id of the file in the from tree or None if
681
:param to_file_id: The id of the file in the to tree or None if
684
def _get_text(tree, file_id, path):
662
def _get_text(tree, file_id):
663
if file_id is not None:
664
return tree.get_file(file_id).readlines()
687
return tree.get_file_lines(path, file_id)
689
from_text = _get_text(self.old_tree, from_file_id, from_path)
690
to_text = _get_text(self.new_tree, to_file_id, to_path)
668
from_text = _get_text(self.old_tree, from_file_id)
669
to_text = _get_text(self.new_tree, to_file_id)
691
670
self.text_differ(from_label, from_text, to_label, to_text,
692
self.to_file, path_encoding=self.path_encoding,
693
context_lines=self.context_lines)
694
672
except errors.BinaryFile:
695
673
self.to_file.write(
696
674
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
675
(from_label, to_label)).encode(self.path_encoding))
698
676
return self.CHANGED
704
682
path_encoding='utf-8'):
705
683
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
684
self.command_template = command_template
707
self._root = osutils.mkdtemp(prefix='brz-diff-')
685
self._root = osutils.mkdtemp(prefix='bzr-diff-')
710
688
def from_string(klass, command_string, old_tree, new_tree, to_file,
711
689
path_encoding='utf-8'):
712
command_template = cmdline.split(command_string)
713
if '@' not in command_string:
714
command_template.extend(['@old_path', '@new_path'])
690
command_template = commands.shlex_split_unicode(command_string)
691
command_template.extend(['%(old_path)s', '%(new_path)s'])
715
692
return klass(command_template, old_tree, new_tree, to_file,
719
def make_from_diff_tree(klass, command_string, external_diff_options=None):
696
def make_from_diff_tree(klass, command_string):
720
697
def from_diff_tree(diff_tree):
721
full_command_string = [command_string]
722
if external_diff_options is not None:
723
full_command_string += ' ' + external_diff_options
724
return klass.from_string(full_command_string, diff_tree.old_tree,
698
return klass.from_string(command_string, diff_tree.old_tree,
725
699
diff_tree.new_tree, diff_tree.to_file)
726
700
return from_diff_tree
728
702
def _get_command(self, old_path, new_path):
729
703
my_map = {'old_path': old_path, 'new_path': new_path}
730
command = [AtTemplate(t).substitute(my_map) for t in
731
self.command_template]
732
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
735
if isinstance(c, text_type):
736
command_encoded.append(c.encode('mbcs'))
738
command_encoded.append(c)
739
return command_encoded
704
return [t % my_map for t in self.command_template]
743
706
def _execute(self, old_path, new_path):
744
707
command = self._get_command(old_path, new_path)
746
709
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
749
712
if e.errno == errno.ENOENT:
750
713
raise errors.ExecutableMissing(command[0])
761
724
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
763
726
if e.errno != errno.EEXIST:
769
"""Returns safe encoding for passing file path to diff tool"""
770
if sys.platform == 'win32':
773
# Don't fallback to 'utf-8' because subprocess may not be able to
774
# handle utf-8 correctly when locale is not utf-8.
775
return sys.getfilesystemencoding() or 'ascii'
777
def _is_safepath(self, path):
778
"""Return true if `path` may be able to pass to subprocess."""
781
return path == path.encode(fenc).decode(fenc)
785
def _safe_filename(self, prefix, relpath):
786
"""Replace unsafe character in `relpath` then join `self._root`,
787
`prefix` and `relpath`."""
789
# encoded_str.replace('?', '_') may break multibyte char.
790
# So we should encode, decode, then replace(u'?', u'_')
791
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
792
relpath_tmp = relpath_tmp.replace(u'?', u'_')
793
return osutils.pathjoin(self._root, prefix, relpath_tmp)
795
def _write_file(self, relpath, tree, prefix, force_temp=False,
796
allow_write=False, file_id=None):
797
if not force_temp and isinstance(tree, WorkingTree):
798
full_path = tree.abspath(relpath)
799
if self._is_safepath(full_path):
802
full_path = self._safe_filename(prefix, relpath)
803
if not force_temp and self._try_symlink_root(tree, prefix):
730
def _write_file(self, file_id, tree, prefix, relpath):
731
full_path = osutils.pathjoin(self._root, prefix, relpath)
732
if self._try_symlink_root(tree, prefix):
805
734
parent_dir = osutils.dirname(full_path)
807
736
os.makedirs(parent_dir)
809
738
if e.errno != errno.EEXIST:
811
source = tree.get_file(relpath, file_id)
740
source = tree.get_file(file_id, relpath)
813
with open(full_path, 'wb') as target:
742
target = open(full_path, 'wb')
814
744
osutils.pumpfile(source, target)
818
mtime = tree.get_file_mtime(relpath, file_id)
819
except FileTimestampUnavailable:
822
os.utime(full_path, (mtime, mtime))
824
osutils.make_readonly(full_path)
749
osutils.make_readonly(full_path)
750
mtime = tree.get_file_mtime(file_id)
751
os.utime(full_path, (mtime, mtime))
827
def _prepare_files(self, old_path, new_path, force_temp=False,
828
allow_write_new=False, file_id=None):
829
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
830
force_temp, file_id=file_id)
831
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
832
force_temp, file_id=file_id,
833
allow_write=allow_write_new)
754
def _prepare_files(self, file_id, old_path, new_path):
755
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
757
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
834
759
return old_disk_path, new_disk_path
836
761
def finish(self):
838
osutils.rmtree(self._root)
840
if e.errno != errno.ENOENT:
841
mutter("The temporary directory \"%s\" was not "
842
"cleanly removed: %s." % (self._root, e))
762
osutils.rmtree(self._root)
844
764
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
845
765
if (old_kind, new_kind) != ('file', 'file'):
846
766
return DiffPath.CANNOT_DIFF
847
(old_disk_path, new_disk_path) = self._prepare_files(
848
old_path, new_path, file_id=file_id)
849
self._execute(old_disk_path, new_disk_path)
851
def edit_file(self, old_path, new_path, file_id=None):
852
"""Use this tool to edit a file.
854
A temporary copy will be edited, and the new contents will be
857
:param file_id: The id of the file to edit.
858
:return: The new contents of the file.
860
old_abs_path, new_abs_path = self._prepare_files(
861
old_path, new_path, allow_write_new=True, force_temp=True,
863
command = self._get_command(old_abs_path, new_abs_path)
864
subprocess.call(command, cwd=self._root)
865
with open(new_abs_path, 'rb') as new_file:
866
return new_file.read()
767
self._prepare_files(file_id, old_path, new_path)
768
self._execute(osutils.pathjoin('old', old_path),
769
osutils.pathjoin('new', new_path))
869
772
class DiffTree(object):
927
829
:param using: Commandline to use to invoke an external diff tool
929
831
if using is not None:
930
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
832
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
932
834
extra_factories = []
933
835
if external_diff_options:
934
836
opts = external_diff_options.split()
935
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
936
""":param path_encoding: not used but required
937
to match the signature of internal_diff.
837
def diff_file(olab, olines, nlab, nlines, to_file):
939
838
external_diff(olab, olines, nlab, nlines, to_file, opts)
941
840
diff_file = internal_diff
942
841
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
943
old_label, new_label, diff_file, context_lines=context_lines)
842
old_label, new_label, diff_file)
944
843
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
947
846
def show_diff(self, specific_files, extra_trees=None):
948
847
"""Write tree diff to self.to_file
950
:param specific_files: the specific files to compare (recursive)
849
:param sepecific_files: the specific files to compare (recursive)
951
850
:param extra_trees: extra trees to use for mapping paths to file_ids
990
889
properties_changed.extend(get_executable_change(executable[0], executable[1]))
992
891
if properties_changed:
993
prop_str = b" (properties changed: %s)" % (
994
b", ".join(properties_changed),)
892
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
998
896
if (old_present, new_present) == (True, False):
999
self.to_file.write(b"=== removed %s '%s'\n" %
1000
(kind[0].encode('ascii'), oldpath_encoded))
897
self.to_file.write("=== removed %s '%s'\n" %
898
(kind[0], oldpath_encoded))
1001
899
newpath = oldpath
1002
900
elif (old_present, new_present) == (False, True):
1003
self.to_file.write(b"=== added %s '%s'\n" %
1004
(kind[1].encode('ascii'), newpath_encoded))
901
self.to_file.write("=== added %s '%s'\n" %
902
(kind[1], newpath_encoded))
1005
903
oldpath = newpath
1007
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1008
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
905
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
906
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1010
908
# if it was produced by iter_changes, it must be
1011
909
# modified *somehow*, either content or execute bit.
1012
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
910
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
1013
911
newpath_encoded, prop_str))
1014
912
if changed_content:
1015
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
913
self.diff(file_id, oldpath, newpath)
1025
923
:param old_path: The path of the file in the old tree
1026
924
:param new_path: The path of the file in the new tree
1028
if old_path is None:
927
old_kind = self.old_tree.kind(file_id)
928
except (errors.NoSuchId, errors.NoSuchFile):
1031
old_kind = self.old_tree.kind(old_path, file_id)
1032
if new_path is None:
931
new_kind = self.new_tree.kind(file_id)
932
except (errors.NoSuchId, errors.NoSuchFile):
1035
new_kind = self.new_tree.kind(new_path, file_id)
1036
self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1038
def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1039
935
result = DiffPath._diff_many(self.differs, file_id, old_path,
1040
936
new_path, old_kind, new_kind)
1041
937
if result is DiffPath.CANNOT_DIFF: