275
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
276
"""Get the trees and specific files to diff given a list of paths.
278
This method works out the trees to be diff'ed and the files of
279
interest within those trees.
282
the list of arguments passed to the diff command
283
:param revision_specs:
284
Zero, one or two RevisionSpecs from the diff command line,
285
saying what revisions to compare.
287
The url of the old branch or tree. If None, the tree to use is
288
taken from the first path, if any, or the current working tree.
290
The url of the new branch or tree. If None, the tree to use is
291
taken from the first path, if any, or the current working tree.
293
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
294
extra_trees is a sequence of additional trees to search in for
273
def diff_cmd_helper(tree, specific_files, external_diff_options,
274
old_revision_spec=None, new_revision_spec=None,
276
old_label='a/', new_label='b/'):
277
"""Helper for cmd_diff.
282
:param specific_files:
283
The specific files to compare, or None
285
:param external_diff_options:
286
If non-None, run an external diff, and pass it these options
288
:param old_revision_spec:
289
If None, use basis tree as old revision, otherwise use the tree for
290
the specified revision.
292
:param new_revision_spec:
293
If None, use working tree as new revision, otherwise use the tree for
294
the specified revision.
296
:param revision_specs:
297
Zero, one or two RevisionSpecs from the command line, saying what revisions
298
to compare. This can be passed as an alternative to the old_revision_spec
299
and new_revision_spec parameters.
301
The more general form is show_diff_trees(), where the caller
302
supplies any two trees.
297
# Get the old and new revision specs
298
old_revision_spec = None
299
new_revision_spec = None
305
# TODO: perhaps remove the old parameters old_revision_spec and
306
# new_revision_spec, since this is only really for use from cmd_diff and
307
# it now always passes through a sequence of revision_specs -- mbp
312
revision = spec.in_store(tree.branch)
314
revision = spec.in_store(None)
315
revision_id = revision.rev_id
316
branch = revision.branch
317
return branch.repository.revision_tree(revision_id)
300
319
if revision_specs is not None:
320
assert (old_revision_spec is None
321
and new_revision_spec is None)
301
322
if len(revision_specs) > 0:
302
323
old_revision_spec = revision_specs[0]
304
old_url = old_revision_spec.get_branch()
305
324
if len(revision_specs) > 1:
306
325
new_revision_spec = revision_specs[1]
308
new_url = new_revision_spec.get_branch()
311
make_paths_wt_relative = True
312
consider_relpath = True
313
if path_list is None or len(path_list) == 0:
314
# If no path is given, the current working tree is used
315
default_location = u'.'
316
consider_relpath = False
317
elif old_url is not None and new_url is not None:
318
other_paths = path_list
319
make_paths_wt_relative = False
321
default_location = path_list[0]
322
other_paths = path_list[1:]
324
# Get the old location
327
old_url = default_location
328
working_tree, branch, relpath = \
329
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
330
if consider_relpath and relpath != '':
331
specific_files.append(relpath)
332
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
334
# Get the new location
336
new_url = default_location
337
if new_url != old_url:
338
working_tree, branch, relpath = \
339
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
340
if consider_relpath and relpath != '':
341
specific_files.append(relpath)
342
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
343
basis_is_default=working_tree is None)
345
# Get the specific files (all files is None, no files is [])
346
if make_paths_wt_relative and working_tree is not None:
347
other_paths = _relative_paths_in_tree(working_tree, other_paths)
348
specific_files.extend(other_paths)
349
if len(specific_files) == 0:
350
specific_files = None
352
# Get extra trees that ought to be searched for file-ids
354
if working_tree is not None and working_tree not in (old_tree, new_tree):
355
extra_trees = (working_tree,)
356
return old_tree, new_tree, specific_files, extra_trees
359
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
360
if branch is None and tree is not None:
362
if spec is None or spec.spec is None:
365
return tree.basis_tree()
367
return branch.basis_tree()
370
return spec.as_tree(branch)
373
def _relative_paths_in_tree(tree, paths):
374
"""Get the relative paths within a working tree.
376
Each path may be either an absolute path or a path relative to the
377
current working directory.
380
for filename in paths:
382
result.append(tree.relpath(osutils.dereference_path(filename)))
383
except errors.PathNotChild:
384
raise errors.BzrCommandError("Files are in different branches")
327
if old_revision_spec is None:
328
old_tree = tree.basis_tree()
330
old_tree = spec_tree(old_revision_spec)
332
if (new_revision_spec is None
333
or new_revision_spec.spec is None):
336
new_tree = spec_tree(new_revision_spec)
338
if new_tree is not tree:
339
extra_trees = (tree,)
343
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
344
external_diff_options,
345
old_label=old_label, new_label=new_label,
346
extra_trees=extra_trees)
388
349
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
389
350
external_diff_options=None,
390
351
old_label='a/', new_label='b/',
391
352
extra_trees=None,
392
path_encoding='utf8',
353
path_encoding='utf8'):
394
354
"""Show in text form the changes from one tree to another.
400
Include only changes to these files - None for all changes.
357
If set, include only changes to these files.
402
359
external_diff_options
403
360
If set, use an external GNU diff and pass these options.
433
390
def _patch_header_date(tree, file_id, path):
434
391
"""Returns a timestamp suitable for use in a patch header."""
435
392
mtime = tree.get_file_mtime(file_id, path)
393
assert mtime is not None, \
394
"got an mtime of None for file-id %s, path %s in tree %s" % (
436
396
return timestamp.format_patch_date(mtime)
439
@deprecated_function(one_three)
399
def _raise_if_nonexistent(paths, old_tree, new_tree):
400
"""Complain if paths are not in either inventory or tree.
402
It's OK with the files exist in either tree's inventory, or
403
if they exist in the tree but are not versioned.
405
This can be used by operations such as bzr status that can accept
406
unknown or ignored files.
408
mutter("check paths: %r", paths)
411
s = old_tree.filter_unversioned_files(paths)
412
s = new_tree.filter_unversioned_files(s)
413
s = [path for path in s if not new_tree.has_filename(path)]
415
raise errors.PathsDoNotExist(sorted(s))
440
418
def get_prop_change(meta_modified):
441
419
if meta_modified:
442
420
return " (properties changed)"
446
def get_executable_change(old_is_x, new_is_x):
447
descr = { True:"+x", False:"-x", None:"??" }
448
if old_is_x != new_is_x:
449
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
454
425
class DiffPath(object):
455
426
"""Base type for command object that compare files"""
651
616
return self.CHANGED
654
class DiffFromTool(DiffPath):
656
def __init__(self, command_template, old_tree, new_tree, to_file,
657
path_encoding='utf-8'):
658
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
659
self.command_template = command_template
660
self._root = osutils.mkdtemp(prefix='bzr-diff-')
663
def from_string(klass, command_string, old_tree, new_tree, to_file,
664
path_encoding='utf-8'):
665
command_template = commands.shlex_split_unicode(command_string)
666
command_template.extend(['%(old_path)s', '%(new_path)s'])
667
return klass(command_template, old_tree, new_tree, to_file,
671
def make_from_diff_tree(klass, command_string):
672
def from_diff_tree(diff_tree):
673
return klass.from_string(command_string, diff_tree.old_tree,
674
diff_tree.new_tree, diff_tree.to_file)
675
return from_diff_tree
677
def _get_command(self, old_path, new_path):
678
my_map = {'old_path': old_path, 'new_path': new_path}
679
return [t % my_map for t in self.command_template]
681
def _execute(self, old_path, new_path):
682
command = self._get_command(old_path, new_path)
684
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
687
if e.errno == errno.ENOENT:
688
raise errors.ExecutableMissing(command[0])
691
self.to_file.write(proc.stdout.read())
694
def _try_symlink_root(self, tree, prefix):
695
if (getattr(tree, 'abspath', None) is None
696
or not osutils.host_os_dereferences_symlinks()):
699
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
701
if e.errno != errno.EEXIST:
705
def _write_file(self, file_id, tree, prefix, relpath):
706
full_path = osutils.pathjoin(self._root, prefix, relpath)
707
if self._try_symlink_root(tree, prefix):
709
parent_dir = osutils.dirname(full_path)
711
os.makedirs(parent_dir)
713
if e.errno != errno.EEXIST:
715
source = tree.get_file(file_id, relpath)
717
target = open(full_path, 'wb')
719
osutils.pumpfile(source, target)
724
osutils.make_readonly(full_path)
725
mtime = tree.get_file_mtime(file_id)
726
os.utime(full_path, (mtime, mtime))
729
def _prepare_files(self, file_id, old_path, new_path):
730
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
732
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
734
return old_disk_path, new_disk_path
737
osutils.rmtree(self._root)
739
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
740
if (old_kind, new_kind) != ('file', 'file'):
741
return DiffPath.CANNOT_DIFF
742
self._prepare_files(file_id, old_path, new_path)
743
self._execute(osutils.pathjoin('old', old_path),
744
osutils.pathjoin('new', new_path))
747
619
class DiffTree(object):
748
620
"""Provides textual representations of the difference between two trees.
824
691
:param sepecific_files: the specific files to compare (recursive)
825
692
:param extra_trees: extra trees to use for mapping paths to file_ids
828
return self._show_diff(specific_files, extra_trees)
830
for differ in self.differs:
833
def _show_diff(self, specific_files, extra_trees):
834
694
# TODO: Generation of pseudo-diffs for added/deleted files could
835
695
# be usefully made into a much faster special case.
836
iterator = self.new_tree.iter_changes(self.old_tree,
837
specific_files=specific_files,
838
extra_trees=extra_trees,
839
require_versioned=True)
697
delta = self.new_tree.changes_from(self.old_tree,
698
specific_files=specific_files,
699
extra_trees=extra_trees, require_versioned=True)
841
def changes_key(change):
842
old_path, new_path = change[1]
847
def get_encoded_path(path):
849
return path.encode(self.path_encoding, "replace")
850
for (file_id, paths, changed_content, versioned, parent, name, kind,
851
executable) in sorted(iterator, key=changes_key):
852
# The root does not get diffed, and items with no known kind (that
853
# is, missing) in both trees are skipped as well.
854
if parent == (None, None) or kind == (None, None):
856
oldpath, newpath = paths
857
oldpath_encoded = get_encoded_path(paths[0])
858
newpath_encoded = get_encoded_path(paths[1])
859
old_present = (kind[0] is not None and versioned[0])
860
new_present = (kind[1] is not None and versioned[1])
861
renamed = (parent[0], name[0]) != (parent[1], name[1])
863
properties_changed = []
864
properties_changed.extend(get_executable_change(executable[0], executable[1]))
866
if properties_changed:
867
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
871
if (old_present, new_present) == (True, False):
872
self.to_file.write("=== removed %s '%s'\n" %
873
(kind[0], oldpath_encoded))
875
elif (old_present, new_present) == (False, True):
876
self.to_file.write("=== added %s '%s'\n" %
877
(kind[1], newpath_encoded))
880
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
881
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
883
# if it was produced by iter_changes, it must be
884
# modified *somehow*, either content or execute bit.
885
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
886
newpath_encoded, prop_str))
888
self.diff(file_id, oldpath, newpath)
702
for path, file_id, kind in delta.removed:
704
path_encoded = path.encode(self.path_encoding, "replace")
705
self.to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
706
self.diff(file_id, path, path)
708
for path, file_id, kind in delta.added:
710
path_encoded = path.encode(self.path_encoding, "replace")
711
self.to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
712
self.diff(file_id, path, path)
713
for (old_path, new_path, file_id, kind,
714
text_modified, meta_modified) in delta.renamed:
716
prop_str = get_prop_change(meta_modified)
717
oldpath_encoded = old_path.encode(self.path_encoding, "replace")
718
newpath_encoded = new_path.encode(self.path_encoding, "replace")
719
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
720
oldpath_encoded, newpath_encoded, prop_str))
722
self.diff(file_id, old_path, new_path)
723
for path, file_id, kind, text_modified, meta_modified in\
726
prop_str = get_prop_change(meta_modified)
727
path_encoded = path.encode(self.path_encoding, "replace")
728
self.to_file.write("=== modified %s '%s'%s\n" % (kind,
729
path_encoded, prop_str))
730
# The file may be in a different location in the old tree (because
731
# the containing dir was renamed, but the file itself was not)
733
old_path = self.old_tree.id2path(file_id)
734
self.diff(file_id, old_path, path)
892
735
return has_changes
894
737
def diff(self, file_id, old_path, new_path):