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(path_encoding, 'replace'),
98
tofile=new_label.encode(path_encoding, 'replace'),
99
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)
102
94
if len(ud) == 0: # Identical contents, nothing to do
104
96
# work-around for difflib being too smart for its own good
105
97
# if /dev/null is "1,0", patch won't recognize it as /dev/null
107
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
99
ud[2] = ud[2].replace('-1,0', '-0,0')
108
100
elif not newlines:
109
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
101
ud[2] = ud[2].replace('+1,0', '+0,0')
112
104
to_file.write(line)
113
if not line.endswith(b'\n'):
114
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")
118
110
def _spawn_external_diff(diffcmd, capture_errors=True):
119
"""Spawn the external diff process, and return the child handle.
111
"""Spawn the externall diff process, and return the child handle.
121
113
:param diffcmd: The command list to spawn
122
114
:param capture_errors: Capture stderr as well as setting LANG=C
144
136
stdout=subprocess.PIPE,
148
140
if e.errno == errno.ENOENT:
149
141
raise errors.NoDiff(str(e))
154
# diff style options as of GNU diff v3.2
155
style_option_list = ['-c', '-C', '--context',
157
'-f', '--forward-ed',
161
'-u', '-U', '--unified',
162
'-y', '--side-by-side',
165
def default_style_unified(diff_opts):
166
"""Default to unified diff style if alternative not specified in diff_opts.
168
diff only allows one style to be specified; they don't override.
169
Note that some of these take optargs, and the optargs can be
170
directly appended to the options.
171
This is only an approximate parser; it doesn't properly understand
174
:param diff_opts: List of options for external (GNU) diff.
175
:return: List of options with default style=='unified'.
177
for s in style_option_list:
185
diff_opts.append('-u')
189
def external_diff(old_label, oldlines, new_label, newlines, to_file,
147
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
191
149
"""Display a diff by calling out to the external diff program."""
192
150
# make sure our own output is properly ordered before the diff
195
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
196
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-')
197
155
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
198
156
newtmpf = os.fdopen(newtmp_fd, 'wb')
214
172
if not diff_opts:
216
if sys.platform == 'win32':
217
# Popen doesn't do the proper encoding for external commands
218
# Since we are dealing with an ANSI api, use mbcs encoding
219
old_label = old_label.encode('mbcs')
220
new_label = new_label.encode('mbcs')
221
174
diffcmd = ['diff',
222
'--label', old_label,
175
'--label', old_filename,
224
'--label', new_label,
177
'--label', new_filename,
229
diff_opts = default_style_unified(diff_opts)
182
# diff only allows one style to be specified; they don't override.
183
# note that some of these take optargs, and the optargs can be
184
# directly appended to the options.
185
# this is only an approximate parser; it doesn't properly understand
187
for s in ['-c', '-u', '-C', '-U',
192
'-y', '--side-by-side',
232
204
diffcmd.extend(diff_opts)
234
206
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
235
out, err = pipe.communicate()
207
out,err = pipe.communicate()
236
208
rc = pipe.returncode
238
210
# internal_diff() adds a trailing newline, add one here for consistency
241
213
# 'diff' gives retcode == 2 for all sorts of errors
242
214
# one of those is 'Binary files differ'.
277
249
msg = 'exit code %d' % rc
279
251
raise errors.BzrError('external diff failed with %s; command: %r'
284
256
oldtmpf.close() # and delete
288
# Warn in case the file couldn't be deleted (in case windows still
289
# holds the file open, but not if the files have already been
294
if e.errno not in (errno.ENOENT,):
295
warning('Failed to delete temporary file: %s %s', path, e)
301
def get_trees_and_branches_to_diff_locked(
302
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
258
# Clean up. Warn in case the files couldn't be deleted
259
# (in case windows still holds the file open, but not
260
# if the files have already been deleted)
262
os.remove(old_abspath)
264
if e.errno not in (errno.ENOENT,):
265
warning('Failed to delete temporary file: %s %s',
268
os.remove(new_abspath)
270
if e.errno not in (errno.ENOENT,):
271
warning('Failed to delete temporary file: %s %s',
275
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url,
303
277
"""Get the trees and specific files to diff given a list of paths.
305
279
This method works out the trees to be diff'ed and the files of
356
325
default_location = path_list[0]
357
326
other_paths = path_list[1:]
359
def lock_tree_or_branch(wt, br):
362
add_cleanup(wt.unlock)
365
add_cleanup(br.unlock)
367
328
# Get the old location
368
329
specific_files = []
369
330
if old_url is None:
370
331
old_url = default_location
371
332
working_tree, branch, relpath = \
372
controldir.ControlDir.open_containing_tree_or_branch(old_url)
373
lock_tree_or_branch(working_tree, branch)
333
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
374
334
if consider_relpath and relpath != '':
375
335
if working_tree is not None and apply_view:
376
336
views.check_path_in_view(working_tree, relpath)
377
337
specific_files.append(relpath)
378
338
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
381
340
# Get the new location
382
341
if new_url is None:
383
342
new_url = default_location
384
343
if new_url != old_url:
385
344
working_tree, branch, relpath = \
386
controldir.ControlDir.open_containing_tree_or_branch(new_url)
387
lock_tree_or_branch(working_tree, branch)
345
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
388
346
if consider_relpath and relpath != '':
389
347
if working_tree is not None and apply_view:
390
348
views.check_path_in_view(working_tree, relpath)
391
349
specific_files.append(relpath)
392
350
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
393
351
basis_is_default=working_tree is None)
396
353
# Get the specific files (all files is None, no files is [])
397
354
if make_paths_wt_relative and working_tree is not None:
398
other_paths = working_tree.safe_relpath_files(
356
from bzrlib.builtins import safe_relpath_files
357
other_paths = safe_relpath_files(working_tree, other_paths,
400
358
apply_view=apply_view)
359
except errors.FileInWrongBranch:
360
raise errors.BzrCommandError("Files are in different branches")
401
361
specific_files.extend(other_paths)
402
362
if len(specific_files) == 0:
403
363
specific_files = None
436
394
old_label='a/', new_label='b/',
437
395
extra_trees=None,
438
396
path_encoding='utf8',
441
context=DEFAULT_CONTEXT_AMOUNT):
442
398
"""Show in text form the changes from one tree to another.
444
:param to_file: The output stream.
445
:param specific_files: Include only changes to these files - None for all
447
:param external_diff_options: If set, use an external GNU diff and pass
449
:param extra_trees: If set, more Trees to use for looking up file ids
450
:param path_encoding: If set, the path will be encoded as specified,
451
otherwise is supposed to be utf8
452
:param format_cls: Formatter class (DiffTree subclass)
404
Include only changes to these files - None for all changes.
406
external_diff_options
407
If set, use an external GNU diff and pass these options.
410
If set, more Trees to use for looking up file ids
413
If set, the path will be encoded as specified, otherwise is supposed
455
context = DEFAULT_CONTEXT_AMOUNT
456
if format_cls is None:
457
format_cls = DiffTree
458
with old_tree.lock_read():
459
418
if extra_trees is not None:
460
419
for tree in extra_trees:
462
421
new_tree.lock_read()
464
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
466
external_diff_options,
467
old_label, new_label, using,
468
context_lines=context)
423
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
425
external_diff_options,
426
old_label, new_label, using)
469
427
return differ.show_diff(specific_files, extra_trees)
471
429
new_tree.unlock()
472
430
if extra_trees is not None:
473
431
for tree in extra_trees:
477
437
def _patch_header_date(tree, file_id, path):
478
438
"""Returns a timestamp suitable for use in a patch header."""
480
mtime = tree.get_file_mtime(path, file_id)
481
except FileTimestampUnavailable:
439
mtime = tree.get_file_mtime(file_id, path)
483
440
return timestamp.format_patch_date(mtime)
486
443
def get_executable_change(old_is_x, new_is_x):
487
descr = { True:b"+x", False:b"-x", None:b"??" }
444
descr = { True:"+x", False:"-x", None:"??" }
488
445
if old_is_x != new_is_x:
489
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
446
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
630
584
# or removed in a diff.
631
585
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
633
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
634
old_label='', new_label='', text_differ=internal_diff,
635
context_lines=DEFAULT_CONTEXT_AMOUNT):
587
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
588
old_label='', new_label='', text_differ=internal_diff):
636
589
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
637
590
self.text_differ = text_differ
638
591
self.old_label = old_label
639
592
self.new_label = new_label
640
593
self.path_encoding = path_encoding
641
self.context_lines = context_lines
643
595
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
644
596
"""Compare two files in unified diff format
666
618
to_file_id = None
668
620
return self.CANNOT_DIFF
669
from_label = '%s%s\t%s' % (self.old_label, old_path,
671
to_label = '%s%s\t%s' % (self.new_label, new_path,
673
return self.diff_text(old_path, new_path, from_label, to_label,
674
from_file_id, to_file_id)
621
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
622
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
623
return self.diff_text(from_file_id, to_file_id, from_label, to_label,
676
def diff_text(self, from_path, to_path, from_label, to_label,
677
from_file_id=None, to_file_id=None):
626
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
627
from_path=None, to_path=None):
678
628
"""Diff the content of given files in two trees
680
:param from_path: The path in the from tree. If None,
630
:param from_file_id: The id of the file in the from tree. If None,
681
631
the file is not present in the from tree.
682
:param to_path: The path in the to tree. This may refer
683
to a different file from from_path. If None,
632
:param to_file_id: The id of the file in the to tree. This may refer
633
to a different file from from_file_id. If None,
684
634
the file is not present in the to tree.
685
:param from_file_id: The id of the file in the from tree or None if
687
:param to_file_id: The id of the file in the to tree or None if
635
:param from_path: The path in the from tree or None if unknown.
636
:param to_path: The path in the to tree or None if unknown.
690
638
def _get_text(tree, file_id, path):
639
if file_id is not None:
640
return tree.get_file(file_id, path).readlines()
693
return tree.get_file_lines(path, file_id)
695
644
from_text = _get_text(self.old_tree, from_file_id, from_path)
696
645
to_text = _get_text(self.new_tree, to_file_id, to_path)
697
646
self.text_differ(from_label, from_text, to_label, to_text,
698
self.to_file, path_encoding=self.path_encoding,
699
context_lines=self.context_lines)
700
648
except errors.BinaryFile:
701
649
self.to_file.write(
702
650
("Binary files %s and %s differ\n" %
703
(from_label, to_label)).encode(self.path_encoding, 'replace'))
651
(from_label, to_label)).encode(self.path_encoding))
704
652
return self.CHANGED
710
658
path_encoding='utf-8'):
711
659
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
712
660
self.command_template = command_template
713
self._root = osutils.mkdtemp(prefix='brz-diff-')
661
self._root = osutils.mkdtemp(prefix='bzr-diff-')
716
664
def from_string(klass, command_string, old_tree, new_tree, to_file,
717
665
path_encoding='utf-8'):
718
command_template = cmdline.split(command_string)
719
if '@' not in command_string:
720
command_template.extend(['@old_path', '@new_path'])
666
command_template = commands.shlex_split_unicode(command_string)
667
command_template.extend(['%(old_path)s', '%(new_path)s'])
721
668
return klass(command_template, old_tree, new_tree, to_file,
725
def make_from_diff_tree(klass, command_string, external_diff_options=None):
672
def make_from_diff_tree(klass, command_string):
726
673
def from_diff_tree(diff_tree):
727
full_command_string = [command_string]
728
if external_diff_options is not None:
729
full_command_string += ' ' + external_diff_options
730
return klass.from_string(full_command_string, diff_tree.old_tree,
674
return klass.from_string(command_string, diff_tree.old_tree,
731
675
diff_tree.new_tree, diff_tree.to_file)
732
676
return from_diff_tree
734
678
def _get_command(self, old_path, new_path):
735
679
my_map = {'old_path': old_path, 'new_path': new_path}
736
command = [AtTemplate(t).substitute(my_map) for t in
737
self.command_template]
738
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
741
if isinstance(c, text_type):
742
command_encoded.append(c.encode('mbcs'))
744
command_encoded.append(c)
745
return command_encoded
680
return [t % my_map for t in self.command_template]
749
682
def _execute(self, old_path, new_path):
750
683
command = self._get_command(old_path, new_path)
752
685
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
755
688
if e.errno == errno.ENOENT:
756
689
raise errors.ExecutableMissing(command[0])
759
692
self.to_file.write(proc.stdout.read())
761
693
return proc.wait()
763
695
def _try_symlink_root(self, tree, prefix):
768
700
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
770
702
if e.errno != errno.EEXIST:
776
"""Returns safe encoding for passing file path to diff tool"""
777
if sys.platform == 'win32':
780
# Don't fallback to 'utf-8' because subprocess may not be able to
781
# handle utf-8 correctly when locale is not utf-8.
782
return sys.getfilesystemencoding() or 'ascii'
784
def _is_safepath(self, path):
785
"""Return true if `path` may be able to pass to subprocess."""
788
return path == path.encode(fenc).decode(fenc)
792
def _safe_filename(self, prefix, relpath):
793
"""Replace unsafe character in `relpath` then join `self._root`,
794
`prefix` and `relpath`."""
796
# encoded_str.replace('?', '_') may break multibyte char.
797
# So we should encode, decode, then replace(u'?', u'_')
798
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
799
relpath_tmp = relpath_tmp.replace(u'?', u'_')
800
return osutils.pathjoin(self._root, prefix, relpath_tmp)
802
def _write_file(self, relpath, tree, prefix, force_temp=False,
803
allow_write=False, file_id=None):
804
if not force_temp and isinstance(tree, WorkingTree):
805
full_path = tree.abspath(relpath)
806
if self._is_safepath(full_path):
809
full_path = self._safe_filename(prefix, relpath)
810
if not force_temp and self._try_symlink_root(tree, prefix):
706
def _write_file(self, file_id, tree, prefix, relpath):
707
full_path = osutils.pathjoin(self._root, prefix, relpath)
708
if self._try_symlink_root(tree, prefix):
812
710
parent_dir = osutils.dirname(full_path)
814
712
os.makedirs(parent_dir)
816
714
if e.errno != errno.EEXIST:
818
source = tree.get_file(relpath, file_id)
716
source = tree.get_file(file_id, relpath)
820
with open(full_path, 'wb') as target:
718
target = open(full_path, 'wb')
821
720
osutils.pumpfile(source, target)
825
mtime = tree.get_file_mtime(relpath, file_id)
826
except FileTimestampUnavailable:
829
os.utime(full_path, (mtime, mtime))
831
osutils.make_readonly(full_path)
725
osutils.make_readonly(full_path)
726
mtime = tree.get_file_mtime(file_id)
727
os.utime(full_path, (mtime, mtime))
834
def _prepare_files(self, old_path, new_path, force_temp=False,
835
allow_write_new=False, file_id=None):
836
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
837
force_temp, file_id=file_id)
838
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
839
force_temp, file_id=file_id,
840
allow_write=allow_write_new)
730
def _prepare_files(self, file_id, old_path, new_path):
731
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
733
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
841
735
return old_disk_path, new_disk_path
843
737
def finish(self):
845
osutils.rmtree(self._root)
847
if e.errno != errno.ENOENT:
848
mutter("The temporary directory \"%s\" was not "
849
"cleanly removed: %s." % (self._root, e))
738
osutils.rmtree(self._root)
851
740
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
852
741
if (old_kind, new_kind) != ('file', 'file'):
853
742
return DiffPath.CANNOT_DIFF
854
(old_disk_path, new_disk_path) = self._prepare_files(
855
old_path, new_path, file_id=file_id)
856
self._execute(old_disk_path, new_disk_path)
858
def edit_file(self, old_path, new_path, file_id=None):
859
"""Use this tool to edit a file.
861
A temporary copy will be edited, and the new contents will be
864
:param file_id: The id of the file to edit.
865
:return: The new contents of the file.
867
old_abs_path, new_abs_path = self._prepare_files(
868
old_path, new_path, allow_write_new=True, force_temp=True,
870
command = self._get_command(old_abs_path, new_abs_path)
871
subprocess.call(command, cwd=self._root)
872
with open(new_abs_path, 'rb') as new_file:
873
return new_file.read()
743
self._prepare_files(file_id, old_path, new_path)
744
self._execute(osutils.pathjoin('old', old_path),
745
osutils.pathjoin('new', new_path))
876
748
class DiffTree(object):
934
805
:param using: Commandline to use to invoke an external diff tool
936
807
if using is not None:
937
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
808
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
939
810
extra_factories = []
940
811
if external_diff_options:
941
812
opts = external_diff_options.split()
942
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
943
""":param path_encoding: not used but required
944
to match the signature of internal_diff.
813
def diff_file(olab, olines, nlab, nlines, to_file):
946
814
external_diff(olab, olines, nlab, nlines, to_file, opts)
948
816
diff_file = internal_diff
949
817
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
950
old_label, new_label, diff_file, context_lines=context_lines)
818
old_label, new_label, diff_file)
951
819
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
954
822
def show_diff(self, specific_files, extra_trees=None):
955
823
"""Write tree diff to self.to_file
957
:param specific_files: the specific files to compare (recursive)
825
:param sepecific_files: the specific files to compare (recursive)
958
826
:param extra_trees: extra trees to use for mapping paths to file_ids
997
865
properties_changed.extend(get_executable_change(executable[0], executable[1]))
999
867
if properties_changed:
1000
prop_str = b" (properties changed: %s)" % (
1001
b", ".join(properties_changed),)
868
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1005
872
if (old_present, new_present) == (True, False):
1006
self.to_file.write(b"=== removed %s '%s'\n" %
1007
(kind[0].encode('ascii'), oldpath_encoded))
873
self.to_file.write("=== removed %s '%s'\n" %
874
(kind[0], oldpath_encoded))
1008
875
newpath = oldpath
1009
876
elif (old_present, new_present) == (False, True):
1010
self.to_file.write(b"=== added %s '%s'\n" %
1011
(kind[1].encode('ascii'), newpath_encoded))
877
self.to_file.write("=== added %s '%s'\n" %
878
(kind[1], newpath_encoded))
1012
879
oldpath = newpath
1014
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1015
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
881
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
882
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1017
884
# if it was produced by iter_changes, it must be
1018
885
# modified *somehow*, either content or execute bit.
1019
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
886
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
1020
887
newpath_encoded, prop_str))
1021
888
if changed_content:
1022
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
889
self._diff(file_id, oldpath, newpath, kind[0], kind[1])
1032
899
:param old_path: The path of the file in the old tree
1033
900
:param new_path: The path of the file in the new tree
1035
if old_path is None:
903
old_kind = self.old_tree.kind(file_id)
904
except (errors.NoSuchId, errors.NoSuchFile):
1038
old_kind = self.old_tree.kind(old_path, file_id)
1039
if new_path is None:
907
new_kind = self.new_tree.kind(file_id)
908
except (errors.NoSuchId, errors.NoSuchFile):
1042
new_kind = self.new_tree.kind(new_path, file_id)
1043
self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1045
def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
910
self._diff(file_id, old_path, new_path, old_kind, new_kind)
913
def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
1046
914
result = DiffPath._diff_many(self.differs, file_id, old_path,
1047
new_path, old_kind, new_kind)
915
new_path, old_kind, new_kind)
1048
916
if result is DiffPath.CANNOT_DIFF:
1049
917
error_path = new_path
1050
918
if error_path is None:
1051
919
error_path = old_path
1052
920
raise errors.NoDiffFound(error_path)
1055
format_registry = Registry()
1056
format_registry.register('default', DiffTree)