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