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