##// END OF EJS Templates
mdiff: add a "blocksinrange" function to filter diff blocks by line range...
Denis Laxalde -
r30717:3eeb8e13 default
parent child Browse files
Show More
@@ -0,0 +1,232 b''
1 from __future__ import absolute_import
2
3 import unittest
4 from mercurial import error, mdiff
5
6 # for readability, line numbers are 0-origin
7 text1 = '''
8 00 at OLD
9 01 at OLD
10 02 at OLD
11 02 at NEW, 03 at OLD
12 03 at NEW, 04 at OLD
13 04 at NEW, 05 at OLD
14 05 at NEW, 06 at OLD
15 07 at OLD
16 08 at OLD
17 09 at OLD
18 10 at OLD
19 11 at OLD
20 '''[1:] # strip initial LF
21
22 text2 = '''
23 00 at NEW
24 01 at NEW
25 02 at NEW, 03 at OLD
26 03 at NEW, 04 at OLD
27 04 at NEW, 05 at OLD
28 05 at NEW, 06 at OLD
29 06 at NEW
30 07 at NEW
31 08 at NEW
32 09 at NEW
33 10 at NEW
34 11 at NEW
35 '''[1:] # strip initial LF
36
37 def filteredblocks(blocks, rangeb):
38 """return `rangea` extracted from `blocks` coming from
39 `mdiff.blocksinrange` along with the mask of blocks within rangeb.
40 """
41 filtered, rangea = mdiff.blocksinrange(blocks, rangeb)
42 skipped = [b not in filtered for b in blocks]
43 return rangea, skipped
44
45 class blocksinrangetests(unittest.TestCase):
46
47 def setUp(self):
48 self.blocks = list(mdiff.allblocks(text1, text2))
49 assert self.blocks == [
50 ([0, 3, 0, 2], '!'),
51 ((3, 7, 2, 6), '='),
52 ([7, 12, 6, 12], '!'),
53 ((12, 12, 12, 12), '='),
54 ], self.blocks
55
56 def testWithinEqual(self):
57 """linerange within an "=" block"""
58 # IDX 0 1
59 # 012345678901
60 # SRC NNOOOONNNNNN (New/Old)
61 # ^^
62 linerange2 = (3, 5)
63 linerange1, skipped = filteredblocks(self.blocks, linerange2)
64 self.assertEqual(linerange1, (4, 6))
65 self.assertEqual(skipped, [True, False, True, True])
66
67 def testWithinEqualStrictly(self):
68 """linerange matching exactly an "=" block"""
69 # IDX 0 1
70 # 012345678901
71 # SRC NNOOOONNNNNN (New/Old)
72 # ^^^^
73 linerange2 = (2, 6)
74 linerange1, skipped = filteredblocks(self.blocks, linerange2)
75 self.assertEqual(linerange1, (3, 7))
76 self.assertEqual(skipped, [True, False, True, True])
77
78 def testWithinEqualLowerbound(self):
79 """linerange at beginning of an "=" block"""
80 # IDX 0 1
81 # 012345678901
82 # SRC NNOOOONNNNNN (New/Old)
83 # ^^
84 linerange2 = (2, 4)
85 linerange1, skipped = filteredblocks(self.blocks, linerange2)
86 self.assertEqual(linerange1, (3, 5))
87 self.assertEqual(skipped, [True, False, True, True])
88
89 def testWithinEqualLowerboundOneline(self):
90 """oneline-linerange at beginning of an "=" block"""
91 # IDX 0 1
92 # 012345678901
93 # SRC NNOOOONNNNNN (New/Old)
94 # ^
95 linerange2 = (2, 3)
96 linerange1, skipped = filteredblocks(self.blocks, linerange2)
97 self.assertEqual(linerange1, (3, 4))
98 self.assertEqual(skipped, [True, False, True, True])
99
100 def testWithinEqualUpperbound(self):
101 """linerange at end of an "=" block"""
102 # IDX 0 1
103 # 012345678901
104 # SRC NNOOOONNNNNN (New/Old)
105 # ^^^
106 linerange2 = (3, 6)
107 linerange1, skipped = filteredblocks(self.blocks, linerange2)
108 self.assertEqual(linerange1, (4, 7))
109 self.assertEqual(skipped, [True, False, True, True])
110
111 def testWithinEqualUpperboundOneLine(self):
112 """oneline-linerange at end of an "=" block"""
113 # IDX 0 1
114 # 012345678901
115 # SRC NNOOOONNNNNN (New/Old)
116 # ^
117 linerange2 = (5, 6)
118 linerange1, skipped = filteredblocks(self.blocks, linerange2)
119 self.assertEqual(linerange1, (6, 7))
120 self.assertEqual(skipped, [True, False, True, True])
121
122 def testWithinFirstBlockNeq(self):
123 """linerange within the first "!" block"""
124 # IDX 0 1
125 # 012345678901
126 # SRC NNOOOONNNNNN (New/Old)
127 # ^
128 # | (empty)
129 # ^
130 # ^^
131 for linerange2 in [
132 (0, 1),
133 (1, 1),
134 (1, 2),
135 (0, 2),
136 ]:
137 linerange1, skipped = filteredblocks(self.blocks, linerange2)
138 self.assertEqual(linerange1, (0, 3))
139 self.assertEqual(skipped, [False, True, True, True])
140
141 def testWithinLastBlockNeq(self):
142 """linerange within the last "!" block"""
143 # IDX 0 1
144 # 012345678901
145 # SRC NNOOOONNNNNN (New/Old)
146 # ^
147 # ^
148 # | (empty)
149 # ^^^^^^
150 # ^
151 for linerange2 in [
152 (6, 7),
153 (7, 8),
154 (7, 7),
155 (6, 12),
156 (11, 12),
157 ]:
158 linerange1, skipped = filteredblocks(self.blocks, linerange2)
159 self.assertEqual(linerange1, (7, 12))
160 self.assertEqual(skipped, [True, True, False, True])
161
162 def testAccrossTwoBlocks(self):
163 """linerange accross two blocks"""
164 # IDX 0 1
165 # 012345678901
166 # SRC NNOOOONNNNNN (New/Old)
167 # ^^^^
168 linerange2 = (1, 5)
169 linerange1, skipped = filteredblocks(self.blocks, linerange2)
170 self.assertEqual(linerange1, (0, 6))
171 self.assertEqual(skipped, [False, False, True, True])
172
173 def testCrossingSeveralBlocks(self):
174 """linerange accross three blocks"""
175 # IDX 0 1
176 # 012345678901
177 # SRC NNOOOONNNNNN (New/Old)
178 # ^^^^^^^
179 linerange2 = (1, 8)
180 linerange1, skipped = filteredblocks(self.blocks, linerange2)
181 self.assertEqual(linerange1, (0, 12))
182 self.assertEqual(skipped, [False, False, False, True])
183
184 def testStartInEqBlock(self):
185 """linerange starting in an "=" block"""
186 # IDX 0 1
187 # 012345678901
188 # SRC NNOOOONNNNNN (New/Old)
189 # ^^^^
190 # ^^^^^^^
191 for linerange2, expectedlinerange1 in [
192 ((5, 9), (6, 12)),
193 ((4, 11), (5, 12)),
194 ]:
195 linerange1, skipped = filteredblocks(self.blocks, linerange2)
196 self.assertEqual(linerange1, expectedlinerange1)
197 self.assertEqual(skipped, [True, False, False, True])
198
199 def testEndInEqBlock(self):
200 """linerange ending in an "=" block"""
201 # IDX 0 1
202 # 012345678901
203 # SRC NNOOOONNNNNN (New/Old)
204 # ^^
205 # ^^^^^
206 for linerange2, expectedlinerange1 in [
207 ((1, 3), (0, 4)),
208 ((0, 4), (0, 5)),
209 ]:
210 linerange1, skipped = filteredblocks(self.blocks, linerange2)
211 self.assertEqual(linerange1, expectedlinerange1)
212 self.assertEqual(skipped, [False, False, True, True])
213
214 def testOutOfRange(self):
215 """linerange exceeding file size"""
216 exctype = error.Abort
217 for linerange2 in [
218 (0, 34),
219 (15, 12),
220 ]:
221 # Could be `with self.assertRaises(error.Abort)` but python2.6
222 # does not have assertRaises context manager.
223 try:
224 mdiff.blocksinrange(self.blocks, linerange2)
225 except exctype as exc:
226 self.assertTrue('line range exceeds file size' in str(exc))
227 else:
228 self.fail('%s not raised' % exctype.__name__)
229
230 if __name__ == '__main__':
231 import silenttestrunner
232 silenttestrunner.main(__name__)
@@ -1,383 +1,422 b''
1 # mdiff.py - diff and patch routines for mercurial
1 # mdiff.py - diff and patch routines for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11 import struct
11 import struct
12 import zlib
12 import zlib
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 base85,
16 base85,
17 bdiff,
17 bdiff,
18 error,
18 error,
19 mpatch,
19 mpatch,
20 util,
20 util,
21 )
21 )
22
22
23 def splitnewlines(text):
23 def splitnewlines(text):
24 '''like str.splitlines, but only split on newlines.'''
24 '''like str.splitlines, but only split on newlines.'''
25 lines = [l + '\n' for l in text.split('\n')]
25 lines = [l + '\n' for l in text.split('\n')]
26 if lines:
26 if lines:
27 if lines[-1] == '\n':
27 if lines[-1] == '\n':
28 lines.pop()
28 lines.pop()
29 else:
29 else:
30 lines[-1] = lines[-1][:-1]
30 lines[-1] = lines[-1][:-1]
31 return lines
31 return lines
32
32
33 class diffopts(object):
33 class diffopts(object):
34 '''context is the number of context lines
34 '''context is the number of context lines
35 text treats all files as text
35 text treats all files as text
36 showfunc enables diff -p output
36 showfunc enables diff -p output
37 git enables the git extended patch format
37 git enables the git extended patch format
38 nodates removes dates from diff headers
38 nodates removes dates from diff headers
39 nobinary ignores binary files
39 nobinary ignores binary files
40 noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode)
40 noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode)
41 ignorews ignores all whitespace changes in the diff
41 ignorews ignores all whitespace changes in the diff
42 ignorewsamount ignores changes in the amount of whitespace
42 ignorewsamount ignores changes in the amount of whitespace
43 ignoreblanklines ignores changes whose lines are all blank
43 ignoreblanklines ignores changes whose lines are all blank
44 upgrade generates git diffs to avoid data loss
44 upgrade generates git diffs to avoid data loss
45 '''
45 '''
46
46
47 defaults = {
47 defaults = {
48 'context': 3,
48 'context': 3,
49 'text': False,
49 'text': False,
50 'showfunc': False,
50 'showfunc': False,
51 'git': False,
51 'git': False,
52 'nodates': False,
52 'nodates': False,
53 'nobinary': False,
53 'nobinary': False,
54 'noprefix': False,
54 'noprefix': False,
55 'ignorews': False,
55 'ignorews': False,
56 'ignorewsamount': False,
56 'ignorewsamount': False,
57 'ignoreblanklines': False,
57 'ignoreblanklines': False,
58 'upgrade': False,
58 'upgrade': False,
59 }
59 }
60
60
61 def __init__(self, **opts):
61 def __init__(self, **opts):
62 for k in self.defaults.keys():
62 for k in self.defaults.keys():
63 v = opts.get(k)
63 v = opts.get(k)
64 if v is None:
64 if v is None:
65 v = self.defaults[k]
65 v = self.defaults[k]
66 setattr(self, k, v)
66 setattr(self, k, v)
67
67
68 try:
68 try:
69 self.context = int(self.context)
69 self.context = int(self.context)
70 except ValueError:
70 except ValueError:
71 raise error.Abort(_('diff context lines count must be '
71 raise error.Abort(_('diff context lines count must be '
72 'an integer, not %r') % self.context)
72 'an integer, not %r') % self.context)
73
73
74 def copy(self, **kwargs):
74 def copy(self, **kwargs):
75 opts = dict((k, getattr(self, k)) for k in self.defaults)
75 opts = dict((k, getattr(self, k)) for k in self.defaults)
76 opts.update(kwargs)
76 opts.update(kwargs)
77 return diffopts(**opts)
77 return diffopts(**opts)
78
78
79 defaultopts = diffopts()
79 defaultopts = diffopts()
80
80
81 def wsclean(opts, text, blank=True):
81 def wsclean(opts, text, blank=True):
82 if opts.ignorews:
82 if opts.ignorews:
83 text = bdiff.fixws(text, 1)
83 text = bdiff.fixws(text, 1)
84 elif opts.ignorewsamount:
84 elif opts.ignorewsamount:
85 text = bdiff.fixws(text, 0)
85 text = bdiff.fixws(text, 0)
86 if blank and opts.ignoreblanklines:
86 if blank and opts.ignoreblanklines:
87 text = re.sub('\n+', '\n', text).strip('\n')
87 text = re.sub('\n+', '\n', text).strip('\n')
88 return text
88 return text
89
89
90 def splitblock(base1, lines1, base2, lines2, opts):
90 def splitblock(base1, lines1, base2, lines2, opts):
91 # The input lines matches except for interwoven blank lines. We
91 # The input lines matches except for interwoven blank lines. We
92 # transform it into a sequence of matching blocks and blank blocks.
92 # transform it into a sequence of matching blocks and blank blocks.
93 lines1 = [(wsclean(opts, l) and 1 or 0) for l in lines1]
93 lines1 = [(wsclean(opts, l) and 1 or 0) for l in lines1]
94 lines2 = [(wsclean(opts, l) and 1 or 0) for l in lines2]
94 lines2 = [(wsclean(opts, l) and 1 or 0) for l in lines2]
95 s1, e1 = 0, len(lines1)
95 s1, e1 = 0, len(lines1)
96 s2, e2 = 0, len(lines2)
96 s2, e2 = 0, len(lines2)
97 while s1 < e1 or s2 < e2:
97 while s1 < e1 or s2 < e2:
98 i1, i2, btype = s1, s2, '='
98 i1, i2, btype = s1, s2, '='
99 if (i1 >= e1 or lines1[i1] == 0
99 if (i1 >= e1 or lines1[i1] == 0
100 or i2 >= e2 or lines2[i2] == 0):
100 or i2 >= e2 or lines2[i2] == 0):
101 # Consume the block of blank lines
101 # Consume the block of blank lines
102 btype = '~'
102 btype = '~'
103 while i1 < e1 and lines1[i1] == 0:
103 while i1 < e1 and lines1[i1] == 0:
104 i1 += 1
104 i1 += 1
105 while i2 < e2 and lines2[i2] == 0:
105 while i2 < e2 and lines2[i2] == 0:
106 i2 += 1
106 i2 += 1
107 else:
107 else:
108 # Consume the matching lines
108 # Consume the matching lines
109 while i1 < e1 and lines1[i1] == 1 and lines2[i2] == 1:
109 while i1 < e1 and lines1[i1] == 1 and lines2[i2] == 1:
110 i1 += 1
110 i1 += 1
111 i2 += 1
111 i2 += 1
112 yield [base1 + s1, base1 + i1, base2 + s2, base2 + i2], btype
112 yield [base1 + s1, base1 + i1, base2 + s2, base2 + i2], btype
113 s1 = i1
113 s1 = i1
114 s2 = i2
114 s2 = i2
115
115
116 def blocksinrange(blocks, rangeb):
117 """filter `blocks` like (a1, a2, b1, b2) from items outside line range
118 `rangeb` from ``(b1, b2)`` point of view.
119
120 Return `filteredblocks, rangea` where:
121
122 * `filteredblocks` is list of ``block = (a1, a2, b1, b2), stype`` items of
123 `blocks` that are inside `rangeb` from ``(b1, b2)`` point of view; a
124 block ``(b1, b2)`` being inside `rangeb` if
125 ``rangeb[0] < b2 and b1 < rangeb[1]``;
126 * `rangea` is the line range w.r.t. to ``(a1, a2)`` parts of `blocks`.
127 """
128 lbb, ubb = rangeb
129 lba, uba = None, None
130 filteredblocks = []
131 for block in blocks:
132 (a1, a2, b1, b2), stype = block
133 if lbb >= b1 and ubb <= b2 and stype == '=':
134 # rangeb is within a single "=" hunk, restrict back linerange1
135 # by offsetting rangeb
136 lba = lbb - b1 + a1
137 uba = ubb - b1 + a1
138 else:
139 if b1 <= lbb < b2:
140 if stype == '=':
141 lba = a2 - (b2 - lbb)
142 else:
143 lba = a1
144 if b1 < ubb <= b2:
145 if stype == '=':
146 uba = a1 + (ubb - b1)
147 else:
148 uba = a2
149 if lbb < b2 and b1 < ubb:
150 filteredblocks.append(block)
151 if lba is None or uba is None or uba < lba:
152 raise error.Abort(_('line range exceeds file size'))
153 return filteredblocks, (lba, uba)
154
116 def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
155 def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
117 """Return (block, type) tuples, where block is an mdiff.blocks
156 """Return (block, type) tuples, where block is an mdiff.blocks
118 line entry. type is '=' for blocks matching exactly one another
157 line entry. type is '=' for blocks matching exactly one another
119 (bdiff blocks), '!' for non-matching blocks and '~' for blocks
158 (bdiff blocks), '!' for non-matching blocks and '~' for blocks
120 matching only after having filtered blank lines.
159 matching only after having filtered blank lines.
121 line1 and line2 are text1 and text2 split with splitnewlines() if
160 line1 and line2 are text1 and text2 split with splitnewlines() if
122 they are already available.
161 they are already available.
123 """
162 """
124 if opts is None:
163 if opts is None:
125 opts = defaultopts
164 opts = defaultopts
126 if opts.ignorews or opts.ignorewsamount:
165 if opts.ignorews or opts.ignorewsamount:
127 text1 = wsclean(opts, text1, False)
166 text1 = wsclean(opts, text1, False)
128 text2 = wsclean(opts, text2, False)
167 text2 = wsclean(opts, text2, False)
129 diff = bdiff.blocks(text1, text2)
168 diff = bdiff.blocks(text1, text2)
130 for i, s1 in enumerate(diff):
169 for i, s1 in enumerate(diff):
131 # The first match is special.
170 # The first match is special.
132 # we've either found a match starting at line 0 or a match later
171 # we've either found a match starting at line 0 or a match later
133 # in the file. If it starts later, old and new below will both be
172 # in the file. If it starts later, old and new below will both be
134 # empty and we'll continue to the next match.
173 # empty and we'll continue to the next match.
135 if i > 0:
174 if i > 0:
136 s = diff[i - 1]
175 s = diff[i - 1]
137 else:
176 else:
138 s = [0, 0, 0, 0]
177 s = [0, 0, 0, 0]
139 s = [s[1], s1[0], s[3], s1[2]]
178 s = [s[1], s1[0], s[3], s1[2]]
140
179
141 # bdiff sometimes gives huge matches past eof, this check eats them,
180 # bdiff sometimes gives huge matches past eof, this check eats them,
142 # and deals with the special first match case described above
181 # and deals with the special first match case described above
143 if s[0] != s[1] or s[2] != s[3]:
182 if s[0] != s[1] or s[2] != s[3]:
144 type = '!'
183 type = '!'
145 if opts.ignoreblanklines:
184 if opts.ignoreblanklines:
146 if lines1 is None:
185 if lines1 is None:
147 lines1 = splitnewlines(text1)
186 lines1 = splitnewlines(text1)
148 if lines2 is None:
187 if lines2 is None:
149 lines2 = splitnewlines(text2)
188 lines2 = splitnewlines(text2)
150 old = wsclean(opts, "".join(lines1[s[0]:s[1]]))
189 old = wsclean(opts, "".join(lines1[s[0]:s[1]]))
151 new = wsclean(opts, "".join(lines2[s[2]:s[3]]))
190 new = wsclean(opts, "".join(lines2[s[2]:s[3]]))
152 if old == new:
191 if old == new:
153 type = '~'
192 type = '~'
154 yield s, type
193 yield s, type
155 yield s1, '='
194 yield s1, '='
156
195
157 def unidiff(a, ad, b, bd, fn1, fn2, opts=defaultopts):
196 def unidiff(a, ad, b, bd, fn1, fn2, opts=defaultopts):
158 def datetag(date, fn=None):
197 def datetag(date, fn=None):
159 if not opts.git and not opts.nodates:
198 if not opts.git and not opts.nodates:
160 return '\t%s\n' % date
199 return '\t%s\n' % date
161 if fn and ' ' in fn:
200 if fn and ' ' in fn:
162 return '\t\n'
201 return '\t\n'
163 return '\n'
202 return '\n'
164
203
165 if not a and not b:
204 if not a and not b:
166 return ""
205 return ""
167
206
168 if opts.noprefix:
207 if opts.noprefix:
169 aprefix = bprefix = ''
208 aprefix = bprefix = ''
170 else:
209 else:
171 aprefix = 'a/'
210 aprefix = 'a/'
172 bprefix = 'b/'
211 bprefix = 'b/'
173
212
174 epoch = util.datestr((0, 0))
213 epoch = util.datestr((0, 0))
175
214
176 fn1 = util.pconvert(fn1)
215 fn1 = util.pconvert(fn1)
177 fn2 = util.pconvert(fn2)
216 fn2 = util.pconvert(fn2)
178
217
179 if not opts.text and (util.binary(a) or util.binary(b)):
218 if not opts.text and (util.binary(a) or util.binary(b)):
180 if a and b and len(a) == len(b) and a == b:
219 if a and b and len(a) == len(b) and a == b:
181 return ""
220 return ""
182 l = ['Binary file %s has changed\n' % fn1]
221 l = ['Binary file %s has changed\n' % fn1]
183 elif not a:
222 elif not a:
184 b = splitnewlines(b)
223 b = splitnewlines(b)
185 if a is None:
224 if a is None:
186 l1 = '--- /dev/null%s' % datetag(epoch)
225 l1 = '--- /dev/null%s' % datetag(epoch)
187 else:
226 else:
188 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
227 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
189 l2 = "+++ %s%s" % (bprefix + fn2, datetag(bd, fn2))
228 l2 = "+++ %s%s" % (bprefix + fn2, datetag(bd, fn2))
190 l3 = "@@ -0,0 +1,%d @@\n" % len(b)
229 l3 = "@@ -0,0 +1,%d @@\n" % len(b)
191 l = [l1, l2, l3] + ["+" + e for e in b]
230 l = [l1, l2, l3] + ["+" + e for e in b]
192 elif not b:
231 elif not b:
193 a = splitnewlines(a)
232 a = splitnewlines(a)
194 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
233 l1 = "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
195 if b is None:
234 if b is None:
196 l2 = '+++ /dev/null%s' % datetag(epoch)
235 l2 = '+++ /dev/null%s' % datetag(epoch)
197 else:
236 else:
198 l2 = "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2))
237 l2 = "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2))
199 l3 = "@@ -1,%d +0,0 @@\n" % len(a)
238 l3 = "@@ -1,%d +0,0 @@\n" % len(a)
200 l = [l1, l2, l3] + ["-" + e for e in a]
239 l = [l1, l2, l3] + ["-" + e for e in a]
201 else:
240 else:
202 al = splitnewlines(a)
241 al = splitnewlines(a)
203 bl = splitnewlines(b)
242 bl = splitnewlines(b)
204 l = list(_unidiff(a, b, al, bl, opts=opts))
243 l = list(_unidiff(a, b, al, bl, opts=opts))
205 if not l:
244 if not l:
206 return ""
245 return ""
207
246
208 l.insert(0, "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1)))
247 l.insert(0, "--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1)))
209 l.insert(1, "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2)))
248 l.insert(1, "+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2)))
210
249
211 for ln in xrange(len(l)):
250 for ln in xrange(len(l)):
212 if l[ln][-1] != '\n':
251 if l[ln][-1] != '\n':
213 l[ln] += "\n\ No newline at end of file\n"
252 l[ln] += "\n\ No newline at end of file\n"
214
253
215 return "".join(l)
254 return "".join(l)
216
255
217 # creates a headerless unified diff
256 # creates a headerless unified diff
218 # t1 and t2 are the text to be diffed
257 # t1 and t2 are the text to be diffed
219 # l1 and l2 are the text broken up into lines
258 # l1 and l2 are the text broken up into lines
220 def _unidiff(t1, t2, l1, l2, opts=defaultopts):
259 def _unidiff(t1, t2, l1, l2, opts=defaultopts):
221 def contextend(l, len):
260 def contextend(l, len):
222 ret = l + opts.context
261 ret = l + opts.context
223 if ret > len:
262 if ret > len:
224 ret = len
263 ret = len
225 return ret
264 return ret
226
265
227 def contextstart(l):
266 def contextstart(l):
228 ret = l - opts.context
267 ret = l - opts.context
229 if ret < 0:
268 if ret < 0:
230 return 0
269 return 0
231 return ret
270 return ret
232
271
233 lastfunc = [0, '']
272 lastfunc = [0, '']
234 def yieldhunk(hunk):
273 def yieldhunk(hunk):
235 (astart, a2, bstart, b2, delta) = hunk
274 (astart, a2, bstart, b2, delta) = hunk
236 aend = contextend(a2, len(l1))
275 aend = contextend(a2, len(l1))
237 alen = aend - astart
276 alen = aend - astart
238 blen = b2 - bstart + aend - a2
277 blen = b2 - bstart + aend - a2
239
278
240 func = ""
279 func = ""
241 if opts.showfunc:
280 if opts.showfunc:
242 lastpos, func = lastfunc
281 lastpos, func = lastfunc
243 # walk backwards from the start of the context up to the start of
282 # walk backwards from the start of the context up to the start of
244 # the previous hunk context until we find a line starting with an
283 # the previous hunk context until we find a line starting with an
245 # alphanumeric char.
284 # alphanumeric char.
246 for i in xrange(astart - 1, lastpos - 1, -1):
285 for i in xrange(astart - 1, lastpos - 1, -1):
247 if l1[i][0].isalnum():
286 if l1[i][0].isalnum():
248 func = ' ' + l1[i].rstrip()[:40]
287 func = ' ' + l1[i].rstrip()[:40]
249 lastfunc[1] = func
288 lastfunc[1] = func
250 break
289 break
251 # by recording this hunk's starting point as the next place to
290 # by recording this hunk's starting point as the next place to
252 # start looking for function lines, we avoid reading any line in
291 # start looking for function lines, we avoid reading any line in
253 # the file more than once.
292 # the file more than once.
254 lastfunc[0] = astart
293 lastfunc[0] = astart
255
294
256 # zero-length hunk ranges report their start line as one less
295 # zero-length hunk ranges report their start line as one less
257 if alen:
296 if alen:
258 astart += 1
297 astart += 1
259 if blen:
298 if blen:
260 bstart += 1
299 bstart += 1
261
300
262 yield "@@ -%d,%d +%d,%d @@%s\n" % (astart, alen,
301 yield "@@ -%d,%d +%d,%d @@%s\n" % (astart, alen,
263 bstart, blen, func)
302 bstart, blen, func)
264 for x in delta:
303 for x in delta:
265 yield x
304 yield x
266 for x in xrange(a2, aend):
305 for x in xrange(a2, aend):
267 yield ' ' + l1[x]
306 yield ' ' + l1[x]
268
307
269 # bdiff.blocks gives us the matching sequences in the files. The loop
308 # bdiff.blocks gives us the matching sequences in the files. The loop
270 # below finds the spaces between those matching sequences and translates
309 # below finds the spaces between those matching sequences and translates
271 # them into diff output.
310 # them into diff output.
272 #
311 #
273 hunk = None
312 hunk = None
274 ignoredlines = 0
313 ignoredlines = 0
275 for s, stype in allblocks(t1, t2, opts, l1, l2):
314 for s, stype in allblocks(t1, t2, opts, l1, l2):
276 a1, a2, b1, b2 = s
315 a1, a2, b1, b2 = s
277 if stype != '!':
316 if stype != '!':
278 if stype == '~':
317 if stype == '~':
279 # The diff context lines are based on t1 content. When
318 # The diff context lines are based on t1 content. When
280 # blank lines are ignored, the new lines offsets must
319 # blank lines are ignored, the new lines offsets must
281 # be adjusted as if equivalent blocks ('~') had the
320 # be adjusted as if equivalent blocks ('~') had the
282 # same sizes on both sides.
321 # same sizes on both sides.
283 ignoredlines += (b2 - b1) - (a2 - a1)
322 ignoredlines += (b2 - b1) - (a2 - a1)
284 continue
323 continue
285 delta = []
324 delta = []
286 old = l1[a1:a2]
325 old = l1[a1:a2]
287 new = l2[b1:b2]
326 new = l2[b1:b2]
288
327
289 b1 -= ignoredlines
328 b1 -= ignoredlines
290 b2 -= ignoredlines
329 b2 -= ignoredlines
291 astart = contextstart(a1)
330 astart = contextstart(a1)
292 bstart = contextstart(b1)
331 bstart = contextstart(b1)
293 prev = None
332 prev = None
294 if hunk:
333 if hunk:
295 # join with the previous hunk if it falls inside the context
334 # join with the previous hunk if it falls inside the context
296 if astart < hunk[1] + opts.context + 1:
335 if astart < hunk[1] + opts.context + 1:
297 prev = hunk
336 prev = hunk
298 astart = hunk[1]
337 astart = hunk[1]
299 bstart = hunk[3]
338 bstart = hunk[3]
300 else:
339 else:
301 for x in yieldhunk(hunk):
340 for x in yieldhunk(hunk):
302 yield x
341 yield x
303 if prev:
342 if prev:
304 # we've joined the previous hunk, record the new ending points.
343 # we've joined the previous hunk, record the new ending points.
305 hunk[1] = a2
344 hunk[1] = a2
306 hunk[3] = b2
345 hunk[3] = b2
307 delta = hunk[4]
346 delta = hunk[4]
308 else:
347 else:
309 # create a new hunk
348 # create a new hunk
310 hunk = [astart, a2, bstart, b2, delta]
349 hunk = [astart, a2, bstart, b2, delta]
311
350
312 delta[len(delta):] = [' ' + x for x in l1[astart:a1]]
351 delta[len(delta):] = [' ' + x for x in l1[astart:a1]]
313 delta[len(delta):] = ['-' + x for x in old]
352 delta[len(delta):] = ['-' + x for x in old]
314 delta[len(delta):] = ['+' + x for x in new]
353 delta[len(delta):] = ['+' + x for x in new]
315
354
316 if hunk:
355 if hunk:
317 for x in yieldhunk(hunk):
356 for x in yieldhunk(hunk):
318 yield x
357 yield x
319
358
320 def b85diff(to, tn):
359 def b85diff(to, tn):
321 '''print base85-encoded binary diff'''
360 '''print base85-encoded binary diff'''
322 def fmtline(line):
361 def fmtline(line):
323 l = len(line)
362 l = len(line)
324 if l <= 26:
363 if l <= 26:
325 l = chr(ord('A') + l - 1)
364 l = chr(ord('A') + l - 1)
326 else:
365 else:
327 l = chr(l - 26 + ord('a') - 1)
366 l = chr(l - 26 + ord('a') - 1)
328 return '%c%s\n' % (l, base85.b85encode(line, True))
367 return '%c%s\n' % (l, base85.b85encode(line, True))
329
368
330 def chunk(text, csize=52):
369 def chunk(text, csize=52):
331 l = len(text)
370 l = len(text)
332 i = 0
371 i = 0
333 while i < l:
372 while i < l:
334 yield text[i:i + csize]
373 yield text[i:i + csize]
335 i += csize
374 i += csize
336
375
337 if to is None:
376 if to is None:
338 to = ''
377 to = ''
339 if tn is None:
378 if tn is None:
340 tn = ''
379 tn = ''
341
380
342 if to == tn:
381 if to == tn:
343 return ''
382 return ''
344
383
345 # TODO: deltas
384 # TODO: deltas
346 ret = []
385 ret = []
347 ret.append('GIT binary patch\n')
386 ret.append('GIT binary patch\n')
348 ret.append('literal %s\n' % len(tn))
387 ret.append('literal %s\n' % len(tn))
349 for l in chunk(zlib.compress(tn)):
388 for l in chunk(zlib.compress(tn)):
350 ret.append(fmtline(l))
389 ret.append(fmtline(l))
351 ret.append('\n')
390 ret.append('\n')
352
391
353 return ''.join(ret)
392 return ''.join(ret)
354
393
355 def patchtext(bin):
394 def patchtext(bin):
356 pos = 0
395 pos = 0
357 t = []
396 t = []
358 while pos < len(bin):
397 while pos < len(bin):
359 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
398 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
360 pos += 12
399 pos += 12
361 t.append(bin[pos:pos + l])
400 t.append(bin[pos:pos + l])
362 pos += l
401 pos += l
363 return "".join(t)
402 return "".join(t)
364
403
365 def patch(a, bin):
404 def patch(a, bin):
366 if len(a) == 0:
405 if len(a) == 0:
367 # skip over trivial delta header
406 # skip over trivial delta header
368 return util.buffer(bin, 12)
407 return util.buffer(bin, 12)
369 return mpatch.patches(a, [bin])
408 return mpatch.patches(a, [bin])
370
409
371 # similar to difflib.SequenceMatcher.get_matching_blocks
410 # similar to difflib.SequenceMatcher.get_matching_blocks
372 def get_matching_blocks(a, b):
411 def get_matching_blocks(a, b):
373 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
412 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
374
413
375 def trivialdiffheader(length):
414 def trivialdiffheader(length):
376 return struct.pack(">lll", 0, 0, length) if length else ''
415 return struct.pack(">lll", 0, 0, length) if length else ''
377
416
378 def replacediffheader(oldlen, newlen):
417 def replacediffheader(oldlen, newlen):
379 return struct.pack(">lll", 0, oldlen, newlen)
418 return struct.pack(">lll", 0, oldlen, newlen)
380
419
381 patches = mpatch.patches
420 patches = mpatch.patches
382 patchedsize = mpatch.patchedsize
421 patchedsize = mpatch.patchedsize
383 textdiff = bdiff.bdiff
422 textdiff = bdiff.bdiff
General Comments 0
You need to be logged in to leave comments. Login now