~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patiencediff.py

  • Committer: Robert Collins
  • Date: 2007-04-30 05:13:58 UTC
  • mfrom: (2470 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2471.
  • Revision ID: robertc@robertcollins.net-20070430051358-8cp7kvp1q0tqhxx0
Merge Johns fix for bug 110399.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
 
19
19
from bisect import bisect
20
 
from copy import copy
21
20
import difflib
22
21
import os
23
22
import sys
24
23
import time
25
24
 
26
 
 
27
 
__all__ = ['SequenceMatcher', 'unified_diff', 'unified_diff_files']
 
25
from bzrlib.trace import mutter
 
26
 
 
27
 
 
28
__all__ = ['PatienceSequenceMatcher', 'unified_diff', 'unified_diff_files']
28
29
 
29
30
 
30
31
def unique_lcs(a, b):
42
43
    http://en.wikipedia.org/wiki/Patience_sorting
43
44
    """
44
45
    # set index[line in a] = position of line in a unless
45
 
    # unless a is a duplicate, in which case it's set to None
 
46
    # a is a duplicate, in which case it's set to None
46
47
    index = {}
47
48
    for i in xrange(len(a)):
48
49
        line = a[i]
105
106
    return result
106
107
 
107
108
 
108
 
def recurse_matches(a, b, ahi, bhi, answer, maxrecursion):
 
109
def recurse_matches(a, b, alo, blo, ahi, bhi, answer, maxrecursion):
109
110
    """Find all of the matching text in the lines of a and b.
110
111
 
111
112
    :param a: A sequence
112
113
    :param b: Another sequence
 
114
    :param alo: The start location of a to check, typically 0
 
115
    :param ahi: The start location of b to check, typically 0
113
116
    :param ahi: The maximum length of a to check, typically len(a)
114
117
    :param bhi: The maximum length of b to check, typically len(b)
115
118
    :param answer: The return array. Will be filled with tuples
116
 
                   indicating [(line_in_a), (line_in_b)]
 
119
                   indicating [(line_in_a, line_in_b)]
117
120
    :param maxrecursion: The maximum depth to recurse.
118
121
                         Must be a positive integer.
119
122
    :return: None, the return value is in the parameter answer, which
120
123
             should be a list
121
124
 
122
125
    """
123
 
    oldlen = len(answer)
124
126
    if maxrecursion < 0:
 
127
        mutter('max recursion depth reached')
125
128
        # this will never happen normally, this check is to prevent DOS attacks
126
129
        return
127
130
    oldlength = len(answer)
128
 
    if len(answer) == 0:
129
 
        alo, blo = 0, 0
130
 
    else:
131
 
        alo, blo = answer[-1]
132
 
        alo += 1
133
 
        blo += 1
134
131
    if alo == ahi or blo == bhi:
135
132
        return
 
133
    last_a_pos = alo-1
 
134
    last_b_pos = blo-1
136
135
    for apos, bpos in unique_lcs(a[alo:ahi], b[blo:bhi]):
137
136
        # recurse between lines which are unique in each file and match
138
137
        apos += alo
139
138
        bpos += blo
140
 
        recurse_matches(a, b, apos, bpos, answer, maxrecursion - 1)
 
139
        # Most of the time, you will have a sequence of similar entries
 
140
        if last_a_pos+1 != apos or last_b_pos+1 != bpos:
 
141
            recurse_matches(a, b, last_a_pos+1, last_b_pos+1,
 
142
                apos, bpos, answer, maxrecursion - 1)
 
143
        last_a_pos = apos
 
144
        last_b_pos = bpos
141
145
        answer.append((apos, bpos))
142
146
    if len(answer) > oldlength:
143
147
        # find matches between the last match and the end
144
 
        recurse_matches(a, b, ahi, bhi, answer, maxrecursion - 1)
 
148
        recurse_matches(a, b, last_a_pos+1, last_b_pos+1,
 
149
                        ahi, bhi, answer, maxrecursion - 1)
145
150
    elif a[alo] == b[blo]:
146
151
        # find matching lines at the very beginning
147
152
        while alo < ahi and blo < bhi and a[alo] == b[blo]:
148
153
            answer.append((alo, blo))
149
154
            alo += 1
150
155
            blo += 1
151
 
        recurse_matches(a, b, ahi, bhi, answer, maxrecursion - 1)
 
156
        recurse_matches(a, b, alo, blo,
 
157
                        ahi, bhi, answer, maxrecursion - 1)
152
158
    elif a[ahi - 1] == b[bhi - 1]:
153
159
        # find matching lines at the very end
154
160
        nahi = ahi - 1
156
162
        while nahi > alo and nbhi > blo and a[nahi - 1] == b[nbhi - 1]:
157
163
            nahi -= 1
158
164
            nbhi -= 1
159
 
        recurse_matches(a, b, nahi, nbhi, answer, maxrecursion - 1)
 
165
        recurse_matches(a, b, last_a_pos+1, last_b_pos+1,
 
166
                        nahi, nbhi, answer, maxrecursion - 1)
160
167
        for i in xrange(ahi - nahi):
161
168
            answer.append((nahi + i, nbhi + i))
162
169
 
163
170
 
164
 
class SequenceMatcher(difflib.SequenceMatcher):
 
171
def _collapse_sequences(matches):
 
172
    """Find sequences of lines.
 
173
 
 
174
    Given a sequence of [(line_in_a, line_in_b),]
 
175
    find regions where they both increment at the same time
 
176
    """
 
177
    answer = []
 
178
    start_a = start_b = None
 
179
    length = 0
 
180
    for i_a, i_b in matches:
 
181
        if (start_a is not None
 
182
            and (i_a == start_a + length) 
 
183
            and (i_b == start_b + length)):
 
184
            length += 1
 
185
        else:
 
186
            if start_a is not None:
 
187
                answer.append((start_a, start_b, length))
 
188
            start_a = i_a
 
189
            start_b = i_b
 
190
            length = 1
 
191
 
 
192
    if length != 0:
 
193
        answer.append((start_a, start_b, length))
 
194
 
 
195
    return answer
 
196
 
 
197
 
 
198
def _check_consistency(answer):
 
199
    # For consistency sake, make sure all matches are only increasing
 
200
    next_a = -1
 
201
    next_b = -1
 
202
    for a,b,match_len in answer:
 
203
        assert a >= next_a, 'Non increasing matches for a'
 
204
        assert b >= next_b, 'Not increasing matches for b'
 
205
        next_a = a + match_len
 
206
        next_b = b + match_len
 
207
 
 
208
 
 
209
class PatienceSequenceMatcher(difflib.SequenceMatcher):
165
210
    """Compare a pair of sequences using longest common subset."""
166
211
 
 
212
    _do_check_consistency = True
 
213
 
167
214
    def __init__(self, isjunk=None, a='', b=''):
168
215
        if isjunk is not None:
169
216
            raise NotImplementedError('Currently we do not support'
170
217
                                      ' isjunk for sequence matching')
171
218
        difflib.SequenceMatcher.__init__(self, isjunk, a, b)
172
219
 
173
 
    def _check_with_diff(self, alo, ahi, blo, bhi, answer):
174
 
        """Use the original diff algorithm on an unmatched section.
175
 
 
176
 
        This will check to make sure the range is worth checking,
177
 
        before doing any work.
178
 
 
179
 
        :param alo: The last line that actually matched
180
 
        :param ahi: The next line that actually matches
181
 
        :param blo: Same as alo, only for the 'b' set
182
 
        :param bhi: Same as ahi
183
 
        :param answer: An array which will have the new ranges appended to it
184
 
        :return: None
 
220
    def get_matching_blocks(self):
 
221
        """Return list of triples describing matching subsequences.
 
222
 
 
223
        Each triple is of the form (i, j, n), and means that
 
224
        a[i:i+n] == b[j:j+n].  The triples are monotonically increasing in
 
225
        i and in j.
 
226
 
 
227
        The last triple is a dummy, (len(a), len(b), 0), and is the only
 
228
        triple with n==0.
 
229
 
 
230
        >>> s = PatienceSequenceMatcher(None, "abxcd", "abcd")
 
231
        >>> s.get_matching_blocks()
 
232
        [(0, 0, 2), (3, 2, 2), (5, 4, 0)]
185
233
        """
186
 
        # WORKAROUND
187
 
        # recurse_matches has an implementation design
188
 
        # which does not match non-unique lines in the
189
 
        # if they do not touch matching unique lines
190
 
        # so we rerun the regular diff algorithm
191
 
        # if find a large enough chunk.
192
 
 
193
 
        # recurse_matches already looked at the direct
194
 
        # neighbors, so we only need to run if there is
195
 
        # enough space to do so
196
 
        if ahi - alo > 2 and bhi - blo > 2:
197
 
            a = self.a[alo+1:ahi-1]
198
 
            b = self.b[blo+1:bhi-1]
199
 
            m = difflib.SequenceMatcher(None, a, b)
200
 
            new_blocks = m.get_matching_blocks()
201
 
            # difflib always adds a final match
202
 
            new_blocks.pop()
203
 
            for blk in new_blocks:
204
 
                answer.append((blk[0]+alo+1,
205
 
                               blk[1]+blo+1,
206
 
                               blk[2]))
207
 
 
208
 
    def __helper(self, alo, ahi, blo, bhi, answer):
 
234
        # jam 20060525 This is the python 2.4.1 difflib get_matching_blocks 
 
235
        # implementation which uses __helper. 2.4.3 got rid of helper for
 
236
        # doing it inline with a queue.
 
237
        # We should consider doing the same for recurse_matches
 
238
 
 
239
        if self.matching_blocks is not None:
 
240
            return self.matching_blocks
 
241
 
209
242
        matches = []
210
 
        a = self.a[alo:ahi]
211
 
        b = self.b[blo:bhi]
212
 
        recurse_matches(a, b, len(a), len(b), matches, 10)
 
243
        recurse_matches(self.a, self.b, 0, 0,
 
244
                        len(self.a), len(self.b), matches, 10)
213
245
        # Matches now has individual line pairs of
214
246
        # line A matches line B, at the given offsets
215
 
 
216
 
        start_a = start_b = None
217
 
        length = 0
218
 
        for i_a, i_b in matches:
219
 
            if (start_a is not None
220
 
                and (i_a == start_a + length) 
221
 
                and (i_b == start_b + length)):
222
 
                length += 1
223
 
            else:
224
 
                # New block
225
 
                if start_a is None:
226
 
                    # We need to check from 0,0 until the current match
227
 
                    self._check_with_diff(alo-1, i_a+alo, blo-1, i_b+blo, 
228
 
                                          answer)
229
 
                else:
230
 
                    answer.append((start_a+alo, start_b+blo, length))
231
 
                    self._check_with_diff(start_a+alo+length, i_a+alo,
232
 
                                          start_b+blo+length, i_b+blo,
233
 
                                          answer)
234
 
 
235
 
                start_a = i_a
236
 
                start_b = i_b
237
 
                length = 1
238
 
 
239
 
        if length != 0:
240
 
            answer.append((start_a+alo, start_b+blo, length))
241
 
            self._check_with_diff(start_a+alo+length, ahi+1,
242
 
                                  start_b+blo+length, bhi+1,
243
 
                                  answer)
244
 
        if not matches:
245
 
            # Nothing matched, so we need to send the complete text
246
 
            self._check_with_diff(alo-1, ahi+1, blo-1, bhi+1, answer)
247
 
 
248
 
        # For consistency sake, make sure all matches are only increasing
249
 
        if __debug__:
250
 
            next_a = -1
251
 
            next_b = -1
252
 
            for a,b,match_len in answer:
253
 
                assert a >= next_a, 'Non increasing matches for a'
254
 
                assert b >= next_b, 'Not increasing matches for b'
255
 
                next_a = a + match_len
256
 
                next_b = b + match_len
 
247
        self.matching_blocks = _collapse_sequences(matches)
 
248
        self.matching_blocks.append( (len(self.a), len(self.b), 0) )
 
249
        if PatienceSequenceMatcher._do_check_consistency:
 
250
            if __debug__:
 
251
                _check_consistency(self.matching_blocks)
 
252
 
 
253
        return self.matching_blocks
257
254
 
258
255
 
259
256
# This is a version of unified_diff which only adds a factory parameter
354
351
    import optparse
355
352
    p = optparse.OptionParser(usage='%prog [options] file_a file_b'
356
353
                                    '\nFiles can be "-" to read from stdin')
357
 
    p.add_option('--cdv', dest='matcher', action='store_const', const='cdv',
358
 
                 default='cdv', help='Use the cdv difference algorithm')
 
354
    p.add_option('--patience', dest='matcher', action='store_const', const='patience',
 
355
                 default='patience', help='Use the patience difference algorithm')
359
356
    p.add_option('--difflib', dest='matcher', action='store_const', const='difflib',
360
 
                 default='cdv', help='Use python\'s difflib algorithm')
 
357
                 default='patience', help='Use python\'s difflib algorithm')
361
358
 
362
 
    algorithms = {'cdv':SequenceMatcher, 'difflib':difflib.SequenceMatcher}
 
359
    algorithms = {'patience':PatienceSequenceMatcher, 'difflib':difflib.SequenceMatcher}
363
360
 
364
361
    (opts, args) = p.parse_args(args)
365
362
    matcher = algorithms[opts.matcher]