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