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
321
default_location = path_list[0]
360
322
other_paths = path_list[1:]
362
def lock_tree_or_branch(wt, br):
365
add_cleanup(wt.unlock)
368
add_cleanup(br.unlock)
370
324
# Get the old location
371
325
specific_files = []
372
326
if old_url is None:
373
327
old_url = default_location
374
328
working_tree, branch, relpath = \
375
controldir.ControlDir.open_containing_tree_or_branch(old_url)
376
lock_tree_or_branch(working_tree, branch)
329
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
377
330
if consider_relpath and relpath != '':
378
if working_tree is not None and apply_view:
379
views.check_path_in_view(working_tree, relpath)
380
331
specific_files.append(relpath)
381
332
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
384
334
# Get the new location
385
335
if new_url is None:
386
336
new_url = default_location
387
337
if new_url != old_url:
388
338
working_tree, branch, relpath = \
389
controldir.ControlDir.open_containing_tree_or_branch(new_url)
390
lock_tree_or_branch(working_tree, branch)
339
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
391
340
if consider_relpath and relpath != '':
392
if working_tree is not None and apply_view:
393
views.check_path_in_view(working_tree, relpath)
394
341
specific_files.append(relpath)
395
342
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
396
basis_is_default=working_tree is None)
343
basis_is_default=working_tree is None)
399
345
# Get the specific files (all files is None, no files is [])
400
346
if make_paths_wt_relative and working_tree is not None:
401
other_paths = working_tree.safe_relpath_files(
403
apply_view=apply_view)
347
other_paths = _relative_paths_in_tree(working_tree, other_paths)
404
348
specific_files.extend(other_paths)
405
349
if len(specific_files) == 0:
406
350
specific_files = None
407
if (working_tree is not None and working_tree.supports_views() and
409
view_files = working_tree.views.lookup_view()
411
specific_files = view_files
412
view_str = views.view_display_str(view_files)
413
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
415
352
# Get extra trees that ought to be searched for file-ids
416
353
extra_trees = None
417
354
if working_tree is not None and working_tree not in (old_tree, new_tree):
418
355
extra_trees = (working_tree,)
419
return (old_tree, new_tree, old_branch, new_branch,
420
specific_files, extra_trees)
356
return old_tree, new_tree, specific_files, extra_trees
423
359
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
434
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")
437
388
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
438
389
external_diff_options=None,
439
390
old_label='a/', new_label='b/',
440
391
extra_trees=None,
441
392
path_encoding='utf8',
444
context=DEFAULT_CONTEXT_AMOUNT):
445
394
"""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)
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
458
context = DEFAULT_CONTEXT_AMOUNT
459
if format_cls is None:
460
format_cls = DiffTree
461
with old_tree.lock_read():
462
414
if extra_trees is not None:
463
415
for tree in extra_trees:
465
417
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)
419
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
421
external_diff_options,
422
old_label, new_label, using)
472
423
return differ.show_diff(specific_files, extra_trees)
474
425
new_tree.unlock()
475
426
if extra_trees is not None:
476
427
for tree in extra_trees:
480
def _patch_header_date(tree, path):
433
def _patch_header_date(tree, file_id, path):
481
434
"""Returns a timestamp suitable for use in a patch header."""
483
mtime = tree.get_file_mtime(path)
484
except FileTimestampUnavailable:
435
mtime = tree.get_file_mtime(file_id, path)
486
436
return timestamp.format_patch_date(mtime)
439
@deprecated_function(one_three)
440
def get_prop_change(meta_modified):
442
return " (properties changed)"
489
446
def get_executable_change(old_is_x, new_is_x):
490
descr = {True: b"+x", False: b"-x", None: b"??"}
447
descr = { True:"+x", False:"-x", None:"??" }
491
448
if old_is_x != new_is_x:
492
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
449
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
652
607
if 'file' not in (old_kind, new_kind):
653
608
return self.CANNOT_DIFF
609
from_file_id = to_file_id = file_id
654
610
if old_kind == 'file':
655
old_date = _patch_header_date(self.old_tree, old_path)
611
old_date = _patch_header_date(self.old_tree, file_id, old_path)
656
612
elif old_kind is None:
657
613
old_date = self.EPOCH_DATE
659
616
return self.CANNOT_DIFF
660
617
if new_kind == 'file':
661
new_date = _patch_header_date(self.new_tree, new_path)
618
new_date = _patch_header_date(self.new_tree, file_id, new_path)
662
619
elif new_kind is None:
663
620
new_date = self.EPOCH_DATE
665
623
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)
624
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
625
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
626
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):
628
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
673
629
"""Diff the content of given files in two trees
675
:param from_path: The path in the from tree. If None,
631
:param from_file_id: The id of the file in the from tree. If None,
676
632
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,
633
:param to_file_id: The id of the file in the to tree. This may refer
634
to a different file from from_file_id. If None,
679
635
the file is not present in the to tree.
681
def _get_text(tree, path):
685
return tree.get_file_lines(path)
686
except errors.NoSuchFile:
637
def _get_text(tree, file_id):
638
if file_id is not None:
639
return tree.get_file(file_id).readlines()
689
from_text = _get_text(self.old_tree, from_path)
690
to_text = _get_text(self.new_tree, to_path)
643
from_text = _get_text(self.old_tree, from_file_id)
644
to_text = _get_text(self.new_tree, to_file_id)
691
645
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
647
except errors.BinaryFile:
695
648
self.to_file.write(
696
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
649
("Binary files %s and %s differ\n" %
650
(from_label, to_label)).encode(self.path_encoding))
698
651
return self.CHANGED
704
657
path_encoding='utf-8'):
705
658
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
659
self.command_template = command_template
707
self._root = osutils.mkdtemp(prefix='brz-diff-')
660
self._root = osutils.mkdtemp(prefix='bzr-diff-')
710
663
def from_string(klass, command_string, old_tree, new_tree, to_file,
711
664
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'])
665
command_template = commands.shlex_split_unicode(command_string)
666
command_template.extend(['%(old_path)s', '%(new_path)s'])
715
667
return klass(command_template, old_tree, new_tree, to_file,
719
def make_from_diff_tree(klass, command_string, external_diff_options=None):
671
def make_from_diff_tree(klass, command_string):
720
672
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,
673
return klass.from_string(command_string, diff_tree.old_tree,
725
674
diff_tree.new_tree, diff_tree.to_file)
726
675
return from_diff_tree
728
677
def _get_command(self, old_path, new_path):
729
678
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
679
return [t % my_map for t in self.command_template]
743
681
def _execute(self, old_path, new_path):
744
682
command = self._get_command(old_path, new_path)
746
684
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
749
687
if e.errno == errno.ENOENT:
750
688
raise errors.ExecutableMissing(command[0])
753
691
self.to_file.write(proc.stdout.read())
755
692
return proc.wait()
757
694
def _try_symlink_root(self, tree, prefix):
758
if (getattr(tree, 'abspath', None) is None or
759
not osutils.host_os_dereferences_symlinks()):
695
if (getattr(tree, 'abspath', None) is None
696
or not osutils.host_os_dereferences_symlinks()):
762
699
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
764
701
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):
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):
806
709
parent_dir = osutils.dirname(full_path)
808
711
os.makedirs(parent_dir)
810
713
if e.errno != errno.EEXIST:
812
source = tree.get_file(relpath)
715
source = tree.get_file(file_id, relpath)
814
with open(full_path, 'wb') as target:
717
target = open(full_path, 'wb')
815
719
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)
724
osutils.make_readonly(full_path)
725
mtime = tree.get_file_mtime(file_id)
726
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)
729
def _prepare_files(self, file_id, old_path, new_path):
730
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
732
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
835
734
return old_disk_path, new_disk_path
837
736
def finish(self):
839
osutils.rmtree(self._root)
841
if e.errno != errno.ENOENT:
842
mutter("The temporary directory \"%s\" was not "
843
"cleanly removed: %s." % (self._root, e))
737
osutils.rmtree(self._root)
845
def diff(self, old_path, new_path, old_kind, new_kind):
739
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
846
740
if (old_kind, new_kind) != ('file', 'file'):
847
741
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()
742
self._prepare_files(file_id, old_path, new_path)
743
self._execute(osutils.pathjoin('old', old_path),
744
osutils.pathjoin('new', new_path))
868
747
class DiffTree(object):
926
804
:param using: Commandline to use to invoke an external diff tool
928
806
if using is not None:
929
extra_factories = [DiffFromTool.make_from_diff_tree(
930
using, external_diff_options)]
807
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
932
809
extra_factories = []
933
810
if external_diff_options:
934
811
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.
812
def diff_file(olab, olines, nlab, nlines, to_file):
940
813
external_diff(olab, olines, nlab, nlines, to_file, opts)
942
815
diff_file = internal_diff
943
816
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
944
old_label, new_label, diff_file, context_lines=context_lines)
817
old_label, new_label, diff_file)
945
818
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
948
821
def show_diff(self, specific_files, extra_trees=None):
949
822
"""Write tree diff to self.to_file
951
:param specific_files: the specific files to compare (recursive)
824
:param sepecific_files: the specific files to compare (recursive)
952
825
:param extra_trees: extra trees to use for mapping paths to file_ids
990
861
renamed = (parent[0], name[0]) != (parent[1], name[1])
992
863
properties_changed = []
993
properties_changed.extend(
994
get_executable_change(executable[0], executable[1]))
864
properties_changed.extend(get_executable_change(executable[0], executable[1]))
996
866
if properties_changed:
997
prop_str = b" (properties changed: %s)" % (
998
b", ".join(properties_changed),)
867
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1002
871
if (old_present, new_present) == (True, False):
1003
self.to_file.write(b"=== removed %s '%s'\n" %
1004
(kind[0].encode('ascii'), oldpath_encoded))
872
self.to_file.write("=== removed %s '%s'\n" %
873
(kind[0], oldpath_encoded))
1005
874
newpath = oldpath
1006
875
elif (old_present, new_present) == (False, True):
1007
self.to_file.write(b"=== added %s '%s'\n" %
1008
(kind[1].encode('ascii'), newpath_encoded))
876
self.to_file.write("=== added %s '%s'\n" %
877
(kind[1], newpath_encoded))
1009
878
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))
880
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
881
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1014
883
# if it was produced by iter_changes, it must be
1015
884
# 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))
885
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
886
newpath_encoded, prop_str))
1018
887
if changed_content:
1019
self._diff(oldpath, newpath, kind[0], kind[1])
888
self.diff(file_id, oldpath, newpath)
1023
892
return has_changes
1025
def diff(self, old_path, new_path):
894
def diff(self, file_id, old_path, new_path):
1026
895
"""Perform a diff of a single file
897
:param file_id: file-id of the file
1028
898
:param old_path: The path of the file in the old tree
1029
899
:param new_path: The path of the file in the new tree
1031
if old_path is None:
902
old_kind = self.old_tree.kind(file_id)
903
except (errors.NoSuchId, errors.NoSuchFile):
1034
old_kind = self.old_tree.kind(old_path)
1035
if new_path is None:
906
new_kind = self.new_tree.kind(file_id)
907
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)
910
result = DiffPath._diff_many(self.differs, file_id, old_path,
911
new_path, old_kind, new_kind)
1044
912
if result is DiffPath.CANNOT_DIFF:
1045
913
error_path = new_path
1046
914
if error_path is None:
1047
915
error_path = old_path
1048
916
raise errors.NoDiffFound(error_path)
1051
format_registry = Registry()
1052
format_registry.register('default', DiffTree)