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