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