/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/diff.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
 
 
 
17
import contextlib
19
18
import difflib
20
19
import os
21
20
import re
22
 
import string
23
21
import sys
24
22
 
25
23
from .lazy_import import lazy_import
26
24
lazy_import(globals(), """
27
25
import errno
 
26
import patiencediff
28
27
import subprocess
29
28
import tempfile
30
29
 
31
30
from breezy import (
32
 
    cmdline,
33
31
    controldir,
34
 
    errors,
35
32
    osutils,
36
 
    patiencediff,
37
33
    textfile,
38
34
    timestamp,
39
35
    views,
43
39
from breezy.i18n import gettext
44
40
""")
45
41
 
 
42
from . import (
 
43
    errors,
 
44
    )
46
45
from .registry import (
47
46
    Registry,
48
47
    )
49
 
from .sixish import text_type
50
48
from .trace import mutter, note, warning
51
49
from .tree import FileTimestampUnavailable
52
50
 
54
52
DEFAULT_CONTEXT_AMOUNT = 3
55
53
 
56
54
 
57
 
class AtTemplate(string.Template):
58
 
    """Templating class that uses @ instead of $."""
59
 
 
60
 
    delimiter = '@'
61
 
 
62
 
 
63
55
# TODO: Rather than building a changeset object, we should probably
64
56
# invoke callbacks on an object.  That object can either accumulate a
65
57
# list, write them out directly, etc etc.
93
85
 
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(
98
 
                                             path_encoding, 'replace'),
99
 
                                         tofile=new_label.encode(
100
 
                                             path_encoding, 'replace'),
101
 
                                         n=context_lines, sequencematcher=sequence_matcher)
 
88
    ud = unified_diff_bytes(
 
89
        oldlines, newlines,
 
90
        fromfile=old_label.encode(path_encoding, 'replace'),
 
91
        tofile=new_label.encode(path_encoding, 'replace'),
 
92
        n=context_lines, sequencematcher=sequence_matcher)
102
93
 
103
94
    ud = list(ud)
104
95
    if len(ud) == 0:  # Identical contents, nothing to do
117
108
    to_file.write(b'\n')
118
109
 
119
110
 
 
111
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
 
112
                       tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
 
113
    r"""
 
114
    Compare two sequences of lines; generate the delta as a unified diff.
 
115
 
 
116
    Unified diffs are a compact way of showing line changes and a few
 
117
    lines of context.  The number of context lines is set by 'n' which
 
118
    defaults to three.
 
119
 
 
120
    By default, the diff control lines (those with ---, +++, or @@) are
 
121
    created with a trailing newline.  This is helpful so that inputs
 
122
    created from file.readlines() result in diffs that are suitable for
 
123
    file.writelines() since both the inputs and outputs have trailing
 
124
    newlines.
 
125
 
 
126
    For inputs that do not have trailing newlines, set the lineterm
 
127
    argument to "" so that the output will be uniformly newline free.
 
128
 
 
129
    The unidiff format normally has a header for filenames and modification
 
130
    times.  Any or all of these may be specified using strings for
 
131
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
 
132
    times are normally expressed in the format returned by time.ctime().
 
133
 
 
134
    Example:
 
135
 
 
136
    >>> for line in bytes_unified_diff(b'one two three four'.split(),
 
137
    ...             b'zero one tree four'.split(), b'Original', b'Current',
 
138
    ...             b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
 
139
    ...             lineterm=b''):
 
140
    ...     print line
 
141
    --- Original Sat Jan 26 23:30:50 1991
 
142
    +++ Current Fri Jun 06 10:20:52 2003
 
143
    @@ -1,4 +1,4 @@
 
144
    +zero
 
145
     one
 
146
    -two
 
147
    -three
 
148
    +tree
 
149
     four
 
150
    """
 
151
    if sequencematcher is None:
 
152
        sequencematcher = difflib.SequenceMatcher
 
153
 
 
154
    if fromfiledate:
 
155
        fromfiledate = b'\t' + bytes(fromfiledate)
 
156
    if tofiledate:
 
157
        tofiledate = b'\t' + bytes(tofiledate)
 
158
 
 
159
    started = False
 
160
    for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
 
161
        if not started:
 
162
            yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
 
163
            yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
 
164
            started = True
 
165
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
 
166
        yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
 
167
        for tag, i1, i2, j1, j2 in group:
 
168
            if tag == 'equal':
 
169
                for line in a[i1:i2]:
 
