13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
23
from bzrlib.lazy_import import lazy_import
31
30
from bzrlib import (
31
branch as _mod_branch,
43
from bzrlib.workingtree import WorkingTree
44
from bzrlib.i18n import gettext
47
from bzrlib.registry import (
50
from bzrlib.trace import mutter, note, warning
52
DEFAULT_CONTEXT_AMOUNT = 3
54
class AtTemplate(string.Template):
55
"""Templating class that uses @ instead of $."""
42
from bzrlib.symbol_versioning import (
46
from bzrlib.trace import mutter, warning
60
49
# TODO: Rather than building a changeset object, we should probably
74
63
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
75
64
allow_binary=False, sequence_matcher=None,
76
path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
65
path_encoding='utf8'):
77
66
# FIXME: difflib is wrong if there is no trailing newline.
78
67
# The syntax used by patch seems to be "\ No newline at
79
68
# end of file" following the last diff line from that
97
86
if sequence_matcher is None:
98
87
sequence_matcher = patiencediff.PatienceSequenceMatcher
99
88
ud = patiencediff.unified_diff(oldlines, newlines,
100
fromfile=old_filename.encode(path_encoding, 'replace'),
101
tofile=new_filename.encode(path_encoding, 'replace'),
102
n=context_lines, sequencematcher=sequence_matcher)
89
fromfile=old_filename.encode(path_encoding),
90
tofile=new_filename.encode(path_encoding),
91
sequencematcher=sequence_matcher)
105
94
if len(ud) == 0: # Identical contents, nothing to do
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',
168
def default_style_unified(diff_opts):
169
"""Default to unified diff style if alternative not specified in diff_opts.
171
diff only allows one style to be specified; they don't override.
172
Note that some of these take optargs, and the optargs can be
173
directly appended to the options.
174
This is only an approximate parser; it doesn't properly understand
177
:param diff_opts: List of options for external (GNU) diff.
178
:return: List of options with default style=='unified'.
180
for s in style_option_list:
188
diff_opts.append('-u')
192
150
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
232
diff_opts = default_style_unified(diff_opts)
185
# diff only allows one style to be specified; they don't override.
186
# note that some of these take optargs, and the optargs can be
187
# directly appended to the options.
188
# this is only an approximate parser; it doesn't properly understand
190
for s in ['-c', '-u', '-C', '-U',
195
'-y', '--side-by-side',
235
207
diffcmd.extend(diff_opts)
237
209
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
238
210
out,err = pipe.communicate()
239
211
rc = pipe.returncode
241
213
# internal_diff() adds a trailing newline, add one here for consistency
278
250
msg = 'signal %d' % (-rc)
280
252
msg = 'exit code %d' % rc
282
raise errors.BzrError('external diff failed with %s; command: %r'
254
raise errors.BzrError('external diff failed with %s; command: %r'
287
259
oldtmpf.close() # and delete
291
# Warn in case the file couldn't be deleted (in case windows still
292
# holds the file open, but not if the files have already been
297
if e.errno not in (errno.ENOENT,):
298
warning('Failed to delete temporary file: %s %s', path, e)
304
def get_trees_and_branches_to_diff_locked(
305
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
261
# Clean up. Warn in case the files couldn't be deleted
262
# (in case windows still holds the file open, but not
263
# if the files have already been deleted)
265
os.remove(old_abspath)
267
if e.errno not in (errno.ENOENT,):
268
warning('Failed to delete temporary file: %s %s',
271
os.remove(new_abspath)
273
if e.errno not in (errno.ENOENT,):
274
warning('Failed to delete temporary file: %s %s',
278
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
306
279
"""Get the trees and specific files to diff given a list of paths.
308
281
This method works out the trees to be diff'ed and the files of
320
293
The url of the new branch or tree. If None, the tree to use is
321
294
taken from the first path, if any, or the current working tree.
323
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
324
will register cleanups that must be run to unlock the trees, etc.
326
if True and a view is set, apply the view or check that the paths
329
a tuple of (old_tree, new_tree, old_branch, new_branch,
330
specific_files, extra_trees) where extra_trees is a sequence of
331
additional trees to search in for file-ids. The trees and branches
332
will be read-locked until the cleanups registered via the add_cleanup
296
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
297
extra_trees is a sequence of additional trees to search in for
335
300
# Get the old and new revision specs
336
301
old_revision_spec = None
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
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()
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):
431
370
return branch.basis_tree()
434
return spec.as_tree(branch)
373
if not spec.needs_branch():
374
branch = _mod_branch.Branch.open(spec.get_branch())
375
revision_id = spec.as_revision_id(branch)
376
return branch.repository.revision_tree(revision_id)
379
def _relative_paths_in_tree(tree, paths):
380
"""Get the relative paths within a working tree.
382
Each path may be either an absolute path or a path relative to the
383
current working directory.
386
for filename in paths:
388
result.append(tree.relpath(osutils.dereference_path(filename)))
389
except errors.PathNotChild:
390
raise errors.BzrCommandError("Files are in different branches")
437
394
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
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
418
old_tree.lock_read()
463
420
if extra_trees is not None:
466
423
new_tree.lock_read()
468
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
470
external_diff_options,
471
old_label, new_label, using,
472
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)
473
429
return differ.show_diff(specific_files, extra_trees)
475
431
new_tree.unlock()
483
439
def _patch_header_date(tree, file_id, path):
484
440
"""Returns a timestamp suitable for use in a patch header."""
486
mtime = tree.get_file_mtime(file_id, path)
487
except errors.FileTimestampUnavailable:
441
mtime = tree.get_file_mtime(file_id, path)
489
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)"
492
471
def get_executable_change(old_is_x, new_is_x):
493
472
descr = { True:"+x", False:"-x", None:"??" }
494
473
if old_is_x != new_is_x:
633
612
# or removed in a diff.
634
613
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
636
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
637
old_label='', new_label='', text_differ=internal_diff,
638
context_lines=DEFAULT_CONTEXT_AMOUNT):
615
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
616
old_label='', new_label='', text_differ=internal_diff):
639
617
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
640
618
self.text_differ = text_differ
641
619
self.old_label = old_label
642
620
self.new_label = new_label
643
621
self.path_encoding = path_encoding
644
self.context_lines = context_lines
646
623
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
647
624
"""Compare two files in unified diff format
671
648
return self.CANNOT_DIFF
672
649
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
673
650
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
674
return self.diff_text(from_file_id, to_file_id, from_label, to_label,
651
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
677
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
678
from_path=None, to_path=None):
653
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
679
654
"""Diff the content of given files in two trees
681
656
:param from_file_id: The id of the file in the from tree. If None,
683
658
:param to_file_id: The id of the file in the to tree. This may refer
684
659
to a different file from from_file_id. If None,
685
660
the file is not present in the to tree.
686
:param from_path: The path in the from tree or None if unknown.
687
:param to_path: The path in the to tree or None if unknown.
689
def _get_text(tree, file_id, path):
662
def _get_text(tree, file_id):
690
663
if file_id is not None:
691
return tree.get_file_lines(file_id, path)
664
return tree.get_file(file_id).readlines()
695
from_text = _get_text(self.old_tree, from_file_id, from_path)
696
to_text = _get_text(self.new_tree, to_file_id, to_path)
668
from_text = _get_text(self.old_tree, from_file_id)
669
to_text = _get_text(self.new_tree, to_file_id)
697
670
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
672
except errors.BinaryFile:
701
673
self.to_file.write(
702
674
("Binary files %s and %s differ\n" %
703
(from_label, to_label)).encode(self.path_encoding,'replace'))
675
(from_label, to_label)).encode(self.path_encoding))
704
676
return self.CHANGED
710
682
path_encoding='utf-8'):
711
683
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
712
684
self.command_template = command_template
713
self._root = osutils.mkdtemp(prefix='bzr-diff-')
685
self._root = tempfile.mkdtemp(prefix='bzr-diff-')
716
688
def from_string(klass, command_string, old_tree, new_tree, to_file,
717
689
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'])
690
command_template = commands.shlex_split_unicode(command_string)
691
command_template.extend(['%(old_path)s', '%(new_path)s'])
721
692
return klass(command_template, old_tree, new_tree, to_file,
725
def make_from_diff_tree(klass, command_string, external_diff_options=None):
696
def make_from_diff_tree(klass, command_string):
726
697
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,
698
return klass.from_string(command_string, diff_tree.old_tree,
731
699
diff_tree.new_tree, diff_tree.to_file)
732
700
return from_diff_tree
734
702
def _get_command(self, old_path, new_path):
735
703
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, unicode):
742
command_encoded.append(c.encode('mbcs'))
744
command_encoded.append(c)
745
return command_encoded
704
return [t % my_map for t in self.command_template]
749
706
def _execute(self, old_path, new_path):
750
707
command = self._get_command(old_path, new_path)
775
"""Returns safe encoding for passing file path to diff tool"""
776
if sys.platform == 'win32':
779
# Don't fallback to 'utf-8' because subprocess may not be able to
780
# handle utf-8 correctly when locale is not utf-8.
781
return sys.getfilesystemencoding() or 'ascii'
783
def _is_safepath(self, path):
784
"""Return true if `path` may be able to pass to subprocess."""
787
return path == path.encode(fenc).decode(fenc)
791
def _safe_filename(self, prefix, relpath):
792
"""Replace unsafe character in `relpath` then join `self._root`,
793
`prefix` and `relpath`."""
795
# encoded_str.replace('?', '_') may break multibyte char.
796
# So we should encode, decode, then replace(u'?', u'_')
797
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
798
relpath_tmp = relpath_tmp.replace(u'?', u'_')
799
return osutils.pathjoin(self._root, prefix, relpath_tmp)
801
def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
803
if not force_temp and isinstance(tree, WorkingTree):
804
full_path = tree.abspath(tree.id2path(file_id))
805
if self._is_safepath(full_path):
808
full_path = self._safe_filename(prefix, relpath)
809
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):
811
734
parent_dir = osutils.dirname(full_path)
827
mtime = tree.get_file_mtime(file_id)
828
except errors.FileTimestampUnavailable:
831
os.utime(full_path, (mtime, mtime))
833
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))
836
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
837
allow_write_new=False):
754
def _prepare_files(self, file_id, old_path, new_path):
838
755
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
839
old_path, force_temp)
840
757
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
841
new_path, force_temp,
842
allow_write=allow_write_new)
843
759
return old_disk_path, new_disk_path
845
761
def finish(self):
847
osutils.rmtree(self._root)
849
if e.errno != errno.ENOENT:
850
mutter("The temporary directory \"%s\" was not "
851
"cleanly removed: %s." % (self._root, e))
762
osutils.rmtree(self._root)
853
764
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
854
765
if (old_kind, new_kind) != ('file', 'file'):
855
766
return DiffPath.CANNOT_DIFF
856
(old_disk_path, new_disk_path) = self._prepare_files(
857
file_id, old_path, new_path)
858
self._execute(old_disk_path, new_disk_path)
860
def edit_file(self, file_id):
861
"""Use this tool to edit a file.
863
A temporary copy will be edited, and the new contents will be
866
:param file_id: The id of the file to edit.
867
:return: The new contents of the file.
869
old_path = self.old_tree.id2path(file_id)
870
new_path = self.new_tree.id2path(file_id)
871
old_abs_path, new_abs_path = self._prepare_files(
872
file_id, old_path, new_path,
873
allow_write_new=True,
875
command = self._get_command(old_abs_path, new_abs_path)
876
subprocess.call(command, cwd=self._root)
877
new_file = open(new_abs_path, 'rb')
879
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))
884
772
class DiffTree(object):
927
815
def from_trees_options(klass, old_tree, new_tree, to_file,
928
816
path_encoding, external_diff_options, old_label,
929
new_label, using, context_lines):
930
818
"""Factory for producing a DiffTree.
932
820
Designed to accept options used by show_diff_trees.
934
821
:param old_tree: The tree to show as old in the comparison
935
822
:param new_tree: The tree to show as new in the comparison
936
823
:param to_file: File to write comparisons to
942
829
:param using: Commandline to use to invoke an external diff tool
944
831
if using is not None:
945
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
832
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
947
834
extra_factories = []
948
835
if external_diff_options:
949
836
opts = external_diff_options.split()
950
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
951
""":param path_encoding: not used but required
952
to match the signature of internal_diff.
837
def diff_file(olab, olines, nlab, nlines, to_file):
954
838
external_diff(olab, olines, nlab, nlines, to_file, opts)
956
840
diff_file = internal_diff
957
841
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
958
old_label, new_label, diff_file, context_lines=context_lines)
842
old_label, new_label, diff_file)
959
843
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
962
846
def show_diff(self, specific_files, extra_trees=None):
963
847
"""Write tree diff to self.to_file
965
:param specific_files: the specific files to compare (recursive)
849
:param sepecific_files: the specific files to compare (recursive)
966
850
:param extra_trees: extra trees to use for mapping paths to file_ids
990
874
return path.encode(self.path_encoding, "replace")
991
875
for (file_id, paths, changed_content, versioned, parent, name, kind,
992
876
executable) in sorted(iterator, key=changes_key):
993
# The root does not get diffed, and items with no known kind (that
994
# is, missing) in both trees are skipped as well.
995
if parent == (None, None) or kind == (None, None):
877
if parent == (None, None):
997
879
oldpath, newpath = paths
998
880
oldpath_encoded = get_encoded_path(paths[0])
1047
929
new_kind = self.new_tree.kind(file_id)
1048
930
except (errors.NoSuchId, errors.NoSuchFile):
1050
self._diff(file_id, old_path, new_path, old_kind, new_kind)
1053
def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
1054
933
result = DiffPath._diff_many(self.differs, file_id, old_path,
1055
934
new_path, old_kind, new_kind)
1056
935
if result is DiffPath.CANNOT_DIFF: