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