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