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