~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/_patiencediff_py.py

  • Committer: Andrew Bennetts
  • Date: 2008-09-05 10:48:03 UTC
  • mto: This revision was merged to the branch mainline in revision 3693.
  • Revision ID: andrew.bennetts@canonical.com-20080905104803-6g72dz6wcldosfs2
Remove monkey-patching of branch._ensure_real from test_remote.py.

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
 
import os
23
 
import sys
24
 
import time
25
21
 
26
22
from bzrlib.trace import mutter
27
23
 
29
25
__all__ = ['PatienceSequenceMatcher', 'unified_diff', 'unified_diff_files']
30
26
 
31
27
 
32
 
def unique_lcs(a, b):
 
28
def unique_lcs_py(a, b):
33
29
    """Find the longest common subset for unique lines.
34
30
 
35
31
    :param a: An indexable object (such as string or list of strings)
44
40
    http://en.wikipedia.org/wiki/Patience_sorting
45
41
    """
46
42
    # set index[line in a] = position of line in a unless
47
 
    # unless a is a duplicate, in which case it's set to None
 
43
    # a is a duplicate, in which case it's set to None
48
44
    index = {}
49
45
    for i in xrange(len(a)):
50
46
        line = a[i]
107
103
    return result
108
104
 
109
105
 
110
 
def recurse_matches(a, b, alo, blo, ahi, bhi, answer, maxrecursion):
 
106
def recurse_matches_py(a, b, alo, blo, ahi, bhi, answer, maxrecursion):
111
107
    """Find all of the matching text in the lines of a and b.
112
108
 
113
109
    :param a: A sequence
133
129
        return
134
130
    last_a_pos = alo-1
135
131
    last_b_pos = blo-1
136
 
    for apos, bpos in unique_lcs(a[alo:ahi], b[blo:bhi]):
 
132
    for apos, bpos in unique_lcs_py(a[alo:ahi], b[blo:bhi]):
137
133
        # recurse between lines which are unique in each file and match
138
134
        apos += alo
139
135
        bpos += blo
140
136
        # Most of the time, you will have a sequence of similar entries
141
137
        if last_a_pos+1 != apos or last_b_pos+1 != bpos:
142
 
            recurse_matches(a, b, last_a_pos+1, last_b_pos+1,
 
138
            recurse_matches_py(a, b, last_a_pos+1, last_b_pos+1,
143
139
                apos, bpos, answer, maxrecursion - 1)
144
140
        last_a_pos = apos
145
141
        last_b_pos = bpos
146
142
        answer.append((apos, bpos))
147
143
    if len(answer) > oldlength:
148
144
        # find matches between the last match and the end
149
 
        recurse_matches(a, b, last_a_pos+1, last_b_pos+1,
150
 
                        ahi, bhi, answer, maxrecursion - 1)
 
145
        recurse_matches_py(a, b, last_a_pos+1, last_b_pos+1,
 
146
                           ahi, bhi, answer, maxrecursion - 1)
151
147
    elif a[alo] == b[blo]:
152
148
        # find matching lines at the very beginning
153
149
        while alo < ahi and blo < bhi and a[alo] == b[blo]:
154
150
            answer.append((alo, blo))
155
151
            alo += 1
156
152
            blo += 1
157
 
        recurse_matches(a, b, alo, blo,
158
 
                        ahi, bhi, answer, maxrecursion - 1)
 
153
        recurse_matches_py(a, b, alo, blo,
 
154
                           ahi, bhi, answer, maxrecursion - 1)
159
155
    elif a[ahi - 1] == b[bhi - 1]:
160
156
        # find matching lines at the very end
161
157
        nahi = ahi - 1
163
159
        while nahi > alo and nbhi > blo and a[nahi - 1] == b[nbhi - 1]:
164
160
            nahi -= 1
165
161
            nbhi -= 1
166
 
        recurse_matches(a, b, last_a_pos+1, last_b_pos+1,
167
 
                        nahi, nbhi, answer, maxrecursion - 1)
 
162
        recurse_matches_py(a, b, last_a_pos+1, last_b_pos+1,
 
163
                           nahi, nbhi, answer, maxrecursion - 1)
168
164
        for i in xrange(ahi - nahi):
169
165
            answer.append((nahi + i, nbhi + i))
170
166
 
180
176
    length = 0
181
177
    for i_a, i_b in matches:
182
178
        if (start_a is not None
183
 
            and (i_a == start_a + length) 
 
179
            and (i_a == start_a + length)
184
180
            and (i_b == start_b + length)):
185
181
            length += 1
186
182
        else:
200
196
    # For consistency sake, make sure all matches are only increasing
201
197
    next_a = -1
202
198
    next_b = -1
203
 
    for a,b,match_len in answer:
204
 
        assert a >= next_a, 'Non increasing matches for a'
205
 
        assert b >= next_b, 'Not increasing matches for b'
 
199
    for (a, b, match_len) in answer:
 
200
        if a < next_a:
 
201
            raise ValueError('Non increasing matches for a')
 
202
        if b < next_b:
 
203
            raise ValueError('Non increasing matches for b')
206
204
        next_a = a + match_len
207
205
        next_b = b + match_len
208
206
 
209
207
 
210
 
class PatienceSequenceMatcher(difflib.SequenceMatcher):
 
208
class PatienceSequenceMatcher_py(difflib.SequenceMatcher):
211
209
    """Compare a pair of sequences using longest common subset."""
212
210
 
213
211
    _do_check_consistency = True
241
239
            return self.matching_blocks
242
240
 
243
241
        matches = []
244
 
        recurse_matches(self.a, self.b, 0, 0,
245
 
                        len(self.a), len(self.b), matches, 10)
 
242
        recurse_matches_py(self.a, self.b, 0, 0,
 
243
                           len(self.a), len(self.b), matches, 10)
246
244
        # Matches now has individual line pairs of
247
245
        # line A matches line B, at the given offsets
248
246
        self.matching_blocks = _collapse_sequences(matches)
249
247
        self.matching_blocks.append( (len(self.a), len(self.b), 0) )
250
 
        if PatienceSequenceMatcher._do_check_consistency:
 
248
        if PatienceSequenceMatcher_py._do_check_consistency:
251
249
            if __debug__:
252
250
                _check_consistency(self.matching_blocks)
253
251
 
254
252
        return self.matching_blocks
255
 
 
256
 
 
257
 
# This is a version of unified_diff which only adds a factory parameter
258
 
# so that you can override the default SequenceMatcher
259
 
# this has been submitted as a patch to python
260
 
def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
261
 
                 tofiledate='', n=3, lineterm='\n',
262
 
                 sequencematcher=None):
263
 
    r"""
