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
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
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],)]
670
623
to_file_id = None
672
625
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)
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,
680
def diff_text(self, from_path, to_path, from_label, to_label,
681
from_file_id=None, to_file_id=None):
631
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
632
from_path=None, to_path=None):
682
633
"""Diff the content of given files in two trees
684
: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,
685
636
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,
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,
688
639
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
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.
694
643
def _get_text(tree, file_id, path):
644
if file_id is not None:
645
return tree.get_file(file_id, path).readlines()
697
return tree.get_file_lines(path)
699
649
from_text = _get_text(self.old_tree, from_file_id, from_path)
700
650
to_text = _get_text(self.new_tree, to_file_id, to_path)
701
651
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
653
except errors.BinaryFile:
705
654
self.to_file.write(
706
("Binary files %s and %s differ\n" %
707
(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))
708
657
return self.CHANGED
714
663
path_encoding='utf-8'):
715
664
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
716
665
self.command_template = command_template
717
self._root = osutils.mkdtemp(prefix='brz-diff-')
666
self._root = osutils.mkdtemp(prefix='bzr-diff-')
720
669
def from_string(klass, command_string, old_tree, new_tree, to_file,
721
670
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'])
671
command_template = commands.shlex_split_unicode(command_string)
672
command_template.extend(['%(old_path)s', '%(new_path)s'])
725
673
return klass(command_template, old_tree, new_tree, to_file,
729
def make_from_diff_tree(klass, command_string, external_diff_options=None):
677
def make_from_diff_tree(klass, command_string):
730
678
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,
679
return klass.from_string(command_string, diff_tree.old_tree,
735
680
diff_tree.new_tree, diff_tree.to_file)
736
681
return from_diff_tree
738
683
def _get_command(self, old_path, new_path):
739
684
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
685
return [t % my_map for t in self.command_template]
753
687
def _execute(self, old_path, new_path):
754
688
command = self._get_command(old_path, new_path)
756
690
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
759
693
if e.errno == errno.ENOENT:
760
694
raise errors.ExecutableMissing(command[0])
763
697
self.to_file.write(proc.stdout.read())
765
698
return proc.wait()
767
700
def _try_symlink_root(self, tree, prefix):
768
if (getattr(tree, 'abspath', None) is None or
769
not osutils.host_os_dereferences_symlinks()):
701
if (getattr(tree, 'abspath', None) is None
702
or not osutils.host_os_dereferences_symlinks()):
772
705
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
774
707
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):
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):
816
715
parent_dir = osutils.dirname(full_path)
818
717
os.makedirs(parent_dir)
820
719
if e.errno != errno.EEXIST:
822
source = tree.get_file(relpath)
721
source = tree.get_file(file_id, relpath)
824
with open(full_path, 'wb') as target:
723
target = open(full_path, 'wb')
825
725
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)
730
osutils.make_readonly(full_path)
731
mtime = tree.get_file_mtime(file_id)
732
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)
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',
845
740
return old_disk_path, new_disk_path
847
742
def finish(self):
849
744
osutils.rmtree(self._root)
851
746
if e.errno != errno.ENOENT:
852
747
mutter("The temporary directory \"%s\" was not "
853
"cleanly removed: %s." % (self._root, e))
748
"cleanly removed: %s." % (self._root, e))
855
750
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
856
751
if (old_kind, new_kind) != ('file', 'file'):
857
752
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()
753
self._prepare_files(file_id, old_path, new_path)
754
self._execute(osutils.pathjoin('old', old_path),
755
osutils.pathjoin('new', new_path))
880
758
class DiffTree(object):
938
815
:param using: Commandline to use to invoke an external diff tool
940
817
if using is not None:
941
extra_factories = [DiffFromTool.make_from_diff_tree(
942
using, external_diff_options)]
818
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
944
820
extra_factories = []
945
821
if external_diff_options:
946
822
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.
823
def diff_file(olab, olines, nlab, nlines, to_file):
952
824
external_diff(olab, olines, nlab, nlines, to_file, opts)
954
826
diff_file = internal_diff
955
827
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
956
old_label, new_label, diff_file, context_lines=context_lines)
828
old_label, new_label, diff_file)
957
829
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
960
832
def show_diff(self, specific_files, extra_trees=None):
961
833
"""Write tree diff to self.to_file
963
:param specific_files: the specific files to compare (recursive)
835
:param sepecific_files: the specific files to compare (recursive)
964
836
:param extra_trees: extra trees to use for mapping paths to file_ids
1002
872
renamed = (parent[0], name[0]) != (parent[1], name[1])
1004
874
properties_changed = []
1005
properties_changed.extend(
1006
get_executable_change(executable[0], executable[1]))
875
properties_changed.extend(get_executable_change(executable[0], executable[1]))
1008
877
if properties_changed:
1009
prop_str = b" (properties changed: %s)" % (
1010
b", ".join(properties_changed),)
878
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1014
882
if (old_present, new_present) == (True, False):
1015
self.to_file.write(b"=== removed %s '%s'\n" %
1016
(kind[0].encode('ascii'), oldpath_encoded))
883
self.to_file.write("=== removed %s '%s'\n" %
884
(kind[0], oldpath_encoded))
1017
885
newpath = oldpath
1018
886
elif (old_present, new_present) == (False, True):
1019
self.to_file.write(b"=== added %s '%s'\n" %
1020
(kind[1].encode('ascii'), newpath_encoded))
887
self.to_file.write("=== added %s '%s'\n" %
888
(kind[1], newpath_encoded))
1021
889
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))
891
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
892
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1026
894
# if it was produced by iter_changes, it must be
1027
895
# 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))
896
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
897
newpath_encoded, prop_str))
1030
898
if changed_content:
1031
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
899
self._diff(file_id, oldpath, newpath, kind[0], kind[1])