##// END OF EJS Templates
pytype: stop excluding patch.py...
Matt Harbison -
r49314:290f9c15 default
parent child Browse files
Show More
@@ -1,3268 +1,3270 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import, print_function
9 from __future__ import absolute_import, print_function
10
10
11 import collections
11 import collections
12 import contextlib
12 import contextlib
13 import copy
13 import copy
14 import errno
14 import errno
15 import os
15 import os
16 import re
16 import re
17 import shutil
17 import shutil
18 import zlib
18 import zlib
19
19
20 from .i18n import _
20 from .i18n import _
21 from .node import (
21 from .node import (
22 hex,
22 hex,
23 sha1nodeconstants,
23 sha1nodeconstants,
24 short,
24 short,
25 )
25 )
26 from .pycompat import open
26 from .pycompat import open
27 from . import (
27 from . import (
28 copies,
28 copies,
29 diffhelper,
29 diffhelper,
30 diffutil,
30 diffutil,
31 encoding,
31 encoding,
32 error,
32 error,
33 mail,
33 mail,
34 mdiff,
34 mdiff,
35 pathutil,
35 pathutil,
36 pycompat,
36 pycompat,
37 scmutil,
37 scmutil,
38 similar,
38 similar,
39 util,
39 util,
40 vfs as vfsmod,
40 vfs as vfsmod,
41 )
41 )
42 from .utils import (
42 from .utils import (
43 dateutil,
43 dateutil,
44 hashutil,
44 hashutil,
45 procutil,
45 procutil,
46 stringutil,
46 stringutil,
47 )
47 )
48
48
49 stringio = util.stringio
49 stringio = util.stringio
50
50
51 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
51 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
52 tabsplitter = re.compile(br'(\t+|[^\t]+)')
52 tabsplitter = re.compile(br'(\t+|[^\t]+)')
53 wordsplitter = re.compile(
53 wordsplitter = re.compile(
54 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
54 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
55 )
55 )
56
56
57 PatchError = error.PatchError
57 PatchError = error.PatchError
58 PatchParseError = error.PatchParseError
58 PatchParseError = error.PatchParseError
59 PatchApplicationError = error.PatchApplicationError
59 PatchApplicationError = error.PatchApplicationError
60
60
61 # public functions
61 # public functions
62
62
63
63
64 def split(stream):
64 def split(stream):
65 '''return an iterator of individual patches from a stream'''
65 '''return an iterator of individual patches from a stream'''
66
66
67 def isheader(line, inheader):
67 def isheader(line, inheader):
68 if inheader and line.startswith((b' ', b'\t')):
68 if inheader and line.startswith((b' ', b'\t')):
69 # continuation
69 # continuation
70 return True
70 return True
71 if line.startswith((b' ', b'-', b'+')):
71 if line.startswith((b' ', b'-', b'+')):
72 # diff line - don't check for header pattern in there
72 # diff line - don't check for header pattern in there
73 return False
73 return False
74 l = line.split(b': ', 1)
74 l = line.split(b': ', 1)
75 return len(l) == 2 and b' ' not in l[0]
75 return len(l) == 2 and b' ' not in l[0]
76
76
77 def chunk(lines):
77 def chunk(lines):
78 return stringio(b''.join(lines))
78 return stringio(b''.join(lines))
79
79
80 def hgsplit(stream, cur):
80 def hgsplit(stream, cur):
81 inheader = True
81 inheader = True
82
82
83 for line in stream:
83 for line in stream:
84 if not line.strip():
84 if not line.strip():
85 inheader = False
85 inheader = False
86 if not inheader and line.startswith(b'# HG changeset patch'):
86 if not inheader and line.startswith(b'# HG changeset patch'):
87 yield chunk(cur)
87 yield chunk(cur)
88 cur = []
88 cur = []
89 inheader = True
89 inheader = True
90
90
91 cur.append(line)
91 cur.append(line)
92
92
93 if cur:
93 if cur:
94 yield chunk(cur)
94 yield chunk(cur)
95
95
96 def mboxsplit(stream, cur):
96 def mboxsplit(stream, cur):
97 for line in stream:
97 for line in stream:
98 if line.startswith(b'From '):
98 if line.startswith(b'From '):
99 for c in split(chunk(cur[1:])):
99 for c in split(chunk(cur[1:])):
100 yield c
100 yield c
101 cur = []
101 cur = []
102
102
103 cur.append(line)
103 cur.append(line)
104
104
105 if cur:
105 if cur:
106 for c in split(chunk(cur[1:])):
106 for c in split(chunk(cur[1:])):
107 yield c
107 yield c
108
108
109 def mimesplit(stream, cur):
109 def mimesplit(stream, cur):
110 def msgfp(m):
110 def msgfp(m):
111 fp = stringio()
111 fp = stringio()
112 # pytype: disable=wrong-arg-types
112 g = mail.Generator(fp, mangle_from_=False)
113 g = mail.Generator(fp, mangle_from_=False)
114 # pytype: enable=wrong-arg-types
113 g.flatten(m)
115 g.flatten(m)
114 fp.seek(0)
116 fp.seek(0)
115 return fp
117 return fp
116
118
117 for line in stream:
119 for line in stream:
118 cur.append(line)
120 cur.append(line)
119 c = chunk(cur)
121 c = chunk(cur)
120
122
121 m = mail.parse(c)
123 m = mail.parse(c)
122 if not m.is_multipart():
124 if not m.is_multipart():
123 yield msgfp(m)
125 yield msgfp(m)
124 else:
126 else:
125 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
127 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
126 for part in m.walk():
128 for part in m.walk():
127 ct = part.get_content_type()
129 ct = part.get_content_type()
128 if ct not in ok_types:
130 if ct not in ok_types:
129 continue
131 continue
130 yield msgfp(part)
132 yield msgfp(part)
131
133
132 def headersplit(stream, cur):
134 def headersplit(stream, cur):
133 inheader = False
135 inheader = False
134
136
135 for line in stream:
137 for line in stream:
136 if not inheader and isheader(line, inheader):
138 if not inheader and isheader(line, inheader):
137 yield chunk(cur)
139 yield chunk(cur)
138 cur = []
140 cur = []
139 inheader = True
141 inheader = True
140 if inheader and not isheader(line, inheader):
142 if inheader and not isheader(line, inheader):
141 inheader = False
143 inheader = False
142
144
143 cur.append(line)
145 cur.append(line)
144
146
145 if cur:
147 if cur:
146 yield chunk(cur)
148 yield chunk(cur)
147
149
148 def remainder(cur):
150 def remainder(cur):
149 yield chunk(cur)
151 yield chunk(cur)
150
152
151 class fiter(object):
153 class fiter(object):
152 def __init__(self, fp):
154 def __init__(self, fp):
153 self.fp = fp
155 self.fp = fp
154
156
155 def __iter__(self):
157 def __iter__(self):
156 return self
158 return self
157
159
158 def next(self):
160 def next(self):
159 l = self.fp.readline()
161 l = self.fp.readline()
160 if not l:
162 if not l:
161 raise StopIteration
163 raise StopIteration
162 return l
164 return l
163
165
164 __next__ = next
166 __next__ = next
165
167
166 inheader = False
168 inheader = False
167 cur = []
169 cur = []
168
170
169 mimeheaders = [b'content-type']
171 mimeheaders = [b'content-type']
170
172
171 if not util.safehasattr(stream, b'next'):
173 if not util.safehasattr(stream, b'next'):
172 # http responses, for example, have readline but not next
174 # http responses, for example, have readline but not next
173 stream = fiter(stream)
175 stream = fiter(stream)
174
176
175 for line in stream:
177 for line in stream:
176 cur.append(line)
178 cur.append(line)
177 if line.startswith(b'# HG changeset patch'):
179 if line.startswith(b'# HG changeset patch'):
178 return hgsplit(stream, cur)
180 return hgsplit(stream, cur)
179 elif line.startswith(b'From '):
181 elif line.startswith(b'From '):
180 return mboxsplit(stream, cur)
182 return mboxsplit(stream, cur)
181 elif isheader(line, inheader):
183 elif isheader(line, inheader):
182 inheader = True
184 inheader = True
183 if line.split(b':', 1)[0].lower() in mimeheaders:
185 if line.split(b':', 1)[0].lower() in mimeheaders:
184 # let email parser handle this
186 # let email parser handle this
185 return mimesplit(stream, cur)
187 return mimesplit(stream, cur)
186 elif line.startswith(b'--- ') and inheader:
188 elif line.startswith(b'--- ') and inheader:
187 # No evil headers seen by diff start, split by hand
189 # No evil headers seen by diff start, split by hand
188 return headersplit(stream, cur)
190 return headersplit(stream, cur)
189 # Not enough info, keep reading
191 # Not enough info, keep reading
190
192
191 # if we are here, we have a very plain patch
193 # if we are here, we have a very plain patch
192 return remainder(cur)
194 return remainder(cur)
193
195
194
196
195 ## Some facility for extensible patch parsing:
197 ## Some facility for extensible patch parsing:
196 # list of pairs ("header to match", "data key")
198 # list of pairs ("header to match", "data key")
197 patchheadermap = [
199 patchheadermap = [
198 (b'Date', b'date'),
200 (b'Date', b'date'),
199 (b'Branch', b'branch'),
201 (b'Branch', b'branch'),
200 (b'Node ID', b'nodeid'),
202 (b'Node ID', b'nodeid'),
201 ]
203 ]
202
204
203
205
204 @contextlib.contextmanager
206 @contextlib.contextmanager
205 def extract(ui, fileobj):
207 def extract(ui, fileobj):
206 """extract patch from data read from fileobj.
208 """extract patch from data read from fileobj.
207
209
208 patch can be a normal patch or contained in an email message.
210 patch can be a normal patch or contained in an email message.
209
211
210 return a dictionary. Standard keys are:
212 return a dictionary. Standard keys are:
211 - filename,
213 - filename,
212 - message,
214 - message,
213 - user,
215 - user,
214 - date,
216 - date,
215 - branch,
217 - branch,
216 - node,
218 - node,
217 - p1,
219 - p1,
218 - p2.
220 - p2.
219 Any item can be missing from the dictionary. If filename is missing,
221 Any item can be missing from the dictionary. If filename is missing,
220 fileobj did not contain a patch. Caller must unlink filename when done."""
222 fileobj did not contain a patch. Caller must unlink filename when done."""
221
223
222 fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-')
224 fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-')
223 tmpfp = os.fdopen(fd, 'wb')
225 tmpfp = os.fdopen(fd, 'wb')
224 try:
226 try:
225 yield _extract(ui, fileobj, tmpname, tmpfp)
227 yield _extract(ui, fileobj, tmpname, tmpfp)
226 finally:
228 finally:
227 tmpfp.close()
229 tmpfp.close()
228 os.unlink(tmpname)
230 os.unlink(tmpname)
229
231
230
232
231 def _extract(ui, fileobj, tmpname, tmpfp):
233 def _extract(ui, fileobj, tmpname, tmpfp):
232
234
233 # attempt to detect the start of a patch
235 # attempt to detect the start of a patch
234 # (this heuristic is borrowed from quilt)
236 # (this heuristic is borrowed from quilt)
235 diffre = re.compile(
237 diffre = re.compile(
236 br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
238 br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
237 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
239 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
238 br'---[ \t].*?^\+\+\+[ \t]|'
240 br'---[ \t].*?^\+\+\+[ \t]|'
239 br'\*\*\*[ \t].*?^---[ \t])',
241 br'\*\*\*[ \t].*?^---[ \t])',
240 re.MULTILINE | re.DOTALL,
242 re.MULTILINE | re.DOTALL,
241 )
243 )
242
244
243 data = {}
245 data = {}
244
246
245 msg = mail.parse(fileobj)
247 msg = mail.parse(fileobj)
246
248
247 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
249 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
248 data[b'user'] = msg['From'] and mail.headdecode(msg['From'])
250 data[b'user'] = msg['From'] and mail.headdecode(msg['From'])
249 if not subject and not data[b'user']:
251 if not subject and not data[b'user']:
250 # Not an email, restore parsed headers if any
252 # Not an email, restore parsed headers if any
251 subject = (
253 subject = (
252 b'\n'.join(
254 b'\n'.join(
253 b': '.join(map(encoding.strtolocal, h)) for h in msg.items()
255 b': '.join(map(encoding.strtolocal, h)) for h in msg.items()
254 )
256 )
255 + b'\n'
257 + b'\n'
256 )
258 )
257
259
258 # should try to parse msg['Date']
260 # should try to parse msg['Date']
259 parents = []
261 parents = []
260
262
261 nodeid = msg['X-Mercurial-Node']
263 nodeid = msg['X-Mercurial-Node']
262 if nodeid:
264 if nodeid:
263 data[b'nodeid'] = nodeid = mail.headdecode(nodeid)
265 data[b'nodeid'] = nodeid = mail.headdecode(nodeid)
264 ui.debug(b'Node ID: %s\n' % nodeid)
266 ui.debug(b'Node ID: %s\n' % nodeid)
265
267
266 if subject:
268 if subject:
267 if subject.startswith(b'[PATCH'):
269 if subject.startswith(b'[PATCH'):
268 pend = subject.find(b']')
270 pend = subject.find(b']')
269 if pend >= 0:
271 if pend >= 0:
270 subject = subject[pend + 1 :].lstrip()
272 subject = subject[pend + 1 :].lstrip()
271 subject = re.sub(br'\n[ \t]+', b' ', subject)
273 subject = re.sub(br'\n[ \t]+', b' ', subject)
272 ui.debug(b'Subject: %s\n' % subject)
274 ui.debug(b'Subject: %s\n' % subject)
273 if data[b'user']:
275 if data[b'user']:
274 ui.debug(b'From: %s\n' % data[b'user'])
276 ui.debug(b'From: %s\n' % data[b'user'])
275 diffs_seen = 0
277 diffs_seen = 0
276 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
278 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
277 message = b''
279 message = b''
278 for part in msg.walk():
280 for part in msg.walk():
279 content_type = pycompat.bytestr(part.get_content_type())
281 content_type = pycompat.bytestr(part.get_content_type())
280 ui.debug(b'Content-Type: %s\n' % content_type)
282 ui.debug(b'Content-Type: %s\n' % content_type)
281 if content_type not in ok_types:
283 if content_type not in ok_types:
282 continue
284 continue
283 payload = part.get_payload(decode=True)
285 payload = part.get_payload(decode=True)
284 m = diffre.search(payload)
286 m = diffre.search(payload)
285 if m:
287 if m:
286 hgpatch = False
288 hgpatch = False
287 hgpatchheader = False
289 hgpatchheader = False
288 ignoretext = False
290 ignoretext = False
289
291
290 ui.debug(b'found patch at byte %d\n' % m.start(0))
292 ui.debug(b'found patch at byte %d\n' % m.start(0))
291 diffs_seen += 1
293 diffs_seen += 1
292 cfp = stringio()
294 cfp = stringio()
293 for line in payload[: m.start(0)].splitlines():
295 for line in payload[: m.start(0)].splitlines():
294 if line.startswith(b'# HG changeset patch') and not hgpatch:
296 if line.startswith(b'# HG changeset patch') and not hgpatch:
295 ui.debug(b'patch generated by hg export\n')
297 ui.debug(b'patch generated by hg export\n')
296 hgpatch = True
298 hgpatch = True
297 hgpatchheader = True
299 hgpatchheader = True
298 # drop earlier commit message content
300 # drop earlier commit message content
299 cfp.seek(0)
301 cfp.seek(0)
300 cfp.truncate()
302 cfp.truncate()
301 subject = None
303 subject = None
302 elif hgpatchheader:
304 elif hgpatchheader:
303 if line.startswith(b'# User '):
305 if line.startswith(b'# User '):
304 data[b'user'] = line[7:]
306 data[b'user'] = line[7:]
305 ui.debug(b'From: %s\n' % data[b'user'])
307 ui.debug(b'From: %s\n' % data[b'user'])
306 elif line.startswith(b"# Parent "):
308 elif line.startswith(b"# Parent "):
307 parents.append(line[9:].lstrip())
309 parents.append(line[9:].lstrip())
308 elif line.startswith(b"# "):
310 elif line.startswith(b"# "):
309 for header, key in patchheadermap:
311 for header, key in patchheadermap:
310 prefix = b'# %s ' % header
312 prefix = b'# %s ' % header
311 if line.startswith(prefix):
313 if line.startswith(prefix):
312 data[key] = line[len(prefix) :]
314 data[key] = line[len(prefix) :]
313 ui.debug(b'%s: %s\n' % (header, data[key]))
315 ui.debug(b'%s: %s\n' % (header, data[key]))
314 else:
316 else:
315 hgpatchheader = False
317 hgpatchheader = False
316 elif line == b'---':
318 elif line == b'---':
317 ignoretext = True
319 ignoretext = True
318 if not hgpatchheader and not ignoretext:
320 if not hgpatchheader and not ignoretext:
319 cfp.write(line)
321 cfp.write(line)
320 cfp.write(b'\n')
322 cfp.write(b'\n')
321 message = cfp.getvalue()
323 message = cfp.getvalue()
322 if tmpfp:
324 if tmpfp:
323 tmpfp.write(payload)
325 tmpfp.write(payload)
324 if not payload.endswith(b'\n'):
326 if not payload.endswith(b'\n'):
325 tmpfp.write(b'\n')
327 tmpfp.write(b'\n')
326 elif not diffs_seen and message and content_type == b'text/plain':
328 elif not diffs_seen and message and content_type == b'text/plain':
327 message += b'\n' + payload
329 message += b'\n' + payload
328
330
329 if subject and not message.startswith(subject):
331 if subject and not message.startswith(subject):
330 message = b'%s\n%s' % (subject, message)
332 message = b'%s\n%s' % (subject, message)
331 data[b'message'] = message
333 data[b'message'] = message
332 tmpfp.close()
334 tmpfp.close()
333 if parents:
335 if parents:
334 data[b'p1'] = parents.pop(0)
336 data[b'p1'] = parents.pop(0)
335 if parents:
337 if parents:
336 data[b'p2'] = parents.pop(0)
338 data[b'p2'] = parents.pop(0)
337
339
338 if diffs_seen:
340 if diffs_seen:
339 data[b'filename'] = tmpname
341 data[b'filename'] = tmpname
340
342
341 return data
343 return data
342
344
343
345
344 class patchmeta(object):
346 class patchmeta(object):
345 """Patched file metadata
347 """Patched file metadata
346
348
347 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
349 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
348 or COPY. 'path' is patched file path. 'oldpath' is set to the
350 or COPY. 'path' is patched file path. 'oldpath' is set to the
349 origin file when 'op' is either COPY or RENAME, None otherwise. If
351 origin file when 'op' is either COPY or RENAME, None otherwise. If
350 file mode is changed, 'mode' is a tuple (islink, isexec) where
352 file mode is changed, 'mode' is a tuple (islink, isexec) where
351 'islink' is True if the file is a symlink and 'isexec' is True if
353 'islink' is True if the file is a symlink and 'isexec' is True if
352 the file is executable. Otherwise, 'mode' is None.
354 the file is executable. Otherwise, 'mode' is None.
353 """
355 """
354
356
355 def __init__(self, path):
357 def __init__(self, path):
356 self.path = path
358 self.path = path
357 self.oldpath = None
359 self.oldpath = None
358 self.mode = None
360 self.mode = None
359 self.op = b'MODIFY'
361 self.op = b'MODIFY'
360 self.binary = False
362 self.binary = False
361
363
362 def setmode(self, mode):
364 def setmode(self, mode):
363 islink = mode & 0o20000
365 islink = mode & 0o20000
364 isexec = mode & 0o100
366 isexec = mode & 0o100
365 self.mode = (islink, isexec)
367 self.mode = (islink, isexec)
366
368
367 def copy(self):
369 def copy(self):
368 other = patchmeta(self.path)
370 other = patchmeta(self.path)
369 other.oldpath = self.oldpath
371 other.oldpath = self.oldpath
370 other.mode = self.mode
372 other.mode = self.mode
371 other.op = self.op
373 other.op = self.op
372 other.binary = self.binary
374 other.binary = self.binary
373 return other
375 return other
374
376
375 def _ispatchinga(self, afile):
377 def _ispatchinga(self, afile):
376 if afile == b'/dev/null':
378 if afile == b'/dev/null':
377 return self.op == b'ADD'
379 return self.op == b'ADD'
378 return afile == b'a/' + (self.oldpath or self.path)
380 return afile == b'a/' + (self.oldpath or self.path)
379
381
380 def _ispatchingb(self, bfile):
382 def _ispatchingb(self, bfile):
381 if bfile == b'/dev/null':
383 if bfile == b'/dev/null':
382 return self.op == b'DELETE'
384 return self.op == b'DELETE'
383 return bfile == b'b/' + self.path
385 return bfile == b'b/' + self.path
384
386
385 def ispatching(self, afile, bfile):
387 def ispatching(self, afile, bfile):
386 return self._ispatchinga(afile) and self._ispatchingb(bfile)
388 return self._ispatchinga(afile) and self._ispatchingb(bfile)
387
389
388 def __repr__(self):
390 def __repr__(self):
389 return "<patchmeta %s %r>" % (self.op, self.path)
391 return "<patchmeta %s %r>" % (self.op, self.path)
390
392
391
393
392 def readgitpatch(lr):
394 def readgitpatch(lr):
393 """extract git-style metadata about patches from <patchname>"""
395 """extract git-style metadata about patches from <patchname>"""
394
396
395 # Filter patch for git information
397 # Filter patch for git information
396 gp = None
398 gp = None
397 gitpatches = []
399 gitpatches = []
398 for line in lr:
400 for line in lr:
399 line = line.rstrip(b'\r\n')
401 line = line.rstrip(b'\r\n')
400 if line.startswith(b'diff --git a/'):
402 if line.startswith(b'diff --git a/'):
401 m = gitre.match(line)
403 m = gitre.match(line)
402 if m:
404 if m:
403 if gp:
405 if gp:
404 gitpatches.append(gp)
406 gitpatches.append(gp)
405 dst = m.group(2)
407 dst = m.group(2)
406 gp = patchmeta(dst)
408 gp = patchmeta(dst)
407 elif gp:
409 elif gp:
408 if line.startswith(b'--- '):
410 if line.startswith(b'--- '):
409 gitpatches.append(gp)
411 gitpatches.append(gp)
410 gp = None
412 gp = None
411 continue
413 continue
412 if line.startswith(b'rename from '):
414 if line.startswith(b'rename from '):
413 gp.op = b'RENAME'
415 gp.op = b'RENAME'
414 gp.oldpath = line[12:]
416 gp.oldpath = line[12:]
415 elif line.startswith(b'rename to '):
417 elif line.startswith(b'rename to '):
416 gp.path = line[10:]
418 gp.path = line[10:]
417 elif line.startswith(b'copy from '):
419 elif line.startswith(b'copy from '):
418 gp.op = b'COPY'
420 gp.op = b'COPY'
419 gp.oldpath = line[10:]
421 gp.oldpath = line[10:]
420 elif line.startswith(b'copy to '):
422 elif line.startswith(b'copy to '):
421 gp.path = line[8:]
423 gp.path = line[8:]
422 elif line.startswith(b'deleted file'):
424 elif line.startswith(b'deleted file'):
423 gp.op = b'DELETE'
425 gp.op = b'DELETE'
424 elif line.startswith(b'new file mode '):
426 elif line.startswith(b'new file mode '):
425 gp.op = b'ADD'
427 gp.op = b'ADD'
426 gp.setmode(int(line[-6:], 8))
428 gp.setmode(int(line[-6:], 8))
427 elif line.startswith(b'new mode '):
429 elif line.startswith(b'new mode '):
428 gp.setmode(int(line[-6:], 8))
430 gp.setmode(int(line[-6:], 8))
429 elif line.startswith(b'GIT binary patch'):
431 elif line.startswith(b'GIT binary patch'):
430 gp.binary = True
432 gp.binary = True
431 if gp:
433 if gp:
432 gitpatches.append(gp)
434 gitpatches.append(gp)
433
435
434 return gitpatches
436 return gitpatches
435
437
436
438
437 class linereader(object):
439 class linereader(object):
438 # simple class to allow pushing lines back into the input stream
440 # simple class to allow pushing lines back into the input stream
439 def __init__(self, fp):
441 def __init__(self, fp):
440 self.fp = fp
442 self.fp = fp
441 self.buf = []
443 self.buf = []
442
444
443 def push(self, line):
445 def push(self, line):
444 if line is not None:
446 if line is not None:
445 self.buf.append(line)
447 self.buf.append(line)
446
448
447 def readline(self):
449 def readline(self):
448 if self.buf:
450 if self.buf:
449 l = self.buf[0]
451 l = self.buf[0]
450 del self.buf[0]
452 del self.buf[0]
451 return l
453 return l
452 return self.fp.readline()
454 return self.fp.readline()
453
455
454 def __iter__(self):
456 def __iter__(self):
455 return iter(self.readline, b'')
457 return iter(self.readline, b'')
456
458
457
459
458 class abstractbackend(object):
460 class abstractbackend(object):
459 def __init__(self, ui):
461 def __init__(self, ui):
460 self.ui = ui
462 self.ui = ui
461
463
462 def getfile(self, fname):
464 def getfile(self, fname):
463 """Return target file data and flags as a (data, (islink,
465 """Return target file data and flags as a (data, (islink,
464 isexec)) tuple. Data is None if file is missing/deleted.
466 isexec)) tuple. Data is None if file is missing/deleted.
465 """
467 """
466 raise NotImplementedError
468 raise NotImplementedError
467
469
468 def setfile(self, fname, data, mode, copysource):
470 def setfile(self, fname, data, mode, copysource):
469 """Write data to target file fname and set its mode. mode is a
471 """Write data to target file fname and set its mode. mode is a
470 (islink, isexec) tuple. If data is None, the file content should
472 (islink, isexec) tuple. If data is None, the file content should
471 be left unchanged. If the file is modified after being copied,
473 be left unchanged. If the file is modified after being copied,
472 copysource is set to the original file name.
474 copysource is set to the original file name.
473 """
475 """
474 raise NotImplementedError
476 raise NotImplementedError
475
477
476 def unlink(self, fname):
478 def unlink(self, fname):
477 """Unlink target file."""
479 """Unlink target file."""
478 raise NotImplementedError
480 raise NotImplementedError
479
481
480 def writerej(self, fname, failed, total, lines):
482 def writerej(self, fname, failed, total, lines):
481 """Write rejected lines for fname. total is the number of hunks
483 """Write rejected lines for fname. total is the number of hunks
482 which failed to apply and total the total number of hunks for this
484 which failed to apply and total the total number of hunks for this
483 files.
485 files.
484 """
486 """
485
487
486 def exists(self, fname):
488 def exists(self, fname):
487 raise NotImplementedError
489 raise NotImplementedError
488
490
489 def close(self):
491 def close(self):
490 raise NotImplementedError
492 raise NotImplementedError
491
493
492
494
493 class fsbackend(abstractbackend):
495 class fsbackend(abstractbackend):
494 def __init__(self, ui, basedir):
496 def __init__(self, ui, basedir):
495 super(fsbackend, self).__init__(ui)
497 super(fsbackend, self).__init__(ui)
496 self.opener = vfsmod.vfs(basedir)
498 self.opener = vfsmod.vfs(basedir)
497
499
498 def getfile(self, fname):
500 def getfile(self, fname):
499 if self.opener.islink(fname):
501 if self.opener.islink(fname):
500 return (self.opener.readlink(fname), (True, False))
502 return (self.opener.readlink(fname), (True, False))
501
503
502 isexec = False
504 isexec = False
503 try:
505 try:
504 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
506 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
505 except OSError as e:
507 except OSError as e:
506 if e.errno != errno.ENOENT:
508 if e.errno != errno.ENOENT:
507 raise
509 raise
508 try:
510 try:
509 return (self.opener.read(fname), (False, isexec))
511 return (self.opener.read(fname), (False, isexec))
510 except IOError as e:
512 except IOError as e:
511 if e.errno != errno.ENOENT:
513 if e.errno != errno.ENOENT:
512 raise
514 raise
513 return None, None
515 return None, None
514
516
515 def setfile(self, fname, data, mode, copysource):
517 def setfile(self, fname, data, mode, copysource):
516 islink, isexec = mode
518 islink, isexec = mode
517 if data is None:
519 if data is None:
518 self.opener.setflags(fname, islink, isexec)
520 self.opener.setflags(fname, islink, isexec)
519 return
521 return
520 if islink:
522 if islink:
521 self.opener.symlink(data, fname)
523 self.opener.symlink(data, fname)
522 else:
524 else:
523 self.opener.write(fname, data)
525 self.opener.write(fname, data)
524 if isexec:
526 if isexec:
525 self.opener.setflags(fname, False, True)
527 self.opener.setflags(fname, False, True)
526
528
527 def unlink(self, fname):
529 def unlink(self, fname):
528 rmdir = self.ui.configbool(b'experimental', b'removeemptydirs')
530 rmdir = self.ui.configbool(b'experimental', b'removeemptydirs')
529 self.opener.unlinkpath(fname, ignoremissing=True, rmdir=rmdir)
531 self.opener.unlinkpath(fname, ignoremissing=True, rmdir=rmdir)
530
532
531 def writerej(self, fname, failed, total, lines):
533 def writerej(self, fname, failed, total, lines):
532 fname = fname + b".rej"
534 fname = fname + b".rej"
533 self.ui.warn(
535 self.ui.warn(
534 _(b"%d out of %d hunks FAILED -- saving rejects to file %s\n")
536 _(b"%d out of %d hunks FAILED -- saving rejects to file %s\n")
535 % (failed, total, fname)
537 % (failed, total, fname)
536 )
538 )
537 fp = self.opener(fname, b'w')
539 fp = self.opener(fname, b'w')
538 fp.writelines(lines)
540 fp.writelines(lines)
539 fp.close()
541 fp.close()
540
542
541 def exists(self, fname):
543 def exists(self, fname):
542 return self.opener.lexists(fname)
544 return self.opener.lexists(fname)
543
545
544
546
545 class workingbackend(fsbackend):
547 class workingbackend(fsbackend):
546 def __init__(self, ui, repo, similarity):
548 def __init__(self, ui, repo, similarity):
547 super(workingbackend, self).__init__(ui, repo.root)
549 super(workingbackend, self).__init__(ui, repo.root)
548 self.repo = repo
550 self.repo = repo
549 self.similarity = similarity
551 self.similarity = similarity
550 self.removed = set()
552 self.removed = set()
551 self.changed = set()
553 self.changed = set()
552 self.copied = []
554 self.copied = []
553
555
554 def _checkknown(self, fname):
556 def _checkknown(self, fname):
555 if not self.repo.dirstate.get_entry(fname).any_tracked and self.exists(
557 if not self.repo.dirstate.get_entry(fname).any_tracked and self.exists(
556 fname
558 fname
557 ):
559 ):
558 raise PatchApplicationError(
560 raise PatchApplicationError(
559 _(b'cannot patch %s: file is not tracked') % fname
561 _(b'cannot patch %s: file is not tracked') % fname
560 )
562 )
561
563
562 def setfile(self, fname, data, mode, copysource):
564 def setfile(self, fname, data, mode, copysource):
563 self._checkknown(fname)
565 self._checkknown(fname)
564 super(workingbackend, self).setfile(fname, data, mode, copysource)
566 super(workingbackend, self).setfile(fname, data, mode, copysource)
565 if copysource is not None:
567 if copysource is not None:
566 self.copied.append((copysource, fname))
568 self.copied.append((copysource, fname))
567 self.changed.add(fname)
569 self.changed.add(fname)
568
570
569 def unlink(self, fname):
571 def unlink(self, fname):
570 self._checkknown(fname)
572 self._checkknown(fname)
571 super(workingbackend, self).unlink(fname)
573 super(workingbackend, self).unlink(fname)
572 self.removed.add(fname)
574 self.removed.add(fname)
573 self.changed.add(fname)
575 self.changed.add(fname)
574
576
575 def close(self):
577 def close(self):
576 wctx = self.repo[None]
578 wctx = self.repo[None]
577 changed = set(self.changed)
579 changed = set(self.changed)
578 for src, dst in self.copied:
580 for src, dst in self.copied:
579 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
581 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
580 if self.removed:
582 if self.removed:
581 wctx.forget(sorted(self.removed))
583 wctx.forget(sorted(self.removed))
582 for f in self.removed:
584 for f in self.removed:
583 if f not in self.repo.dirstate:
585 if f not in self.repo.dirstate:
584 # File was deleted and no longer belongs to the
586 # File was deleted and no longer belongs to the
585 # dirstate, it was probably marked added then
587 # dirstate, it was probably marked added then
586 # deleted, and should not be considered by
588 # deleted, and should not be considered by
587 # marktouched().
589 # marktouched().
588 changed.discard(f)
590 changed.discard(f)
589 if changed:
591 if changed:
590 scmutil.marktouched(self.repo, changed, self.similarity)
592 scmutil.marktouched(self.repo, changed, self.similarity)
591 return sorted(self.changed)
593 return sorted(self.changed)
592
594
593
595
594 class filestore(object):
596 class filestore(object):
595 def __init__(self, maxsize=None):
597 def __init__(self, maxsize=None):
596 self.opener = None
598 self.opener = None
597 self.files = {}
599 self.files = {}
598 self.created = 0
600 self.created = 0
599 self.maxsize = maxsize
601 self.maxsize = maxsize
600 if self.maxsize is None:
602 if self.maxsize is None:
601 self.maxsize = 4 * (2 ** 20)
603 self.maxsize = 4 * (2 ** 20)
602 self.size = 0
604 self.size = 0
603 self.data = {}
605 self.data = {}
604
606
605 def setfile(self, fname, data, mode, copied=None):
607 def setfile(self, fname, data, mode, copied=None):
606 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
608 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
607 self.data[fname] = (data, mode, copied)
609 self.data[fname] = (data, mode, copied)
608 self.size += len(data)
610 self.size += len(data)
609 else:
611 else:
610 if self.opener is None:
612 if self.opener is None:
611 root = pycompat.mkdtemp(prefix=b'hg-patch-')
613 root = pycompat.mkdtemp(prefix=b'hg-patch-')
612 self.opener = vfsmod.vfs(root)
614 self.opener = vfsmod.vfs(root)
613 # Avoid filename issues with these simple names
615 # Avoid filename issues with these simple names
614 fn = b'%d' % self.created
616 fn = b'%d' % self.created
615 self.opener.write(fn, data)
617 self.opener.write(fn, data)
616 self.created += 1
618 self.created += 1
617 self.files[fname] = (fn, mode, copied)
619 self.files[fname] = (fn, mode, copied)
618
620
619 def getfile(self, fname):
621 def getfile(self, fname):
620 if fname in self.data:
622 if fname in self.data:
621 return self.data[fname]
623 return self.data[fname]
622 if not self.opener or fname not in self.files:
624 if not self.opener or fname not in self.files:
623 return None, None, None
625 return None, None, None
624 fn, mode, copied = self.files[fname]
626 fn, mode, copied = self.files[fname]
625 return self.opener.read(fn), mode, copied
627 return self.opener.read(fn), mode, copied
626
628
627 def close(self):
629 def close(self):
628 if self.opener:
630 if self.opener:
629 shutil.rmtree(self.opener.base)
631 shutil.rmtree(self.opener.base)
630
632
631
633
632 class repobackend(abstractbackend):
634 class repobackend(abstractbackend):
633 def __init__(self, ui, repo, ctx, store):
635 def __init__(self, ui, repo, ctx, store):
634 super(repobackend, self).__init__(ui)
636 super(repobackend, self).__init__(ui)
635 self.repo = repo
637 self.repo = repo
636 self.ctx = ctx
638 self.ctx = ctx
637 self.store = store
639 self.store = store
638 self.changed = set()
640 self.changed = set()
639 self.removed = set()
641 self.removed = set()
640 self.copied = {}
642 self.copied = {}
641
643
642 def _checkknown(self, fname):
644 def _checkknown(self, fname):
643 if fname not in self.ctx:
645 if fname not in self.ctx:
644 raise PatchApplicationError(
646 raise PatchApplicationError(
645 _(b'cannot patch %s: file is not tracked') % fname
647 _(b'cannot patch %s: file is not tracked') % fname
646 )
648 )
647
649
648 def getfile(self, fname):
650 def getfile(self, fname):
649 try:
651 try:
650 fctx = self.ctx[fname]
652 fctx = self.ctx[fname]
651 except error.LookupError:
653 except error.LookupError:
652 return None, None
654 return None, None
653 flags = fctx.flags()
655 flags = fctx.flags()
654 return fctx.data(), (b'l' in flags, b'x' in flags)
656 return fctx.data(), (b'l' in flags, b'x' in flags)
655
657
656 def setfile(self, fname, data, mode, copysource):
658 def setfile(self, fname, data, mode, copysource):
657 if copysource:
659 if copysource:
658 self._checkknown(copysource)
660 self._checkknown(copysource)
659 if data is None:
661 if data is None:
660 data = self.ctx[fname].data()
662 data = self.ctx[fname].data()
661 self.store.setfile(fname, data, mode, copysource)
663 self.store.setfile(fname, data, mode, copysource)
662 self.changed.add(fname)
664 self.changed.add(fname)
663 if copysource:
665 if copysource:
664 self.copied[fname] = copysource
666 self.copied[fname] = copysource
665
667
666 def unlink(self, fname):
668 def unlink(self, fname):
667 self._checkknown(fname)
669 self._checkknown(fname)
668 self.removed.add(fname)
670 self.removed.add(fname)
669
671
670 def exists(self, fname):
672 def exists(self, fname):
671 return fname in self.ctx
673 return fname in self.ctx
672
674
673 def close(self):
675 def close(self):
674 return self.changed | self.removed
676 return self.changed | self.removed
675
677
676
678
677 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
679 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
678 unidesc = re.compile(br'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
680 unidesc = re.compile(br'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
679 contextdesc = re.compile(br'(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
681 contextdesc = re.compile(br'(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
680 eolmodes = [b'strict', b'crlf', b'lf', b'auto']
682 eolmodes = [b'strict', b'crlf', b'lf', b'auto']
681
683
682
684
683 class patchfile(object):
685 class patchfile(object):
684 def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
686 def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
685 self.fname = gp.path
687 self.fname = gp.path
686 self.eolmode = eolmode
688 self.eolmode = eolmode
687 self.eol = None
689 self.eol = None
688 self.backend = backend
690 self.backend = backend
689 self.ui = ui
691 self.ui = ui
690 self.lines = []
692 self.lines = []
691 self.exists = False
693 self.exists = False
692 self.missing = True
694 self.missing = True
693 self.mode = gp.mode
695 self.mode = gp.mode
694 self.copysource = gp.oldpath
696 self.copysource = gp.oldpath
695 self.create = gp.op in (b'ADD', b'COPY', b'RENAME')
697 self.create = gp.op in (b'ADD', b'COPY', b'RENAME')
696 self.remove = gp.op == b'DELETE'
698 self.remove = gp.op == b'DELETE'
697 if self.copysource is None:
699 if self.copysource is None:
698 data, mode = backend.getfile(self.fname)
700 data, mode = backend.getfile(self.fname)
699 else:
701 else:
700 data, mode = store.getfile(self.copysource)[:2]
702 data, mode = store.getfile(self.copysource)[:2]
701 if data is not None:
703 if data is not None:
702 self.exists = self.copysource is None or backend.exists(self.fname)
704 self.exists = self.copysource is None or backend.exists(self.fname)
703 self.missing = False
705 self.missing = False
704 if data:
706 if data:
705 self.lines = mdiff.splitnewlines(data)
707 self.lines = mdiff.splitnewlines(data)
706 if self.mode is None:
708 if self.mode is None:
707 self.mode = mode
709 self.mode = mode
708 if self.lines:
710 if self.lines:
709 # Normalize line endings
711 # Normalize line endings
710 if self.lines[0].endswith(b'\r\n'):
712 if self.lines[0].endswith(b'\r\n'):
711 self.eol = b'\r\n'
713 self.eol = b'\r\n'
712 elif self.lines[0].endswith(b'\n'):
714 elif self.lines[0].endswith(b'\n'):
713 self.eol = b'\n'
715 self.eol = b'\n'
714 if eolmode != b'strict':
716 if eolmode != b'strict':
715 nlines = []
717 nlines = []
716 for l in self.lines:
718 for l in self.lines:
717 if l.endswith(b'\r\n'):
719 if l.endswith(b'\r\n'):
718 l = l[:-2] + b'\n'
720 l = l[:-2] + b'\n'
719 nlines.append(l)
721 nlines.append(l)
720 self.lines = nlines
722 self.lines = nlines
721 else:
723 else:
722 if self.create:
724 if self.create:
723 self.missing = False
725 self.missing = False
724 if self.mode is None:
726 if self.mode is None:
725 self.mode = (False, False)
727 self.mode = (False, False)
726 if self.missing:
728 if self.missing:
727 self.ui.warn(_(b"unable to find '%s' for patching\n") % self.fname)
729 self.ui.warn(_(b"unable to find '%s' for patching\n") % self.fname)
728 self.ui.warn(
730 self.ui.warn(
729 _(
731 _(
730 b"(use '--prefix' to apply patch relative to the "
732 b"(use '--prefix' to apply patch relative to the "
731 b"current directory)\n"
733 b"current directory)\n"
732 )
734 )
733 )
735 )
734
736
735 self.hash = {}
737 self.hash = {}
736 self.dirty = 0
738 self.dirty = 0
737 self.offset = 0
739 self.offset = 0
738 self.skew = 0
740 self.skew = 0
739 self.rej = []
741 self.rej = []
740 self.fileprinted = False
742 self.fileprinted = False
741 self.printfile(False)
743 self.printfile(False)
742 self.hunks = 0
744 self.hunks = 0
743
745
744 def writelines(self, fname, lines, mode):
746 def writelines(self, fname, lines, mode):
745 if self.eolmode == b'auto':
747 if self.eolmode == b'auto':
746 eol = self.eol
748 eol = self.eol
747 elif self.eolmode == b'crlf':
749 elif self.eolmode == b'crlf':
748 eol = b'\r\n'
750 eol = b'\r\n'
749 else:
751 else:
750 eol = b'\n'
752 eol = b'\n'
751
753
752 if self.eolmode != b'strict' and eol and eol != b'\n':
754 if self.eolmode != b'strict' and eol and eol != b'\n':
753 rawlines = []
755 rawlines = []
754 for l in lines:
756 for l in lines:
755 if l and l.endswith(b'\n'):
757 if l and l.endswith(b'\n'):
756 l = l[:-1] + eol
758 l = l[:-1] + eol
757 rawlines.append(l)
759 rawlines.append(l)
758 lines = rawlines
760 lines = rawlines
759
761
760 self.backend.setfile(fname, b''.join(lines), mode, self.copysource)
762 self.backend.setfile(fname, b''.join(lines), mode, self.copysource)
761
763
762 def printfile(self, warn):
764 def printfile(self, warn):
763 if self.fileprinted:
765 if self.fileprinted:
764 return
766 return
765 if warn or self.ui.verbose:
767 if warn or self.ui.verbose:
766 self.fileprinted = True
768 self.fileprinted = True
767 s = _(b"patching file %s\n") % self.fname
769 s = _(b"patching file %s\n") % self.fname
768 if warn:
770 if warn:
769 self.ui.warn(s)
771 self.ui.warn(s)
770 else:
772 else:
771 self.ui.note(s)
773 self.ui.note(s)
772
774
773 def findlines(self, l, linenum):
775 def findlines(self, l, linenum):
774 # looks through the hash and finds candidate lines. The
776 # looks through the hash and finds candidate lines. The
775 # result is a list of line numbers sorted based on distance
777 # result is a list of line numbers sorted based on distance
776 # from linenum
778 # from linenum
777
779
778 cand = self.hash.get(l, [])
780 cand = self.hash.get(l, [])
779 if len(cand) > 1:
781 if len(cand) > 1:
780 # resort our list of potentials forward then back.
782 # resort our list of potentials forward then back.
781 cand.sort(key=lambda x: abs(x - linenum))
783 cand.sort(key=lambda x: abs(x - linenum))
782 return cand
784 return cand
783
785
784 def write_rej(self):
786 def write_rej(self):
785 # our rejects are a little different from patch(1). This always
787 # our rejects are a little different from patch(1). This always
786 # creates rejects in the same form as the original patch. A file
788 # creates rejects in the same form as the original patch. A file
787 # header is inserted so that you can run the reject through patch again
789 # header is inserted so that you can run the reject through patch again
788 # without having to type the filename.
790 # without having to type the filename.
789 if not self.rej:
791 if not self.rej:
790 return
792 return
791 base = os.path.basename(self.fname)
793 base = os.path.basename(self.fname)
792 lines = [b"--- %s\n+++ %s\n" % (base, base)]
794 lines = [b"--- %s\n+++ %s\n" % (base, base)]
793 for x in self.rej:
795 for x in self.rej:
794 for l in x.hunk:
796 for l in x.hunk:
795 lines.append(l)
797 lines.append(l)
796 if l[-1:] != b'\n':
798 if l[-1:] != b'\n':
797 lines.append(b'\n' + diffhelper.MISSING_NEWLINE_MARKER)
799 lines.append(b'\n' + diffhelper.MISSING_NEWLINE_MARKER)
798 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
800 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
799
801
800 def apply(self, h):
802 def apply(self, h):
801 if not h.complete():
803 if not h.complete():
802 raise PatchParseError(
804 raise PatchParseError(
803 _(b"bad hunk #%d %s (%d %d %d %d)")
805 _(b"bad hunk #%d %s (%d %d %d %d)")
804 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
806 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
805 )
807 )
806
808
807 self.hunks += 1
809 self.hunks += 1
808
810
809 if self.missing:
811 if self.missing:
810 self.rej.append(h)
812 self.rej.append(h)
811 return -1
813 return -1
812
814
813 if self.exists and self.create:
815 if self.exists and self.create:
814 if self.copysource:
816 if self.copysource:
815 self.ui.warn(
817 self.ui.warn(
816 _(b"cannot create %s: destination already exists\n")
818 _(b"cannot create %s: destination already exists\n")
817 % self.fname
819 % self.fname
818 )
820 )
819 else:
821 else:
820 self.ui.warn(_(b"file %s already exists\n") % self.fname)
822 self.ui.warn(_(b"file %s already exists\n") % self.fname)
821 self.rej.append(h)
823 self.rej.append(h)
822 return -1
824 return -1
823
825
824 if isinstance(h, binhunk):
826 if isinstance(h, binhunk):
825 if self.remove:
827 if self.remove:
826 self.backend.unlink(self.fname)
828 self.backend.unlink(self.fname)
827 else:
829 else:
828 l = h.new(self.lines)
830 l = h.new(self.lines)
829 self.lines[:] = l
831 self.lines[:] = l
830 self.offset += len(l)
832 self.offset += len(l)
831 self.dirty = True
833 self.dirty = True
832 return 0
834 return 0
833
835
834 horig = h
836 horig = h
835 if (
837 if (
836 self.eolmode in (b'crlf', b'lf')
838 self.eolmode in (b'crlf', b'lf')
837 or self.eolmode == b'auto'
839 or self.eolmode == b'auto'
838 and self.eol
840 and self.eol
839 ):
841 ):
840 # If new eols are going to be normalized, then normalize
842 # If new eols are going to be normalized, then normalize
841 # hunk data before patching. Otherwise, preserve input
843 # hunk data before patching. Otherwise, preserve input
842 # line-endings.
844 # line-endings.
843 h = h.getnormalized()
845 h = h.getnormalized()
844
846
845 # fast case first, no offsets, no fuzz
847 # fast case first, no offsets, no fuzz
846 old, oldstart, new, newstart = h.fuzzit(0, False)
848 old, oldstart, new, newstart = h.fuzzit(0, False)
847 oldstart += self.offset
849 oldstart += self.offset
848 orig_start = oldstart
850 orig_start = oldstart
849 # if there's skew we want to emit the "(offset %d lines)" even
851 # if there's skew we want to emit the "(offset %d lines)" even
850 # when the hunk cleanly applies at start + skew, so skip the
852 # when the hunk cleanly applies at start + skew, so skip the
851 # fast case code
853 # fast case code
852 if self.skew == 0 and diffhelper.testhunk(old, self.lines, oldstart):
854 if self.skew == 0 and diffhelper.testhunk(old, self.lines, oldstart):
853 if self.remove:
855 if self.remove:
854 self.backend.unlink(self.fname)
856 self.backend.unlink(self.fname)
855 else:
857 else:
856 self.lines[oldstart : oldstart + len(old)] = new
858 self.lines[oldstart : oldstart + len(old)] = new
857 self.offset += len(new) - len(old)
859 self.offset += len(new) - len(old)
858 self.dirty = True
860 self.dirty = True
859 return 0
861 return 0
860
862
861 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
863 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
862 self.hash = {}
864 self.hash = {}
863 for x, s in enumerate(self.lines):
865 for x, s in enumerate(self.lines):
864 self.hash.setdefault(s, []).append(x)
866 self.hash.setdefault(s, []).append(x)
865
867
866 for fuzzlen in pycompat.xrange(
868 for fuzzlen in pycompat.xrange(
867 self.ui.configint(b"patch", b"fuzz") + 1
869 self.ui.configint(b"patch", b"fuzz") + 1
868 ):
870 ):
869 for toponly in [True, False]:
871 for toponly in [True, False]:
870 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
872 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
871 oldstart = oldstart + self.offset + self.skew
873 oldstart = oldstart + self.offset + self.skew
872 oldstart = min(oldstart, len(self.lines))
874 oldstart = min(oldstart, len(self.lines))
873 if old:
875 if old:
874 cand = self.findlines(old[0][1:], oldstart)
876 cand = self.findlines(old[0][1:], oldstart)
875 else:
877 else:
876 # Only adding lines with no or fuzzed context, just
878 # Only adding lines with no or fuzzed context, just
877 # take the skew in account
879 # take the skew in account
878 cand = [oldstart]
880 cand = [oldstart]
879
881
880 for l in cand:
882 for l in cand:
881 if not old or diffhelper.testhunk(old, self.lines, l):
883 if not old or diffhelper.testhunk(old, self.lines, l):
882 self.lines[l : l + len(old)] = new
884 self.lines[l : l + len(old)] = new
883 self.offset += len(new) - len(old)
885 self.offset += len(new) - len(old)
884 self.skew = l - orig_start
886 self.skew = l - orig_start
885 self.dirty = True
887 self.dirty = True
886 offset = l - orig_start - fuzzlen
888 offset = l - orig_start - fuzzlen
887 if fuzzlen:
889 if fuzzlen:
888 msg = _(
890 msg = _(
889 b"Hunk #%d succeeded at %d "
891 b"Hunk #%d succeeded at %d "
890 b"with fuzz %d "
892 b"with fuzz %d "
891 b"(offset %d lines).\n"
893 b"(offset %d lines).\n"
892 )
894 )
893 self.printfile(True)
895 self.printfile(True)
894 self.ui.warn(
896 self.ui.warn(
895 msg % (h.number, l + 1, fuzzlen, offset)
897 msg % (h.number, l + 1, fuzzlen, offset)
896 )
898 )
897 else:
899 else:
898 msg = _(
900 msg = _(
899 b"Hunk #%d succeeded at %d "
901 b"Hunk #%d succeeded at %d "
900 b"(offset %d lines).\n"
902 b"(offset %d lines).\n"
901 )
903 )
902 self.ui.note(msg % (h.number, l + 1, offset))
904 self.ui.note(msg % (h.number, l + 1, offset))
903 return fuzzlen
905 return fuzzlen
904 self.printfile(True)
906 self.printfile(True)
905 self.ui.warn(_(b"Hunk #%d FAILED at %d\n") % (h.number, orig_start))
907 self.ui.warn(_(b"Hunk #%d FAILED at %d\n") % (h.number, orig_start))
906 self.rej.append(horig)
908 self.rej.append(horig)
907 return -1
909 return -1
908
910
909 def close(self):
911 def close(self):
910 if self.dirty:
912 if self.dirty:
911 self.writelines(self.fname, self.lines, self.mode)
913 self.writelines(self.fname, self.lines, self.mode)
912 self.write_rej()
914 self.write_rej()
913 return len(self.rej)
915 return len(self.rej)
914
916
915
917
916 class header(object):
918 class header(object):
917 """patch header"""
919 """patch header"""
918
920
919 diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$')
921 diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$')
920 diff_re = re.compile(b'diff -r .* (.*)$')
922 diff_re = re.compile(b'diff -r .* (.*)$')
921 allhunks_re = re.compile(b'(?:index|deleted file) ')
923 allhunks_re = re.compile(b'(?:index|deleted file) ')
922 pretty_re = re.compile(b'(?:new file|deleted file) ')
924 pretty_re = re.compile(b'(?:new file|deleted file) ')
923 special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ')
925 special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ')
924 newfile_re = re.compile(b'(?:new file|copy to|rename to)')
926 newfile_re = re.compile(b'(?:new file|copy to|rename to)')
925
927
926 def __init__(self, header):
928 def __init__(self, header):
927 self.header = header
929 self.header = header
928 self.hunks = []
930 self.hunks = []
929
931
930 def binary(self):
932 def binary(self):
931 return any(h.startswith(b'index ') for h in self.header)
933 return any(h.startswith(b'index ') for h in self.header)
932
934
933 def pretty(self, fp):
935 def pretty(self, fp):
934 for h in self.header:
936 for h in self.header:
935 if h.startswith(b'index '):
937 if h.startswith(b'index '):
936 fp.write(_(b'this modifies a binary file (all or nothing)\n'))
938 fp.write(_(b'this modifies a binary file (all or nothing)\n'))
937 break
939 break
938 if self.pretty_re.match(h):
940 if self.pretty_re.match(h):
939 fp.write(h)
941 fp.write(h)
940 if self.binary():
942 if self.binary():
941 fp.write(_(b'this is a binary file\n'))
943 fp.write(_(b'this is a binary file\n'))
942 break
944 break
943 if h.startswith(b'---'):
945 if h.startswith(b'---'):
944 fp.write(
946 fp.write(
945 _(b'%d hunks, %d lines changed\n')
947 _(b'%d hunks, %d lines changed\n')
946 % (
948 % (
947 len(self.hunks),
949 len(self.hunks),
948 sum([max(h.added, h.removed) for h in self.hunks]),
950 sum([max(h.added, h.removed) for h in self.hunks]),
949 )
951 )
950 )
952 )
951 break
953 break
952 fp.write(h)
954 fp.write(h)
953
955
954 def write(self, fp):
956 def write(self, fp):
955 fp.write(b''.join(self.header))
957 fp.write(b''.join(self.header))
956
958
957 def allhunks(self):
959 def allhunks(self):
958 return any(self.allhunks_re.match(h) for h in self.header)
960 return any(self.allhunks_re.match(h) for h in self.header)
959
961
960 def files(self):
962 def files(self):
961 match = self.diffgit_re.match(self.header[0])
963 match = self.diffgit_re.match(self.header[0])
962 if match:
964 if match:
963 fromfile, tofile = match.groups()
965 fromfile, tofile = match.groups()
964 if fromfile == tofile:
966 if fromfile == tofile:
965 return [fromfile]
967 return [fromfile]
966 return [fromfile, tofile]
968 return [fromfile, tofile]
967 else:
969 else:
968 return self.diff_re.match(self.header[0]).groups()
970 return self.diff_re.match(self.header[0]).groups()
969
971
970 def filename(self):
972 def filename(self):
971 return self.files()[-1]
973 return self.files()[-1]
972
974
973 def __repr__(self):
975 def __repr__(self):
974 return '<header %s>' % (
976 return '<header %s>' % (
975 ' '.join(pycompat.rapply(pycompat.fsdecode, self.files()))
977 ' '.join(pycompat.rapply(pycompat.fsdecode, self.files()))
976 )
978 )
977
979
978 def isnewfile(self):
980 def isnewfile(self):
979 return any(self.newfile_re.match(h) for h in self.header)
981 return any(self.newfile_re.match(h) for h in self.header)
980
982
981 def special(self):
983 def special(self):
982 # Special files are shown only at the header level and not at the hunk
984 # Special files are shown only at the header level and not at the hunk
983 # level for example a file that has been deleted is a special file.
985 # level for example a file that has been deleted is a special file.
984 # The user cannot change the content of the operation, in the case of
986 # The user cannot change the content of the operation, in the case of
985 # the deleted file he has to take the deletion or not take it, he
987 # the deleted file he has to take the deletion or not take it, he
986 # cannot take some of it.
988 # cannot take some of it.
987 # Newly added files are special if they are empty, they are not special
989 # Newly added files are special if they are empty, they are not special
988 # if they have some content as we want to be able to change it
990 # if they have some content as we want to be able to change it
989 nocontent = len(self.header) == 2
991 nocontent = len(self.header) == 2
990 emptynewfile = self.isnewfile() and nocontent
992 emptynewfile = self.isnewfile() and nocontent
991 return emptynewfile or any(
993 return emptynewfile or any(
992 self.special_re.match(h) for h in self.header
994 self.special_re.match(h) for h in self.header
993 )
995 )
994
996
995
997
996 class recordhunk(object):
998 class recordhunk(object):
997 """patch hunk
999 """patch hunk
998
1000
999 XXX shouldn't we merge this with the other hunk class?
1001 XXX shouldn't we merge this with the other hunk class?
1000 """
1002 """
1001
1003
1002 def __init__(
1004 def __init__(
1003 self,
1005 self,
1004 header,
1006 header,
1005 fromline,
1007 fromline,
1006 toline,
1008 toline,
1007 proc,
1009 proc,
1008 before,
1010 before,
1009 hunk,
1011 hunk,
1010 after,
1012 after,
1011 maxcontext=None,
1013 maxcontext=None,
1012 ):
1014 ):
1013 def trimcontext(lines, reverse=False):
1015 def trimcontext(lines, reverse=False):
1014 if maxcontext is not None:
1016 if maxcontext is not None:
1015 delta = len(lines) - maxcontext
1017 delta = len(lines) - maxcontext
1016 if delta > 0:
1018 if delta > 0:
1017 if reverse:
1019 if reverse:
1018 return delta, lines[delta:]
1020 return delta, lines[delta:]
1019 else:
1021 else:
1020 return delta, lines[:maxcontext]
1022 return delta, lines[:maxcontext]
1021 return 0, lines
1023 return 0, lines
1022
1024
1023 self.header = header
1025 self.header = header
1024 trimedbefore, self.before = trimcontext(before, True)
1026 trimedbefore, self.before = trimcontext(before, True)
1025 self.fromline = fromline + trimedbefore
1027 self.fromline = fromline + trimedbefore
1026 self.toline = toline + trimedbefore
1028 self.toline = toline + trimedbefore
1027 _trimedafter, self.after = trimcontext(after, False)
1029 _trimedafter, self.after = trimcontext(after, False)
1028 self.proc = proc
1030 self.proc = proc
1029 self.hunk = hunk
1031 self.hunk = hunk
1030 self.added, self.removed = self.countchanges(self.hunk)
1032 self.added, self.removed = self.countchanges(self.hunk)
1031
1033
1032 def __eq__(self, v):
1034 def __eq__(self, v):
1033 if not isinstance(v, recordhunk):
1035 if not isinstance(v, recordhunk):
1034 return False
1036 return False
1035
1037
1036 return (
1038 return (
1037 (v.hunk == self.hunk)
1039 (v.hunk == self.hunk)
1038 and (v.proc == self.proc)
1040 and (v.proc == self.proc)
1039 and (self.fromline == v.fromline)
1041 and (self.fromline == v.fromline)
1040 and (self.header.files() == v.header.files())
1042 and (self.header.files() == v.header.files())
1041 )
1043 )
1042
1044
1043 def __hash__(self):
1045 def __hash__(self):
1044 return hash(
1046 return hash(
1045 (
1047 (
1046 tuple(self.hunk),
1048 tuple(self.hunk),
1047 tuple(self.header.files()),
1049 tuple(self.header.files()),
1048 self.fromline,
1050 self.fromline,
1049 self.proc,
1051 self.proc,
1050 )
1052 )
1051 )
1053 )
1052
1054
1053 def countchanges(self, hunk):
1055 def countchanges(self, hunk):
1054 """hunk -> (n+,n-)"""
1056 """hunk -> (n+,n-)"""
1055 add = len([h for h in hunk if h.startswith(b'+')])
1057 add = len([h for h in hunk if h.startswith(b'+')])
1056 rem = len([h for h in hunk if h.startswith(b'-')])
1058 rem = len([h for h in hunk if h.startswith(b'-')])
1057 return add, rem
1059 return add, rem
1058
1060
1059 def reversehunk(self):
1061 def reversehunk(self):
1060 """return another recordhunk which is the reverse of the hunk
1062 """return another recordhunk which is the reverse of the hunk
1061
1063
1062 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
1064 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
1063 that, swap fromline/toline and +/- signs while keep other things
1065 that, swap fromline/toline and +/- signs while keep other things
1064 unchanged.
1066 unchanged.
1065 """
1067 """
1066 m = {b'+': b'-', b'-': b'+', b'\\': b'\\'}
1068 m = {b'+': b'-', b'-': b'+', b'\\': b'\\'}
1067 hunk = [b'%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
1069 hunk = [b'%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
1068 return recordhunk(
1070 return recordhunk(
1069 self.header,
1071 self.header,
1070 self.toline,
1072 self.toline,
1071 self.fromline,
1073 self.fromline,
1072 self.proc,
1074 self.proc,
1073 self.before,
1075 self.before,
1074 hunk,
1076 hunk,
1075 self.after,
1077 self.after,
1076 )
1078 )
1077
1079
1078 def write(self, fp):
1080 def write(self, fp):
1079 delta = len(self.before) + len(self.after)
1081 delta = len(self.before) + len(self.after)
1080 if self.after and self.after[-1] == diffhelper.MISSING_NEWLINE_MARKER:
1082 if self.after and self.after[-1] == diffhelper.MISSING_NEWLINE_MARKER:
1081 delta -= 1
1083 delta -= 1
1082 fromlen = delta + self.removed
1084 fromlen = delta + self.removed
1083 tolen = delta + self.added
1085 tolen = delta + self.added
1084 fp.write(
1086 fp.write(
1085 b'@@ -%d,%d +%d,%d @@%s\n'
1087 b'@@ -%d,%d +%d,%d @@%s\n'
1086 % (
1088 % (
1087 self.fromline,
1089 self.fromline,
1088 fromlen,
1090 fromlen,
1089 self.toline,
1091 self.toline,
1090 tolen,
1092 tolen,
1091 self.proc and (b' ' + self.proc),
1093 self.proc and (b' ' + self.proc),
1092 )
1094 )
1093 )
1095 )
1094 fp.write(b''.join(self.before + self.hunk + self.after))
1096 fp.write(b''.join(self.before + self.hunk + self.after))
1095
1097
1096 pretty = write
1098 pretty = write
1097
1099
1098 def filename(self):
1100 def filename(self):
1099 return self.header.filename()
1101 return self.header.filename()
1100
1102
1101 @encoding.strmethod
1103 @encoding.strmethod
1102 def __repr__(self):
1104 def __repr__(self):
1103 return b'<hunk %r@%d>' % (self.filename(), self.fromline)
1105 return b'<hunk %r@%d>' % (self.filename(), self.fromline)
1104
1106
1105
1107
1106 def getmessages():
1108 def getmessages():
1107 return {
1109 return {
1108 b'multiple': {
1110 b'multiple': {
1109 b'apply': _(b"apply change %d/%d to '%s'?"),
1111 b'apply': _(b"apply change %d/%d to '%s'?"),
1110 b'discard': _(b"discard change %d/%d to '%s'?"),
1112 b'discard': _(b"discard change %d/%d to '%s'?"),
1111 b'keep': _(b"keep change %d/%d to '%s'?"),
1113 b'keep': _(b"keep change %d/%d to '%s'?"),
1112 b'record': _(b"record change %d/%d to '%s'?"),
1114 b'record': _(b"record change %d/%d to '%s'?"),
1113 },
1115 },
1114 b'single': {
1116 b'single': {
1115 b'apply': _(b"apply this change to '%s'?"),
1117 b'apply': _(b"apply this change to '%s'?"),
1116 b'discard': _(b"discard this change to '%s'?"),
1118 b'discard': _(b"discard this change to '%s'?"),
1117 b'keep': _(b"keep this change to '%s'?"),
1119 b'keep': _(b"keep this change to '%s'?"),
1118 b'record': _(b"record this change to '%s'?"),
1120 b'record': _(b"record this change to '%s'?"),
1119 },
1121 },
1120 b'help': {
1122 b'help': {
1121 b'apply': _(
1123 b'apply': _(
1122 b'[Ynesfdaq?]'
1124 b'[Ynesfdaq?]'
1123 b'$$ &Yes, apply this change'
1125 b'$$ &Yes, apply this change'
1124 b'$$ &No, skip this change'
1126 b'$$ &No, skip this change'
1125 b'$$ &Edit this change manually'
1127 b'$$ &Edit this change manually'
1126 b'$$ &Skip remaining changes to this file'
1128 b'$$ &Skip remaining changes to this file'
1127 b'$$ Apply remaining changes to this &file'
1129 b'$$ Apply remaining changes to this &file'
1128 b'$$ &Done, skip remaining changes and files'
1130 b'$$ &Done, skip remaining changes and files'
1129 b'$$ Apply &all changes to all remaining files'
1131 b'$$ Apply &all changes to all remaining files'
1130 b'$$ &Quit, applying no changes'
1132 b'$$ &Quit, applying no changes'
1131 b'$$ &? (display help)'
1133 b'$$ &? (display help)'
1132 ),
1134 ),
1133 b'discard': _(
1135 b'discard': _(
1134 b'[Ynesfdaq?]'
1136 b'[Ynesfdaq?]'
1135 b'$$ &Yes, discard this change'
1137 b'$$ &Yes, discard this change'
1136 b'$$ &No, skip this change'
1138 b'$$ &No, skip this change'
1137 b'$$ &Edit this change manually'
1139 b'$$ &Edit this change manually'
1138 b'$$ &Skip remaining changes to this file'
1140 b'$$ &Skip remaining changes to this file'
1139 b'$$ Discard remaining changes to this &file'
1141 b'$$ Discard remaining changes to this &file'
1140 b'$$ &Done, skip remaining changes and files'
1142 b'$$ &Done, skip remaining changes and files'
1141 b'$$ Discard &all changes to all remaining files'
1143 b'$$ Discard &all changes to all remaining files'
1142 b'$$ &Quit, discarding no changes'
1144 b'$$ &Quit, discarding no changes'
1143 b'$$ &? (display help)'
1145 b'$$ &? (display help)'
1144 ),
1146 ),
1145 b'keep': _(
1147 b'keep': _(
1146 b'[Ynesfdaq?]'
1148 b'[Ynesfdaq?]'
1147 b'$$ &Yes, keep this change'
1149 b'$$ &Yes, keep this change'
1148 b'$$ &No, skip this change'
1150 b'$$ &No, skip this change'
1149 b'$$ &Edit this change manually'
1151 b'$$ &Edit this change manually'
1150 b'$$ &Skip remaining changes to this file'
1152 b'$$ &Skip remaining changes to this file'
1151 b'$$ Keep remaining changes to this &file'
1153 b'$$ Keep remaining changes to this &file'
1152 b'$$ &Done, skip remaining changes and files'
1154 b'$$ &Done, skip remaining changes and files'
1153 b'$$ Keep &all changes to all remaining files'
1155 b'$$ Keep &all changes to all remaining files'
1154 b'$$ &Quit, keeping all changes'
1156 b'$$ &Quit, keeping all changes'
1155 b'$$ &? (display help)'
1157 b'$$ &? (display help)'
1156 ),
1158 ),
1157 b'record': _(
1159 b'record': _(
1158 b'[Ynesfdaq?]'
1160 b'[Ynesfdaq?]'
1159 b'$$ &Yes, record this change'
1161 b'$$ &Yes, record this change'
1160 b'$$ &No, skip this change'
1162 b'$$ &No, skip this change'
1161 b'$$ &Edit this change manually'
1163 b'$$ &Edit this change manually'
1162 b'$$ &Skip remaining changes to this file'
1164 b'$$ &Skip remaining changes to this file'
1163 b'$$ Record remaining changes to this &file'
1165 b'$$ Record remaining changes to this &file'
1164 b'$$ &Done, skip remaining changes and files'
1166 b'$$ &Done, skip remaining changes and files'
1165 b'$$ Record &all changes to all remaining files'
1167 b'$$ Record &all changes to all remaining files'
1166 b'$$ &Quit, recording no changes'
1168 b'$$ &Quit, recording no changes'
1167 b'$$ &? (display help)'
1169 b'$$ &? (display help)'
1168 ),
1170 ),
1169 },
1171 },
1170 }
1172 }
1171
1173
1172
1174
1173 def filterpatch(ui, headers, match, operation=None):
1175 def filterpatch(ui, headers, match, operation=None):
1174 """Interactively filter patch chunks into applied-only chunks"""
1176 """Interactively filter patch chunks into applied-only chunks"""
1175 messages = getmessages()
1177 messages = getmessages()
1176
1178
1177 if operation is None:
1179 if operation is None:
1178 operation = b'record'
1180 operation = b'record'
1179
1181
1180 def prompt(skipfile, skipall, query, chunk):
1182 def prompt(skipfile, skipall, query, chunk):
1181 """prompt query, and process base inputs
1183 """prompt query, and process base inputs
1182
1184
1183 - y/n for the rest of file
1185 - y/n for the rest of file
1184 - y/n for the rest
1186 - y/n for the rest
1185 - ? (help)
1187 - ? (help)
1186 - q (quit)
1188 - q (quit)
1187
1189
1188 Return True/False and possibly updated skipfile and skipall.
1190 Return True/False and possibly updated skipfile and skipall.
1189 """
1191 """
1190 newpatches = None
1192 newpatches = None
1191 if skipall is not None:
1193 if skipall is not None:
1192 return skipall, skipfile, skipall, newpatches
1194 return skipall, skipfile, skipall, newpatches
1193 if skipfile is not None:
1195 if skipfile is not None:
1194 return skipfile, skipfile, skipall, newpatches
1196 return skipfile, skipfile, skipall, newpatches
1195 while True:
1197 while True:
1196 resps = messages[b'help'][operation]
1198 resps = messages[b'help'][operation]
1197 # IMPORTANT: keep the last line of this prompt short (<40 english
1199 # IMPORTANT: keep the last line of this prompt short (<40 english
1198 # chars is a good target) because of issue6158.
1200 # chars is a good target) because of issue6158.
1199 r = ui.promptchoice(b"%s\n(enter ? for help) %s" % (query, resps))
1201 r = ui.promptchoice(b"%s\n(enter ? for help) %s" % (query, resps))
1200 ui.write(b"\n")
1202 ui.write(b"\n")
1201 if r == 8: # ?
1203 if r == 8: # ?
1202 for c, t in ui.extractchoices(resps)[1]:
1204 for c, t in ui.extractchoices(resps)[1]:
1203 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
1205 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
1204 continue
1206 continue
1205 elif r == 0: # yes
1207 elif r == 0: # yes
1206 ret = True
1208 ret = True
1207 elif r == 1: # no
1209 elif r == 1: # no
1208 ret = False
1210 ret = False
1209 elif r == 2: # Edit patch
1211 elif r == 2: # Edit patch
1210 if chunk is None:
1212 if chunk is None:
1211 ui.write(_(b'cannot edit patch for whole file'))
1213 ui.write(_(b'cannot edit patch for whole file'))
1212 ui.write(b"\n")
1214 ui.write(b"\n")
1213 continue
1215 continue
1214 if chunk.header.binary():
1216 if chunk.header.binary():
1215 ui.write(_(b'cannot edit patch for binary file'))
1217 ui.write(_(b'cannot edit patch for binary file'))
1216 ui.write(b"\n")
1218 ui.write(b"\n")
1217 continue
1219 continue
1218 # Patch comment based on the Git one (based on comment at end of
1220 # Patch comment based on the Git one (based on comment at end of
1219 # https://mercurial-scm.org/wiki/RecordExtension)
1221 # https://mercurial-scm.org/wiki/RecordExtension)
1220 phelp = b'---' + _(
1222 phelp = b'---' + _(
1221 b"""
1223 b"""
1222 To remove '-' lines, make them ' ' lines (context).
1224 To remove '-' lines, make them ' ' lines (context).
1223 To remove '+' lines, delete them.
1225 To remove '+' lines, delete them.
1224 Lines starting with # will be removed from the patch.
1226 Lines starting with # will be removed from the patch.
1225
1227
1226 If the patch applies cleanly, the edited hunk will immediately be
1228 If the patch applies cleanly, the edited hunk will immediately be
1227 added to the record list. If it does not apply cleanly, a rejects
1229 added to the record list. If it does not apply cleanly, a rejects
1228 file will be generated: you can use that when you try again. If
1230 file will be generated: you can use that when you try again. If
1229 all lines of the hunk are removed, then the edit is aborted and
1231 all lines of the hunk are removed, then the edit is aborted and
1230 the hunk is left unchanged.
1232 the hunk is left unchanged.
1231 """
1233 """
1232 )
1234 )
1233 (patchfd, patchfn) = pycompat.mkstemp(
1235 (patchfd, patchfn) = pycompat.mkstemp(
1234 prefix=b"hg-editor-", suffix=b".diff"
1236 prefix=b"hg-editor-", suffix=b".diff"
1235 )
1237 )
1236 ncpatchfp = None
1238 ncpatchfp = None
1237 try:
1239 try:
1238 # Write the initial patch
1240 # Write the initial patch
1239 f = util.nativeeolwriter(os.fdopen(patchfd, 'wb'))
1241 f = util.nativeeolwriter(os.fdopen(patchfd, 'wb'))
1240 chunk.header.write(f)
1242 chunk.header.write(f)
1241 chunk.write(f)
1243 chunk.write(f)
1242 f.write(
1244 f.write(
1243 b''.join(
1245 b''.join(
1244 [b'# ' + i + b'\n' for i in phelp.splitlines()]
1246 [b'# ' + i + b'\n' for i in phelp.splitlines()]
1245 )
1247 )
1246 )
1248 )
1247 f.close()
1249 f.close()
1248 # Start the editor and wait for it to complete
1250 # Start the editor and wait for it to complete
1249 editor = ui.geteditor()
1251 editor = ui.geteditor()
1250 ret = ui.system(
1252 ret = ui.system(
1251 b"%s \"%s\"" % (editor, patchfn),
1253 b"%s \"%s\"" % (editor, patchfn),
1252 environ={b'HGUSER': ui.username()},
1254 environ={b'HGUSER': ui.username()},
1253 blockedtag=b'filterpatch',
1255 blockedtag=b'filterpatch',
1254 )
1256 )
1255 if ret != 0:
1257 if ret != 0:
1256 ui.warn(_(b"editor exited with exit code %d\n") % ret)
1258 ui.warn(_(b"editor exited with exit code %d\n") % ret)
1257 continue
1259 continue
1258 # Remove comment lines
1260 # Remove comment lines
1259 patchfp = open(patchfn, 'rb')
1261 patchfp = open(patchfn, 'rb')
1260 ncpatchfp = stringio()
1262 ncpatchfp = stringio()
1261 for line in util.iterfile(patchfp):
1263 for line in util.iterfile(patchfp):
1262 line = util.fromnativeeol(line)
1264 line = util.fromnativeeol(line)
1263 if not line.startswith(b'#'):
1265 if not line.startswith(b'#'):
1264 ncpatchfp.write(line)
1266 ncpatchfp.write(line)
1265 patchfp.close()
1267 patchfp.close()
1266 ncpatchfp.seek(0)
1268 ncpatchfp.seek(0)
1267 newpatches = parsepatch(ncpatchfp)
1269 newpatches = parsepatch(ncpatchfp)
1268 finally:
1270 finally:
1269 os.unlink(patchfn)
1271 os.unlink(patchfn)
1270 del ncpatchfp
1272 del ncpatchfp
1271 # Signal that the chunk shouldn't be applied as-is, but
1273 # Signal that the chunk shouldn't be applied as-is, but
1272 # provide the new patch to be used instead.
1274 # provide the new patch to be used instead.
1273 ret = False
1275 ret = False
1274 elif r == 3: # Skip
1276 elif r == 3: # Skip
1275 ret = skipfile = False
1277 ret = skipfile = False
1276 elif r == 4: # file (Record remaining)
1278 elif r == 4: # file (Record remaining)
1277 ret = skipfile = True
1279 ret = skipfile = True
1278 elif r == 5: # done, skip remaining
1280 elif r == 5: # done, skip remaining
1279 ret = skipall = False
1281 ret = skipall = False
1280 elif r == 6: # all
1282 elif r == 6: # all
1281 ret = skipall = True
1283 ret = skipall = True
1282 elif r == 7: # quit
1284 elif r == 7: # quit
1283 raise error.CanceledError(_(b'user quit'))
1285 raise error.CanceledError(_(b'user quit'))
1284 return ret, skipfile, skipall, newpatches
1286 return ret, skipfile, skipall, newpatches
1285
1287
1286 seen = set()
1288 seen = set()
1287 applied = {} # 'filename' -> [] of chunks
1289 applied = {} # 'filename' -> [] of chunks
1288 skipfile, skipall = None, None
1290 skipfile, skipall = None, None
1289 pos, total = 1, sum(len(h.hunks) for h in headers)
1291 pos, total = 1, sum(len(h.hunks) for h in headers)
1290 for h in headers:
1292 for h in headers:
1291 pos += len(h.hunks)
1293 pos += len(h.hunks)
1292 skipfile = None
1294 skipfile = None
1293 fixoffset = 0
1295 fixoffset = 0
1294 hdr = b''.join(h.header)
1296 hdr = b''.join(h.header)
1295 if hdr in seen:
1297 if hdr in seen:
1296 continue
1298 continue
1297 seen.add(hdr)
1299 seen.add(hdr)
1298 if skipall is None:
1300 if skipall is None:
1299 h.pretty(ui)
1301 h.pretty(ui)
1300 files = h.files()
1302 files = h.files()
1301 msg = _(b'examine changes to %s?') % _(b' and ').join(
1303 msg = _(b'examine changes to %s?') % _(b' and ').join(
1302 b"'%s'" % f for f in files
1304 b"'%s'" % f for f in files
1303 )
1305 )
1304 if all(match.exact(f) for f in files):
1306 if all(match.exact(f) for f in files):
1305 r, skipall, np = True, None, None
1307 r, skipall, np = True, None, None
1306 else:
1308 else:
1307 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1309 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1308 if not r:
1310 if not r:
1309 continue
1311 continue
1310 applied[h.filename()] = [h]
1312 applied[h.filename()] = [h]
1311 if h.allhunks():
1313 if h.allhunks():
1312 applied[h.filename()] += h.hunks
1314 applied[h.filename()] += h.hunks
1313 continue
1315 continue
1314 for i, chunk in enumerate(h.hunks):
1316 for i, chunk in enumerate(h.hunks):
1315 if skipfile is None and skipall is None:
1317 if skipfile is None and skipall is None:
1316 chunk.pretty(ui)
1318 chunk.pretty(ui)
1317 if total == 1:
1319 if total == 1:
1318 msg = messages[b'single'][operation] % chunk.filename()
1320 msg = messages[b'single'][operation] % chunk.filename()
1319 else:
1321 else:
1320 idx = pos - len(h.hunks) + i
1322 idx = pos - len(h.hunks) + i
1321 msg = messages[b'multiple'][operation] % (
1323 msg = messages[b'multiple'][operation] % (
1322 idx,
1324 idx,
1323 total,
1325 total,
1324 chunk.filename(),
1326 chunk.filename(),
1325 )
1327 )
1326 r, skipfile, skipall, newpatches = prompt(
1328 r, skipfile, skipall, newpatches = prompt(
1327 skipfile, skipall, msg, chunk
1329 skipfile, skipall, msg, chunk
1328 )
1330 )
1329 if r:
1331 if r:
1330 if fixoffset:
1332 if fixoffset:
1331 chunk = copy.copy(chunk)
1333 chunk = copy.copy(chunk)
1332 chunk.toline += fixoffset
1334 chunk.toline += fixoffset
1333 applied[chunk.filename()].append(chunk)
1335 applied[chunk.filename()].append(chunk)
1334 elif newpatches is not None:
1336 elif newpatches is not None:
1335 for newpatch in newpatches:
1337 for newpatch in newpatches:
1336 for newhunk in newpatch.hunks:
1338 for newhunk in newpatch.hunks:
1337 if fixoffset:
1339 if fixoffset:
1338 newhunk.toline += fixoffset
1340 newhunk.toline += fixoffset
1339 applied[newhunk.filename()].append(newhunk)
1341 applied[newhunk.filename()].append(newhunk)
1340 else:
1342 else:
1341 fixoffset += chunk.removed - chunk.added
1343 fixoffset += chunk.removed - chunk.added
1342 return (
1344 return (
1343 sum(
1345 sum(
1344 [
1346 [
1345 h
1347 h
1346 for h in pycompat.itervalues(applied)
1348 for h in pycompat.itervalues(applied)
1347 if h[0].special() or len(h) > 1
1349 if h[0].special() or len(h) > 1
1348 ],
1350 ],
1349 [],
1351 [],
1350 ),
1352 ),
1351 {},
1353 {},
1352 )
1354 )
1353
1355
1354
1356
1355 class hunk(object):
1357 class hunk(object):
1356 def __init__(self, desc, num, lr, context):
1358 def __init__(self, desc, num, lr, context):
1357 self.number = num
1359 self.number = num
1358 self.desc = desc
1360 self.desc = desc
1359 self.hunk = [desc]
1361 self.hunk = [desc]
1360 self.a = []
1362 self.a = []
1361 self.b = []
1363 self.b = []
1362 self.starta = self.lena = None
1364 self.starta = self.lena = None
1363 self.startb = self.lenb = None
1365 self.startb = self.lenb = None
1364 if lr is not None:
1366 if lr is not None:
1365 if context:
1367 if context:
1366 self.read_context_hunk(lr)
1368 self.read_context_hunk(lr)
1367 else:
1369 else:
1368 self.read_unified_hunk(lr)
1370 self.read_unified_hunk(lr)
1369
1371
1370 def getnormalized(self):
1372 def getnormalized(self):
1371 """Return a copy with line endings normalized to LF."""
1373 """Return a copy with line endings normalized to LF."""
1372
1374
1373 def normalize(lines):
1375 def normalize(lines):
1374 nlines = []
1376 nlines = []
1375 for line in lines:
1377 for line in lines:
1376 if line.endswith(b'\r\n'):
1378 if line.endswith(b'\r\n'):
1377 line = line[:-2] + b'\n'
1379 line = line[:-2] + b'\n'
1378 nlines.append(line)
1380 nlines.append(line)
1379 return nlines
1381 return nlines
1380
1382
1381 # Dummy object, it is rebuilt manually
1383 # Dummy object, it is rebuilt manually
1382 nh = hunk(self.desc, self.number, None, None)
1384 nh = hunk(self.desc, self.number, None, None)
1383 nh.number = self.number
1385 nh.number = self.number
1384 nh.desc = self.desc
1386 nh.desc = self.desc
1385 nh.hunk = self.hunk
1387 nh.hunk = self.hunk
1386 nh.a = normalize(self.a)
1388 nh.a = normalize(self.a)
1387 nh.b = normalize(self.b)
1389 nh.b = normalize(self.b)
1388 nh.starta = self.starta
1390 nh.starta = self.starta
1389 nh.startb = self.startb
1391 nh.startb = self.startb
1390 nh.lena = self.lena
1392 nh.lena = self.lena
1391 nh.lenb = self.lenb
1393 nh.lenb = self.lenb
1392 return nh
1394 return nh
1393
1395
1394 def read_unified_hunk(self, lr):
1396 def read_unified_hunk(self, lr):
1395 m = unidesc.match(self.desc)
1397 m = unidesc.match(self.desc)
1396 if not m:
1398 if not m:
1397 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1399 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1398 self.starta, self.lena, self.startb, self.lenb = m.groups()
1400 self.starta, self.lena, self.startb, self.lenb = m.groups()
1399 if self.lena is None:
1401 if self.lena is None:
1400 self.lena = 1
1402 self.lena = 1
1401 else:
1403 else:
1402 self.lena = int(self.lena)
1404 self.lena = int(self.lena)
1403 if self.lenb is None:
1405 if self.lenb is None:
1404 self.lenb = 1
1406 self.lenb = 1
1405 else:
1407 else:
1406 self.lenb = int(self.lenb)
1408 self.lenb = int(self.lenb)
1407 self.starta = int(self.starta)
1409 self.starta = int(self.starta)
1408 self.startb = int(self.startb)
1410 self.startb = int(self.startb)
1409 try:
1411 try:
1410 diffhelper.addlines(
1412 diffhelper.addlines(
1411 lr, self.hunk, self.lena, self.lenb, self.a, self.b
1413 lr, self.hunk, self.lena, self.lenb, self.a, self.b
1412 )
1414 )
1413 except error.ParseError as e:
1415 except error.ParseError as e:
1414 raise PatchParseError(_(b"bad hunk #%d: %s") % (self.number, e))
1416 raise PatchParseError(_(b"bad hunk #%d: %s") % (self.number, e))
1415 # if we hit eof before finishing out the hunk, the last line will
1417 # if we hit eof before finishing out the hunk, the last line will
1416 # be zero length. Lets try to fix it up.
1418 # be zero length. Lets try to fix it up.
1417 while len(self.hunk[-1]) == 0:
1419 while len(self.hunk[-1]) == 0:
1418 del self.hunk[-1]
1420 del self.hunk[-1]
1419 del self.a[-1]
1421 del self.a[-1]
1420 del self.b[-1]
1422 del self.b[-1]
1421 self.lena -= 1
1423 self.lena -= 1
1422 self.lenb -= 1
1424 self.lenb -= 1
1423 self._fixnewline(lr)
1425 self._fixnewline(lr)
1424
1426
1425 def read_context_hunk(self, lr):
1427 def read_context_hunk(self, lr):
1426 self.desc = lr.readline()
1428 self.desc = lr.readline()
1427 m = contextdesc.match(self.desc)
1429 m = contextdesc.match(self.desc)
1428 if not m:
1430 if not m:
1429 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1431 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1430 self.starta, aend = m.groups()
1432 self.starta, aend = m.groups()
1431 self.starta = int(self.starta)
1433 self.starta = int(self.starta)
1432 if aend is None:
1434 if aend is None:
1433 aend = self.starta
1435 aend = self.starta
1434 self.lena = int(aend) - self.starta
1436 self.lena = int(aend) - self.starta
1435 if self.starta:
1437 if self.starta:
1436 self.lena += 1
1438 self.lena += 1
1437 for x in pycompat.xrange(self.lena):
1439 for x in pycompat.xrange(self.lena):
1438 l = lr.readline()
1440 l = lr.readline()
1439 if l.startswith(b'---'):
1441 if l.startswith(b'---'):
1440 # lines addition, old block is empty
1442 # lines addition, old block is empty
1441 lr.push(l)
1443 lr.push(l)
1442 break
1444 break
1443 s = l[2:]
1445 s = l[2:]
1444 if l.startswith(b'- ') or l.startswith(b'! '):
1446 if l.startswith(b'- ') or l.startswith(b'! '):
1445 u = b'-' + s
1447 u = b'-' + s
1446 elif l.startswith(b' '):
1448 elif l.startswith(b' '):
1447 u = b' ' + s
1449 u = b' ' + s
1448 else:
1450 else:
1449 raise PatchParseError(
1451 raise PatchParseError(
1450 _(b"bad hunk #%d old text line %d") % (self.number, x)
1452 _(b"bad hunk #%d old text line %d") % (self.number, x)
1451 )
1453 )
1452 self.a.append(u)
1454 self.a.append(u)
1453 self.hunk.append(u)
1455 self.hunk.append(u)
1454
1456
1455 l = lr.readline()
1457 l = lr.readline()
1456 if l.startswith(br'\ '):
1458 if l.startswith(br'\ '):
1457 s = self.a[-1][:-1]
1459 s = self.a[-1][:-1]
1458 self.a[-1] = s
1460 self.a[-1] = s
1459 self.hunk[-1] = s
1461 self.hunk[-1] = s
1460 l = lr.readline()
1462 l = lr.readline()
1461 m = contextdesc.match(l)
1463 m = contextdesc.match(l)
1462 if not m:
1464 if not m:
1463 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1465 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1464 self.startb, bend = m.groups()
1466 self.startb, bend = m.groups()
1465 self.startb = int(self.startb)
1467 self.startb = int(self.startb)
1466 if bend is None:
1468 if bend is None:
1467 bend = self.startb
1469 bend = self.startb
1468 self.lenb = int(bend) - self.startb
1470 self.lenb = int(bend) - self.startb
1469 if self.startb:
1471 if self.startb:
1470 self.lenb += 1
1472 self.lenb += 1
1471 hunki = 1
1473 hunki = 1
1472 for x in pycompat.xrange(self.lenb):
1474 for x in pycompat.xrange(self.lenb):
1473 l = lr.readline()
1475 l = lr.readline()
1474 if l.startswith(br'\ '):
1476 if l.startswith(br'\ '):
1475 # XXX: the only way to hit this is with an invalid line range.
1477 # XXX: the only way to hit this is with an invalid line range.
1476 # The no-eol marker is not counted in the line range, but I
1478 # The no-eol marker is not counted in the line range, but I
1477 # guess there are diff(1) out there which behave differently.
1479 # guess there are diff(1) out there which behave differently.
1478 s = self.b[-1][:-1]
1480 s = self.b[-1][:-1]
1479 self.b[-1] = s
1481 self.b[-1] = s
1480 self.hunk[hunki - 1] = s
1482 self.hunk[hunki - 1] = s
1481 continue
1483 continue
1482 if not l:
1484 if not l:
1483 # line deletions, new block is empty and we hit EOF
1485 # line deletions, new block is empty and we hit EOF
1484 lr.push(l)
1486 lr.push(l)
1485 break
1487 break
1486 s = l[2:]
1488 s = l[2:]
1487 if l.startswith(b'+ ') or l.startswith(b'! '):
1489 if l.startswith(b'+ ') or l.startswith(b'! '):
1488 u = b'+' + s
1490 u = b'+' + s
1489 elif l.startswith(b' '):
1491 elif l.startswith(b' '):
1490 u = b' ' + s
1492 u = b' ' + s
1491 elif len(self.b) == 0:
1493 elif len(self.b) == 0:
1492 # line deletions, new block is empty
1494 # line deletions, new block is empty
1493 lr.push(l)
1495 lr.push(l)
1494 break
1496 break
1495 else:
1497 else:
1496 raise PatchParseError(
1498 raise PatchParseError(
1497 _(b"bad hunk #%d old text line %d") % (self.number, x)
1499 _(b"bad hunk #%d old text line %d") % (self.number, x)
1498 )
1500 )
1499 self.b.append(s)
1501 self.b.append(s)
1500 while True:
1502 while True:
1501 if hunki >= len(self.hunk):
1503 if hunki >= len(self.hunk):
1502 h = b""
1504 h = b""
1503 else:
1505 else:
1504 h = self.hunk[hunki]
1506 h = self.hunk[hunki]
1505 hunki += 1
1507 hunki += 1
1506 if h == u:
1508 if h == u:
1507 break
1509 break
1508 elif h.startswith(b'-'):
1510 elif h.startswith(b'-'):
1509 continue
1511 continue
1510 else:
1512 else:
1511 self.hunk.insert(hunki - 1, u)
1513 self.hunk.insert(hunki - 1, u)
1512 break
1514 break
1513
1515
1514 if not self.a:
1516 if not self.a:
1515 # this happens when lines were only added to the hunk
1517 # this happens when lines were only added to the hunk
1516 for x in self.hunk:
1518 for x in self.hunk:
1517 if x.startswith(b'-') or x.startswith(b' '):
1519 if x.startswith(b'-') or x.startswith(b' '):
1518 self.a.append(x)
1520 self.a.append(x)
1519 if not self.b:
1521 if not self.b:
1520 # this happens when lines were only deleted from the hunk
1522 # this happens when lines were only deleted from the hunk
1521 for x in self.hunk:
1523 for x in self.hunk:
1522 if x.startswith(b'+') or x.startswith(b' '):
1524 if x.startswith(b'+') or x.startswith(b' '):
1523 self.b.append(x[1:])
1525 self.b.append(x[1:])
1524 # @@ -start,len +start,len @@
1526 # @@ -start,len +start,len @@
1525 self.desc = b"@@ -%d,%d +%d,%d @@\n" % (
1527 self.desc = b"@@ -%d,%d +%d,%d @@\n" % (
1526 self.starta,
1528 self.starta,
1527 self.lena,
1529 self.lena,
1528 self.startb,
1530 self.startb,
1529 self.lenb,
1531 self.lenb,
1530 )
1532 )
1531 self.hunk[0] = self.desc
1533 self.hunk[0] = self.desc
1532 self._fixnewline(lr)
1534 self._fixnewline(lr)
1533
1535
1534 def _fixnewline(self, lr):
1536 def _fixnewline(self, lr):
1535 l = lr.readline()
1537 l = lr.readline()
1536 if l.startswith(br'\ '):
1538 if l.startswith(br'\ '):
1537 diffhelper.fixnewline(self.hunk, self.a, self.b)
1539 diffhelper.fixnewline(self.hunk, self.a, self.b)
1538 else:
1540 else:
1539 lr.push(l)
1541 lr.push(l)
1540
1542
1541 def complete(self):
1543 def complete(self):
1542 return len(self.a) == self.lena and len(self.b) == self.lenb
1544 return len(self.a) == self.lena and len(self.b) == self.lenb
1543
1545
1544 def _fuzzit(self, old, new, fuzz, toponly):
1546 def _fuzzit(self, old, new, fuzz, toponly):
1545 # this removes context lines from the top and bottom of list 'l'. It
1547 # this removes context lines from the top and bottom of list 'l'. It
1546 # checks the hunk to make sure only context lines are removed, and then
1548 # checks the hunk to make sure only context lines are removed, and then
1547 # returns a new shortened list of lines.
1549 # returns a new shortened list of lines.
1548 fuzz = min(fuzz, len(old))
1550 fuzz = min(fuzz, len(old))
1549 if fuzz:
1551 if fuzz:
1550 top = 0
1552 top = 0
1551 bot = 0
1553 bot = 0
1552 hlen = len(self.hunk)
1554 hlen = len(self.hunk)
1553 for x in pycompat.xrange(hlen - 1):
1555 for x in pycompat.xrange(hlen - 1):
1554 # the hunk starts with the @@ line, so use x+1
1556 # the hunk starts with the @@ line, so use x+1
1555 if self.hunk[x + 1].startswith(b' '):
1557 if self.hunk[x + 1].startswith(b' '):
1556 top += 1
1558 top += 1
1557 else:
1559 else:
1558 break
1560 break
1559 if not toponly:
1561 if not toponly:
1560 for x in pycompat.xrange(hlen - 1):
1562 for x in pycompat.xrange(hlen - 1):
1561 if self.hunk[hlen - bot - 1].startswith(b' '):
1563 if self.hunk[hlen - bot - 1].startswith(b' '):
1562 bot += 1
1564 bot += 1
1563 else:
1565 else:
1564 break
1566 break
1565
1567
1566 bot = min(fuzz, bot)
1568 bot = min(fuzz, bot)
1567 top = min(fuzz, top)
1569 top = min(fuzz, top)
1568 return old[top : len(old) - bot], new[top : len(new) - bot], top
1570 return old[top : len(old) - bot], new[top : len(new) - bot], top
1569 return old, new, 0
1571 return old, new, 0
1570
1572
1571 def fuzzit(self, fuzz, toponly):
1573 def fuzzit(self, fuzz, toponly):
1572 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1574 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1573 oldstart = self.starta + top
1575 oldstart = self.starta + top
1574 newstart = self.startb + top
1576 newstart = self.startb + top
1575 # zero length hunk ranges already have their start decremented
1577 # zero length hunk ranges already have their start decremented
1576 if self.lena and oldstart > 0:
1578 if self.lena and oldstart > 0:
1577 oldstart -= 1
1579 oldstart -= 1
1578 if self.lenb and newstart > 0:
1580 if self.lenb and newstart > 0:
1579 newstart -= 1
1581 newstart -= 1
1580 return old, oldstart, new, newstart
1582 return old, oldstart, new, newstart
1581
1583
1582
1584
1583 class binhunk(object):
1585 class binhunk(object):
1584 """A binary patch file."""
1586 """A binary patch file."""
1585
1587
1586 def __init__(self, lr, fname):
1588 def __init__(self, lr, fname):
1587 self.text = None
1589 self.text = None
1588 self.delta = False
1590 self.delta = False
1589 self.hunk = [b'GIT binary patch\n']
1591 self.hunk = [b'GIT binary patch\n']
1590 self._fname = fname
1592 self._fname = fname
1591 self._read(lr)
1593 self._read(lr)
1592
1594
1593 def complete(self):
1595 def complete(self):
1594 return self.text is not None
1596 return self.text is not None
1595
1597
1596 def new(self, lines):
1598 def new(self, lines):
1597 if self.delta:
1599 if self.delta:
1598 return [applybindelta(self.text, b''.join(lines))]
1600 return [applybindelta(self.text, b''.join(lines))]
1599 return [self.text]
1601 return [self.text]
1600
1602
1601 def _read(self, lr):
1603 def _read(self, lr):
1602 def getline(lr, hunk):
1604 def getline(lr, hunk):
1603 l = lr.readline()
1605 l = lr.readline()
1604 hunk.append(l)
1606 hunk.append(l)
1605 return l.rstrip(b'\r\n')
1607 return l.rstrip(b'\r\n')
1606
1608
1607 while True:
1609 while True:
1608 line = getline(lr, self.hunk)
1610 line = getline(lr, self.hunk)
1609 if not line:
1611 if not line:
1610 raise PatchParseError(
1612 raise PatchParseError(
1611 _(b'could not extract "%s" binary data') % self._fname
1613 _(b'could not extract "%s" binary data') % self._fname
1612 )
1614 )
1613 if line.startswith(b'literal '):
1615 if line.startswith(b'literal '):
1614 size = int(line[8:].rstrip())
1616 size = int(line[8:].rstrip())
1615 break
1617 break
1616 if line.startswith(b'delta '):
1618 if line.startswith(b'delta '):
1617 size = int(line[6:].rstrip())
1619 size = int(line[6:].rstrip())
1618 self.delta = True
1620 self.delta = True
1619 break
1621 break
1620 dec = []
1622 dec = []
1621 line = getline(lr, self.hunk)
1623 line = getline(lr, self.hunk)
1622 while len(line) > 1:
1624 while len(line) > 1:
1623 l = line[0:1]
1625 l = line[0:1]
1624 if l <= b'Z' and l >= b'A':
1626 if l <= b'Z' and l >= b'A':
1625 l = ord(l) - ord(b'A') + 1
1627 l = ord(l) - ord(b'A') + 1
1626 else:
1628 else:
1627 l = ord(l) - ord(b'a') + 27
1629 l = ord(l) - ord(b'a') + 27
1628 try:
1630 try:
1629 dec.append(util.b85decode(line[1:])[:l])
1631 dec.append(util.b85decode(line[1:])[:l])
1630 except ValueError as e:
1632 except ValueError as e:
1631 raise PatchParseError(
1633 raise PatchParseError(
1632 _(b'could not decode "%s" binary patch: %s')
1634 _(b'could not decode "%s" binary patch: %s')
1633 % (self._fname, stringutil.forcebytestr(e))
1635 % (self._fname, stringutil.forcebytestr(e))
1634 )
1636 )
1635 line = getline(lr, self.hunk)
1637 line = getline(lr, self.hunk)
1636 text = zlib.decompress(b''.join(dec))
1638 text = zlib.decompress(b''.join(dec))
1637 if len(text) != size:
1639 if len(text) != size:
1638 raise PatchParseError(
1640 raise PatchParseError(
1639 _(b'"%s" length is %d bytes, should be %d')
1641 _(b'"%s" length is %d bytes, should be %d')
1640 % (self._fname, len(text), size)
1642 % (self._fname, len(text), size)
1641 )
1643 )
1642 self.text = text
1644 self.text = text
1643
1645
1644
1646
1645 def parsefilename(str):
1647 def parsefilename(str):
1646 # --- filename \t|space stuff
1648 # --- filename \t|space stuff
1647 s = str[4:].rstrip(b'\r\n')
1649 s = str[4:].rstrip(b'\r\n')
1648 i = s.find(b'\t')
1650 i = s.find(b'\t')
1649 if i < 0:
1651 if i < 0:
1650 i = s.find(b' ')
1652 i = s.find(b' ')
1651 if i < 0:
1653 if i < 0:
1652 return s
1654 return s
1653 return s[:i]
1655 return s[:i]
1654
1656
1655
1657
1656 def reversehunks(hunks):
1658 def reversehunks(hunks):
1657 '''reverse the signs in the hunks given as argument
1659 '''reverse the signs in the hunks given as argument
1658
1660
1659 This function operates on hunks coming out of patch.filterpatch, that is
1661 This function operates on hunks coming out of patch.filterpatch, that is
1660 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1662 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1661
1663
1662 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1664 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1663 ... --- a/folder1/g
1665 ... --- a/folder1/g
1664 ... +++ b/folder1/g
1666 ... +++ b/folder1/g
1665 ... @@ -1,7 +1,7 @@
1667 ... @@ -1,7 +1,7 @@
1666 ... +firstline
1668 ... +firstline
1667 ... c
1669 ... c
1668 ... 1
1670 ... 1
1669 ... 2
1671 ... 2
1670 ... + 3
1672 ... + 3
1671 ... -4
1673 ... -4
1672 ... 5
1674 ... 5
1673 ... d
1675 ... d
1674 ... +lastline"""
1676 ... +lastline"""
1675 >>> hunks = parsepatch([rawpatch])
1677 >>> hunks = parsepatch([rawpatch])
1676 >>> hunkscomingfromfilterpatch = []
1678 >>> hunkscomingfromfilterpatch = []
1677 >>> for h in hunks:
1679 >>> for h in hunks:
1678 ... hunkscomingfromfilterpatch.append(h)
1680 ... hunkscomingfromfilterpatch.append(h)
1679 ... hunkscomingfromfilterpatch.extend(h.hunks)
1681 ... hunkscomingfromfilterpatch.extend(h.hunks)
1680
1682
1681 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1683 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1682 >>> from . import util
1684 >>> from . import util
1683 >>> fp = util.stringio()
1685 >>> fp = util.stringio()
1684 >>> for c in reversedhunks:
1686 >>> for c in reversedhunks:
1685 ... c.write(fp)
1687 ... c.write(fp)
1686 >>> fp.seek(0) or None
1688 >>> fp.seek(0) or None
1687 >>> reversedpatch = fp.read()
1689 >>> reversedpatch = fp.read()
1688 >>> print(pycompat.sysstr(reversedpatch))
1690 >>> print(pycompat.sysstr(reversedpatch))
1689 diff --git a/folder1/g b/folder1/g
1691 diff --git a/folder1/g b/folder1/g
1690 --- a/folder1/g
1692 --- a/folder1/g
1691 +++ b/folder1/g
1693 +++ b/folder1/g
1692 @@ -1,4 +1,3 @@
1694 @@ -1,4 +1,3 @@
1693 -firstline
1695 -firstline
1694 c
1696 c
1695 1
1697 1
1696 2
1698 2
1697 @@ -2,6 +1,6 @@
1699 @@ -2,6 +1,6 @@
1698 c
1700 c
1699 1
1701 1
1700 2
1702 2
1701 - 3
1703 - 3
1702 +4
1704 +4
1703 5
1705 5
1704 d
1706 d
1705 @@ -6,3 +5,2 @@
1707 @@ -6,3 +5,2 @@
1706 5
1708 5
1707 d
1709 d
1708 -lastline
1710 -lastline
1709
1711
1710 '''
1712 '''
1711
1713
1712 newhunks = []
1714 newhunks = []
1713 for c in hunks:
1715 for c in hunks:
1714 if util.safehasattr(c, b'reversehunk'):
1716 if util.safehasattr(c, b'reversehunk'):
1715 c = c.reversehunk()
1717 c = c.reversehunk()
1716 newhunks.append(c)
1718 newhunks.append(c)
1717 return newhunks
1719 return newhunks
1718
1720
1719
1721
1720 def parsepatch(originalchunks, maxcontext=None):
1722 def parsepatch(originalchunks, maxcontext=None):
1721 """patch -> [] of headers -> [] of hunks
1723 """patch -> [] of headers -> [] of hunks
1722
1724
1723 If maxcontext is not None, trim context lines if necessary.
1725 If maxcontext is not None, trim context lines if necessary.
1724
1726
1725 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1727 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1726 ... --- a/folder1/g
1728 ... --- a/folder1/g
1727 ... +++ b/folder1/g
1729 ... +++ b/folder1/g
1728 ... @@ -1,8 +1,10 @@
1730 ... @@ -1,8 +1,10 @@
1729 ... 1
1731 ... 1
1730 ... 2
1732 ... 2
1731 ... -3
1733 ... -3
1732 ... 4
1734 ... 4
1733 ... 5
1735 ... 5
1734 ... 6
1736 ... 6
1735 ... +6.1
1737 ... +6.1
1736 ... +6.2
1738 ... +6.2
1737 ... 7
1739 ... 7
1738 ... 8
1740 ... 8
1739 ... +9'''
1741 ... +9'''
1740 >>> out = util.stringio()
1742 >>> out = util.stringio()
1741 >>> headers = parsepatch([rawpatch], maxcontext=1)
1743 >>> headers = parsepatch([rawpatch], maxcontext=1)
1742 >>> for header in headers:
1744 >>> for header in headers:
1743 ... header.write(out)
1745 ... header.write(out)
1744 ... for hunk in header.hunks:
1746 ... for hunk in header.hunks:
1745 ... hunk.write(out)
1747 ... hunk.write(out)
1746 >>> print(pycompat.sysstr(out.getvalue()))
1748 >>> print(pycompat.sysstr(out.getvalue()))
1747 diff --git a/folder1/g b/folder1/g
1749 diff --git a/folder1/g b/folder1/g
1748 --- a/folder1/g
1750 --- a/folder1/g
1749 +++ b/folder1/g
1751 +++ b/folder1/g
1750 @@ -2,3 +2,2 @@
1752 @@ -2,3 +2,2 @@
1751 2
1753 2
1752 -3
1754 -3
1753 4
1755 4
1754 @@ -6,2 +5,4 @@
1756 @@ -6,2 +5,4 @@
1755 6
1757 6
1756 +6.1
1758 +6.1
1757 +6.2
1759 +6.2
1758 7
1760 7
1759 @@ -8,1 +9,2 @@
1761 @@ -8,1 +9,2 @@
1760 8
1762 8
1761 +9
1763 +9
1762 """
1764 """
1763
1765
1764 class parser(object):
1766 class parser(object):
1765 """patch parsing state machine"""
1767 """patch parsing state machine"""
1766
1768
1767 def __init__(self):
1769 def __init__(self):
1768 self.fromline = 0
1770 self.fromline = 0
1769 self.toline = 0
1771 self.toline = 0
1770 self.proc = b''
1772 self.proc = b''
1771 self.header = None
1773 self.header = None
1772 self.context = []
1774 self.context = []
1773 self.before = []
1775 self.before = []
1774 self.hunk = []
1776 self.hunk = []
1775 self.headers = []
1777 self.headers = []
1776
1778
1777 def addrange(self, limits):
1779 def addrange(self, limits):
1778 self.addcontext([])
1780 self.addcontext([])
1779 fromstart, fromend, tostart, toend, proc = limits
1781 fromstart, fromend, tostart, toend, proc = limits
1780 self.fromline = int(fromstart)
1782 self.fromline = int(fromstart)
1781 self.toline = int(tostart)
1783 self.toline = int(tostart)
1782 self.proc = proc
1784 self.proc = proc
1783
1785
1784 def addcontext(self, context):
1786 def addcontext(self, context):
1785 if self.hunk:
1787 if self.hunk:
1786 h = recordhunk(
1788 h = recordhunk(
1787 self.header,
1789 self.header,
1788 self.fromline,
1790 self.fromline,
1789 self.toline,
1791 self.toline,
1790 self.proc,
1792 self.proc,
1791 self.before,
1793 self.before,
1792 self.hunk,
1794 self.hunk,
1793 context,
1795 context,
1794 maxcontext,
1796 maxcontext,
1795 )
1797 )
1796 self.header.hunks.append(h)
1798 self.header.hunks.append(h)
1797 self.fromline += len(self.before) + h.removed
1799 self.fromline += len(self.before) + h.removed
1798 self.toline += len(self.before) + h.added
1800 self.toline += len(self.before) + h.added
1799 self.before = []
1801 self.before = []
1800 self.hunk = []
1802 self.hunk = []
1801 self.context = context
1803 self.context = context
1802
1804
1803 def addhunk(self, hunk):
1805 def addhunk(self, hunk):
1804 if self.context:
1806 if self.context:
1805 self.before = self.context
1807 self.before = self.context
1806 self.context = []
1808 self.context = []
1807 if self.hunk:
1809 if self.hunk:
1808 self.addcontext([])
1810 self.addcontext([])
1809 self.hunk = hunk
1811 self.hunk = hunk
1810
1812
1811 def newfile(self, hdr):
1813 def newfile(self, hdr):
1812 self.addcontext([])
1814 self.addcontext([])
1813 h = header(hdr)
1815 h = header(hdr)
1814 self.headers.append(h)
1816 self.headers.append(h)
1815 self.header = h
1817 self.header = h
1816
1818
1817 def addother(self, line):
1819 def addother(self, line):
1818 pass # 'other' lines are ignored
1820 pass # 'other' lines are ignored
1819
1821
1820 def finished(self):
1822 def finished(self):
1821 self.addcontext([])
1823 self.addcontext([])
1822 return self.headers
1824 return self.headers
1823
1825
1824 transitions = {
1826 transitions = {
1825 b'file': {
1827 b'file': {
1826 b'context': addcontext,
1828 b'context': addcontext,
1827 b'file': newfile,
1829 b'file': newfile,
1828 b'hunk': addhunk,
1830 b'hunk': addhunk,
1829 b'range': addrange,
1831 b'range': addrange,
1830 },
1832 },
1831 b'context': {
1833 b'context': {
1832 b'file': newfile,
1834 b'file': newfile,
1833 b'hunk': addhunk,
1835 b'hunk': addhunk,
1834 b'range': addrange,
1836 b'range': addrange,
1835 b'other': addother,
1837 b'other': addother,
1836 },
1838 },
1837 b'hunk': {
1839 b'hunk': {
1838 b'context': addcontext,
1840 b'context': addcontext,
1839 b'file': newfile,
1841 b'file': newfile,
1840 b'range': addrange,
1842 b'range': addrange,
1841 },
1843 },
1842 b'range': {b'context': addcontext, b'hunk': addhunk},
1844 b'range': {b'context': addcontext, b'hunk': addhunk},
1843 b'other': {b'other': addother},
1845 b'other': {b'other': addother},
1844 }
1846 }
1845
1847
1846 p = parser()
1848 p = parser()
1847 fp = stringio()
1849 fp = stringio()
1848 fp.write(b''.join(originalchunks))
1850 fp.write(b''.join(originalchunks))
1849 fp.seek(0)
1851 fp.seek(0)
1850
1852
1851 state = b'context'
1853 state = b'context'
1852 for newstate, data in scanpatch(fp):
1854 for newstate, data in scanpatch(fp):
1853 try:
1855 try:
1854 p.transitions[state][newstate](p, data)
1856 p.transitions[state][newstate](p, data)
1855 except KeyError:
1857 except KeyError:
1856 raise PatchParseError(
1858 raise PatchParseError(
1857 b'unhandled transition: %s -> %s' % (state, newstate)
1859 b'unhandled transition: %s -> %s' % (state, newstate)
1858 )
1860 )
1859 state = newstate
1861 state = newstate
1860 del fp
1862 del fp
1861 return p.finished()
1863 return p.finished()
1862
1864
1863
1865
1864 def pathtransform(path, strip, prefix):
1866 def pathtransform(path, strip, prefix):
1865 """turn a path from a patch into a path suitable for the repository
1867 """turn a path from a patch into a path suitable for the repository
1866
1868
1867 prefix, if not empty, is expected to be normalized with a / at the end.
1869 prefix, if not empty, is expected to be normalized with a / at the end.
1868
1870
1869 Returns (stripped components, path in repository).
1871 Returns (stripped components, path in repository).
1870
1872
1871 >>> pathtransform(b'a/b/c', 0, b'')
1873 >>> pathtransform(b'a/b/c', 0, b'')
1872 ('', 'a/b/c')
1874 ('', 'a/b/c')
1873 >>> pathtransform(b' a/b/c ', 0, b'')
1875 >>> pathtransform(b' a/b/c ', 0, b'')
1874 ('', ' a/b/c')
1876 ('', ' a/b/c')
1875 >>> pathtransform(b' a/b/c ', 2, b'')
1877 >>> pathtransform(b' a/b/c ', 2, b'')
1876 ('a/b/', 'c')
1878 ('a/b/', 'c')
1877 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1879 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1878 ('', 'd/e/a/b/c')
1880 ('', 'd/e/a/b/c')
1879 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1881 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1880 ('a//b/', 'd/e/c')
1882 ('a//b/', 'd/e/c')
1881 >>> pathtransform(b'a/b/c', 3, b'')
1883 >>> pathtransform(b'a/b/c', 3, b'')
1882 Traceback (most recent call last):
1884 Traceback (most recent call last):
1883 PatchApplicationError: unable to strip away 1 of 3 dirs from a/b/c
1885 PatchApplicationError: unable to strip away 1 of 3 dirs from a/b/c
1884 """
1886 """
1885 pathlen = len(path)
1887 pathlen = len(path)
1886 i = 0
1888 i = 0
1887 if strip == 0:
1889 if strip == 0:
1888 return b'', prefix + path.rstrip()
1890 return b'', prefix + path.rstrip()
1889 count = strip
1891 count = strip
1890 while count > 0:
1892 while count > 0:
1891 i = path.find(b'/', i)
1893 i = path.find(b'/', i)
1892 if i == -1:
1894 if i == -1:
1893 raise PatchApplicationError(
1895 raise PatchApplicationError(
1894 _(b"unable to strip away %d of %d dirs from %s")
1896 _(b"unable to strip away %d of %d dirs from %s")
1895 % (count, strip, path)
1897 % (count, strip, path)
1896 )
1898 )
1897 i += 1
1899 i += 1
1898 # consume '//' in the path
1900 # consume '//' in the path
1899 while i < pathlen - 1 and path[i : i + 1] == b'/':
1901 while i < pathlen - 1 and path[i : i + 1] == b'/':
1900 i += 1
1902 i += 1
1901 count -= 1
1903 count -= 1
1902 return path[:i].lstrip(), prefix + path[i:].rstrip()
1904 return path[:i].lstrip(), prefix + path[i:].rstrip()
1903
1905
1904
1906
1905 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1907 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1906 nulla = afile_orig == b"/dev/null"
1908 nulla = afile_orig == b"/dev/null"
1907 nullb = bfile_orig == b"/dev/null"
1909 nullb = bfile_orig == b"/dev/null"
1908 create = nulla and hunk.starta == 0 and hunk.lena == 0
1910 create = nulla and hunk.starta == 0 and hunk.lena == 0
1909 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1911 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1910 abase, afile = pathtransform(afile_orig, strip, prefix)
1912 abase, afile = pathtransform(afile_orig, strip, prefix)
1911 gooda = not nulla and backend.exists(afile)
1913 gooda = not nulla and backend.exists(afile)
1912 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1914 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1913 if afile == bfile:
1915 if afile == bfile:
1914 goodb = gooda
1916 goodb = gooda
1915 else:
1917 else:
1916 goodb = not nullb and backend.exists(bfile)
1918 goodb = not nullb and backend.exists(bfile)
1917 missing = not goodb and not gooda and not create
1919 missing = not goodb and not gooda and not create
1918
1920
1919 # some diff programs apparently produce patches where the afile is
1921 # some diff programs apparently produce patches where the afile is
1920 # not /dev/null, but afile starts with bfile
1922 # not /dev/null, but afile starts with bfile
1921 abasedir = afile[: afile.rfind(b'/') + 1]
1923 abasedir = afile[: afile.rfind(b'/') + 1]
1922 bbasedir = bfile[: bfile.rfind(b'/') + 1]
1924 bbasedir = bfile[: bfile.rfind(b'/') + 1]
1923 if (
1925 if (
1924 missing
1926 missing
1925 and abasedir == bbasedir
1927 and abasedir == bbasedir
1926 and afile.startswith(bfile)
1928 and afile.startswith(bfile)
1927 and hunk.starta == 0
1929 and hunk.starta == 0
1928 and hunk.lena == 0
1930 and hunk.lena == 0
1929 ):
1931 ):
1930 create = True
1932 create = True
1931 missing = False
1933 missing = False
1932
1934
1933 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1935 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1934 # diff is between a file and its backup. In this case, the original
1936 # diff is between a file and its backup. In this case, the original
1935 # file should be patched (see original mpatch code).
1937 # file should be patched (see original mpatch code).
1936 isbackup = abase == bbase and bfile.startswith(afile)
1938 isbackup = abase == bbase and bfile.startswith(afile)
1937 fname = None
1939 fname = None
1938 if not missing:
1940 if not missing:
1939 if gooda and goodb:
1941 if gooda and goodb:
1940 if isbackup:
1942 if isbackup:
1941 fname = afile
1943 fname = afile
1942 else:
1944 else:
1943 fname = bfile
1945 fname = bfile
1944 elif gooda:
1946 elif gooda:
1945 fname = afile
1947 fname = afile
1946
1948
1947 if not fname:
1949 if not fname:
1948 if not nullb:
1950 if not nullb:
1949 if isbackup:
1951 if isbackup:
1950 fname = afile
1952 fname = afile
1951 else:
1953 else:
1952 fname = bfile
1954 fname = bfile
1953 elif not nulla:
1955 elif not nulla:
1954 fname = afile
1956 fname = afile
1955 else:
1957 else:
1956 raise PatchParseError(_(b"undefined source and destination files"))
1958 raise PatchParseError(_(b"undefined source and destination files"))
1957
1959
1958 gp = patchmeta(fname)
1960 gp = patchmeta(fname)
1959 if create:
1961 if create:
1960 gp.op = b'ADD'
1962 gp.op = b'ADD'
1961 elif remove:
1963 elif remove:
1962 gp.op = b'DELETE'
1964 gp.op = b'DELETE'
1963 return gp
1965 return gp
1964
1966
1965
1967
1966 def scanpatch(fp):
1968 def scanpatch(fp):
1967 """like patch.iterhunks, but yield different events
1969 """like patch.iterhunks, but yield different events
1968
1970
1969 - ('file', [header_lines + fromfile + tofile])
1971 - ('file', [header_lines + fromfile + tofile])
1970 - ('context', [context_lines])
1972 - ('context', [context_lines])
1971 - ('hunk', [hunk_lines])
1973 - ('hunk', [hunk_lines])
1972 - ('range', (-start,len, +start,len, proc))
1974 - ('range', (-start,len, +start,len, proc))
1973 """
1975 """
1974 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1976 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1975 lr = linereader(fp)
1977 lr = linereader(fp)
1976
1978
1977 def scanwhile(first, p):
1979 def scanwhile(first, p):
1978 """scan lr while predicate holds"""
1980 """scan lr while predicate holds"""
1979 lines = [first]
1981 lines = [first]
1980 for line in iter(lr.readline, b''):
1982 for line in iter(lr.readline, b''):
1981 if p(line):
1983 if p(line):
1982 lines.append(line)
1984 lines.append(line)
1983 else:
1985 else:
1984 lr.push(line)
1986 lr.push(line)
1985 break
1987 break
1986 return lines
1988 return lines
1987
1989
1988 for line in iter(lr.readline, b''):
1990 for line in iter(lr.readline, b''):
1989 if line.startswith(b'diff --git a/') or line.startswith(b'diff -r '):
1991 if line.startswith(b'diff --git a/') or line.startswith(b'diff -r '):
1990
1992
1991 def notheader(line):
1993 def notheader(line):
1992 s = line.split(None, 1)
1994 s = line.split(None, 1)
1993 return not s or s[0] not in (b'---', b'diff')
1995 return not s or s[0] not in (b'---', b'diff')
1994
1996
1995 header = scanwhile(line, notheader)
1997 header = scanwhile(line, notheader)
1996 fromfile = lr.readline()
1998 fromfile = lr.readline()
1997 if fromfile.startswith(b'---'):
1999 if fromfile.startswith(b'---'):
1998 tofile = lr.readline()
2000 tofile = lr.readline()
1999 header += [fromfile, tofile]
2001 header += [fromfile, tofile]
2000 else:
2002 else:
2001 lr.push(fromfile)
2003 lr.push(fromfile)
2002 yield b'file', header
2004 yield b'file', header
2003 elif line.startswith(b' '):
2005 elif line.startswith(b' '):
2004 cs = (b' ', b'\\')
2006 cs = (b' ', b'\\')
2005 yield b'context', scanwhile(line, lambda l: l.startswith(cs))
2007 yield b'context', scanwhile(line, lambda l: l.startswith(cs))
2006 elif line.startswith((b'-', b'+')):
2008 elif line.startswith((b'-', b'+')):
2007 cs = (b'-', b'+', b'\\')
2009 cs = (b'-', b'+', b'\\')
2008 yield b'hunk', scanwhile(line, lambda l: l.startswith(cs))
2010 yield b'hunk', scanwhile(line, lambda l: l.startswith(cs))
2009 else:
2011 else:
2010 m = lines_re.match(line)
2012 m = lines_re.match(line)
2011 if m:
2013 if m:
2012 yield b'range', m.groups()
2014 yield b'range', m.groups()
2013 else:
2015 else:
2014 yield b'other', line
2016 yield b'other', line
2015
2017
2016
2018
2017 def scangitpatch(lr, firstline):
2019 def scangitpatch(lr, firstline):
2018 """
2020 """
2019 Git patches can emit:
2021 Git patches can emit:
2020 - rename a to b
2022 - rename a to b
2021 - change b
2023 - change b
2022 - copy a to c
2024 - copy a to c
2023 - change c
2025 - change c
2024
2026
2025 We cannot apply this sequence as-is, the renamed 'a' could not be
2027 We cannot apply this sequence as-is, the renamed 'a' could not be
2026 found for it would have been renamed already. And we cannot copy
2028 found for it would have been renamed already. And we cannot copy
2027 from 'b' instead because 'b' would have been changed already. So
2029 from 'b' instead because 'b' would have been changed already. So
2028 we scan the git patch for copy and rename commands so we can
2030 we scan the git patch for copy and rename commands so we can
2029 perform the copies ahead of time.
2031 perform the copies ahead of time.
2030 """
2032 """
2031 pos = 0
2033 pos = 0
2032 try:
2034 try:
2033 pos = lr.fp.tell()
2035 pos = lr.fp.tell()
2034 fp = lr.fp
2036 fp = lr.fp
2035 except IOError:
2037 except IOError:
2036 fp = stringio(lr.fp.read())
2038 fp = stringio(lr.fp.read())
2037 gitlr = linereader(fp)
2039 gitlr = linereader(fp)
2038 gitlr.push(firstline)
2040 gitlr.push(firstline)
2039 gitpatches = readgitpatch(gitlr)
2041 gitpatches = readgitpatch(gitlr)
2040 fp.seek(pos)
2042 fp.seek(pos)
2041 return gitpatches
2043 return gitpatches
2042
2044
2043
2045
2044 def iterhunks(fp):
2046 def iterhunks(fp):
2045 """Read a patch and yield the following events:
2047 """Read a patch and yield the following events:
2046 - ("file", afile, bfile, firsthunk): select a new target file.
2048 - ("file", afile, bfile, firsthunk): select a new target file.
2047 - ("hunk", hunk): a new hunk is ready to be applied, follows a
2049 - ("hunk", hunk): a new hunk is ready to be applied, follows a
2048 "file" event.
2050 "file" event.
2049 - ("git", gitchanges): current diff is in git format, gitchanges
2051 - ("git", gitchanges): current diff is in git format, gitchanges
2050 maps filenames to gitpatch records. Unique event.
2052 maps filenames to gitpatch records. Unique event.
2051 """
2053 """
2052 afile = b""
2054 afile = b""
2053 bfile = b""
2055 bfile = b""
2054 state = None
2056 state = None
2055 hunknum = 0
2057 hunknum = 0
2056 emitfile = newfile = False
2058 emitfile = newfile = False
2057 gitpatches = None
2059 gitpatches = None
2058
2060
2059 # our states
2061 # our states
2060 BFILE = 1
2062 BFILE = 1
2061 context = None
2063 context = None
2062 lr = linereader(fp)
2064 lr = linereader(fp)
2063
2065
2064 for x in iter(lr.readline, b''):
2066 for x in iter(lr.readline, b''):
2065 if state == BFILE and (
2067 if state == BFILE and (
2066 (not context and x.startswith(b'@'))
2068 (not context and x.startswith(b'@'))
2067 or (context is not False and x.startswith(b'***************'))
2069 or (context is not False and x.startswith(b'***************'))
2068 or x.startswith(b'GIT binary patch')
2070 or x.startswith(b'GIT binary patch')
2069 ):
2071 ):
2070 gp = None
2072 gp = None
2071 if gitpatches and gitpatches[-1].ispatching(afile, bfile):
2073 if gitpatches and gitpatches[-1].ispatching(afile, bfile):
2072 gp = gitpatches.pop()
2074 gp = gitpatches.pop()
2073 if x.startswith(b'GIT binary patch'):
2075 if x.startswith(b'GIT binary patch'):
2074 h = binhunk(lr, gp.path)
2076 h = binhunk(lr, gp.path)
2075 else:
2077 else:
2076 if context is None and x.startswith(b'***************'):
2078 if context is None and x.startswith(b'***************'):
2077 context = True
2079 context = True
2078 h = hunk(x, hunknum + 1, lr, context)
2080 h = hunk(x, hunknum + 1, lr, context)
2079 hunknum += 1
2081 hunknum += 1
2080 if emitfile:
2082 if emitfile:
2081 emitfile = False
2083 emitfile = False
2082 yield b'file', (afile, bfile, h, gp and gp.copy() or None)
2084 yield b'file', (afile, bfile, h, gp and gp.copy() or None)
2083 yield b'hunk', h
2085 yield b'hunk', h
2084 elif x.startswith(b'diff --git a/'):
2086 elif x.startswith(b'diff --git a/'):
2085 m = gitre.match(x.rstrip(b'\r\n'))
2087 m = gitre.match(x.rstrip(b'\r\n'))
2086 if not m:
2088 if not m:
2087 continue
2089 continue
2088 if gitpatches is None:
2090 if gitpatches is None:
2089 # scan whole input for git metadata
2091 # scan whole input for git metadata
2090 gitpatches = scangitpatch(lr, x)
2092 gitpatches = scangitpatch(lr, x)
2091 yield b'git', [
2093 yield b'git', [
2092 g.copy() for g in gitpatches if g.op in (b'COPY', b'RENAME')
2094 g.copy() for g in gitpatches if g.op in (b'COPY', b'RENAME')
2093 ]
2095 ]
2094 gitpatches.reverse()
2096 gitpatches.reverse()
2095 afile = b'a/' + m.group(1)
2097 afile = b'a/' + m.group(1)
2096 bfile = b'b/' + m.group(2)
2098 bfile = b'b/' + m.group(2)
2097 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
2099 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
2098 gp = gitpatches.pop()
2100 gp = gitpatches.pop()
2099 yield b'file', (
2101 yield b'file', (
2100 b'a/' + gp.path,
2102 b'a/' + gp.path,
2101 b'b/' + gp.path,
2103 b'b/' + gp.path,
2102 None,
2104 None,
2103 gp.copy(),
2105 gp.copy(),
2104 )
2106 )
2105 if not gitpatches:
2107 if not gitpatches:
2106 raise PatchParseError(
2108 raise PatchParseError(
2107 _(b'failed to synchronize metadata for "%s"') % afile[2:]
2109 _(b'failed to synchronize metadata for "%s"') % afile[2:]
2108 )
2110 )
2109 newfile = True
2111 newfile = True
2110 elif x.startswith(b'---'):
2112 elif x.startswith(b'---'):
2111 # check for a unified diff
2113 # check for a unified diff
2112 l2 = lr.readline()
2114 l2 = lr.readline()
2113 if not l2.startswith(b'+++'):
2115 if not l2.startswith(b'+++'):
2114 lr.push(l2)
2116 lr.push(l2)
2115 continue
2117 continue
2116 newfile = True
2118 newfile = True
2117 context = False
2119 context = False
2118 afile = parsefilename(x)
2120 afile = parsefilename(x)
2119 bfile = parsefilename(l2)
2121 bfile = parsefilename(l2)
2120 elif x.startswith(b'***'):
2122 elif x.startswith(b'***'):
2121 # check for a context diff
2123 # check for a context diff
2122 l2 = lr.readline()
2124 l2 = lr.readline()
2123 if not l2.startswith(b'---'):
2125 if not l2.startswith(b'---'):
2124 lr.push(l2)
2126 lr.push(l2)
2125 continue
2127 continue
2126 l3 = lr.readline()
2128 l3 = lr.readline()
2127 lr.push(l3)
2129 lr.push(l3)
2128 if not l3.startswith(b"***************"):
2130 if not l3.startswith(b"***************"):
2129 lr.push(l2)
2131 lr.push(l2)
2130 continue
2132 continue
2131 newfile = True
2133 newfile = True
2132 context = True
2134 context = True
2133 afile = parsefilename(x)
2135 afile = parsefilename(x)
2134 bfile = parsefilename(l2)
2136 bfile = parsefilename(l2)
2135
2137
2136 if newfile:
2138 if newfile:
2137 newfile = False
2139 newfile = False
2138 emitfile = True
2140 emitfile = True
2139 state = BFILE
2141 state = BFILE
2140 hunknum = 0
2142 hunknum = 0
2141
2143
2142 while gitpatches:
2144 while gitpatches:
2143 gp = gitpatches.pop()
2145 gp = gitpatches.pop()
2144 yield b'file', (b'a/' + gp.path, b'b/' + gp.path, None, gp.copy())
2146 yield b'file', (b'a/' + gp.path, b'b/' + gp.path, None, gp.copy())
2145
2147
2146
2148
2147 def applybindelta(binchunk, data):
2149 def applybindelta(binchunk, data):
2148 """Apply a binary delta hunk
2150 """Apply a binary delta hunk
2149 The algorithm used is the algorithm from git's patch-delta.c
2151 The algorithm used is the algorithm from git's patch-delta.c
2150 """
2152 """
2151
2153
2152 def deltahead(binchunk):
2154 def deltahead(binchunk):
2153 i = 0
2155 i = 0
2154 for c in pycompat.bytestr(binchunk):
2156 for c in pycompat.bytestr(binchunk):
2155 i += 1
2157 i += 1
2156 if not (ord(c) & 0x80):
2158 if not (ord(c) & 0x80):
2157 return i
2159 return i
2158 return i
2160 return i
2159
2161
2160 out = b""
2162 out = b""
2161 s = deltahead(binchunk)
2163 s = deltahead(binchunk)
2162 binchunk = binchunk[s:]
2164 binchunk = binchunk[s:]
2163 s = deltahead(binchunk)
2165 s = deltahead(binchunk)
2164 binchunk = binchunk[s:]
2166 binchunk = binchunk[s:]
2165 i = 0
2167 i = 0
2166 while i < len(binchunk):
2168 while i < len(binchunk):
2167 cmd = ord(binchunk[i : i + 1])
2169 cmd = ord(binchunk[i : i + 1])
2168 i += 1
2170 i += 1
2169 if cmd & 0x80:
2171 if cmd & 0x80:
2170 offset = 0
2172 offset = 0
2171 size = 0
2173 size = 0
2172 if cmd & 0x01:
2174 if cmd & 0x01:
2173 offset = ord(binchunk[i : i + 1])
2175 offset = ord(binchunk[i : i + 1])
2174 i += 1
2176 i += 1
2175 if cmd & 0x02:
2177 if cmd & 0x02:
2176 offset |= ord(binchunk[i : i + 1]) << 8
2178 offset |= ord(binchunk[i : i + 1]) << 8
2177 i += 1
2179 i += 1
2178 if cmd & 0x04:
2180 if cmd & 0x04:
2179 offset |= ord(binchunk[i : i + 1]) << 16
2181 offset |= ord(binchunk[i : i + 1]) << 16
2180 i += 1
2182 i += 1
2181 if cmd & 0x08:
2183 if cmd & 0x08:
2182 offset |= ord(binchunk[i : i + 1]) << 24
2184 offset |= ord(binchunk[i : i + 1]) << 24
2183 i += 1
2185 i += 1
2184 if cmd & 0x10:
2186 if cmd & 0x10:
2185 size = ord(binchunk[i : i + 1])
2187 size = ord(binchunk[i : i + 1])
2186 i += 1
2188 i += 1
2187 if cmd & 0x20:
2189 if cmd & 0x20:
2188 size |= ord(binchunk[i : i + 1]) << 8
2190 size |= ord(binchunk[i : i + 1]) << 8
2189 i += 1
2191 i += 1
2190 if cmd & 0x40:
2192 if cmd & 0x40:
2191 size |= ord(binchunk[i : i + 1]) << 16
2193 size |= ord(binchunk[i : i + 1]) << 16
2192 i += 1
2194 i += 1
2193 if size == 0:
2195 if size == 0:
2194 size = 0x10000
2196 size = 0x10000
2195 offset_end = offset + size
2197 offset_end = offset + size
2196 out += data[offset:offset_end]
2198 out += data[offset:offset_end]
2197 elif cmd != 0:
2199 elif cmd != 0:
2198 offset_end = i + cmd
2200 offset_end = i + cmd
2199 out += binchunk[i:offset_end]
2201 out += binchunk[i:offset_end]
2200 i += cmd
2202 i += cmd
2201 else:
2203 else:
2202 raise PatchApplicationError(_(b'unexpected delta opcode 0'))
2204 raise PatchApplicationError(_(b'unexpected delta opcode 0'))
2203 return out
2205 return out
2204
2206
2205
2207
2206 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'):
2208 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'):
2207 """Reads a patch from fp and tries to apply it.
2209 """Reads a patch from fp and tries to apply it.
2208
2210
2209 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2211 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2210 there was any fuzz.
2212 there was any fuzz.
2211
2213
2212 If 'eolmode' is 'strict', the patch content and patched file are
2214 If 'eolmode' is 'strict', the patch content and patched file are
2213 read in binary mode. Otherwise, line endings are ignored when
2215 read in binary mode. Otherwise, line endings are ignored when
2214 patching then normalized according to 'eolmode'.
2216 patching then normalized according to 'eolmode'.
2215 """
2217 """
2216 return _applydiff(
2218 return _applydiff(
2217 ui,
2219 ui,
2218 fp,
2220 fp,
2219 patchfile,
2221 patchfile,
2220 backend,
2222 backend,
2221 store,
2223 store,
2222 strip=strip,
2224 strip=strip,
2223 prefix=prefix,
2225 prefix=prefix,
2224 eolmode=eolmode,
2226 eolmode=eolmode,
2225 )
2227 )
2226
2228
2227
2229
2228 def _canonprefix(repo, prefix):
2230 def _canonprefix(repo, prefix):
2229 if prefix:
2231 if prefix:
2230 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2232 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2231 if prefix != b'':
2233 if prefix != b'':
2232 prefix += b'/'
2234 prefix += b'/'
2233 return prefix
2235 return prefix
2234
2236
2235
2237
2236 def _applydiff(
2238 def _applydiff(
2237 ui, fp, patcher, backend, store, strip=1, prefix=b'', eolmode=b'strict'
2239 ui, fp, patcher, backend, store, strip=1, prefix=b'', eolmode=b'strict'
2238 ):
2240 ):
2239 prefix = _canonprefix(backend.repo, prefix)
2241 prefix = _canonprefix(backend.repo, prefix)
2240
2242
2241 def pstrip(p):
2243 def pstrip(p):
2242 return pathtransform(p, strip - 1, prefix)[1]
2244 return pathtransform(p, strip - 1, prefix)[1]
2243
2245
2244 rejects = 0
2246 rejects = 0
2245 err = 0
2247 err = 0
2246 current_file = None
2248 current_file = None
2247
2249
2248 for state, values in iterhunks(fp):
2250 for state, values in iterhunks(fp):
2249 if state == b'hunk':
2251 if state == b'hunk':
2250 if not current_file:
2252 if not current_file:
2251 continue
2253 continue
2252 ret = current_file.apply(values)
2254 ret = current_file.apply(values)
2253 if ret > 0:
2255 if ret > 0:
2254 err = 1
2256 err = 1
2255 elif state == b'file':
2257 elif state == b'file':
2256 if current_file:
2258 if current_file:
2257 rejects += current_file.close()
2259 rejects += current_file.close()
2258 current_file = None
2260 current_file = None
2259 afile, bfile, first_hunk, gp = values
2261 afile, bfile, first_hunk, gp = values
2260 if gp:
2262 if gp:
2261 gp.path = pstrip(gp.path)
2263 gp.path = pstrip(gp.path)
2262 if gp.oldpath:
2264 if gp.oldpath:
2263 gp.oldpath = pstrip(gp.oldpath)
2265 gp.oldpath = pstrip(gp.oldpath)
2264 else:
2266 else:
2265 gp = makepatchmeta(
2267 gp = makepatchmeta(
2266 backend, afile, bfile, first_hunk, strip, prefix
2268 backend, afile, bfile, first_hunk, strip, prefix
2267 )
2269 )
2268 if gp.op == b'RENAME':
2270 if gp.op == b'RENAME':
2269 backend.unlink(gp.oldpath)
2271 backend.unlink(gp.oldpath)
2270 if not first_hunk:
2272 if not first_hunk:
2271 if gp.op == b'DELETE':
2273 if gp.op == b'DELETE':
2272 backend.unlink(gp.path)
2274 backend.unlink(gp.path)
2273 continue
2275 continue
2274 data, mode = None, None
2276 data, mode = None, None
2275 if gp.op in (b'RENAME', b'COPY'):
2277 if gp.op in (b'RENAME', b'COPY'):
2276 data, mode = store.getfile(gp.oldpath)[:2]
2278 data, mode = store.getfile(gp.oldpath)[:2]
2277 if data is None:
2279 if data is None:
2278 # This means that the old path does not exist
2280 # This means that the old path does not exist
2279 raise PatchApplicationError(
2281 raise PatchApplicationError(
2280 _(b"source file '%s' does not exist") % gp.oldpath
2282 _(b"source file '%s' does not exist") % gp.oldpath
2281 )
2283 )
2282 if gp.mode:
2284 if gp.mode:
2283 mode = gp.mode
2285 mode = gp.mode
2284 if gp.op == b'ADD':
2286 if gp.op == b'ADD':
2285 # Added files without content have no hunk and
2287 # Added files without content have no hunk and
2286 # must be created
2288 # must be created
2287 data = b''
2289 data = b''
2288 if data or mode:
2290 if data or mode:
2289 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists(
2291 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists(
2290 gp.path
2292 gp.path
2291 ):
2293 ):
2292 raise PatchApplicationError(
2294 raise PatchApplicationError(
2293 _(
2295 _(
2294 b"cannot create %s: destination "
2296 b"cannot create %s: destination "
2295 b"already exists"
2297 b"already exists"
2296 )
2298 )
2297 % gp.path
2299 % gp.path
2298 )
2300 )
2299 backend.setfile(gp.path, data, mode, gp.oldpath)
2301 backend.setfile(gp.path, data, mode, gp.oldpath)
2300 continue
2302 continue
2301 try:
2303 try:
2302 current_file = patcher(ui, gp, backend, store, eolmode=eolmode)
2304 current_file = patcher(ui, gp, backend, store, eolmode=eolmode)
2303 except PatchError as inst:
2305 except PatchError as inst:
2304 ui.warn(stringutil.forcebytestr(inst) + b'\n')
2306 ui.warn(stringutil.forcebytestr(inst) + b'\n')
2305 current_file = None
2307 current_file = None
2306 rejects += 1
2308 rejects += 1
2307 continue
2309 continue
2308 elif state == b'git':
2310 elif state == b'git':
2309 for gp in values:
2311 for gp in values:
2310 path = pstrip(gp.oldpath)
2312 path = pstrip(gp.oldpath)
2311 data, mode = backend.getfile(path)
2313 data, mode = backend.getfile(path)
2312 if data is None:
2314 if data is None:
2313 # The error ignored here will trigger a getfile()
2315 # The error ignored here will trigger a getfile()
2314 # error in a place more appropriate for error
2316 # error in a place more appropriate for error
2315 # handling, and will not interrupt the patching
2317 # handling, and will not interrupt the patching
2316 # process.
2318 # process.
2317 pass
2319 pass
2318 else:
2320 else:
2319 store.setfile(path, data, mode)
2321 store.setfile(path, data, mode)
2320 else:
2322 else:
2321 raise error.Abort(_(b'unsupported parser state: %s') % state)
2323 raise error.Abort(_(b'unsupported parser state: %s') % state)
2322
2324
2323 if current_file:
2325 if current_file:
2324 rejects += current_file.close()
2326 rejects += current_file.close()
2325
2327
2326 if rejects:
2328 if rejects:
2327 return -1
2329 return -1
2328 return err
2330 return err
2329
2331
2330
2332
2331 def _externalpatch(ui, repo, patcher, patchname, strip, files, similarity):
2333 def _externalpatch(ui, repo, patcher, patchname, strip, files, similarity):
2332 """use <patcher> to apply <patchname> to the working directory.
2334 """use <patcher> to apply <patchname> to the working directory.
2333 returns whether patch was applied with fuzz factor."""
2335 returns whether patch was applied with fuzz factor."""
2334
2336
2335 fuzz = False
2337 fuzz = False
2336 args = []
2338 args = []
2337 cwd = repo.root
2339 cwd = repo.root
2338 if cwd:
2340 if cwd:
2339 args.append(b'-d %s' % procutil.shellquote(cwd))
2341 args.append(b'-d %s' % procutil.shellquote(cwd))
2340 cmd = b'%s %s -p%d < %s' % (
2342 cmd = b'%s %s -p%d < %s' % (
2341 patcher,
2343 patcher,
2342 b' '.join(args),
2344 b' '.join(args),
2343 strip,
2345 strip,
2344 procutil.shellquote(patchname),
2346 procutil.shellquote(patchname),
2345 )
2347 )
2346 ui.debug(b'Using external patch tool: %s\n' % cmd)
2348 ui.debug(b'Using external patch tool: %s\n' % cmd)
2347 fp = procutil.popen(cmd, b'rb')
2349 fp = procutil.popen(cmd, b'rb')
2348 try:
2350 try:
2349 for line in util.iterfile(fp):
2351 for line in util.iterfile(fp):
2350 line = line.rstrip()
2352 line = line.rstrip()
2351 ui.note(line + b'\n')
2353 ui.note(line + b'\n')
2352 if line.startswith(b'patching file '):
2354 if line.startswith(b'patching file '):
2353 pf = util.parsepatchoutput(line)
2355 pf = util.parsepatchoutput(line)
2354 printed_file = False
2356 printed_file = False
2355 files.add(pf)
2357 files.add(pf)
2356 elif line.find(b'with fuzz') >= 0:
2358 elif line.find(b'with fuzz') >= 0:
2357 fuzz = True
2359 fuzz = True
2358 if not printed_file:
2360 if not printed_file:
2359 ui.warn(pf + b'\n')
2361 ui.warn(pf + b'\n')
2360 printed_file = True
2362 printed_file = True
2361 ui.warn(line + b'\n')
2363 ui.warn(line + b'\n')
2362 elif line.find(b'saving rejects to file') >= 0:
2364 elif line.find(b'saving rejects to file') >= 0:
2363 ui.warn(line + b'\n')
2365 ui.warn(line + b'\n')
2364 elif line.find(b'FAILED') >= 0:
2366 elif line.find(b'FAILED') >= 0:
2365 if not printed_file:
2367 if not printed_file:
2366 ui.warn(pf + b'\n')
2368 ui.warn(pf + b'\n')
2367 printed_file = True
2369 printed_file = True
2368 ui.warn(line + b'\n')
2370 ui.warn(line + b'\n')
2369 finally:
2371 finally:
2370 if files:
2372 if files:
2371 scmutil.marktouched(repo, files, similarity)
2373 scmutil.marktouched(repo, files, similarity)
2372 code = fp.close()
2374 code = fp.close()
2373 if code:
2375 if code:
2374 raise PatchApplicationError(
2376 raise PatchApplicationError(
2375 _(b"patch command failed: %s") % procutil.explainexit(code)
2377 _(b"patch command failed: %s") % procutil.explainexit(code)
2376 )
2378 )
2377 return fuzz
2379 return fuzz
2378
2380
2379
2381
2380 def patchbackend(
2382 def patchbackend(
2381 ui, backend, patchobj, strip, prefix, files=None, eolmode=b'strict'
2383 ui, backend, patchobj, strip, prefix, files=None, eolmode=b'strict'
2382 ):
2384 ):
2383 if files is None:
2385 if files is None:
2384 files = set()
2386 files = set()
2385 if eolmode is None:
2387 if eolmode is None:
2386 eolmode = ui.config(b'patch', b'eol')
2388 eolmode = ui.config(b'patch', b'eol')
2387 if eolmode.lower() not in eolmodes:
2389 if eolmode.lower() not in eolmodes:
2388 raise error.Abort(_(b'unsupported line endings type: %s') % eolmode)
2390 raise error.Abort(_(b'unsupported line endings type: %s') % eolmode)
2389 eolmode = eolmode.lower()
2391 eolmode = eolmode.lower()
2390
2392
2391 store = filestore()
2393 store = filestore()
2392 try:
2394 try:
2393 fp = open(patchobj, b'rb')
2395 fp = open(patchobj, b'rb')
2394 except TypeError:
2396 except TypeError:
2395 fp = patchobj
2397 fp = patchobj
2396 try:
2398 try:
2397 ret = applydiff(
2399 ret = applydiff(
2398 ui, fp, backend, store, strip=strip, prefix=prefix, eolmode=eolmode
2400 ui, fp, backend, store, strip=strip, prefix=prefix, eolmode=eolmode
2399 )
2401 )
2400 finally:
2402 finally:
2401 if fp != patchobj:
2403 if fp != patchobj:
2402 fp.close()
2404 fp.close()
2403 files.update(backend.close())
2405 files.update(backend.close())
2404 store.close()
2406 store.close()
2405 if ret < 0:
2407 if ret < 0:
2406 raise PatchApplicationError(_(b'patch failed to apply'))
2408 raise PatchApplicationError(_(b'patch failed to apply'))
2407 return ret > 0
2409 return ret > 0
2408
2410
2409
2411
2410 def internalpatch(
2412 def internalpatch(
2411 ui,
2413 ui,
2412 repo,
2414 repo,
2413 patchobj,
2415 patchobj,
2414 strip,
2416 strip,
2415 prefix=b'',
2417 prefix=b'',
2416 files=None,
2418 files=None,
2417 eolmode=b'strict',
2419 eolmode=b'strict',
2418 similarity=0,
2420 similarity=0,
2419 ):
2421 ):
2420 """use builtin patch to apply <patchobj> to the working directory.
2422 """use builtin patch to apply <patchobj> to the working directory.
2421 returns whether patch was applied with fuzz factor."""
2423 returns whether patch was applied with fuzz factor."""
2422 backend = workingbackend(ui, repo, similarity)
2424 backend = workingbackend(ui, repo, similarity)
2423 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2425 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2424
2426
2425
2427
2426 def patchrepo(
2428 def patchrepo(
2427 ui, repo, ctx, store, patchobj, strip, prefix, files=None, eolmode=b'strict'
2429 ui, repo, ctx, store, patchobj, strip, prefix, files=None, eolmode=b'strict'
2428 ):
2430 ):
2429 backend = repobackend(ui, repo, ctx, store)
2431 backend = repobackend(ui, repo, ctx, store)
2430 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2432 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2431
2433
2432
2434
2433 def patch(
2435 def patch(
2434 ui,
2436 ui,
2435 repo,
2437 repo,
2436 patchname,
2438 patchname,
2437 strip=1,
2439 strip=1,
2438 prefix=b'',
2440 prefix=b'',
2439 files=None,
2441 files=None,
2440 eolmode=b'strict',
2442 eolmode=b'strict',
2441 similarity=0,
2443 similarity=0,
2442 ):
2444 ):
2443 """Apply <patchname> to the working directory.
2445 """Apply <patchname> to the working directory.
2444
2446
2445 'eolmode' specifies how end of lines should be handled. It can be:
2447 'eolmode' specifies how end of lines should be handled. It can be:
2446 - 'strict': inputs are read in binary mode, EOLs are preserved
2448 - 'strict': inputs are read in binary mode, EOLs are preserved
2447 - 'crlf': EOLs are ignored when patching and reset to CRLF
2449 - 'crlf': EOLs are ignored when patching and reset to CRLF
2448 - 'lf': EOLs are ignored when patching and reset to LF
2450 - 'lf': EOLs are ignored when patching and reset to LF
2449 - None: get it from user settings, default to 'strict'
2451 - None: get it from user settings, default to 'strict'
2450 'eolmode' is ignored when using an external patcher program.
2452 'eolmode' is ignored when using an external patcher program.
2451
2453
2452 Returns whether patch was applied with fuzz factor.
2454 Returns whether patch was applied with fuzz factor.
2453 """
2455 """
2454 patcher = ui.config(b'ui', b'patch')
2456 patcher = ui.config(b'ui', b'patch')
2455 if files is None:
2457 if files is None:
2456 files = set()
2458 files = set()
2457 if patcher:
2459 if patcher:
2458 return _externalpatch(
2460 return _externalpatch(
2459 ui, repo, patcher, patchname, strip, files, similarity
2461 ui, repo, patcher, patchname, strip, files, similarity
2460 )
2462 )
2461 return internalpatch(
2463 return internalpatch(
2462 ui, repo, patchname, strip, prefix, files, eolmode, similarity
2464 ui, repo, patchname, strip, prefix, files, eolmode, similarity
2463 )
2465 )
2464
2466
2465
2467
2466 def changedfiles(ui, repo, patchpath, strip=1, prefix=b''):
2468 def changedfiles(ui, repo, patchpath, strip=1, prefix=b''):
2467 backend = fsbackend(ui, repo.root)
2469 backend = fsbackend(ui, repo.root)
2468 prefix = _canonprefix(repo, prefix)
2470 prefix = _canonprefix(repo, prefix)
2469 with open(patchpath, b'rb') as fp:
2471 with open(patchpath, b'rb') as fp:
2470 changed = set()
2472 changed = set()
2471 for state, values in iterhunks(fp):
2473 for state, values in iterhunks(fp):
2472 if state == b'file':
2474 if state == b'file':
2473 afile, bfile, first_hunk, gp = values
2475 afile, bfile, first_hunk, gp = values
2474 if gp:
2476 if gp:
2475 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2477 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2476 if gp.oldpath:
2478 if gp.oldpath:
2477 gp.oldpath = pathtransform(
2479 gp.oldpath = pathtransform(
2478 gp.oldpath, strip - 1, prefix
2480 gp.oldpath, strip - 1, prefix
2479 )[1]
2481 )[1]
2480 else:
2482 else:
2481 gp = makepatchmeta(
2483 gp = makepatchmeta(
2482 backend, afile, bfile, first_hunk, strip, prefix
2484 backend, afile, bfile, first_hunk, strip, prefix
2483 )
2485 )
2484 changed.add(gp.path)
2486 changed.add(gp.path)
2485 if gp.op == b'RENAME':
2487 if gp.op == b'RENAME':
2486 changed.add(gp.oldpath)
2488 changed.add(gp.oldpath)
2487 elif state not in (b'hunk', b'git'):
2489 elif state not in (b'hunk', b'git'):
2488 raise error.Abort(_(b'unsupported parser state: %s') % state)
2490 raise error.Abort(_(b'unsupported parser state: %s') % state)
2489 return changed
2491 return changed
2490
2492
2491
2493
2492 class GitDiffRequired(Exception):
2494 class GitDiffRequired(Exception):
2493 pass
2495 pass
2494
2496
2495
2497
2496 diffopts = diffutil.diffallopts
2498 diffopts = diffutil.diffallopts
2497 diffallopts = diffutil.diffallopts
2499 diffallopts = diffutil.diffallopts
2498 difffeatureopts = diffutil.difffeatureopts
2500 difffeatureopts = diffutil.difffeatureopts
2499
2501
2500
2502
2501 def diff(
2503 def diff(
2502 repo,
2504 repo,
2503 node1=None,
2505 node1=None,
2504 node2=None,
2506 node2=None,
2505 match=None,
2507 match=None,
2506 changes=None,
2508 changes=None,
2507 opts=None,
2509 opts=None,
2508 losedatafn=None,
2510 losedatafn=None,
2509 pathfn=None,
2511 pathfn=None,
2510 copy=None,
2512 copy=None,
2511 copysourcematch=None,
2513 copysourcematch=None,
2512 hunksfilterfn=None,
2514 hunksfilterfn=None,
2513 ):
2515 ):
2514 """yields diff of changes to files between two nodes, or node and
2516 """yields diff of changes to files between two nodes, or node and
2515 working directory.
2517 working directory.
2516
2518
2517 if node1 is None, use first dirstate parent instead.
2519 if node1 is None, use first dirstate parent instead.
2518 if node2 is None, compare node1 with working directory.
2520 if node2 is None, compare node1 with working directory.
2519
2521
2520 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2522 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2521 every time some change cannot be represented with the current
2523 every time some change cannot be represented with the current
2522 patch format. Return False to upgrade to git patch format, True to
2524 patch format. Return False to upgrade to git patch format, True to
2523 accept the loss or raise an exception to abort the diff. It is
2525 accept the loss or raise an exception to abort the diff. It is
2524 called with the name of current file being diffed as 'fn'. If set
2526 called with the name of current file being diffed as 'fn'. If set
2525 to None, patches will always be upgraded to git format when
2527 to None, patches will always be upgraded to git format when
2526 necessary.
2528 necessary.
2527
2529
2528 prefix is a filename prefix that is prepended to all filenames on
2530 prefix is a filename prefix that is prepended to all filenames on
2529 display (used for subrepos).
2531 display (used for subrepos).
2530
2532
2531 relroot, if not empty, must be normalized with a trailing /. Any match
2533 relroot, if not empty, must be normalized with a trailing /. Any match
2532 patterns that fall outside it will be ignored.
2534 patterns that fall outside it will be ignored.
2533
2535
2534 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2536 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2535 information.
2537 information.
2536
2538
2537 if copysourcematch is not None, then copy sources will be filtered by this
2539 if copysourcematch is not None, then copy sources will be filtered by this
2538 matcher
2540 matcher
2539
2541
2540 hunksfilterfn, if not None, should be a function taking a filectx and
2542 hunksfilterfn, if not None, should be a function taking a filectx and
2541 hunks generator that may yield filtered hunks.
2543 hunks generator that may yield filtered hunks.
2542 """
2544 """
2543 if not node1 and not node2:
2545 if not node1 and not node2:
2544 node1 = repo.dirstate.p1()
2546 node1 = repo.dirstate.p1()
2545
2547
2546 ctx1 = repo[node1]
2548 ctx1 = repo[node1]
2547 ctx2 = repo[node2]
2549 ctx2 = repo[node2]
2548
2550
2549 for fctx1, fctx2, hdr, hunks in diffhunks(
2551 for fctx1, fctx2, hdr, hunks in diffhunks(
2550 repo,
2552 repo,
2551 ctx1=ctx1,
2553 ctx1=ctx1,
2552 ctx2=ctx2,
2554 ctx2=ctx2,
2553 match=match,
2555 match=match,
2554 changes=changes,
2556 changes=changes,
2555 opts=opts,
2557 opts=opts,
2556 losedatafn=losedatafn,
2558 losedatafn=losedatafn,
2557 pathfn=pathfn,
2559 pathfn=pathfn,
2558 copy=copy,
2560 copy=copy,
2559 copysourcematch=copysourcematch,
2561 copysourcematch=copysourcematch,
2560 ):
2562 ):
2561 if hunksfilterfn is not None:
2563 if hunksfilterfn is not None:
2562 # If the file has been removed, fctx2 is None; but this should
2564 # If the file has been removed, fctx2 is None; but this should
2563 # not occur here since we catch removed files early in
2565 # not occur here since we catch removed files early in
2564 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2566 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2565 assert (
2567 assert (
2566 fctx2 is not None
2568 fctx2 is not None
2567 ), b'fctx2 unexpectly None in diff hunks filtering'
2569 ), b'fctx2 unexpectly None in diff hunks filtering'
2568 hunks = hunksfilterfn(fctx2, hunks)
2570 hunks = hunksfilterfn(fctx2, hunks)
2569 text = b''.join(b''.join(hlines) for hrange, hlines in hunks)
2571 text = b''.join(b''.join(hlines) for hrange, hlines in hunks)
2570 if hdr and (text or len(hdr) > 1):
2572 if hdr and (text or len(hdr) > 1):
2571 yield b'\n'.join(hdr) + b'\n'
2573 yield b'\n'.join(hdr) + b'\n'
2572 if text:
2574 if text:
2573 yield text
2575 yield text
2574
2576
2575
2577
2576 def diffhunks(
2578 def diffhunks(
2577 repo,
2579 repo,
2578 ctx1,
2580 ctx1,
2579 ctx2,
2581 ctx2,
2580 match=None,
2582 match=None,
2581 changes=None,
2583 changes=None,
2582 opts=None,
2584 opts=None,
2583 losedatafn=None,
2585 losedatafn=None,
2584 pathfn=None,
2586 pathfn=None,
2585 copy=None,
2587 copy=None,
2586 copysourcematch=None,
2588 copysourcematch=None,
2587 ):
2589 ):
2588 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2590 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2589 where `header` is a list of diff headers and `hunks` is an iterable of
2591 where `header` is a list of diff headers and `hunks` is an iterable of
2590 (`hunkrange`, `hunklines`) tuples.
2592 (`hunkrange`, `hunklines`) tuples.
2591
2593
2592 See diff() for the meaning of parameters.
2594 See diff() for the meaning of parameters.
2593 """
2595 """
2594
2596
2595 if opts is None:
2597 if opts is None:
2596 opts = mdiff.defaultopts
2598 opts = mdiff.defaultopts
2597
2599
2598 def lrugetfilectx():
2600 def lrugetfilectx():
2599 cache = {}
2601 cache = {}
2600 order = collections.deque()
2602 order = collections.deque()
2601
2603
2602 def getfilectx(f, ctx):
2604 def getfilectx(f, ctx):
2603 fctx = ctx.filectx(f, filelog=cache.get(f))
2605 fctx = ctx.filectx(f, filelog=cache.get(f))
2604 if f not in cache:
2606 if f not in cache:
2605 if len(cache) > 20:
2607 if len(cache) > 20:
2606 del cache[order.popleft()]
2608 del cache[order.popleft()]
2607 cache[f] = fctx.filelog()
2609 cache[f] = fctx.filelog()
2608 else:
2610 else:
2609 order.remove(f)
2611 order.remove(f)
2610 order.append(f)
2612 order.append(f)
2611 return fctx
2613 return fctx
2612
2614
2613 return getfilectx
2615 return getfilectx
2614
2616
2615 getfilectx = lrugetfilectx()
2617 getfilectx = lrugetfilectx()
2616
2618
2617 if not changes:
2619 if not changes:
2618 changes = ctx1.status(ctx2, match=match)
2620 changes = ctx1.status(ctx2, match=match)
2619 if isinstance(changes, list):
2621 if isinstance(changes, list):
2620 modified, added, removed = changes[:3]
2622 modified, added, removed = changes[:3]
2621 else:
2623 else:
2622 modified, added, removed = (
2624 modified, added, removed = (
2623 changes.modified,
2625 changes.modified,
2624 changes.added,
2626 changes.added,
2625 changes.removed,
2627 changes.removed,
2626 )
2628 )
2627
2629
2628 if not modified and not added and not removed:
2630 if not modified and not added and not removed:
2629 return []
2631 return []
2630
2632
2631 if repo.ui.debugflag:
2633 if repo.ui.debugflag:
2632 hexfunc = hex
2634 hexfunc = hex
2633 else:
2635 else:
2634 hexfunc = short
2636 hexfunc = short
2635 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2637 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2636
2638
2637 if copy is None:
2639 if copy is None:
2638 copy = {}
2640 copy = {}
2639 if opts.git or opts.upgrade:
2641 if opts.git or opts.upgrade:
2640 copy = copies.pathcopies(ctx1, ctx2, match=match)
2642 copy = copies.pathcopies(ctx1, ctx2, match=match)
2641
2643
2642 if copysourcematch:
2644 if copysourcematch:
2643 # filter out copies where source side isn't inside the matcher
2645 # filter out copies where source side isn't inside the matcher
2644 # (copies.pathcopies() already filtered out the destination)
2646 # (copies.pathcopies() already filtered out the destination)
2645 copy = {
2647 copy = {
2646 dst: src
2648 dst: src
2647 for dst, src in pycompat.iteritems(copy)
2649 for dst, src in pycompat.iteritems(copy)
2648 if copysourcematch(src)
2650 if copysourcematch(src)
2649 }
2651 }
2650
2652
2651 modifiedset = set(modified)
2653 modifiedset = set(modified)
2652 addedset = set(added)
2654 addedset = set(added)
2653 removedset = set(removed)
2655 removedset = set(removed)
2654 for f in modified:
2656 for f in modified:
2655 if f not in ctx1:
2657 if f not in ctx1:
2656 # Fix up added, since merged-in additions appear as
2658 # Fix up added, since merged-in additions appear as
2657 # modifications during merges
2659 # modifications during merges
2658 modifiedset.remove(f)
2660 modifiedset.remove(f)
2659 addedset.add(f)
2661 addedset.add(f)
2660 for f in removed:
2662 for f in removed:
2661 if f not in ctx1:
2663 if f not in ctx1:
2662 # Merged-in additions that are then removed are reported as removed.
2664 # Merged-in additions that are then removed are reported as removed.
2663 # They are not in ctx1, so We don't want to show them in the diff.
2665 # They are not in ctx1, so We don't want to show them in the diff.
2664 removedset.remove(f)
2666 removedset.remove(f)
2665 modified = sorted(modifiedset)
2667 modified = sorted(modifiedset)
2666 added = sorted(addedset)
2668 added = sorted(addedset)
2667 removed = sorted(removedset)
2669 removed = sorted(removedset)
2668 for dst, src in list(copy.items()):
2670 for dst, src in list(copy.items()):
2669 if src not in ctx1:
2671 if src not in ctx1:
2670 # Files merged in during a merge and then copied/renamed are
2672 # Files merged in during a merge and then copied/renamed are
2671 # reported as copies. We want to show them in the diff as additions.
2673 # reported as copies. We want to show them in the diff as additions.
2672 del copy[dst]
2674 del copy[dst]
2673
2675
2674 prefetchmatch = scmutil.matchfiles(
2676 prefetchmatch = scmutil.matchfiles(
2675 repo, list(modifiedset | addedset | removedset)
2677 repo, list(modifiedset | addedset | removedset)
2676 )
2678 )
2677 revmatches = [
2679 revmatches = [
2678 (ctx1.rev(), prefetchmatch),
2680 (ctx1.rev(), prefetchmatch),
2679 (ctx2.rev(), prefetchmatch),
2681 (ctx2.rev(), prefetchmatch),
2680 ]
2682 ]
2681 scmutil.prefetchfiles(repo, revmatches)
2683 scmutil.prefetchfiles(repo, revmatches)
2682
2684
2683 def difffn(opts, losedata):
2685 def difffn(opts, losedata):
2684 return trydiff(
2686 return trydiff(
2685 repo,
2687 repo,
2686 revs,
2688 revs,
2687 ctx1,
2689 ctx1,
2688 ctx2,
2690 ctx2,
2689 modified,
2691 modified,
2690 added,
2692 added,
2691 removed,
2693 removed,
2692 copy,
2694 copy,
2693 getfilectx,
2695 getfilectx,
2694 opts,
2696 opts,
2695 losedata,
2697 losedata,
2696 pathfn,
2698 pathfn,
2697 )
2699 )
2698
2700
2699 if opts.upgrade and not opts.git:
2701 if opts.upgrade and not opts.git:
2700 try:
2702 try:
2701
2703
2702 def losedata(fn):
2704 def losedata(fn):
2703 if not losedatafn or not losedatafn(fn=fn):
2705 if not losedatafn or not losedatafn(fn=fn):
2704 raise GitDiffRequired
2706 raise GitDiffRequired
2705
2707
2706 # Buffer the whole output until we are sure it can be generated
2708 # Buffer the whole output until we are sure it can be generated
2707 return list(difffn(opts.copy(git=False), losedata))
2709 return list(difffn(opts.copy(git=False), losedata))
2708 except GitDiffRequired:
2710 except GitDiffRequired:
2709 return difffn(opts.copy(git=True), None)
2711 return difffn(opts.copy(git=True), None)
2710 else:
2712 else:
2711 return difffn(opts, None)
2713 return difffn(opts, None)
2712
2714
2713
2715
2714 def diffsinglehunk(hunklines):
2716 def diffsinglehunk(hunklines):
2715 """yield tokens for a list of lines in a single hunk"""
2717 """yield tokens for a list of lines in a single hunk"""
2716 for line in hunklines:
2718 for line in hunklines:
2717 # chomp
2719 # chomp
2718 chompline = line.rstrip(b'\r\n')
2720 chompline = line.rstrip(b'\r\n')
2719 # highlight tabs and trailing whitespace
2721 # highlight tabs and trailing whitespace
2720 stripline = chompline.rstrip()
2722 stripline = chompline.rstrip()
2721 if line.startswith(b'-'):
2723 if line.startswith(b'-'):
2722 label = b'diff.deleted'
2724 label = b'diff.deleted'
2723 elif line.startswith(b'+'):
2725 elif line.startswith(b'+'):
2724 label = b'diff.inserted'
2726 label = b'diff.inserted'
2725 else:
2727 else:
2726 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2728 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2727 for token in tabsplitter.findall(stripline):
2729 for token in tabsplitter.findall(stripline):
2728 if token.startswith(b'\t'):
2730 if token.startswith(b'\t'):
2729 yield (token, b'diff.tab')
2731 yield (token, b'diff.tab')
2730 else:
2732 else:
2731 yield (token, label)
2733 yield (token, label)
2732
2734
2733 if chompline != stripline:
2735 if chompline != stripline:
2734 yield (chompline[len(stripline) :], b'diff.trailingwhitespace')
2736 yield (chompline[len(stripline) :], b'diff.trailingwhitespace')
2735 if chompline != line:
2737 if chompline != line:
2736 yield (line[len(chompline) :], b'')
2738 yield (line[len(chompline) :], b'')
2737
2739
2738
2740
2739 def diffsinglehunkinline(hunklines):
2741 def diffsinglehunkinline(hunklines):
2740 """yield tokens for a list of lines in a single hunk, with inline colors"""
2742 """yield tokens for a list of lines in a single hunk, with inline colors"""
2741 # prepare deleted, and inserted content
2743 # prepare deleted, and inserted content
2742 a = bytearray()
2744 a = bytearray()
2743 b = bytearray()
2745 b = bytearray()
2744 for line in hunklines:
2746 for line in hunklines:
2745 if line[0:1] == b'-':
2747 if line[0:1] == b'-':
2746 a += line[1:]
2748 a += line[1:]
2747 elif line[0:1] == b'+':
2749 elif line[0:1] == b'+':
2748 b += line[1:]
2750 b += line[1:]
2749 else:
2751 else:
2750 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2752 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2751 # fast path: if either side is empty, use diffsinglehunk
2753 # fast path: if either side is empty, use diffsinglehunk
2752 if not a or not b:
2754 if not a or not b:
2753 for t in diffsinglehunk(hunklines):
2755 for t in diffsinglehunk(hunklines):
2754 yield t
2756 yield t
2755 return
2757 return
2756 # re-split the content into words
2758 # re-split the content into words
2757 al = wordsplitter.findall(bytes(a))
2759 al = wordsplitter.findall(bytes(a))
2758 bl = wordsplitter.findall(bytes(b))
2760 bl = wordsplitter.findall(bytes(b))
2759 # re-arrange the words to lines since the diff algorithm is line-based
2761 # re-arrange the words to lines since the diff algorithm is line-based
2760 aln = [s if s == b'\n' else s + b'\n' for s in al]
2762 aln = [s if s == b'\n' else s + b'\n' for s in al]
2761 bln = [s if s == b'\n' else s + b'\n' for s in bl]
2763 bln = [s if s == b'\n' else s + b'\n' for s in bl]
2762 an = b''.join(aln)
2764 an = b''.join(aln)
2763 bn = b''.join(bln)
2765 bn = b''.join(bln)
2764 # run the diff algorithm, prepare atokens and btokens
2766 # run the diff algorithm, prepare atokens and btokens
2765 atokens = []
2767 atokens = []
2766 btokens = []
2768 btokens = []
2767 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2769 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2768 for (a1, a2, b1, b2), btype in blocks:
2770 for (a1, a2, b1, b2), btype in blocks:
2769 changed = btype == b'!'
2771 changed = btype == b'!'
2770 for token in mdiff.splitnewlines(b''.join(al[a1:a2])):
2772 for token in mdiff.splitnewlines(b''.join(al[a1:a2])):
2771 atokens.append((changed, token))
2773 atokens.append((changed, token))
2772 for token in mdiff.splitnewlines(b''.join(bl[b1:b2])):
2774 for token in mdiff.splitnewlines(b''.join(bl[b1:b2])):
2773 btokens.append((changed, token))
2775 btokens.append((changed, token))
2774
2776
2775 # yield deleted tokens, then inserted ones
2777 # yield deleted tokens, then inserted ones
2776 for prefix, label, tokens in [
2778 for prefix, label, tokens in [
2777 (b'-', b'diff.deleted', atokens),
2779 (b'-', b'diff.deleted', atokens),
2778 (b'+', b'diff.inserted', btokens),
2780 (b'+', b'diff.inserted', btokens),
2779 ]:
2781 ]:
2780 nextisnewline = True
2782 nextisnewline = True
2781 for changed, token in tokens:
2783 for changed, token in tokens:
2782 if nextisnewline:
2784 if nextisnewline:
2783 yield (prefix, label)
2785 yield (prefix, label)
2784 nextisnewline = False
2786 nextisnewline = False
2785 # special handling line end
2787 # special handling line end
2786 isendofline = token.endswith(b'\n')
2788 isendofline = token.endswith(b'\n')
2787 if isendofline:
2789 if isendofline:
2788 chomp = token[:-1] # chomp
2790 chomp = token[:-1] # chomp
2789 if chomp.endswith(b'\r'):
2791 if chomp.endswith(b'\r'):
2790 chomp = chomp[:-1]
2792 chomp = chomp[:-1]
2791 endofline = token[len(chomp) :]
2793 endofline = token[len(chomp) :]
2792 token = chomp.rstrip() # detect spaces at the end
2794 token = chomp.rstrip() # detect spaces at the end
2793 endspaces = chomp[len(token) :]
2795 endspaces = chomp[len(token) :]
2794 # scan tabs
2796 # scan tabs
2795 for maybetab in tabsplitter.findall(token):
2797 for maybetab in tabsplitter.findall(token):
2796 if b'\t' == maybetab[0:1]:
2798 if b'\t' == maybetab[0:1]:
2797 currentlabel = b'diff.tab'
2799 currentlabel = b'diff.tab'
2798 else:
2800 else:
2799 if changed:
2801 if changed:
2800 currentlabel = label + b'.changed'
2802 currentlabel = label + b'.changed'
2801 else:
2803 else:
2802 currentlabel = label + b'.unchanged'
2804 currentlabel = label + b'.unchanged'
2803 yield (maybetab, currentlabel)
2805 yield (maybetab, currentlabel)
2804 if isendofline:
2806 if isendofline:
2805 if endspaces:
2807 if endspaces:
2806 yield (endspaces, b'diff.trailingwhitespace')
2808 yield (endspaces, b'diff.trailingwhitespace')
2807 yield (endofline, b'')
2809 yield (endofline, b'')
2808 nextisnewline = True
2810 nextisnewline = True
2809
2811
2810
2812
2811 def difflabel(func, *args, **kw):
2813 def difflabel(func, *args, **kw):
2812 '''yields 2-tuples of (output, label) based on the output of func()'''
2814 '''yields 2-tuples of (output, label) based on the output of func()'''
2813 if kw.get('opts') and kw['opts'].worddiff:
2815 if kw.get('opts') and kw['opts'].worddiff:
2814 dodiffhunk = diffsinglehunkinline
2816 dodiffhunk = diffsinglehunkinline
2815 else:
2817 else:
2816 dodiffhunk = diffsinglehunk
2818 dodiffhunk = diffsinglehunk
2817 headprefixes = [
2819 headprefixes = [
2818 (b'diff', b'diff.diffline'),
2820 (b'diff', b'diff.diffline'),
2819 (b'copy', b'diff.extended'),
2821 (b'copy', b'diff.extended'),
2820 (b'rename', b'diff.extended'),
2822 (b'rename', b'diff.extended'),
2821 (b'old', b'diff.extended'),
2823 (b'old', b'diff.extended'),
2822 (b'new', b'diff.extended'),
2824 (b'new', b'diff.extended'),
2823 (b'deleted', b'diff.extended'),
2825 (b'deleted', b'diff.extended'),
2824 (b'index', b'diff.extended'),
2826 (b'index', b'diff.extended'),
2825 (b'similarity', b'diff.extended'),
2827 (b'similarity', b'diff.extended'),
2826 (b'---', b'diff.file_a'),
2828 (b'---', b'diff.file_a'),
2827 (b'+++', b'diff.file_b'),
2829 (b'+++', b'diff.file_b'),
2828 ]
2830 ]
2829 textprefixes = [
2831 textprefixes = [
2830 (b'@', b'diff.hunk'),
2832 (b'@', b'diff.hunk'),
2831 # - and + are handled by diffsinglehunk
2833 # - and + are handled by diffsinglehunk
2832 ]
2834 ]
2833 head = False
2835 head = False
2834
2836
2835 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2837 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2836 hunkbuffer = []
2838 hunkbuffer = []
2837
2839
2838 def consumehunkbuffer():
2840 def consumehunkbuffer():
2839 if hunkbuffer:
2841 if hunkbuffer:
2840 for token in dodiffhunk(hunkbuffer):
2842 for token in dodiffhunk(hunkbuffer):
2841 yield token
2843 yield token
2842 hunkbuffer[:] = []
2844 hunkbuffer[:] = []
2843
2845
2844 for chunk in func(*args, **kw):
2846 for chunk in func(*args, **kw):
2845 lines = chunk.split(b'\n')
2847 lines = chunk.split(b'\n')
2846 linecount = len(lines)
2848 linecount = len(lines)
2847 for i, line in enumerate(lines):
2849 for i, line in enumerate(lines):
2848 if head:
2850 if head:
2849 if line.startswith(b'@'):
2851 if line.startswith(b'@'):
2850 head = False
2852 head = False
2851 else:
2853 else:
2852 if line and not line.startswith(
2854 if line and not line.startswith(
2853 (b' ', b'+', b'-', b'@', b'\\')
2855 (b' ', b'+', b'-', b'@', b'\\')
2854 ):
2856 ):
2855 head = True
2857 head = True
2856 diffline = False
2858 diffline = False
2857 if not head and line and line.startswith((b'+', b'-')):
2859 if not head and line and line.startswith((b'+', b'-')):
2858 diffline = True
2860 diffline = True
2859
2861
2860 prefixes = textprefixes
2862 prefixes = textprefixes
2861 if head:
2863 if head:
2862 prefixes = headprefixes
2864 prefixes = headprefixes
2863 if diffline:
2865 if diffline:
2864 # buffered
2866 # buffered
2865 bufferedline = line
2867 bufferedline = line
2866 if i + 1 < linecount:
2868 if i + 1 < linecount:
2867 bufferedline += b"\n"
2869 bufferedline += b"\n"
2868 hunkbuffer.append(bufferedline)
2870 hunkbuffer.append(bufferedline)
2869 else:
2871 else:
2870 # unbuffered
2872 # unbuffered
2871 for token in consumehunkbuffer():
2873 for token in consumehunkbuffer():
2872 yield token
2874 yield token
2873 stripline = line.rstrip()
2875 stripline = line.rstrip()
2874 for prefix, label in prefixes:
2876 for prefix, label in prefixes:
2875 if stripline.startswith(prefix):
2877 if stripline.startswith(prefix):
2876 yield (stripline, label)
2878 yield (stripline, label)
2877 if line != stripline:
2879 if line != stripline:
2878 yield (
2880 yield (
2879 line[len(stripline) :],
2881 line[len(stripline) :],
2880 b'diff.trailingwhitespace',
2882 b'diff.trailingwhitespace',
2881 )
2883 )
2882 break
2884 break
2883 else:
2885 else:
2884 yield (line, b'')
2886 yield (line, b'')
2885 if i + 1 < linecount:
2887 if i + 1 < linecount:
2886 yield (b'\n', b'')
2888 yield (b'\n', b'')
2887 for token in consumehunkbuffer():
2889 for token in consumehunkbuffer():
2888 yield token
2890 yield token
2889
2891
2890
2892
2891 def diffui(*args, **kw):
2893 def diffui(*args, **kw):
2892 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2894 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2893 return difflabel(diff, *args, **kw)
2895 return difflabel(diff, *args, **kw)
2894
2896
2895
2897
2896 def _filepairs(modified, added, removed, copy, opts):
2898 def _filepairs(modified, added, removed, copy, opts):
2897 """generates tuples (f1, f2, copyop), where f1 is the name of the file
2899 """generates tuples (f1, f2, copyop), where f1 is the name of the file
2898 before and f2 is the the name after. For added files, f1 will be None,
2900 before and f2 is the the name after. For added files, f1 will be None,
2899 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2901 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2900 or 'rename' (the latter two only if opts.git is set)."""
2902 or 'rename' (the latter two only if opts.git is set)."""
2901 gone = set()
2903 gone = set()
2902
2904
2903 copyto = {v: k for k, v in copy.items()}
2905 copyto = {v: k for k, v in copy.items()}
2904
2906
2905 addedset, removedset = set(added), set(removed)
2907 addedset, removedset = set(added), set(removed)
2906
2908
2907 for f in sorted(modified + added + removed):
2909 for f in sorted(modified + added + removed):
2908 copyop = None
2910 copyop = None
2909 f1, f2 = f, f
2911 f1, f2 = f, f
2910 if f in addedset:
2912 if f in addedset:
2911 f1 = None
2913 f1 = None
2912 if f in copy:
2914 if f in copy:
2913 if opts.git:
2915 if opts.git:
2914 f1 = copy[f]
2916 f1 = copy[f]
2915 if f1 in removedset and f1 not in gone:
2917 if f1 in removedset and f1 not in gone:
2916 copyop = b'rename'
2918 copyop = b'rename'
2917 gone.add(f1)
2919 gone.add(f1)
2918 else:
2920 else:
2919 copyop = b'copy'
2921 copyop = b'copy'
2920 elif f in removedset:
2922 elif f in removedset:
2921 f2 = None
2923 f2 = None
2922 if opts.git:
2924 if opts.git:
2923 # have we already reported a copy above?
2925 # have we already reported a copy above?
2924 if (
2926 if (
2925 f in copyto
2927 f in copyto
2926 and copyto[f] in addedset
2928 and copyto[f] in addedset
2927 and copy[copyto[f]] == f
2929 and copy[copyto[f]] == f
2928 ):
2930 ):
2929 continue
2931 continue
2930 yield f1, f2, copyop
2932 yield f1, f2, copyop
2931
2933
2932
2934
2933 def _gitindex(text):
2935 def _gitindex(text):
2934 if not text:
2936 if not text:
2935 text = b""
2937 text = b""
2936 l = len(text)
2938 l = len(text)
2937 s = hashutil.sha1(b'blob %d\0' % l)
2939 s = hashutil.sha1(b'blob %d\0' % l)
2938 s.update(text)
2940 s.update(text)
2939 return hex(s.digest())
2941 return hex(s.digest())
2940
2942
2941
2943
2942 _gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
2944 _gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
2943
2945
2944
2946
2945 def trydiff(
2947 def trydiff(
2946 repo,
2948 repo,
2947 revs,
2949 revs,
2948 ctx1,
2950 ctx1,
2949 ctx2,
2951 ctx2,
2950 modified,
2952 modified,
2951 added,
2953 added,
2952 removed,
2954 removed,
2953 copy,
2955 copy,
2954 getfilectx,
2956 getfilectx,
2955 opts,
2957 opts,
2956 losedatafn,
2958 losedatafn,
2957 pathfn,
2959 pathfn,
2958 ):
2960 ):
2959 """given input data, generate a diff and yield it in blocks
2961 """given input data, generate a diff and yield it in blocks
2960
2962
2961 If generating a diff would lose data like flags or binary data and
2963 If generating a diff would lose data like flags or binary data and
2962 losedatafn is not None, it will be called.
2964 losedatafn is not None, it will be called.
2963
2965
2964 pathfn is applied to every path in the diff output.
2966 pathfn is applied to every path in the diff output.
2965 """
2967 """
2966
2968
2967 if opts.noprefix:
2969 if opts.noprefix:
2968 aprefix = bprefix = b''
2970 aprefix = bprefix = b''
2969 else:
2971 else:
2970 aprefix = b'a/'
2972 aprefix = b'a/'
2971 bprefix = b'b/'
2973 bprefix = b'b/'
2972
2974
2973 def diffline(f, revs):
2975 def diffline(f, revs):
2974 revinfo = b' '.join([b"-r %s" % rev for rev in revs])
2976 revinfo = b' '.join([b"-r %s" % rev for rev in revs])
2975 return b'diff %s %s' % (revinfo, f)
2977 return b'diff %s %s' % (revinfo, f)
2976
2978
2977 def isempty(fctx):
2979 def isempty(fctx):
2978 return fctx is None or fctx.size() == 0
2980 return fctx is None or fctx.size() == 0
2979
2981
2980 date1 = dateutil.datestr(ctx1.date())
2982 date1 = dateutil.datestr(ctx1.date())
2981 date2 = dateutil.datestr(ctx2.date())
2983 date2 = dateutil.datestr(ctx2.date())
2982
2984
2983 if not pathfn:
2985 if not pathfn:
2984 pathfn = lambda f: f
2986 pathfn = lambda f: f
2985
2987
2986 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2988 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2987 content1 = None
2989 content1 = None
2988 content2 = None
2990 content2 = None
2989 fctx1 = None
2991 fctx1 = None
2990 fctx2 = None
2992 fctx2 = None
2991 flag1 = None
2993 flag1 = None
2992 flag2 = None
2994 flag2 = None
2993 if f1:
2995 if f1:
2994 fctx1 = getfilectx(f1, ctx1)
2996 fctx1 = getfilectx(f1, ctx1)
2995 if opts.git or losedatafn:
2997 if opts.git or losedatafn:
2996 flag1 = ctx1.flags(f1)
2998 flag1 = ctx1.flags(f1)
2997 if f2:
2999 if f2:
2998 fctx2 = getfilectx(f2, ctx2)
3000 fctx2 = getfilectx(f2, ctx2)
2999 if opts.git or losedatafn:
3001 if opts.git or losedatafn:
3000 flag2 = ctx2.flags(f2)
3002 flag2 = ctx2.flags(f2)
3001 # if binary is True, output "summary" or "base85", but not "text diff"
3003 # if binary is True, output "summary" or "base85", but not "text diff"
3002 if opts.text:
3004 if opts.text:
3003 binary = False
3005 binary = False
3004 else:
3006 else:
3005 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
3007 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
3006
3008
3007 if losedatafn and not opts.git:
3009 if losedatafn and not opts.git:
3008 if (
3010 if (
3009 binary
3011 binary
3010 or
3012 or
3011 # copy/rename
3013 # copy/rename
3012 f2 in copy
3014 f2 in copy
3013 or
3015 or
3014 # empty file creation
3016 # empty file creation
3015 (not f1 and isempty(fctx2))
3017 (not f1 and isempty(fctx2))
3016 or
3018 or
3017 # empty file deletion
3019 # empty file deletion
3018 (isempty(fctx1) and not f2)
3020 (isempty(fctx1) and not f2)
3019 or
3021 or
3020 # create with flags
3022 # create with flags
3021 (not f1 and flag2)
3023 (not f1 and flag2)
3022 or
3024 or
3023 # change flags
3025 # change flags
3024 (f1 and f2 and flag1 != flag2)
3026 (f1 and f2 and flag1 != flag2)
3025 ):
3027 ):
3026 losedatafn(f2 or f1)
3028 losedatafn(f2 or f1)
3027
3029
3028 path1 = pathfn(f1 or f2)
3030 path1 = pathfn(f1 or f2)
3029 path2 = pathfn(f2 or f1)
3031 path2 = pathfn(f2 or f1)
3030 header = []
3032 header = []
3031 if opts.git:
3033 if opts.git:
3032 header.append(
3034 header.append(
3033 b'diff --git %s%s %s%s' % (aprefix, path1, bprefix, path2)
3035 b'diff --git %s%s %s%s' % (aprefix, path1, bprefix, path2)
3034 )
3036 )
3035 if not f1: # added
3037 if not f1: # added
3036 header.append(b'new file mode %s' % _gitmode[flag2])
3038 header.append(b'new file mode %s' % _gitmode[flag2])
3037 elif not f2: # removed
3039 elif not f2: # removed
3038 header.append(b'deleted file mode %s' % _gitmode[flag1])
3040 header.append(b'deleted file mode %s' % _gitmode[flag1])
3039 else: # modified/copied/renamed
3041 else: # modified/copied/renamed
3040 mode1, mode2 = _gitmode[flag1], _gitmode[flag2]
3042 mode1, mode2 = _gitmode[flag1], _gitmode[flag2]
3041 if mode1 != mode2:
3043 if mode1 != mode2:
3042 header.append(b'old mode %s' % mode1)
3044 header.append(b'old mode %s' % mode1)
3043 header.append(b'new mode %s' % mode2)
3045 header.append(b'new mode %s' % mode2)
3044 if copyop is not None:
3046 if copyop is not None:
3045 if opts.showsimilarity:
3047 if opts.showsimilarity:
3046 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
3048 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
3047 header.append(b'similarity index %d%%' % sim)
3049 header.append(b'similarity index %d%%' % sim)
3048 header.append(b'%s from %s' % (copyop, path1))
3050 header.append(b'%s from %s' % (copyop, path1))
3049 header.append(b'%s to %s' % (copyop, path2))
3051 header.append(b'%s to %s' % (copyop, path2))
3050 elif revs:
3052 elif revs:
3051 header.append(diffline(path1, revs))
3053 header.append(diffline(path1, revs))
3052
3054
3053 # fctx.is | diffopts | what to | is fctx.data()
3055 # fctx.is | diffopts | what to | is fctx.data()
3054 # binary() | text nobinary git index | output? | outputted?
3056 # binary() | text nobinary git index | output? | outputted?
3055 # ------------------------------------|----------------------------
3057 # ------------------------------------|----------------------------
3056 # yes | no no no * | summary | no
3058 # yes | no no no * | summary | no
3057 # yes | no no yes * | base85 | yes
3059 # yes | no no yes * | base85 | yes
3058 # yes | no yes no * | summary | no
3060 # yes | no yes no * | summary | no
3059 # yes | no yes yes 0 | summary | no
3061 # yes | no yes yes 0 | summary | no
3060 # yes | no yes yes >0 | summary | semi [1]
3062 # yes | no yes yes >0 | summary | semi [1]
3061 # yes | yes * * * | text diff | yes
3063 # yes | yes * * * | text diff | yes
3062 # no | * * * * | text diff | yes
3064 # no | * * * * | text diff | yes
3063 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
3065 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
3064 if binary and (
3066 if binary and (
3065 not opts.git or (opts.git and opts.nobinary and not opts.index)
3067 not opts.git or (opts.git and opts.nobinary and not opts.index)
3066 ):
3068 ):
3067 # fast path: no binary content will be displayed, content1 and
3069 # fast path: no binary content will be displayed, content1 and
3068 # content2 are only used for equivalent test. cmp() could have a
3070 # content2 are only used for equivalent test. cmp() could have a
3069 # fast path.
3071 # fast path.
3070 if fctx1 is not None:
3072 if fctx1 is not None:
3071 content1 = b'\0'
3073 content1 = b'\0'
3072 if fctx2 is not None:
3074 if fctx2 is not None:
3073 if fctx1 is not None and not fctx1.cmp(fctx2):
3075 if fctx1 is not None and not fctx1.cmp(fctx2):
3074 content2 = b'\0' # not different
3076 content2 = b'\0' # not different
3075 else:
3077 else:
3076 content2 = b'\0\0'
3078 content2 = b'\0\0'
3077 else:
3079 else:
3078 # normal path: load contents
3080 # normal path: load contents
3079 if fctx1 is not None:
3081 if fctx1 is not None:
3080 content1 = fctx1.data()
3082 content1 = fctx1.data()
3081 if fctx2 is not None:
3083 if fctx2 is not None:
3082 content2 = fctx2.data()
3084 content2 = fctx2.data()
3083
3085
3084 data1 = (ctx1, fctx1, path1, flag1, content1, date1)
3086 data1 = (ctx1, fctx1, path1, flag1, content1, date1)
3085 data2 = (ctx2, fctx2, path2, flag2, content2, date2)
3087 data2 = (ctx2, fctx2, path2, flag2, content2, date2)
3086 yield diffcontent(data1, data2, header, binary, opts)
3088 yield diffcontent(data1, data2, header, binary, opts)
3087
3089
3088
3090
3089 def diffcontent(data1, data2, header, binary, opts):
3091 def diffcontent(data1, data2, header, binary, opts):
3090 """diffs two versions of a file.
3092 """diffs two versions of a file.
3091
3093
3092 data1 and data2 are tuples containg:
3094 data1 and data2 are tuples containg:
3093
3095
3094 * ctx: changeset for the file
3096 * ctx: changeset for the file
3095 * fctx: file context for that file
3097 * fctx: file context for that file
3096 * path1: name of the file
3098 * path1: name of the file
3097 * flag: flags of the file
3099 * flag: flags of the file
3098 * content: full content of the file (can be null in case of binary)
3100 * content: full content of the file (can be null in case of binary)
3099 * date: date of the changeset
3101 * date: date of the changeset
3100
3102
3101 header: the patch header
3103 header: the patch header
3102 binary: whether the any of the version of file is binary or not
3104 binary: whether the any of the version of file is binary or not
3103 opts: user passed options
3105 opts: user passed options
3104
3106
3105 It exists as a separate function so that extensions like extdiff can wrap
3107 It exists as a separate function so that extensions like extdiff can wrap
3106 it and use the file content directly.
3108 it and use the file content directly.
3107 """
3109 """
3108
3110
3109 ctx1, fctx1, path1, flag1, content1, date1 = data1
3111 ctx1, fctx1, path1, flag1, content1, date1 = data1
3110 ctx2, fctx2, path2, flag2, content2, date2 = data2
3112 ctx2, fctx2, path2, flag2, content2, date2 = data2
3111 index1 = _gitindex(content1) if path1 in ctx1 else sha1nodeconstants.nullhex
3113 index1 = _gitindex(content1) if path1 in ctx1 else sha1nodeconstants.nullhex
3112 index2 = _gitindex(content2) if path2 in ctx2 else sha1nodeconstants.nullhex
3114 index2 = _gitindex(content2) if path2 in ctx2 else sha1nodeconstants.nullhex
3113 if binary and opts.git and not opts.nobinary:
3115 if binary and opts.git and not opts.nobinary:
3114 text = mdiff.b85diff(content1, content2)
3116 text = mdiff.b85diff(content1, content2)
3115 if text:
3117 if text:
3116 header.append(b'index %s..%s' % (index1, index2))
3118 header.append(b'index %s..%s' % (index1, index2))
3117 hunks = ((None, [text]),)
3119 hunks = ((None, [text]),)
3118 else:
3120 else:
3119 if opts.git and opts.index > 0:
3121 if opts.git and opts.index > 0:
3120 flag = flag1
3122 flag = flag1
3121 if flag is None:
3123 if flag is None:
3122 flag = flag2
3124 flag = flag2
3123 header.append(
3125 header.append(
3124 b'index %s..%s %s'
3126 b'index %s..%s %s'
3125 % (
3127 % (
3126 index1[0 : opts.index],
3128 index1[0 : opts.index],
3127 index2[0 : opts.index],
3129 index2[0 : opts.index],
3128 _gitmode[flag],
3130 _gitmode[flag],
3129 )
3131 )
3130 )
3132 )
3131
3133
3132 uheaders, hunks = mdiff.unidiff(
3134 uheaders, hunks = mdiff.unidiff(
3133 content1,
3135 content1,
3134 date1,
3136 date1,
3135 content2,
3137 content2,
3136 date2,
3138 date2,
3137 path1,
3139 path1,
3138 path2,
3140 path2,
3139 binary=binary,
3141 binary=binary,
3140 opts=opts,
3142 opts=opts,
3141 )
3143 )
3142 header.extend(uheaders)
3144 header.extend(uheaders)
3143 return fctx1, fctx2, header, hunks
3145 return fctx1, fctx2, header, hunks
3144
3146
3145
3147
3146 def diffstatsum(stats):
3148 def diffstatsum(stats):
3147 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
3149 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
3148 for f, a, r, b in stats:
3150 for f, a, r, b in stats:
3149 maxfile = max(maxfile, encoding.colwidth(f))
3151 maxfile = max(maxfile, encoding.colwidth(f))
3150 maxtotal = max(maxtotal, a + r)
3152 maxtotal = max(maxtotal, a + r)
3151 addtotal += a
3153 addtotal += a
3152 removetotal += r
3154 removetotal += r
3153 binary = binary or b
3155 binary = binary or b
3154
3156
3155 return maxfile, maxtotal, addtotal, removetotal, binary
3157 return maxfile, maxtotal, addtotal, removetotal, binary
3156
3158
3157
3159
3158 def diffstatdata(lines):
3160 def diffstatdata(lines):
3159 diffre = re.compile(br'^diff .*-r [a-z0-9]+\s(.*)$')
3161 diffre = re.compile(br'^diff .*-r [a-z0-9]+\s(.*)$')
3160
3162
3161 results = []
3163 results = []
3162 filename, adds, removes, isbinary = None, 0, 0, False
3164 filename, adds, removes, isbinary = None, 0, 0, False
3163
3165
3164 def addresult():
3166 def addresult():
3165 if filename:
3167 if filename:
3166 results.append((filename, adds, removes, isbinary))
3168 results.append((filename, adds, removes, isbinary))
3167
3169
3168 # inheader is used to track if a line is in the
3170 # inheader is used to track if a line is in the
3169 # header portion of the diff. This helps properly account
3171 # header portion of the diff. This helps properly account
3170 # for lines that start with '--' or '++'
3172 # for lines that start with '--' or '++'
3171 inheader = False
3173 inheader = False
3172
3174
3173 for line in lines:
3175 for line in lines:
3174 if line.startswith(b'diff'):
3176 if line.startswith(b'diff'):
3175 addresult()
3177 addresult()
3176 # starting a new file diff
3178 # starting a new file diff
3177 # set numbers to 0 and reset inheader
3179 # set numbers to 0 and reset inheader
3178 inheader = True
3180 inheader = True
3179 adds, removes, isbinary = 0, 0, False
3181 adds, removes, isbinary = 0, 0, False
3180 if line.startswith(b'diff --git a/'):
3182 if line.startswith(b'diff --git a/'):
3181 filename = gitre.search(line).group(2)
3183 filename = gitre.search(line).group(2)
3182 elif line.startswith(b'diff -r'):
3184 elif line.startswith(b'diff -r'):
3183 # format: "diff -r ... -r ... filename"
3185 # format: "diff -r ... -r ... filename"
3184 filename = diffre.search(line).group(1)
3186 filename = diffre.search(line).group(1)
3185 elif line.startswith(b'@@'):
3187 elif line.startswith(b'@@'):
3186 inheader = False
3188 inheader = False
3187 elif line.startswith(b'+') and not inheader:
3189 elif line.startswith(b'+') and not inheader:
3188 adds += 1
3190 adds += 1
3189 elif line.startswith(b'-') and not inheader:
3191 elif line.startswith(b'-') and not inheader:
3190 removes += 1
3192 removes += 1
3191 elif line.startswith(b'GIT binary patch') or line.startswith(
3193 elif line.startswith(b'GIT binary patch') or line.startswith(
3192 b'Binary file'
3194 b'Binary file'
3193 ):
3195 ):
3194 isbinary = True
3196 isbinary = True
3195 elif line.startswith(b'rename from'):
3197 elif line.startswith(b'rename from'):
3196 filename = line[12:]
3198 filename = line[12:]
3197 elif line.startswith(b'rename to'):
3199 elif line.startswith(b'rename to'):
3198 filename += b' => %s' % line[10:]
3200 filename += b' => %s' % line[10:]
3199 addresult()
3201 addresult()
3200 return results
3202 return results
3201
3203
3202
3204
3203 def diffstat(lines, width=80):
3205 def diffstat(lines, width=80):
3204 output = []
3206 output = []
3205 stats = diffstatdata(lines)
3207 stats = diffstatdata(lines)
3206 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
3208 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
3207
3209
3208 countwidth = len(str(maxtotal))
3210 countwidth = len(str(maxtotal))
3209 if hasbinary and countwidth < 3:
3211 if hasbinary and countwidth < 3:
3210 countwidth = 3
3212 countwidth = 3
3211 graphwidth = width - countwidth - maxname - 6
3213 graphwidth = width - countwidth - maxname - 6
3212 if graphwidth < 10:
3214 if graphwidth < 10:
3213 graphwidth = 10
3215 graphwidth = 10
3214
3216
3215 def scale(i):
3217 def scale(i):
3216 if maxtotal <= graphwidth:
3218 if maxtotal <= graphwidth:
3217 return i
3219 return i
3218 # If diffstat runs out of room it doesn't print anything,
3220 # If diffstat runs out of room it doesn't print anything,
3219 # which isn't very useful, so always print at least one + or -
3221 # which isn't very useful, so always print at least one + or -
3220 # if there were at least some changes.
3222 # if there were at least some changes.
3221 return max(i * graphwidth // maxtotal, int(bool(i)))
3223 return max(i * graphwidth // maxtotal, int(bool(i)))
3222
3224
3223 for filename, adds, removes, isbinary in stats:
3225 for filename, adds, removes, isbinary in stats:
3224 if isbinary:
3226 if isbinary:
3225 count = b'Bin'
3227 count = b'Bin'
3226 else:
3228 else:
3227 count = b'%d' % (adds + removes)
3229 count = b'%d' % (adds + removes)
3228 pluses = b'+' * scale(adds)
3230 pluses = b'+' * scale(adds)
3229 minuses = b'-' * scale(removes)
3231 minuses = b'-' * scale(removes)
3230 output.append(
3232 output.append(
3231 b' %s%s | %*s %s%s\n'
3233 b' %s%s | %*s %s%s\n'
3232 % (
3234 % (
3233 filename,
3235 filename,
3234 b' ' * (maxname - encoding.colwidth(filename)),
3236 b' ' * (maxname - encoding.colwidth(filename)),
3235 countwidth,
3237 countwidth,
3236 count,
3238 count,
3237 pluses,
3239 pluses,
3238 minuses,
3240 minuses,
3239 )
3241 )
3240 )
3242 )
3241
3243
3242 if stats:
3244 if stats:
3243 output.append(
3245 output.append(
3244 _(b' %d files changed, %d insertions(+), %d deletions(-)\n')
3246 _(b' %d files changed, %d insertions(+), %d deletions(-)\n')
3245 % (len(stats), totaladds, totalremoves)
3247 % (len(stats), totaladds, totalremoves)
3246 )
3248 )
3247
3249
3248 return b''.join(output)
3250 return b''.join(output)
3249
3251
3250
3252
3251 def diffstatui(*args, **kw):
3253 def diffstatui(*args, **kw):
3252 """like diffstat(), but yields 2-tuples of (output, label) for
3254 """like diffstat(), but yields 2-tuples of (output, label) for
3253 ui.write()
3255 ui.write()
3254 """
3256 """
3255
3257
3256 for line in diffstat(*args, **kw).splitlines():
3258 for line in diffstat(*args, **kw).splitlines():
3257 if line and line[-1] in b'+-':
3259 if line and line[-1] in b'+-':
3258 name, graph = line.rsplit(b' ', 1)
3260 name, graph = line.rsplit(b' ', 1)
3259 yield (name + b' ', b'')
3261 yield (name + b' ', b'')
3260 m = re.search(br'\++', graph)
3262 m = re.search(br'\++', graph)
3261 if m:
3263 if m:
3262 yield (m.group(0), b'diffstat.inserted')
3264 yield (m.group(0), b'diffstat.inserted')
3263 m = re.search(br'-+', graph)
3265 m = re.search(br'-+', graph)
3264 if m:
3266 if m:
3265 yield (m.group(0), b'diffstat.deleted')
3267 yield (m.group(0), b'diffstat.deleted')
3266 else:
3268 else:
3267 yield (line, b'')
3269 yield (line, b'')
3268 yield (b'\n', b'')
3270 yield (b'\n', b'')
@@ -1,88 +1,86 b''
1 #require pytype py3 slow
1 #require pytype py3 slow
2
2
3 $ cd $RUNTESTDIR/..
3 $ cd $RUNTESTDIR/..
4
4
5 Many of the individual files that are excluded here confuse pytype
5 Many of the individual files that are excluded here confuse pytype
6 because they do a mix of Python 2 and Python 3 things
6 because they do a mix of Python 2 and Python 3 things
7 conditionally. There's no good way to help it out with that as far as
7 conditionally. There's no good way to help it out with that as far as
8 I can tell, so let's just hide those files from it for now. We should
8 I can tell, so let's just hide those files from it for now. We should
9 endeavor to empty this list out over time, as some of these are
9 endeavor to empty this list out over time, as some of these are
10 probably hiding real problems.
10 probably hiding real problems.
11
11
12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
13 mercurial/chgserver.py # [attribute-error]
13 mercurial/chgserver.py # [attribute-error]
14 mercurial/context.py # many [attribute-error]
14 mercurial/context.py # many [attribute-error]
15 mercurial/crecord.py # tons of [attribute-error], [module-attr]
15 mercurial/crecord.py # tons of [attribute-error], [module-attr]
16 mercurial/debugcommands.py # [wrong-arg-types]
16 mercurial/debugcommands.py # [wrong-arg-types]
17 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
17 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
18 mercurial/exchange.py # [attribute-error]
18 mercurial/exchange.py # [attribute-error]
19 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
19 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
20 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
20 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
21 mercurial/hgweb/webcommands.py # [missing-parameter]
21 mercurial/hgweb/webcommands.py # [missing-parameter]
22 mercurial/hgweb/wsgicgi.py # confused values in os.environ
22 mercurial/hgweb/wsgicgi.py # confused values in os.environ
23 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
23 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
24 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
24 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
25 mercurial/keepalive.py # [attribute-error]
25 mercurial/keepalive.py # [attribute-error]
26 mercurial/localrepo.py # [attribute-error]
26 mercurial/localrepo.py # [attribute-error]
27 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
27 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
28 mercurial/minirst.py # [unsupported-operands], [attribute-error]
28 mercurial/minirst.py # [unsupported-operands], [attribute-error]
29 mercurial/patch.py # [wrong-arg-types]
30 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
29 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
31 mercurial/pure/parsers.py # [attribute-error]
30 mercurial/pure/parsers.py # [attribute-error]
32 mercurial/pycompat.py # bytes vs str issues
31 mercurial/pycompat.py # bytes vs str issues
33 mercurial/repoview.py # [attribute-error]
32 mercurial/repoview.py # [attribute-error]
34 mercurial/sslutil.py # [attribute-error]
33 mercurial/sslutil.py # [attribute-error]
35 mercurial/testing/storage.py # tons of [attribute-error]
34 mercurial/testing/storage.py # tons of [attribute-error]
36 mercurial/ui.py # [attribute-error], [wrong-arg-types]
35 mercurial/ui.py # [attribute-error], [wrong-arg-types]
37 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
36 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
38 mercurial/util.py # [attribute-error], [wrong-arg-count]
37 mercurial/util.py # [attribute-error], [wrong-arg-count]
39 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
38 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
40 mercurial/utils/memorytop.py # not 3.6 compatible
39 mercurial/utils/memorytop.py # not 3.6 compatible
41 mercurial/win32.py # [not-callable]
40 mercurial/win32.py # [not-callable]
42 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
41 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
43 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
42 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
44 mercurial/wireprotov1peer.py # [attribute-error]
43 mercurial/wireprotov1peer.py # [attribute-error]
45 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
44 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
46
45
47 TODO: use --no-cache on test server? Caching the files locally helps during
46 TODO: use --no-cache on test server? Caching the files locally helps during
48 development, but may be a hinderance for CI testing.
47 development, but may be a hinderance for CI testing.
49
48
50 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
49 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
51 > -x mercurial/bundlerepo.py \
50 > -x mercurial/bundlerepo.py \
52 > -x mercurial/chgserver.py \
51 > -x mercurial/chgserver.py \
53 > -x mercurial/context.py \
52 > -x mercurial/context.py \
54 > -x mercurial/crecord.py \
53 > -x mercurial/crecord.py \
55 > -x mercurial/debugcommands.py \
54 > -x mercurial/debugcommands.py \
56 > -x mercurial/dispatch.py \
55 > -x mercurial/dispatch.py \
57 > -x mercurial/exchange.py \
56 > -x mercurial/exchange.py \
58 > -x mercurial/hgweb/hgweb_mod.py \
57 > -x mercurial/hgweb/hgweb_mod.py \
59 > -x mercurial/hgweb/server.py \
58 > -x mercurial/hgweb/server.py \
60 > -x mercurial/hgweb/webcommands.py \
59 > -x mercurial/hgweb/webcommands.py \
61 > -x mercurial/hgweb/wsgicgi.py \
60 > -x mercurial/hgweb/wsgicgi.py \
62 > -x mercurial/httppeer.py \
61 > -x mercurial/httppeer.py \
63 > -x mercurial/interfaces \
62 > -x mercurial/interfaces \
64 > -x mercurial/keepalive.py \
63 > -x mercurial/keepalive.py \
65 > -x mercurial/localrepo.py \
64 > -x mercurial/localrepo.py \
66 > -x mercurial/manifest.py \
65 > -x mercurial/manifest.py \
67 > -x mercurial/minirst.py \
66 > -x mercurial/minirst.py \
68 > -x mercurial/patch.py \
69 > -x mercurial/pure/osutil.py \
67 > -x mercurial/pure/osutil.py \
70 > -x mercurial/pure/parsers.py \
68 > -x mercurial/pure/parsers.py \
71 > -x mercurial/pycompat.py \
69 > -x mercurial/pycompat.py \
72 > -x mercurial/repoview.py \
70 > -x mercurial/repoview.py \
73 > -x mercurial/sslutil.py \
71 > -x mercurial/sslutil.py \
74 > -x mercurial/testing/storage.py \
72 > -x mercurial/testing/storage.py \
75 > -x mercurial/thirdparty \
73 > -x mercurial/thirdparty \
76 > -x mercurial/ui.py \
74 > -x mercurial/ui.py \
77 > -x mercurial/unionrepo.py \
75 > -x mercurial/unionrepo.py \
78 > -x mercurial/utils/procutil.py \
76 > -x mercurial/utils/procutil.py \
79 > -x mercurial/utils/memorytop.py \
77 > -x mercurial/utils/memorytop.py \
80 > -x mercurial/win32.py \
78 > -x mercurial/win32.py \
81 > -x mercurial/wireprotoframing.py \
79 > -x mercurial/wireprotoframing.py \
82 > -x mercurial/wireprotoserver.py \
80 > -x mercurial/wireprotoserver.py \
83 > -x mercurial/wireprotov1peer.py \
81 > -x mercurial/wireprotov1peer.py \
84 > -x mercurial/wireprotov1server.py \
82 > -x mercurial/wireprotov1server.py \
85 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
83 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
86
84
87 Only show the results on a failure, because the output on success is also
85 Only show the results on a failure, because the output on success is also
88 voluminous and variable.
86 voluminous and variable.
General Comments 0
You need to be logged in to leave comments. Login now