87
74
# In the meantime we at least make sure the patch isn't
78
# Special workaround for Python2.3, where difflib fails if
79
# both sequences are empty.
80
if not oldlines and not newlines:
90
83
if allow_binary is False:
91
84
textfile.check_text_lines(oldlines)
92
85
textfile.check_text_lines(newlines)
94
87
if sequence_matcher is None:
95
88
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)
89
ud = patiencediff.unified_diff(oldlines, newlines,
90
fromfile=old_filename.encode(path_encoding),
91
tofile=new_filename.encode(path_encoding),
92
sequencematcher=sequence_matcher)
104
if len(ud) == 0: # Identical contents, nothing to do
95
if len(ud) == 0: # Identical contents, nothing to do
106
97
# work-around for difflib being too smart for its own good
107
98
# 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')
100
ud[2] = ud[2].replace('-1,0', '-0,0')
110
101
elif not newlines:
111
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
102
ud[2] = ud[2].replace('+1,0', '+0,0')
114
105
to_file.write(line)
115
if not line.endswith(b'\n'):
116
to_file.write(b"\n\\ No newline at end of file\n")
106
if not line.endswith('\n'):
107
to_file.write("\n\\ No newline at end of file\n")
120
111
def _spawn_external_diff(diffcmd, capture_errors=True):
121
"""Spawn the external diff process, and return the child handle.
112
"""Spawn the externall diff process, and return the child handle.
123
114
:param diffcmd: The command list to spawn
124
115
:param capture_errors: Capture stderr as well as setting LANG=C
359
326
default_location = path_list[0]
360
327
other_paths = path_list[1:]
362
def lock_tree_or_branch(wt, br):
365
add_cleanup(wt.unlock)
368
add_cleanup(br.unlock)
370
329
# Get the old location
371
330
specific_files = []
372
331
if old_url is None:
373
332
old_url = default_location
374
333
working_tree, branch, relpath = \
375
controldir.ControlDir.open_containing_tree_or_branch(old_url)
376
lock_tree_or_branch(working_tree, branch)
334
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
377
335
if consider_relpath and relpath != '':
378
336
if working_tree is not None and apply_view:
379
337
views.check_path_in_view(working_tree, relpath)
380
338
specific_files.append(relpath)
381
339
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
384
341
# Get the new location
385
342
if new_url is None:
386
343
new_url = default_location
387
344
if new_url != old_url:
388
345
working_tree, branch, relpath = \
389
controldir.ControlDir.open_containing_tree_or_branch(new_url)
390
lock_tree_or_branch(working_tree, branch)
346
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
391
347
if consider_relpath and relpath != '':
392
348
if working_tree is not None and apply_view:
393
349
views.check_path_in_view(working_tree, relpath)
394
350
specific_files.append(relpath)
395
351
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
396
basis_is_default=working_tree is None)
352
basis_is_default=working_tree is None)
399
354
# Get the specific files (all files is None, no files is [])
400
355
if make_paths_wt_relative and working_tree is not None:
401
other_paths = working_tree.safe_relpath_files(
357
from bzrlib.builtins import safe_relpath_files
358
other_paths = safe_relpath_files(working_tree, other_paths,
403
359
apply_view=apply_view)
360
except errors.FileInWrongBranch:
361
raise errors.BzrCommandError("Files are in different branches")
404
362
specific_files.extend(other_paths)
405
363
if len(specific_files) == 0:
406
364
specific_files = None
407
if (working_tree is not None and working_tree.supports_views() and
365
if (working_tree is not None and working_tree.supports_views()
409
367
view_files = working_tree.views.lookup_view()
411
369
specific_files = view_files
412
370
view_str = views.view_display_str(view_files)
413
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
371
note("*** ignoring files outside view: %s" % view_str)
415
373
# Get extra trees that ought to be searched for file-ids
416
374
extra_trees = None
417
375
if working_tree is not None and working_tree not in (old_tree, new_tree):
418
376
extra_trees = (working_tree,)
419
return (old_tree, new_tree, old_branch, new_branch,
420
specific_files, extra_trees)
377
return old_tree, new_tree, specific_files, extra_trees
423
379
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
424
380
if branch is None and tree is not None:
439
395
old_label='a/', new_label='b/',
440
396
extra_trees=None,
441
397
path_encoding='utf8',
444
context=DEFAULT_CONTEXT_AMOUNT):
445
399
"""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)
405
Include only changes to these files - None for all changes.
407
external_diff_options
408
If set, use an external GNU diff and pass these options.
411
If set, more Trees to use for looking up file ids
414
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
419
if extra_trees is not None:
463
420
for tree in extra_trees:
465
422
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)
424
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
426
external_diff_options,
427
old_label, new_label, using)
472
428
return differ.show_diff(specific_files, extra_trees)
474
430
new_tree.unlock()
475
431
if extra_trees is not None:
476
432
for tree in extra_trees:
480
438
def _patch_header_date(tree, file_id, path):
481
439
"""Returns a timestamp suitable for use in a patch header."""
483
mtime = tree.get_file_mtime(path)
484
except FileTimestampUnavailable:
440
mtime = tree.get_file_mtime(file_id, path)
486
441
return timestamp.format_patch_date(mtime)
444
@deprecated_function(one_three)
445
def get_prop_change(meta_modified):
447
return " (properties changed)"
489
451
def get_executable_change(old_is_x, new_is_x):
490
descr = {True: b"+x", False: b"-x", None: b"??"}
452
descr = { True:"+x", False:"-x", None:"??" }
491
453
if old_is_x != new_is_x:
492
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
454
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
670
626
to_file_id = None
672
628
return self.CANNOT_DIFF
673
from_label = '%s%s\t%s' % (self.old_label, old_path,
675
to_label = '%s%s\t%s' % (self.new_label, new_path,
677
return self.diff_text(old_path, new_path, from_label, to_label,
678
from_file_id, to_file_id)
629
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
630
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
631
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
680
def diff_text(self, from_path, to_path, from_label, to_label,
681
from_file_id=None, to_file_id=None):
633
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
682
634
"""Diff the content of given files in two trees
684
:param from_path: The path in the from tree. If None,
636
:param from_file_id: The id of the file in the from tree. If None,
685
637
the file is not present in the from tree.
686
:param to_path: The path in the to tree. This may refer
687
to a different file from from_path. If None,
638
:param to_file_id: The id of the file in the to tree. This may refer
639
to a different file from from_file_id. If None,
688
640
the file is not present in the to tree.
689
:param from_file_id: The id of the file in the from tree or None if
691
:param to_file_id: The id of the file in the to tree or None if
694
def _get_text(tree, file_id, path):
642
def _get_text(tree, file_id):
643
if file_id is not None:
644
return tree.get_file(file_id).readlines()
697
return tree.get_file_lines(path)
699
from_text = _get_text(self.old_tree, from_file_id, from_path)
700
to_text = _get_text(self.new_tree, to_file_id, to_path)
648
from_text = _get_text(self.old_tree, from_file_id)
649
to_text = _get_text(self.new_tree, to_file_id)
701
650
self.text_differ(from_label, from_text, to_label, to_text,
702
self.to_file, path_encoding=self.path_encoding,
703
context_lines=self.context_lines)
704
652
except errors.BinaryFile:
705
653
self.to_file.write(
706
("Binary files %s and %s differ\n" %
707
(from_label, to_label)).encode(self.path_encoding, 'replace'))
654
("Binary files %s and %s differ\n" %
655
(from_label, to_label)).encode(self.path_encoding))
708
656
return self.CHANGED
714
662
path_encoding='utf-8'):
715
663
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
716
664
self.command_template = command_template
717
self._root = osutils.mkdtemp(prefix='brz-diff-')
665
self._root = osutils.mkdtemp(prefix='bzr-diff-')
720
668
def from_string(klass, command_string, old_tree, new_tree, to_file,
721
669
path_encoding='utf-8'):
722
command_template = cmdline.split(command_string)
723
if '@' not in command_string:
724
command_template.extend(['@old_path', '@new_path'])
670
command_template = commands.shlex_split_unicode(command_string)
671
command_template.extend(['%(old_path)s', '%(new_path)s'])
725
672
return klass(command_template, old_tree, new_tree, to_file,
729
def make_from_diff_tree(klass, command_string, external_diff_options=None):
676
def make_from_diff_tree(klass, command_string):
730
677
def from_diff_tree(diff_tree):
731
full_command_string = [command_string]
732
if external_diff_options is not None:
733
full_command_string += ' ' + external_diff_options
734
return klass.from_string(full_command_string, diff_tree.old_tree,
678
return klass.from_string(command_string, diff_tree.old_tree,
735
679
diff_tree.new_tree, diff_tree.to_file)
736
680
return from_diff_tree
738
682
def _get_command(self, old_path, new_path):
739
683
my_map = {'old_path': old_path, 'new_path': new_path}
740
command = [AtTemplate(t).substitute(my_map) for t in
741
self.command_template]
742
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
745
if isinstance(c, text_type):
746
command_encoded.append(c.encode('mbcs'))
748
command_encoded.append(c)
749
return command_encoded
684
return [t % my_map for t in self.command_template]
753
686
def _execute(self, old_path, new_path):
754
687
command = self._get_command(old_path, new_path)
756
689
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
759
692
if e.errno == errno.ENOENT:
760
693
raise errors.ExecutableMissing(command[0])
763
696
self.to_file.write(proc.stdout.read())
765
697
return proc.wait()
767
699
def _try_symlink_root(self, tree, prefix):
768
if (getattr(tree, 'abspath', None) is None or
769
not osutils.host_os_dereferences_symlinks()):
700
if (getattr(tree, 'abspath', None) is None
701
or not osutils.host_os_dereferences_symlinks()):
772
704
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
774
706
if e.errno != errno.EEXIST:
780
"""Returns safe encoding for passing file path to diff tool"""
781
if sys.platform == 'win32':
784
# Don't fallback to 'utf-8' because subprocess may not be able to
785
# handle utf-8 correctly when locale is not utf-8.
786
return sys.getfilesystemencoding() or 'ascii'
788
def _is_safepath(self, path):
789
"""Return true if `path` may be able to pass to subprocess."""
792
return path == path.encode(fenc).decode(fenc)
796
def _safe_filename(self, prefix, relpath):
797
"""Replace unsafe character in `relpath` then join `self._root`,
798
`prefix` and `relpath`."""
800
# encoded_str.replace('?', '_') may break multibyte char.
801
# So we should encode, decode, then replace(u'?', u'_')
802
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
803
relpath_tmp = relpath_tmp.replace(u'?', u'_')
804
return osutils.pathjoin(self._root, prefix, relpath_tmp)
806
def _write_file(self, relpath, tree, prefix, force_temp=False,
807
allow_write=False, file_id=None):
808
if not force_temp and isinstance(tree, WorkingTree):
809
full_path = tree.abspath(relpath)
810
if self._is_safepath(full_path):
813
full_path = self._safe_filename(prefix, relpath)
814
if not force_temp and self._try_symlink_root(tree, prefix):
710
def _write_file(self, file_id, tree, prefix, relpath):
711
full_path = osutils.pathjoin(self._root, prefix, relpath)
712
if self._try_symlink_root(tree, prefix):
816
714
parent_dir = osutils.dirname(full_path)
818
716
os.makedirs(parent_dir)
820
718
if e.errno != errno.EEXIST:
822
source = tree.get_file(relpath)
720
source = tree.get_file(file_id, relpath)
824
with open(full_path, 'wb') as target:
722
target = open(full_path, 'wb')
825
724
osutils.pumpfile(source, target)
829
mtime = tree.get_file_mtime(relpath)
830
except FileTimestampUnavailable:
833
os.utime(full_path, (mtime, mtime))
835
osutils.make_readonly(full_path)
729
osutils.make_readonly(full_path)
730
mtime = tree.get_file_mtime(file_id)
731
os.utime(full_path, (mtime, mtime))
838
def _prepare_files(self, old_path, new_path, force_temp=False,
839
allow_write_new=False, file_id=None):
840
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
841
force_temp, file_id=file_id)
842
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
843
force_temp, file_id=file_id,
844
allow_write=allow_write_new)
734
def _prepare_files(self, file_id, old_path, new_path):
735
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
737
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
845
739
return old_disk_path, new_disk_path
847
741
def finish(self):
849
osutils.rmtree(self._root)
851
if e.errno != errno.ENOENT:
852
mutter("The temporary directory \"%s\" was not "
853
"cleanly removed: %s." % (self._root, e))
742
osutils.rmtree(self._root)
855
744
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
856
745
if (old_kind, new_kind) != ('file', 'file'):
857
746
return DiffPath.CANNOT_DIFF
858
(old_disk_path, new_disk_path) = self._prepare_files(
859
old_path, new_path, file_id=file_id)
860
self._execute(old_disk_path, new_disk_path)
862
def edit_file(self, old_path, new_path, file_id=None):
863
"""Use this tool to edit a file.
865
A temporary copy will be edited, and the new contents will be
868
:param file_id: The id of the file to edit.
869
:return: The new contents of the file.
871
old_abs_path, new_abs_path = self._prepare_files(
872
old_path, new_path, allow_write_new=True, force_temp=True,
874
command = self._get_command(old_abs_path, new_abs_path)
875
subprocess.call(command, cwd=self._root)
876
with open(new_abs_path, 'rb') as new_file:
877
return new_file.read()
747
self._prepare_files(file_id, old_path, new_path)
748
self._execute(osutils.pathjoin('old', old_path),
749
osutils.pathjoin('new', new_path))
880
752
class DiffTree(object):
938
809
:param using: Commandline to use to invoke an external diff tool
940
811
if using is not None:
941
extra_factories = [DiffFromTool.make_from_diff_tree(
942
using, external_diff_options)]
812
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
944
814
extra_factories = []
945
815
if external_diff_options:
946
816
opts = external_diff_options.split()
948
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
949
""":param path_encoding: not used but required
950
to match the signature of internal_diff.
817
def diff_file(olab, olines, nlab, nlines, to_file):
952
818
external_diff(olab, olines, nlab, nlines, to_file, opts)
954
820
diff_file = internal_diff
955
821
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
956
old_label, new_label, diff_file, context_lines=context_lines)
822
old_label, new_label, diff_file)
957
823
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
960
826
def show_diff(self, specific_files, extra_trees=None):
961
827
"""Write tree diff to self.to_file
963
:param specific_files: the specific files to compare (recursive)
829
:param sepecific_files: the specific files to compare (recursive)
964
830
:param extra_trees: extra trees to use for mapping paths to file_ids
1002
866
renamed = (parent[0], name[0]) != (parent[1], name[1])
1004
868
properties_changed = []
1005
properties_changed.extend(
1006
get_executable_change(executable[0], executable[1]))
869
properties_changed.extend(get_executable_change(executable[0], executable[1]))
1008
871
if properties_changed:
1009
prop_str = b" (properties changed: %s)" % (
1010
b", ".join(properties_changed),)
872
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1014
876
if (old_present, new_present) == (True, False):
1015
self.to_file.write(b"=== removed %s '%s'\n" %
1016
(kind[0].encode('ascii'), oldpath_encoded))
877
self.to_file.write("=== removed %s '%s'\n" %
878
(kind[0], oldpath_encoded))
1017
879
newpath = oldpath
1018
880
elif (old_present, new_present) == (False, True):
1019
self.to_file.write(b"=== added %s '%s'\n" %
1020
(kind[1].encode('ascii'), newpath_encoded))
881
self.to_file.write("=== added %s '%s'\n" %
882
(kind[1], newpath_encoded))
1021
883
oldpath = newpath
1023
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1024
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
885
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
886
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1026
888
# if it was produced by iter_changes, it must be
1027
889
# modified *somehow*, either content or execute bit.
1028
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1029
newpath_encoded, prop_str))
890
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
891
newpath_encoded, prop_str))
1030
892
if changed_content:
1031
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
893
self.diff(file_id, oldpath, newpath)