87
73
# In the meantime we at least make sure the patch isn't
77
# Special workaround for Python2.3, where difflib fails if
78
# both sequences are empty.
79
if not oldlines and not newlines:
90
82
if allow_binary is False:
91
83
textfile.check_text_lines(oldlines)
92
84
textfile.check_text_lines(newlines)
94
86
if sequence_matcher is None:
95
87
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)
88
ud = patiencediff.unified_diff(oldlines, newlines,
89
fromfile=old_filename.encode(path_encoding),
90
tofile=new_filename.encode(path_encoding),
91
sequencematcher=sequence_matcher)
104
if len(ud) == 0: # Identical contents, nothing to do
94
if len(ud) == 0: # Identical contents, nothing to do
106
96
# work-around for difflib being too smart for its own good
107
97
# 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')
99
ud[2] = ud[2].replace('-1,0', '-0,0')
110
100
elif not newlines:
111
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
101
ud[2] = ud[2].replace('+1,0', '+0,0')
114
104
to_file.write(line)
115
if not line.endswith(b'\n'):
116
to_file.write(b"\n\\ No newline at end of file\n")
105
if not line.endswith('\n'):
106
to_file.write("\n\\ No newline at end of file\n")
120
110
def _spawn_external_diff(diffcmd, capture_errors=True):
121
"""Spawn the external diff process, and return the child handle.
111
"""Spawn the externall diff process, and return the child handle.
123
113
:param diffcmd: The command list to spawn
124
114
: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,
147
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
195
149
"""Display a diff by calling out to the external diff program."""
196
150
# 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-')
153
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
154
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
201
155
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
202
156
newtmpf = os.fdopen(newtmp_fd, 'wb')
359
330
default_location = path_list[0]
360
331
other_paths = path_list[1:]
362
def lock_tree_or_branch(wt, br):
365
add_cleanup(wt.unlock)
368
add_cleanup(br.unlock)
370
333
# Get the old location
371
334
specific_files = []
372
335
if old_url is None:
373
336
old_url = default_location
374
337
working_tree, branch, relpath = \
375
controldir.ControlDir.open_containing_tree_or_branch(old_url)
376
lock_tree_or_branch(working_tree, branch)
338
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
377
339
if consider_relpath and relpath != '':
378
340
if working_tree is not None and apply_view:
379
341
views.check_path_in_view(working_tree, relpath)
380
342
specific_files.append(relpath)
381
343
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
384
345
# Get the new location
385
346
if new_url is None:
386
347
new_url = default_location
387
348
if new_url != old_url:
388
349
working_tree, branch, relpath = \
389
controldir.ControlDir.open_containing_tree_or_branch(new_url)
390
lock_tree_or_branch(working_tree, branch)
350
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
391
351
if consider_relpath and relpath != '':
392
352
if working_tree is not None and apply_view:
393
353
views.check_path_in_view(working_tree, relpath)
394
354
specific_files.append(relpath)
395
355
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
396
basis_is_default=working_tree is None)
356
basis_is_default=working_tree is None)
399
358
# Get the specific files (all files is None, no files is [])
400
359
if make_paths_wt_relative and working_tree is not None:
401
other_paths = working_tree.safe_relpath_files(
361
from bzrlib.builtins import safe_relpath_files
362
other_paths = safe_relpath_files(working_tree, other_paths,
403
363
apply_view=apply_view)
364
except errors.FileInWrongBranch:
365
raise errors.BzrCommandError("Files are in different branches")
404
366
specific_files.extend(other_paths)
405
367
if len(specific_files) == 0:
406
368
specific_files = None
407
if (working_tree is not None and working_tree.supports_views() and
369
if (working_tree is not None and working_tree.supports_views()
409
371
view_files = working_tree.views.lookup_view()
411
373
specific_files = view_files
412
374
view_str = views.view_display_str(view_files)
413
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
375
note("*** Ignoring files outside view. View is %s" % view_str)
415
377
# Get extra trees that ought to be searched for file-ids
416
378
extra_trees = None
417
379
if working_tree is not None and working_tree not in (old_tree, new_tree):
418
380
extra_trees = (working_tree,)
419
return (old_tree, new_tree, old_branch, new_branch,
420
specific_files, extra_trees)
381
return old_tree, new_tree, specific_files, extra_trees
423
383
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
424
384
if branch is None and tree is not None:
439
399
old_label='a/', new_label='b/',
440
400
extra_trees=None,
441
401
path_encoding='utf8',
444
context=DEFAULT_CONTEXT_AMOUNT):
445
403
"""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)
409
Include only changes to these files - None for all changes.
411
external_diff_options
412
If set, use an external GNU diff and pass these options.
415
If set, more Trees to use for looking up file ids
418
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
423
if extra_trees is not None:
463
424
for tree in extra_trees:
465
426
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)
428
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
430
external_diff_options,
431
old_label, new_label, using)
472
432
return differ.show_diff(specific_files, extra_trees)
474
434
new_tree.unlock()
475
435
if extra_trees is not None:
476
436
for tree in extra_trees:
480
def _patch_header_date(tree, path):
442
def _patch_header_date(tree, file_id, path):
481
443
"""Returns a timestamp suitable for use in a patch header."""
483
mtime = tree.get_file_mtime(path)
484
except FileTimestampUnavailable:
444
mtime = tree.get_file_mtime(file_id, path)
486
445
return timestamp.format_patch_date(mtime)
489
448
def get_executable_change(old_is_x, new_is_x):
490
descr = {True: b"+x", False: b"-x", None: b"??"}
449
descr = { True:"+x", False:"-x", None:"??" }
491
450
if old_is_x != new_is_x:
492
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
451
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
652
609
if 'file' not in (old_kind, new_kind):
653
610
return self.CANNOT_DIFF
611
from_file_id = to_file_id = file_id
654
612
if old_kind == 'file':
655
old_date = _patch_header_date(self.old_tree, old_path)
613
old_date = _patch_header_date(self.old_tree, file_id, old_path)
656
614
elif old_kind is None:
657
615
old_date = self.EPOCH_DATE
659
618
return self.CANNOT_DIFF
660
619
if new_kind == 'file':
661
new_date = _patch_header_date(self.new_tree, new_path)
620
new_date = _patch_header_date(self.new_tree, file_id, new_path)
662
621
elif new_kind is None:
663
622
new_date = self.EPOCH_DATE
665
625
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)
626
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
627
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
628
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):
631
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
632
from_path=None, to_path=None):
673
633
"""Diff the content of given files in two trees
675
:param from_path: The path in the from tree. If None,
635
:param from_file_id: The id of the file in the from tree. If None,
676
636
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,
637
:param to_file_id: The id of the file in the to tree. This may refer
638
to a different file from from_file_id. If None,
679
639
the file is not present in the to tree.
640
:param from_path: The path in the from tree or None if unknown.
641
: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:
643
def _get_text(tree, file_id, path):
644
if file_id is not None:
645
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)
649
from_text = _get_text(self.old_tree, from_file_id, from_path)
650
to_text = _get_text(self.new_tree, to_file_id, to_path)
691
651
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
653
except errors.BinaryFile:
695
654
self.to_file.write(
696
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
655
("Binary files %s and %s differ\n" %
656
(from_label, to_label)).encode(self.path_encoding))
698
657
return self.CHANGED
704
663
path_encoding='utf-8'):
705
664
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
665
self.command_template = command_template
707
self._root = osutils.mkdtemp(prefix='brz-diff-')
666
self._root = osutils.mkdtemp(prefix='bzr-diff-')
710
669
def from_string(klass, command_string, old_tree, new_tree, to_file,
711
670
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'])
671
command_template = commands.shlex_split_unicode(command_string)
672
command_template.extend(['%(old_path)s', '%(new_path)s'])
715
673
return klass(command_template, old_tree, new_tree, to_file,
719
def make_from_diff_tree(klass, command_string, external_diff_options=None):
677
def make_from_diff_tree(klass, command_string):
720
678
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,
679
return klass.from_string(command_string, diff_tree.old_tree,
725
680
diff_tree.new_tree, diff_tree.to_file)
726
681
return from_diff_tree
728
683
def _get_command(self, old_path, new_path):
729
684
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
685
return [t % my_map for t in self.command_template]
743
687
def _execute(self, old_path, new_path):
744
688
command = self._get_command(old_path, new_path)
746
690
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
749
693
if e.errno == errno.ENOENT:
750
694
raise errors.ExecutableMissing(command[0])
753
697
self.to_file.write(proc.stdout.read())
755
698
return proc.wait()
757
700
def _try_symlink_root(self, tree, prefix):
758
if (getattr(tree, 'abspath', None) is None or
759
not osutils.host_os_dereferences_symlinks()):
701
if (getattr(tree, 'abspath', None) is None
702
or not osutils.host_os_dereferences_symlinks()):
762
705
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
764
707
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,
798
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)
804
if not force_temp and self._try_symlink_root(tree, prefix):
711
def _write_file(self, file_id, tree, prefix, relpath):
712
full_path = osutils.pathjoin(self._root, prefix, relpath)
713
if self._try_symlink_root(tree, prefix):
806
715
parent_dir = osutils.dirname(full_path)
808
717
os.makedirs(parent_dir)
810
719
if e.errno != errno.EEXIST:
812
source = tree.get_file(relpath)
721
source = tree.get_file(file_id, relpath)
814
with open(full_path, 'wb') as target:
723
target = open(full_path, 'wb')
815
725
osutils.pumpfile(source, target)
819
mtime = tree.get_file_mtime(relpath)
820
except FileTimestampUnavailable:
823
os.utime(full_path, (mtime, mtime))
825
osutils.make_readonly(full_path)
730
osutils.make_readonly(full_path)
731
mtime = tree.get_file_mtime(file_id)
732
os.utime(full_path, (mtime, mtime))
828
def _prepare_files(self, old_path, new_path, force_temp=False,
829
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)
735
def _prepare_files(self, file_id, old_path, new_path):
736
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
738
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
835
740
return old_disk_path, new_disk_path
837
742
def finish(self):
839
744
osutils.rmtree(self._root)
841
746
if e.errno != errno.ENOENT:
842
747
mutter("The temporary directory \"%s\" was not "
843
"cleanly removed: %s." % (self._root, e))
748
"cleanly removed: %s." % (self._root, e))
845
def diff(self, old_path, new_path, old_kind, new_kind):
750
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
846
751
if (old_kind, new_kind) != ('file', 'file'):
847
752
return DiffPath.CANNOT_DIFF
848
(old_disk_path, new_disk_path) = self._prepare_files(
850
self._execute(old_disk_path, new_disk_path)
852
def edit_file(self, old_path, new_path):
853
"""Use this tool to edit a file.
855
A temporary copy will be edited, and the new contents will be
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)
862
command = self._get_command(old_abs_path, new_abs_path)
863
subprocess.call(command, cwd=self._root)
864
with open(new_abs_path, 'rb') as new_file:
865
return new_file.read()
753
self._prepare_files(file_id, old_path, new_path)
754
self._execute(osutils.pathjoin('old', old_path),
755
osutils.pathjoin('new', new_path))
868
758
class DiffTree(object):
926
815
:param using: Commandline to use to invoke an external diff tool
928
817
if using is not None:
929
extra_factories = [DiffFromTool.make_from_diff_tree(
930
using, external_diff_options)]
818
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
932
820
extra_factories = []
933
821
if external_diff_options:
934
822
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.
823
def diff_file(olab, olines, nlab, nlines, to_file):
940
824
external_diff(olab, olines, nlab, nlines, to_file, opts)
942
826
diff_file = internal_diff
943
827
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
944
old_label, new_label, diff_file, context_lines=context_lines)
828
old_label, new_label, diff_file)
945
829
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
948
832
def show_diff(self, specific_files, extra_trees=None):
949
833
"""Write tree diff to self.to_file
951
:param specific_files: the specific files to compare (recursive)
835
:param sepecific_files: the specific files to compare (recursive)
952
836
:param extra_trees: extra trees to use for mapping paths to file_ids
990
872
renamed = (parent[0], name[0]) != (parent[1], name[1])
992
874
properties_changed = []
993
properties_changed.extend(
994
get_executable_change(executable[0], executable[1]))
875
properties_changed.extend(get_executable_change(executable[0], executable[1]))
996
877
if properties_changed:
997
prop_str = b" (properties changed: %s)" % (
998
b", ".join(properties_changed),)
878
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1002
882
if (old_present, new_present) == (True, False):
1003
self.to_file.write(b"=== removed %s '%s'\n" %
1004
(kind[0].encode('ascii'), oldpath_encoded))
883
self.to_file.write("=== removed %s '%s'\n" %
884
(kind[0], oldpath_encoded))
1005
885
newpath = oldpath
1006
886
elif (old_present, new_present) == (False, True):
1007
self.to_file.write(b"=== added %s '%s'\n" %
1008
(kind[1].encode('ascii'), newpath_encoded))
887
self.to_file.write("=== added %s '%s'\n" %
888
(kind[1], newpath_encoded))
1009
889
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))
891
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
892
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1014
894
# if it was produced by iter_changes, it must be
1015
895
# 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))
896
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
897
newpath_encoded, prop_str))
1018
898
if changed_content:
1019
self._diff(oldpath, newpath, kind[0], kind[1])
899
self._diff(file_id, oldpath, newpath, kind[0], kind[1])
1023
903
return has_changes
1025
def diff(self, old_path, new_path):
905
def diff(self, file_id, old_path, new_path):
1026
906
"""Perform a diff of a single file
908
:param file_id: file-id of the file
1028
909
:param old_path: The path of the file in the old tree
1029
910
:param new_path: The path of the file in the new tree
1031
if old_path is None:
913
old_kind = self.old_tree.kind(file_id)
914
except (errors.NoSuchId, errors.NoSuchFile):
1034
old_kind = self.old_tree.kind(old_path)
1035
if new_path is None:
917
new_kind = self.new_tree.kind(file_id)
918
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)
920
self._diff(file_id, old_path, new_path, old_kind, new_kind)
923
def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
924
result = DiffPath._diff_many(self.differs, file_id, old_path,
925
new_path, old_kind, new_kind)
1044
926
if result is DiffPath.CANNOT_DIFF:
1045
927
error_path = new_path
1046
928
if error_path is None:
1047
929
error_path = old_path
1048
930
raise errors.NoDiffFound(error_path)
1051
format_registry = Registry()
1052
format_registry.register('default', DiffTree)