278
249
msg = 'signal %d' % (-rc)
280
251
msg = 'exit code %d' % rc
282
raise errors.BzrError('external diff failed with %s; command: %r'
253
raise errors.BzrError('external diff failed with %s; command: %r'
287
258
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):
260
# Clean up. Warn in case the files couldn't be deleted
261
# (in case windows still holds the file open, but not
262
# if the files have already been deleted)
264
os.remove(old_abspath)
266
if e.errno not in (errno.ENOENT,):
267
warning('Failed to delete temporary file: %s %s',
270
os.remove(new_abspath)
272
if e.errno not in (errno.ENOENT,):
273
warning('Failed to delete temporary file: %s %s',
277
@deprecated_function(one_zero)
278
def diff_cmd_helper(tree, specific_files, external_diff_options,
279
old_revision_spec=None, new_revision_spec=None,
281
old_label='a/', new_label='b/'):
282
"""Helper for cmd_diff.
287
:param specific_files:
288
The specific files to compare, or None
290
:param external_diff_options:
291
If non-None, run an external diff, and pass it these options
293
:param old_revision_spec:
294
If None, use basis tree as old revision, otherwise use the tree for
295
the specified revision.
297
:param new_revision_spec:
298
If None, use working tree as new revision, otherwise use the tree for
299
the specified revision.
301
:param revision_specs:
302
Zero, one or two RevisionSpecs from the command line, saying what revisions
303
to compare. This can be passed as an alternative to the old_revision_spec
304
and new_revision_spec parameters.
306
The more general form is show_diff_trees(), where the caller
307
supplies any two trees.
310
# TODO: perhaps remove the old parameters old_revision_spec and
311
# new_revision_spec, since this is only really for use from cmd_diff and
312
# it now always passes through a sequence of revision_specs -- mbp
317
revision = spec.in_store(tree.branch)
319
revision = spec.in_store(None)
320
revision_id = revision.rev_id
321
branch = revision.branch
322
return branch.repository.revision_tree(revision_id)
324
if revision_specs is not None:
325
assert (old_revision_spec is None
326
and new_revision_spec is None)
327
if len(revision_specs) > 0:
328
old_revision_spec = revision_specs[0]
329
if len(revision_specs) > 1:
330
new_revision_spec = revision_specs[1]
332
if old_revision_spec is None:
333
old_tree = tree.basis_tree()
335
old_tree = spec_tree(old_revision_spec)
337
if (new_revision_spec is None
338
or new_revision_spec.spec is None):
341
new_tree = spec_tree(new_revision_spec)
343
if new_tree is not tree:
344
extra_trees = (tree,)
348
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
349
external_diff_options,
350
old_label=old_label, new_label=new_label,
351
extra_trees=extra_trees)
354
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
306
355
"""Get the trees and specific files to diff given a list of paths.
308
357
This method works out the trees to be diff'ed and the files of
359
398
default_location = path_list[0]
360
399
other_paths = path_list[1:]
362
def lock_tree_or_branch(wt, br):
365
add_cleanup(wt.unlock)
368
add_cleanup(br.unlock)
370
401
# Get the old location
371
402
specific_files = []
372
403
if old_url is None:
373
404
old_url = default_location
374
405
working_tree, branch, relpath = \
375
controldir.ControlDir.open_containing_tree_or_branch(old_url)
376
lock_tree_or_branch(working_tree, branch)
377
if consider_relpath and relpath != '':
378
if working_tree is not None and apply_view:
379
views.check_path_in_view(working_tree, relpath)
406
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
380
408
specific_files.append(relpath)
381
409
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
384
411
# Get the new location
385
412
if new_url is None:
386
413
new_url = default_location
387
414
if new_url != old_url:
388
415
working_tree, branch, relpath = \
389
controldir.ControlDir.open_containing_tree_or_branch(new_url)
390
lock_tree_or_branch(working_tree, branch)
391
if consider_relpath and relpath != '':
392
if working_tree is not None and apply_view:
393
views.check_path_in_view(working_tree, relpath)
416
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
394
418
specific_files.append(relpath)
395
419
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
396
420
basis_is_default=working_tree is None)
399
422
# Get the specific files (all files is None, no files is [])
400
423
if make_paths_wt_relative and working_tree is not None:
401
other_paths = working_tree.safe_relpath_files(
403
apply_view=apply_view)
424
other_paths = _relative_paths_in_tree(working_tree, other_paths)
404
425
specific_files.extend(other_paths)
405
426
if len(specific_files) == 0:
406
427
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
429
# Get extra trees that ought to be searched for file-ids
416
430
extra_trees = None
417
431
if working_tree is not None and working_tree not in (old_tree, new_tree):
418
432
extra_trees = (working_tree,)
419
return (old_tree, new_tree, old_branch, new_branch,
420
specific_files, extra_trees)
433
return old_tree, new_tree, specific_files, extra_trees
423
436
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
483
513
def _patch_header_date(tree, file_id, path):
484
514
"""Returns a timestamp suitable for use in a patch header."""
486
mtime = tree.get_file_mtime(file_id, path)
487
except errors.FileTimestampUnavailable:
515
mtime = tree.get_file_mtime(file_id, path)
516
assert mtime is not None, \
517
"got an mtime of None for file-id %s, path %s in tree %s" % (
489
519
return timestamp.format_patch_date(mtime)
492
def get_executable_change(old_is_x, new_is_x):
493
descr = { True:"+x", False:"-x", None:"??" }
494
if old_is_x != new_is_x:
495
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
522
def _raise_if_nonexistent(paths, old_tree, new_tree):
523
"""Complain if paths are not in either inventory or tree.
525
It's OK with the files exist in either tree's inventory, or
526
if they exist in the tree but are not versioned.
528
This can be used by operations such as bzr status that can accept
529
unknown or ignored files.
531
mutter("check paths: %r", paths)
534
s = old_tree.filter_unversioned_files(paths)
535
s = new_tree.filter_unversioned_files(s)
536
s = [path for path in s if not new_tree.has_filename(path)]
538
raise errors.PathsDoNotExist(sorted(s))
541
def get_prop_change(meta_modified):
543
return " (properties changed)"
500
548
class DiffPath(object):
683
727
:param to_file_id: The id of the file in the to tree. This may refer
684
728
to a different file from from_file_id. If None,
685
729
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):
731
def _get_text(tree, file_id):
690
732
if file_id is not None:
691
return tree.get_file_lines(file_id, path)
733
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)
737
from_text = _get_text(self.old_tree, from_file_id)
738
to_text = _get_text(self.new_tree, to_file_id)
697
739
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
741
except errors.BinaryFile:
701
742
self.to_file.write(
702
743
("Binary files %s and %s differ\n" %
703
(from_label, to_label)).encode(self.path_encoding,'replace'))
744
(from_label, to_label)).encode(self.path_encoding))
704
745
return self.CHANGED
710
751
path_encoding='utf-8'):
711
752
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
712
753
self.command_template = command_template
713
self._root = osutils.mkdtemp(prefix='bzr-diff-')
754
self._root = tempfile.mkdtemp(prefix='bzr-diff-')
716
757
def from_string(klass, command_string, old_tree, new_tree, to_file,
717
758
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'])
759
command_template = commands.shlex_split_unicode(command_string)
760
command_template.extend(['%(old_path)s', '%(new_path)s'])
721
761
return klass(command_template, old_tree, new_tree, to_file,
725
def make_from_diff_tree(klass, command_string, external_diff_options=None):
765
def make_from_diff_tree(klass, command_string):
726
766
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,
767
return klass.from_string(command_string, diff_tree.old_tree,
731
768
diff_tree.new_tree, diff_tree.to_file)
732
769
return from_diff_tree
734
771
def _get_command(self, old_path, new_path):
735
772
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
773
return [t % my_map for t in self.command_template]
749
775
def _execute(self, old_path, new_path):
750
command = self._get_command(old_path, new_path)
752
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
755
if e.errno == errno.ENOENT:
756
raise errors.ExecutableMissing(command[0])
776
proc = subprocess.Popen(self._get_command(old_path, new_path),
777
stdout=subprocess.PIPE, cwd=self._root)
759
778
self.to_file.write(proc.stdout.read())
760
779
return proc.wait()
762
def _try_symlink_root(self, tree, prefix):
763
if (getattr(tree, 'abspath', None) is None
764
or not osutils.host_os_dereferences_symlinks()):
767
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
769
if e.errno != errno.EEXIST:
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):
811
parent_dir = osutils.dirname(full_path)
781
def _write_file(self, file_id, tree, prefix, old_path):
782
full_old_path = osutils.pathjoin(self._root, prefix, old_path)
783
parent_dir = osutils.dirname(full_old_path)
813
785
os.makedirs(parent_dir)
814
786
except OSError, e:
815
787
if e.errno != errno.EEXIST:
817
source = tree.get_file(file_id, relpath)
789
source = tree.get_file(file_id)
819
target = open(full_path, 'wb')
791
target = open(full_old_path, 'wb')
821
793
osutils.pumpfile(source, target)
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)
836
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
837
allow_write_new=False):
800
def _prepare_files(self, file_id, old_path, new_path):
838
801
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
839
old_path, force_temp)
840
803
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
841
new_path, force_temp,
842
allow_write=allow_write_new)
843
805
return old_disk_path, new_disk_path
845
807
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))
808
shutil.rmtree(self._root)
853
810
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
854
811
if (old_kind, new_kind) != ('file', 'file'):
855
812
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()
813
self._prepare_files(file_id, old_path, new_path)
814
self._execute(osutils.pathjoin('old', old_path),
815
osutils.pathjoin('new', new_path))
884
818
class DiffTree(object):
942
875
:param using: Commandline to use to invoke an external diff tool
944
877
if using is not None:
945
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
878
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
947
880
extra_factories = []
948
881
if external_diff_options:
882
assert isinstance(external_diff_options, basestring)
949
883
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.
884
def diff_file(olab, olines, nlab, nlines, to_file):
954
885
external_diff(olab, olines, nlab, nlines, to_file, opts)
956
887
diff_file = internal_diff
957
888
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
958
old_label, new_label, diff_file, context_lines=context_lines)
889
old_label, new_label, diff_file)
959
890
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
962
893
def show_diff(self, specific_files, extra_trees=None):
963
894
"""Write tree diff to self.to_file
965
:param specific_files: the specific files to compare (recursive)
896
:param sepecific_files: the specific files to compare (recursive)
966
897
:param extra_trees: extra trees to use for mapping paths to file_ids