170
                    yield b' ' + line
 
171
                continue
 
172
            if tag == 'replace' or tag == 'delete':
 
173
                for line in a[i1:i2]:
 
174
                    yield b'-' + line
 
175
            if tag == 'replace' or tag == 'insert':
 
176
                for line in b[j1:j2]:
 
177
                    yield b'+' + line
 
178
 
 
179
 
120
180
def _spawn_external_diff(diffcmd, capture_errors=True):
121
181
    """Spawn the external diff process, and return the child handle.
122
182
 
302
362
 
303
363
 
304
364
def get_trees_and_branches_to_diff_locked(
305
 
        path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
365
        path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
306
366
    """Get the trees and specific files to diff given a list of paths.
307
367
 
308
368
    This method works out the trees to be diff'ed and the files of
319
379
    :param new_url:
320
380
        The url of the new branch or tree. If None, the tree to use is
321
381
        taken from the first path, if any, or the current working tree.
322
 
    :param add_cleanup:
323
 
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
382
    :param exit_stack:
 
383
        an ExitStack object. get_trees_and_branches_to_diff
324
384
        will register cleanups that must be run to unlock the trees, etc.
325
385
    :param apply_view:
326
386
        if True and a view is set, apply the view or check that the paths
329
389
        a tuple of (old_tree, new_tree, old_branch, new_branch,
330
390
        specific_files, extra_trees) where extra_trees is a sequence of
331
391
        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
 
392
        will be read-locked until the cleanups registered via the exit_stack
333
393
        param are run.
334
394
    """
335
395
    # Get the old and new revision specs
361
421
 
362
422
    def lock_tree_or_branch(wt, br):
363
423
        if wt is not None:
364
 
            wt.lock_read()
365
 
            add_cleanup(wt.unlock)
 
424
            exit_stack.enter_context(wt.lock_read())
366
425
        elif br is not None:
367
 
            br.lock_read()
368
 
            add_cleanup(br.unlock)
 
426
            exit_stack.enter_context(br.lock_read())
369
427
 
370
428
    # Get the old location
371
429
    specific_files = []
458
516
        context = DEFAULT_CONTEXT_AMOUNT
459
517
    if format_cls is None:
460
518
        format_cls = DiffTree
461
 
    with old_tree.lock_read():
 
519
    with contextlib.ExitStack() as exit_stack:
 
520
        exit_stack.enter_context(old_tree.lock_read())
462
521
        if extra_trees is not None:
463
522
            for tree in extra_trees:
464
 
                tree.lock_read()
465
 
        new_tree.lock_read()
466
 
        try:
467
 
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
468
 
                                                   path_encoding,
469
 
                                                   external_diff_options,
470
 
                                                   old_label, new_label, using,
471
 
                                                   context_lines=context)
472
 
            return differ.show_diff(specific_files, extra_trees)
473
 
        finally:
474
 
            new_tree.unlock()
475
 
            if extra_trees is not None:
476
 
                for tree in extra_trees:
477
 
                    tree.unlock()
478
 
 
479
 
 
480
 
def _patch_header_date(tree, file_id, path):
 
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,
 
526
                                               path_encoding,
 
527
                                               external_diff_options,
 
528
                                               old_label, new_label, using,
 
529
                                               context_lines=context)
 
530
        return differ.show_diff(specific_files, extra_trees)
 
531
 
 
532
 
 
533
def _patch_header_date(tree, path):
481
534
    """Returns a timestamp suitable for use in a patch header."""
482
535
    try:
483
536
        mtime = tree.get_file_mtime(path)
526
579
                     diff_tree.to_file, diff_tree.path_encoding)
527
580
 
528
581
    @staticmethod
529
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
 
582
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
530
583
        for file_differ in differs:
531
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
532
 
                                      new_kind)
 
584
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
533
585
            if result is not DiffPath.CANNOT_DIFF:
534
586
                return result
535
587
        else:
553
605
    def from_diff_tree(klass, diff_tree):
554
606
        return klass(diff_tree.differs)
555
607
 
556
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
608
    def diff(self, old_path, new_path, old_kind, new_kind):
557
609
        """Perform comparison
558
610
 
559
 
        :param file_id: The file_id of the file to compare
560
611
        :param old_path: Path of the file in the old tree
561
612
        :param new_path: Path of the file in the new tree
562
613
        :param old_kind: Old file-kind of the file
