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