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