564
615
        """
565
616
        if None in (old_kind, new_kind):
566
617
            return DiffPath.CANNOT_DIFF
567
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
568
 
                                     new_path, old_kind, None)
 
618
        result = DiffPath._diff_many(
 
619
            self.differs, old_path, new_path, old_kind, None)
569
620
        if result is DiffPath.CANNOT_DIFF:
570
621
            return result
571
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
572
 
                                   None, new_kind)
 
622
        return DiffPath._diff_many(
 
623
            self.differs, old_path, new_path, None, new_kind)
 
624
 
 
625
 
 
626
class DiffTreeReference(DiffPath):
 
627
 
 
628
    def diff(self, old_path, new_path, old_kind, new_kind):
 
629
        """Perform comparison between two tree references.  (dummy)
 
630
 
 
631
        """
 
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
 
638
        return self.CHANGED
573
639
 
574
640
 
575
641
class DiffDirectory(DiffPath):
576
642
 
577
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
643
    def diff(self, old_path, new_path, old_kind, new_kind):
578
644
        """Perform comparison between two directories.  (dummy)
579
645
 
580
646
        """
589
655
 
590
656
class DiffSymlink(DiffPath):
591
657
 
592
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
658
    def diff(self, old_path, new_path, old_kind, new_kind):
593
659
        """Perform comparison between two symlinks
594
660
 
595
 
        :param file_id: The file_id of the file to compare
596
661
        :param old_path: Path of the file in the old tree
597
662
        :param new_path: Path of the file in the new tree
598
663
        :param old_kind: Old file-kind of the file
644
709
        self.path_encoding = path_encoding
645
710
        self.context_lines = context_lines
646
711
 
647
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
712
    def diff(self, old_path, new_path, old_kind, new_kind):
648
713
        """Compare two files in unified diff format
649
714
 
650
 
        :param file_id: The file_id of the file to compare
651
715
        :param old_path: Path of the file in the old tree
652
716
        :param new_path: Path of the file in the new tree
653
717
        :param old_kind: Old file-kind of the file
655
719
        """
656
720
        if 'file' not in (old_kind, new_kind):
657
721
            return self.CANNOT_DIFF
658
 
        from_file_id = to_file_id = file_id
659
722
        if old_kind == 'file':
660
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
 
723
            old_date = _patch_header_date(self.old_tree, old_path)
661
724
        elif old_kind is None:
662
725
            old_date = self.EPOCH_DATE
663
 
            from_file_id = None
664
726
        else:
665
727
            return self.CANNOT_DIFF
666
728
        if new_kind == 'file':
667
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
 
729
            new_date = _patch_header_date(self.new_tree, new_path)
668
730
        elif new_kind is None:
669
731
            new_date = self.EPOCH_DATE
670
 
            to_file_id = None
671
732
        else:
672
733
            return self.CANNOT_DIFF
673
 
        from_label = '%s%s\t%s' % (self.old_label, old_path,
674
 
                                   old_date)
675
 
        to_label = '%s%s\t%s' % (self.new_label, new_path,
676
 
                                 new_date)
677
 
        return self.diff_text(old_path, new_path, from_label, to_label,
678
 
                              from_file_id, to_file_id)
 
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)
679
739
 
680
 
    def diff_text(self, from_path, to_path, from_label, to_label,
681
 
                  from_file_id=None, to_file_id=None):
 
740
    def diff_text(self, from_path, to_path, from_label, to_label):
682
741
        """Diff the content of given files in two trees
683
742
 
684
743
        :param from_path: The path in the from tree. If None,
686
745
        :param to_path: The path in the to tree. This may refer
687
746
            to a different file from from_path.  If None,
688
747
            the file is not present in the to tree.
689
 
        :param from_file_id: The id of the file in the from tree or None if
690
 
            unknown.
691
 
        :param to_file_id: The id of the file in the to tree or None if
692
 
            unknown.
693
748
        """
694
 
        def _get_text(tree, file_id, path):
695
 
            if file_id is None:
696
 
                return []
697
 
            return tree.get_file_lines(path)
 
749
        def _get_text(tree, path):
 
750
            if path is None:
 
751
                return []
 
752
            try:
 
753
                return tree.get_file_lines(path)
 
754
            except errors.NoSuchFile:
 
755
                return []
698
756
        try:
699
 
            from_text = _get_text(self.old_tree, from_file_id, from_path)
700
 
            to_text = _get_text(self.new_tree, to_file_id, to_path)
 
