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