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