##// END OF EJS Templates
mdiff: mark diffopts as having dynamic attributes...
Augie Fackler -
r43784:f2f460cd default
parent child Browse files
Show More
@@ -1,554 +1,557 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 .pycompat import (
15 from .pycompat import (
16 getattr,
16 getattr,
17 setattr,
17 setattr,
18 )
18 )
19 from . import (
19 from . import (
20 encoding,
20 encoding,
21 error,
21 error,
22 policy,
22 policy,
23 pycompat,
23 pycompat,
24 util,
24 util,
25 )
25 )
26 from .utils import dateutil
26 from .utils import dateutil
27
27
28 _missing_newline_marker = b"\\ No newline at end of file\n"
28 _missing_newline_marker = b"\\ No newline at end of file\n"
29
29
30 bdiff = policy.importmod(r'bdiff')
30 bdiff = policy.importmod(r'bdiff')
31 mpatch = policy.importmod(r'mpatch')
31 mpatch = policy.importmod(r'mpatch')
32
32
33 blocks = bdiff.blocks
33 blocks = bdiff.blocks
34 fixws = bdiff.fixws
34 fixws = bdiff.fixws
35 patches = mpatch.patches
35 patches = mpatch.patches
36 patchedsize = mpatch.patchedsize
36 patchedsize = mpatch.patchedsize
37 textdiff = bdiff.bdiff
37 textdiff = bdiff.bdiff
38 splitnewlines = bdiff.splitnewlines
38 splitnewlines = bdiff.splitnewlines
39
39
40
40
41 # TODO: this looks like it could be an attrs, which might help pytype
41 class diffopts(object):
42 class diffopts(object):
42 '''context is the number of context lines
43 '''context is the number of context lines
43 text treats all files as text
44 text treats all files as text
44 showfunc enables diff -p output
45 showfunc enables diff -p output
45 git enables the git extended patch format
46 git enables the git extended patch format
46 nodates removes dates from diff headers
47 nodates removes dates from diff headers
47 nobinary ignores binary files
48 nobinary ignores binary files
48 noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode)
49 noprefix disables the 'a/' and 'b/' prefixes (ignored in plain mode)
49 ignorews ignores all whitespace changes in the diff
50 ignorews ignores all whitespace changes in the diff
50 ignorewsamount ignores changes in the amount of whitespace
51 ignorewsamount ignores changes in the amount of whitespace
51 ignoreblanklines ignores changes whose lines are all blank
52 ignoreblanklines ignores changes whose lines are all blank
52 upgrade generates git diffs to avoid data loss
53 upgrade generates git diffs to avoid data loss
53 '''
54 '''
54
55
56 _HAS_DYNAMIC_ATTRIBUTES = True
57
55 defaults = {
58 defaults = {
56 b'context': 3,
59 b'context': 3,
57 b'text': False,
60 b'text': False,
58 b'showfunc': False,
61 b'showfunc': False,
59 b'git': False,
62 b'git': False,
60 b'nodates': False,
63 b'nodates': False,
61 b'nobinary': False,
64 b'nobinary': False,
62 b'noprefix': False,
65 b'noprefix': False,
63 b'index': 0,
66 b'index': 0,
64 b'ignorews': False,
67 b'ignorews': False,
65 b'ignorewsamount': False,
68 b'ignorewsamount': False,
66 b'ignorewseol': False,
69 b'ignorewseol': False,
67 b'ignoreblanklines': False,
70 b'ignoreblanklines': False,
68 b'upgrade': False,
71 b'upgrade': False,
69 b'showsimilarity': False,
72 b'showsimilarity': False,
70 b'worddiff': False,
73 b'worddiff': False,
71 b'xdiff': False,
74 b'xdiff': False,
72 }
75 }
73
76
74 def __init__(self, **opts):
77 def __init__(self, **opts):
75 opts = pycompat.byteskwargs(opts)
78 opts = pycompat.byteskwargs(opts)
76 for k in self.defaults.keys():
79 for k in self.defaults.keys():
77 v = opts.get(k)
80 v = opts.get(k)
78 if v is None:
81 if v is None:
79 v = self.defaults[k]
82 v = self.defaults[k]
80 setattr(self, k, v)
83 setattr(self, k, v)
81
84
82 try:
85 try:
83 self.context = int(self.context)
86 self.context = int(self.context)
84 except ValueError:
87 except ValueError:
85 raise error.Abort(
88 raise error.Abort(
86 _(b'diff context lines count must be an integer, not %r')
89 _(b'diff context lines count must be an integer, not %r')
87 % pycompat.bytestr(self.context)
90 % pycompat.bytestr(self.context)
88 )
91 )
89
92
90 def copy(self, **kwargs):
93 def copy(self, **kwargs):
91 opts = dict((k, getattr(self, k)) for k in self.defaults)
94 opts = dict((k, getattr(self, k)) for k in self.defaults)
92 opts = pycompat.strkwargs(opts)
95 opts = pycompat.strkwargs(opts)
93 opts.update(kwargs)
96 opts.update(kwargs)
94 return diffopts(**opts)
97 return diffopts(**opts)
95
98
96
99
97 defaultopts = diffopts()
100 defaultopts = diffopts()
98
101
99
102
100 def wsclean(opts, text, blank=True):
103 def wsclean(opts, text, blank=True):
101 if opts.ignorews:
104 if opts.ignorews:
102 text = bdiff.fixws(text, 1)
105 text = bdiff.fixws(text, 1)
103 elif opts.ignorewsamount:
106 elif opts.ignorewsamount:
104 text = bdiff.fixws(text, 0)
107 text = bdiff.fixws(text, 0)
105 if blank and opts.ignoreblanklines:
108 if blank and opts.ignoreblanklines:
106 text = re.sub(b'\n+', b'\n', text).strip(b'\n')
109 text = re.sub(b'\n+', b'\n', text).strip(b'\n')
107 if opts.ignorewseol:
110 if opts.ignorewseol:
108 text = re.sub(br'[ \t\r\f]+\n', br'\n', text)
111 text = re.sub(br'[ \t\r\f]+\n', br'\n', text)
109 return text
112 return text
110
113
111
114
112 def splitblock(base1, lines1, base2, lines2, opts):
115 def splitblock(base1, lines1, base2, lines2, opts):
113 # The input lines matches except for interwoven blank lines. We
116 # The input lines matches except for interwoven blank lines. We
114 # transform it into a sequence of matching blocks and blank blocks.
117 # transform it into a sequence of matching blocks and blank blocks.
115 lines1 = [(wsclean(opts, l) and 1 or 0) for l in lines1]
118 lines1 = [(wsclean(opts, l) and 1 or 0) for l in lines1]
116 lines2 = [(wsclean(opts, l) and 1 or 0) for l in lines2]
119 lines2 = [(wsclean(opts, l) and 1 or 0) for l in lines2]
117 s1, e1 = 0, len(lines1)
120 s1, e1 = 0, len(lines1)
118 s2, e2 = 0, len(lines2)
121 s2, e2 = 0, len(lines2)
119 while s1 < e1 or s2 < e2:
122 while s1 < e1 or s2 < e2:
120 i1, i2, btype = s1, s2, b'='
123 i1, i2, btype = s1, s2, b'='
121 if i1 >= e1 or lines1[i1] == 0 or i2 >= e2 or lines2[i2] == 0:
124 if i1 >= e1 or lines1[i1] == 0 or i2 >= e2 or lines2[i2] == 0:
122 # Consume the block of blank lines
125 # Consume the block of blank lines
123 btype = b'~'
126 btype = b'~'
124 while i1 < e1 and lines1[i1] == 0:
127 while i1 < e1 and lines1[i1] == 0:
125 i1 += 1
128 i1 += 1
126 while i2 < e2 and lines2[i2] == 0:
129 while i2 < e2 and lines2[i2] == 0:
127 i2 += 1
130 i2 += 1
128 else:
131 else:
129 # Consume the matching lines
132 # Consume the matching lines
130 while i1 < e1 and lines1[i1] == 1 and lines2[i2] == 1:
133 while i1 < e1 and lines1[i1] == 1 and lines2[i2] == 1:
131 i1 += 1
134 i1 += 1
132 i2 += 1
135 i2 += 1
133 yield [base1 + s1, base1 + i1, base2 + s2, base2 + i2], btype
136 yield [base1 + s1, base1 + i1, base2 + s2, base2 + i2], btype
134 s1 = i1
137 s1 = i1
135 s2 = i2
138 s2 = i2
136
139
137
140
138 def hunkinrange(hunk, linerange):
141 def hunkinrange(hunk, linerange):
139 """Return True if `hunk` defined as (start, length) is in `linerange`
142 """Return True if `hunk` defined as (start, length) is in `linerange`
140 defined as (lowerbound, upperbound).
143 defined as (lowerbound, upperbound).
141
144
142 >>> hunkinrange((5, 10), (2, 7))
145 >>> hunkinrange((5, 10), (2, 7))
143 True
146 True
144 >>> hunkinrange((5, 10), (6, 12))
147 >>> hunkinrange((5, 10), (6, 12))
145 True
148 True
146 >>> hunkinrange((5, 10), (13, 17))
149 >>> hunkinrange((5, 10), (13, 17))
147 True
150 True
148 >>> hunkinrange((5, 10), (3, 17))
151 >>> hunkinrange((5, 10), (3, 17))
149 True
152 True
150 >>> hunkinrange((5, 10), (1, 3))
153 >>> hunkinrange((5, 10), (1, 3))
151 False
154 False
152 >>> hunkinrange((5, 10), (18, 20))
155 >>> hunkinrange((5, 10), (18, 20))
153 False
156 False
154 >>> hunkinrange((5, 10), (1, 5))
157 >>> hunkinrange((5, 10), (1, 5))
155 False
158 False
156 >>> hunkinrange((5, 10), (15, 27))
159 >>> hunkinrange((5, 10), (15, 27))
157 False
160 False
158 """
161 """
159 start, length = hunk
162 start, length = hunk
160 lowerbound, upperbound = linerange
163 lowerbound, upperbound = linerange
161 return lowerbound < start + length and start < upperbound
164 return lowerbound < start + length and start < upperbound
162
165
163
166
164 def blocksinrange(blocks, rangeb):
167 def blocksinrange(blocks, rangeb):
165 """filter `blocks` like (a1, a2, b1, b2) from items outside line range
168 """filter `blocks` like (a1, a2, b1, b2) from items outside line range
166 `rangeb` from ``(b1, b2)`` point of view.
169 `rangeb` from ``(b1, b2)`` point of view.
167
170
168 Return `filteredblocks, rangea` where:
171 Return `filteredblocks, rangea` where:
169
172
170 * `filteredblocks` is list of ``block = (a1, a2, b1, b2), stype`` items of
173 * `filteredblocks` is list of ``block = (a1, a2, b1, b2), stype`` items of
171 `blocks` that are inside `rangeb` from ``(b1, b2)`` point of view; a
174 `blocks` that are inside `rangeb` from ``(b1, b2)`` point of view; a
172 block ``(b1, b2)`` being inside `rangeb` if
175 block ``(b1, b2)`` being inside `rangeb` if
173 ``rangeb[0] < b2 and b1 < rangeb[1]``;
176 ``rangeb[0] < b2 and b1 < rangeb[1]``;
174 * `rangea` is the line range w.r.t. to ``(a1, a2)`` parts of `blocks`.
177 * `rangea` is the line range w.r.t. to ``(a1, a2)`` parts of `blocks`.
175 """
178 """
176 lbb, ubb = rangeb
179 lbb, ubb = rangeb
177 lba, uba = None, None
180 lba, uba = None, None
178 filteredblocks = []
181 filteredblocks = []
179 for block in blocks:
182 for block in blocks:
180 (a1, a2, b1, b2), stype = block
183 (a1, a2, b1, b2), stype = block
181 if lbb >= b1 and ubb <= b2 and stype == b'=':
184 if lbb >= b1 and ubb <= b2 and stype == b'=':
182 # rangeb is within a single "=" hunk, restrict back linerange1
185 # rangeb is within a single "=" hunk, restrict back linerange1
183 # by offsetting rangeb
186 # by offsetting rangeb
184 lba = lbb - b1 + a1
187 lba = lbb - b1 + a1
185 uba = ubb - b1 + a1
188 uba = ubb - b1 + a1
186 else:
189 else:
187 if b1 <= lbb < b2:
190 if b1 <= lbb < b2:
188 if stype == b'=':
191 if stype == b'=':
189 lba = a2 - (b2 - lbb)
192 lba = a2 - (b2 - lbb)
190 else:
193 else:
191 lba = a1
194 lba = a1
192 if b1 < ubb <= b2:
195 if b1 < ubb <= b2:
193 if stype == b'=':
196 if stype == b'=':
194 uba = a1 + (ubb - b1)
197 uba = a1 + (ubb - b1)
195 else:
198 else:
196 uba = a2
199 uba = a2
197 if hunkinrange((b1, (b2 - b1)), rangeb):
200 if hunkinrange((b1, (b2 - b1)), rangeb):
198 filteredblocks.append(block)
201 filteredblocks.append(block)
199 if lba is None or uba is None or uba < lba:
202 if lba is None or uba is None or uba < lba:
200 raise error.Abort(_(b'line range exceeds file size'))
203 raise error.Abort(_(b'line range exceeds file size'))
201 return filteredblocks, (lba, uba)
204 return filteredblocks, (lba, uba)
202
205
203
206
204 def chooseblocksfunc(opts=None):
207 def chooseblocksfunc(opts=None):
205 if (
208 if (
206 opts is None
209 opts is None
207 or not opts.xdiff
210 or not opts.xdiff
208 or not util.safehasattr(bdiff, b'xdiffblocks')
211 or not util.safehasattr(bdiff, b'xdiffblocks')
209 ):
212 ):
210 return bdiff.blocks
213 return bdiff.blocks
211 else:
214 else:
212 return bdiff.xdiffblocks
215 return bdiff.xdiffblocks
213
216
214
217
215 def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
218 def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
216 """Return (block, type) tuples, where block is an mdiff.blocks
219 """Return (block, type) tuples, where block is an mdiff.blocks
217 line entry. type is '=' for blocks matching exactly one another
220 line entry. type is '=' for blocks matching exactly one another
218 (bdiff blocks), '!' for non-matching blocks and '~' for blocks
221 (bdiff blocks), '!' for non-matching blocks and '~' for blocks
219 matching only after having filtered blank lines.
222 matching only after having filtered blank lines.
220 line1 and line2 are text1 and text2 split with splitnewlines() if
223 line1 and line2 are text1 and text2 split with splitnewlines() if
221 they are already available.
224 they are already available.
222 """
225 """
223 if opts is None:
226 if opts is None:
224 opts = defaultopts
227 opts = defaultopts
225 if opts.ignorews or opts.ignorewsamount or opts.ignorewseol:
228 if opts.ignorews or opts.ignorewsamount or opts.ignorewseol:
226 text1 = wsclean(opts, text1, False)
229 text1 = wsclean(opts, text1, False)
227 text2 = wsclean(opts, text2, False)
230 text2 = wsclean(opts, text2, False)
228 diff = chooseblocksfunc(opts)(text1, text2)
231 diff = chooseblocksfunc(opts)(text1, text2)
229 for i, s1 in enumerate(diff):
232 for i, s1 in enumerate(diff):
230 # The first match is special.
233 # The first match is special.
231 # we've either found a match starting at line 0 or a match later
234 # we've either found a match starting at line 0 or a match later
232 # in the file. If it starts later, old and new below will both be
235 # in the file. If it starts later, old and new below will both be
233 # empty and we'll continue to the next match.
236 # empty and we'll continue to the next match.
234 if i > 0:
237 if i > 0:
235 s = diff[i - 1]
238 s = diff[i - 1]
236 else:
239 else:
237 s = [0, 0, 0, 0]
240 s = [0, 0, 0, 0]
238 s = [s[1], s1[0], s[3], s1[2]]
241 s = [s[1], s1[0], s[3], s1[2]]
239
242
240 # bdiff sometimes gives huge matches past eof, this check eats them,
243 # bdiff sometimes gives huge matches past eof, this check eats them,
241 # and deals with the special first match case described above
244 # and deals with the special first match case described above
242 if s[0] != s[1] or s[2] != s[3]:
245 if s[0] != s[1] or s[2] != s[3]:
243 type = b'!'
246 type = b'!'
244 if opts.ignoreblanklines:
247 if opts.ignoreblanklines:
245 if lines1 is None:
248 if lines1 is None:
246 lines1 = splitnewlines(text1)
249 lines1 = splitnewlines(text1)
247 if lines2 is None:
250 if lines2 is None:
248 lines2 = splitnewlines(text2)
251 lines2 = splitnewlines(text2)
249 old = wsclean(opts, b"".join(lines1[s[0] : s[1]]))
252 old = wsclean(opts, b"".join(lines1[s[0] : s[1]]))
250 new = wsclean(opts, b"".join(lines2[s[2] : s[3]]))
253 new = wsclean(opts, b"".join(lines2[s[2] : s[3]]))
251 if old == new:
254 if old == new:
252 type = b'~'
255 type = b'~'
253 yield s, type
256 yield s, type
254 yield s1, b'='
257 yield s1, b'='
255
258
256
259
257 def unidiff(a, ad, b, bd, fn1, fn2, binary, opts=defaultopts):
260 def unidiff(a, ad, b, bd, fn1, fn2, binary, opts=defaultopts):
258 """Return a unified diff as a (headers, hunks) tuple.
261 """Return a unified diff as a (headers, hunks) tuple.
259
262
260 If the diff is not null, `headers` is a list with unified diff header
263 If the diff is not null, `headers` is a list with unified diff header
261 lines "--- <original>" and "+++ <new>" and `hunks` is a generator yielding
264 lines "--- <original>" and "+++ <new>" and `hunks` is a generator yielding
262 (hunkrange, hunklines) coming from _unidiff().
265 (hunkrange, hunklines) coming from _unidiff().
263 Otherwise, `headers` and `hunks` are empty.
266 Otherwise, `headers` and `hunks` are empty.
264
267
265 Set binary=True if either a or b should be taken as a binary file.
268 Set binary=True if either a or b should be taken as a binary file.
266 """
269 """
267
270
268 def datetag(date, fn=None):
271 def datetag(date, fn=None):
269 if not opts.git and not opts.nodates:
272 if not opts.git and not opts.nodates:
270 return b'\t%s' % date
273 return b'\t%s' % date
271 if fn and b' ' in fn:
274 if fn and b' ' in fn:
272 return b'\t'
275 return b'\t'
273 return b''
276 return b''
274
277
275 sentinel = [], ()
278 sentinel = [], ()
276 if not a and not b:
279 if not a and not b:
277 return sentinel
280 return sentinel
278
281
279 if opts.noprefix:
282 if opts.noprefix:
280 aprefix = bprefix = b''
283 aprefix = bprefix = b''
281 else:
284 else:
282 aprefix = b'a/'
285 aprefix = b'a/'
283 bprefix = b'b/'
286 bprefix = b'b/'
284
287
285 epoch = dateutil.datestr((0, 0))
288 epoch = dateutil.datestr((0, 0))
286
289
287 fn1 = util.pconvert(fn1)
290 fn1 = util.pconvert(fn1)
288 fn2 = util.pconvert(fn2)
291 fn2 = util.pconvert(fn2)
289
292
290 if binary:
293 if binary:
291 if a and b and len(a) == len(b) and a == b:
294 if a and b and len(a) == len(b) and a == b:
292 return sentinel
295 return sentinel
293 headerlines = []
296 headerlines = []
294 hunks = ((None, [b'Binary file %s has changed\n' % fn1]),)
297 hunks = ((None, [b'Binary file %s has changed\n' % fn1]),)
295 elif not a:
298 elif not a:
296 without_newline = not b.endswith(b'\n')
299 without_newline = not b.endswith(b'\n')
297 b = splitnewlines(b)
300 b = splitnewlines(b)
298 if a is None:
301 if a is None:
299 l1 = b'--- /dev/null%s' % datetag(epoch)
302 l1 = b'--- /dev/null%s' % datetag(epoch)
300 else:
303 else:
301 l1 = b"--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
304 l1 = b"--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
302 l2 = b"+++ %s%s" % (bprefix + fn2, datetag(bd, fn2))
305 l2 = b"+++ %s%s" % (bprefix + fn2, datetag(bd, fn2))
303 headerlines = [l1, l2]
306 headerlines = [l1, l2]
304 size = len(b)
307 size = len(b)
305 hunkrange = (0, 0, 1, size)
308 hunkrange = (0, 0, 1, size)
306 hunklines = [b"@@ -0,0 +1,%d @@\n" % size] + [b"+" + e for e in b]
309 hunklines = [b"@@ -0,0 +1,%d @@\n" % size] + [b"+" + e for e in b]
307 if without_newline:
310 if without_newline:
308 hunklines[-1] += b'\n'
311 hunklines[-1] += b'\n'
309 hunklines.append(_missing_newline_marker)
312 hunklines.append(_missing_newline_marker)
310 hunks = ((hunkrange, hunklines),)
313 hunks = ((hunkrange, hunklines),)
311 elif not b:
314 elif not b:
312 without_newline = not a.endswith(b'\n')
315 without_newline = not a.endswith(b'\n')
313 a = splitnewlines(a)
316 a = splitnewlines(a)
314 l1 = b"--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
317 l1 = b"--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1))
315 if b is None:
318 if b is None:
316 l2 = b'+++ /dev/null%s' % datetag(epoch)
319 l2 = b'+++ /dev/null%s' % datetag(epoch)
317 else:
320 else:
318 l2 = b"+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2))
321 l2 = b"+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2))
319 headerlines = [l1, l2]
322 headerlines = [l1, l2]
320 size = len(a)
323 size = len(a)
321 hunkrange = (1, size, 0, 0)
324 hunkrange = (1, size, 0, 0)
322 hunklines = [b"@@ -1,%d +0,0 @@\n" % size] + [b"-" + e for e in a]
325 hunklines = [b"@@ -1,%d +0,0 @@\n" % size] + [b"-" + e for e in a]
323 if without_newline:
326 if without_newline:
324 hunklines[-1] += b'\n'
327 hunklines[-1] += b'\n'
325 hunklines.append(_missing_newline_marker)
328 hunklines.append(_missing_newline_marker)
326 hunks = ((hunkrange, hunklines),)
329 hunks = ((hunkrange, hunklines),)
327 else:
330 else:
328 hunks = _unidiff(a, b, opts=opts)
331 hunks = _unidiff(a, b, opts=opts)
329 if not next(hunks):
332 if not next(hunks):
330 return sentinel
333 return sentinel
331
334
332 headerlines = [
335 headerlines = [
333 b"--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1)),
336 b"--- %s%s%s" % (aprefix, fn1, datetag(ad, fn1)),
334 b"+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2)),
337 b"+++ %s%s%s" % (bprefix, fn2, datetag(bd, fn2)),
335 ]
338 ]
336
339
337 return headerlines, hunks
340 return headerlines, hunks
338
341
339
342
340 def _unidiff(t1, t2, opts=defaultopts):
343 def _unidiff(t1, t2, opts=defaultopts):
341 """Yield hunks of a headerless unified diff from t1 and t2 texts.
344 """Yield hunks of a headerless unified diff from t1 and t2 texts.
342
345
343 Each hunk consists of a (hunkrange, hunklines) tuple where `hunkrange` is a
346 Each hunk consists of a (hunkrange, hunklines) tuple where `hunkrange` is a
344 tuple (s1, l1, s2, l2) representing the range information of the hunk to
347 tuple (s1, l1, s2, l2) representing the range information of the hunk to
345 form the '@@ -s1,l1 +s2,l2 @@' header and `hunklines` is a list of lines
348 form the '@@ -s1,l1 +s2,l2 @@' header and `hunklines` is a list of lines
346 of the hunk combining said header followed by line additions and
349 of the hunk combining said header followed by line additions and
347 deletions.
350 deletions.
348
351
349 The hunks are prefixed with a bool.
352 The hunks are prefixed with a bool.
350 """
353 """
351 l1 = splitnewlines(t1)
354 l1 = splitnewlines(t1)
352 l2 = splitnewlines(t2)
355 l2 = splitnewlines(t2)
353
356
354 def contextend(l, len):
357 def contextend(l, len):
355 ret = l + opts.context
358 ret = l + opts.context
356 if ret > len:
359 if ret > len:
357 ret = len
360 ret = len
358 return ret
361 return ret
359
362
360 def contextstart(l):
363 def contextstart(l):
361 ret = l - opts.context
364 ret = l - opts.context
362 if ret < 0:
365 if ret < 0:
363 return 0
366 return 0
364 return ret
367 return ret
365
368
366 lastfunc = [0, b'']
369 lastfunc = [0, b'']
367
370
368 def yieldhunk(hunk):
371 def yieldhunk(hunk):
369 (astart, a2, bstart, b2, delta) = hunk
372 (astart, a2, bstart, b2, delta) = hunk
370 aend = contextend(a2, len(l1))
373 aend = contextend(a2, len(l1))
371 alen = aend - astart
374 alen = aend - astart
372 blen = b2 - bstart + aend - a2
375 blen = b2 - bstart + aend - a2
373
376
374 func = b""
377 func = b""
375 if opts.showfunc:
378 if opts.showfunc:
376 lastpos, func = lastfunc
379 lastpos, func = lastfunc
377 # walk backwards from the start of the context up to the start of
380 # walk backwards from the start of the context up to the start of
378 # the previous hunk context until we find a line starting with an
381 # the previous hunk context until we find a line starting with an
379 # alphanumeric char.
382 # alphanumeric char.
380 for i in pycompat.xrange(astart - 1, lastpos - 1, -1):
383 for i in pycompat.xrange(astart - 1, lastpos - 1, -1):
381 if l1[i][0:1].isalnum():
384 if l1[i][0:1].isalnum():
382 func = b' ' + l1[i].rstrip()
385 func = b' ' + l1[i].rstrip()
383 # split long function name if ASCII. otherwise we have no
386 # split long function name if ASCII. otherwise we have no
384 # idea where the multi-byte boundary is, so just leave it.
387 # idea where the multi-byte boundary is, so just leave it.
385 if encoding.isasciistr(func):
388 if encoding.isasciistr(func):
386 func = func[:41]
389 func = func[:41]
387 lastfunc[1] = func
390 lastfunc[1] = func
388 break
391 break
389 # by recording this hunk's starting point as the next place to
392 # by recording this hunk's starting point as the next place to
390 # start looking for function lines, we avoid reading any line in
393 # start looking for function lines, we avoid reading any line in
391 # the file more than once.
394 # the file more than once.
392 lastfunc[0] = astart
395 lastfunc[0] = astart
393
396
394 # zero-length hunk ranges report their start line as one less
397 # zero-length hunk ranges report their start line as one less
395 if alen:
398 if alen:
396 astart += 1
399 astart += 1
397 if blen:
400 if blen:
398 bstart += 1
401 bstart += 1
399
402
400 hunkrange = astart, alen, bstart, blen
403 hunkrange = astart, alen, bstart, blen
401 hunklines = (
404 hunklines = (
402 [b"@@ -%d,%d +%d,%d @@%s\n" % (hunkrange + (func,))]
405 [b"@@ -%d,%d +%d,%d @@%s\n" % (hunkrange + (func,))]
403 + delta
406 + delta
404 + [b' ' + l1[x] for x in pycompat.xrange(a2, aend)]
407 + [b' ' + l1[x] for x in pycompat.xrange(a2, aend)]
405 )
408 )
406 # If either file ends without a newline and the last line of
409 # If either file ends without a newline and the last line of
407 # that file is part of a hunk, a marker is printed. If the
410 # that file is part of a hunk, a marker is printed. If the
408 # last line of both files is identical and neither ends in
411 # last line of both files is identical and neither ends in
409 # a newline, print only one marker. That's the only case in
412 # a newline, print only one marker. That's the only case in
410 # which the hunk can end in a shared line without a newline.
413 # which the hunk can end in a shared line without a newline.
411 skip = False
414 skip = False
412 if not t1.endswith(b'\n') and astart + alen == len(l1) + 1:
415 if not t1.endswith(b'\n') and astart + alen == len(l1) + 1:
413 for i in pycompat.xrange(len(hunklines) - 1, -1, -1):
416 for i in pycompat.xrange(len(hunklines) - 1, -1, -1):
414 if hunklines[i].startswith((b'-', b' ')):
417 if hunklines[i].startswith((b'-', b' ')):
415 if hunklines[i].startswith(b' '):
418 if hunklines[i].startswith(b' '):
416 skip = True
419 skip = True
417 hunklines[i] += b'\n'
420 hunklines[i] += b'\n'
418 hunklines.insert(i + 1, _missing_newline_marker)
421 hunklines.insert(i + 1, _missing_newline_marker)
419 break
422 break
420 if not skip and not t2.endswith(b'\n') and bstart + blen == len(l2) + 1:
423 if not skip and not t2.endswith(b'\n') and bstart + blen == len(l2) + 1:
421 for i in pycompat.xrange(len(hunklines) - 1, -1, -1):
424 for i in pycompat.xrange(len(hunklines) - 1, -1, -1):
422 if hunklines[i].startswith(b'+'):
425 if hunklines[i].startswith(b'+'):
423 hunklines[i] += b'\n'
426 hunklines[i] += b'\n'
424 hunklines.insert(i + 1, _missing_newline_marker)
427 hunklines.insert(i + 1, _missing_newline_marker)
425 break
428 break
426 yield hunkrange, hunklines
429 yield hunkrange, hunklines
427
430
428 # bdiff.blocks gives us the matching sequences in the files. The loop
431 # bdiff.blocks gives us the matching sequences in the files. The loop
429 # below finds the spaces between those matching sequences and translates
432 # below finds the spaces between those matching sequences and translates
430 # them into diff output.
433 # them into diff output.
431 #
434 #
432 hunk = None
435 hunk = None
433 ignoredlines = 0
436 ignoredlines = 0
434 has_hunks = False
437 has_hunks = False
435 for s, stype in allblocks(t1, t2, opts, l1, l2):
438 for s, stype in allblocks(t1, t2, opts, l1, l2):
436 a1, a2, b1, b2 = s
439 a1, a2, b1, b2 = s
437 if stype != b'!':
440 if stype != b'!':
438 if stype == b'~':
441 if stype == b'~':
439 # The diff context lines are based on t1 content. When
442 # The diff context lines are based on t1 content. When
440 # blank lines are ignored, the new lines offsets must
443 # blank lines are ignored, the new lines offsets must
441 # be adjusted as if equivalent blocks ('~') had the
444 # be adjusted as if equivalent blocks ('~') had the
442 # same sizes on both sides.
445 # same sizes on both sides.
443 ignoredlines += (b2 - b1) - (a2 - a1)
446 ignoredlines += (b2 - b1) - (a2 - a1)
444 continue
447 continue
445 delta = []
448 delta = []
446 old = l1[a1:a2]
449 old = l1[a1:a2]
447 new = l2[b1:b2]
450 new = l2[b1:b2]
448
451
449 b1 -= ignoredlines
452 b1 -= ignoredlines
450 b2 -= ignoredlines
453 b2 -= ignoredlines
451 astart = contextstart(a1)
454 astart = contextstart(a1)
452 bstart = contextstart(b1)
455 bstart = contextstart(b1)
453 prev = None
456 prev = None
454 if hunk:
457 if hunk:
455 # join with the previous hunk if it falls inside the context
458 # join with the previous hunk if it falls inside the context
456 if astart < hunk[1] + opts.context + 1:
459 if astart < hunk[1] + opts.context + 1:
457 prev = hunk
460 prev = hunk
458 astart = hunk[1]
461 astart = hunk[1]
459 bstart = hunk[3]
462 bstart = hunk[3]
460 else:
463 else:
461 if not has_hunks:
464 if not has_hunks:
462 has_hunks = True
465 has_hunks = True
463 yield True
466 yield True
464 for x in yieldhunk(hunk):
467 for x in yieldhunk(hunk):
465 yield x
468 yield x
466 if prev:
469 if prev:
467 # we've joined the previous hunk, record the new ending points.
470 # we've joined the previous hunk, record the new ending points.
468 hunk[1] = a2
471 hunk[1] = a2
469 hunk[3] = b2
472 hunk[3] = b2
470 delta = hunk[4]
473 delta = hunk[4]
471 else:
474 else:
472 # create a new hunk
475 # create a new hunk
473 hunk = [astart, a2, bstart, b2, delta]
476 hunk = [astart, a2, bstart, b2, delta]
474
477
475 delta[len(delta) :] = [b' ' + x for x in l1[astart:a1]]
478 delta[len(delta) :] = [b' ' + x for x in l1[astart:a1]]
476 delta[len(delta) :] = [b'-' + x for x in old]
479 delta[len(delta) :] = [b'-' + x for x in old]
477 delta[len(delta) :] = [b'+' + x for x in new]
480 delta[len(delta) :] = [b'+' + x for x in new]
478
481
479 if hunk:
482 if hunk:
480 if not has_hunks:
483 if not has_hunks:
481 has_hunks = True
484 has_hunks = True
482 yield True
485 yield True
483 for x in yieldhunk(hunk):
486 for x in yieldhunk(hunk):
484 yield x
487 yield x
485 elif not has_hunks:
488 elif not has_hunks:
486 yield False
489 yield False
487
490
488
491
489 def b85diff(to, tn):
492 def b85diff(to, tn):
490 '''print base85-encoded binary diff'''
493 '''print base85-encoded binary diff'''
491
494
492 def fmtline(line):
495 def fmtline(line):
493 l = len(line)
496 l = len(line)
494 if l <= 26:
497 if l <= 26:
495 l = pycompat.bytechr(ord(b'A') + l - 1)
498 l = pycompat.bytechr(ord(b'A') + l - 1)
496 else:
499 else:
497 l = pycompat.bytechr(l - 26 + ord(b'a') - 1)
500 l = pycompat.bytechr(l - 26 + ord(b'a') - 1)
498 return b'%c%s\n' % (l, util.b85encode(line, True))
501 return b'%c%s\n' % (l, util.b85encode(line, True))
499
502
500 def chunk(text, csize=52):
503 def chunk(text, csize=52):
501 l = len(text)
504 l = len(text)
502 i = 0
505 i = 0
503 while i < l:
506 while i < l:
504 yield text[i : i + csize]
507 yield text[i : i + csize]
505 i += csize
508 i += csize
506
509
507 if to is None:
510 if to is None:
508 to = b''
511 to = b''
509 if tn is None:
512 if tn is None:
510 tn = b''
513 tn = b''
511
514
512 if to == tn:
515 if to == tn:
513 return b''
516 return b''
514
517
515 # TODO: deltas
518 # TODO: deltas
516 ret = []
519 ret = []
517 ret.append(b'GIT binary patch\n')
520 ret.append(b'GIT binary patch\n')
518 ret.append(b'literal %d\n' % len(tn))
521 ret.append(b'literal %d\n' % len(tn))
519 for l in chunk(zlib.compress(tn)):
522 for l in chunk(zlib.compress(tn)):
520 ret.append(fmtline(l))
523 ret.append(fmtline(l))
521 ret.append(b'\n')
524 ret.append(b'\n')
522
525
523 return b''.join(ret)
526 return b''.join(ret)
524
527
525
528
526 def patchtext(bin):
529 def patchtext(bin):
527 pos = 0
530 pos = 0
528 t = []
531 t = []
529 while pos < len(bin):
532 while pos < len(bin):
530 p1, p2, l = struct.unpack(b">lll", bin[pos : pos + 12])
533 p1, p2, l = struct.unpack(b">lll", bin[pos : pos + 12])
531 pos += 12
534 pos += 12
532 t.append(bin[pos : pos + l])
535 t.append(bin[pos : pos + l])
533 pos += l
536 pos += l
534 return b"".join(t)
537 return b"".join(t)
535
538
536
539
537 def patch(a, bin):
540 def patch(a, bin):
538 if len(a) == 0:
541 if len(a) == 0:
539 # skip over trivial delta header
542 # skip over trivial delta header
540 return util.buffer(bin, 12)
543 return util.buffer(bin, 12)
541 return mpatch.patches(a, [bin])
544 return mpatch.patches(a, [bin])
542
545
543
546
544 # similar to difflib.SequenceMatcher.get_matching_blocks
547 # similar to difflib.SequenceMatcher.get_matching_blocks
545 def get_matching_blocks(a, b):
548 def get_matching_blocks(a, b):
546 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
549 return [(d[0], d[2], d[1] - d[0]) for d in bdiff.blocks(a, b)]
547
550
548
551
549 def trivialdiffheader(length):
552 def trivialdiffheader(length):
550 return struct.pack(b">lll", 0, 0, length) if length else b''
553 return struct.pack(b">lll", 0, 0, length) if length else b''
551
554
552
555
553 def replacediffheader(oldlen, newlen):
556 def replacediffheader(oldlen, newlen):
554 return struct.pack(b">lll", 0, oldlen, newlen)
557 return struct.pack(b">lll", 0, oldlen, newlen)
General Comments 0
You need to be logged in to leave comments. Login now