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