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