87
82
# In the meantime we at least make sure the patch isn't
86
# Special workaround for Python2.3, where difflib fails if
87
# both sequences are empty.
88
if not oldlines and not newlines:
90
91
if allow_binary is False:
91
92
textfile.check_text_lines(oldlines)
92
93
textfile.check_text_lines(newlines)
94
95
if sequence_matcher is None:
95
96
sequence_matcher = patiencediff.PatienceSequenceMatcher
96
ud = patiencediff.unified_diff_bytes(oldlines, newlines,
97
fromfile=old_label.encode(
98
path_encoding, 'replace'),
99
tofile=new_label.encode(
100
path_encoding, 'replace'),
101
n=context_lines, sequencematcher=sequence_matcher)
97
ud = patiencediff.unified_diff(oldlines, newlines,
98
fromfile=old_filename.encode(path_encoding),
99
tofile=new_filename.encode(path_encoding),
100
sequencematcher=sequence_matcher)
104
if len(ud) == 0: # Identical contents, nothing to do
103
if len(ud) == 0: # Identical contents, nothing to do
106
105
# work-around for difflib being too smart for its own good
107
106
# if /dev/null is "1,0", patch won't recognize it as /dev/null
109
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
108
ud[2] = ud[2].replace('-1,0', '-0,0')
110
109
elif not newlines:
111
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
110
ud[2] = ud[2].replace('+1,0', '+0,0')
114
113
to_file.write(line)
115
if not line.endswith(b'\n'):
116
to_file.write(b"\n\\ No newline at end of file\n")
114
if not line.endswith('\n'):
115
to_file.write("\n\\ No newline at end of file\n")
120
119
def _spawn_external_diff(diffcmd, capture_errors=True):
121
"""Spawn the external diff process, and return the child handle.
120
"""Spawn the externall diff process, and return the child handle.
123
122
:param diffcmd: The command list to spawn
124
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',
169
def default_style_unified(diff_opts):
170
"""Default to unified diff style if alternative not specified in diff_opts.
172
diff only allows one style to be specified; they don't override.
173
Note that some of these take optargs, and the optargs can be
174
directly appended to the options.
175
This is only an approximate parser; it doesn't properly understand
178
:param diff_opts: List of options for external (GNU) diff.
179
:return: List of options with default style=='unified'.
181
for s in style_option_list:
189
diff_opts.append('-u')
193
def external_diff(old_label, oldlines, new_label, newlines, to_file,
156
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
195
158
"""Display a diff by calling out to the external diff program."""
196
159
# make sure our own output is properly ordered before the diff
199
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
200
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='brz-diff-new-')
162
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
163
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
201
164
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
202
165
newtmpf = os.fdopen(newtmp_fd, 'wb')
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)
394
364
specific_files.append(relpath)
395
365
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
396
basis_is_default=working_tree is None)
366
basis_is_default=working_tree is None)
397
367
new_branch = branch
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
407
if (working_tree is not None and working_tree.supports_views() and
380
if (working_tree is not None and working_tree.supports_views()
409
382
view_files = working_tree.views.lookup_view()
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
with old_tree.lock_read():
462
435
if extra_trees is not None:
463
436
for tree in extra_trees:
465
438
new_tree.lock_read()
467
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
469
external_diff_options,
470
old_label, new_label, using,
471
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)
472
444
return differ.show_diff(specific_files, extra_trees)
474
446
new_tree.unlock()
475
447
if extra_trees is not None:
476
448
for tree in extra_trees:
480
def _patch_header_date(tree, path):
454
def _patch_header_date(tree, file_id, path):
481
455
"""Returns a timestamp suitable for use in a patch header."""
483
mtime = tree.get_file_mtime(path)
484
except FileTimestampUnavailable:
457
mtime = tree.get_file_mtime(file_id, path)
458
except errors.FileTimestampUnavailable:
486
460
return timestamp.format_patch_date(mtime)
489
463
def get_executable_change(old_is_x, new_is_x):
490
descr = {True: b"+x", False: b"-x", None: b"??"}
464
descr = { True:"+x", False:"-x", None:"??" }
491
465
if old_is_x != new_is_x:
492
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
466
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
652
624
if 'file' not in (old_kind, new_kind):
653
625
return self.CANNOT_DIFF
626
from_file_id = to_file_id = file_id
654
627
if old_kind == 'file':
655
old_date = _patch_header_date(self.old_tree, old_path)
628
old_date = _patch_header_date(self.old_tree, file_id, old_path)
656
629
elif old_kind is None:
657
630
old_date = self.EPOCH_DATE
659
633
return self.CANNOT_DIFF
660
634
if new_kind == 'file':
661
new_date = _patch_header_date(self.new_tree, new_path)
635
new_date = _patch_header_date(self.new_tree, file_id, new_path)
662
636
elif new_kind is None:
663
637
new_date = self.EPOCH_DATE
665
640
return self.CANNOT_DIFF
666
from_label = '%s%s\t%s' % (self.old_label, old_path,
668
to_label = '%s%s\t%s' % (self.new_label, new_path,
670
return self.diff_text(old_path, new_path, from_label, to_label)
641
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
642
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
643
return self.diff_text(from_file_id, to_file_id, from_label, to_label,
672
def diff_text(self, from_path, to_path, from_label, to_label):
646
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
647
from_path=None, to_path=None):
673
648
"""Diff the content of given files in two trees
675
:param from_path: The path in the from tree. If None,
650
:param from_file_id: The id of the file in the from tree. If None,
676
651
the file is not present in the from tree.
677
:param to_path: The path in the to tree. This may refer
678
to a different file from from_path. If None,
652
:param to_file_id: The id of the file in the to tree. This may refer
653
to a different file from from_file_id. If None,
679
654
the file is not present in the to tree.
655
:param from_path: The path in the from tree or None if unknown.
656
:param to_path: The path in the to tree or None if unknown.
681
def _get_text(tree, path):
685
return tree.get_file_lines(path)
686
except errors.NoSuchFile:
658
def _get_text(tree, file_id, path):
659
if file_id is not None:
660
return tree.get_file(file_id, path).readlines()
689
from_text = _get_text(self.old_tree, from_path)
690
to_text = _get_text(self.new_tree, to_path)
664
from_text = _get_text(self.old_tree, from_file_id, from_path)
665
to_text = _get_text(self.new_tree, to_file_id, to_path)
691
666
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
668
except errors.BinaryFile:
695
669
self.to_file.write(
696
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
670
("Binary files %s and %s differ\n" %
671
(from_label, to_label)).encode(self.path_encoding))
698
672
return self.CHANGED
719
def make_from_diff_tree(klass, command_string, external_diff_options=None):
693
def make_from_diff_tree(klass, command_string):
720
694
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,
695
return klass.from_string(command_string, diff_tree.old_tree,
725
696
diff_tree.new_tree, diff_tree.to_file)
726
697
return from_diff_tree
728
699
def _get_command(self, old_path, new_path):
729
700
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
701
return [AtTemplate(t).substitute(my_map) for t in
702
self.command_template]
743
704
def _execute(self, old_path, new_path):
744
705
command = self._get_command(old_path, new_path)
746
707
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
749
710
if e.errno == errno.ENOENT:
750
711
raise errors.ExecutableMissing(command[0])
753
714
self.to_file.write(proc.stdout.read())
755
715
return proc.wait()
757
717
def _try_symlink_root(self, tree, prefix):
758
if (getattr(tree, 'abspath', None) is None or
759
not osutils.host_os_dereferences_symlinks()):
718
if (getattr(tree, 'abspath', None) is None
719
or not osutils.host_os_dereferences_symlinks()):
762
722
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
764
724
if e.errno != errno.EEXIST:
770
"""Returns safe encoding for passing file path to diff tool"""
771
if sys.platform == 'win32':
774
# Don't fallback to 'utf-8' because subprocess may not be able to
775
# handle utf-8 correctly when locale is not utf-8.
776
return sys.getfilesystemencoding() or 'ascii'
778
def _is_safepath(self, path):
779
"""Return true if `path` may be able to pass to subprocess."""
782
return path == path.encode(fenc).decode(fenc)
786
def _safe_filename(self, prefix, relpath):
787
"""Replace unsafe character in `relpath` then join `self._root`,
788
`prefix` and `relpath`."""
790
# encoded_str.replace('?', '_') may break multibyte char.
791
# So we should encode, decode, then replace(u'?', u'_')
792
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
793
relpath_tmp = relpath_tmp.replace(u'?', u'_')
794
return osutils.pathjoin(self._root, prefix, relpath_tmp)
796
def _write_file(self, relpath, tree, prefix, force_temp=False,
728
def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
797
729
allow_write=False):
798
730
if not force_temp and isinstance(tree, WorkingTree):
799
full_path = tree.abspath(relpath)
800
if self._is_safepath(full_path):
803
full_path = self._safe_filename(prefix, relpath)
731
return tree.abspath(tree.id2path(file_id))
733
full_path = osutils.pathjoin(self._root, prefix, relpath)
804
734
if not force_temp and self._try_symlink_root(tree, prefix):
806
736
parent_dir = osutils.dirname(full_path)
808
738
os.makedirs(parent_dir)
810
740
if e.errno != errno.EEXIST:
812
source = tree.get_file(relpath)
742
source = tree.get_file(file_id, relpath)
814
with open(full_path, 'wb') as target:
744
target = open(full_path, 'wb')
815
746
osutils.pumpfile(source, target)
819
mtime = tree.get_file_mtime(relpath)
820
except FileTimestampUnavailable:
823
os.utime(full_path, (mtime, mtime))
824
751
if not allow_write:
825
752
osutils.make_readonly(full_path)
754
mtime = tree.get_file_mtime(file_id)
755
except errors.FileTimestampUnavailable:
757
os.utime(full_path, (mtime, mtime))
828
def _prepare_files(self, old_path, new_path, force_temp=False,
760
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
829
761
allow_write_new=False):
830
old_disk_path = self._write_file(
831
old_path, self.old_tree, 'old', force_temp)
832
new_disk_path = self._write_file(
833
new_path, self.new_tree, 'new', force_temp,
834
allow_write=allow_write_new)
762
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
763
old_path, force_temp)
764
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
765
new_path, force_temp,
766
allow_write=allow_write_new)
835
767
return old_disk_path, new_disk_path
837
769
def finish(self):
839
771
osutils.rmtree(self._root)
841
773
if e.errno != errno.ENOENT:
842
774
mutter("The temporary directory \"%s\" was not "
843
"cleanly removed: %s." % (self._root, e))
775
"cleanly removed: %s." % (self._root, e))
845
def diff(self, old_path, new_path, old_kind, new_kind):
777
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
846
778
if (old_kind, new_kind) != ('file', 'file'):
847
779
return DiffPath.CANNOT_DIFF
848
780
(old_disk_path, new_disk_path) = self._prepare_files(
781
file_id, old_path, new_path)
850
782
self._execute(old_disk_path, new_disk_path)
852
def edit_file(self, old_path, new_path):
784
def edit_file(self, file_id):
853
785
"""Use this tool to edit a file.
855
787
A temporary copy will be edited, and the new contents will be
790
:param file_id: The id of the file to edit.
858
791
: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)
862
command = self._get_command(old_abs_path, new_abs_path)
793
old_path = self.old_tree.id2path(file_id)
794
new_path = self.new_tree.id2path(file_id)
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))
863
800
subprocess.call(command, cwd=self._root)
864
with open(new_abs_path, 'rb') as new_file:
801
new_file = open(new_abs_path, 'r')
865
803
return new_file.read()
868
808
class DiffTree(object):
926
865
:param using: Commandline to use to invoke an external diff tool
928
867
if using is not None:
929
extra_factories = [DiffFromTool.make_from_diff_tree(
930
using, external_diff_options)]
868
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
932
870
extra_factories = []
933
871
if external_diff_options:
934
872
opts = external_diff_options.split()
936
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
937
""":param path_encoding: not used but required
938
to match the signature of internal_diff.
873
def diff_file(olab, olines, nlab, nlines, to_file):
940
874
external_diff(olab, olines, nlab, nlines, to_file, opts)
942
876
diff_file = internal_diff
943
877
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
944
old_label, new_label, diff_file, context_lines=context_lines)
878
old_label, new_label, diff_file)
945
879
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
948
882
def show_diff(self, specific_files, extra_trees=None):
949
883
"""Write tree diff to self.to_file
951
:param specific_files: the specific files to compare (recursive)
885
:param sepecific_files: the specific files to compare (recursive)
952
886
:param extra_trees: extra trees to use for mapping paths to file_ids
990
922
renamed = (parent[0], name[0]) != (parent[1], name[1])
992
924
properties_changed = []
993
properties_changed.extend(
994
get_executable_change(executable[0], executable[1]))
925
properties_changed.extend(get_executable_change(executable[0], executable[1]))
996
927
if properties_changed:
997
prop_str = b" (properties changed: %s)" % (
998
b", ".join(properties_changed),)
928
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1002
932
if (old_present, new_present) == (True, False):
1003
self.to_file.write(b"=== removed %s '%s'\n" %
1004
(kind[0].encode('ascii'), oldpath_encoded))
933
self.to_file.write("=== removed %s '%s'\n" %
934
(kind[0], oldpath_encoded))
1005
935
newpath = oldpath
1006
936
elif (old_present, new_present) == (False, True):
1007
self.to_file.write(b"=== added %s '%s'\n" %
1008
(kind[1].encode('ascii'), newpath_encoded))
937
self.to_file.write("=== added %s '%s'\n" %
938
(kind[1], newpath_encoded))
1009
939
oldpath = newpath
1011
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1012
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
941
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
942
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1014
944
# if it was produced by iter_changes, it must be
1015
945
# modified *somehow*, either content or execute bit.
1016
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1017
newpath_encoded, prop_str))
946
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
947
newpath_encoded, prop_str))
1018
948
if changed_content:
1019
self._diff(oldpath, newpath, kind[0], kind[1])
949
self._diff(file_id, oldpath, newpath, kind[0], kind[1])
1023
953
return has_changes
1025
def diff(self, old_path, new_path):
955
def diff(self, file_id, old_path, new_path):
1026
956
"""Perform a diff of a single file
958
:param file_id: file-id of the file
1028
959
:param old_path: The path of the file in the old tree
1029
960
:param new_path: The path of the file in the new tree
1031
if old_path is None:
963
old_kind = self.old_tree.kind(file_id)
964
except (errors.NoSuchId, errors.NoSuchFile):
1034
old_kind = self.old_tree.kind(old_path)
1035
if new_path is None:
967
new_kind = self.new_tree.kind(file_id)
968
except (errors.NoSuchId, errors.NoSuchFile):
1038
new_kind = self.new_tree.kind(new_path)
1039
self._diff(old_path, new_path, old_kind, new_kind)
1041
def _diff(self, old_path, new_path, old_kind, new_kind):
1042
result = DiffPath._diff_many(
1043
self.differs, old_path, new_path, old_kind, new_kind)
970
self._diff(file_id, old_path, new_path, old_kind, new_kind)
973
def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
974
result = DiffPath._diff_many(self.differs, file_id, old_path,
975
new_path, old_kind, new_kind)
1044
976
if result is DiffPath.CANNOT_DIFF:
1045
977
error_path = new_path
1046
978
if error_path is None:
1047
979
error_path = old_path
1048
980
raise errors.NoDiffFound(error_path)
1051
format_registry = Registry()
1052
format_registry.register('default', DiffTree)