339
236
msg = 'signal %d' % (-rc)
341
238
msg = 'exit code %d' % rc
240
raise errors.BzrError('external diff failed with %s; command: %r'
343
raise errors.BzrError('external diff failed with %s; command: %r'
347
245
oldtmpf.close() # and delete
351
# Warn in case the file couldn't be deleted (in case windows still
352
# holds the file open, but not if the files have already been
357
if e.errno not in (errno.ENOENT,):
358
warning('Failed to delete temporary file: %s %s', path, e)
364
def get_trees_and_branches_to_diff_locked(
365
path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
366
"""Get the trees and specific files to diff given a list of paths.
368
This method works out the trees to be diff'ed and the files of
369
interest within those trees.
372
the list of arguments passed to the diff command
373
:param revision_specs:
374
Zero, one or two RevisionSpecs from the diff command line,
375
saying what revisions to compare.
377
The url of the old branch or tree. If None, the tree to use is
378
taken from the first path, if any, or the current working tree.
380
The url of the new branch or tree. If None, the tree to use is
381
taken from the first path, if any, or the current working tree.
383
an ExitStack object. get_trees_and_branches_to_diff
384
will register cleanups that must be run to unlock the trees, etc.
386
if True and a view is set, apply the view or check that the paths
389
a tuple of (old_tree, new_tree, old_branch, new_branch,
390
specific_files, extra_trees) where extra_trees is a sequence of
391
additional trees to search in for file-ids. The trees and branches
392
will be read-locked until the cleanups registered via the exit_stack
395
# Get the old and new revision specs
396
old_revision_spec = None
397
new_revision_spec = None
247
# Clean up. Warn in case the files couldn't be deleted
248
# (in case windows still holds the file open, but not
249
# if the files have already been deleted)
251
os.remove(old_abspath)
253
if e.errno not in (errno.ENOENT,):
254
warning('Failed to delete temporary file: %s %s',
257
os.remove(new_abspath)
259
if e.errno not in (errno.ENOENT,):
260
warning('Failed to delete temporary file: %s %s',
264
@deprecated_function(zero_eight)
265
def show_diff(b, from_spec, specific_files, external_diff_options=None,
266
revision2=None, output=None, b2=None):
267
"""Shortcut for showing the diff to the working tree.
269
Please use show_diff_trees instead.
275
None for 'basis tree', or otherwise the old revision to compare against.
277
The more general form is show_diff_trees(), where the caller
278
supplies any two trees.
283
if from_spec is None:
284
old_tree = b.bzrdir.open_workingtree()
286
old_tree = old_tree = old_tree.basis_tree()
288
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
290
if revision2 is None:
292
new_tree = b.bzrdir.open_workingtree()
294
new_tree = b2.bzrdir.open_workingtree()
296
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
298
return show_diff_trees(old_tree, new_tree, output, specific_files,
299
external_diff_options)
302
def diff_cmd_helper(tree, specific_files, external_diff_options,
303
old_revision_spec=None, new_revision_spec=None,
305
old_label='a/', new_label='b/'):
306
"""Helper for cmd_diff.
311
:param specific_files:
312
The specific files to compare, or None
314
:param external_diff_options:
315
If non-None, run an external diff, and pass it these options
317
:param old_revision_spec:
318
If None, use basis tree as old revision, otherwise use the tree for
319
the specified revision.
321
:param new_revision_spec:
322
If None, use working tree as new revision, otherwise use the tree for
323
the specified revision.
325
:param revision_specs:
326
Zero, one or two RevisionSpecs from the command line, saying what revisions
327
to compare. This can be passed as an alternative to the old_revision_spec
328
and new_revision_spec parameters.
330
The more general form is show_diff_trees(), where the caller
331
supplies any two trees.
334
# TODO: perhaps remove the old parameters old_revision_spec and
335
# new_revision_spec, since this is only really for use from cmd_diff and
336
# it now always passes through a sequence of revision_specs -- mbp
341
revision = spec.in_store(tree.branch)
343
revision = spec.in_store(None)
344
revision_id = revision.rev_id
345
branch = revision.branch
346
return branch.repository.revision_tree(revision_id)
398
348
if revision_specs is not None:
349
assert (old_revision_spec is None
350
and new_revision_spec is None)
399
351
if len(revision_specs) > 0:
400
352
old_revision_spec = revision_specs[0]
402
old_url = old_revision_spec.get_branch()
403
353
if len(revision_specs) > 1:
404
354
new_revision_spec = revision_specs[1]
406
new_url = new_revision_spec.get_branch()
409
make_paths_wt_relative = True
410
consider_relpath = True
411
if path_list is None or len(path_list) == 0:
412
# If no path is given, the current working tree is used
413
default_location = u'.'
414
consider_relpath = False
415
elif old_url is not None and new_url is not None:
416
other_paths = path_list
417
make_paths_wt_relative = False
419
default_location = path_list[0]
420
other_paths = path_list[1:]
422
def lock_tree_or_branch(wt, br):
424
exit_stack.enter_context(wt.lock_read())
426
exit_stack.enter_context(br.lock_read())
428
# Get the old location
431
old_url = default_location
432
working_tree, branch, relpath = \
433
controldir.ControlDir.open_containing_tree_or_branch(old_url)
434
lock_tree_or_branch(working_tree, branch)
435
if consider_relpath and relpath != '':
436
if working_tree is not None and apply_view:
437
views.check_path_in_view(working_tree, relpath)
438
specific_files.append(relpath)
439
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
442
# Get the new location
444
new_url = default_location
445
if new_url != old_url:
446
working_tree, branch, relpath = \
447
controldir.ControlDir.open_containing_tree_or_branch(new_url)
448
lock_tree_or_branch(working_tree, branch)
449
if consider_relpath and relpath != '':
450
if working_tree is not None and apply_view:
451
views.check_path_in_view(working_tree, relpath)
452
specific_files.append(relpath)
453
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
454
basis_is_default=working_tree is None)
457
# Get the specific files (all files is None, no files is [])
458
if make_paths_wt_relative and working_tree is not None:
459
other_paths = working_tree.safe_relpath_files(
461
apply_view=apply_view)
462
specific_files.extend(other_paths)
463
if len(specific_files) == 0:
464
specific_files = None
465
if (working_tree is not None and working_tree.supports_views() and
467
view_files = working_tree.views.lookup_view()
469
specific_files = view_files
470
view_str = views.view_display_str(view_files)
471
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
473
# Get extra trees that ought to be searched for file-ids
475
if working_tree is not None and working_tree not in (old_tree, new_tree):
476
extra_trees = (working_tree,)
477
return (old_tree, new_tree, old_branch, new_branch,
478
specific_files, extra_trees)
481
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
482
if branch is None and tree is not None:
484
if spec is None or spec.spec is None:
487
return tree.basis_tree()
489
return branch.basis_tree()
492
return spec.as_tree(branch)
356
if old_revision_spec is None:
357
old_tree = tree.basis_tree()
359
old_tree = spec_tree(old_revision_spec)
361
if (new_revision_spec is None
362
or new_revision_spec.spec is None):
365
new_tree = spec_tree(new_revision_spec)
367
if new_tree is not tree:
368
extra_trees = (tree,)
372
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
373
external_diff_options,
374
old_label=old_label, new_label=new_label,
375
extra_trees=extra_trees)
495
378
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
496
379
external_diff_options=None,
497
380
old_label='a/', new_label='b/',
499
path_encoding='utf8',
502
context=DEFAULT_CONTEXT_AMOUNT):
503
382
"""Show in text form the changes from one tree to another.
505
:param to_file: The output stream.
506
:param specific_files: Include only changes to these files - None for all
508
:param external_diff_options: If set, use an external GNU diff and pass
510
:param extra_trees: If set, more Trees to use for looking up file ids
511
:param path_encoding: If set, the path will be encoded as specified,
512
otherwise is supposed to be utf8
513
:param format_cls: Formatter class (DiffTree subclass)
385
If set, include only changes to these files.
387
external_diff_options
388
If set, use an external GNU diff and pass these options.
391
If set, more Trees to use for looking up file ids
516
context = DEFAULT_CONTEXT_AMOUNT
517
if format_cls is None:
518
format_cls = DiffTree
519
with contextlib.ExitStack() as exit_stack:
520
exit_stack.enter_context(old_tree.lock_read())
521
395
if extra_trees is not None:
522
396
for tree in extra_trees:
523
exit_stack.enter_context(tree.lock_read())
524
exit_stack.enter_context(new_tree.lock_read())
525
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
527
external_diff_options,
528
old_label, new_label, using,
529
context_lines=context)
530
return differ.show_diff(specific_files, extra_trees)
533
def _patch_header_date(tree, path):
534
"""Returns a timestamp suitable for use in a patch header."""
536
mtime = tree.get_file_mtime(path)
537
except FileTimestampUnavailable:
539
return timestamp.format_patch_date(mtime)
542
def get_executable_change(old_is_x, new_is_x):
543
descr = {True: b"+x", False: b"-x", None: b"??"}
544
if old_is_x != new_is_x:
545
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
550
class DiffPath(object):
551
"""Base type for command object that compare files"""
553
# The type or contents of the file were unsuitable for diffing
554
CANNOT_DIFF = 'CANNOT_DIFF'
555
# The file has changed in a semantic way
557
# The file content may have changed, but there is no semantic change
558
UNCHANGED = 'UNCHANGED'
560
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
563
:param old_tree: The tree to show as the old tree in the comparison
564
:param new_tree: The tree to show as new in the comparison
565
:param to_file: The file to write comparison data to
566
:param path_encoding: The character encoding to write paths in
568
self.old_tree = old_tree
569
self.new_tree = new_tree
570
self.to_file = to_file
571
self.path_encoding = path_encoding
577
def from_diff_tree(klass, diff_tree):
578
return klass(diff_tree.old_tree, diff_tree.new_tree,
579
diff_tree.to_file, diff_tree.path_encoding)
582
def _diff_many(differs, old_path, new_path, old_kind, new_kind):
583
for file_differ in differs:
584
result = file_differ.diff(old_path, new_path, old_kind, new_kind)
585
if result is not DiffPath.CANNOT_DIFF:
588
return DiffPath.CANNOT_DIFF
591
class DiffKindChange(object):
592
"""Special differ for file kind changes.
594
Represents kind change as deletion + creation. Uses the other differs
598
def __init__(self, differs):
599
self.differs = differs
605
def from_diff_tree(klass, diff_tree):
606
return klass(diff_tree.differs)
608
def diff(self, old_path, new_path, old_kind, new_kind):
609
"""Perform comparison
611
:param old_path: Path of the file in the old tree
612
:param new_path: Path of the file in the new tree
613
:param old_kind: Old file-kind of the file
614
:param new_kind: New file-kind of the file
616
if None in (old_kind, new_kind):
617
return DiffPath.CANNOT_DIFF
618
result = DiffPath._diff_many(
619
self.differs, old_path, new_path, old_kind, None)
620
if result is DiffPath.CANNOT_DIFF:
622
return DiffPath._diff_many(
623
self.differs, old_path, new_path, None, new_kind)
626
class DiffTreeReference(DiffPath):
628
def diff(self, old_path, new_path, old_kind, new_kind):
629
"""Perform comparison between two tree references. (dummy)
632
if 'tree-reference' not in (old_kind, new_kind):
633
return self.CANNOT_DIFF
634
if old_kind not in ('tree-reference', None):
635
return self.CANNOT_DIFF
636
if new_kind not in ('tree-reference', None):
637
return self.CANNOT_DIFF
641
class DiffDirectory(DiffPath):
643
def diff(self, old_path, new_path, old_kind, new_kind):
644
"""Perform comparison between two directories. (dummy)
647
if 'directory' not in (old_kind, new_kind):
648
return self.CANNOT_DIFF
649
if old_kind not in ('directory', None):
650
return self.CANNOT_DIFF
651
if new_kind not in ('directory', None):
652
return self.CANNOT_DIFF
656
class DiffSymlink(DiffPath):
658
def diff(self, old_path, new_path, old_kind, new_kind):
659
"""Perform comparison between two symlinks
661
:param old_path: Path of the file in the old tree
662
:param new_path: Path of the file in the new tree
663
:param old_kind: Old file-kind of the file
664
:param new_kind: New file-kind of the file
666
if 'symlink' not in (old_kind, new_kind):
667
return self.CANNOT_DIFF
668
if old_kind == 'symlink':
669
old_target = self.old_tree.get_symlink_target(old_path)
670
elif old_kind is None:
673
return self.CANNOT_DIFF
674
if new_kind == 'symlink':
675
new_target = self.new_tree.get_symlink_target(new_path)
676
elif new_kind is None:
679
return self.CANNOT_DIFF
680
return self.diff_symlink(old_target, new_target)
682
def diff_symlink(self, old_target, new_target):
683
if old_target is None:
684
self.to_file.write(b'=== target is \'%s\'\n' %
685
new_target.encode(self.path_encoding, 'replace'))
686
elif new_target is None:
687
self.to_file.write(b'=== target was \'%s\'\n' %
688
old_target.encode(self.path_encoding, 'replace'))
690
self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
691
(old_target.encode(self.path_encoding, 'replace'),
692
new_target.encode(self.path_encoding, 'replace')))
696
class DiffText(DiffPath):
400
return _show_diff_trees(old_tree, new_tree, to_file,
401
specific_files, external_diff_options,
402
old_label=old_label, new_label=new_label,
403
extra_trees=extra_trees)
406
if extra_trees is not None:
407
for tree in extra_trees:
413
def _show_diff_trees(old_tree, new_tree, to_file,
414
specific_files, external_diff_options,
415
old_label='a/', new_label='b/', extra_trees=None):
698
417
# GNU Patch uses the epoch date to detect files that are being added
699
418
# or removed in a diff.
700
419
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
702
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
703
old_label='', new_label='', text_differ=internal_diff,
704
context_lines=DEFAULT_CONTEXT_AMOUNT):
705
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
self.text_differ = text_differ
707
self.old_label = old_label
708
self.new_label = new_label
709
self.path_encoding = path_encoding
710
self.context_lines = context_lines
712
def diff(self, old_path, new_path, old_kind, new_kind):
713
"""Compare two files in unified diff format
715
:param old_path: Path of the file in the old tree
716
:param new_path: Path of the file in the new tree
717
:param old_kind: Old file-kind of the file
718
:param new_kind: New file-kind of the file
720
if 'file' not in (old_kind, new_kind):
721
return self.CANNOT_DIFF
722
if old_kind == 'file':
723
old_date = _patch_header_date(self.old_tree, old_path)
724
elif old_kind is None:
725
old_date = self.EPOCH_DATE
727
return self.CANNOT_DIFF
728
if new_kind == 'file':
729
new_date = _patch_header_date(self.new_tree, new_path)
730
elif new_kind is None:
731
new_date = self.EPOCH_DATE
733
return self.CANNOT_DIFF
734
from_label = '%s%s\t%s' % (
735
self.old_label, old_path or new_path, old_date)
736
to_label = '%s%s\t%s' % (
737
self.new_label, new_path or old_path, new_date)
738
return self.diff_text(old_path, new_path, from_label, to_label)
740
def diff_text(self, from_path, to_path, from_label, to_label):
741
"""Diff the content of given files in two trees
743
:param from_path: The path in the from tree. If None,
744
the file is not present in the from tree.
745
:param to_path: The path in the to tree. This may refer
746
to a different file from from_path. If None,
747
the file is not present in the to tree.
749
def _get_text(tree, path):
753
return tree.get_file_lines(path)
754
except errors.NoSuchFile:
757
from_text = _get_text(self.old_tree, from_path)
758
to_text = _get_text(self.new_tree, to_path)
759
self.text_differ(from_label, from_text, to_label, to_text,
760
self.to_file, path_encoding=self.path_encoding,
761
context_lines=self.context_lines)
762
except errors.BinaryFile:
764
("Binary files %s%s and %s%s differ\n" %
765
(self.old_label, from_path or to_path,
766
self.new_label, to_path or from_path)
767
).encode(self.path_encoding, 'replace'))
771
class DiffFromTool(DiffPath):
773
def __init__(self, command_template, old_tree, new_tree, to_file,
774
path_encoding='utf-8'):
775
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
776
self.command_template = command_template
777
self._root = osutils.mkdtemp(prefix='brz-diff-')
780
def from_string(klass, command_template, old_tree, new_tree, to_file,
781
path_encoding='utf-8'):
782
return klass(command_template, old_tree, new_tree, to_file,
786
def make_from_diff_tree(klass, command_string, external_diff_options=None):
787
def from_diff_tree(diff_tree):
788
full_command_string = [command_string]
789
if external_diff_options is not None:
790
full_command_string += ' ' + external_diff_options
791
return klass.from_string(full_command_string, diff_tree.old_tree,
792
diff_tree.new_tree, diff_tree.to_file)
793
return from_diff_tree
795
def _get_command(self, old_path, new_path):
796
my_map = {'old_path': old_path, 'new_path': new_path}
797
command = [t.format(**my_map) for t in
798
self.command_template]
799
if command == self.command_template:
800
command += [old_path, new_path]
801
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
804
if isinstance(c, str):
805
command_encoded.append(c.encode('mbcs'))
807
command_encoded.append(c)
808
return command_encoded
812
def _execute(self, old_path, new_path):
813
command = self._get_command(old_path, new_path)
815
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
818
if e.errno == errno.ENOENT:
819
raise errors.ExecutableMissing(command[0])
822
self.to_file.write(proc.stdout.read())
826
def _try_symlink_root(self, tree, prefix):
827
if (getattr(tree, 'abspath', None) is None or
828
not osutils.host_os_dereferences_symlinks()):
831
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
833
if e.errno != errno.EEXIST:
839
"""Returns safe encoding for passing file path to diff tool"""
840
if sys.platform == 'win32':
843
# Don't fallback to 'utf-8' because subprocess may not be able to
844
# handle utf-8 correctly when locale is not utf-8.
845
return sys.getfilesystemencoding() or 'ascii'
847
def _is_safepath(self, path):
848
"""Return true if `path` may be able to pass to subprocess."""
851
return path == path.encode(fenc).decode(fenc)
855
def _safe_filename(self, prefix, relpath):
856
"""Replace unsafe character in `relpath` then join `self._root`,
857
`prefix` and `relpath`."""
859
# encoded_str.replace('?', '_') may break multibyte char.
860
# So we should encode, decode, then replace(u'?', u'_')
861
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
862
relpath_tmp = relpath_tmp.replace(u'?', u'_')
863
return osutils.pathjoin(self._root, prefix, relpath_tmp)
865
def _write_file(self, relpath, tree, prefix, force_temp=False,
867
if not force_temp and isinstance(tree, WorkingTree):
868
full_path = tree.abspath(relpath)
869
if self._is_safepath(full_path):
872
full_path = self._safe_filename(prefix, relpath)
873
if not force_temp and self._try_symlink_root(tree, prefix):
875
parent_dir = osutils.dirname(full_path)
877
os.makedirs(parent_dir)
879
if e.errno != errno.EEXIST:
881
with tree.get_file(relpath) as source, \
882
open(full_path, 'wb') as target:
883
osutils.pumpfile(source, target)
885
mtime = tree.get_file_mtime(relpath)
886
except FileTimestampUnavailable:
889
os.utime(full_path, (mtime, mtime))
891
osutils.make_readonly(full_path)
894
def _prepare_files(self, old_path, new_path, force_temp=False,
895
allow_write_new=False):
896
old_disk_path = self._write_file(
897
old_path, self.old_tree, 'old', force_temp)
898
new_disk_path = self._write_file(
899
new_path, self.new_tree, 'new', force_temp,
900
allow_write=allow_write_new)
901
return old_disk_path, new_disk_path
905
osutils.rmtree(self._root)
907
if e.errno != errno.ENOENT:
908
mutter("The temporary directory \"%s\" was not "
909
"cleanly removed: %s." % (self._root, e))
911
def diff(self, old_path, new_path, old_kind, new_kind):
912
if (old_kind, new_kind) != ('file', 'file'):
913
return DiffPath.CANNOT_DIFF
914
(old_disk_path, new_disk_path) = self._prepare_files(
916
self._execute(old_disk_path, new_disk_path)
918
def edit_file(self, old_path, new_path):
919
"""Use this tool to edit a file.
921
A temporary copy will be edited, and the new contents will be
924
:return: The new contents of the file.
926
old_abs_path, new_abs_path = self._prepare_files(
927
old_path, new_path, allow_write_new=True, force_temp=True)
928
command = self._get_command(old_abs_path, new_abs_path)
929
subprocess.call(command, cwd=self._root)
930
with open(new_abs_path, 'rb') as new_file:
931
return new_file.read()
934
class DiffTree(object):
935
"""Provides textual representations of the difference between two trees.
937
A DiffTree examines two trees and where a file-id has altered
938
between them, generates a textual representation of the difference.
939
DiffTree uses a sequence of DiffPath objects which are each
940
given the opportunity to handle a given altered fileid. The list
941
of DiffPath objects can be extended globally by appending to
942
DiffTree.diff_factories, or for a specific diff operation by
943
supplying the extra_factories option to the appropriate method.
421
# TODO: Generation of pseudo-diffs for added/deleted files could
422
# be usefully made into a much faster special case.
424
if external_diff_options:
425
assert isinstance(external_diff_options, basestring)
426
opts = external_diff_options.split()
427
def diff_file(olab, olines, nlab, nlines, to_file):
428
external_diff(olab, olines, nlab, nlines, to_file, opts)
430
diff_file = internal_diff
432
delta = new_tree.changes_from(old_tree,
433
specific_files=specific_files,
434
extra_trees=extra_trees, require_versioned=True)
437
for path, file_id, kind in delta.removed:
439
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
440
old_name = '%s%s\t%s' % (old_label, path,
441
_patch_header_date(old_tree, file_id, path))
442
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
443
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
444
new_name, None, None, to_file)
445
for path, file_id, kind in delta.added:
447
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
448
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
449
new_name = '%s%s\t%s' % (new_label, path,
450
_patch_header_date(new_tree, file_id, path))
451
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
452
old_name, None, None, to_file,
454
for (old_path, new_path, file_id, kind,
455
text_modified, meta_modified) in delta.renamed:
457
prop_str = get_prop_change(meta_modified)
458
print >>to_file, '=== renamed %s %r => %r%s' % (
459
kind, old_path.encode('utf8'),
460
new_path.encode('utf8'), prop_str)
461
old_name = '%s%s\t%s' % (old_label, old_path,
462
_patch_header_date(old_tree, file_id,
464
new_name = '%s%s\t%s' % (new_label, new_path,
465
_patch_header_date(new_tree, file_id,
467
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
469
text_modified, kind, to_file, diff_file)
470
for path, file_id, kind, text_modified, meta_modified in delta.modified:
472
prop_str = get_prop_change(meta_modified)
473
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
474
# The file may be in a different location in the old tree (because
475
# the containing dir was renamed, but the file itself was not)
476
old_path = old_tree.id2path(file_id)
477
old_name = '%s%s\t%s' % (old_label, old_path,
478
_patch_header_date(old_tree, file_id, old_path))
479
new_name = '%s%s\t%s' % (new_label, path,
480
_patch_header_date(new_tree, file_id, path))
482
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
484
True, kind, to_file, diff_file)
489
def _patch_header_date(tree, file_id, path):
490
"""Returns a timestamp suitable for use in a patch header."""
491
mtime = tree.get_file_mtime(file_id, path)
492
assert mtime is not None, \
493
"got an mtime of None for file-id %s, path %s in tree %s" % (
495
return timestamp.format_patch_date(mtime)
498
def _raise_if_nonexistent(paths, old_tree, new_tree):
499
"""Complain if paths are not in either inventory or tree.
501
It's OK with the files exist in either tree's inventory, or
502
if they exist in the tree but are not versioned.
504
This can be used by operations such as bzr status that can accept
505
unknown or ignored files.
946
# list of factories that can provide instances of DiffPath objects
947
# may be extended by plugins.
948
diff_factories = [DiffSymlink.from_diff_tree,
949
DiffDirectory.from_diff_tree,
950
DiffTreeReference.from_diff_tree]
952
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
953
diff_text=None, extra_factories=None):
956
:param old_tree: Tree to show as old in the comparison
957
:param new_tree: Tree to show as new in the comparison
958
:param to_file: File to write comparision to
959
:param path_encoding: Character encoding to write paths in
960
:param diff_text: DiffPath-type object to use as a last resort for
962
:param extra_factories: Factories of DiffPaths to try before any other
964
if diff_text is None:
965
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
966
'', '', internal_diff)
967
self.old_tree = old_tree
968
self.new_tree = new_tree
969
self.to_file = to_file
970
self.path_encoding = path_encoding
972
if extra_factories is not None:
973
self.differs.extend(f(self) for f in extra_factories)
974
self.differs.extend(f(self) for f in self.diff_factories)
975
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
978
def from_trees_options(klass, old_tree, new_tree, to_file,
979
path_encoding, external_diff_options, old_label,
980
new_label, using, context_lines):
981
"""Factory for producing a DiffTree.
983
Designed to accept options used by show_diff_trees.
985
:param old_tree: The tree to show as old in the comparison
986
:param new_tree: The tree to show as new in the comparison
987
:param to_file: File to write comparisons to
988
:param path_encoding: Character encoding to use for writing paths
989
:param external_diff_options: If supplied, use the installed diff
990
binary to perform file comparison, using supplied options.
991
:param old_label: Prefix to use for old file labels
992
:param new_label: Prefix to use for new file labels
993
:param using: Commandline to use to invoke an external diff tool
995
if using is not None:
996
extra_factories = [DiffFromTool.make_from_diff_tree(
997
using, external_diff_options)]
1000
if external_diff_options:
1001
opts = external_diff_options.split()
1003
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
1004
""":param path_encoding: not used but required
1005
to match the signature of internal_diff.
1007
external_diff(olab, olines, nlab, nlines, to_file, opts)
1009
diff_file = internal_diff
1010
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
1011
old_label, new_label, diff_file, context_lines=context_lines)
1012
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
1015
def show_diff(self, specific_files, extra_trees=None):
1016
"""Write tree diff to self.to_file
1018
:param specific_files: the specific files to compare (recursive)
1019
:param extra_trees: extra trees to use for mapping paths to file_ids
1022
return self._show_diff(specific_files, extra_trees)
1024
for differ in self.differs:
1027
def _show_diff(self, specific_files, extra_trees):
1028
# TODO: Generation of pseudo-diffs for added/deleted files could
1029
# be usefully made into a much faster special case.
1030
iterator = self.new_tree.iter_changes(self.old_tree,
1031
specific_files=specific_files,
1032
extra_trees=extra_trees,
1033
require_versioned=True)
1036
def changes_key(change):
1037
old_path, new_path = change.path
1043
def get_encoded_path(path):
1044
if path is not None:
1045
return path.encode(self.path_encoding, "replace")
1046
for change in sorted(iterator, key=changes_key):
1047
# The root does not get diffed, and items with no known kind (that
1048
# is, missing) in both trees are skipped as well.
1049
if change.parent_id == (None, None) or change.kind == (None, None):
1051
if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1053
'Ignoring "%s" as symlinks are not '
1054
'supported on this filesystem.' % (change.path[0],))
1056
oldpath, newpath = change.path
1057
oldpath_encoded = get_encoded_path(oldpath)
1058
newpath_encoded = get_encoded_path(newpath)
1059
old_present = (change.kind[0] is not None and change.versioned[0])
1060
new_present = (change.kind[1] is not None and change.versioned[1])
1061
executable = change.executable
1063
renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
1065
properties_changed = []
1066
properties_changed.extend(
1067
get_executable_change(executable[0], executable[1]))
1069
if properties_changed:
1070
prop_str = b" (properties changed: %s)" % (
1071
b", ".join(properties_changed),)
1075
if (old_present, new_present) == (True, False):
1076
self.to_file.write(b"=== removed %s '%s'\n" %
1077
(kind[0].encode('ascii'), oldpath_encoded))
1078
elif (old_present, new_present) == (False, True):
1079
self.to_file.write(b"=== added %s '%s'\n" %
1080
(kind[1].encode('ascii'), newpath_encoded))
1082
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1083
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1085
# if it was produced by iter_changes, it must be
1086
# modified *somehow*, either content or execute bit.
1087
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1088
newpath_encoded, prop_str))
1089
if change.changed_content:
1090
self._diff(oldpath, newpath, kind[0], kind[1])
1096
def diff(self, old_path, new_path):
1097
"""Perform a diff of a single file
1099
:param old_path: The path of the file in the old tree
1100
:param new_path: The path of the file in the new tree
1102
if old_path is None:
1105
old_kind = self.old_tree.kind(old_path)
1106
if new_path is None:
1109
new_kind = self.new_tree.kind(new_path)
1110
self._diff(old_path, new_path, old_kind, new_kind)
1112
def _diff(self, old_path, new_path, old_kind, new_kind):
1113
result = DiffPath._diff_many(
1114
self.differs, old_path, new_path, old_kind, new_kind)
1115
if result is DiffPath.CANNOT_DIFF:
1116
error_path = new_path
1117
if error_path is None:
1118
error_path = old_path
1119
raise errors.NoDiffFound(error_path)
1122
format_registry = Registry()
1123
format_registry.register('default', DiffTree)
507
mutter("check paths: %r", paths)
510
s = old_tree.filter_unversioned_files(paths)
511
s = new_tree.filter_unversioned_files(s)
512
s = [path for path in s if not new_tree.has_filename(path)]
514
raise errors.PathsDoNotExist(sorted(s))
517
def get_prop_change(meta_modified):
519
return " (properties changed)"
524
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
525
new_path, new_tree, text_modified,
526
kind, to_file, diff_file):
528
new_entry = new_tree.inventory[file_id]
529
old_tree.inventory[file_id].diff(diff_file,