757
            from_text = _get_text(self.old_tree, from_path)
 
758
            to_text = _get_text(self.new_tree, to_path)
701
759
            self.text_differ(from_label, from_text, to_label, to_text,
702
760
                             self.to_file, path_encoding=self.path_encoding,
703
761
                             context_lines=self.context_lines)
704
762
        except errors.BinaryFile:
705
763
            self.to_file.write(
706
 
                ("Binary files %s and %s differ\n" %
707
 
                 (from_label, to_label)).encode(self.path_encoding, 'replace'))
 
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'))
708
768
        return self.CHANGED
709
769
 
710
770
 
717
777
        self._root = osutils.mkdtemp(prefix='brz-diff-')
718
778
 
719
779
    @classmethod
720
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
 
780
    def from_string(klass, command_template, old_tree, new_tree, to_file,
721
781
                    path_encoding='utf-8'):
722
 
        command_template = cmdline.split(command_string)
723
 
        if '@' not in command_string:
724
 
            command_template.extend(['@old_path', '@new_path'])
725
782
        return klass(command_template, old_tree, new_tree, to_file,
726
783
                     path_encoding)
727
784
 
737
794
 
738
795
    def _get_command(self, old_path, new_path):
739
796
        my_map = {'old_path': old_path, 'new_path': new_path}
740
 
        command = [AtTemplate(t).substitute(my_map) for t in
 
797
        command = [t.format(**my_map) for t in
741
798
                   self.command_template]
 
799
        if command == self.command_template:
 
800
            command += [old_path, new_path]
742
801
        if sys.platform == 'win32':  # Popen doesn't accept unicode on win32
743
802
            command_encoded = []
744
803
            for c in command:
745
 
                if isinstance(c, text_type):
 
804
                if isinstance(c, str):
746
805
                    command_encoded.append(c.encode('mbcs'))
747
806
                else:
748
807
                    command_encoded.append(c)
804
863
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
805
864
 
806
865
    def _write_file(self, relpath, tree, prefix, force_temp=False,
807
 
                    allow_write=False, file_id=None):
 
866
                    allow_write=False):
808
867
        if not force_temp and isinstance(tree, WorkingTree):
809
868
            full_path = tree.abspath(relpath)
810
869
            if self._is_safepath(full_path):
819
878
        except OSError as e:
820
879
            if e.errno != errno.EEXIST:
821
880
                raise
822
 
        source = tree.get_file(relpath)
823
 
        try:
824
 
            with open(full_path, 'wb') as target:
825
 
                osutils.pumpfile(source, target)
826
 
        finally:
827
 
            source.close()
 
881
        with tree.get_file(relpath) as source, \
 
882
                open(full_path, 'wb') as target:
 
883
            osutils.pumpfile(source, target)
828
884
        try:
829
885
            mtime = tree.get_file_mtime(relpath)
830
886
        except FileTimestampUnavailable:
836
892
        return full_path
837
893
 
838
894
    def _prepare_files(self, old_path, new_path, force_temp=False,
839
 
                       allow_write_new=False, file_id=None):
840
 
        old_disk_path = self._write_file(old_path, self.old_tree, 'old',
841
 
                                         force_temp, file_id=file_id)
842
 
        new_disk_path = self._write_file(new_path, self.new_tree, 'new',
843
 
                                         force_temp, file_id=file_id,
844
 
                                         allow_write=allow_write_new)
 
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)
845
901
        return old_disk_path, new_disk_path
846
902
 
847
903
    def finish(self):
852
908
                mutter("The temporary directory \"%s\" was not "
853
909
                       "cleanly removed: %s." % (self._root, e))
854
910
 
855
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
911
    def diff(self, old_path, new_path, old_kind, new_kind):
856
912
        if (old_kind, new_kind) != ('file', 'file'):
857
913
            return DiffPath.CANNOT_DIFF
858
914
        (old_disk_path, new_disk_path) = self._prepare_files(
859
 
            old_path, new_path, file_id=file_id)
 
915
            old_path, new_path)
860
916
        self._execute(old_disk_path, new_disk_path)
861
917
 
862
 
    def edit_file(self, old_path, new_path, file_id=None):
 
918
    def edit_file(self, old_path, new_path):
863
919
        """Use this tool to edit a file.
864
920
 
865
921
        A temporary copy will be edited, and the new contents will be
866
922
        returned.
867
923
 
868
 
        :param file_id: The id of the file to edit.
869
924
        :return: The new contents of the file.
870
925
        """
