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