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')
102
# work around for difflib emitting random spaces after the label
103
ud[0] = ud[0][:-2] + '\n'
104
ud[1] = ud[1][:-2] + '\n'
114
107
to_file.write(line)
115
if not line.endswith(b'\n'):
116
to_file.write(b"\n\\ No newline at end of file\n")
108
if not line.endswith('\n'):
109
to_file.write("\n\\ No newline at end of file\n")
120
113
def _spawn_external_diff(diffcmd, capture_errors=True):
121
"""Spawn the external diff process, and return the child handle.
114
"""Spawn the externall diff process, and return the child handle.
123
116
:param diffcmd: The command list to spawn
124
117
: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,
150
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
195
152
"""Display a diff by calling out to the external diff program."""
196
153
# 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-')
156
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
157
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
201
158
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
202
159
newtmpf = os.fdopen(newtmp_fd, 'wb')
359
324
default_location = path_list[0]
360
325
other_paths = path_list[1:]
362
def lock_tree_or_branch(wt, br):
365
add_cleanup(wt.unlock)
368
add_cleanup(br.unlock)
370
327
# Get the old location
371
328
specific_files = []
372
329
if old_url is None:
373
330
old_url = default_location
374
331
working_tree, branch, relpath = \
375
controldir.ControlDir.open_containing_tree_or_branch(old_url)
376
lock_tree_or_branch(working_tree, branch)
332
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
377
333
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
334
specific_files.append(relpath)
381
335
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
384
337
# Get the new location
385
338
if new_url is None:
386
339
new_url = default_location
387
340
if new_url != old_url:
388
341
working_tree, branch, relpath = \
389
controldir.ControlDir.open_containing_tree_or_branch(new_url)
390
lock_tree_or_branch(working_tree, branch)
342
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
391
343
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
344
specific_files.append(relpath)
395
345
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
396
basis_is_default=working_tree is None)
346
basis_is_default=working_tree is None)
399
348
# Get the specific files (all files is None, no files is [])
400
349
if make_paths_wt_relative and working_tree is not None:
401
other_paths = working_tree.safe_relpath_files(
403
apply_view=apply_view)
350
other_paths = _relative_paths_in_tree(working_tree, other_paths)
404
351
specific_files.extend(other_paths)
405
352
if len(specific_files) == 0:
406
353
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
355
# Get extra trees that ought to be searched for file-ids
416
356
extra_trees = None
417
357
if working_tree is not None and working_tree not in (old_tree, new_tree):
418
358
extra_trees = (working_tree,)
419
return (old_tree, new_tree, old_branch, new_branch,
420
specific_files, extra_trees)
359
return old_tree, new_tree, specific_files, extra_trees
423
362
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
439
396
old_label='a/', new_label='b/',
440
397
extra_trees=None,
441
398
path_encoding='utf8',
444
context=DEFAULT_CONTEXT_AMOUNT):
445
400
"""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)
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
458
context = DEFAULT_CONTEXT_AMOUNT
459
if format_cls is None:
460
format_cls = DiffTree
461
with old_tree.lock_read():
462
420
if extra_trees is not None:
463
421
for tree in extra_trees:
465
423
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)
425
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
427
external_diff_options,
428
old_label, new_label, using)
472
429
return differ.show_diff(specific_files, extra_trees)
474
431
new_tree.unlock()
475
432
if extra_trees is not None:
476
433
for tree in extra_trees:
480
def _patch_header_date(tree, path):
439
def _patch_header_date(tree, file_id, path):
481
440
"""Returns a timestamp suitable for use in a patch header."""
483
mtime = tree.get_file_mtime(path)
484
except FileTimestampUnavailable:
441
mtime = tree.get_file_mtime(file_id, path)
486
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)"
489
471
def get_executable_change(old_is_x, new_is_x):
490
descr = {True: b"+x", False: b"-x", None: b"??"}
472
descr = { True:"+x", False:"-x", None:"??" }
491
473
if old_is_x != new_is_x:
492
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
474
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
652
632
if 'file' not in (old_kind, new_kind):
653
633
return self.CANNOT_DIFF
634
from_file_id = to_file_id = file_id
654
635
if old_kind == 'file':
655
old_date = _patch_header_date(self.old_tree, old_path)
636
old_date = _patch_header_date(self.old_tree, file_id, old_path)
656
637
elif old_kind is None:
657
638
old_date = self.EPOCH_DATE
659
641
return self.CANNOT_DIFF
660
642
if new_kind == 'file':
661
new_date = _patch_header_date(self.new_tree, new_path)
643
new_date = _patch_header_date(self.new_tree, file_id, new_path)
662
644
elif new_kind is None:
663
645
new_date = self.EPOCH_DATE
665
648
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)
649
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
650
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
651
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):
653
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
673
654
"""Diff the content of given files in two trees
675
: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,
676
657
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,
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,
679
660
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:
662
def _get_text(tree, file_id):
663
if file_id is not None:
664
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)
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
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
674
("Binary files %s and %s differ\n" %
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 = tempfile.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])
753
716
self.to_file.write(proc.stdout.read())
755
717
return proc.wait()
757
719
def _try_symlink_root(self, tree, prefix):
758
if (getattr(tree, 'abspath', None) is None or
759
not osutils.host_os_dereferences_symlinks()):
720
if (getattr(tree, 'abspath', None) is None
721
or not osutils.host_os_dereferences_symlinks()):
762
724
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
764
726
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):
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):
806
734
parent_dir = osutils.dirname(full_path)
808
736
os.makedirs(parent_dir)
810
738
if e.errno != errno.EEXIST:
812
source = tree.get_file(relpath)
740
source = tree.get_file(file_id, relpath)
814
with open(full_path, 'wb') as target:
742
target = open(full_path, 'wb')
815
744
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)
749
osutils.make_readonly(full_path)
750
mtime = tree.get_file_mtime(file_id)
751
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)
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',
835
759
return old_disk_path, new_disk_path
837
761
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))
762
osutils.rmtree(self._root)
845
def diff(self, old_path, new_path, old_kind, new_kind):
764
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
846
765
if (old_kind, new_kind) != ('file', 'file'):
847
766
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()
767
self._prepare_files(file_id, old_path, new_path)
768
self._execute(osutils.pathjoin('old', old_path),
769
osutils.pathjoin('new', new_path))
868
772
class DiffTree(object):
926
829
:param using: Commandline to use to invoke an external diff tool
928
831
if using is not None:
929
extra_factories = [DiffFromTool.make_from_diff_tree(
930
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()
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.
837
def diff_file(olab, olines, nlab, nlines, to_file):
940
838
external_diff(olab, olines, nlab, nlines, to_file, opts)
942
840
diff_file = internal_diff
943
841
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
944
old_label, new_label, diff_file, context_lines=context_lines)
842
old_label, new_label, diff_file)
945
843
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
948
846
def show_diff(self, specific_files, extra_trees=None):
949
847
"""Write tree diff to self.to_file
951
:param specific_files: the specific files to compare (recursive)
849
:param sepecific_files: the specific files to compare (recursive)
952
850
:param extra_trees: extra trees to use for mapping paths to file_ids
990
886
renamed = (parent[0], name[0]) != (parent[1], name[1])
992
888
properties_changed = []
993
properties_changed.extend(
994
get_executable_change(executable[0], executable[1]))
889
properties_changed.extend(get_executable_change(executable[0], executable[1]))
996
891
if properties_changed:
997
prop_str = b" (properties changed: %s)" % (
998
b", ".join(properties_changed),)
892
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1002
896
if (old_present, new_present) == (True, False):
1003
self.to_file.write(b"=== removed %s '%s'\n" %
1004
(kind[0].encode('ascii'), oldpath_encoded))
897
self.to_file.write("=== removed %s '%s'\n" %
898
(kind[0], oldpath_encoded))
1005
899
newpath = oldpath
1006
900
elif (old_present, new_present) == (False, True):
1007
self.to_file.write(b"=== added %s '%s'\n" %
1008
(kind[1].encode('ascii'), newpath_encoded))
901
self.to_file.write("=== added %s '%s'\n" %
902
(kind[1], newpath_encoded))
1009
903
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))
905
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
906
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1014
908
# if it was produced by iter_changes, it must be
1015
909
# 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))
910
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
911
newpath_encoded, prop_str))
1018
912
if changed_content:
1019
self._diff(oldpath, newpath, kind[0], kind[1])
913
self.diff(file_id, oldpath, newpath)
1023
917
return has_changes
1025
def diff(self, old_path, new_path):
919
def diff(self, file_id, old_path, new_path):
1026
920
"""Perform a diff of a single file
922
:param file_id: file-id of the file
1028
923
:param old_path: The path of the file in the old tree
1029
924
:param new_path: The path of the file in the new tree
1031
if old_path is None:
927
old_kind = self.old_tree.kind(file_id)
928
except (errors.NoSuchId, errors.NoSuchFile):
1034
old_kind = self.old_tree.kind(old_path)
1035
if new_path is None:
931
new_kind = self.new_tree.kind(file_id)
932
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)
935
result = DiffPath._diff_many(self.differs, file_id, old_path,
936
new_path, old_kind, new_kind)
1044
937
if result is DiffPath.CANNOT_DIFF:
1045
938
error_path = new_path
1046
939
if error_path is None:
1047
940
error_path = old_path
1048
941
raise errors.NoDiffFound(error_path)
1051
format_registry = Registry()
1052
format_registry.register('default', DiffTree)