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