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