871
926
        old_abs_path, new_abs_path = self._prepare_files(
872
 
            old_path, new_path, allow_write_new=True, force_temp=True,
873
 
            file_id=file_id)
 
927
            old_path, new_path, allow_write_new=True, force_temp=True)
874
928
        command = self._get_command(old_abs_path, new_abs_path)
875
929
        subprocess.call(command, cwd=self._root)
876
930
        with open(new_abs_path, 'rb') as new_file:
892
946
    # list of factories that can provide instances of DiffPath objects
893
947
    # may be extended by plugins.
894
948
    diff_factories = [DiffSymlink.from_diff_tree,
895
 
                      DiffDirectory.from_diff_tree]
 
949
                      DiffDirectory.from_diff_tree,
 
950
                      DiffTreeReference.from_diff_tree]
896
951
 
897
952
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
898
953
                 diff_text=None, extra_factories=None):
979
1034
        has_changes = 0
980
1035
 
981
1036
        def changes_key(change):
982
 
            old_path, new_path = change[1]
 
1037
            old_path, new_path = change.path
983
1038
            path = new_path
984
1039
            if path is None:
985
1040
                path = old_path
988
1043
        def get_encoded_path(path):
989
1044
            if path is not None:
990
1045
                return path.encode(self.path_encoding, "replace")
991
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
992
 
             executable) in sorted(iterator, key=changes_key):
 
1046
        for change in sorted(iterator, key=changes_key):
993
1047
            # The root does not get diffed, and items with no known kind (that
994
1048
            # is, missing) in both trees are skipped as well.
995
 
            if parent == (None, None) or kind == (None, None):
996
 
                continue
997
 
            oldpath, newpath = paths
998
 
            oldpath_encoded = get_encoded_path(paths[0])
999
 
            newpath_encoded = get_encoded_path(paths[1])
1000
 
            old_present = (kind[0] is not None and versioned[0])
1001
 
            new_present = (kind[1] is not None and versioned[1])
1002
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
 
1049
            if change.parent_id == (None, None) or change.kind == (None, None):
 
1050
                continue
 
1051
            if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
 
1052
                warning(
 
1053
                    'Ignoring "%s" as symlinks are not '
 
1054
                    'supported on this filesystem.' % (change.path[0],))
 
1055
                continue
 
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
 
1062
            kind = change.kind
 
1063
            renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
1003
1064
 
1004
1065
            properties_changed = []
1005
1066
            properties_changed.extend(
1014
1075
            if (old_present, new_present) == (True, False):
1015
1076
                self.to_file.write(b"=== removed %s '%s'\n" %
1016
1077
                                   (kind[0].encode('ascii'), oldpath_encoded))
1017
 
                newpath = oldpath
1018
1078
            elif (old_present, new_present) == (False, True):
1019
1079
                self.to_file.write(b"=== added %s '%s'\n" %
1020
1080
                                   (kind[1].encode('ascii'), newpath_encoded))
1021
 
                oldpath = newpath
1022
1081
            elif renamed:
1023
1082
                self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1024
1083
                                   (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1027
1086
                # modified *somehow*, either content or execute bit.
1028
1087
                self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1029
1088
                                                                  newpath_encoded, prop_str))
1030
 
            if changed_content:
1031
 
                self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
 
1089
            if change.changed_content:
 
1090
                self._diff(oldpath, newpath, kind[0], kind[1])
1032
1091
                has_changes = 1
1033
1092
            if renamed:
1034
1093
                has_changes = 1
1035
1094
        return has_changes
1036
1095
 
1037
 
    def diff(self, file_id, old_path, new_path):
 
1096
    def diff(self, old_path, new_path):
1038
1097
        """Perform a diff of a single file
1039
1098
 
1040
 
        :param file_id: file-id of the file
1041
1099
        :param old_path: The path of the file in the old tree
1042
1100
        :param new_path: The path of the file in the new tree
1043
1101
        """
1049
1107
            new_kind = None
1050
1108
        else:
1051
1109
            new_kind = self.new_tree.kind(new_path)
1052
 
        self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
 
1110
        self._diff(old_path, new_path, old_kind, new_kind)
1053
1111
 
1054
 
    def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1055
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
1056
 
                                     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)
1057
1115
        if result is DiffPath.CANNOT_DIFF:
1058
1116
            error_path = new_path
1059
1117
            if error_path is None: