/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: Jelmer Vernooij
  • Date: 2019-12-23 01:39:21 UTC
  • mfrom: (7424 work)
  • mto: This revision was merged to the branch mainline in revision 7425.
  • Revision ID: jelmer@jelmer.uk-20191223013921-2kzd0wlcoylgxksk
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import difflib
20
20
import os
21
21
import re
22
 
import string
23
22
import sys
24
23
 
25
24
from .lazy_import import lazy_import
26
25
lazy_import(globals(), """
27
26
import errno
 
27
import patiencediff
28
28
import subprocess
29
29
import tempfile
30
30
 
31
31
from breezy import (
32
 
    cmdline,
 
32
    cleanup,
33
33
    controldir,
34
34
    errors,
35
35
    osutils,
36
 
    patiencediff,
37
36
    textfile,
38
37
    timestamp,
39
38
    views,
54
53
DEFAULT_CONTEXT_AMOUNT = 3
55
54
 
56
55
 
57
 
class AtTemplate(string.Template):
58
 
    """Templating class that uses @ instead of $."""
59
 
 
60
 
    delimiter = '@'
61
 
 
62
 
 
63
56
# TODO: Rather than building a changeset object, we should probably
64
57
# invoke callbacks on an object.  That object can either accumulate a
65
58
# list, write them out directly, etc etc.
93
86
 
94
87
    if sequence_matcher is None:
95
88
        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)
 
89
    ud = unified_diff_bytes(
 
90
        oldlines, newlines,
 
91
        fromfile=old_label.encode(path_encoding, 'replace'),
 
92
        tofile=new_label.encode(path_encoding, 'replace'),
 
93
        n=context_lines, sequencematcher=sequence_matcher)
102
94
 
103
95
    ud = list(ud)
104
96
    if len(ud) == 0:  # Identical contents, nothing to do
117
109
    to_file.write(b'\n')
118
110
 
119
111
 
 
112
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
 
113
                       tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
 
114
    r"""
 
115
    Compare two sequences of lines; generate the delta as a unified diff.
 
116
 
 
117
    Unified diffs are a compact way of showing line changes and a few
 
118
    lines of context.  The number of context lines is set by 'n' which
 
119
    defaults to three.
 
120
 
 
121
    By default, the diff control lines (those with ---, +++, or @@) are
 
122
    created with a trailing newline.  This is helpful so that inputs
 
123
    created from file.readlines() result in diffs that are suitable for
 
124
    file.writelines() since both the inputs and outputs have trailing
 
125
    newlines.
 
126
 
 
127
    For inputs that do not have trailing newlines, set the lineterm
 
128
    argument to "" so that the output will be uniformly newline free.
 
129
 
 
130
    The unidiff format normally has a header for filenames and modification
 
131
    times.  Any or all of these may be specified using strings for
 
132
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
 
133
    times are normally expressed in the format returned by time.ctime().
 
134
 
 
135
    Example:
 
136
 
 
137
    >>> for line in bytes_unified_diff(b'one two three four'.split(),
 
138
    ...             b'zero one tree four'.split(), b'Original', b'Current',
 
139
    ...             b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
 
140
    ...             lineterm=b''):
 
141
    ...     print line
 
142
    --- Original Sat Jan 26 23:30:50 1991
 
143
    +++ Current Fri Jun 06 10:20:52 2003
 
144
    @@ -1,4 +1,4 @@
 
145
    +zero
 
146
     one
 
147
    -two
 
148
    -three
 
149
    +tree
 
150
     four
 
151
    """
 
152
    if sequencematcher is None:
 
153
        sequencematcher = difflib.SequenceMatcher
 
154
 
 
155
    if fromfiledate:
 
156
        fromfiledate = b'\t' + bytes(fromfiledate)
 
157
    if tofiledate:
 
158
        tofiledate = b'\t' + bytes(tofiledate)
 
159
 
 
160
    started = False
 
161
    for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
 
162
        if not started:
 
163
            yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
 
164
            yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
 
165
            started = True
 
166
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
 
167
        yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
 
168
        for tag, i1, i2, j1, j2 in group:
 
169
            if tag == 'equal':
 
170
                for line in a[i1:i2]:
 
171
                    yield b' ' + line
 
172
                continue
 
173
            if tag == 'replace' or tag == 'delete':
 
174
                for line in a[i1:i2]:
 
175
                    yield b'-' + line
 
176
            if tag == 'replace' or tag == 'insert':
 
177
                for line in b[j1:j2]:
 
178
                    yield b'+' + line
 
179
 
 
180
 
120
181
def _spawn_external_diff(diffcmd, capture_errors=True):
121
182
    """Spawn the external diff process, and return the child handle.
122
183
 
302
363
 
303
364
 
304
365
def get_trees_and_branches_to_diff_locked(
305
 
        path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
366
        path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
306
367
    """Get the trees and specific files to diff given a list of paths.
307
368
 
308
369
    This method works out the trees to be diff'ed and the files of
319
380
    :param new_url:
320
381
        The url of the new branch or tree. If None, the tree to use is
321
382
        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
 
383
    :param exit_stack:
 
384
        an ExitStack object. get_trees_and_branches_to_diff
324
385
        will register cleanups that must be run to unlock the trees, etc.
325
386
    :param apply_view:
326
387
        if True and a view is set, apply the view or check that the paths
329
390
        a tuple of (old_tree, new_tree, old_branch, new_branch,
330
391
        specific_files, extra_trees) where extra_trees is a sequence of
331
392
        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
 
393
        will be read-locked until the cleanups registered via the exit_stack
333
394
        param are run.
334
395
    """
335
396
    # Get the old and new revision specs
361
422
 
362
423
    def lock_tree_or_branch(wt, br):
363
424
        if wt is not None:
364
 
            wt.lock_read()
365
 
            add_cleanup(wt.unlock)
 
425
            exit_stack.enter_context(wt.lock_read())
366
426
        elif br is not None:
367
 
            br.lock_read()
368
 
            add_cleanup(br.unlock)
 
427
            exit_stack.enter_context(br.lock_read())
369
428
 
370
429
    # Get the old location
371
430
    specific_files = []
458
517
        context = DEFAULT_CONTEXT_AMOUNT
459
518
    if format_cls is None:
460
519
        format_cls = DiffTree
461
 
    with old_tree.lock_read():
 
520
    with cleanup.ExitStack() as exit_stack:
 
521
        exit_stack.enter_context(old_tree.lock_read())
462
522
        if extra_trees is not None:
463
523
            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()
 
524
                exit_stack.enter_context(tree.lock_read())
 
525
        exit_stack.enter_context(new_tree.lock_read())
 
526
        differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
527
                                               path_encoding,
 
528
                                               external_diff_options,
 
529
                                               old_label, new_label, using,
 
530
                                               context_lines=context)
 
531
        return differ.show_diff(specific_files, extra_trees)
478
532
 
479
533
 
480
534
def _patch_header_date(tree, path):
570
624
            self.differs, old_path, new_path, None, new_kind)
571
625
 
572
626
 
 
627
class DiffTreeReference(DiffPath):
 
628
 
 
629
    def diff(self, old_path, new_path, old_kind, new_kind):
 
630
        """Perform comparison between two tree references.  (dummy)
 
631
 
 
632
        """
 
633
        if 'tree-reference' not in (old_kind, new_kind):
 
634
            return self.CANNOT_DIFF
 
635
        if old_kind not in ('tree-reference', None):
 
636
            return self.CANNOT_DIFF
 
637
        if new_kind not in ('tree-reference', None):
 
638
            return self.CANNOT_DIFF
 
639
        return self.CHANGED
 
640
 
 
641
 
573
642
class DiffDirectory(DiffPath):
574
643
 
575
644
    def diff(self, old_path, new_path, old_kind, new_kind):
693
762
                             context_lines=self.context_lines)
694
763
        except errors.BinaryFile:
695
764
            self.to_file.write(
696
 
                ("Binary files %s and %s differ\n" %
697
 
                 (from_label, to_label)).encode(self.path_encoding, 'replace'))
 
765
                ("Binary files %s%s and %s%s differ\n" %
 
766
                 (self.old_label, from_path, self.new_label, to_path)).encode(self.path_encoding, 'replace'))
698
767
        return self.CHANGED
699
768
 
700
769
 
707
776
        self._root = osutils.mkdtemp(prefix='brz-diff-')