264
 
    Compare two sequences of lines; generate the delta as a unified diff.
265
 
 
266
 
    Unified diffs are a compact way of showing line changes and a few
267
 
    lines of context.  The number of context lines is set by 'n' which
268
 
    defaults to three.
269
 
 
270
 
    By default, the diff control lines (those with ---, +++, or @@) are
271
 
    created with a trailing newline.  This is helpful so that inputs
272
 
    created from file.readlines() result in diffs that are suitable for
273
 
    file.writelines() since both the inputs and outputs have trailing
274
 
    newlines.
275
 
 
276
 
    For inputs that do not have trailing newlines, set the lineterm
277
 
    argument to "" so that the output will be uniformly newline free.
278
 
 
279
 
    The unidiff format normally has a header for filenames and modification
280
 
    times.  Any or all of these may be specified using strings for
281
 
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
282
 
    times are normally expressed in the format returned by time.ctime().
283
 
 
284
 
    Example:
285
 
 
286
 
    >>> for line in unified_diff('one two three four'.split(),
287
 
    ...             'zero one tree four'.split(), 'Original', 'Current',
288
 
    ...             'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:20:52 2003',
289
 
    ...             lineterm=''):
290
 
    ...     print line
291
 
    --- Original Sat Jan 26 23:30:50 1991
292
 
    +++ Current Fri Jun 06 10:20:52 2003
293
 
    @@ -1,4 +1,4 @@
294
 
    +zero
295
 
     one
296
 
    -two
297
 
    -three
298
 
    +tree
299
 
     four
300
 
    """
301
 
    if sequencematcher is None:
302
 
        sequencematcher = difflib.SequenceMatcher
303
 
 
304
 
    started = False
305
 
    for group in sequencematcher(None,a,b).get_grouped_opcodes(n):
306
 
        if not started:
307
 
            yield '--- %s %s%s' % (fromfile, fromfiledate, lineterm)
308
 
            yield '+++ %s %s%s' % (tofile, tofiledate, lineterm)
309
 
            started = True
310
 
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
311
 
        yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
312
 
        for tag, i1, i2, j1, j2 in group:
313
 
            if tag == 'equal':
314
 
                for line in a[i1:i2]:
315
 
                    yield ' ' + line
316
 
                continue
317
 
            if tag == 'replace' or tag == 'delete':
318
 
                for line in a[i1:i2]:
319
 
                    yield '-' + line
320
 
            if tag == 'replace' or tag == 'insert':
321
 
                for line in b[j1:j2]:
322
 
                    yield '+' + line
323
 
 
324
 
 
325
 
def unified_diff_files(a, b, sequencematcher=None):
326
 
    """Generate the diff for two files.
327
 
    """
328
 
    # Should this actually be an error?
329
 
    if a == b:
330
 
        return []
331
 
    if a == '-':
332
 
        file_a = sys.stdin
333
 
        time_a = time.time()
334
 
    else:
335
 
        file_a = open(a, 'rb')
336
 
        time_a = os.stat(a).st_mtime
337
 
 
338
 
    if b == '-':
339
 
        file_b = sys.stdin
340
 
        time_b = time.time()
341
 
    else:
342
 
        file_b = open(b, 'rb')
343
 
        time_b = os.stat(b).st_mtime
344
 
 
345
 
    # TODO: Include fromfiledate and tofiledate
346
 
    return unified_diff(file_a.readlines(), file_b.readlines(),
347
 
                        fromfile=a, tofile=b,
348
 
                        sequencematcher=sequencematcher)
349
 
 
350
 
 
351
 
def main(args):
352
 
    import optparse
353
 
    p = optparse.OptionParser(usage='%prog [options] file_a file_b'
354
 
                                    '\nFiles can be "-" to read from stdin')
355
 
    p.add_option('--patience', dest='matcher', action='store_const', const='patience',
356
 
                 default='patience', help='Use the patience difference algorithm')
357
 
    p.add_option('--difflib', dest='matcher', action='store_const', const='difflib',
358
 
                 default='patience', help='Use python\'s difflib algorithm')
359
 
 
360
 
    algorithms = {'patience':PatienceSequenceMatcher, 'difflib':difflib.SequenceMatcher}
361
 
 
362
 
    (opts, args) = p.parse_args(args)
363
 
    matcher = algorithms[opts.matcher]
364
 
 
365
 
    if len(args) != 2:
366
 
        print 'You must supply 2 filenames to diff'
367
 
        return -1
368
 
 
369
 
    for line in unified_diff_files(args[0], args[1], sequencematcher=matcher):
370
 
        sys.stdout.write(line)
371
 
    
372
 
if __name__ == '__main__':
373
 
    sys.exit(main(sys.argv[1:]))