##// END OF EJS Templates
patch: ensure valid git diffs if source/destination file is missing (issue4046)...
Johan Bjork -
r19973:1184edae stable
parent child Browse files
Show More
@@ -1,1882 +1,1882 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 import cStringIO, email, os, errno, re, posixpath
9 import cStringIO, email, os, errno, re, posixpath
10 import tempfile, zlib, shutil
10 import tempfile, zlib, shutil
11 # On python2.4 you have to import these by name or they fail to
11 # On python2.4 you have to import these by name or they fail to
12 # load. This was not a problem on Python 2.7.
12 # load. This was not a problem on Python 2.7.
13 import email.Generator
13 import email.Generator
14 import email.Parser
14 import email.Parser
15
15
16 from i18n import _
16 from i18n import _
17 from node import hex, short
17 from node import hex, short
18 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
18 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
19 import context
19 import context
20
20
21 gitre = re.compile('diff --git a/(.*) b/(.*)')
21 gitre = re.compile('diff --git a/(.*) b/(.*)')
22
22
23 class PatchError(Exception):
23 class PatchError(Exception):
24 pass
24 pass
25
25
26
26
27 # public functions
27 # public functions
28
28
29 def split(stream):
29 def split(stream):
30 '''return an iterator of individual patches from a stream'''
30 '''return an iterator of individual patches from a stream'''
31 def isheader(line, inheader):
31 def isheader(line, inheader):
32 if inheader and line[0] in (' ', '\t'):
32 if inheader and line[0] in (' ', '\t'):
33 # continuation
33 # continuation
34 return True
34 return True
35 if line[0] in (' ', '-', '+'):
35 if line[0] in (' ', '-', '+'):
36 # diff line - don't check for header pattern in there
36 # diff line - don't check for header pattern in there
37 return False
37 return False
38 l = line.split(': ', 1)
38 l = line.split(': ', 1)
39 return len(l) == 2 and ' ' not in l[0]
39 return len(l) == 2 and ' ' not in l[0]
40
40
41 def chunk(lines):
41 def chunk(lines):
42 return cStringIO.StringIO(''.join(lines))
42 return cStringIO.StringIO(''.join(lines))
43
43
44 def hgsplit(stream, cur):
44 def hgsplit(stream, cur):
45 inheader = True
45 inheader = True
46
46
47 for line in stream:
47 for line in stream:
48 if not line.strip():
48 if not line.strip():
49 inheader = False
49 inheader = False
50 if not inheader and line.startswith('# HG changeset patch'):
50 if not inheader and line.startswith('# HG changeset patch'):
51 yield chunk(cur)
51 yield chunk(cur)
52 cur = []
52 cur = []
53 inheader = True
53 inheader = True
54
54
55 cur.append(line)
55 cur.append(line)
56
56
57 if cur:
57 if cur:
58 yield chunk(cur)
58 yield chunk(cur)
59
59
60 def mboxsplit(stream, cur):
60 def mboxsplit(stream, cur):
61 for line in stream:
61 for line in stream:
62 if line.startswith('From '):
62 if line.startswith('From '):
63 for c in split(chunk(cur[1:])):
63 for c in split(chunk(cur[1:])):
64 yield c
64 yield c
65 cur = []
65 cur = []
66
66
67 cur.append(line)
67 cur.append(line)
68
68
69 if cur:
69 if cur:
70 for c in split(chunk(cur[1:])):
70 for c in split(chunk(cur[1:])):
71 yield c
71 yield c
72
72
73 def mimesplit(stream, cur):
73 def mimesplit(stream, cur):
74 def msgfp(m):
74 def msgfp(m):
75 fp = cStringIO.StringIO()
75 fp = cStringIO.StringIO()
76 g = email.Generator.Generator(fp, mangle_from_=False)
76 g = email.Generator.Generator(fp, mangle_from_=False)
77 g.flatten(m)
77 g.flatten(m)
78 fp.seek(0)
78 fp.seek(0)
79 return fp
79 return fp
80
80
81 for line in stream:
81 for line in stream:
82 cur.append(line)
82 cur.append(line)
83 c = chunk(cur)
83 c = chunk(cur)
84
84
85 m = email.Parser.Parser().parse(c)
85 m = email.Parser.Parser().parse(c)
86 if not m.is_multipart():
86 if not m.is_multipart():
87 yield msgfp(m)
87 yield msgfp(m)
88 else:
88 else:
89 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
89 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
90 for part in m.walk():
90 for part in m.walk():
91 ct = part.get_content_type()
91 ct = part.get_content_type()
92 if ct not in ok_types:
92 if ct not in ok_types:
93 continue
93 continue
94 yield msgfp(part)
94 yield msgfp(part)
95
95
96 def headersplit(stream, cur):
96 def headersplit(stream, cur):
97 inheader = False
97 inheader = False
98
98
99 for line in stream:
99 for line in stream:
100 if not inheader and isheader(line, inheader):
100 if not inheader and isheader(line, inheader):
101 yield chunk(cur)
101 yield chunk(cur)
102 cur = []
102 cur = []
103 inheader = True
103 inheader = True
104 if inheader and not isheader(line, inheader):
104 if inheader and not isheader(line, inheader):
105 inheader = False
105 inheader = False
106
106
107 cur.append(line)
107 cur.append(line)
108
108
109 if cur:
109 if cur:
110 yield chunk(cur)
110 yield chunk(cur)
111
111
112 def remainder(cur):
112 def remainder(cur):
113 yield chunk(cur)
113 yield chunk(cur)
114
114
115 class fiter(object):
115 class fiter(object):
116 def __init__(self, fp):
116 def __init__(self, fp):
117 self.fp = fp
117 self.fp = fp
118
118
119 def __iter__(self):
119 def __iter__(self):
120 return self
120 return self
121
121
122 def next(self):
122 def next(self):
123 l = self.fp.readline()
123 l = self.fp.readline()
124 if not l:
124 if not l:
125 raise StopIteration
125 raise StopIteration
126 return l
126 return l
127
127
128 inheader = False
128 inheader = False
129 cur = []
129 cur = []
130
130
131 mimeheaders = ['content-type']
131 mimeheaders = ['content-type']
132
132
133 if not util.safehasattr(stream, 'next'):
133 if not util.safehasattr(stream, 'next'):
134 # http responses, for example, have readline but not next
134 # http responses, for example, have readline but not next
135 stream = fiter(stream)
135 stream = fiter(stream)
136
136
137 for line in stream:
137 for line in stream:
138 cur.append(line)
138 cur.append(line)
139 if line.startswith('# HG changeset patch'):
139 if line.startswith('# HG changeset patch'):
140 return hgsplit(stream, cur)
140 return hgsplit(stream, cur)
141 elif line.startswith('From '):
141 elif line.startswith('From '):
142 return mboxsplit(stream, cur)
142 return mboxsplit(stream, cur)
143 elif isheader(line, inheader):
143 elif isheader(line, inheader):
144 inheader = True
144 inheader = True
145 if line.split(':', 1)[0].lower() in mimeheaders:
145 if line.split(':', 1)[0].lower() in mimeheaders:
146 # let email parser handle this
146 # let email parser handle this
147 return mimesplit(stream, cur)
147 return mimesplit(stream, cur)
148 elif line.startswith('--- ') and inheader:
148 elif line.startswith('--- ') and inheader:
149 # No evil headers seen by diff start, split by hand
149 # No evil headers seen by diff start, split by hand
150 return headersplit(stream, cur)
150 return headersplit(stream, cur)
151 # Not enough info, keep reading
151 # Not enough info, keep reading
152
152
153 # if we are here, we have a very plain patch
153 # if we are here, we have a very plain patch
154 return remainder(cur)
154 return remainder(cur)
155
155
156 def extract(ui, fileobj):
156 def extract(ui, fileobj):
157 '''extract patch from data read from fileobj.
157 '''extract patch from data read from fileobj.
158
158
159 patch can be a normal patch or contained in an email message.
159 patch can be a normal patch or contained in an email message.
160
160
161 return tuple (filename, message, user, date, branch, node, p1, p2).
161 return tuple (filename, message, user, date, branch, node, p1, p2).
162 Any item in the returned tuple can be None. If filename is None,
162 Any item in the returned tuple can be None. If filename is None,
163 fileobj did not contain a patch. Caller must unlink filename when done.'''
163 fileobj did not contain a patch. Caller must unlink filename when done.'''
164
164
165 # attempt to detect the start of a patch
165 # attempt to detect the start of a patch
166 # (this heuristic is borrowed from quilt)
166 # (this heuristic is borrowed from quilt)
167 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
167 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
168 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
168 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
169 r'---[ \t].*?^\+\+\+[ \t]|'
169 r'---[ \t].*?^\+\+\+[ \t]|'
170 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
170 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
171
171
172 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
172 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
173 tmpfp = os.fdopen(fd, 'w')
173 tmpfp = os.fdopen(fd, 'w')
174 try:
174 try:
175 msg = email.Parser.Parser().parse(fileobj)
175 msg = email.Parser.Parser().parse(fileobj)
176
176
177 subject = msg['Subject']
177 subject = msg['Subject']
178 user = msg['From']
178 user = msg['From']
179 if not subject and not user:
179 if not subject and not user:
180 # Not an email, restore parsed headers if any
180 # Not an email, restore parsed headers if any
181 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
181 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
182
182
183 # should try to parse msg['Date']
183 # should try to parse msg['Date']
184 date = None
184 date = None
185 nodeid = None
185 nodeid = None
186 branch = None
186 branch = None
187 parents = []
187 parents = []
188
188
189 if subject:
189 if subject:
190 if subject.startswith('[PATCH'):
190 if subject.startswith('[PATCH'):
191 pend = subject.find(']')
191 pend = subject.find(']')
192 if pend >= 0:
192 if pend >= 0:
193 subject = subject[pend + 1:].lstrip()
193 subject = subject[pend + 1:].lstrip()
194 subject = re.sub(r'\n[ \t]+', ' ', subject)
194 subject = re.sub(r'\n[ \t]+', ' ', subject)
195 ui.debug('Subject: %s\n' % subject)
195 ui.debug('Subject: %s\n' % subject)
196 if user:
196 if user:
197 ui.debug('From: %s\n' % user)
197 ui.debug('From: %s\n' % user)
198 diffs_seen = 0
198 diffs_seen = 0
199 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
199 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
200 message = ''
200 message = ''
201 for part in msg.walk():
201 for part in msg.walk():
202 content_type = part.get_content_type()
202 content_type = part.get_content_type()
203 ui.debug('Content-Type: %s\n' % content_type)
203 ui.debug('Content-Type: %s\n' % content_type)
204 if content_type not in ok_types:
204 if content_type not in ok_types:
205 continue
205 continue
206 payload = part.get_payload(decode=True)
206 payload = part.get_payload(decode=True)
207 m = diffre.search(payload)
207 m = diffre.search(payload)
208 if m:
208 if m:
209 hgpatch = False
209 hgpatch = False
210 hgpatchheader = False
210 hgpatchheader = False
211 ignoretext = False
211 ignoretext = False
212
212
213 ui.debug('found patch at byte %d\n' % m.start(0))
213 ui.debug('found patch at byte %d\n' % m.start(0))
214 diffs_seen += 1
214 diffs_seen += 1
215 cfp = cStringIO.StringIO()
215 cfp = cStringIO.StringIO()
216 for line in payload[:m.start(0)].splitlines():
216 for line in payload[:m.start(0)].splitlines():
217 if line.startswith('# HG changeset patch') and not hgpatch:
217 if line.startswith('# HG changeset patch') and not hgpatch:
218 ui.debug('patch generated by hg export\n')
218 ui.debug('patch generated by hg export\n')
219 hgpatch = True
219 hgpatch = True
220 hgpatchheader = True
220 hgpatchheader = True
221 # drop earlier commit message content
221 # drop earlier commit message content
222 cfp.seek(0)
222 cfp.seek(0)
223 cfp.truncate()
223 cfp.truncate()
224 subject = None
224 subject = None
225 elif hgpatchheader:
225 elif hgpatchheader:
226 if line.startswith('# User '):
226 if line.startswith('# User '):
227 user = line[7:]
227 user = line[7:]
228 ui.debug('From: %s\n' % user)
228 ui.debug('From: %s\n' % user)
229 elif line.startswith("# Date "):
229 elif line.startswith("# Date "):
230 date = line[7:]
230 date = line[7:]
231 elif line.startswith("# Branch "):
231 elif line.startswith("# Branch "):
232 branch = line[9:]
232 branch = line[9:]
233 elif line.startswith("# Node ID "):
233 elif line.startswith("# Node ID "):
234 nodeid = line[10:]
234 nodeid = line[10:]
235 elif line.startswith("# Parent "):
235 elif line.startswith("# Parent "):
236 parents.append(line[9:].lstrip())
236 parents.append(line[9:].lstrip())
237 elif not line.startswith("# "):
237 elif not line.startswith("# "):
238 hgpatchheader = False
238 hgpatchheader = False
239 elif line == '---':
239 elif line == '---':
240 ignoretext = True
240 ignoretext = True
241 if not hgpatchheader and not ignoretext:
241 if not hgpatchheader and not ignoretext:
242 cfp.write(line)
242 cfp.write(line)
243 cfp.write('\n')
243 cfp.write('\n')
244 message = cfp.getvalue()
244 message = cfp.getvalue()
245 if tmpfp:
245 if tmpfp:
246 tmpfp.write(payload)
246 tmpfp.write(payload)
247 if not payload.endswith('\n'):
247 if not payload.endswith('\n'):
248 tmpfp.write('\n')
248 tmpfp.write('\n')
249 elif not diffs_seen and message and content_type == 'text/plain':
249 elif not diffs_seen and message and content_type == 'text/plain':
250 message += '\n' + payload
250 message += '\n' + payload
251 except: # re-raises
251 except: # re-raises
252 tmpfp.close()
252 tmpfp.close()
253 os.unlink(tmpname)
253 os.unlink(tmpname)
254 raise
254 raise
255
255
256 if subject and not message.startswith(subject):
256 if subject and not message.startswith(subject):
257 message = '%s\n%s' % (subject, message)
257 message = '%s\n%s' % (subject, message)
258 tmpfp.close()
258 tmpfp.close()
259 if not diffs_seen:
259 if not diffs_seen:
260 os.unlink(tmpname)
260 os.unlink(tmpname)
261 return None, message, user, date, branch, None, None, None
261 return None, message, user, date, branch, None, None, None
262 p1 = parents and parents.pop(0) or None
262 p1 = parents and parents.pop(0) or None
263 p2 = parents and parents.pop(0) or None
263 p2 = parents and parents.pop(0) or None
264 return tmpname, message, user, date, branch, nodeid, p1, p2
264 return tmpname, message, user, date, branch, nodeid, p1, p2
265
265
266 class patchmeta(object):
266 class patchmeta(object):
267 """Patched file metadata
267 """Patched file metadata
268
268
269 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
269 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
270 or COPY. 'path' is patched file path. 'oldpath' is set to the
270 or COPY. 'path' is patched file path. 'oldpath' is set to the
271 origin file when 'op' is either COPY or RENAME, None otherwise. If
271 origin file when 'op' is either COPY or RENAME, None otherwise. If
272 file mode is changed, 'mode' is a tuple (islink, isexec) where
272 file mode is changed, 'mode' is a tuple (islink, isexec) where
273 'islink' is True if the file is a symlink and 'isexec' is True if
273 'islink' is True if the file is a symlink and 'isexec' is True if
274 the file is executable. Otherwise, 'mode' is None.
274 the file is executable. Otherwise, 'mode' is None.
275 """
275 """
276 def __init__(self, path):
276 def __init__(self, path):
277 self.path = path
277 self.path = path
278 self.oldpath = None
278 self.oldpath = None
279 self.mode = None
279 self.mode = None
280 self.op = 'MODIFY'
280 self.op = 'MODIFY'
281 self.binary = False
281 self.binary = False
282
282
283 def setmode(self, mode):
283 def setmode(self, mode):
284 islink = mode & 020000
284 islink = mode & 020000
285 isexec = mode & 0100
285 isexec = mode & 0100
286 self.mode = (islink, isexec)
286 self.mode = (islink, isexec)
287
287
288 def copy(self):
288 def copy(self):
289 other = patchmeta(self.path)
289 other = patchmeta(self.path)
290 other.oldpath = self.oldpath
290 other.oldpath = self.oldpath
291 other.mode = self.mode
291 other.mode = self.mode
292 other.op = self.op
292 other.op = self.op
293 other.binary = self.binary
293 other.binary = self.binary
294 return other
294 return other
295
295
296 def _ispatchinga(self, afile):
296 def _ispatchinga(self, afile):
297 if afile == '/dev/null':
297 if afile == '/dev/null':
298 return self.op == 'ADD'
298 return self.op == 'ADD'
299 return afile == 'a/' + (self.oldpath or self.path)
299 return afile == 'a/' + (self.oldpath or self.path)
300
300
301 def _ispatchingb(self, bfile):
301 def _ispatchingb(self, bfile):
302 if bfile == '/dev/null':
302 if bfile == '/dev/null':
303 return self.op == 'DELETE'
303 return self.op == 'DELETE'
304 return bfile == 'b/' + self.path
304 return bfile == 'b/' + self.path
305
305
306 def ispatching(self, afile, bfile):
306 def ispatching(self, afile, bfile):
307 return self._ispatchinga(afile) and self._ispatchingb(bfile)
307 return self._ispatchinga(afile) and self._ispatchingb(bfile)
308
308
309 def __repr__(self):
309 def __repr__(self):
310 return "<patchmeta %s %r>" % (self.op, self.path)
310 return "<patchmeta %s %r>" % (self.op, self.path)
311
311
312 def readgitpatch(lr):
312 def readgitpatch(lr):
313 """extract git-style metadata about patches from <patchname>"""
313 """extract git-style metadata about patches from <patchname>"""
314
314
315 # Filter patch for git information
315 # Filter patch for git information
316 gp = None
316 gp = None
317 gitpatches = []
317 gitpatches = []
318 for line in lr:
318 for line in lr:
319 line = line.rstrip(' \r\n')
319 line = line.rstrip(' \r\n')
320 if line.startswith('diff --git a/'):
320 if line.startswith('diff --git a/'):
321 m = gitre.match(line)
321 m = gitre.match(line)
322 if m:
322 if m:
323 if gp:
323 if gp:
324 gitpatches.append(gp)
324 gitpatches.append(gp)
325 dst = m.group(2)
325 dst = m.group(2)
326 gp = patchmeta(dst)
326 gp = patchmeta(dst)
327 elif gp:
327 elif gp:
328 if line.startswith('--- '):
328 if line.startswith('--- '):
329 gitpatches.append(gp)
329 gitpatches.append(gp)
330 gp = None
330 gp = None
331 continue
331 continue
332 if line.startswith('rename from '):
332 if line.startswith('rename from '):
333 gp.op = 'RENAME'
333 gp.op = 'RENAME'
334 gp.oldpath = line[12:]
334 gp.oldpath = line[12:]
335 elif line.startswith('rename to '):
335 elif line.startswith('rename to '):
336 gp.path = line[10:]
336 gp.path = line[10:]
337 elif line.startswith('copy from '):
337 elif line.startswith('copy from '):
338 gp.op = 'COPY'
338 gp.op = 'COPY'
339 gp.oldpath = line[10:]
339 gp.oldpath = line[10:]
340 elif line.startswith('copy to '):
340 elif line.startswith('copy to '):
341 gp.path = line[8:]
341 gp.path = line[8:]
342 elif line.startswith('deleted file'):
342 elif line.startswith('deleted file'):
343 gp.op = 'DELETE'
343 gp.op = 'DELETE'
344 elif line.startswith('new file mode '):
344 elif line.startswith('new file mode '):
345 gp.op = 'ADD'
345 gp.op = 'ADD'
346 gp.setmode(int(line[-6:], 8))
346 gp.setmode(int(line[-6:], 8))
347 elif line.startswith('new mode '):
347 elif line.startswith('new mode '):
348 gp.setmode(int(line[-6:], 8))
348 gp.setmode(int(line[-6:], 8))
349 elif line.startswith('GIT binary patch'):
349 elif line.startswith('GIT binary patch'):
350 gp.binary = True
350 gp.binary = True
351 if gp:
351 if gp:
352 gitpatches.append(gp)
352 gitpatches.append(gp)
353
353
354 return gitpatches
354 return gitpatches
355
355
356 class linereader(object):
356 class linereader(object):
357 # simple class to allow pushing lines back into the input stream
357 # simple class to allow pushing lines back into the input stream
358 def __init__(self, fp):
358 def __init__(self, fp):
359 self.fp = fp
359 self.fp = fp
360 self.buf = []
360 self.buf = []
361
361
362 def push(self, line):
362 def push(self, line):
363 if line is not None:
363 if line is not None:
364 self.buf.append(line)
364 self.buf.append(line)
365
365
366 def readline(self):
366 def readline(self):
367 if self.buf:
367 if self.buf:
368 l = self.buf[0]
368 l = self.buf[0]
369 del self.buf[0]
369 del self.buf[0]
370 return l
370 return l
371 return self.fp.readline()
371 return self.fp.readline()
372
372
373 def __iter__(self):
373 def __iter__(self):
374 while True:
374 while True:
375 l = self.readline()
375 l = self.readline()
376 if not l:
376 if not l:
377 break
377 break
378 yield l
378 yield l
379
379
380 class abstractbackend(object):
380 class abstractbackend(object):
381 def __init__(self, ui):
381 def __init__(self, ui):
382 self.ui = ui
382 self.ui = ui
383
383
384 def getfile(self, fname):
384 def getfile(self, fname):
385 """Return target file data and flags as a (data, (islink,
385 """Return target file data and flags as a (data, (islink,
386 isexec)) tuple.
386 isexec)) tuple.
387 """
387 """
388 raise NotImplementedError
388 raise NotImplementedError
389
389
390 def setfile(self, fname, data, mode, copysource):
390 def setfile(self, fname, data, mode, copysource):
391 """Write data to target file fname and set its mode. mode is a
391 """Write data to target file fname and set its mode. mode is a
392 (islink, isexec) tuple. If data is None, the file content should
392 (islink, isexec) tuple. If data is None, the file content should
393 be left unchanged. If the file is modified after being copied,
393 be left unchanged. If the file is modified after being copied,
394 copysource is set to the original file name.
394 copysource is set to the original file name.
395 """
395 """
396 raise NotImplementedError
396 raise NotImplementedError
397
397
398 def unlink(self, fname):
398 def unlink(self, fname):
399 """Unlink target file."""
399 """Unlink target file."""
400 raise NotImplementedError
400 raise NotImplementedError
401
401
402 def writerej(self, fname, failed, total, lines):
402 def writerej(self, fname, failed, total, lines):
403 """Write rejected lines for fname. total is the number of hunks
403 """Write rejected lines for fname. total is the number of hunks
404 which failed to apply and total the total number of hunks for this
404 which failed to apply and total the total number of hunks for this
405 files.
405 files.
406 """
406 """
407 pass
407 pass
408
408
409 def exists(self, fname):
409 def exists(self, fname):
410 raise NotImplementedError
410 raise NotImplementedError
411
411
412 class fsbackend(abstractbackend):
412 class fsbackend(abstractbackend):
413 def __init__(self, ui, basedir):
413 def __init__(self, ui, basedir):
414 super(fsbackend, self).__init__(ui)
414 super(fsbackend, self).__init__(ui)
415 self.opener = scmutil.opener(basedir)
415 self.opener = scmutil.opener(basedir)
416
416
417 def _join(self, f):
417 def _join(self, f):
418 return os.path.join(self.opener.base, f)
418 return os.path.join(self.opener.base, f)
419
419
420 def getfile(self, fname):
420 def getfile(self, fname):
421 path = self._join(fname)
421 path = self._join(fname)
422 if os.path.islink(path):
422 if os.path.islink(path):
423 return (os.readlink(path), (True, False))
423 return (os.readlink(path), (True, False))
424 isexec = False
424 isexec = False
425 try:
425 try:
426 isexec = os.lstat(path).st_mode & 0100 != 0
426 isexec = os.lstat(path).st_mode & 0100 != 0
427 except OSError, e:
427 except OSError, e:
428 if e.errno != errno.ENOENT:
428 if e.errno != errno.ENOENT:
429 raise
429 raise
430 return (self.opener.read(fname), (False, isexec))
430 return (self.opener.read(fname), (False, isexec))
431
431
432 def setfile(self, fname, data, mode, copysource):
432 def setfile(self, fname, data, mode, copysource):
433 islink, isexec = mode
433 islink, isexec = mode
434 if data is None:
434 if data is None:
435 util.setflags(self._join(fname), islink, isexec)
435 util.setflags(self._join(fname), islink, isexec)
436 return
436 return
437 if islink:
437 if islink:
438 self.opener.symlink(data, fname)
438 self.opener.symlink(data, fname)
439 else:
439 else:
440 self.opener.write(fname, data)
440 self.opener.write(fname, data)
441 if isexec:
441 if isexec:
442 util.setflags(self._join(fname), False, True)
442 util.setflags(self._join(fname), False, True)
443
443
444 def unlink(self, fname):
444 def unlink(self, fname):
445 util.unlinkpath(self._join(fname), ignoremissing=True)
445 util.unlinkpath(self._join(fname), ignoremissing=True)
446
446
447 def writerej(self, fname, failed, total, lines):
447 def writerej(self, fname, failed, total, lines):
448 fname = fname + ".rej"
448 fname = fname + ".rej"
449 self.ui.warn(
449 self.ui.warn(
450 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
450 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
451 (failed, total, fname))
451 (failed, total, fname))
452 fp = self.opener(fname, 'w')
452 fp = self.opener(fname, 'w')
453 fp.writelines(lines)
453 fp.writelines(lines)
454 fp.close()
454 fp.close()
455
455
456 def exists(self, fname):
456 def exists(self, fname):
457 return os.path.lexists(self._join(fname))
457 return os.path.lexists(self._join(fname))
458
458
459 class workingbackend(fsbackend):
459 class workingbackend(fsbackend):
460 def __init__(self, ui, repo, similarity):
460 def __init__(self, ui, repo, similarity):
461 super(workingbackend, self).__init__(ui, repo.root)
461 super(workingbackend, self).__init__(ui, repo.root)
462 self.repo = repo
462 self.repo = repo
463 self.similarity = similarity
463 self.similarity = similarity
464 self.removed = set()
464 self.removed = set()
465 self.changed = set()
465 self.changed = set()
466 self.copied = []
466 self.copied = []
467
467
468 def _checkknown(self, fname):
468 def _checkknown(self, fname):
469 if self.repo.dirstate[fname] == '?' and self.exists(fname):
469 if self.repo.dirstate[fname] == '?' and self.exists(fname):
470 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
470 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
471
471
472 def setfile(self, fname, data, mode, copysource):
472 def setfile(self, fname, data, mode, copysource):
473 self._checkknown(fname)
473 self._checkknown(fname)
474 super(workingbackend, self).setfile(fname, data, mode, copysource)
474 super(workingbackend, self).setfile(fname, data, mode, copysource)
475 if copysource is not None:
475 if copysource is not None:
476 self.copied.append((copysource, fname))
476 self.copied.append((copysource, fname))
477 self.changed.add(fname)
477 self.changed.add(fname)
478
478
479 def unlink(self, fname):
479 def unlink(self, fname):
480 self._checkknown(fname)
480 self._checkknown(fname)
481 super(workingbackend, self).unlink(fname)
481 super(workingbackend, self).unlink(fname)
482 self.removed.add(fname)
482 self.removed.add(fname)
483 self.changed.add(fname)
483 self.changed.add(fname)
484
484
485 def close(self):
485 def close(self):
486 wctx = self.repo[None]
486 wctx = self.repo[None]
487 changed = set(self.changed)
487 changed = set(self.changed)
488 for src, dst in self.copied:
488 for src, dst in self.copied:
489 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
489 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
490 if self.removed:
490 if self.removed:
491 wctx.forget(sorted(self.removed))
491 wctx.forget(sorted(self.removed))
492 for f in self.removed:
492 for f in self.removed:
493 if f not in self.repo.dirstate:
493 if f not in self.repo.dirstate:
494 # File was deleted and no longer belongs to the
494 # File was deleted and no longer belongs to the
495 # dirstate, it was probably marked added then
495 # dirstate, it was probably marked added then
496 # deleted, and should not be considered by
496 # deleted, and should not be considered by
497 # marktouched().
497 # marktouched().
498 changed.discard(f)
498 changed.discard(f)
499 if changed:
499 if changed:
500 scmutil.marktouched(self.repo, changed, self.similarity)
500 scmutil.marktouched(self.repo, changed, self.similarity)
501 return sorted(self.changed)
501 return sorted(self.changed)
502
502
503 class filestore(object):
503 class filestore(object):
504 def __init__(self, maxsize=None):
504 def __init__(self, maxsize=None):
505 self.opener = None
505 self.opener = None
506 self.files = {}
506 self.files = {}
507 self.created = 0
507 self.created = 0
508 self.maxsize = maxsize
508 self.maxsize = maxsize
509 if self.maxsize is None:
509 if self.maxsize is None:
510 self.maxsize = 4*(2**20)
510 self.maxsize = 4*(2**20)
511 self.size = 0
511 self.size = 0
512 self.data = {}
512 self.data = {}
513
513
514 def setfile(self, fname, data, mode, copied=None):
514 def setfile(self, fname, data, mode, copied=None):
515 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
515 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
516 self.data[fname] = (data, mode, copied)
516 self.data[fname] = (data, mode, copied)
517 self.size += len(data)
517 self.size += len(data)
518 else:
518 else:
519 if self.opener is None:
519 if self.opener is None:
520 root = tempfile.mkdtemp(prefix='hg-patch-')
520 root = tempfile.mkdtemp(prefix='hg-patch-')
521 self.opener = scmutil.opener(root)
521 self.opener = scmutil.opener(root)
522 # Avoid filename issues with these simple names
522 # Avoid filename issues with these simple names
523 fn = str(self.created)
523 fn = str(self.created)
524 self.opener.write(fn, data)
524 self.opener.write(fn, data)
525 self.created += 1
525 self.created += 1
526 self.files[fname] = (fn, mode, copied)
526 self.files[fname] = (fn, mode, copied)
527
527
528 def getfile(self, fname):
528 def getfile(self, fname):
529 if fname in self.data:
529 if fname in self.data:
530 return self.data[fname]
530 return self.data[fname]
531 if not self.opener or fname not in self.files:
531 if not self.opener or fname not in self.files:
532 raise IOError
532 raise IOError
533 fn, mode, copied = self.files[fname]
533 fn, mode, copied = self.files[fname]
534 return self.opener.read(fn), mode, copied
534 return self.opener.read(fn), mode, copied
535
535
536 def close(self):
536 def close(self):
537 if self.opener:
537 if self.opener:
538 shutil.rmtree(self.opener.base)
538 shutil.rmtree(self.opener.base)
539
539
540 class repobackend(abstractbackend):
540 class repobackend(abstractbackend):
541 def __init__(self, ui, repo, ctx, store):
541 def __init__(self, ui, repo, ctx, store):
542 super(repobackend, self).__init__(ui)
542 super(repobackend, self).__init__(ui)
543 self.repo = repo
543 self.repo = repo
544 self.ctx = ctx
544 self.ctx = ctx
545 self.store = store
545 self.store = store
546 self.changed = set()
546 self.changed = set()
547 self.removed = set()
547 self.removed = set()
548 self.copied = {}
548 self.copied = {}
549
549
550 def _checkknown(self, fname):
550 def _checkknown(self, fname):
551 if fname not in self.ctx:
551 if fname not in self.ctx:
552 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
552 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
553
553
554 def getfile(self, fname):
554 def getfile(self, fname):
555 try:
555 try:
556 fctx = self.ctx[fname]
556 fctx = self.ctx[fname]
557 except error.LookupError:
557 except error.LookupError:
558 raise IOError
558 raise IOError
559 flags = fctx.flags()
559 flags = fctx.flags()
560 return fctx.data(), ('l' in flags, 'x' in flags)
560 return fctx.data(), ('l' in flags, 'x' in flags)
561
561
562 def setfile(self, fname, data, mode, copysource):
562 def setfile(self, fname, data, mode, copysource):
563 if copysource:
563 if copysource:
564 self._checkknown(copysource)
564 self._checkknown(copysource)
565 if data is None:
565 if data is None:
566 data = self.ctx[fname].data()
566 data = self.ctx[fname].data()
567 self.store.setfile(fname, data, mode, copysource)
567 self.store.setfile(fname, data, mode, copysource)
568 self.changed.add(fname)
568 self.changed.add(fname)
569 if copysource:
569 if copysource:
570 self.copied[fname] = copysource
570 self.copied[fname] = copysource
571
571
572 def unlink(self, fname):
572 def unlink(self, fname):
573 self._checkknown(fname)
573 self._checkknown(fname)
574 self.removed.add(fname)
574 self.removed.add(fname)
575
575
576 def exists(self, fname):
576 def exists(self, fname):
577 return fname in self.ctx
577 return fname in self.ctx
578
578
579 def close(self):
579 def close(self):
580 return self.changed | self.removed
580 return self.changed | self.removed
581
581
582 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
582 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
583 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
583 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
584 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
584 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
585 eolmodes = ['strict', 'crlf', 'lf', 'auto']
585 eolmodes = ['strict', 'crlf', 'lf', 'auto']
586
586
587 class patchfile(object):
587 class patchfile(object):
588 def __init__(self, ui, gp, backend, store, eolmode='strict'):
588 def __init__(self, ui, gp, backend, store, eolmode='strict'):
589 self.fname = gp.path
589 self.fname = gp.path
590 self.eolmode = eolmode
590 self.eolmode = eolmode
591 self.eol = None
591 self.eol = None
592 self.backend = backend
592 self.backend = backend
593 self.ui = ui
593 self.ui = ui
594 self.lines = []
594 self.lines = []
595 self.exists = False
595 self.exists = False
596 self.missing = True
596 self.missing = True
597 self.mode = gp.mode
597 self.mode = gp.mode
598 self.copysource = gp.oldpath
598 self.copysource = gp.oldpath
599 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
599 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
600 self.remove = gp.op == 'DELETE'
600 self.remove = gp.op == 'DELETE'
601 try:
601 try:
602 if self.copysource is None:
602 if self.copysource is None:
603 data, mode = backend.getfile(self.fname)
603 data, mode = backend.getfile(self.fname)
604 self.exists = True
604 self.exists = True
605 else:
605 else:
606 data, mode = store.getfile(self.copysource)[:2]
606 data, mode = store.getfile(self.copysource)[:2]
607 self.exists = backend.exists(self.fname)
607 self.exists = backend.exists(self.fname)
608 self.missing = False
608 self.missing = False
609 if data:
609 if data:
610 self.lines = mdiff.splitnewlines(data)
610 self.lines = mdiff.splitnewlines(data)
611 if self.mode is None:
611 if self.mode is None:
612 self.mode = mode
612 self.mode = mode
613 if self.lines:
613 if self.lines:
614 # Normalize line endings
614 # Normalize line endings
615 if self.lines[0].endswith('\r\n'):
615 if self.lines[0].endswith('\r\n'):
616 self.eol = '\r\n'
616 self.eol = '\r\n'
617 elif self.lines[0].endswith('\n'):
617 elif self.lines[0].endswith('\n'):
618 self.eol = '\n'
618 self.eol = '\n'
619 if eolmode != 'strict':
619 if eolmode != 'strict':
620 nlines = []
620 nlines = []
621 for l in self.lines:
621 for l in self.lines:
622 if l.endswith('\r\n'):
622 if l.endswith('\r\n'):
623 l = l[:-2] + '\n'
623 l = l[:-2] + '\n'
624 nlines.append(l)
624 nlines.append(l)
625 self.lines = nlines
625 self.lines = nlines
626 except IOError:
626 except IOError:
627 if self.create:
627 if self.create:
628 self.missing = False
628 self.missing = False
629 if self.mode is None:
629 if self.mode is None:
630 self.mode = (False, False)
630 self.mode = (False, False)
631 if self.missing:
631 if self.missing:
632 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
632 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
633
633
634 self.hash = {}
634 self.hash = {}
635 self.dirty = 0
635 self.dirty = 0
636 self.offset = 0
636 self.offset = 0
637 self.skew = 0
637 self.skew = 0
638 self.rej = []
638 self.rej = []
639 self.fileprinted = False
639 self.fileprinted = False
640 self.printfile(False)
640 self.printfile(False)
641 self.hunks = 0
641 self.hunks = 0
642
642
643 def writelines(self, fname, lines, mode):
643 def writelines(self, fname, lines, mode):
644 if self.eolmode == 'auto':
644 if self.eolmode == 'auto':
645 eol = self.eol
645 eol = self.eol
646 elif self.eolmode == 'crlf':
646 elif self.eolmode == 'crlf':
647 eol = '\r\n'
647 eol = '\r\n'
648 else:
648 else:
649 eol = '\n'
649 eol = '\n'
650
650
651 if self.eolmode != 'strict' and eol and eol != '\n':
651 if self.eolmode != 'strict' and eol and eol != '\n':
652 rawlines = []
652 rawlines = []
653 for l in lines:
653 for l in lines:
654 if l and l[-1] == '\n':
654 if l and l[-1] == '\n':
655 l = l[:-1] + eol
655 l = l[:-1] + eol
656 rawlines.append(l)
656 rawlines.append(l)
657 lines = rawlines
657 lines = rawlines
658
658
659 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
659 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
660
660
661 def printfile(self, warn):
661 def printfile(self, warn):
662 if self.fileprinted:
662 if self.fileprinted:
663 return
663 return
664 if warn or self.ui.verbose:
664 if warn or self.ui.verbose:
665 self.fileprinted = True
665 self.fileprinted = True
666 s = _("patching file %s\n") % self.fname
666 s = _("patching file %s\n") % self.fname
667 if warn:
667 if warn:
668 self.ui.warn(s)
668 self.ui.warn(s)
669 else:
669 else:
670 self.ui.note(s)
670 self.ui.note(s)
671
671
672
672
673 def findlines(self, l, linenum):
673 def findlines(self, l, linenum):
674 # looks through the hash and finds candidate lines. The
674 # looks through the hash and finds candidate lines. The
675 # result is a list of line numbers sorted based on distance
675 # result is a list of line numbers sorted based on distance
676 # from linenum
676 # from linenum
677
677
678 cand = self.hash.get(l, [])
678 cand = self.hash.get(l, [])
679 if len(cand) > 1:
679 if len(cand) > 1:
680 # resort our list of potentials forward then back.
680 # resort our list of potentials forward then back.
681 cand.sort(key=lambda x: abs(x - linenum))
681 cand.sort(key=lambda x: abs(x - linenum))
682 return cand
682 return cand
683
683
684 def write_rej(self):
684 def write_rej(self):
685 # our rejects are a little different from patch(1). This always
685 # our rejects are a little different from patch(1). This always
686 # creates rejects in the same form as the original patch. A file
686 # creates rejects in the same form as the original patch. A file
687 # header is inserted so that you can run the reject through patch again
687 # header is inserted so that you can run the reject through patch again
688 # without having to type the filename.
688 # without having to type the filename.
689 if not self.rej:
689 if not self.rej:
690 return
690 return
691 base = os.path.basename(self.fname)
691 base = os.path.basename(self.fname)
692 lines = ["--- %s\n+++ %s\n" % (base, base)]
692 lines = ["--- %s\n+++ %s\n" % (base, base)]
693 for x in self.rej:
693 for x in self.rej:
694 for l in x.hunk:
694 for l in x.hunk:
695 lines.append(l)
695 lines.append(l)
696 if l[-1] != '\n':
696 if l[-1] != '\n':
697 lines.append("\n\ No newline at end of file\n")
697 lines.append("\n\ No newline at end of file\n")
698 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
698 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
699
699
700 def apply(self, h):
700 def apply(self, h):
701 if not h.complete():
701 if not h.complete():
702 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
702 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
703 (h.number, h.desc, len(h.a), h.lena, len(h.b),
703 (h.number, h.desc, len(h.a), h.lena, len(h.b),
704 h.lenb))
704 h.lenb))
705
705
706 self.hunks += 1
706 self.hunks += 1
707
707
708 if self.missing:
708 if self.missing:
709 self.rej.append(h)
709 self.rej.append(h)
710 return -1
710 return -1
711
711
712 if self.exists and self.create:
712 if self.exists and self.create:
713 if self.copysource:
713 if self.copysource:
714 self.ui.warn(_("cannot create %s: destination already "
714 self.ui.warn(_("cannot create %s: destination already "
715 "exists\n" % self.fname))
715 "exists\n" % self.fname))
716 else:
716 else:
717 self.ui.warn(_("file %s already exists\n") % self.fname)
717 self.ui.warn(_("file %s already exists\n") % self.fname)
718 self.rej.append(h)
718 self.rej.append(h)
719 return -1
719 return -1
720
720
721 if isinstance(h, binhunk):
721 if isinstance(h, binhunk):
722 if self.remove:
722 if self.remove:
723 self.backend.unlink(self.fname)
723 self.backend.unlink(self.fname)
724 else:
724 else:
725 self.lines[:] = h.new()
725 self.lines[:] = h.new()
726 self.offset += len(h.new())
726 self.offset += len(h.new())
727 self.dirty = True
727 self.dirty = True
728 return 0
728 return 0
729
729
730 horig = h
730 horig = h
731 if (self.eolmode in ('crlf', 'lf')
731 if (self.eolmode in ('crlf', 'lf')
732 or self.eolmode == 'auto' and self.eol):
732 or self.eolmode == 'auto' and self.eol):
733 # If new eols are going to be normalized, then normalize
733 # If new eols are going to be normalized, then normalize
734 # hunk data before patching. Otherwise, preserve input
734 # hunk data before patching. Otherwise, preserve input
735 # line-endings.
735 # line-endings.
736 h = h.getnormalized()
736 h = h.getnormalized()
737
737
738 # fast case first, no offsets, no fuzz
738 # fast case first, no offsets, no fuzz
739 old, oldstart, new, newstart = h.fuzzit(0, False)
739 old, oldstart, new, newstart = h.fuzzit(0, False)
740 oldstart += self.offset
740 oldstart += self.offset
741 orig_start = oldstart
741 orig_start = oldstart
742 # if there's skew we want to emit the "(offset %d lines)" even
742 # if there's skew we want to emit the "(offset %d lines)" even
743 # when the hunk cleanly applies at start + skew, so skip the
743 # when the hunk cleanly applies at start + skew, so skip the
744 # fast case code
744 # fast case code
745 if (self.skew == 0 and
745 if (self.skew == 0 and
746 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
746 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
747 if self.remove:
747 if self.remove:
748 self.backend.unlink(self.fname)
748 self.backend.unlink(self.fname)
749 else:
749 else:
750 self.lines[oldstart:oldstart + len(old)] = new
750 self.lines[oldstart:oldstart + len(old)] = new
751 self.offset += len(new) - len(old)
751 self.offset += len(new) - len(old)
752 self.dirty = True
752 self.dirty = True
753 return 0
753 return 0
754
754
755 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
755 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
756 self.hash = {}
756 self.hash = {}
757 for x, s in enumerate(self.lines):
757 for x, s in enumerate(self.lines):
758 self.hash.setdefault(s, []).append(x)
758 self.hash.setdefault(s, []).append(x)
759
759
760 for fuzzlen in xrange(3):
760 for fuzzlen in xrange(3):
761 for toponly in [True, False]:
761 for toponly in [True, False]:
762 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
762 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
763 oldstart = oldstart + self.offset + self.skew
763 oldstart = oldstart + self.offset + self.skew
764 oldstart = min(oldstart, len(self.lines))
764 oldstart = min(oldstart, len(self.lines))
765 if old:
765 if old:
766 cand = self.findlines(old[0][1:], oldstart)
766 cand = self.findlines(old[0][1:], oldstart)
767 else:
767 else:
768 # Only adding lines with no or fuzzed context, just
768 # Only adding lines with no or fuzzed context, just
769 # take the skew in account
769 # take the skew in account
770 cand = [oldstart]
770 cand = [oldstart]
771
771
772 for l in cand:
772 for l in cand:
773 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
773 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
774 self.lines[l : l + len(old)] = new
774 self.lines[l : l + len(old)] = new
775 self.offset += len(new) - len(old)
775 self.offset += len(new) - len(old)
776 self.skew = l - orig_start
776 self.skew = l - orig_start
777 self.dirty = True
777 self.dirty = True
778 offset = l - orig_start - fuzzlen
778 offset = l - orig_start - fuzzlen
779 if fuzzlen:
779 if fuzzlen:
780 msg = _("Hunk #%d succeeded at %d "
780 msg = _("Hunk #%d succeeded at %d "
781 "with fuzz %d "
781 "with fuzz %d "
782 "(offset %d lines).\n")
782 "(offset %d lines).\n")
783 self.printfile(True)
783 self.printfile(True)
784 self.ui.warn(msg %
784 self.ui.warn(msg %
785 (h.number, l + 1, fuzzlen, offset))
785 (h.number, l + 1, fuzzlen, offset))
786 else:
786 else:
787 msg = _("Hunk #%d succeeded at %d "
787 msg = _("Hunk #%d succeeded at %d "
788 "(offset %d lines).\n")
788 "(offset %d lines).\n")
789 self.ui.note(msg % (h.number, l + 1, offset))
789 self.ui.note(msg % (h.number, l + 1, offset))
790 return fuzzlen
790 return fuzzlen
791 self.printfile(True)
791 self.printfile(True)
792 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
792 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
793 self.rej.append(horig)
793 self.rej.append(horig)
794 return -1
794 return -1
795
795
796 def close(self):
796 def close(self):
797 if self.dirty:
797 if self.dirty:
798 self.writelines(self.fname, self.lines, self.mode)
798 self.writelines(self.fname, self.lines, self.mode)
799 self.write_rej()
799 self.write_rej()
800 return len(self.rej)
800 return len(self.rej)
801
801
802 class hunk(object):
802 class hunk(object):
803 def __init__(self, desc, num, lr, context):
803 def __init__(self, desc, num, lr, context):
804 self.number = num
804 self.number = num
805 self.desc = desc
805 self.desc = desc
806 self.hunk = [desc]
806 self.hunk = [desc]
807 self.a = []
807 self.a = []
808 self.b = []
808 self.b = []
809 self.starta = self.lena = None
809 self.starta = self.lena = None
810 self.startb = self.lenb = None
810 self.startb = self.lenb = None
811 if lr is not None:
811 if lr is not None:
812 if context:
812 if context:
813 self.read_context_hunk(lr)
813 self.read_context_hunk(lr)
814 else:
814 else:
815 self.read_unified_hunk(lr)
815 self.read_unified_hunk(lr)
816
816
817 def getnormalized(self):
817 def getnormalized(self):
818 """Return a copy with line endings normalized to LF."""
818 """Return a copy with line endings normalized to LF."""
819
819
820 def normalize(lines):
820 def normalize(lines):
821 nlines = []
821 nlines = []
822 for line in lines:
822 for line in lines:
823 if line.endswith('\r\n'):
823 if line.endswith('\r\n'):
824 line = line[:-2] + '\n'
824 line = line[:-2] + '\n'
825 nlines.append(line)
825 nlines.append(line)
826 return nlines
826 return nlines
827
827
828 # Dummy object, it is rebuilt manually
828 # Dummy object, it is rebuilt manually
829 nh = hunk(self.desc, self.number, None, None)
829 nh = hunk(self.desc, self.number, None, None)
830 nh.number = self.number
830 nh.number = self.number
831 nh.desc = self.desc
831 nh.desc = self.desc
832 nh.hunk = self.hunk
832 nh.hunk = self.hunk
833 nh.a = normalize(self.a)
833 nh.a = normalize(self.a)
834 nh.b = normalize(self.b)
834 nh.b = normalize(self.b)
835 nh.starta = self.starta
835 nh.starta = self.starta
836 nh.startb = self.startb
836 nh.startb = self.startb
837 nh.lena = self.lena
837 nh.lena = self.lena
838 nh.lenb = self.lenb
838 nh.lenb = self.lenb
839 return nh
839 return nh
840
840
841 def read_unified_hunk(self, lr):
841 def read_unified_hunk(self, lr):
842 m = unidesc.match(self.desc)
842 m = unidesc.match(self.desc)
843 if not m:
843 if not m:
844 raise PatchError(_("bad hunk #%d") % self.number)
844 raise PatchError(_("bad hunk #%d") % self.number)
845 self.starta, self.lena, self.startb, self.lenb = m.groups()
845 self.starta, self.lena, self.startb, self.lenb = m.groups()
846 if self.lena is None:
846 if self.lena is None:
847 self.lena = 1
847 self.lena = 1
848 else:
848 else:
849 self.lena = int(self.lena)
849 self.lena = int(self.lena)
850 if self.lenb is None:
850 if self.lenb is None:
851 self.lenb = 1
851 self.lenb = 1
852 else:
852 else:
853 self.lenb = int(self.lenb)
853 self.lenb = int(self.lenb)
854 self.starta = int(self.starta)
854 self.starta = int(self.starta)
855 self.startb = int(self.startb)
855 self.startb = int(self.startb)
856 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
856 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
857 self.b)
857 self.b)
858 # if we hit eof before finishing out the hunk, the last line will
858 # if we hit eof before finishing out the hunk, the last line will
859 # be zero length. Lets try to fix it up.
859 # be zero length. Lets try to fix it up.
860 while len(self.hunk[-1]) == 0:
860 while len(self.hunk[-1]) == 0:
861 del self.hunk[-1]
861 del self.hunk[-1]
862 del self.a[-1]
862 del self.a[-1]
863 del self.b[-1]
863 del self.b[-1]
864 self.lena -= 1
864 self.lena -= 1
865 self.lenb -= 1
865 self.lenb -= 1
866 self._fixnewline(lr)
866 self._fixnewline(lr)
867
867
868 def read_context_hunk(self, lr):
868 def read_context_hunk(self, lr):
869 self.desc = lr.readline()
869 self.desc = lr.readline()
870 m = contextdesc.match(self.desc)
870 m = contextdesc.match(self.desc)
871 if not m:
871 if not m:
872 raise PatchError(_("bad hunk #%d") % self.number)
872 raise PatchError(_("bad hunk #%d") % self.number)
873 self.starta, aend = m.groups()
873 self.starta, aend = m.groups()
874 self.starta = int(self.starta)
874 self.starta = int(self.starta)
875 if aend is None:
875 if aend is None:
876 aend = self.starta
876 aend = self.starta
877 self.lena = int(aend) - self.starta
877 self.lena = int(aend) - self.starta
878 if self.starta:
878 if self.starta:
879 self.lena += 1
879 self.lena += 1
880 for x in xrange(self.lena):
880 for x in xrange(self.lena):
881 l = lr.readline()
881 l = lr.readline()
882 if l.startswith('---'):
882 if l.startswith('---'):
883 # lines addition, old block is empty
883 # lines addition, old block is empty
884 lr.push(l)
884 lr.push(l)
885 break
885 break
886 s = l[2:]
886 s = l[2:]
887 if l.startswith('- ') or l.startswith('! '):
887 if l.startswith('- ') or l.startswith('! '):
888 u = '-' + s
888 u = '-' + s
889 elif l.startswith(' '):
889 elif l.startswith(' '):
890 u = ' ' + s
890 u = ' ' + s
891 else:
891 else:
892 raise PatchError(_("bad hunk #%d old text line %d") %
892 raise PatchError(_("bad hunk #%d old text line %d") %
893 (self.number, x))
893 (self.number, x))
894 self.a.append(u)
894 self.a.append(u)
895 self.hunk.append(u)
895 self.hunk.append(u)
896
896
897 l = lr.readline()
897 l = lr.readline()
898 if l.startswith('\ '):
898 if l.startswith('\ '):
899 s = self.a[-1][:-1]
899 s = self.a[-1][:-1]
900 self.a[-1] = s
900 self.a[-1] = s
901 self.hunk[-1] = s
901 self.hunk[-1] = s
902 l = lr.readline()
902 l = lr.readline()
903 m = contextdesc.match(l)
903 m = contextdesc.match(l)
904 if not m:
904 if not m:
905 raise PatchError(_("bad hunk #%d") % self.number)
905 raise PatchError(_("bad hunk #%d") % self.number)
906 self.startb, bend = m.groups()
906 self.startb, bend = m.groups()
907 self.startb = int(self.startb)
907 self.startb = int(self.startb)
908 if bend is None:
908 if bend is None:
909 bend = self.startb
909 bend = self.startb
910 self.lenb = int(bend) - self.startb
910 self.lenb = int(bend) - self.startb
911 if self.startb:
911 if self.startb:
912 self.lenb += 1
912 self.lenb += 1
913 hunki = 1
913 hunki = 1
914 for x in xrange(self.lenb):
914 for x in xrange(self.lenb):
915 l = lr.readline()
915 l = lr.readline()
916 if l.startswith('\ '):
916 if l.startswith('\ '):
917 # XXX: the only way to hit this is with an invalid line range.
917 # XXX: the only way to hit this is with an invalid line range.
918 # The no-eol marker is not counted in the line range, but I
918 # The no-eol marker is not counted in the line range, but I
919 # guess there are diff(1) out there which behave differently.
919 # guess there are diff(1) out there which behave differently.
920 s = self.b[-1][:-1]
920 s = self.b[-1][:-1]
921 self.b[-1] = s
921 self.b[-1] = s
922 self.hunk[hunki - 1] = s
922 self.hunk[hunki - 1] = s
923 continue
923 continue
924 if not l:
924 if not l:
925 # line deletions, new block is empty and we hit EOF
925 # line deletions, new block is empty and we hit EOF
926 lr.push(l)
926 lr.push(l)
927 break
927 break
928 s = l[2:]
928 s = l[2:]
929 if l.startswith('+ ') or l.startswith('! '):
929 if l.startswith('+ ') or l.startswith('! '):
930 u = '+' + s
930 u = '+' + s
931 elif l.startswith(' '):
931 elif l.startswith(' '):
932 u = ' ' + s
932 u = ' ' + s
933 elif len(self.b) == 0:
933 elif len(self.b) == 0:
934 # line deletions, new block is empty
934 # line deletions, new block is empty
935 lr.push(l)
935 lr.push(l)
936 break
936 break
937 else:
937 else:
938 raise PatchError(_("bad hunk #%d old text line %d") %
938 raise PatchError(_("bad hunk #%d old text line %d") %
939 (self.number, x))
939 (self.number, x))
940 self.b.append(s)
940 self.b.append(s)
941 while True:
941 while True:
942 if hunki >= len(self.hunk):
942 if hunki >= len(self.hunk):
943 h = ""
943 h = ""
944 else:
944 else:
945 h = self.hunk[hunki]
945 h = self.hunk[hunki]
946 hunki += 1
946 hunki += 1
947 if h == u:
947 if h == u:
948 break
948 break
949 elif h.startswith('-'):
949 elif h.startswith('-'):
950 continue
950 continue
951 else:
951 else:
952 self.hunk.insert(hunki - 1, u)
952 self.hunk.insert(hunki - 1, u)
953 break
953 break
954
954
955 if not self.a:
955 if not self.a:
956 # this happens when lines were only added to the hunk
956 # this happens when lines were only added to the hunk
957 for x in self.hunk:
957 for x in self.hunk:
958 if x.startswith('-') or x.startswith(' '):
958 if x.startswith('-') or x.startswith(' '):
959 self.a.append(x)
959 self.a.append(x)
960 if not self.b:
960 if not self.b:
961 # this happens when lines were only deleted from the hunk
961 # this happens when lines were only deleted from the hunk
962 for x in self.hunk:
962 for x in self.hunk:
963 if x.startswith('+') or x.startswith(' '):
963 if x.startswith('+') or x.startswith(' '):
964 self.b.append(x[1:])
964 self.b.append(x[1:])
965 # @@ -start,len +start,len @@
965 # @@ -start,len +start,len @@
966 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
966 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
967 self.startb, self.lenb)
967 self.startb, self.lenb)
968 self.hunk[0] = self.desc
968 self.hunk[0] = self.desc
969 self._fixnewline(lr)
969 self._fixnewline(lr)
970
970
971 def _fixnewline(self, lr):
971 def _fixnewline(self, lr):
972 l = lr.readline()
972 l = lr.readline()
973 if l.startswith('\ '):
973 if l.startswith('\ '):
974 diffhelpers.fix_newline(self.hunk, self.a, self.b)
974 diffhelpers.fix_newline(self.hunk, self.a, self.b)
975 else:
975 else:
976 lr.push(l)
976 lr.push(l)
977
977
978 def complete(self):
978 def complete(self):
979 return len(self.a) == self.lena and len(self.b) == self.lenb
979 return len(self.a) == self.lena and len(self.b) == self.lenb
980
980
981 def _fuzzit(self, old, new, fuzz, toponly):
981 def _fuzzit(self, old, new, fuzz, toponly):
982 # this removes context lines from the top and bottom of list 'l'. It
982 # this removes context lines from the top and bottom of list 'l'. It
983 # checks the hunk to make sure only context lines are removed, and then
983 # checks the hunk to make sure only context lines are removed, and then
984 # returns a new shortened list of lines.
984 # returns a new shortened list of lines.
985 fuzz = min(fuzz, len(old))
985 fuzz = min(fuzz, len(old))
986 if fuzz:
986 if fuzz:
987 top = 0
987 top = 0
988 bot = 0
988 bot = 0
989 hlen = len(self.hunk)
989 hlen = len(self.hunk)
990 for x in xrange(hlen - 1):
990 for x in xrange(hlen - 1):
991 # the hunk starts with the @@ line, so use x+1
991 # the hunk starts with the @@ line, so use x+1
992 if self.hunk[x + 1][0] == ' ':
992 if self.hunk[x + 1][0] == ' ':
993 top += 1
993 top += 1
994 else:
994 else:
995 break
995 break
996 if not toponly:
996 if not toponly:
997 for x in xrange(hlen - 1):
997 for x in xrange(hlen - 1):
998 if self.hunk[hlen - bot - 1][0] == ' ':
998 if self.hunk[hlen - bot - 1][0] == ' ':
999 bot += 1
999 bot += 1
1000 else:
1000 else:
1001 break
1001 break
1002
1002
1003 bot = min(fuzz, bot)
1003 bot = min(fuzz, bot)
1004 top = min(fuzz, top)
1004 top = min(fuzz, top)
1005 return old[top:len(old) - bot], new[top:len(new) - bot], top
1005 return old[top:len(old) - bot], new[top:len(new) - bot], top
1006 return old, new, 0
1006 return old, new, 0
1007
1007
1008 def fuzzit(self, fuzz, toponly):
1008 def fuzzit(self, fuzz, toponly):
1009 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1009 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1010 oldstart = self.starta + top
1010 oldstart = self.starta + top
1011 newstart = self.startb + top
1011 newstart = self.startb + top
1012 # zero length hunk ranges already have their start decremented
1012 # zero length hunk ranges already have their start decremented
1013 if self.lena and oldstart > 0:
1013 if self.lena and oldstart > 0:
1014 oldstart -= 1
1014 oldstart -= 1
1015 if self.lenb and newstart > 0:
1015 if self.lenb and newstart > 0:
1016 newstart -= 1
1016 newstart -= 1
1017 return old, oldstart, new, newstart
1017 return old, oldstart, new, newstart
1018
1018
1019 class binhunk(object):
1019 class binhunk(object):
1020 'A binary patch file. Only understands literals so far.'
1020 'A binary patch file. Only understands literals so far.'
1021 def __init__(self, lr, fname):
1021 def __init__(self, lr, fname):
1022 self.text = None
1022 self.text = None
1023 self.hunk = ['GIT binary patch\n']
1023 self.hunk = ['GIT binary patch\n']
1024 self._fname = fname
1024 self._fname = fname
1025 self._read(lr)
1025 self._read(lr)
1026
1026
1027 def complete(self):
1027 def complete(self):
1028 return self.text is not None
1028 return self.text is not None
1029
1029
1030 def new(self):
1030 def new(self):
1031 return [self.text]
1031 return [self.text]
1032
1032
1033 def _read(self, lr):
1033 def _read(self, lr):
1034 def getline(lr, hunk):
1034 def getline(lr, hunk):
1035 l = lr.readline()
1035 l = lr.readline()
1036 hunk.append(l)
1036 hunk.append(l)
1037 return l.rstrip('\r\n')
1037 return l.rstrip('\r\n')
1038
1038
1039 while True:
1039 while True:
1040 line = getline(lr, self.hunk)
1040 line = getline(lr, self.hunk)
1041 if not line:
1041 if not line:
1042 raise PatchError(_('could not extract "%s" binary data')
1042 raise PatchError(_('could not extract "%s" binary data')
1043 % self._fname)
1043 % self._fname)
1044 if line.startswith('literal '):
1044 if line.startswith('literal '):
1045 break
1045 break
1046 size = int(line[8:].rstrip())
1046 size = int(line[8:].rstrip())
1047 dec = []
1047 dec = []
1048 line = getline(lr, self.hunk)
1048 line = getline(lr, self.hunk)
1049 while len(line) > 1:
1049 while len(line) > 1:
1050 l = line[0]
1050 l = line[0]
1051 if l <= 'Z' and l >= 'A':
1051 if l <= 'Z' and l >= 'A':
1052 l = ord(l) - ord('A') + 1
1052 l = ord(l) - ord('A') + 1
1053 else:
1053 else:
1054 l = ord(l) - ord('a') + 27
1054 l = ord(l) - ord('a') + 27
1055 try:
1055 try:
1056 dec.append(base85.b85decode(line[1:])[:l])
1056 dec.append(base85.b85decode(line[1:])[:l])
1057 except ValueError, e:
1057 except ValueError, e:
1058 raise PatchError(_('could not decode "%s" binary patch: %s')
1058 raise PatchError(_('could not decode "%s" binary patch: %s')
1059 % (self._fname, str(e)))
1059 % (self._fname, str(e)))
1060 line = getline(lr, self.hunk)
1060 line = getline(lr, self.hunk)
1061 text = zlib.decompress(''.join(dec))
1061 text = zlib.decompress(''.join(dec))
1062 if len(text) != size:
1062 if len(text) != size:
1063 raise PatchError(_('"%s" length is %d bytes, should be %d')
1063 raise PatchError(_('"%s" length is %d bytes, should be %d')
1064 % (self._fname, len(text), size))
1064 % (self._fname, len(text), size))
1065 self.text = text
1065 self.text = text
1066
1066
1067 def parsefilename(str):
1067 def parsefilename(str):
1068 # --- filename \t|space stuff
1068 # --- filename \t|space stuff
1069 s = str[4:].rstrip('\r\n')
1069 s = str[4:].rstrip('\r\n')
1070 i = s.find('\t')
1070 i = s.find('\t')
1071 if i < 0:
1071 if i < 0:
1072 i = s.find(' ')
1072 i = s.find(' ')
1073 if i < 0:
1073 if i < 0:
1074 return s
1074 return s
1075 return s[:i]
1075 return s[:i]
1076
1076
1077 def pathstrip(path, strip):
1077 def pathstrip(path, strip):
1078 pathlen = len(path)
1078 pathlen = len(path)
1079 i = 0
1079 i = 0
1080 if strip == 0:
1080 if strip == 0:
1081 return '', path.rstrip()
1081 return '', path.rstrip()
1082 count = strip
1082 count = strip
1083 while count > 0:
1083 while count > 0:
1084 i = path.find('/', i)
1084 i = path.find('/', i)
1085 if i == -1:
1085 if i == -1:
1086 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1086 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1087 (count, strip, path))
1087 (count, strip, path))
1088 i += 1
1088 i += 1
1089 # consume '//' in the path
1089 # consume '//' in the path
1090 while i < pathlen - 1 and path[i] == '/':
1090 while i < pathlen - 1 and path[i] == '/':
1091 i += 1
1091 i += 1
1092 count -= 1
1092 count -= 1
1093 return path[:i].lstrip(), path[i:].rstrip()
1093 return path[:i].lstrip(), path[i:].rstrip()
1094
1094
1095 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1095 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1096 nulla = afile_orig == "/dev/null"
1096 nulla = afile_orig == "/dev/null"
1097 nullb = bfile_orig == "/dev/null"
1097 nullb = bfile_orig == "/dev/null"
1098 create = nulla and hunk.starta == 0 and hunk.lena == 0
1098 create = nulla and hunk.starta == 0 and hunk.lena == 0
1099 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1099 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1100 abase, afile = pathstrip(afile_orig, strip)
1100 abase, afile = pathstrip(afile_orig, strip)
1101 gooda = not nulla and backend.exists(afile)
1101 gooda = not nulla and backend.exists(afile)
1102 bbase, bfile = pathstrip(bfile_orig, strip)
1102 bbase, bfile = pathstrip(bfile_orig, strip)
1103 if afile == bfile:
1103 if afile == bfile:
1104 goodb = gooda
1104 goodb = gooda
1105 else:
1105 else:
1106 goodb = not nullb and backend.exists(bfile)
1106 goodb = not nullb and backend.exists(bfile)
1107 missing = not goodb and not gooda and not create
1107 missing = not goodb and not gooda and not create
1108
1108
1109 # some diff programs apparently produce patches where the afile is
1109 # some diff programs apparently produce patches where the afile is
1110 # not /dev/null, but afile starts with bfile
1110 # not /dev/null, but afile starts with bfile
1111 abasedir = afile[:afile.rfind('/') + 1]
1111 abasedir = afile[:afile.rfind('/') + 1]
1112 bbasedir = bfile[:bfile.rfind('/') + 1]
1112 bbasedir = bfile[:bfile.rfind('/') + 1]
1113 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1113 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1114 and hunk.starta == 0 and hunk.lena == 0):
1114 and hunk.starta == 0 and hunk.lena == 0):
1115 create = True
1115 create = True
1116 missing = False
1116 missing = False
1117
1117
1118 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1118 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1119 # diff is between a file and its backup. In this case, the original
1119 # diff is between a file and its backup. In this case, the original
1120 # file should be patched (see original mpatch code).
1120 # file should be patched (see original mpatch code).
1121 isbackup = (abase == bbase and bfile.startswith(afile))
1121 isbackup = (abase == bbase and bfile.startswith(afile))
1122 fname = None
1122 fname = None
1123 if not missing:
1123 if not missing:
1124 if gooda and goodb:
1124 if gooda and goodb:
1125 fname = isbackup and afile or bfile
1125 fname = isbackup and afile or bfile
1126 elif gooda:
1126 elif gooda:
1127 fname = afile
1127 fname = afile
1128
1128
1129 if not fname:
1129 if not fname:
1130 if not nullb:
1130 if not nullb:
1131 fname = isbackup and afile or bfile
1131 fname = isbackup and afile or bfile
1132 elif not nulla:
1132 elif not nulla:
1133 fname = afile
1133 fname = afile
1134 else:
1134 else:
1135 raise PatchError(_("undefined source and destination files"))
1135 raise PatchError(_("undefined source and destination files"))
1136
1136
1137 gp = patchmeta(fname)
1137 gp = patchmeta(fname)
1138 if create:
1138 if create:
1139 gp.op = 'ADD'
1139 gp.op = 'ADD'
1140 elif remove:
1140 elif remove:
1141 gp.op = 'DELETE'
1141 gp.op = 'DELETE'
1142 return gp
1142 return gp
1143
1143
1144 def scangitpatch(lr, firstline):
1144 def scangitpatch(lr, firstline):
1145 """
1145 """
1146 Git patches can emit:
1146 Git patches can emit:
1147 - rename a to b
1147 - rename a to b
1148 - change b
1148 - change b
1149 - copy a to c
1149 - copy a to c
1150 - change c
1150 - change c
1151
1151
1152 We cannot apply this sequence as-is, the renamed 'a' could not be
1152 We cannot apply this sequence as-is, the renamed 'a' could not be
1153 found for it would have been renamed already. And we cannot copy
1153 found for it would have been renamed already. And we cannot copy
1154 from 'b' instead because 'b' would have been changed already. So
1154 from 'b' instead because 'b' would have been changed already. So
1155 we scan the git patch for copy and rename commands so we can
1155 we scan the git patch for copy and rename commands so we can
1156 perform the copies ahead of time.
1156 perform the copies ahead of time.
1157 """
1157 """
1158 pos = 0
1158 pos = 0
1159 try:
1159 try:
1160 pos = lr.fp.tell()
1160 pos = lr.fp.tell()
1161 fp = lr.fp
1161 fp = lr.fp
1162 except IOError:
1162 except IOError:
1163 fp = cStringIO.StringIO(lr.fp.read())
1163 fp = cStringIO.StringIO(lr.fp.read())
1164 gitlr = linereader(fp)
1164 gitlr = linereader(fp)
1165 gitlr.push(firstline)
1165 gitlr.push(firstline)
1166 gitpatches = readgitpatch(gitlr)
1166 gitpatches = readgitpatch(gitlr)
1167 fp.seek(pos)
1167 fp.seek(pos)
1168 return gitpatches
1168 return gitpatches
1169
1169
1170 def iterhunks(fp):
1170 def iterhunks(fp):
1171 """Read a patch and yield the following events:
1171 """Read a patch and yield the following events:
1172 - ("file", afile, bfile, firsthunk): select a new target file.
1172 - ("file", afile, bfile, firsthunk): select a new target file.
1173 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1173 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1174 "file" event.
1174 "file" event.
1175 - ("git", gitchanges): current diff is in git format, gitchanges
1175 - ("git", gitchanges): current diff is in git format, gitchanges
1176 maps filenames to gitpatch records. Unique event.
1176 maps filenames to gitpatch records. Unique event.
1177 """
1177 """
1178 afile = ""
1178 afile = ""
1179 bfile = ""
1179 bfile = ""
1180 state = None
1180 state = None
1181 hunknum = 0
1181 hunknum = 0
1182 emitfile = newfile = False
1182 emitfile = newfile = False
1183 gitpatches = None
1183 gitpatches = None
1184
1184
1185 # our states
1185 # our states
1186 BFILE = 1
1186 BFILE = 1
1187 context = None
1187 context = None
1188 lr = linereader(fp)
1188 lr = linereader(fp)
1189
1189
1190 while True:
1190 while True:
1191 x = lr.readline()
1191 x = lr.readline()
1192 if not x:
1192 if not x:
1193 break
1193 break
1194 if state == BFILE and (
1194 if state == BFILE and (
1195 (not context and x[0] == '@')
1195 (not context and x[0] == '@')
1196 or (context is not False and x.startswith('***************'))
1196 or (context is not False and x.startswith('***************'))
1197 or x.startswith('GIT binary patch')):
1197 or x.startswith('GIT binary patch')):
1198 gp = None
1198 gp = None
1199 if (gitpatches and
1199 if (gitpatches and
1200 gitpatches[-1].ispatching(afile, bfile)):
1200 gitpatches[-1].ispatching(afile, bfile)):
1201 gp = gitpatches.pop()
1201 gp = gitpatches.pop()
1202 if x.startswith('GIT binary patch'):
1202 if x.startswith('GIT binary patch'):
1203 h = binhunk(lr, gp.path)
1203 h = binhunk(lr, gp.path)
1204 else:
1204 else:
1205 if context is None and x.startswith('***************'):
1205 if context is None and x.startswith('***************'):
1206 context = True
1206 context = True
1207 h = hunk(x, hunknum + 1, lr, context)
1207 h = hunk(x, hunknum + 1, lr, context)
1208 hunknum += 1
1208 hunknum += 1
1209 if emitfile:
1209 if emitfile:
1210 emitfile = False
1210 emitfile = False
1211 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1211 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1212 yield 'hunk', h
1212 yield 'hunk', h
1213 elif x.startswith('diff --git a/'):
1213 elif x.startswith('diff --git a/'):
1214 m = gitre.match(x.rstrip(' \r\n'))
1214 m = gitre.match(x.rstrip(' \r\n'))
1215 if not m:
1215 if not m:
1216 continue
1216 continue
1217 if gitpatches is None:
1217 if gitpatches is None:
1218 # scan whole input for git metadata
1218 # scan whole input for git metadata
1219 gitpatches = scangitpatch(lr, x)
1219 gitpatches = scangitpatch(lr, x)
1220 yield 'git', [g.copy() for g in gitpatches
1220 yield 'git', [g.copy() for g in gitpatches
1221 if g.op in ('COPY', 'RENAME')]
1221 if g.op in ('COPY', 'RENAME')]
1222 gitpatches.reverse()
1222 gitpatches.reverse()
1223 afile = 'a/' + m.group(1)
1223 afile = 'a/' + m.group(1)
1224 bfile = 'b/' + m.group(2)
1224 bfile = 'b/' + m.group(2)
1225 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1225 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1226 gp = gitpatches.pop()
1226 gp = gitpatches.pop()
1227 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1227 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1228 if not gitpatches:
1228 if not gitpatches:
1229 raise PatchError(_('failed to synchronize metadata for "%s"')
1229 raise PatchError(_('failed to synchronize metadata for "%s"')
1230 % afile[2:])
1230 % afile[2:])
1231 gp = gitpatches[-1]
1231 gp = gitpatches[-1]
1232 newfile = True
1232 newfile = True
1233 elif x.startswith('---'):
1233 elif x.startswith('---'):
1234 # check for a unified diff
1234 # check for a unified diff
1235 l2 = lr.readline()
1235 l2 = lr.readline()
1236 if not l2.startswith('+++'):
1236 if not l2.startswith('+++'):
1237 lr.push(l2)
1237 lr.push(l2)
1238 continue
1238 continue
1239 newfile = True
1239 newfile = True
1240 context = False
1240 context = False
1241 afile = parsefilename(x)
1241 afile = parsefilename(x)
1242 bfile = parsefilename(l2)
1242 bfile = parsefilename(l2)
1243 elif x.startswith('***'):
1243 elif x.startswith('***'):
1244 # check for a context diff
1244 # check for a context diff
1245 l2 = lr.readline()
1245 l2 = lr.readline()
1246 if not l2.startswith('---'):
1246 if not l2.startswith('---'):
1247 lr.push(l2)
1247 lr.push(l2)
1248 continue
1248 continue
1249 l3 = lr.readline()
1249 l3 = lr.readline()
1250 lr.push(l3)
1250 lr.push(l3)
1251 if not l3.startswith("***************"):
1251 if not l3.startswith("***************"):
1252 lr.push(l2)
1252 lr.push(l2)
1253 continue
1253 continue
1254 newfile = True
1254 newfile = True
1255 context = True
1255 context = True
1256 afile = parsefilename(x)
1256 afile = parsefilename(x)
1257 bfile = parsefilename(l2)
1257 bfile = parsefilename(l2)
1258
1258
1259 if newfile:
1259 if newfile:
1260 newfile = False
1260 newfile = False
1261 emitfile = True
1261 emitfile = True
1262 state = BFILE
1262 state = BFILE
1263 hunknum = 0
1263 hunknum = 0
1264
1264
1265 while gitpatches:
1265 while gitpatches:
1266 gp = gitpatches.pop()
1266 gp = gitpatches.pop()
1267 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1267 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1268
1268
1269 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1269 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1270 """Reads a patch from fp and tries to apply it.
1270 """Reads a patch from fp and tries to apply it.
1271
1271
1272 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1272 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1273 there was any fuzz.
1273 there was any fuzz.
1274
1274
1275 If 'eolmode' is 'strict', the patch content and patched file are
1275 If 'eolmode' is 'strict', the patch content and patched file are
1276 read in binary mode. Otherwise, line endings are ignored when
1276 read in binary mode. Otherwise, line endings are ignored when
1277 patching then normalized according to 'eolmode'.
1277 patching then normalized according to 'eolmode'.
1278 """
1278 """
1279 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1279 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1280 eolmode=eolmode)
1280 eolmode=eolmode)
1281
1281
1282 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1282 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1283 eolmode='strict'):
1283 eolmode='strict'):
1284
1284
1285 def pstrip(p):
1285 def pstrip(p):
1286 return pathstrip(p, strip - 1)[1]
1286 return pathstrip(p, strip - 1)[1]
1287
1287
1288 rejects = 0
1288 rejects = 0
1289 err = 0
1289 err = 0
1290 current_file = None
1290 current_file = None
1291
1291
1292 for state, values in iterhunks(fp):
1292 for state, values in iterhunks(fp):
1293 if state == 'hunk':
1293 if state == 'hunk':
1294 if not current_file:
1294 if not current_file:
1295 continue
1295 continue
1296 ret = current_file.apply(values)
1296 ret = current_file.apply(values)
1297 if ret > 0:
1297 if ret > 0:
1298 err = 1
1298 err = 1
1299 elif state == 'file':
1299 elif state == 'file':
1300 if current_file:
1300 if current_file:
1301 rejects += current_file.close()
1301 rejects += current_file.close()
1302 current_file = None
1302 current_file = None
1303 afile, bfile, first_hunk, gp = values
1303 afile, bfile, first_hunk, gp = values
1304 if gp:
1304 if gp:
1305 gp.path = pstrip(gp.path)
1305 gp.path = pstrip(gp.path)
1306 if gp.oldpath:
1306 if gp.oldpath:
1307 gp.oldpath = pstrip(gp.oldpath)
1307 gp.oldpath = pstrip(gp.oldpath)
1308 else:
1308 else:
1309 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1309 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1310 if gp.op == 'RENAME':
1310 if gp.op == 'RENAME':
1311 backend.unlink(gp.oldpath)
1311 backend.unlink(gp.oldpath)
1312 if not first_hunk:
1312 if not first_hunk:
1313 if gp.op == 'DELETE':
1313 if gp.op == 'DELETE':
1314 backend.unlink(gp.path)
1314 backend.unlink(gp.path)
1315 continue
1315 continue
1316 data, mode = None, None
1316 data, mode = None, None
1317 if gp.op in ('RENAME', 'COPY'):
1317 if gp.op in ('RENAME', 'COPY'):
1318 data, mode = store.getfile(gp.oldpath)[:2]
1318 data, mode = store.getfile(gp.oldpath)[:2]
1319 if gp.mode:
1319 if gp.mode:
1320 mode = gp.mode
1320 mode = gp.mode
1321 if gp.op == 'ADD':
1321 if gp.op == 'ADD':
1322 # Added files without content have no hunk and
1322 # Added files without content have no hunk and
1323 # must be created
1323 # must be created
1324 data = ''
1324 data = ''
1325 if data or mode:
1325 if data or mode:
1326 if (gp.op in ('ADD', 'RENAME', 'COPY')
1326 if (gp.op in ('ADD', 'RENAME', 'COPY')
1327 and backend.exists(gp.path)):
1327 and backend.exists(gp.path)):
1328 raise PatchError(_("cannot create %s: destination "
1328 raise PatchError(_("cannot create %s: destination "
1329 "already exists") % gp.path)
1329 "already exists") % gp.path)
1330 backend.setfile(gp.path, data, mode, gp.oldpath)
1330 backend.setfile(gp.path, data, mode, gp.oldpath)
1331 continue
1331 continue
1332 try:
1332 try:
1333 current_file = patcher(ui, gp, backend, store,
1333 current_file = patcher(ui, gp, backend, store,
1334 eolmode=eolmode)
1334 eolmode=eolmode)
1335 except PatchError, inst:
1335 except PatchError, inst:
1336 ui.warn(str(inst) + '\n')
1336 ui.warn(str(inst) + '\n')
1337 current_file = None
1337 current_file = None
1338 rejects += 1
1338 rejects += 1
1339 continue
1339 continue
1340 elif state == 'git':
1340 elif state == 'git':
1341 for gp in values:
1341 for gp in values:
1342 path = pstrip(gp.oldpath)
1342 path = pstrip(gp.oldpath)
1343 try:
1343 try:
1344 data, mode = backend.getfile(path)
1344 data, mode = backend.getfile(path)
1345 except IOError, e:
1345 except IOError, e:
1346 if e.errno != errno.ENOENT:
1346 if e.errno != errno.ENOENT:
1347 raise
1347 raise
1348 # The error ignored here will trigger a getfile()
1348 # The error ignored here will trigger a getfile()
1349 # error in a place more appropriate for error
1349 # error in a place more appropriate for error
1350 # handling, and will not interrupt the patching
1350 # handling, and will not interrupt the patching
1351 # process.
1351 # process.
1352 else:
1352 else:
1353 store.setfile(path, data, mode)
1353 store.setfile(path, data, mode)
1354 else:
1354 else:
1355 raise util.Abort(_('unsupported parser state: %s') % state)
1355 raise util.Abort(_('unsupported parser state: %s') % state)
1356
1356
1357 if current_file:
1357 if current_file:
1358 rejects += current_file.close()
1358 rejects += current_file.close()
1359
1359
1360 if rejects:
1360 if rejects:
1361 return -1
1361 return -1
1362 return err
1362 return err
1363
1363
1364 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1364 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1365 similarity):
1365 similarity):
1366 """use <patcher> to apply <patchname> to the working directory.
1366 """use <patcher> to apply <patchname> to the working directory.
1367 returns whether patch was applied with fuzz factor."""
1367 returns whether patch was applied with fuzz factor."""
1368
1368
1369 fuzz = False
1369 fuzz = False
1370 args = []
1370 args = []
1371 cwd = repo.root
1371 cwd = repo.root
1372 if cwd:
1372 if cwd:
1373 args.append('-d %s' % util.shellquote(cwd))
1373 args.append('-d %s' % util.shellquote(cwd))
1374 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1374 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1375 util.shellquote(patchname)))
1375 util.shellquote(patchname)))
1376 try:
1376 try:
1377 for line in fp:
1377 for line in fp:
1378 line = line.rstrip()
1378 line = line.rstrip()
1379 ui.note(line + '\n')
1379 ui.note(line + '\n')
1380 if line.startswith('patching file '):
1380 if line.startswith('patching file '):
1381 pf = util.parsepatchoutput(line)
1381 pf = util.parsepatchoutput(line)
1382 printed_file = False
1382 printed_file = False
1383 files.add(pf)
1383 files.add(pf)
1384 elif line.find('with fuzz') >= 0:
1384 elif line.find('with fuzz') >= 0:
1385 fuzz = True
1385 fuzz = True
1386 if not printed_file:
1386 if not printed_file:
1387 ui.warn(pf + '\n')
1387 ui.warn(pf + '\n')
1388 printed_file = True
1388 printed_file = True
1389 ui.warn(line + '\n')
1389 ui.warn(line + '\n')
1390 elif line.find('saving rejects to file') >= 0:
1390 elif line.find('saving rejects to file') >= 0:
1391 ui.warn(line + '\n')
1391 ui.warn(line + '\n')
1392 elif line.find('FAILED') >= 0:
1392 elif line.find('FAILED') >= 0:
1393 if not printed_file:
1393 if not printed_file:
1394 ui.warn(pf + '\n')
1394 ui.warn(pf + '\n')
1395 printed_file = True
1395 printed_file = True
1396 ui.warn(line + '\n')
1396 ui.warn(line + '\n')
1397 finally:
1397 finally:
1398 if files:
1398 if files:
1399 scmutil.marktouched(repo, files, similarity)
1399 scmutil.marktouched(repo, files, similarity)
1400 code = fp.close()
1400 code = fp.close()
1401 if code:
1401 if code:
1402 raise PatchError(_("patch command failed: %s") %
1402 raise PatchError(_("patch command failed: %s") %
1403 util.explainexit(code)[0])
1403 util.explainexit(code)[0])
1404 return fuzz
1404 return fuzz
1405
1405
1406 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1406 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1407 if files is None:
1407 if files is None:
1408 files = set()
1408 files = set()
1409 if eolmode is None:
1409 if eolmode is None:
1410 eolmode = ui.config('patch', 'eol', 'strict')
1410 eolmode = ui.config('patch', 'eol', 'strict')
1411 if eolmode.lower() not in eolmodes:
1411 if eolmode.lower() not in eolmodes:
1412 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1412 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1413 eolmode = eolmode.lower()
1413 eolmode = eolmode.lower()
1414
1414
1415 store = filestore()
1415 store = filestore()
1416 try:
1416 try:
1417 fp = open(patchobj, 'rb')
1417 fp = open(patchobj, 'rb')
1418 except TypeError:
1418 except TypeError:
1419 fp = patchobj
1419 fp = patchobj
1420 try:
1420 try:
1421 ret = applydiff(ui, fp, backend, store, strip=strip,
1421 ret = applydiff(ui, fp, backend, store, strip=strip,
1422 eolmode=eolmode)
1422 eolmode=eolmode)
1423 finally:
1423 finally:
1424 if fp != patchobj:
1424 if fp != patchobj:
1425 fp.close()
1425 fp.close()
1426 files.update(backend.close())
1426 files.update(backend.close())
1427 store.close()
1427 store.close()
1428 if ret < 0:
1428 if ret < 0:
1429 raise PatchError(_('patch failed to apply'))
1429 raise PatchError(_('patch failed to apply'))
1430 return ret > 0
1430 return ret > 0
1431
1431
1432 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1432 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1433 similarity=0):
1433 similarity=0):
1434 """use builtin patch to apply <patchobj> to the working directory.
1434 """use builtin patch to apply <patchobj> to the working directory.
1435 returns whether patch was applied with fuzz factor."""
1435 returns whether patch was applied with fuzz factor."""
1436 backend = workingbackend(ui, repo, similarity)
1436 backend = workingbackend(ui, repo, similarity)
1437 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1437 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1438
1438
1439 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1439 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1440 eolmode='strict'):
1440 eolmode='strict'):
1441 backend = repobackend(ui, repo, ctx, store)
1441 backend = repobackend(ui, repo, ctx, store)
1442 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1442 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1443
1443
1444 def makememctx(repo, parents, text, user, date, branch, files, store,
1444 def makememctx(repo, parents, text, user, date, branch, files, store,
1445 editor=None):
1445 editor=None):
1446 def getfilectx(repo, memctx, path):
1446 def getfilectx(repo, memctx, path):
1447 data, (islink, isexec), copied = store.getfile(path)
1447 data, (islink, isexec), copied = store.getfile(path)
1448 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1448 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1449 copied=copied)
1449 copied=copied)
1450 extra = {}
1450 extra = {}
1451 if branch:
1451 if branch:
1452 extra['branch'] = encoding.fromlocal(branch)
1452 extra['branch'] = encoding.fromlocal(branch)
1453 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1453 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1454 date, extra)
1454 date, extra)
1455 if editor:
1455 if editor:
1456 ctx._text = editor(repo, ctx, [])
1456 ctx._text = editor(repo, ctx, [])
1457 return ctx
1457 return ctx
1458
1458
1459 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1459 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1460 similarity=0):
1460 similarity=0):
1461 """Apply <patchname> to the working directory.
1461 """Apply <patchname> to the working directory.
1462
1462
1463 'eolmode' specifies how end of lines should be handled. It can be:
1463 'eolmode' specifies how end of lines should be handled. It can be:
1464 - 'strict': inputs are read in binary mode, EOLs are preserved
1464 - 'strict': inputs are read in binary mode, EOLs are preserved
1465 - 'crlf': EOLs are ignored when patching and reset to CRLF
1465 - 'crlf': EOLs are ignored when patching and reset to CRLF
1466 - 'lf': EOLs are ignored when patching and reset to LF
1466 - 'lf': EOLs are ignored when patching and reset to LF
1467 - None: get it from user settings, default to 'strict'
1467 - None: get it from user settings, default to 'strict'
1468 'eolmode' is ignored when using an external patcher program.
1468 'eolmode' is ignored when using an external patcher program.
1469
1469
1470 Returns whether patch was applied with fuzz factor.
1470 Returns whether patch was applied with fuzz factor.
1471 """
1471 """
1472 patcher = ui.config('ui', 'patch')
1472 patcher = ui.config('ui', 'patch')
1473 if files is None:
1473 if files is None:
1474 files = set()
1474 files = set()
1475 try:
1475 try:
1476 if patcher:
1476 if patcher:
1477 return _externalpatch(ui, repo, patcher, patchname, strip,
1477 return _externalpatch(ui, repo, patcher, patchname, strip,
1478 files, similarity)
1478 files, similarity)
1479 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1479 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1480 similarity)
1480 similarity)
1481 except PatchError, err:
1481 except PatchError, err:
1482 raise util.Abort(str(err))
1482 raise util.Abort(str(err))
1483
1483
1484 def changedfiles(ui, repo, patchpath, strip=1):
1484 def changedfiles(ui, repo, patchpath, strip=1):
1485 backend = fsbackend(ui, repo.root)
1485 backend = fsbackend(ui, repo.root)
1486 fp = open(patchpath, 'rb')
1486 fp = open(patchpath, 'rb')
1487 try:
1487 try:
1488 changed = set()
1488 changed = set()
1489 for state, values in iterhunks(fp):
1489 for state, values in iterhunks(fp):
1490 if state == 'file':
1490 if state == 'file':
1491 afile, bfile, first_hunk, gp = values
1491 afile, bfile, first_hunk, gp = values
1492 if gp:
1492 if gp:
1493 gp.path = pathstrip(gp.path, strip - 1)[1]
1493 gp.path = pathstrip(gp.path, strip - 1)[1]
1494 if gp.oldpath:
1494 if gp.oldpath:
1495 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1495 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1496 else:
1496 else:
1497 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1497 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1498 changed.add(gp.path)
1498 changed.add(gp.path)
1499 if gp.op == 'RENAME':
1499 if gp.op == 'RENAME':
1500 changed.add(gp.oldpath)
1500 changed.add(gp.oldpath)
1501 elif state not in ('hunk', 'git'):
1501 elif state not in ('hunk', 'git'):
1502 raise util.Abort(_('unsupported parser state: %s') % state)
1502 raise util.Abort(_('unsupported parser state: %s') % state)
1503 return changed
1503 return changed
1504 finally:
1504 finally:
1505 fp.close()
1505 fp.close()
1506
1506
1507 class GitDiffRequired(Exception):
1507 class GitDiffRequired(Exception):
1508 pass
1508 pass
1509
1509
1510 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1510 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1511 def get(key, name=None, getter=ui.configbool):
1511 def get(key, name=None, getter=ui.configbool):
1512 return ((opts and opts.get(key)) or
1512 return ((opts and opts.get(key)) or
1513 getter(section, name or key, None, untrusted=untrusted))
1513 getter(section, name or key, None, untrusted=untrusted))
1514 return mdiff.diffopts(
1514 return mdiff.diffopts(
1515 text=opts and opts.get('text'),
1515 text=opts and opts.get('text'),
1516 git=get('git'),
1516 git=get('git'),
1517 nodates=get('nodates'),
1517 nodates=get('nodates'),
1518 showfunc=get('show_function', 'showfunc'),
1518 showfunc=get('show_function', 'showfunc'),
1519 ignorews=get('ignore_all_space', 'ignorews'),
1519 ignorews=get('ignore_all_space', 'ignorews'),
1520 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1520 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1521 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1521 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1522 context=get('unified', getter=ui.config))
1522 context=get('unified', getter=ui.config))
1523
1523
1524 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1524 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1525 losedatafn=None, prefix=''):
1525 losedatafn=None, prefix=''):
1526 '''yields diff of changes to files between two nodes, or node and
1526 '''yields diff of changes to files between two nodes, or node and
1527 working directory.
1527 working directory.
1528
1528
1529 if node1 is None, use first dirstate parent instead.
1529 if node1 is None, use first dirstate parent instead.
1530 if node2 is None, compare node1 with working directory.
1530 if node2 is None, compare node1 with working directory.
1531
1531
1532 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1532 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1533 every time some change cannot be represented with the current
1533 every time some change cannot be represented with the current
1534 patch format. Return False to upgrade to git patch format, True to
1534 patch format. Return False to upgrade to git patch format, True to
1535 accept the loss or raise an exception to abort the diff. It is
1535 accept the loss or raise an exception to abort the diff. It is
1536 called with the name of current file being diffed as 'fn'. If set
1536 called with the name of current file being diffed as 'fn'. If set
1537 to None, patches will always be upgraded to git format when
1537 to None, patches will always be upgraded to git format when
1538 necessary.
1538 necessary.
1539
1539
1540 prefix is a filename prefix that is prepended to all filenames on
1540 prefix is a filename prefix that is prepended to all filenames on
1541 display (used for subrepos).
1541 display (used for subrepos).
1542 '''
1542 '''
1543
1543
1544 if opts is None:
1544 if opts is None:
1545 opts = mdiff.defaultopts
1545 opts = mdiff.defaultopts
1546
1546
1547 if not node1 and not node2:
1547 if not node1 and not node2:
1548 node1 = repo.dirstate.p1()
1548 node1 = repo.dirstate.p1()
1549
1549
1550 def lrugetfilectx():
1550 def lrugetfilectx():
1551 cache = {}
1551 cache = {}
1552 order = util.deque()
1552 order = util.deque()
1553 def getfilectx(f, ctx):
1553 def getfilectx(f, ctx):
1554 fctx = ctx.filectx(f, filelog=cache.get(f))
1554 fctx = ctx.filectx(f, filelog=cache.get(f))
1555 if f not in cache:
1555 if f not in cache:
1556 if len(cache) > 20:
1556 if len(cache) > 20:
1557 del cache[order.popleft()]
1557 del cache[order.popleft()]
1558 cache[f] = fctx.filelog()
1558 cache[f] = fctx.filelog()
1559 else:
1559 else:
1560 order.remove(f)
1560 order.remove(f)
1561 order.append(f)
1561 order.append(f)
1562 return fctx
1562 return fctx
1563 return getfilectx
1563 return getfilectx
1564 getfilectx = lrugetfilectx()
1564 getfilectx = lrugetfilectx()
1565
1565
1566 ctx1 = repo[node1]
1566 ctx1 = repo[node1]
1567 ctx2 = repo[node2]
1567 ctx2 = repo[node2]
1568
1568
1569 if not changes:
1569 if not changes:
1570 changes = repo.status(ctx1, ctx2, match=match)
1570 changes = repo.status(ctx1, ctx2, match=match)
1571 modified, added, removed = changes[:3]
1571 modified, added, removed = changes[:3]
1572
1572
1573 if not modified and not added and not removed:
1573 if not modified and not added and not removed:
1574 return []
1574 return []
1575
1575
1576 revs = None
1576 revs = None
1577 hexfunc = repo.ui.debugflag and hex or short
1577 hexfunc = repo.ui.debugflag and hex or short
1578 revs = [hexfunc(node) for node in [node1, node2] if node]
1578 revs = [hexfunc(node) for node in [node1, node2] if node]
1579
1579
1580 copy = {}
1580 copy = {}
1581 if opts.git or opts.upgrade:
1581 if opts.git or opts.upgrade:
1582 copy = copies.pathcopies(ctx1, ctx2)
1582 copy = copies.pathcopies(ctx1, ctx2)
1583
1583
1584 def difffn(opts, losedata):
1584 def difffn(opts, losedata):
1585 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1585 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1586 copy, getfilectx, opts, losedata, prefix)
1586 copy, getfilectx, opts, losedata, prefix)
1587 if opts.upgrade and not opts.git:
1587 if opts.upgrade and not opts.git:
1588 try:
1588 try:
1589 def losedata(fn):
1589 def losedata(fn):
1590 if not losedatafn or not losedatafn(fn=fn):
1590 if not losedatafn or not losedatafn(fn=fn):
1591 raise GitDiffRequired
1591 raise GitDiffRequired
1592 # Buffer the whole output until we are sure it can be generated
1592 # Buffer the whole output until we are sure it can be generated
1593 return list(difffn(opts.copy(git=False), losedata))
1593 return list(difffn(opts.copy(git=False), losedata))
1594 except GitDiffRequired:
1594 except GitDiffRequired:
1595 return difffn(opts.copy(git=True), None)
1595 return difffn(opts.copy(git=True), None)
1596 else:
1596 else:
1597 return difffn(opts, None)
1597 return difffn(opts, None)
1598
1598
1599 def difflabel(func, *args, **kw):
1599 def difflabel(func, *args, **kw):
1600 '''yields 2-tuples of (output, label) based on the output of func()'''
1600 '''yields 2-tuples of (output, label) based on the output of func()'''
1601 headprefixes = [('diff', 'diff.diffline'),
1601 headprefixes = [('diff', 'diff.diffline'),
1602 ('copy', 'diff.extended'),
1602 ('copy', 'diff.extended'),
1603 ('rename', 'diff.extended'),
1603 ('rename', 'diff.extended'),
1604 ('old', 'diff.extended'),
1604 ('old', 'diff.extended'),
1605 ('new', 'diff.extended'),
1605 ('new', 'diff.extended'),
1606 ('deleted', 'diff.extended'),
1606 ('deleted', 'diff.extended'),
1607 ('---', 'diff.file_a'),
1607 ('---', 'diff.file_a'),
1608 ('+++', 'diff.file_b')]
1608 ('+++', 'diff.file_b')]
1609 textprefixes = [('@', 'diff.hunk'),
1609 textprefixes = [('@', 'diff.hunk'),
1610 ('-', 'diff.deleted'),
1610 ('-', 'diff.deleted'),
1611 ('+', 'diff.inserted')]
1611 ('+', 'diff.inserted')]
1612 head = False
1612 head = False
1613 for chunk in func(*args, **kw):
1613 for chunk in func(*args, **kw):
1614 lines = chunk.split('\n')
1614 lines = chunk.split('\n')
1615 for i, line in enumerate(lines):
1615 for i, line in enumerate(lines):
1616 if i != 0:
1616 if i != 0:
1617 yield ('\n', '')
1617 yield ('\n', '')
1618 if head:
1618 if head:
1619 if line.startswith('@'):
1619 if line.startswith('@'):
1620 head = False
1620 head = False
1621 else:
1621 else:
1622 if line and line[0] not in ' +-@\\':
1622 if line and line[0] not in ' +-@\\':
1623 head = True
1623 head = True
1624 stripline = line
1624 stripline = line
1625 if not head and line and line[0] in '+-':
1625 if not head and line and line[0] in '+-':
1626 # highlight trailing whitespace, but only in changed lines
1626 # highlight trailing whitespace, but only in changed lines
1627 stripline = line.rstrip()
1627 stripline = line.rstrip()
1628 prefixes = textprefixes
1628 prefixes = textprefixes
1629 if head:
1629 if head:
1630 prefixes = headprefixes
1630 prefixes = headprefixes
1631 for prefix, label in prefixes:
1631 for prefix, label in prefixes:
1632 if stripline.startswith(prefix):
1632 if stripline.startswith(prefix):
1633 yield (stripline, label)
1633 yield (stripline, label)
1634 break
1634 break
1635 else:
1635 else:
1636 yield (line, '')
1636 yield (line, '')
1637 if line != stripline:
1637 if line != stripline:
1638 yield (line[len(stripline):], 'diff.trailingwhitespace')
1638 yield (line[len(stripline):], 'diff.trailingwhitespace')
1639
1639
1640 def diffui(*args, **kw):
1640 def diffui(*args, **kw):
1641 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1641 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1642 return difflabel(diff, *args, **kw)
1642 return difflabel(diff, *args, **kw)
1643
1643
1644 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1644 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1645 copy, getfilectx, opts, losedatafn, prefix):
1645 copy, getfilectx, opts, losedatafn, prefix):
1646
1646
1647 def join(f):
1647 def join(f):
1648 return posixpath.join(prefix, f)
1648 return posixpath.join(prefix, f)
1649
1649
1650 def addmodehdr(header, omode, nmode):
1650 def addmodehdr(header, omode, nmode):
1651 if omode != nmode:
1651 if omode != nmode:
1652 header.append('old mode %s\n' % omode)
1652 header.append('old mode %s\n' % omode)
1653 header.append('new mode %s\n' % nmode)
1653 header.append('new mode %s\n' % nmode)
1654
1654
1655 def addindexmeta(meta, revs):
1655 def addindexmeta(meta, revs):
1656 if opts.git:
1656 if opts.git:
1657 i = len(revs)
1657 i = len(revs)
1658 if i==2:
1658 if i==2:
1659 meta.append('index %s..%s\n' % tuple(revs))
1659 meta.append('index %s..%s\n' % tuple(revs))
1660 elif i==3:
1660 elif i==3:
1661 meta.append('index %s,%s..%s\n' % tuple(revs))
1661 meta.append('index %s,%s..%s\n' % tuple(revs))
1662
1662
1663 def gitindex(text):
1663 def gitindex(text):
1664 if not text:
1664 if not text:
1665 text = ""
1665 text = ""
1666 l = len(text)
1666 l = len(text)
1667 s = util.sha1('blob %d\0' % l)
1667 s = util.sha1('blob %d\0' % l)
1668 s.update(text)
1668 s.update(text)
1669 return s.hexdigest()
1669 return s.hexdigest()
1670
1670
1671 def diffline(a, b, revs):
1671 def diffline(a, b, revs):
1672 if opts.git:
1672 if opts.git:
1673 line = 'diff --git a/%s b/%s\n' % (a, b)
1673 line = 'diff --git a/%s b/%s\n' % (a, b)
1674 elif not repo.ui.quiet:
1674 elif not repo.ui.quiet:
1675 if revs:
1675 if revs:
1676 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1676 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1677 line = 'diff %s %s\n' % (revinfo, a)
1677 line = 'diff %s %s\n' % (revinfo, a)
1678 else:
1678 else:
1679 line = 'diff %s\n' % a
1679 line = 'diff %s\n' % a
1680 else:
1680 else:
1681 line = ''
1681 line = ''
1682 return line
1682 return line
1683
1683
1684 date1 = util.datestr(ctx1.date())
1684 date1 = util.datestr(ctx1.date())
1685 man1 = ctx1.manifest()
1685 man1 = ctx1.manifest()
1686
1686
1687 gone = set()
1687 gone = set()
1688 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1688 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1689
1689
1690 copyto = dict([(v, k) for k, v in copy.items()])
1690 copyto = dict([(v, k) for k, v in copy.items()])
1691
1691
1692 if opts.git:
1692 if opts.git:
1693 revs = None
1693 revs = None
1694
1694
1695 for f in sorted(modified + added + removed):
1695 for f in sorted(modified + added + removed):
1696 to = None
1696 to = None
1697 tn = None
1697 tn = None
1698 dodiff = True
1698 dodiff = True
1699 header = []
1699 header = []
1700 if f in man1:
1700 if f in man1:
1701 to = getfilectx(f, ctx1).data()
1701 to = getfilectx(f, ctx1).data()
1702 if f not in removed:
1702 if f not in removed:
1703 tn = getfilectx(f, ctx2).data()
1703 tn = getfilectx(f, ctx2).data()
1704 a, b = f, f
1704 a, b = f, f
1705 if opts.git or losedatafn:
1705 if opts.git or losedatafn:
1706 if f in added:
1706 if f in added or (f in modified and to is None):
1707 mode = gitmode[ctx2.flags(f)]
1707 mode = gitmode[ctx2.flags(f)]
1708 if f in copy or f in copyto:
1708 if f in copy or f in copyto:
1709 if opts.git:
1709 if opts.git:
1710 if f in copy:
1710 if f in copy:
1711 a = copy[f]
1711 a = copy[f]
1712 else:
1712 else:
1713 a = copyto[f]
1713 a = copyto[f]
1714 omode = gitmode[man1.flags(a)]
1714 omode = gitmode[man1.flags(a)]
1715 addmodehdr(header, omode, mode)
1715 addmodehdr(header, omode, mode)
1716 if a in removed and a not in gone:
1716 if a in removed and a not in gone:
1717 op = 'rename'
1717 op = 'rename'
1718 gone.add(a)
1718 gone.add(a)
1719 else:
1719 else:
1720 op = 'copy'
1720 op = 'copy'
1721 header.append('%s from %s\n' % (op, join(a)))
1721 header.append('%s from %s\n' % (op, join(a)))
1722 header.append('%s to %s\n' % (op, join(f)))
1722 header.append('%s to %s\n' % (op, join(f)))
1723 to = getfilectx(a, ctx1).data()
1723 to = getfilectx(a, ctx1).data()
1724 else:
1724 else:
1725 losedatafn(f)
1725 losedatafn(f)
1726 else:
1726 else:
1727 if opts.git:
1727 if opts.git:
1728 header.append('new file mode %s\n' % mode)
1728 header.append('new file mode %s\n' % mode)
1729 elif ctx2.flags(f):
1729 elif ctx2.flags(f):
1730 losedatafn(f)
1730 losedatafn(f)
1731 # In theory, if tn was copied or renamed we should check
1731 # In theory, if tn was copied or renamed we should check
1732 # if the source is binary too but the copy record already
1732 # if the source is binary too but the copy record already
1733 # forces git mode.
1733 # forces git mode.
1734 if util.binary(tn):
1734 if util.binary(tn):
1735 if opts.git:
1735 if opts.git:
1736 dodiff = 'binary'
1736 dodiff = 'binary'
1737 else:
1737 else:
1738 losedatafn(f)
1738 losedatafn(f)
1739 if not opts.git and not tn:
1739 if not opts.git and not tn:
1740 # regular diffs cannot represent new empty file
1740 # regular diffs cannot represent new empty file
1741 losedatafn(f)
1741 losedatafn(f)
1742 elif f in removed:
1742 elif f in removed or (f in modified and tn is None):
1743 if opts.git:
1743 if opts.git:
1744 # have we already reported a copy above?
1744 # have we already reported a copy above?
1745 if ((f in copy and copy[f] in added
1745 if ((f in copy and copy[f] in added
1746 and copyto[copy[f]] == f) or
1746 and copyto[copy[f]] == f) or
1747 (f in copyto and copyto[f] in added
1747 (f in copyto and copyto[f] in added
1748 and copy[copyto[f]] == f)):
1748 and copy[copyto[f]] == f)):
1749 dodiff = False
1749 dodiff = False
1750 else:
1750 else:
1751 header.append('deleted file mode %s\n' %
1751 header.append('deleted file mode %s\n' %
1752 gitmode[man1.flags(f)])
1752 gitmode[man1.flags(f)])
1753 if util.binary(to):
1753 if util.binary(to):
1754 dodiff = 'binary'
1754 dodiff = 'binary'
1755 elif not to or util.binary(to):
1755 elif not to or util.binary(to):
1756 # regular diffs cannot represent empty file deletion
1756 # regular diffs cannot represent empty file deletion
1757 losedatafn(f)
1757 losedatafn(f)
1758 else:
1758 else:
1759 oflag = man1.flags(f)
1759 oflag = man1.flags(f)
1760 nflag = ctx2.flags(f)
1760 nflag = ctx2.flags(f)
1761 binary = util.binary(to) or util.binary(tn)
1761 binary = util.binary(to) or util.binary(tn)
1762 if opts.git:
1762 if opts.git:
1763 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1763 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1764 if binary:
1764 if binary:
1765 dodiff = 'binary'
1765 dodiff = 'binary'
1766 elif binary or nflag != oflag:
1766 elif binary or nflag != oflag:
1767 losedatafn(f)
1767 losedatafn(f)
1768
1768
1769 if dodiff:
1769 if dodiff:
1770 if opts.git or revs:
1770 if opts.git or revs:
1771 header.insert(0, diffline(join(a), join(b), revs))
1771 header.insert(0, diffline(join(a), join(b), revs))
1772 if dodiff == 'binary':
1772 if dodiff == 'binary':
1773 text = mdiff.b85diff(to, tn)
1773 text = mdiff.b85diff(to, tn)
1774 if text:
1774 if text:
1775 addindexmeta(header, [gitindex(to), gitindex(tn)])
1775 addindexmeta(header, [gitindex(to), gitindex(tn)])
1776 else:
1776 else:
1777 text = mdiff.unidiff(to, date1,
1777 text = mdiff.unidiff(to, date1,
1778 # ctx2 date may be dynamic
1778 # ctx2 date may be dynamic
1779 tn, util.datestr(ctx2.date()),
1779 tn, util.datestr(ctx2.date()),
1780 join(a), join(b), opts=opts)
1780 join(a), join(b), opts=opts)
1781 if header and (text or len(header) > 1):
1781 if header and (text or len(header) > 1):
1782 yield ''.join(header)
1782 yield ''.join(header)
1783 if text:
1783 if text:
1784 yield text
1784 yield text
1785
1785
1786 def diffstatsum(stats):
1786 def diffstatsum(stats):
1787 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1787 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1788 for f, a, r, b in stats:
1788 for f, a, r, b in stats:
1789 maxfile = max(maxfile, encoding.colwidth(f))
1789 maxfile = max(maxfile, encoding.colwidth(f))
1790 maxtotal = max(maxtotal, a + r)
1790 maxtotal = max(maxtotal, a + r)
1791 addtotal += a
1791 addtotal += a
1792 removetotal += r
1792 removetotal += r
1793 binary = binary or b
1793 binary = binary or b
1794
1794
1795 return maxfile, maxtotal, addtotal, removetotal, binary
1795 return maxfile, maxtotal, addtotal, removetotal, binary
1796
1796
1797 def diffstatdata(lines):
1797 def diffstatdata(lines):
1798 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1798 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1799
1799
1800 results = []
1800 results = []
1801 filename, adds, removes, isbinary = None, 0, 0, False
1801 filename, adds, removes, isbinary = None, 0, 0, False
1802
1802
1803 def addresult():
1803 def addresult():
1804 if filename:
1804 if filename:
1805 results.append((filename, adds, removes, isbinary))
1805 results.append((filename, adds, removes, isbinary))
1806
1806
1807 for line in lines:
1807 for line in lines:
1808 if line.startswith('diff'):
1808 if line.startswith('diff'):
1809 addresult()
1809 addresult()
1810 # set numbers to 0 anyway when starting new file
1810 # set numbers to 0 anyway when starting new file
1811 adds, removes, isbinary = 0, 0, False
1811 adds, removes, isbinary = 0, 0, False
1812 if line.startswith('diff --git a/'):
1812 if line.startswith('diff --git a/'):
1813 filename = gitre.search(line).group(1)
1813 filename = gitre.search(line).group(1)
1814 elif line.startswith('diff -r'):
1814 elif line.startswith('diff -r'):
1815 # format: "diff -r ... -r ... filename"
1815 # format: "diff -r ... -r ... filename"
1816 filename = diffre.search(line).group(1)
1816 filename = diffre.search(line).group(1)
1817 elif line.startswith('+') and not line.startswith('+++ '):
1817 elif line.startswith('+') and not line.startswith('+++ '):
1818 adds += 1
1818 adds += 1
1819 elif line.startswith('-') and not line.startswith('--- '):
1819 elif line.startswith('-') and not line.startswith('--- '):
1820 removes += 1
1820 removes += 1
1821 elif (line.startswith('GIT binary patch') or
1821 elif (line.startswith('GIT binary patch') or
1822 line.startswith('Binary file')):
1822 line.startswith('Binary file')):
1823 isbinary = True
1823 isbinary = True
1824 addresult()
1824 addresult()
1825 return results
1825 return results
1826
1826
1827 def diffstat(lines, width=80, git=False):
1827 def diffstat(lines, width=80, git=False):
1828 output = []
1828 output = []
1829 stats = diffstatdata(lines)
1829 stats = diffstatdata(lines)
1830 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1830 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1831
1831
1832 countwidth = len(str(maxtotal))
1832 countwidth = len(str(maxtotal))
1833 if hasbinary and countwidth < 3:
1833 if hasbinary and countwidth < 3:
1834 countwidth = 3
1834 countwidth = 3
1835 graphwidth = width - countwidth - maxname - 6
1835 graphwidth = width - countwidth - maxname - 6
1836 if graphwidth < 10:
1836 if graphwidth < 10:
1837 graphwidth = 10
1837 graphwidth = 10
1838
1838
1839 def scale(i):
1839 def scale(i):
1840 if maxtotal <= graphwidth:
1840 if maxtotal <= graphwidth:
1841 return i
1841 return i
1842 # If diffstat runs out of room it doesn't print anything,
1842 # If diffstat runs out of room it doesn't print anything,
1843 # which isn't very useful, so always print at least one + or -
1843 # which isn't very useful, so always print at least one + or -
1844 # if there were at least some changes.
1844 # if there were at least some changes.
1845 return max(i * graphwidth // maxtotal, int(bool(i)))
1845 return max(i * graphwidth // maxtotal, int(bool(i)))
1846
1846
1847 for filename, adds, removes, isbinary in stats:
1847 for filename, adds, removes, isbinary in stats:
1848 if isbinary:
1848 if isbinary:
1849 count = 'Bin'
1849 count = 'Bin'
1850 else:
1850 else:
1851 count = adds + removes
1851 count = adds + removes
1852 pluses = '+' * scale(adds)
1852 pluses = '+' * scale(adds)
1853 minuses = '-' * scale(removes)
1853 minuses = '-' * scale(removes)
1854 output.append(' %s%s | %*s %s%s\n' %
1854 output.append(' %s%s | %*s %s%s\n' %
1855 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1855 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1856 countwidth, count, pluses, minuses))
1856 countwidth, count, pluses, minuses))
1857
1857
1858 if stats:
1858 if stats:
1859 output.append(_(' %d files changed, %d insertions(+), '
1859 output.append(_(' %d files changed, %d insertions(+), '
1860 '%d deletions(-)\n')
1860 '%d deletions(-)\n')
1861 % (len(stats), totaladds, totalremoves))
1861 % (len(stats), totaladds, totalremoves))
1862
1862
1863 return ''.join(output)
1863 return ''.join(output)
1864
1864
1865 def diffstatui(*args, **kw):
1865 def diffstatui(*args, **kw):
1866 '''like diffstat(), but yields 2-tuples of (output, label) for
1866 '''like diffstat(), but yields 2-tuples of (output, label) for
1867 ui.write()
1867 ui.write()
1868 '''
1868 '''
1869
1869
1870 for line in diffstat(*args, **kw).splitlines():
1870 for line in diffstat(*args, **kw).splitlines():
1871 if line and line[-1] in '+-':
1871 if line and line[-1] in '+-':
1872 name, graph = line.rsplit(' ', 1)
1872 name, graph = line.rsplit(' ', 1)
1873 yield (name + ' ', '')
1873 yield (name + ' ', '')
1874 m = re.search(r'\++', graph)
1874 m = re.search(r'\++', graph)
1875 if m:
1875 if m:
1876 yield (m.group(0), 'diffstat.inserted')
1876 yield (m.group(0), 'diffstat.inserted')
1877 m = re.search(r'-+', graph)
1877 m = re.search(r'-+', graph)
1878 if m:
1878 if m:
1879 yield (m.group(0), 'diffstat.deleted')
1879 yield (m.group(0), 'diffstat.deleted')
1880 else:
1880 else:
1881 yield (line, '')
1881 yield (line, '')
1882 yield ('\n', '')
1882 yield ('\n', '')
@@ -1,362 +1,382 b''
1 $ hg init
1 $ hg init
2 $ echo start > start
2 $ echo start > start
3 $ hg ci -Amstart
3 $ hg ci -Amstart
4 adding start
4 adding start
5
5
6 New file:
6 New file:
7
7
8 $ echo new > new
8 $ echo new > new
9 $ hg ci -Amnew
9 $ hg ci -Amnew
10 adding new
10 adding new
11 $ hg diff --git -r 0
11 $ hg diff --git -r 0
12 diff --git a/new b/new
12 diff --git a/new b/new
13 new file mode 100644
13 new file mode 100644
14 --- /dev/null
14 --- /dev/null
15 +++ b/new
15 +++ b/new
16 @@ -0,0 +1,1 @@
16 @@ -0,0 +1,1 @@
17 +new
17 +new
18
18
19 Copy:
19 Copy:
20
20
21 $ hg cp new copy
21 $ hg cp new copy
22 $ hg ci -mcopy
22 $ hg ci -mcopy
23 $ hg diff --git -r 1:tip
23 $ hg diff --git -r 1:tip
24 diff --git a/new b/copy
24 diff --git a/new b/copy
25 copy from new
25 copy from new
26 copy to copy
26 copy to copy
27
27
28 Rename:
28 Rename:
29
29
30 $ hg mv copy rename
30 $ hg mv copy rename
31 $ hg ci -mrename
31 $ hg ci -mrename
32 $ hg diff --git -r 2:tip
32 $ hg diff --git -r 2:tip
33 diff --git a/copy b/rename
33 diff --git a/copy b/rename
34 rename from copy
34 rename from copy
35 rename to rename
35 rename to rename
36
36
37 Delete:
37 Delete:
38
38
39 $ hg rm rename
39 $ hg rm rename
40 $ hg ci -mdelete
40 $ hg ci -mdelete
41 $ hg diff --git -r 3:tip
41 $ hg diff --git -r 3:tip
42 diff --git a/rename b/rename
42 diff --git a/rename b/rename
43 deleted file mode 100644
43 deleted file mode 100644
44 --- a/rename
44 --- a/rename
45 +++ /dev/null
45 +++ /dev/null
46 @@ -1,1 +0,0 @@
46 @@ -1,1 +0,0 @@
47 -new
47 -new
48
48
49 $ cat > src <<EOF
49 $ cat > src <<EOF
50 > 1
50 > 1
51 > 2
51 > 2
52 > 3
52 > 3
53 > 4
53 > 4
54 > 5
54 > 5
55 > EOF
55 > EOF
56 $ hg ci -Amsrc
56 $ hg ci -Amsrc
57 adding src
57 adding src
58
58
59 #if execbit
59 #if execbit
60
60
61 chmod 644:
61 chmod 644:
62
62
63 $ chmod +x src
63 $ chmod +x src
64 $ hg ci -munexec
64 $ hg ci -munexec
65 $ hg diff --git -r 5:tip
65 $ hg diff --git -r 5:tip
66 diff --git a/src b/src
66 diff --git a/src b/src
67 old mode 100644
67 old mode 100644
68 new mode 100755
68 new mode 100755
69
69
70 Rename+mod+chmod:
70 Rename+mod+chmod:
71
71
72 $ hg mv src dst
72 $ hg mv src dst
73 $ chmod -x dst
73 $ chmod -x dst
74 $ echo a >> dst
74 $ echo a >> dst
75 $ hg ci -mrenamemod
75 $ hg ci -mrenamemod
76 $ hg diff --git -r 6:tip
76 $ hg diff --git -r 6:tip
77 diff --git a/src b/dst
77 diff --git a/src b/dst
78 old mode 100755
78 old mode 100755
79 new mode 100644
79 new mode 100644
80 rename from src
80 rename from src
81 rename to dst
81 rename to dst
82 --- a/src
82 --- a/src
83 +++ b/dst
83 +++ b/dst
84 @@ -3,3 +3,4 @@
84 @@ -3,3 +3,4 @@
85 3
85 3
86 4
86 4
87 5
87 5
88 +a
88 +a
89
89
90 Nonexistent in tip+chmod:
90 Nonexistent in tip+chmod:
91
91
92 $ hg diff --git -r 5:6
92 $ hg diff --git -r 5:6
93 diff --git a/src b/src
93 diff --git a/src b/src
94 old mode 100644
94 old mode 100644
95 new mode 100755
95 new mode 100755
96
96
97 #else
97 #else
98
98
99 Dummy changes when no exec bit, mocking the execbit commit structure
99 Dummy changes when no exec bit, mocking the execbit commit structure
100
100
101 $ echo change >> src
101 $ echo change >> src
102 $ hg ci -munexec
102 $ hg ci -munexec
103 $ hg mv src dst
103 $ hg mv src dst
104 $ hg ci -mrenamemod
104 $ hg ci -mrenamemod
105
105
106 #endif
106 #endif
107
107
108 Binary diff:
108 Binary diff:
109
109
110 $ cp "$TESTDIR/binfile.bin" .
110 $ cp "$TESTDIR/binfile.bin" .
111 $ hg add binfile.bin
111 $ hg add binfile.bin
112 $ hg diff --git > b.diff
112 $ hg diff --git > b.diff
113 $ cat b.diff
113 $ cat b.diff
114 diff --git a/binfile.bin b/binfile.bin
114 diff --git a/binfile.bin b/binfile.bin
115 new file mode 100644
115 new file mode 100644
116 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
116 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
117 GIT binary patch
117 GIT binary patch
118 literal 593
118 literal 593
119 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
119 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
120 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
120 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
121 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
121 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
122 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
122 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
123 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
123 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
124 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
124 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
125 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
125 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
126 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
126 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
127 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
127 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
128 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
128 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
129 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
129 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
130 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
130 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
131
131
132
132
133 Import binary diff:
133 Import binary diff:
134
134
135 $ hg revert binfile.bin
135 $ hg revert binfile.bin
136 $ rm binfile.bin
136 $ rm binfile.bin
137 $ hg import -mfoo b.diff
137 $ hg import -mfoo b.diff
138 applying b.diff
138 applying b.diff
139 $ cmp binfile.bin "$TESTDIR/binfile.bin"
139 $ cmp binfile.bin "$TESTDIR/binfile.bin"
140
140
141 Rename binary file:
141 Rename binary file:
142
142
143 $ hg mv binfile.bin renamed.bin
143 $ hg mv binfile.bin renamed.bin
144 $ hg diff --git
144 $ hg diff --git
145 diff --git a/binfile.bin b/renamed.bin
145 diff --git a/binfile.bin b/renamed.bin
146 rename from binfile.bin
146 rename from binfile.bin
147 rename to renamed.bin
147 rename to renamed.bin
148
148
149 Diff across many revisions:
149 Diff across many revisions:
150
150
151 $ hg mv dst dst2
151 $ hg mv dst dst2
152 $ hg ci -m 'mv dst dst2'
152 $ hg ci -m 'mv dst dst2'
153
153
154 $ echo >> start
154 $ echo >> start
155 $ hg ci -m 'change start'
155 $ hg ci -m 'change start'
156
156
157 $ hg revert -r -2 start
157 $ hg revert -r -2 start
158 $ hg mv dst2 dst3
158 $ hg mv dst2 dst3
159 $ hg ci -m 'mv dst2 dst3; revert start'
159 $ hg ci -m 'mv dst2 dst3; revert start'
160
160
161 $ hg diff --git -r 9:11
161 $ hg diff --git -r 9:11
162 diff --git a/dst2 b/dst3
162 diff --git a/dst2 b/dst3
163 rename from dst2
163 rename from dst2
164 rename to dst3
164 rename to dst3
165
165
166 Reversed:
166 Reversed:
167
167
168 $ hg diff --git -r 11:9
168 $ hg diff --git -r 11:9
169 diff --git a/dst3 b/dst2
169 diff --git a/dst3 b/dst2
170 rename from dst3
170 rename from dst3
171 rename to dst2
171 rename to dst2
172
172
173
173
174 $ echo a >> foo
174 $ echo a >> foo
175 $ hg add foo
175 $ hg add foo
176 $ hg ci -m 'add foo'
176 $ hg ci -m 'add foo'
177 $ echo b >> foo
177 $ echo b >> foo
178 $ hg ci -m 'change foo'
178 $ hg ci -m 'change foo'
179 $ hg mv foo bar
179 $ hg mv foo bar
180 $ hg ci -m 'mv foo bar'
180 $ hg ci -m 'mv foo bar'
181 $ echo c >> bar
181 $ echo c >> bar
182 $ hg ci -m 'change bar'
182 $ hg ci -m 'change bar'
183
183
184 File created before r1 and renamed before r2:
184 File created before r1 and renamed before r2:
185
185
186 $ hg diff --git -r -3:-1
186 $ hg diff --git -r -3:-1
187 diff --git a/foo b/bar
187 diff --git a/foo b/bar
188 rename from foo
188 rename from foo
189 rename to bar
189 rename to bar
190 --- a/foo
190 --- a/foo
191 +++ b/bar
191 +++ b/bar
192 @@ -1,2 +1,3 @@
192 @@ -1,2 +1,3 @@
193 a
193 a
194 b
194 b
195 +c
195 +c
196
196
197 Reversed:
197 Reversed:
198
198
199 $ hg diff --git -r -1:-3
199 $ hg diff --git -r -1:-3
200 diff --git a/bar b/foo
200 diff --git a/bar b/foo
201 rename from bar
201 rename from bar
202 rename to foo
202 rename to foo
203 --- a/bar
203 --- a/bar
204 +++ b/foo
204 +++ b/foo
205 @@ -1,3 +1,2 @@
205 @@ -1,3 +1,2 @@
206 a
206 a
207 b
207 b
208 -c
208 -c
209
209
210 File created in r1 and renamed before r2:
210 File created in r1 and renamed before r2:
211
211
212 $ hg diff --git -r -4:-1
212 $ hg diff --git -r -4:-1
213 diff --git a/foo b/bar
213 diff --git a/foo b/bar
214 rename from foo
214 rename from foo
215 rename to bar
215 rename to bar
216 --- a/foo
216 --- a/foo
217 +++ b/bar
217 +++ b/bar
218 @@ -1,1 +1,3 @@
218 @@ -1,1 +1,3 @@
219 a
219 a
220 +b
220 +b
221 +c
221 +c
222
222
223 Reversed:
223 Reversed:
224
224
225 $ hg diff --git -r -1:-4
225 $ hg diff --git -r -1:-4
226 diff --git a/bar b/foo
226 diff --git a/bar b/foo
227 rename from bar
227 rename from bar
228 rename to foo
228 rename to foo
229 --- a/bar
229 --- a/bar
230 +++ b/foo
230 +++ b/foo
231 @@ -1,3 +1,1 @@
231 @@ -1,3 +1,1 @@
232 a
232 a
233 -b
233 -b
234 -c
234 -c
235
235
236 File created after r1 and renamed before r2:
236 File created after r1 and renamed before r2:
237
237
238 $ hg diff --git -r -5:-1
238 $ hg diff --git -r -5:-1
239 diff --git a/bar b/bar
239 diff --git a/bar b/bar
240 new file mode 100644
240 new file mode 100644
241 --- /dev/null
241 --- /dev/null
242 +++ b/bar
242 +++ b/bar
243 @@ -0,0 +1,3 @@
243 @@ -0,0 +1,3 @@
244 +a
244 +a
245 +b
245 +b
246 +c
246 +c
247
247
248 Reversed:
248 Reversed:
249
249
250 $ hg diff --git -r -1:-5
250 $ hg diff --git -r -1:-5
251 diff --git a/bar b/bar
251 diff --git a/bar b/bar
252 deleted file mode 100644
252 deleted file mode 100644
253 --- a/bar
253 --- a/bar
254 +++ /dev/null
254 +++ /dev/null
255 @@ -1,3 +0,0 @@
255 @@ -1,3 +0,0 @@
256 -a
256 -a
257 -b
257 -b
258 -c
258 -c
259
259
260
260
261 Comparing with the working dir:
261 Comparing with the working dir:
262
262
263 $ echo >> start
263 $ echo >> start
264 $ hg ci -m 'change start again'
264 $ hg ci -m 'change start again'
265
265
266 $ echo > created
266 $ echo > created
267 $ hg add created
267 $ hg add created
268 $ hg ci -m 'add created'
268 $ hg ci -m 'add created'
269
269
270 $ hg mv created created2
270 $ hg mv created created2
271 $ hg ci -m 'mv created created2'
271 $ hg ci -m 'mv created created2'
272
272
273 $ hg mv created2 created3
273 $ hg mv created2 created3
274
274
275 There's a copy in the working dir:
275 There's a copy in the working dir:
276
276
277 $ hg diff --git
277 $ hg diff --git
278 diff --git a/created2 b/created3
278 diff --git a/created2 b/created3
279 rename from created2
279 rename from created2
280 rename to created3
280 rename to created3
281
281
282 There's another copy between the original rev and the wd:
282 There's another copy between the original rev and the wd:
283
283
284 $ hg diff --git -r -2
284 $ hg diff --git -r -2
285 diff --git a/created b/created3
285 diff --git a/created b/created3
286 rename from created
286 rename from created
287 rename to created3
287 rename to created3
288
288
289 The source of the copy was created after the original rev:
289 The source of the copy was created after the original rev:
290
290
291 $ hg diff --git -r -3
291 $ hg diff --git -r -3
292 diff --git a/created3 b/created3
292 diff --git a/created3 b/created3
293 new file mode 100644
293 new file mode 100644
294 --- /dev/null
294 --- /dev/null
295 +++ b/created3
295 +++ b/created3
296 @@ -0,0 +1,1 @@
296 @@ -0,0 +1,1 @@
297 +
297 +
298 $ hg ci -m 'mv created2 created3'
298 $ hg ci -m 'mv created2 created3'
299
299
300
300
301 $ echo > brand-new
301 $ echo > brand-new
302 $ hg add brand-new
302 $ hg add brand-new
303 $ hg ci -m 'add brand-new'
303 $ hg ci -m 'add brand-new'
304 $ hg mv brand-new brand-new2
304 $ hg mv brand-new brand-new2
305
305
306 Created in parent of wd; renamed in the wd:
306 Created in parent of wd; renamed in the wd:
307
307
308 $ hg diff --git
308 $ hg diff --git
309 diff --git a/brand-new b/brand-new2
309 diff --git a/brand-new b/brand-new2
310 rename from brand-new
310 rename from brand-new
311 rename to brand-new2
311 rename to brand-new2
312
312
313 Created between r1 and parent of wd; renamed in the wd:
313 Created between r1 and parent of wd; renamed in the wd:
314
314
315 $ hg diff --git -r -2
315 $ hg diff --git -r -2
316 diff --git a/brand-new2 b/brand-new2
316 diff --git a/brand-new2 b/brand-new2
317 new file mode 100644
317 new file mode 100644
318 --- /dev/null
318 --- /dev/null
319 +++ b/brand-new2
319 +++ b/brand-new2
320 @@ -0,0 +1,1 @@
320 @@ -0,0 +1,1 @@
321 +
321 +
322 $ hg ci -m 'mv brand-new brand-new2'
322 $ hg ci -m 'mv brand-new brand-new2'
323
323
324 One file is copied to many destinations and removed:
324 One file is copied to many destinations and removed:
325
325
326 $ hg cp brand-new2 brand-new3
326 $ hg cp brand-new2 brand-new3
327 $ hg mv brand-new2 brand-new3-2
327 $ hg mv brand-new2 brand-new3-2
328 $ hg ci -m 'multiple renames/copies'
328 $ hg ci -m 'multiple renames/copies'
329 $ hg diff --git -r -2 -r -1
329 $ hg diff --git -r -2 -r -1
330 diff --git a/brand-new2 b/brand-new3
330 diff --git a/brand-new2 b/brand-new3
331 rename from brand-new2
331 rename from brand-new2
332 rename to brand-new3
332 rename to brand-new3
333 diff --git a/brand-new2 b/brand-new3-2
333 diff --git a/brand-new2 b/brand-new3-2
334 copy from brand-new2
334 copy from brand-new2
335 copy to brand-new3-2
335 copy to brand-new3-2
336
336
337 Reversed:
337 Reversed:
338
338
339 $ hg diff --git -r -1 -r -2
339 $ hg diff --git -r -1 -r -2
340 diff --git a/brand-new3-2 b/brand-new2
340 diff --git a/brand-new3-2 b/brand-new2
341 rename from brand-new3-2
341 rename from brand-new3-2
342 rename to brand-new2
342 rename to brand-new2
343 diff --git a/brand-new3 b/brand-new3
343 diff --git a/brand-new3 b/brand-new3
344 deleted file mode 100644
344 deleted file mode 100644
345 --- a/brand-new3
345 --- a/brand-new3
346 +++ /dev/null
346 +++ /dev/null
347 @@ -1,1 +0,0 @@
347 @@ -1,1 +0,0 @@
348 -
348 -
349
349
350 There should be a trailing TAB if there are spaces in the file name:
350 There should be a trailing TAB if there are spaces in the file name:
351
351
352 $ echo foo > 'with spaces'
352 $ echo foo > 'with spaces'
353 $ hg add 'with spaces'
353 $ hg add 'with spaces'
354 $ hg diff --git
354 $ hg diff --git
355 diff --git a/with spaces b/with spaces
355 diff --git a/with spaces b/with spaces
356 new file mode 100644
356 new file mode 100644
357 --- /dev/null
357 --- /dev/null
358 +++ b/with spaces
358 +++ b/with spaces
359 @@ -0,0 +1,1 @@
359 @@ -0,0 +1,1 @@
360 +foo
360 +foo
361 $ hg ci -m 'add filename with spaces'
361 $ hg ci -m 'add filename with spaces'
362
362
363 Additions should be properly marked even in the middle of a merge
364
365 $ hg up -r -2
366 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
367 $ echo "New File" >> inmerge
368 $ hg add inmerge
369 $ hg ci -m "file in merge"
370 created new head
371 $ hg up 23
372 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
373 $ hg merge
374 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
375 (branch merge, don't forget to commit)
376 $ hg diff -g
377 diff --git a/inmerge b/inmerge
378 new file mode 100644
379 --- /dev/null
380 +++ b/inmerge
381 @@ -0,0 +1,1 @@
382 +New File
@@ -1,521 +1,523 b''
1 $ echo "[extensions]" >> $HGRCPATH
1 $ echo "[extensions]" >> $HGRCPATH
2 $ echo "mq=" >> $HGRCPATH
2 $ echo "mq=" >> $HGRCPATH
3 $ echo "shelve=" >> $HGRCPATH
3 $ echo "shelve=" >> $HGRCPATH
4 $ echo "[defaults]" >> $HGRCPATH
4 $ echo "[defaults]" >> $HGRCPATH
5 $ echo "diff = --nodates --git" >> $HGRCPATH
5 $ echo "diff = --nodates --git" >> $HGRCPATH
6 $ echo "qnew = --date '0 0'" >> $HGRCPATH
6 $ echo "qnew = --date '0 0'" >> $HGRCPATH
7
7
8 $ hg init repo
8 $ hg init repo
9 $ cd repo
9 $ cd repo
10 $ mkdir a b
10 $ mkdir a b
11 $ echo a > a/a
11 $ echo a > a/a
12 $ echo b > b/b
12 $ echo b > b/b
13 $ echo c > c
13 $ echo c > c
14 $ echo d > d
14 $ echo d > d
15 $ echo x > x
15 $ echo x > x
16 $ hg addremove -q
16 $ hg addremove -q
17
17
18 shelving in an empty repo should be possible
18 shelving in an empty repo should be possible
19
19
20 $ hg shelve
20 $ hg shelve
21 shelved as default
21 shelved as default
22 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
22 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
23
23
24 $ hg unshelve
24 $ hg unshelve
25 unshelving change 'default'
25 unshelving change 'default'
26 adding changesets
26 adding changesets
27 adding manifests
27 adding manifests
28 adding file changes
28 adding file changes
29 added 1 changesets with 5 changes to 5 files
29 added 1 changesets with 5 changes to 5 files
30
30
31 $ hg commit -q -m 'initial commit'
31 $ hg commit -q -m 'initial commit'
32
32
33 $ hg shelve
33 $ hg shelve
34 nothing changed
34 nothing changed
35 [1]
35 [1]
36
36
37 create an mq patch - shelving should work fine with a patch applied
37 create an mq patch - shelving should work fine with a patch applied
38
38
39 $ echo n > n
39 $ echo n > n
40 $ hg add n
40 $ hg add n
41 $ hg commit n -m second
41 $ hg commit n -m second
42 $ hg qnew second.patch
42 $ hg qnew second.patch
43
43
44 shelve a change that we will delete later
44 shelve a change that we will delete later
45
45
46 $ echo a >> a/a
46 $ echo a >> a/a
47 $ hg shelve
47 $ hg shelve
48 shelved as default
48 shelved as default
49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
50
50
51 set up some more complex changes to shelve
51 set up some more complex changes to shelve
52
52
53 $ echo a >> a/a
53 $ echo a >> a/a
54 $ hg mv b b.rename
54 $ hg mv b b.rename
55 moving b/b to b.rename/b (glob)
55 moving b/b to b.rename/b (glob)
56 $ hg cp c c.copy
56 $ hg cp c c.copy
57 $ hg status -C
57 $ hg status -C
58 M a/a
58 M a/a
59 A b.rename/b
59 A b.rename/b
60 b/b
60 b/b
61 A c.copy
61 A c.copy
62 c
62 c
63 R b/b
63 R b/b
64
64
65 prevent some foot-shooting
65 prevent some foot-shooting
66
66
67 $ hg shelve -n foo/bar
67 $ hg shelve -n foo/bar
68 abort: shelved change names may not contain slashes
68 abort: shelved change names may not contain slashes
69 [255]
69 [255]
70 $ hg shelve -n .baz
70 $ hg shelve -n .baz
71 abort: shelved change names may not start with '.'
71 abort: shelved change names may not start with '.'
72 [255]
72 [255]
73
73
74 the common case - no options or filenames
74 the common case - no options or filenames
75
75
76 $ hg shelve
76 $ hg shelve
77 shelved as default-01
77 shelved as default-01
78 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
78 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
79 $ hg status -C
79 $ hg status -C
80
80
81 ensure that our shelved changes exist
81 ensure that our shelved changes exist
82
82
83 $ hg shelve -l
83 $ hg shelve -l
84 default-01 (*) [mq]: second.patch (glob)
84 default-01 (*) [mq]: second.patch (glob)
85 default (*) [mq]: second.patch (glob)
85 default (*) [mq]: second.patch (glob)
86
86
87 $ hg shelve -l -p default
87 $ hg shelve -l -p default
88 default (*) [mq]: second.patch (glob)
88 default (*) [mq]: second.patch (glob)
89
89
90 diff --git a/a/a b/a/a
90 diff --git a/a/a b/a/a
91 --- a/a/a
91 --- a/a/a
92 +++ b/a/a
92 +++ b/a/a
93 @@ -1,1 +1,2 @@
93 @@ -1,1 +1,2 @@
94 a
94 a
95 +a
95 +a
96
96
97 delete our older shelved change
97 delete our older shelved change
98
98
99 $ hg shelve -d default
99 $ hg shelve -d default
100 $ hg qfinish -a -q
100 $ hg qfinish -a -q
101
101
102 local edits should not prevent a shelved change from applying
102 local edits should not prevent a shelved change from applying
103
103
104 $ printf "z\na\n" > a/a
104 $ printf "z\na\n" > a/a
105 $ hg unshelve --keep
105 $ hg unshelve --keep
106 unshelving change 'default-01'
106 unshelving change 'default-01'
107 adding changesets
107 adding changesets
108 adding manifests
108 adding manifests
109 adding file changes
109 adding file changes
110 added 1 changesets with 3 changes to 8 files (+1 heads)
110 added 1 changesets with 3 changes to 8 files (+1 heads)
111 merging a/a
111 merging a/a
112
112
113 $ hg revert --all -q
113 $ hg revert --all -q
114 $ rm a/a.orig b.rename/b c.copy
114 $ rm a/a.orig b.rename/b c.copy
115
115
116 apply it and make sure our state is as expected
116 apply it and make sure our state is as expected
117
117
118 $ hg unshelve
118 $ hg unshelve
119 unshelving change 'default-01'
119 unshelving change 'default-01'
120 adding changesets
120 adding changesets
121 adding manifests
121 adding manifests
122 adding file changes
122 adding file changes
123 added 1 changesets with 3 changes to 8 files
123 added 1 changesets with 3 changes to 8 files
124 $ hg status -C
124 $ hg status -C
125 M a/a
125 M a/a
126 A b.rename/b
126 A b.rename/b
127 b/b
127 b/b
128 A c.copy
128 A c.copy
129 c
129 c
130 R b/b
130 R b/b
131 $ hg shelve -l
131 $ hg shelve -l
132
132
133 $ hg unshelve
133 $ hg unshelve
134 abort: no shelved changes to apply!
134 abort: no shelved changes to apply!
135 [255]
135 [255]
136 $ hg unshelve foo
136 $ hg unshelve foo
137 abort: shelved change 'foo' not found
137 abort: shelved change 'foo' not found
138 [255]
138 [255]
139
139
140 named shelves, specific filenames, and "commit messages" should all work
140 named shelves, specific filenames, and "commit messages" should all work
141
141
142 $ hg status -C
142 $ hg status -C
143 M a/a
143 M a/a
144 A b.rename/b
144 A b.rename/b
145 b/b
145 b/b
146 A c.copy
146 A c.copy
147 c
147 c
148 R b/b
148 R b/b
149 $ hg shelve -q -n wibble -m wat a
149 $ hg shelve -q -n wibble -m wat a
150
150
151 expect "a" to no longer be present, but status otherwise unchanged
151 expect "a" to no longer be present, but status otherwise unchanged
152
152
153 $ hg status -C
153 $ hg status -C
154 A b.rename/b
154 A b.rename/b
155 b/b
155 b/b
156 A c.copy
156 A c.copy
157 c
157 c
158 R b/b
158 R b/b
159 $ hg shelve -l --stat
159 $ hg shelve -l --stat
160 wibble (*) wat (glob)
160 wibble (*) wat (glob)
161 a/a | 1 +
161 a/a | 1 +
162 1 files changed, 1 insertions(+), 0 deletions(-)
162 1 files changed, 1 insertions(+), 0 deletions(-)
163
163
164 and now "a/a" should reappear
164 and now "a/a" should reappear
165
165
166 $ cd a
166 $ cd a
167 $ hg unshelve -q wibble
167 $ hg unshelve -q wibble
168 $ cd ..
168 $ cd ..
169 $ hg status -C
169 $ hg status -C
170 M a/a
170 M a/a
171 A b.rename/b
171 A b.rename/b
172 b/b
172 b/b
173 A c.copy
173 A c.copy
174 c
174 c
175 R b/b
175 R b/b
176
176
177 cause unshelving to result in a merge with 'a' conflicting
177 cause unshelving to result in a merge with 'a' conflicting
178
178
179 $ hg shelve -q
179 $ hg shelve -q
180 $ echo c>>a/a
180 $ echo c>>a/a
181 $ hg commit -m second
181 $ hg commit -m second
182 $ hg tip --template '{files}\n'
182 $ hg tip --template '{files}\n'
183 a/a
183 a/a
184
184
185 add an unrelated change that should be preserved
185 add an unrelated change that should be preserved
186
186
187 $ mkdir foo
187 $ mkdir foo
188 $ echo foo > foo/foo
188 $ echo foo > foo/foo
189 $ hg add foo/foo
189 $ hg add foo/foo
190
190
191 force a conflicted merge to occur
191 force a conflicted merge to occur
192
192
193 $ hg unshelve
193 $ hg unshelve
194 unshelving change 'default'
194 unshelving change 'default'
195 adding changesets
195 adding changesets
196 adding manifests
196 adding manifests
197 adding file changes
197 adding file changes
198 added 1 changesets with 3 changes to 8 files (+1 heads)
198 added 1 changesets with 3 changes to 8 files (+1 heads)
199 merging a/a
199 merging a/a
200 warning: conflicts during merge.
200 warning: conflicts during merge.
201 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
201 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
202 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
202 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
203 [1]
203 [1]
204
204
205 ensure that we have a merge with unresolved conflicts
205 ensure that we have a merge with unresolved conflicts
206
206
207 $ hg heads -q --template '{rev}\n'
207 $ hg heads -q --template '{rev}\n'
208 5
208 5
209 4
209 4
210 $ hg parents -q --template '{rev}\n'
210 $ hg parents -q --template '{rev}\n'
211 4
211 4
212 5
212 5
213 $ hg status
213 $ hg status
214 M a/a
214 M a/a
215 M b.rename/b
215 M b.rename/b
216 M c.copy
216 M c.copy
217 R b/b
217 R b/b
218 ? a/a.orig
218 ? a/a.orig
219 $ hg diff
219 $ hg diff
220 diff --git a/a/a b/a/a
220 diff --git a/a/a b/a/a
221 --- a/a/a
221 --- a/a/a
222 +++ b/a/a
222 +++ b/a/a
223 @@ -1,2 +1,6 @@
223 @@ -1,2 +1,6 @@
224 a
224 a
225 +<<<<<<< local
225 +<<<<<<< local
226 c
226 c
227 +=======
227 +=======
228 +a
228 +a
229 +>>>>>>> other
229 +>>>>>>> other
230 diff --git a/b.rename/b b/b.rename/b
230 diff --git a/b.rename/b b/b.rename/b
231 new file mode 100644
231 --- /dev/null
232 --- /dev/null
232 +++ b/b.rename/b
233 +++ b/b.rename/b
233 @@ -0,0 +1,1 @@
234 @@ -0,0 +1,1 @@
234 +b
235 +b
235 diff --git a/b/b b/b/b
236 diff --git a/b/b b/b/b
236 deleted file mode 100644
237 deleted file mode 100644
237 --- a/b/b
238 --- a/b/b
238 +++ /dev/null
239 +++ /dev/null
239 @@ -1,1 +0,0 @@
240 @@ -1,1 +0,0 @@
240 -b
241 -b
241 diff --git a/c.copy b/c.copy
242 diff --git a/c.copy b/c.copy
243 new file mode 100644
242 --- /dev/null
244 --- /dev/null
243 +++ b/c.copy
245 +++ b/c.copy
244 @@ -0,0 +1,1 @@
246 @@ -0,0 +1,1 @@
245 +c
247 +c
246 $ hg resolve -l
248 $ hg resolve -l
247 U a/a
249 U a/a
248
250
249 $ hg shelve
251 $ hg shelve
250 abort: unshelve already in progress
252 abort: unshelve already in progress
251 (use 'hg unshelve --continue' or 'hg unshelve --abort')
253 (use 'hg unshelve --continue' or 'hg unshelve --abort')
252 [255]
254 [255]
253
255
254 abort the unshelve and be happy
256 abort the unshelve and be happy
255
257
256 $ hg status
258 $ hg status
257 M a/a
259 M a/a
258 M b.rename/b
260 M b.rename/b
259 M c.copy
261 M c.copy
260 R b/b
262 R b/b
261 ? a/a.orig
263 ? a/a.orig
262 $ hg unshelve -a
264 $ hg unshelve -a
263 rebase aborted
265 rebase aborted
264 unshelve of 'default' aborted
266 unshelve of 'default' aborted
265 $ hg heads -q
267 $ hg heads -q
266 3:2e69b451d1ea
268 3:2e69b451d1ea
267 $ hg parents
269 $ hg parents
268 changeset: 3:2e69b451d1ea
270 changeset: 3:2e69b451d1ea
269 tag: tip
271 tag: tip
270 user: test
272 user: test
271 date: Thu Jan 01 00:00:00 1970 +0000
273 date: Thu Jan 01 00:00:00 1970 +0000
272 summary: second
274 summary: second
273
275
274 $ hg resolve -l
276 $ hg resolve -l
275 $ hg status
277 $ hg status
276 A foo/foo
278 A foo/foo
277 ? a/a.orig
279 ? a/a.orig
278
280
279 try to continue with no unshelve underway
281 try to continue with no unshelve underway
280
282
281 $ hg unshelve -c
283 $ hg unshelve -c
282 abort: no unshelve operation underway
284 abort: no unshelve operation underway
283 [255]
285 [255]
284 $ hg status
286 $ hg status
285 A foo/foo
287 A foo/foo
286 ? a/a.orig
288 ? a/a.orig
287
289
288 redo the unshelve to get a conflict
290 redo the unshelve to get a conflict
289
291
290 $ hg unshelve -q
292 $ hg unshelve -q
291 warning: conflicts during merge.
293 warning: conflicts during merge.
292 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
294 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
293 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
295 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
294 [1]
296 [1]
295
297
296 attempt to continue
298 attempt to continue
297
299
298 $ hg unshelve -c
300 $ hg unshelve -c
299 abort: unresolved conflicts, can't continue
301 abort: unresolved conflicts, can't continue
300 (see 'hg resolve', then 'hg unshelve --continue')
302 (see 'hg resolve', then 'hg unshelve --continue')
301 [255]
303 [255]
302
304
303 $ hg revert -r . a/a
305 $ hg revert -r . a/a
304 $ hg resolve -m a/a
306 $ hg resolve -m a/a
305
307
306 $ hg commit -m 'commit while unshelve in progress'
308 $ hg commit -m 'commit while unshelve in progress'
307 abort: unshelve already in progress
309 abort: unshelve already in progress
308 (use 'hg unshelve --continue' or 'hg unshelve --abort')
310 (use 'hg unshelve --continue' or 'hg unshelve --abort')
309 [255]
311 [255]
310
312
311 $ hg unshelve -c
313 $ hg unshelve -c
312 unshelve of 'default' complete
314 unshelve of 'default' complete
313
315
314 ensure the repo is as we hope
316 ensure the repo is as we hope
315
317
316 $ hg parents
318 $ hg parents
317 changeset: 3:2e69b451d1ea
319 changeset: 3:2e69b451d1ea
318 tag: tip
320 tag: tip
319 user: test
321 user: test
320 date: Thu Jan 01 00:00:00 1970 +0000
322 date: Thu Jan 01 00:00:00 1970 +0000
321 summary: second
323 summary: second
322
324
323 $ hg heads -q
325 $ hg heads -q
324 3:2e69b451d1ea
326 3:2e69b451d1ea
325
327
326 $ hg status -C
328 $ hg status -C
327 A b.rename/b
329 A b.rename/b
328 b/b
330 b/b
329 A c.copy
331 A c.copy
330 c
332 c
331 A foo/foo
333 A foo/foo
332 R b/b
334 R b/b
333 ? a/a.orig
335 ? a/a.orig
334
336
335 there should be no shelves left
337 there should be no shelves left
336
338
337 $ hg shelve -l
339 $ hg shelve -l
338
340
339 #if execbit
341 #if execbit
340
342
341 ensure that metadata-only changes are shelved
343 ensure that metadata-only changes are shelved
342
344
343 $ chmod +x a/a
345 $ chmod +x a/a
344 $ hg shelve -q -n execbit a/a
346 $ hg shelve -q -n execbit a/a
345 $ hg status a/a
347 $ hg status a/a
346 $ hg unshelve -q execbit
348 $ hg unshelve -q execbit
347 $ hg status a/a
349 $ hg status a/a
348 M a/a
350 M a/a
349 $ hg revert a/a
351 $ hg revert a/a
350
352
351 #endif
353 #endif
352
354
353 #if symlink
355 #if symlink
354
356
355 $ rm a/a
357 $ rm a/a
356 $ ln -s foo a/a
358 $ ln -s foo a/a
357 $ hg shelve -q -n symlink a/a
359 $ hg shelve -q -n symlink a/a
358 $ hg status a/a
360 $ hg status a/a
359 $ hg unshelve -q symlink
361 $ hg unshelve -q symlink
360 $ hg status a/a
362 $ hg status a/a
361 M a/a
363 M a/a
362 $ hg revert a/a
364 $ hg revert a/a
363
365
364 #endif
366 #endif
365
367
366 set up another conflict between a commit and a shelved change
368 set up another conflict between a commit and a shelved change
367
369
368 $ hg revert -q -C -a
370 $ hg revert -q -C -a
369 $ rm a/a.orig b.rename/b c.copy
371 $ rm a/a.orig b.rename/b c.copy
370 $ echo a >> a/a
372 $ echo a >> a/a
371 $ hg shelve -q
373 $ hg shelve -q
372 $ echo x >> a/a
374 $ echo x >> a/a
373 $ hg ci -m 'create conflict'
375 $ hg ci -m 'create conflict'
374 $ hg add foo/foo
376 $ hg add foo/foo
375
377
376 if we resolve a conflict while unshelving, the unshelve should succeed
378 if we resolve a conflict while unshelving, the unshelve should succeed
377
379
378 $ HGMERGE=true hg unshelve
380 $ HGMERGE=true hg unshelve
379 unshelving change 'default'
381 unshelving change 'default'
380 adding changesets
382 adding changesets
381 adding manifests
383 adding manifests
382 adding file changes
384 adding file changes
383 added 1 changesets with 1 changes to 6 files (+1 heads)
385 added 1 changesets with 1 changes to 6 files (+1 heads)
384 merging a/a
386 merging a/a
385 $ hg parents -q
387 $ hg parents -q
386 4:33f7f61e6c5e
388 4:33f7f61e6c5e
387 $ hg shelve -l
389 $ hg shelve -l
388 $ hg status
390 $ hg status
389 A foo/foo
391 A foo/foo
390 $ cat a/a
392 $ cat a/a
391 a
393 a
392 c
394 c
393 x
395 x
394
396
395 test keep and cleanup
397 test keep and cleanup
396
398
397 $ hg shelve
399 $ hg shelve
398 shelved as default
400 shelved as default
399 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
401 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
400 $ hg shelve --list
402 $ hg shelve --list
401 default (*) create conflict (glob)
403 default (*) create conflict (glob)
402 $ hg unshelve --keep
404 $ hg unshelve --keep
403 unshelving change 'default'
405 unshelving change 'default'
404 adding changesets
406 adding changesets
405 adding manifests
407 adding manifests
406 adding file changes
408 adding file changes
407 added 1 changesets with 1 changes to 7 files
409 added 1 changesets with 1 changes to 7 files
408 $ hg shelve --list
410 $ hg shelve --list
409 default (*) create conflict (glob)
411 default (*) create conflict (glob)
410 $ hg shelve --cleanup
412 $ hg shelve --cleanup
411 $ hg shelve --list
413 $ hg shelve --list
412
414
413 test bookmarks
415 test bookmarks
414
416
415 $ hg bookmark test
417 $ hg bookmark test
416 $ hg bookmark
418 $ hg bookmark
417 * test 4:33f7f61e6c5e
419 * test 4:33f7f61e6c5e
418 $ hg shelve
420 $ hg shelve
419 shelved as test
421 shelved as test
420 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
422 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
421 $ hg bookmark
423 $ hg bookmark
422 * test 4:33f7f61e6c5e
424 * test 4:33f7f61e6c5e
423 $ hg unshelve
425 $ hg unshelve
424 unshelving change 'test'
426 unshelving change 'test'
425 adding changesets
427 adding changesets
426 adding manifests
428 adding manifests
427 adding file changes
429 adding file changes
428 added 1 changesets with 1 changes to 7 files
430 added 1 changesets with 1 changes to 7 files
429 $ hg bookmark
431 $ hg bookmark
430 * test 4:33f7f61e6c5e
432 * test 4:33f7f61e6c5e
431
433
432 shelve should still work even if mq is disabled
434 shelve should still work even if mq is disabled
433
435
434 $ hg --config extensions.mq=! shelve
436 $ hg --config extensions.mq=! shelve
435 shelved as test
437 shelved as test
436 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
438 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
437 $ hg --config extensions.mq=! shelve --list
439 $ hg --config extensions.mq=! shelve --list
438 test (1s ago) create conflict
440 test (1s ago) create conflict
439 $ hg --config extensions.mq=! unshelve
441 $ hg --config extensions.mq=! unshelve
440 unshelving change 'test'
442 unshelving change 'test'
441 adding changesets
443 adding changesets
442 adding manifests
444 adding manifests
443 adding file changes
445 adding file changes
444 added 1 changesets with 1 changes to 7 files
446 added 1 changesets with 1 changes to 7 files
445
447
446 shelve should leave dirstate clean (issue 4055)
448 shelve should leave dirstate clean (issue 4055)
447
449
448 $ cd ..
450 $ cd ..
449 $ hg init shelverebase
451 $ hg init shelverebase
450 $ cd shelverebase
452 $ cd shelverebase
451 $ printf 'x\ny\n' > x
453 $ printf 'x\ny\n' > x
452 $ echo z > z
454 $ echo z > z
453 $ hg commit -Aqm xy
455 $ hg commit -Aqm xy
454 $ echo z >> x
456 $ echo z >> x
455 $ hg commit -Aqm z
457 $ hg commit -Aqm z
456 $ hg up 0
458 $ hg up 0
457 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
459 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
458 $ printf 'a\nx\ny\nz\n' > x
460 $ printf 'a\nx\ny\nz\n' > x
459 $ hg commit -Aqm xyz
461 $ hg commit -Aqm xyz
460 $ echo c >> z
462 $ echo c >> z
461 $ hg shelve
463 $ hg shelve
462 shelved as default
464 shelved as default
463 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
465 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
464 $ hg rebase -d 1 --config extensions.rebase=
466 $ hg rebase -d 1 --config extensions.rebase=
465 merging x
467 merging x
466 saved backup bundle to $TESTTMP/shelverebase/.hg/strip-backup/323bfa07f744-backup.hg (glob)
468 saved backup bundle to $TESTTMP/shelverebase/.hg/strip-backup/323bfa07f744-backup.hg (glob)
467 $ hg unshelve
469 $ hg unshelve
468 unshelving change 'default'
470 unshelving change 'default'
469 adding changesets
471 adding changesets
470 adding manifests
472 adding manifests
471 adding file changes
473 adding file changes
472 added 2 changesets with 2 changes to 2 files (+1 heads)
474 added 2 changesets with 2 changes to 2 files (+1 heads)
473 $ hg status
475 $ hg status
474 M z
476 M z
475
477
476 $ cd ..
478 $ cd ..
477
479
478 shelve should only unshelve pending changes (issue 4068)
480 shelve should only unshelve pending changes (issue 4068)
479
481
480 $ hg init onlypendingchanges
482 $ hg init onlypendingchanges
481 $ cd onlypendingchanges
483 $ cd onlypendingchanges
482 $ touch a
484 $ touch a
483 $ hg ci -Aqm a
485 $ hg ci -Aqm a
484 $ touch b
486 $ touch b
485 $ hg ci -Aqm b
487 $ hg ci -Aqm b
486 $ hg up -q 0
488 $ hg up -q 0
487 $ touch c
489 $ touch c
488 $ hg ci -Aqm c
490 $ hg ci -Aqm c
489
491
490 $ touch d
492 $ touch d
491 $ hg add d
493 $ hg add d
492 $ hg shelve
494 $ hg shelve
493 shelved as default
495 shelved as default
494 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
496 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
495 $ hg up -q 1
497 $ hg up -q 1
496 $ hg unshelve
498 $ hg unshelve
497 unshelving change 'default'
499 unshelving change 'default'
498 adding changesets
500 adding changesets
499 adding manifests
501 adding manifests
500 adding file changes
502 adding file changes
501 added 1 changesets with 1 changes to 3 files
503 added 1 changesets with 1 changes to 3 files
502 $ hg status
504 $ hg status
503 A d
505 A d
504
506
505 unshelve should work on an ancestor of the original commit
507 unshelve should work on an ancestor of the original commit
506
508
507 $ hg shelve
509 $ hg shelve
508 shelved as default
510 shelved as default
509 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
511 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
510 $ hg up 0
512 $ hg up 0
511 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
513 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
512 $ hg unshelve
514 $ hg unshelve
513 unshelving change 'default'
515 unshelving change 'default'
514 adding changesets
516 adding changesets
515 adding manifests
517 adding manifests
516 adding file changes
518 adding file changes
517 added 1 changesets with 1 changes to 3 files
519 added 1 changesets with 1 changes to 3 files
518 $ hg status
520 $ hg status
519 A d
521 A d
520
522
521 $ cd ..
523 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now