708
777
 
709
778
    @classmethod
710
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
 
779
    def from_string(klass, command_template, old_tree, new_tree, to_file,
711
780
                    path_encoding='utf-8'):
712
 
        command_template = cmdline.split(command_string)
713
 
        if '@' not in command_string:
714
 
            command_template.extend(['@old_path', '@new_path'])
715
781
        return klass(command_template, old_tree, new_tree, to_file,
716
782
                     path_encoding)
717
783
 
727
793
 
728
794
    def _get_command(self, old_path, new_path):
729
795
        my_map = {'old_path': old_path, 'new_path': new_path}
730
 
        command = [AtTemplate(t).substitute(my_map) for t in
 
796
        command = [t.format(**my_map) for t in
731
797
                   self.command_template]
732
798
        if command == self.command_template:
733
799
            command += [old_path, new_path]
811
877
        except OSError as e:
812
878
            if e.errno != errno.EEXIST:
813
879
                raise
814
 
        source = tree.get_file(relpath)
815
 
        try:
816
 
            with open(full_path, 'wb') as target:
817
 
                osutils.pumpfile(source, target)
818
 
        finally:
819
 
            source.close()
 
880
        with tree.get_file(relpath) as source, \
 
881
                open(full_path, 'wb') as target:
 
882
            osutils.pumpfile(source, target)
820
883
        try:
821
884
            mtime = tree.get_file_mtime(relpath)
822
885
        except FileTimestampUnavailable:
882
945
    # list of factories that can provide instances of DiffPath objects
883
946
    # may be extended by plugins.
884
947
    diff_factories = [DiffSymlink.from_diff_tree,
885
 
                      DiffDirectory.from_diff_tree]
 
948
                      DiffDirectory.from_diff_tree,
 
949
                      DiffTreeReference.from_diff_tree]
886
950
 
887
951
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
888
952
                 diff_text=None, extra_factories=None):
969
1033
        has_changes = 0
970
1034
 
971
1035
        def changes_key(change):
972
 
            old_path, new_path = change[1]
 
1036
            old_path, new_path = change.path
973
1037
            path = new_path
974
1038
            if path is None:
975
1039
                path = old_path
978
1042
        def get_encoded_path(path):
979
1043
            if path is not None:
980
1044
                return path.encode(self.path_encoding, "replace")
981
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
982
 
             executable) in sorted(iterator, key=changes_key):
 
1045
        for change in sorted(iterator, key=changes_key):
983
1046
            # The root does not get diffed, and items with no known kind (that
984
1047
            # is, missing) in both trees are skipped as well.
985
 
            if parent == (None, None) or kind == (None, None):
986
 
                continue
987
 
            oldpath, newpath = paths
988
 
            oldpath_encoded = get_encoded_path(paths[0])
989
 
            newpath_encoded = get_encoded_path(paths[1])
990
 
            old_present = (kind[0] is not None and versioned[0])
991
 
            new_present = (kind[1] is not None and versioned[1])
992
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
 
1048
            if change.parent_id == (None, None) or change.kind == (None, None):
 
1049
                continue
 
1050
            if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
 
1051
                warning(
 
1052
                    'Ignoring "%s" as symlinks are not '
 
1053
                    'supported on this filesystem.' % (change.path[0],))
 
1054
                continue
 
1055
            oldpath, newpath = change.path
 
1056
            oldpath_encoded = get_encoded_path(change.path[0])
 
1057
            newpath_encoded = get_encoded_path(change.path[1])
 
1058
            old_present = (change.kind[0] is not None and change.versioned[0])
 
1059
            new_present = (change.kind[1] is not None and change.versioned[1])
 
1060
            executable = change.executable
 
1061
            kind = change.kind
 
1062
            renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
993
1063
 
994
1064
            properties_changed = []
995
1065
            properties_changed.extend(
1017
1087
                # modified *somehow*, either content or execute bit.
1018
1088
                self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1019
1089
                                                                  newpath_encoded, prop_str))
1020
 
            if changed_content:
 
1090
            if change.changed_content:
1021
1091
                self._diff(oldpath, newpath, kind[0], kind[1])
1022
1092
                has_changes = 1
1023
1093
            if renamed: