##// END OF EJS Templates
patch: separate reverse copy data (issue1959)
Wagner Bruna -
r10466:d1f209bb stable
parent child Browse files
Show More
@@ -1,1658 +1,1663 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from i18n import _
9 from i18n import _
10 from node import hex, nullid, short
10 from node import hex, nullid, short
11 import base85, cmdutil, mdiff, util, diffhelpers, copies
11 import base85, cmdutil, mdiff, util, diffhelpers, copies
12 import cStringIO, email.Parser, os, re
12 import cStringIO, email.Parser, os, re
13 import sys, tempfile, zlib
13 import sys, tempfile, zlib
14
14
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
16
16
17 class PatchError(Exception):
17 class PatchError(Exception):
18 pass
18 pass
19
19
20 class NoHunks(PatchError):
20 class NoHunks(PatchError):
21 pass
21 pass
22
22
23 # helper functions
23 # helper functions
24
24
25 def copyfile(src, dst, basedir):
25 def copyfile(src, dst, basedir):
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
27 if os.path.exists(absdst):
27 if os.path.exists(absdst):
28 raise util.Abort(_("cannot create %s: destination already exists") %
28 raise util.Abort(_("cannot create %s: destination already exists") %
29 dst)
29 dst)
30
30
31 dstdir = os.path.dirname(absdst)
31 dstdir = os.path.dirname(absdst)
32 if dstdir and not os.path.isdir(dstdir):
32 if dstdir and not os.path.isdir(dstdir):
33 try:
33 try:
34 os.makedirs(dstdir)
34 os.makedirs(dstdir)
35 except IOError:
35 except IOError:
36 raise util.Abort(
36 raise util.Abort(
37 _("cannot create %s: unable to create destination directory")
37 _("cannot create %s: unable to create destination directory")
38 % dst)
38 % dst)
39
39
40 util.copyfile(abssrc, absdst)
40 util.copyfile(abssrc, absdst)
41
41
42 # public functions
42 # public functions
43
43
44 def split(stream):
44 def split(stream):
45 '''return an iterator of individual patches from a stream'''
45 '''return an iterator of individual patches from a stream'''
46 def isheader(line, inheader):
46 def isheader(line, inheader):
47 if inheader and line[0] in (' ', '\t'):
47 if inheader and line[0] in (' ', '\t'):
48 # continuation
48 # continuation
49 return True
49 return True
50 l = line.split(': ', 1)
50 l = line.split(': ', 1)
51 return len(l) == 2 and ' ' not in l[0]
51 return len(l) == 2 and ' ' not in l[0]
52
52
53 def chunk(lines):
53 def chunk(lines):
54 return cStringIO.StringIO(''.join(lines))
54 return cStringIO.StringIO(''.join(lines))
55
55
56 def hgsplit(stream, cur):
56 def hgsplit(stream, cur):
57 inheader = True
57 inheader = True
58
58
59 for line in stream:
59 for line in stream:
60 if not line.strip():
60 if not line.strip():
61 inheader = False
61 inheader = False
62 if not inheader and line.startswith('# HG changeset patch'):
62 if not inheader and line.startswith('# HG changeset patch'):
63 yield chunk(cur)
63 yield chunk(cur)
64 cur = []
64 cur = []
65 inheader = True
65 inheader = True
66
66
67 cur.append(line)
67 cur.append(line)
68
68
69 if cur:
69 if cur:
70 yield chunk(cur)
70 yield chunk(cur)
71
71
72 def mboxsplit(stream, cur):
72 def mboxsplit(stream, cur):
73 for line in stream:
73 for line in stream:
74 if line.startswith('From '):
74 if line.startswith('From '):
75 for c in split(chunk(cur[1:])):
75 for c in split(chunk(cur[1:])):
76 yield c
76 yield c
77 cur = []
77 cur = []
78
78
79 cur.append(line)
79 cur.append(line)
80
80
81 if cur:
81 if cur:
82 for c in split(chunk(cur[1:])):
82 for c in split(chunk(cur[1:])):
83 yield c
83 yield c
84
84
85 def mimesplit(stream, cur):
85 def mimesplit(stream, cur):
86 def msgfp(m):
86 def msgfp(m):
87 fp = cStringIO.StringIO()
87 fp = cStringIO.StringIO()
88 g = email.Generator.Generator(fp, mangle_from_=False)
88 g = email.Generator.Generator(fp, mangle_from_=False)
89 g.flatten(m)
89 g.flatten(m)
90 fp.seek(0)
90 fp.seek(0)
91 return fp
91 return fp
92
92
93 for line in stream:
93 for line in stream:
94 cur.append(line)
94 cur.append(line)
95 c = chunk(cur)
95 c = chunk(cur)
96
96
97 m = email.Parser.Parser().parse(c)
97 m = email.Parser.Parser().parse(c)
98 if not m.is_multipart():
98 if not m.is_multipart():
99 yield msgfp(m)
99 yield msgfp(m)
100 else:
100 else:
101 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
101 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
102 for part in m.walk():
102 for part in m.walk():
103 ct = part.get_content_type()
103 ct = part.get_content_type()
104 if ct not in ok_types:
104 if ct not in ok_types:
105 continue
105 continue
106 yield msgfp(part)
106 yield msgfp(part)
107
107
108 def headersplit(stream, cur):
108 def headersplit(stream, cur):
109 inheader = False
109 inheader = False
110
110
111 for line in stream:
111 for line in stream:
112 if not inheader and isheader(line, inheader):
112 if not inheader and isheader(line, inheader):
113 yield chunk(cur)
113 yield chunk(cur)
114 cur = []
114 cur = []
115 inheader = True
115 inheader = True
116 if inheader and not isheader(line, inheader):
116 if inheader and not isheader(line, inheader):
117 inheader = False
117 inheader = False
118
118
119 cur.append(line)
119 cur.append(line)
120
120
121 if cur:
121 if cur:
122 yield chunk(cur)
122 yield chunk(cur)
123
123
124 def remainder(cur):
124 def remainder(cur):
125 yield chunk(cur)
125 yield chunk(cur)
126
126
127 class fiter(object):
127 class fiter(object):
128 def __init__(self, fp):
128 def __init__(self, fp):
129 self.fp = fp
129 self.fp = fp
130
130
131 def __iter__(self):
131 def __iter__(self):
132 return self
132 return self
133
133
134 def next(self):
134 def next(self):
135 l = self.fp.readline()
135 l = self.fp.readline()
136 if not l:
136 if not l:
137 raise StopIteration
137 raise StopIteration
138 return l
138 return l
139
139
140 inheader = False
140 inheader = False
141 cur = []
141 cur = []
142
142
143 mimeheaders = ['content-type']
143 mimeheaders = ['content-type']
144
144
145 if not hasattr(stream, 'next'):
145 if not hasattr(stream, 'next'):
146 # http responses, for example, have readline but not next
146 # http responses, for example, have readline but not next
147 stream = fiter(stream)
147 stream = fiter(stream)
148
148
149 for line in stream:
149 for line in stream:
150 cur.append(line)
150 cur.append(line)
151 if line.startswith('# HG changeset patch'):
151 if line.startswith('# HG changeset patch'):
152 return hgsplit(stream, cur)
152 return hgsplit(stream, cur)
153 elif line.startswith('From '):
153 elif line.startswith('From '):
154 return mboxsplit(stream, cur)
154 return mboxsplit(stream, cur)
155 elif isheader(line, inheader):
155 elif isheader(line, inheader):
156 inheader = True
156 inheader = True
157 if line.split(':', 1)[0].lower() in mimeheaders:
157 if line.split(':', 1)[0].lower() in mimeheaders:
158 # let email parser handle this
158 # let email parser handle this
159 return mimesplit(stream, cur)
159 return mimesplit(stream, cur)
160 elif inheader:
160 elif inheader:
161 # No evil headers seen, split by hand
161 # No evil headers seen, split by hand
162 return headersplit(stream, cur)
162 return headersplit(stream, cur)
163 # Not enough info, keep reading
163 # Not enough info, keep reading
164
164
165 # if we are here, we have a very plain patch
165 # if we are here, we have a very plain patch
166 return remainder(cur)
166 return remainder(cur)
167
167
168 def extract(ui, fileobj):
168 def extract(ui, fileobj):
169 '''extract patch from data read from fileobj.
169 '''extract patch from data read from fileobj.
170
170
171 patch can be a normal patch or contained in an email message.
171 patch can be a normal patch or contained in an email message.
172
172
173 return tuple (filename, message, user, date, node, p1, p2).
173 return tuple (filename, message, user, date, node, p1, p2).
174 Any item in the returned tuple can be None. If filename is None,
174 Any item in the returned tuple can be None. If filename is None,
175 fileobj did not contain a patch. Caller must unlink filename when done.'''
175 fileobj did not contain a patch. Caller must unlink filename when done.'''
176
176
177 # attempt to detect the start of a patch
177 # attempt to detect the start of a patch
178 # (this heuristic is borrowed from quilt)
178 # (this heuristic is borrowed from quilt)
179 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
179 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
180 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
180 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
181 r'(---|\*\*\*)[ \t])', re.MULTILINE)
181 r'(---|\*\*\*)[ \t])', re.MULTILINE)
182
182
183 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
183 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
184 tmpfp = os.fdopen(fd, 'w')
184 tmpfp = os.fdopen(fd, 'w')
185 try:
185 try:
186 msg = email.Parser.Parser().parse(fileobj)
186 msg = email.Parser.Parser().parse(fileobj)
187
187
188 subject = msg['Subject']
188 subject = msg['Subject']
189 user = msg['From']
189 user = msg['From']
190 if not subject and not user:
190 if not subject and not user:
191 # Not an email, restore parsed headers if any
191 # Not an email, restore parsed headers if any
192 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
192 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
193
193
194 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
194 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
195 # should try to parse msg['Date']
195 # should try to parse msg['Date']
196 date = None
196 date = None
197 nodeid = None
197 nodeid = None
198 branch = None
198 branch = None
199 parents = []
199 parents = []
200
200
201 if subject:
201 if subject:
202 if subject.startswith('[PATCH'):
202 if subject.startswith('[PATCH'):
203 pend = subject.find(']')
203 pend = subject.find(']')
204 if pend >= 0:
204 if pend >= 0:
205 subject = subject[pend + 1:].lstrip()
205 subject = subject[pend + 1:].lstrip()
206 subject = subject.replace('\n\t', ' ')
206 subject = subject.replace('\n\t', ' ')
207 ui.debug('Subject: %s\n' % subject)
207 ui.debug('Subject: %s\n' % subject)
208 if user:
208 if user:
209 ui.debug('From: %s\n' % user)
209 ui.debug('From: %s\n' % user)
210 diffs_seen = 0
210 diffs_seen = 0
211 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
211 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
212 message = ''
212 message = ''
213 for part in msg.walk():
213 for part in msg.walk():
214 content_type = part.get_content_type()
214 content_type = part.get_content_type()
215 ui.debug('Content-Type: %s\n' % content_type)
215 ui.debug('Content-Type: %s\n' % content_type)
216 if content_type not in ok_types:
216 if content_type not in ok_types:
217 continue
217 continue
218 payload = part.get_payload(decode=True)
218 payload = part.get_payload(decode=True)
219 m = diffre.search(payload)
219 m = diffre.search(payload)
220 if m:
220 if m:
221 hgpatch = False
221 hgpatch = False
222 ignoretext = False
222 ignoretext = False
223
223
224 ui.debug('found patch at byte %d\n' % m.start(0))
224 ui.debug('found patch at byte %d\n' % m.start(0))
225 diffs_seen += 1
225 diffs_seen += 1
226 cfp = cStringIO.StringIO()
226 cfp = cStringIO.StringIO()
227 for line in payload[:m.start(0)].splitlines():
227 for line in payload[:m.start(0)].splitlines():
228 if line.startswith('# HG changeset patch'):
228 if line.startswith('# HG changeset patch'):
229 ui.debug('patch generated by hg export\n')
229 ui.debug('patch generated by hg export\n')
230 hgpatch = True
230 hgpatch = True
231 # drop earlier commit message content
231 # drop earlier commit message content
232 cfp.seek(0)
232 cfp.seek(0)
233 cfp.truncate()
233 cfp.truncate()
234 subject = None
234 subject = None
235 elif hgpatch:
235 elif hgpatch:
236 if line.startswith('# User '):
236 if line.startswith('# User '):
237 user = line[7:]
237 user = line[7:]
238 ui.debug('From: %s\n' % user)
238 ui.debug('From: %s\n' % user)
239 elif line.startswith("# Date "):
239 elif line.startswith("# Date "):
240 date = line[7:]
240 date = line[7:]
241 elif line.startswith("# Branch "):
241 elif line.startswith("# Branch "):
242 branch = line[9:]
242 branch = line[9:]
243 elif line.startswith("# Node ID "):
243 elif line.startswith("# Node ID "):
244 nodeid = line[10:]
244 nodeid = line[10:]
245 elif line.startswith("# Parent "):
245 elif line.startswith("# Parent "):
246 parents.append(line[10:])
246 parents.append(line[10:])
247 elif line == '---' and gitsendmail:
247 elif line == '---' and gitsendmail:
248 ignoretext = True
248 ignoretext = True
249 if not line.startswith('# ') and not ignoretext:
249 if not line.startswith('# ') and not ignoretext:
250 cfp.write(line)
250 cfp.write(line)
251 cfp.write('\n')
251 cfp.write('\n')
252 message = cfp.getvalue()
252 message = cfp.getvalue()
253 if tmpfp:
253 if tmpfp:
254 tmpfp.write(payload)
254 tmpfp.write(payload)
255 if not payload.endswith('\n'):
255 if not payload.endswith('\n'):
256 tmpfp.write('\n')
256 tmpfp.write('\n')
257 elif not diffs_seen and message and content_type == 'text/plain':
257 elif not diffs_seen and message and content_type == 'text/plain':
258 message += '\n' + payload
258 message += '\n' + payload
259 except:
259 except:
260 tmpfp.close()
260 tmpfp.close()
261 os.unlink(tmpname)
261 os.unlink(tmpname)
262 raise
262 raise
263
263
264 if subject and not message.startswith(subject):
264 if subject and not message.startswith(subject):
265 message = '%s\n%s' % (subject, message)
265 message = '%s\n%s' % (subject, message)
266 tmpfp.close()
266 tmpfp.close()
267 if not diffs_seen:
267 if not diffs_seen:
268 os.unlink(tmpname)
268 os.unlink(tmpname)
269 return None, message, user, date, branch, None, None, None
269 return None, message, user, date, branch, None, None, None
270 p1 = parents and parents.pop(0) or None
270 p1 = parents and parents.pop(0) or None
271 p2 = parents and parents.pop(0) or None
271 p2 = parents and parents.pop(0) or None
272 return tmpname, message, user, date, branch, nodeid, p1, p2
272 return tmpname, message, user, date, branch, nodeid, p1, p2
273
273
274 GP_PATCH = 1 << 0 # we have to run patch
274 GP_PATCH = 1 << 0 # we have to run patch
275 GP_FILTER = 1 << 1 # there's some copy/rename operation
275 GP_FILTER = 1 << 1 # there's some copy/rename operation
276 GP_BINARY = 1 << 2 # there's a binary patch
276 GP_BINARY = 1 << 2 # there's a binary patch
277
277
278 class patchmeta(object):
278 class patchmeta(object):
279 """Patched file metadata
279 """Patched file metadata
280
280
281 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
282 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 or COPY. 'path' is patched file path. 'oldpath' is set to the
283 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 origin file when 'op' is either COPY or RENAME, None otherwise. If
284 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 file mode is changed, 'mode' is a tuple (islink, isexec) where
285 'islink' is True if the file is a symlink and 'isexec' is True if
285 'islink' is True if the file is a symlink and 'isexec' is True if
286 the file is executable. Otherwise, 'mode' is None.
286 the file is executable. Otherwise, 'mode' is None.
287 """
287 """
288 def __init__(self, path):
288 def __init__(self, path):
289 self.path = path
289 self.path = path
290 self.oldpath = None
290 self.oldpath = None
291 self.mode = None
291 self.mode = None
292 self.op = 'MODIFY'
292 self.op = 'MODIFY'
293 self.lineno = 0
293 self.lineno = 0
294 self.binary = False
294 self.binary = False
295
295
296 def setmode(self, mode):
296 def setmode(self, mode):
297 islink = mode & 020000
297 islink = mode & 020000
298 isexec = mode & 0100
298 isexec = mode & 0100
299 self.mode = (islink, isexec)
299 self.mode = (islink, isexec)
300
300
301 def readgitpatch(lr):
301 def readgitpatch(lr):
302 """extract git-style metadata about patches from <patchname>"""
302 """extract git-style metadata about patches from <patchname>"""
303
303
304 # Filter patch for git information
304 # Filter patch for git information
305 gp = None
305 gp = None
306 gitpatches = []
306 gitpatches = []
307 # Can have a git patch with only metadata, causing patch to complain
307 # Can have a git patch with only metadata, causing patch to complain
308 dopatch = 0
308 dopatch = 0
309
309
310 lineno = 0
310 lineno = 0
311 for line in lr:
311 for line in lr:
312 lineno += 1
312 lineno += 1
313 line = line.rstrip(' \r\n')
313 line = line.rstrip(' \r\n')
314 if line.startswith('diff --git'):
314 if line.startswith('diff --git'):
315 m = gitre.match(line)
315 m = gitre.match(line)
316 if m:
316 if m:
317 if gp:
317 if gp:
318 gitpatches.append(gp)
318 gitpatches.append(gp)
319 dst = m.group(2)
319 dst = m.group(2)
320 gp = patchmeta(dst)
320 gp = patchmeta(dst)
321 gp.lineno = lineno
321 gp.lineno = lineno
322 elif gp:
322 elif gp:
323 if line.startswith('--- '):
323 if line.startswith('--- '):
324 if gp.op in ('COPY', 'RENAME'):
324 if gp.op in ('COPY', 'RENAME'):
325 dopatch |= GP_FILTER
325 dopatch |= GP_FILTER
326 gitpatches.append(gp)
326 gitpatches.append(gp)
327 gp = None
327 gp = None
328 dopatch |= GP_PATCH
328 dopatch |= GP_PATCH
329 continue
329 continue
330 if line.startswith('rename from '):
330 if line.startswith('rename from '):
331 gp.op = 'RENAME'
331 gp.op = 'RENAME'
332 gp.oldpath = line[12:]
332 gp.oldpath = line[12:]
333 elif line.startswith('rename to '):
333 elif line.startswith('rename to '):
334 gp.path = line[10:]
334 gp.path = line[10:]
335 elif line.startswith('copy from '):
335 elif line.startswith('copy from '):
336 gp.op = 'COPY'
336 gp.op = 'COPY'
337 gp.oldpath = line[10:]
337 gp.oldpath = line[10:]
338 elif line.startswith('copy to '):
338 elif line.startswith('copy to '):
339 gp.path = line[8:]
339 gp.path = line[8:]
340 elif line.startswith('deleted file'):
340 elif line.startswith('deleted file'):
341 gp.op = 'DELETE'
341 gp.op = 'DELETE'
342 # is the deleted file a symlink?
342 # is the deleted file a symlink?
343 gp.setmode(int(line[-6:], 8))
343 gp.setmode(int(line[-6:], 8))
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 dopatch |= GP_BINARY
350 dopatch |= GP_BINARY
351 gp.binary = True
351 gp.binary = True
352 if gp:
352 if gp:
353 gitpatches.append(gp)
353 gitpatches.append(gp)
354
354
355 if not gitpatches:
355 if not gitpatches:
356 dopatch = GP_PATCH
356 dopatch = GP_PATCH
357
357
358 return (dopatch, gitpatches)
358 return (dopatch, gitpatches)
359
359
360 class linereader(object):
360 class linereader(object):
361 # simple class to allow pushing lines back into the input stream
361 # simple class to allow pushing lines back into the input stream
362 def __init__(self, fp, textmode=False):
362 def __init__(self, fp, textmode=False):
363 self.fp = fp
363 self.fp = fp
364 self.buf = []
364 self.buf = []
365 self.textmode = textmode
365 self.textmode = textmode
366 self.eol = None
366 self.eol = None
367
367
368 def push(self, line):
368 def push(self, line):
369 if line is not None:
369 if line is not None:
370 self.buf.append(line)
370 self.buf.append(line)
371
371
372 def readline(self):
372 def readline(self):
373 if self.buf:
373 if self.buf:
374 l = self.buf[0]
374 l = self.buf[0]
375 del self.buf[0]
375 del self.buf[0]
376 return l
376 return l
377 l = self.fp.readline()
377 l = self.fp.readline()
378 if not self.eol:
378 if not self.eol:
379 if l.endswith('\r\n'):
379 if l.endswith('\r\n'):
380 self.eol = '\r\n'
380 self.eol = '\r\n'
381 elif l.endswith('\n'):
381 elif l.endswith('\n'):
382 self.eol = '\n'
382 self.eol = '\n'
383 if self.textmode and l.endswith('\r\n'):
383 if self.textmode and l.endswith('\r\n'):
384 l = l[:-2] + '\n'
384 l = l[:-2] + '\n'
385 return l
385 return l
386
386
387 def __iter__(self):
387 def __iter__(self):
388 while 1:
388 while 1:
389 l = self.readline()
389 l = self.readline()
390 if not l:
390 if not l:
391 break
391 break
392 yield l
392 yield l
393
393
394 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
394 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
395 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
395 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
396 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
396 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
397 eolmodes = ['strict', 'crlf', 'lf', 'auto']
397 eolmodes = ['strict', 'crlf', 'lf', 'auto']
398
398
399 class patchfile(object):
399 class patchfile(object):
400 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
400 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
401 self.fname = fname
401 self.fname = fname
402 self.eolmode = eolmode
402 self.eolmode = eolmode
403 self.eol = None
403 self.eol = None
404 self.opener = opener
404 self.opener = opener
405 self.ui = ui
405 self.ui = ui
406 self.lines = []
406 self.lines = []
407 self.exists = False
407 self.exists = False
408 self.missing = missing
408 self.missing = missing
409 if not missing:
409 if not missing:
410 try:
410 try:
411 self.lines = self.readlines(fname)
411 self.lines = self.readlines(fname)
412 self.exists = True
412 self.exists = True
413 except IOError:
413 except IOError:
414 pass
414 pass
415 else:
415 else:
416 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
416 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
417
417
418 self.hash = {}
418 self.hash = {}
419 self.dirty = 0
419 self.dirty = 0
420 self.offset = 0
420 self.offset = 0
421 self.skew = 0
421 self.skew = 0
422 self.rej = []
422 self.rej = []
423 self.fileprinted = False
423 self.fileprinted = False
424 self.printfile(False)
424 self.printfile(False)
425 self.hunks = 0
425 self.hunks = 0
426
426
427 def readlines(self, fname):
427 def readlines(self, fname):
428 if os.path.islink(fname):
428 if os.path.islink(fname):
429 return [os.readlink(fname)]
429 return [os.readlink(fname)]
430 fp = self.opener(fname, 'r')
430 fp = self.opener(fname, 'r')
431 try:
431 try:
432 lr = linereader(fp, self.eolmode != 'strict')
432 lr = linereader(fp, self.eolmode != 'strict')
433 lines = list(lr)
433 lines = list(lr)
434 self.eol = lr.eol
434 self.eol = lr.eol
435 return lines
435 return lines
436 finally:
436 finally:
437 fp.close()
437 fp.close()
438
438
439 def writelines(self, fname, lines):
439 def writelines(self, fname, lines):
440 # Ensure supplied data ends in fname, being a regular file or
440 # Ensure supplied data ends in fname, being a regular file or
441 # a symlink. updatedir() will -too magically- take care of
441 # a symlink. updatedir() will -too magically- take care of
442 # setting it to the proper type afterwards.
442 # setting it to the proper type afterwards.
443 islink = os.path.islink(fname)
443 islink = os.path.islink(fname)
444 if islink:
444 if islink:
445 fp = cStringIO.StringIO()
445 fp = cStringIO.StringIO()
446 else:
446 else:
447 fp = self.opener(fname, 'w')
447 fp = self.opener(fname, 'w')
448 try:
448 try:
449 if self.eolmode == 'auto':
449 if self.eolmode == 'auto':
450 eol = self.eol
450 eol = self.eol
451 elif self.eolmode == 'crlf':
451 elif self.eolmode == 'crlf':
452 eol = '\r\n'
452 eol = '\r\n'
453 else:
453 else:
454 eol = '\n'
454 eol = '\n'
455
455
456 if self.eolmode != 'strict' and eol and eol != '\n':
456 if self.eolmode != 'strict' and eol and eol != '\n':
457 for l in lines:
457 for l in lines:
458 if l and l[-1] == '\n':
458 if l and l[-1] == '\n':
459 l = l[:-1] + eol
459 l = l[:-1] + eol
460 fp.write(l)
460 fp.write(l)
461 else:
461 else:
462 fp.writelines(lines)
462 fp.writelines(lines)
463 if islink:
463 if islink:
464 self.opener.symlink(fp.getvalue(), fname)
464 self.opener.symlink(fp.getvalue(), fname)
465 finally:
465 finally:
466 fp.close()
466 fp.close()
467
467
468 def unlink(self, fname):
468 def unlink(self, fname):
469 os.unlink(fname)
469 os.unlink(fname)
470
470
471 def printfile(self, warn):
471 def printfile(self, warn):
472 if self.fileprinted:
472 if self.fileprinted:
473 return
473 return
474 if warn or self.ui.verbose:
474 if warn or self.ui.verbose:
475 self.fileprinted = True
475 self.fileprinted = True
476 s = _("patching file %s\n") % self.fname
476 s = _("patching file %s\n") % self.fname
477 if warn:
477 if warn:
478 self.ui.warn(s)
478 self.ui.warn(s)
479 else:
479 else:
480 self.ui.note(s)
480 self.ui.note(s)
481
481
482
482
483 def findlines(self, l, linenum):
483 def findlines(self, l, linenum):
484 # looks through the hash and finds candidate lines. The
484 # looks through the hash and finds candidate lines. The
485 # result is a list of line numbers sorted based on distance
485 # result is a list of line numbers sorted based on distance
486 # from linenum
486 # from linenum
487
487
488 cand = self.hash.get(l, [])
488 cand = self.hash.get(l, [])
489 if len(cand) > 1:
489 if len(cand) > 1:
490 # resort our list of potentials forward then back.
490 # resort our list of potentials forward then back.
491 cand.sort(key=lambda x: abs(x - linenum))
491 cand.sort(key=lambda x: abs(x - linenum))
492 return cand
492 return cand
493
493
494 def hashlines(self):
494 def hashlines(self):
495 self.hash = {}
495 self.hash = {}
496 for x, s in enumerate(self.lines):
496 for x, s in enumerate(self.lines):
497 self.hash.setdefault(s, []).append(x)
497 self.hash.setdefault(s, []).append(x)
498
498
499 def write_rej(self):
499 def write_rej(self):
500 # our rejects are a little different from patch(1). This always
500 # our rejects are a little different from patch(1). This always
501 # creates rejects in the same form as the original patch. A file
501 # creates rejects in the same form as the original patch. A file
502 # header is inserted so that you can run the reject through patch again
502 # header is inserted so that you can run the reject through patch again
503 # without having to type the filename.
503 # without having to type the filename.
504
504
505 if not self.rej:
505 if not self.rej:
506 return
506 return
507
507
508 fname = self.fname + ".rej"
508 fname = self.fname + ".rej"
509 self.ui.warn(
509 self.ui.warn(
510 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
510 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
511 (len(self.rej), self.hunks, fname))
511 (len(self.rej), self.hunks, fname))
512
512
513 def rejlines():
513 def rejlines():
514 base = os.path.basename(self.fname)
514 base = os.path.basename(self.fname)
515 yield "--- %s\n+++ %s\n" % (base, base)
515 yield "--- %s\n+++ %s\n" % (base, base)
516 for x in self.rej:
516 for x in self.rej:
517 for l in x.hunk:
517 for l in x.hunk:
518 yield l
518 yield l
519 if l[-1] != '\n':
519 if l[-1] != '\n':
520 yield "\n\ No newline at end of file\n"
520 yield "\n\ No newline at end of file\n"
521
521
522 self.writelines(fname, rejlines())
522 self.writelines(fname, rejlines())
523
523
524 def write(self, dest=None):
524 def write(self, dest=None):
525 if not self.dirty:
525 if not self.dirty:
526 return
526 return
527 if not dest:
527 if not dest:
528 dest = self.fname
528 dest = self.fname
529 self.writelines(dest, self.lines)
529 self.writelines(dest, self.lines)
530
530
531 def close(self):
531 def close(self):
532 self.write()
532 self.write()
533 self.write_rej()
533 self.write_rej()
534
534
535 def apply(self, h):
535 def apply(self, h):
536 if not h.complete():
536 if not h.complete():
537 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
537 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
538 (h.number, h.desc, len(h.a), h.lena, len(h.b),
538 (h.number, h.desc, len(h.a), h.lena, len(h.b),
539 h.lenb))
539 h.lenb))
540
540
541 self.hunks += 1
541 self.hunks += 1
542
542
543 if self.missing:
543 if self.missing:
544 self.rej.append(h)
544 self.rej.append(h)
545 return -1
545 return -1
546
546
547 if self.exists and h.createfile():
547 if self.exists and h.createfile():
548 self.ui.warn(_("file %s already exists\n") % self.fname)
548 self.ui.warn(_("file %s already exists\n") % self.fname)
549 self.rej.append(h)
549 self.rej.append(h)
550 return -1
550 return -1
551
551
552 if isinstance(h, binhunk):
552 if isinstance(h, binhunk):
553 if h.rmfile():
553 if h.rmfile():
554 self.unlink(self.fname)
554 self.unlink(self.fname)
555 else:
555 else:
556 self.lines[:] = h.new()
556 self.lines[:] = h.new()
557 self.offset += len(h.new())
557 self.offset += len(h.new())
558 self.dirty = 1
558 self.dirty = 1
559 return 0
559 return 0
560
560
561 horig = h
561 horig = h
562 if (self.eolmode in ('crlf', 'lf')
562 if (self.eolmode in ('crlf', 'lf')
563 or self.eolmode == 'auto' and self.eol):
563 or self.eolmode == 'auto' and self.eol):
564 # If new eols are going to be normalized, then normalize
564 # If new eols are going to be normalized, then normalize
565 # hunk data before patching. Otherwise, preserve input
565 # hunk data before patching. Otherwise, preserve input
566 # line-endings.
566 # line-endings.
567 h = h.getnormalized()
567 h = h.getnormalized()
568
568
569 # fast case first, no offsets, no fuzz
569 # fast case first, no offsets, no fuzz
570 old = h.old()
570 old = h.old()
571 # patch starts counting at 1 unless we are adding the file
571 # patch starts counting at 1 unless we are adding the file
572 if h.starta == 0:
572 if h.starta == 0:
573 start = 0
573 start = 0
574 else:
574 else:
575 start = h.starta + self.offset - 1
575 start = h.starta + self.offset - 1
576 orig_start = start
576 orig_start = start
577 # if there's skew we want to emit the "(offset %d lines)" even
577 # if there's skew we want to emit the "(offset %d lines)" even
578 # when the hunk cleanly applies at start + skew, so skip the
578 # when the hunk cleanly applies at start + skew, so skip the
579 # fast case code
579 # fast case code
580 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
580 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
581 if h.rmfile():
581 if h.rmfile():
582 self.unlink(self.fname)
582 self.unlink(self.fname)
583 else:
583 else:
584 self.lines[start : start + h.lena] = h.new()
584 self.lines[start : start + h.lena] = h.new()
585 self.offset += h.lenb - h.lena
585 self.offset += h.lenb - h.lena
586 self.dirty = 1
586 self.dirty = 1
587 return 0
587 return 0
588
588
589 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
589 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
590 self.hashlines()
590 self.hashlines()
591 if h.hunk[-1][0] != ' ':
591 if h.hunk[-1][0] != ' ':
592 # if the hunk tried to put something at the bottom of the file
592 # if the hunk tried to put something at the bottom of the file
593 # override the start line and use eof here
593 # override the start line and use eof here
594 search_start = len(self.lines)
594 search_start = len(self.lines)
595 else:
595 else:
596 search_start = orig_start + self.skew
596 search_start = orig_start + self.skew
597
597
598 for fuzzlen in xrange(3):
598 for fuzzlen in xrange(3):
599 for toponly in [True, False]:
599 for toponly in [True, False]:
600 old = h.old(fuzzlen, toponly)
600 old = h.old(fuzzlen, toponly)
601
601
602 cand = self.findlines(old[0][1:], search_start)
602 cand = self.findlines(old[0][1:], search_start)
603 for l in cand:
603 for l in cand:
604 if diffhelpers.testhunk(old, self.lines, l) == 0:
604 if diffhelpers.testhunk(old, self.lines, l) == 0:
605 newlines = h.new(fuzzlen, toponly)
605 newlines = h.new(fuzzlen, toponly)
606 self.lines[l : l + len(old)] = newlines
606 self.lines[l : l + len(old)] = newlines
607 self.offset += len(newlines) - len(old)
607 self.offset += len(newlines) - len(old)
608 self.skew = l - orig_start
608 self.skew = l - orig_start
609 self.dirty = 1
609 self.dirty = 1
610 if fuzzlen:
610 if fuzzlen:
611 fuzzstr = "with fuzz %d " % fuzzlen
611 fuzzstr = "with fuzz %d " % fuzzlen
612 f = self.ui.warn
612 f = self.ui.warn
613 self.printfile(True)
613 self.printfile(True)
614 else:
614 else:
615 fuzzstr = ""
615 fuzzstr = ""
616 f = self.ui.note
616 f = self.ui.note
617 offset = l - orig_start - fuzzlen
617 offset = l - orig_start - fuzzlen
618 if offset == 1:
618 if offset == 1:
619 msg = _("Hunk #%d succeeded at %d %s"
619 msg = _("Hunk #%d succeeded at %d %s"
620 "(offset %d line).\n")
620 "(offset %d line).\n")
621 else:
621 else:
622 msg = _("Hunk #%d succeeded at %d %s"
622 msg = _("Hunk #%d succeeded at %d %s"
623 "(offset %d lines).\n")
623 "(offset %d lines).\n")
624 f(msg % (h.number, l + 1, fuzzstr, offset))
624 f(msg % (h.number, l + 1, fuzzstr, offset))
625 return fuzzlen
625 return fuzzlen
626 self.printfile(True)
626 self.printfile(True)
627 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
627 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
628 self.rej.append(horig)
628 self.rej.append(horig)
629 return -1
629 return -1
630
630
631 class hunk(object):
631 class hunk(object):
632 def __init__(self, desc, num, lr, context, create=False, remove=False):
632 def __init__(self, desc, num, lr, context, create=False, remove=False):
633 self.number = num
633 self.number = num
634 self.desc = desc
634 self.desc = desc
635 self.hunk = [desc]
635 self.hunk = [desc]
636 self.a = []
636 self.a = []
637 self.b = []
637 self.b = []
638 self.starta = self.lena = None
638 self.starta = self.lena = None
639 self.startb = self.lenb = None
639 self.startb = self.lenb = None
640 if lr is not None:
640 if lr is not None:
641 if context:
641 if context:
642 self.read_context_hunk(lr)
642 self.read_context_hunk(lr)
643 else:
643 else:
644 self.read_unified_hunk(lr)
644 self.read_unified_hunk(lr)
645 self.create = create
645 self.create = create
646 self.remove = remove and not create
646 self.remove = remove and not create
647
647
648 def getnormalized(self):
648 def getnormalized(self):
649 """Return a copy with line endings normalized to LF."""
649 """Return a copy with line endings normalized to LF."""
650
650
651 def normalize(lines):
651 def normalize(lines):
652 nlines = []
652 nlines = []
653 for line in lines:
653 for line in lines:
654 if line.endswith('\r\n'):
654 if line.endswith('\r\n'):
655 line = line[:-2] + '\n'
655 line = line[:-2] + '\n'
656 nlines.append(line)
656 nlines.append(line)
657 return nlines
657 return nlines
658
658
659 # Dummy object, it is rebuilt manually
659 # Dummy object, it is rebuilt manually
660 nh = hunk(self.desc, self.number, None, None, False, False)
660 nh = hunk(self.desc, self.number, None, None, False, False)
661 nh.number = self.number
661 nh.number = self.number
662 nh.desc = self.desc
662 nh.desc = self.desc
663 nh.a = normalize(self.a)
663 nh.a = normalize(self.a)
664 nh.b = normalize(self.b)
664 nh.b = normalize(self.b)
665 nh.starta = self.starta
665 nh.starta = self.starta
666 nh.startb = self.startb
666 nh.startb = self.startb
667 nh.lena = self.lena
667 nh.lena = self.lena
668 nh.lenb = self.lenb
668 nh.lenb = self.lenb
669 nh.create = self.create
669 nh.create = self.create
670 nh.remove = self.remove
670 nh.remove = self.remove
671 return nh
671 return nh
672
672
673 def read_unified_hunk(self, lr):
673 def read_unified_hunk(self, lr):
674 m = unidesc.match(self.desc)
674 m = unidesc.match(self.desc)
675 if not m:
675 if not m:
676 raise PatchError(_("bad hunk #%d") % self.number)
676 raise PatchError(_("bad hunk #%d") % self.number)
677 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
677 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
678 if self.lena is None:
678 if self.lena is None:
679 self.lena = 1
679 self.lena = 1
680 else:
680 else:
681 self.lena = int(self.lena)
681 self.lena = int(self.lena)
682 if self.lenb is None:
682 if self.lenb is None:
683 self.lenb = 1
683 self.lenb = 1
684 else:
684 else:
685 self.lenb = int(self.lenb)
685 self.lenb = int(self.lenb)
686 self.starta = int(self.starta)
686 self.starta = int(self.starta)
687 self.startb = int(self.startb)
687 self.startb = int(self.startb)
688 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
688 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
689 # if we hit eof before finishing out the hunk, the last line will
689 # if we hit eof before finishing out the hunk, the last line will
690 # be zero length. Lets try to fix it up.
690 # be zero length. Lets try to fix it up.
691 while len(self.hunk[-1]) == 0:
691 while len(self.hunk[-1]) == 0:
692 del self.hunk[-1]
692 del self.hunk[-1]
693 del self.a[-1]
693 del self.a[-1]
694 del self.b[-1]
694 del self.b[-1]
695 self.lena -= 1
695 self.lena -= 1
696 self.lenb -= 1
696 self.lenb -= 1
697
697
698 def read_context_hunk(self, lr):
698 def read_context_hunk(self, lr):
699 self.desc = lr.readline()
699 self.desc = lr.readline()
700 m = contextdesc.match(self.desc)
700 m = contextdesc.match(self.desc)
701 if not m:
701 if not m:
702 raise PatchError(_("bad hunk #%d") % self.number)
702 raise PatchError(_("bad hunk #%d") % self.number)
703 foo, self.starta, foo2, aend, foo3 = m.groups()
703 foo, self.starta, foo2, aend, foo3 = m.groups()
704 self.starta = int(self.starta)
704 self.starta = int(self.starta)
705 if aend is None:
705 if aend is None:
706 aend = self.starta
706 aend = self.starta
707 self.lena = int(aend) - self.starta
707 self.lena = int(aend) - self.starta
708 if self.starta:
708 if self.starta:
709 self.lena += 1
709 self.lena += 1
710 for x in xrange(self.lena):
710 for x in xrange(self.lena):
711 l = lr.readline()
711 l = lr.readline()
712 if l.startswith('---'):
712 if l.startswith('---'):
713 lr.push(l)
713 lr.push(l)
714 break
714 break
715 s = l[2:]
715 s = l[2:]
716 if l.startswith('- ') or l.startswith('! '):
716 if l.startswith('- ') or l.startswith('! '):
717 u = '-' + s
717 u = '-' + s
718 elif l.startswith(' '):
718 elif l.startswith(' '):
719 u = ' ' + s
719 u = ' ' + s
720 else:
720 else:
721 raise PatchError(_("bad hunk #%d old text line %d") %
721 raise PatchError(_("bad hunk #%d old text line %d") %
722 (self.number, x))
722 (self.number, x))
723 self.a.append(u)
723 self.a.append(u)
724 self.hunk.append(u)
724 self.hunk.append(u)
725
725
726 l = lr.readline()
726 l = lr.readline()
727 if l.startswith('\ '):
727 if l.startswith('\ '):
728 s = self.a[-1][:-1]
728 s = self.a[-1][:-1]
729 self.a[-1] = s
729 self.a[-1] = s
730 self.hunk[-1] = s
730 self.hunk[-1] = s
731 l = lr.readline()
731 l = lr.readline()
732 m = contextdesc.match(l)
732 m = contextdesc.match(l)
733 if not m:
733 if not m:
734 raise PatchError(_("bad hunk #%d") % self.number)
734 raise PatchError(_("bad hunk #%d") % self.number)
735 foo, self.startb, foo2, bend, foo3 = m.groups()
735 foo, self.startb, foo2, bend, foo3 = m.groups()
736 self.startb = int(self.startb)
736 self.startb = int(self.startb)
737 if bend is None:
737 if bend is None:
738 bend = self.startb
738 bend = self.startb
739 self.lenb = int(bend) - self.startb
739 self.lenb = int(bend) - self.startb
740 if self.startb:
740 if self.startb:
741 self.lenb += 1
741 self.lenb += 1
742 hunki = 1
742 hunki = 1
743 for x in xrange(self.lenb):
743 for x in xrange(self.lenb):
744 l = lr.readline()
744 l = lr.readline()
745 if l.startswith('\ '):
745 if l.startswith('\ '):
746 s = self.b[-1][:-1]
746 s = self.b[-1][:-1]
747 self.b[-1] = s
747 self.b[-1] = s
748 self.hunk[hunki - 1] = s
748 self.hunk[hunki - 1] = s
749 continue
749 continue
750 if not l:
750 if not l:
751 lr.push(l)
751 lr.push(l)
752 break
752 break
753 s = l[2:]
753 s = l[2:]
754 if l.startswith('+ ') or l.startswith('! '):
754 if l.startswith('+ ') or l.startswith('! '):
755 u = '+' + s
755 u = '+' + s
756 elif l.startswith(' '):
756 elif l.startswith(' '):
757 u = ' ' + s
757 u = ' ' + s
758 elif len(self.b) == 0:
758 elif len(self.b) == 0:
759 # this can happen when the hunk does not add any lines
759 # this can happen when the hunk does not add any lines
760 lr.push(l)
760 lr.push(l)
761 break
761 break
762 else:
762 else:
763 raise PatchError(_("bad hunk #%d old text line %d") %
763 raise PatchError(_("bad hunk #%d old text line %d") %
764 (self.number, x))
764 (self.number, x))
765 self.b.append(s)
765 self.b.append(s)
766 while True:
766 while True:
767 if hunki >= len(self.hunk):
767 if hunki >= len(self.hunk):
768 h = ""
768 h = ""
769 else:
769 else:
770 h = self.hunk[hunki]
770 h = self.hunk[hunki]
771 hunki += 1
771 hunki += 1
772 if h == u:
772 if h == u:
773 break
773 break
774 elif h.startswith('-'):
774 elif h.startswith('-'):
775 continue
775 continue
776 else:
776 else:
777 self.hunk.insert(hunki - 1, u)
777 self.hunk.insert(hunki - 1, u)
778 break
778 break
779
779
780 if not self.a:
780 if not self.a:
781 # this happens when lines were only added to the hunk
781 # this happens when lines were only added to the hunk
782 for x in self.hunk:
782 for x in self.hunk:
783 if x.startswith('-') or x.startswith(' '):
783 if x.startswith('-') or x.startswith(' '):
784 self.a.append(x)
784 self.a.append(x)
785 if not self.b:
785 if not self.b:
786 # this happens when lines were only deleted from the hunk
786 # this happens when lines were only deleted from the hunk
787 for x in self.hunk:
787 for x in self.hunk:
788 if x.startswith('+') or x.startswith(' '):
788 if x.startswith('+') or x.startswith(' '):
789 self.b.append(x[1:])
789 self.b.append(x[1:])
790 # @@ -start,len +start,len @@
790 # @@ -start,len +start,len @@
791 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
791 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
792 self.startb, self.lenb)
792 self.startb, self.lenb)
793 self.hunk[0] = self.desc
793 self.hunk[0] = self.desc
794
794
795 def fix_newline(self):
795 def fix_newline(self):
796 diffhelpers.fix_newline(self.hunk, self.a, self.b)
796 diffhelpers.fix_newline(self.hunk, self.a, self.b)
797
797
798 def complete(self):
798 def complete(self):
799 return len(self.a) == self.lena and len(self.b) == self.lenb
799 return len(self.a) == self.lena and len(self.b) == self.lenb
800
800
801 def createfile(self):
801 def createfile(self):
802 return self.starta == 0 and self.lena == 0 and self.create
802 return self.starta == 0 and self.lena == 0 and self.create
803
803
804 def rmfile(self):
804 def rmfile(self):
805 return self.startb == 0 and self.lenb == 0 and self.remove
805 return self.startb == 0 and self.lenb == 0 and self.remove
806
806
807 def fuzzit(self, l, fuzz, toponly):
807 def fuzzit(self, l, fuzz, toponly):
808 # this removes context lines from the top and bottom of list 'l'. It
808 # this removes context lines from the top and bottom of list 'l'. It
809 # checks the hunk to make sure only context lines are removed, and then
809 # checks the hunk to make sure only context lines are removed, and then
810 # returns a new shortened list of lines.
810 # returns a new shortened list of lines.
811 fuzz = min(fuzz, len(l)-1)
811 fuzz = min(fuzz, len(l)-1)
812 if fuzz:
812 if fuzz:
813 top = 0
813 top = 0
814 bot = 0
814 bot = 0
815 hlen = len(self.hunk)
815 hlen = len(self.hunk)
816 for x in xrange(hlen - 1):
816 for x in xrange(hlen - 1):
817 # the hunk starts with the @@ line, so use x+1
817 # the hunk starts with the @@ line, so use x+1
818 if self.hunk[x + 1][0] == ' ':
818 if self.hunk[x + 1][0] == ' ':
819 top += 1
819 top += 1
820 else:
820 else:
821 break
821 break
822 if not toponly:
822 if not toponly:
823 for x in xrange(hlen - 1):
823 for x in xrange(hlen - 1):
824 if self.hunk[hlen - bot - 1][0] == ' ':
824 if self.hunk[hlen - bot - 1][0] == ' ':
825 bot += 1
825 bot += 1
826 else:
826 else:
827 break
827 break
828
828
829 # top and bot now count context in the hunk
829 # top and bot now count context in the hunk
830 # adjust them if either one is short
830 # adjust them if either one is short
831 context = max(top, bot, 3)
831 context = max(top, bot, 3)
832 if bot < context:
832 if bot < context:
833 bot = max(0, fuzz - (context - bot))
833 bot = max(0, fuzz - (context - bot))
834 else:
834 else:
835 bot = min(fuzz, bot)
835 bot = min(fuzz, bot)
836 if top < context:
836 if top < context:
837 top = max(0, fuzz - (context - top))
837 top = max(0, fuzz - (context - top))
838 else:
838 else:
839 top = min(fuzz, top)
839 top = min(fuzz, top)
840
840
841 return l[top:len(l)-bot]
841 return l[top:len(l)-bot]
842 return l
842 return l
843
843
844 def old(self, fuzz=0, toponly=False):
844 def old(self, fuzz=0, toponly=False):
845 return self.fuzzit(self.a, fuzz, toponly)
845 return self.fuzzit(self.a, fuzz, toponly)
846
846
847 def new(self, fuzz=0, toponly=False):
847 def new(self, fuzz=0, toponly=False):
848 return self.fuzzit(self.b, fuzz, toponly)
848 return self.fuzzit(self.b, fuzz, toponly)
849
849
850 class binhunk:
850 class binhunk:
851 'A binary patch file. Only understands literals so far.'
851 'A binary patch file. Only understands literals so far.'
852 def __init__(self, gitpatch):
852 def __init__(self, gitpatch):
853 self.gitpatch = gitpatch
853 self.gitpatch = gitpatch
854 self.text = None
854 self.text = None
855 self.hunk = ['GIT binary patch\n']
855 self.hunk = ['GIT binary patch\n']
856
856
857 def createfile(self):
857 def createfile(self):
858 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
858 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
859
859
860 def rmfile(self):
860 def rmfile(self):
861 return self.gitpatch.op == 'DELETE'
861 return self.gitpatch.op == 'DELETE'
862
862
863 def complete(self):
863 def complete(self):
864 return self.text is not None
864 return self.text is not None
865
865
866 def new(self):
866 def new(self):
867 return [self.text]
867 return [self.text]
868
868
869 def extract(self, lr):
869 def extract(self, lr):
870 line = lr.readline()
870 line = lr.readline()
871 self.hunk.append(line)
871 self.hunk.append(line)
872 while line and not line.startswith('literal '):
872 while line and not line.startswith('literal '):
873 line = lr.readline()
873 line = lr.readline()
874 self.hunk.append(line)
874 self.hunk.append(line)
875 if not line:
875 if not line:
876 raise PatchError(_('could not extract binary patch'))
876 raise PatchError(_('could not extract binary patch'))
877 size = int(line[8:].rstrip())
877 size = int(line[8:].rstrip())
878 dec = []
878 dec = []
879 line = lr.readline()
879 line = lr.readline()
880 self.hunk.append(line)
880 self.hunk.append(line)
881 while len(line) > 1:
881 while len(line) > 1:
882 l = line[0]
882 l = line[0]
883 if l <= 'Z' and l >= 'A':
883 if l <= 'Z' and l >= 'A':
884 l = ord(l) - ord('A') + 1
884 l = ord(l) - ord('A') + 1
885 else:
885 else:
886 l = ord(l) - ord('a') + 27
886 l = ord(l) - ord('a') + 27
887 dec.append(base85.b85decode(line[1:-1])[:l])
887 dec.append(base85.b85decode(line[1:-1])[:l])
888 line = lr.readline()
888 line = lr.readline()
889 self.hunk.append(line)
889 self.hunk.append(line)
890 text = zlib.decompress(''.join(dec))
890 text = zlib.decompress(''.join(dec))
891 if len(text) != size:
891 if len(text) != size:
892 raise PatchError(_('binary patch is %d bytes, not %d') %
892 raise PatchError(_('binary patch is %d bytes, not %d') %
893 len(text), size)
893 len(text), size)
894 self.text = text
894 self.text = text
895
895
896 def parsefilename(str):
896 def parsefilename(str):
897 # --- filename \t|space stuff
897 # --- filename \t|space stuff
898 s = str[4:].rstrip('\r\n')
898 s = str[4:].rstrip('\r\n')
899 i = s.find('\t')
899 i = s.find('\t')
900 if i < 0:
900 if i < 0:
901 i = s.find(' ')
901 i = s.find(' ')
902 if i < 0:
902 if i < 0:
903 return s
903 return s
904 return s[:i]
904 return s[:i]
905
905
906 def selectfile(afile_orig, bfile_orig, hunk, strip):
906 def selectfile(afile_orig, bfile_orig, hunk, strip):
907 def pathstrip(path, count=1):
907 def pathstrip(path, count=1):
908 pathlen = len(path)
908 pathlen = len(path)
909 i = 0
909 i = 0
910 if count == 0:
910 if count == 0:
911 return '', path.rstrip()
911 return '', path.rstrip()
912 while count > 0:
912 while count > 0:
913 i = path.find('/', i)
913 i = path.find('/', i)
914 if i == -1:
914 if i == -1:
915 raise PatchError(_("unable to strip away %d dirs from %s") %
915 raise PatchError(_("unable to strip away %d dirs from %s") %
916 (count, path))
916 (count, path))
917 i += 1
917 i += 1
918 # consume '//' in the path
918 # consume '//' in the path
919 while i < pathlen - 1 and path[i] == '/':
919 while i < pathlen - 1 and path[i] == '/':
920 i += 1
920 i += 1
921 count -= 1
921 count -= 1
922 return path[:i].lstrip(), path[i:].rstrip()
922 return path[:i].lstrip(), path[i:].rstrip()
923
923
924 nulla = afile_orig == "/dev/null"
924 nulla = afile_orig == "/dev/null"
925 nullb = bfile_orig == "/dev/null"
925 nullb = bfile_orig == "/dev/null"
926 abase, afile = pathstrip(afile_orig, strip)
926 abase, afile = pathstrip(afile_orig, strip)
927 gooda = not nulla and util.lexists(afile)
927 gooda = not nulla and util.lexists(afile)
928 bbase, bfile = pathstrip(bfile_orig, strip)
928 bbase, bfile = pathstrip(bfile_orig, strip)
929 if afile == bfile:
929 if afile == bfile:
930 goodb = gooda
930 goodb = gooda
931 else:
931 else:
932 goodb = not nullb and os.path.exists(bfile)
932 goodb = not nullb and os.path.exists(bfile)
933 createfunc = hunk.createfile
933 createfunc = hunk.createfile
934 missing = not goodb and not gooda and not createfunc()
934 missing = not goodb and not gooda and not createfunc()
935
935
936 # some diff programs apparently produce create patches where the
936 # some diff programs apparently produce create patches where the
937 # afile is not /dev/null, but rather the same name as the bfile
937 # afile is not /dev/null, but rather the same name as the bfile
938 if missing and afile == bfile:
938 if missing and afile == bfile:
939 # this isn't very pretty
939 # this isn't very pretty
940 hunk.create = True
940 hunk.create = True
941 if createfunc():
941 if createfunc():
942 missing = False
942 missing = False
943 else:
943 else:
944 hunk.create = False
944 hunk.create = False
945
945
946 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
946 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
947 # diff is between a file and its backup. In this case, the original
947 # diff is between a file and its backup. In this case, the original
948 # file should be patched (see original mpatch code).
948 # file should be patched (see original mpatch code).
949 isbackup = (abase == bbase and bfile.startswith(afile))
949 isbackup = (abase == bbase and bfile.startswith(afile))
950 fname = None
950 fname = None
951 if not missing:
951 if not missing:
952 if gooda and goodb:
952 if gooda and goodb:
953 fname = isbackup and afile or bfile
953 fname = isbackup and afile or bfile
954 elif gooda:
954 elif gooda:
955 fname = afile
955 fname = afile
956
956
957 if not fname:
957 if not fname:
958 if not nullb:
958 if not nullb:
959 fname = isbackup and afile or bfile
959 fname = isbackup and afile or bfile
960 elif not nulla:
960 elif not nulla:
961 fname = afile
961 fname = afile
962 else:
962 else:
963 raise PatchError(_("undefined source and destination files"))
963 raise PatchError(_("undefined source and destination files"))
964
964
965 return fname, missing
965 return fname, missing
966
966
967 def scangitpatch(lr, firstline):
967 def scangitpatch(lr, firstline):
968 """
968 """
969 Git patches can emit:
969 Git patches can emit:
970 - rename a to b
970 - rename a to b
971 - change b
971 - change b
972 - copy a to c
972 - copy a to c
973 - change c
973 - change c
974
974
975 We cannot apply this sequence as-is, the renamed 'a' could not be
975 We cannot apply this sequence as-is, the renamed 'a' could not be
976 found for it would have been renamed already. And we cannot copy
976 found for it would have been renamed already. And we cannot copy
977 from 'b' instead because 'b' would have been changed already. So
977 from 'b' instead because 'b' would have been changed already. So
978 we scan the git patch for copy and rename commands so we can
978 we scan the git patch for copy and rename commands so we can
979 perform the copies ahead of time.
979 perform the copies ahead of time.
980 """
980 """
981 pos = 0
981 pos = 0
982 try:
982 try:
983 pos = lr.fp.tell()
983 pos = lr.fp.tell()
984 fp = lr.fp
984 fp = lr.fp
985 except IOError:
985 except IOError:
986 fp = cStringIO.StringIO(lr.fp.read())
986 fp = cStringIO.StringIO(lr.fp.read())
987 gitlr = linereader(fp, lr.textmode)
987 gitlr = linereader(fp, lr.textmode)
988 gitlr.push(firstline)
988 gitlr.push(firstline)
989 (dopatch, gitpatches) = readgitpatch(gitlr)
989 (dopatch, gitpatches) = readgitpatch(gitlr)
990 fp.seek(pos)
990 fp.seek(pos)
991 return dopatch, gitpatches
991 return dopatch, gitpatches
992
992
993 def iterhunks(ui, fp, sourcefile=None):
993 def iterhunks(ui, fp, sourcefile=None):
994 """Read a patch and yield the following events:
994 """Read a patch and yield the following events:
995 - ("file", afile, bfile, firsthunk): select a new target file.
995 - ("file", afile, bfile, firsthunk): select a new target file.
996 - ("hunk", hunk): a new hunk is ready to be applied, follows a
996 - ("hunk", hunk): a new hunk is ready to be applied, follows a
997 "file" event.
997 "file" event.
998 - ("git", gitchanges): current diff is in git format, gitchanges
998 - ("git", gitchanges): current diff is in git format, gitchanges
999 maps filenames to gitpatch records. Unique event.
999 maps filenames to gitpatch records. Unique event.
1000 """
1000 """
1001 changed = {}
1001 changed = {}
1002 current_hunk = None
1002 current_hunk = None
1003 afile = ""
1003 afile = ""
1004 bfile = ""
1004 bfile = ""
1005 state = None
1005 state = None
1006 hunknum = 0
1006 hunknum = 0
1007 emitfile = False
1007 emitfile = False
1008 git = False
1008 git = False
1009
1009
1010 # our states
1010 # our states
1011 BFILE = 1
1011 BFILE = 1
1012 context = None
1012 context = None
1013 lr = linereader(fp)
1013 lr = linereader(fp)
1014 dopatch = True
1014 dopatch = True
1015 # gitworkdone is True if a git operation (copy, rename, ...) was
1015 # gitworkdone is True if a git operation (copy, rename, ...) was
1016 # performed already for the current file. Useful when the file
1016 # performed already for the current file. Useful when the file
1017 # section may have no hunk.
1017 # section may have no hunk.
1018 gitworkdone = False
1018 gitworkdone = False
1019
1019
1020 while True:
1020 while True:
1021 newfile = False
1021 newfile = False
1022 x = lr.readline()
1022 x = lr.readline()
1023 if not x:
1023 if not x:
1024 break
1024 break
1025 if current_hunk:
1025 if current_hunk:
1026 if x.startswith('\ '):
1026 if x.startswith('\ '):
1027 current_hunk.fix_newline()
1027 current_hunk.fix_newline()
1028 yield 'hunk', current_hunk
1028 yield 'hunk', current_hunk
1029 current_hunk = None
1029 current_hunk = None
1030 gitworkdone = False
1030 gitworkdone = False
1031 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1031 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1032 ((context is not False) and x.startswith('***************')))):
1032 ((context is not False) and x.startswith('***************')))):
1033 try:
1033 try:
1034 if context is None and x.startswith('***************'):
1034 if context is None and x.startswith('***************'):
1035 context = True
1035 context = True
1036 gpatch = changed.get(bfile)
1036 gpatch = changed.get(bfile)
1037 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1037 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1038 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1038 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1039 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1039 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1040 except PatchError, err:
1040 except PatchError, err:
1041 ui.debug(err)
1041 ui.debug(err)
1042 current_hunk = None
1042 current_hunk = None
1043 continue
1043 continue
1044 hunknum += 1
1044 hunknum += 1
1045 if emitfile:
1045 if emitfile:
1046 emitfile = False
1046 emitfile = False
1047 yield 'file', (afile, bfile, current_hunk)
1047 yield 'file', (afile, bfile, current_hunk)
1048 elif state == BFILE and x.startswith('GIT binary patch'):
1048 elif state == BFILE and x.startswith('GIT binary patch'):
1049 current_hunk = binhunk(changed[bfile])
1049 current_hunk = binhunk(changed[bfile])
1050 hunknum += 1
1050 hunknum += 1
1051 if emitfile:
1051 if emitfile:
1052 emitfile = False
1052 emitfile = False
1053 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1053 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1054 current_hunk.extract(lr)
1054 current_hunk.extract(lr)
1055 elif x.startswith('diff --git'):
1055 elif x.startswith('diff --git'):
1056 # check for git diff, scanning the whole patch file if needed
1056 # check for git diff, scanning the whole patch file if needed
1057 m = gitre.match(x)
1057 m = gitre.match(x)
1058 if m:
1058 if m:
1059 afile, bfile = m.group(1, 2)
1059 afile, bfile = m.group(1, 2)
1060 if not git:
1060 if not git:
1061 git = True
1061 git = True
1062 dopatch, gitpatches = scangitpatch(lr, x)
1062 dopatch, gitpatches = scangitpatch(lr, x)
1063 yield 'git', gitpatches
1063 yield 'git', gitpatches
1064 for gp in gitpatches:
1064 for gp in gitpatches:
1065 changed[gp.path] = gp
1065 changed[gp.path] = gp
1066 # else error?
1066 # else error?
1067 # copy/rename + modify should modify target, not source
1067 # copy/rename + modify should modify target, not source
1068 gp = changed.get(bfile)
1068 gp = changed.get(bfile)
1069 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
1069 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
1070 afile = bfile
1070 afile = bfile
1071 gitworkdone = True
1071 gitworkdone = True
1072 newfile = True
1072 newfile = True
1073 elif x.startswith('---'):
1073 elif x.startswith('---'):
1074 # check for a unified diff
1074 # check for a unified diff
1075 l2 = lr.readline()
1075 l2 = lr.readline()
1076 if not l2.startswith('+++'):
1076 if not l2.startswith('+++'):
1077 lr.push(l2)
1077 lr.push(l2)
1078 continue
1078 continue
1079 newfile = True
1079 newfile = True
1080 context = False
1080 context = False
1081 afile = parsefilename(x)
1081 afile = parsefilename(x)
1082 bfile = parsefilename(l2)
1082 bfile = parsefilename(l2)
1083 elif x.startswith('***'):
1083 elif x.startswith('***'):
1084 # check for a context diff
1084 # check for a context diff
1085 l2 = lr.readline()
1085 l2 = lr.readline()
1086 if not l2.startswith('---'):
1086 if not l2.startswith('---'):
1087 lr.push(l2)
1087 lr.push(l2)
1088 continue
1088 continue
1089 l3 = lr.readline()
1089 l3 = lr.readline()
1090 lr.push(l3)
1090 lr.push(l3)
1091 if not l3.startswith("***************"):
1091 if not l3.startswith("***************"):
1092 lr.push(l2)
1092 lr.push(l2)
1093 continue
1093 continue
1094 newfile = True
1094 newfile = True
1095 context = True
1095 context = True
1096 afile = parsefilename(x)
1096 afile = parsefilename(x)
1097 bfile = parsefilename(l2)
1097 bfile = parsefilename(l2)
1098
1098
1099 if newfile:
1099 if newfile:
1100 emitfile = True
1100 emitfile = True
1101 state = BFILE
1101 state = BFILE
1102 hunknum = 0
1102 hunknum = 0
1103 if current_hunk:
1103 if current_hunk:
1104 if current_hunk.complete():
1104 if current_hunk.complete():
1105 yield 'hunk', current_hunk
1105 yield 'hunk', current_hunk
1106 else:
1106 else:
1107 raise PatchError(_("malformed patch %s %s") % (afile,
1107 raise PatchError(_("malformed patch %s %s") % (afile,
1108 current_hunk.desc))
1108 current_hunk.desc))
1109
1109
1110 if hunknum == 0 and dopatch and not gitworkdone:
1110 if hunknum == 0 and dopatch and not gitworkdone:
1111 raise NoHunks
1111 raise NoHunks
1112
1112
1113 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1113 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1114 """
1114 """
1115 Reads a patch from fp and tries to apply it.
1115 Reads a patch from fp and tries to apply it.
1116
1116
1117 The dict 'changed' is filled in with all of the filenames changed
1117 The dict 'changed' is filled in with all of the filenames changed
1118 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1118 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1119 found and 1 if there was any fuzz.
1119 found and 1 if there was any fuzz.
1120
1120
1121 If 'eolmode' is 'strict', the patch content and patched file are
1121 If 'eolmode' is 'strict', the patch content and patched file are
1122 read in binary mode. Otherwise, line endings are ignored when
1122 read in binary mode. Otherwise, line endings are ignored when
1123 patching then normalized according to 'eolmode'.
1123 patching then normalized according to 'eolmode'.
1124 """
1124 """
1125 rejects = 0
1125 rejects = 0
1126 err = 0
1126 err = 0
1127 current_file = None
1127 current_file = None
1128 gitpatches = None
1128 gitpatches = None
1129 opener = util.opener(os.getcwd())
1129 opener = util.opener(os.getcwd())
1130
1130
1131 def closefile():
1131 def closefile():
1132 if not current_file:
1132 if not current_file:
1133 return 0
1133 return 0
1134 current_file.close()
1134 current_file.close()
1135 return len(current_file.rej)
1135 return len(current_file.rej)
1136
1136
1137 for state, values in iterhunks(ui, fp, sourcefile):
1137 for state, values in iterhunks(ui, fp, sourcefile):
1138 if state == 'hunk':
1138 if state == 'hunk':
1139 if not current_file:
1139 if not current_file:
1140 continue
1140 continue
1141 current_hunk = values
1141 current_hunk = values
1142 ret = current_file.apply(current_hunk)
1142 ret = current_file.apply(current_hunk)
1143 if ret >= 0:
1143 if ret >= 0:
1144 changed.setdefault(current_file.fname, None)
1144 changed.setdefault(current_file.fname, None)
1145 if ret > 0:
1145 if ret > 0:
1146 err = 1
1146 err = 1
1147 elif state == 'file':
1147 elif state == 'file':
1148 rejects += closefile()
1148 rejects += closefile()
1149 afile, bfile, first_hunk = values
1149 afile, bfile, first_hunk = values
1150 try:
1150 try:
1151 if sourcefile:
1151 if sourcefile:
1152 current_file = patchfile(ui, sourcefile, opener,
1152 current_file = patchfile(ui, sourcefile, opener,
1153 eolmode=eolmode)
1153 eolmode=eolmode)
1154 else:
1154 else:
1155 current_file, missing = selectfile(afile, bfile,
1155 current_file, missing = selectfile(afile, bfile,
1156 first_hunk, strip)
1156 first_hunk, strip)
1157 current_file = patchfile(ui, current_file, opener,
1157 current_file = patchfile(ui, current_file, opener,
1158 missing, eolmode)
1158 missing, eolmode)
1159 except PatchError, err:
1159 except PatchError, err:
1160 ui.warn(str(err) + '\n')
1160 ui.warn(str(err) + '\n')
1161 current_file, current_hunk = None, None
1161 current_file, current_hunk = None, None
1162 rejects += 1
1162 rejects += 1
1163 continue
1163 continue
1164 elif state == 'git':
1164 elif state == 'git':
1165 gitpatches = values
1165 gitpatches = values
1166 cwd = os.getcwd()
1166 cwd = os.getcwd()
1167 for gp in gitpatches:
1167 for gp in gitpatches:
1168 if gp.op in ('COPY', 'RENAME'):
1168 if gp.op in ('COPY', 'RENAME'):
1169 copyfile(gp.oldpath, gp.path, cwd)
1169 copyfile(gp.oldpath, gp.path, cwd)
1170 changed[gp.path] = gp
1170 changed[gp.path] = gp
1171 else:
1171 else:
1172 raise util.Abort(_('unsupported parser state: %s') % state)
1172 raise util.Abort(_('unsupported parser state: %s') % state)
1173
1173
1174 rejects += closefile()
1174 rejects += closefile()
1175
1175
1176 if rejects:
1176 if rejects:
1177 return -1
1177 return -1
1178 return err
1178 return err
1179
1179
1180 def diffopts(ui, opts=None, untrusted=False):
1180 def diffopts(ui, opts=None, untrusted=False):
1181 def get(key, name=None, getter=ui.configbool):
1181 def get(key, name=None, getter=ui.configbool):
1182 return ((opts and opts.get(key)) or
1182 return ((opts and opts.get(key)) or
1183 getter('diff', name or key, None, untrusted=untrusted))
1183 getter('diff', name or key, None, untrusted=untrusted))
1184 return mdiff.diffopts(
1184 return mdiff.diffopts(
1185 text=opts and opts.get('text'),
1185 text=opts and opts.get('text'),
1186 git=get('git'),
1186 git=get('git'),
1187 nodates=get('nodates'),
1187 nodates=get('nodates'),
1188 showfunc=get('show_function', 'showfunc'),
1188 showfunc=get('show_function', 'showfunc'),
1189 ignorews=get('ignore_all_space', 'ignorews'),
1189 ignorews=get('ignore_all_space', 'ignorews'),
1190 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1190 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1191 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1191 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1192 context=get('unified', getter=ui.config))
1192 context=get('unified', getter=ui.config))
1193
1193
1194 def updatedir(ui, repo, patches, similarity=0):
1194 def updatedir(ui, repo, patches, similarity=0):
1195 '''Update dirstate after patch application according to metadata'''
1195 '''Update dirstate after patch application according to metadata'''
1196 if not patches:
1196 if not patches:
1197 return
1197 return
1198 copies = []
1198 copies = []
1199 removes = set()
1199 removes = set()
1200 cfiles = patches.keys()
1200 cfiles = patches.keys()
1201 cwd = repo.getcwd()
1201 cwd = repo.getcwd()
1202 if cwd:
1202 if cwd:
1203 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1203 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1204 for f in patches:
1204 for f in patches:
1205 gp = patches[f]
1205 gp = patches[f]
1206 if not gp:
1206 if not gp:
1207 continue
1207 continue
1208 if gp.op == 'RENAME':
1208 if gp.op == 'RENAME':
1209 copies.append((gp.oldpath, gp.path))
1209 copies.append((gp.oldpath, gp.path))
1210 removes.add(gp.oldpath)
1210 removes.add(gp.oldpath)
1211 elif gp.op == 'COPY':
1211 elif gp.op == 'COPY':
1212 copies.append((gp.oldpath, gp.path))
1212 copies.append((gp.oldpath, gp.path))
1213 elif gp.op == 'DELETE':
1213 elif gp.op == 'DELETE':
1214 removes.add(gp.path)
1214 removes.add(gp.path)
1215 for src, dst in copies:
1215 for src, dst in copies:
1216 repo.copy(src, dst)
1216 repo.copy(src, dst)
1217 if (not similarity) and removes:
1217 if (not similarity) and removes:
1218 repo.remove(sorted(removes), True)
1218 repo.remove(sorted(removes), True)
1219 for f in patches:
1219 for f in patches:
1220 gp = patches[f]
1220 gp = patches[f]
1221 if gp and gp.mode:
1221 if gp and gp.mode:
1222 islink, isexec = gp.mode
1222 islink, isexec = gp.mode
1223 dst = repo.wjoin(gp.path)
1223 dst = repo.wjoin(gp.path)
1224 # patch won't create empty files
1224 # patch won't create empty files
1225 if gp.op == 'ADD' and not os.path.exists(dst):
1225 if gp.op == 'ADD' and not os.path.exists(dst):
1226 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1226 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1227 repo.wwrite(gp.path, '', flags)
1227 repo.wwrite(gp.path, '', flags)
1228 elif gp.op != 'DELETE':
1228 elif gp.op != 'DELETE':
1229 util.set_flags(dst, islink, isexec)
1229 util.set_flags(dst, islink, isexec)
1230 cmdutil.addremove(repo, cfiles, similarity=similarity)
1230 cmdutil.addremove(repo, cfiles, similarity=similarity)
1231 files = patches.keys()
1231 files = patches.keys()
1232 files.extend([r for r in removes if r not in files])
1232 files.extend([r for r in removes if r not in files])
1233 return sorted(files)
1233 return sorted(files)
1234
1234
1235 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1235 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1236 """use <patcher> to apply <patchname> to the working directory.
1236 """use <patcher> to apply <patchname> to the working directory.
1237 returns whether patch was applied with fuzz factor."""
1237 returns whether patch was applied with fuzz factor."""
1238
1238
1239 fuzz = False
1239 fuzz = False
1240 if cwd:
1240 if cwd:
1241 args.append('-d %s' % util.shellquote(cwd))
1241 args.append('-d %s' % util.shellquote(cwd))
1242 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1242 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1243 util.shellquote(patchname)))
1243 util.shellquote(patchname)))
1244
1244
1245 for line in fp:
1245 for line in fp:
1246 line = line.rstrip()
1246 line = line.rstrip()
1247 ui.note(line + '\n')
1247 ui.note(line + '\n')
1248 if line.startswith('patching file '):
1248 if line.startswith('patching file '):
1249 pf = util.parse_patch_output(line)
1249 pf = util.parse_patch_output(line)
1250 printed_file = False
1250 printed_file = False
1251 files.setdefault(pf, None)
1251 files.setdefault(pf, None)
1252 elif line.find('with fuzz') >= 0:
1252 elif line.find('with fuzz') >= 0:
1253 fuzz = True
1253 fuzz = True
1254 if not printed_file:
1254 if not printed_file:
1255 ui.warn(pf + '\n')
1255 ui.warn(pf + '\n')
1256 printed_file = True
1256 printed_file = True
1257 ui.warn(line + '\n')
1257 ui.warn(line + '\n')
1258 elif line.find('saving rejects to file') >= 0:
1258 elif line.find('saving rejects to file') >= 0:
1259 ui.warn(line + '\n')
1259 ui.warn(line + '\n')
1260 elif line.find('FAILED') >= 0:
1260 elif line.find('FAILED') >= 0:
1261 if not printed_file:
1261 if not printed_file:
1262 ui.warn(pf + '\n')
1262 ui.warn(pf + '\n')
1263 printed_file = True
1263 printed_file = True
1264 ui.warn(line + '\n')
1264 ui.warn(line + '\n')
1265 code = fp.close()
1265 code = fp.close()
1266 if code:
1266 if code:
1267 raise PatchError(_("patch command failed: %s") %
1267 raise PatchError(_("patch command failed: %s") %
1268 util.explain_exit(code)[0])
1268 util.explain_exit(code)[0])
1269 return fuzz
1269 return fuzz
1270
1270
1271 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1271 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1272 """use builtin patch to apply <patchobj> to the working directory.
1272 """use builtin patch to apply <patchobj> to the working directory.
1273 returns whether patch was applied with fuzz factor."""
1273 returns whether patch was applied with fuzz factor."""
1274
1274
1275 if files is None:
1275 if files is None:
1276 files = {}
1276 files = {}
1277 if eolmode is None:
1277 if eolmode is None:
1278 eolmode = ui.config('patch', 'eol', 'strict')
1278 eolmode = ui.config('patch', 'eol', 'strict')
1279 if eolmode.lower() not in eolmodes:
1279 if eolmode.lower() not in eolmodes:
1280 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1280 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1281 eolmode = eolmode.lower()
1281 eolmode = eolmode.lower()
1282
1282
1283 try:
1283 try:
1284 fp = open(patchobj, 'rb')
1284 fp = open(patchobj, 'rb')
1285 except TypeError:
1285 except TypeError:
1286 fp = patchobj
1286 fp = patchobj
1287 if cwd:
1287 if cwd:
1288 curdir = os.getcwd()
1288 curdir = os.getcwd()
1289 os.chdir(cwd)
1289 os.chdir(cwd)
1290 try:
1290 try:
1291 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1291 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1292 finally:
1292 finally:
1293 if cwd:
1293 if cwd:
1294 os.chdir(curdir)
1294 os.chdir(curdir)
1295 if fp != patchobj:
1295 if fp != patchobj:
1296 fp.close()
1296 fp.close()
1297 if ret < 0:
1297 if ret < 0:
1298 raise PatchError
1298 raise PatchError
1299 return ret > 0
1299 return ret > 0
1300
1300
1301 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1301 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1302 """Apply <patchname> to the working directory.
1302 """Apply <patchname> to the working directory.
1303
1303
1304 'eolmode' specifies how end of lines should be handled. It can be:
1304 'eolmode' specifies how end of lines should be handled. It can be:
1305 - 'strict': inputs are read in binary mode, EOLs are preserved
1305 - 'strict': inputs are read in binary mode, EOLs are preserved
1306 - 'crlf': EOLs are ignored when patching and reset to CRLF
1306 - 'crlf': EOLs are ignored when patching and reset to CRLF
1307 - 'lf': EOLs are ignored when patching and reset to LF
1307 - 'lf': EOLs are ignored when patching and reset to LF
1308 - None: get it from user settings, default to 'strict'
1308 - None: get it from user settings, default to 'strict'
1309 'eolmode' is ignored when using an external patcher program.
1309 'eolmode' is ignored when using an external patcher program.
1310
1310
1311 Returns whether patch was applied with fuzz factor.
1311 Returns whether patch was applied with fuzz factor.
1312 """
1312 """
1313 patcher = ui.config('ui', 'patch')
1313 patcher = ui.config('ui', 'patch')
1314 args = []
1314 args = []
1315 if files is None:
1315 if files is None:
1316 files = {}
1316 files = {}
1317 try:
1317 try:
1318 if patcher:
1318 if patcher:
1319 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1319 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1320 files)
1320 files)
1321 else:
1321 else:
1322 try:
1322 try:
1323 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1323 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1324 except NoHunks:
1324 except NoHunks:
1325 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1325 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1326 or 'patch')
1326 or 'patch')
1327 ui.debug('no valid hunks found; trying with %r instead\n' %
1327 ui.debug('no valid hunks found; trying with %r instead\n' %
1328 patcher)
1328 patcher)
1329 if util.needbinarypatch():
1329 if util.needbinarypatch():
1330 args.append('--binary')
1330 args.append('--binary')
1331 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1331 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1332 files)
1332 files)
1333 except PatchError, err:
1333 except PatchError, err:
1334 s = str(err)
1334 s = str(err)
1335 if s:
1335 if s:
1336 raise util.Abort(s)
1336 raise util.Abort(s)
1337 else:
1337 else:
1338 raise util.Abort(_('patch failed to apply'))
1338 raise util.Abort(_('patch failed to apply'))
1339
1339
1340 def b85diff(to, tn):
1340 def b85diff(to, tn):
1341 '''print base85-encoded binary diff'''
1341 '''print base85-encoded binary diff'''
1342 def gitindex(text):
1342 def gitindex(text):
1343 if not text:
1343 if not text:
1344 return '0' * 40
1344 return '0' * 40
1345 l = len(text)
1345 l = len(text)
1346 s = util.sha1('blob %d\0' % l)
1346 s = util.sha1('blob %d\0' % l)
1347 s.update(text)
1347 s.update(text)
1348 return s.hexdigest()
1348 return s.hexdigest()
1349
1349
1350 def fmtline(line):
1350 def fmtline(line):
1351 l = len(line)
1351 l = len(line)
1352 if l <= 26:
1352 if l <= 26:
1353 l = chr(ord('A') + l - 1)
1353 l = chr(ord('A') + l - 1)
1354 else:
1354 else:
1355 l = chr(l - 26 + ord('a') - 1)
1355 l = chr(l - 26 + ord('a') - 1)
1356 return '%c%s\n' % (l, base85.b85encode(line, True))
1356 return '%c%s\n' % (l, base85.b85encode(line, True))
1357
1357
1358 def chunk(text, csize=52):
1358 def chunk(text, csize=52):
1359 l = len(text)
1359 l = len(text)
1360 i = 0
1360 i = 0
1361 while i < l:
1361 while i < l:
1362 yield text[i:i + csize]
1362 yield text[i:i + csize]
1363 i += csize
1363 i += csize
1364
1364
1365 tohash = gitindex(to)
1365 tohash = gitindex(to)
1366 tnhash = gitindex(tn)
1366 tnhash = gitindex(tn)
1367 if tohash == tnhash:
1367 if tohash == tnhash:
1368 return ""
1368 return ""
1369
1369
1370 # TODO: deltas
1370 # TODO: deltas
1371 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1371 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1372 (tohash, tnhash, len(tn))]
1372 (tohash, tnhash, len(tn))]
1373 for l in chunk(zlib.compress(tn)):
1373 for l in chunk(zlib.compress(tn)):
1374 ret.append(fmtline(l))
1374 ret.append(fmtline(l))
1375 ret.append('\n')
1375 ret.append('\n')
1376 return ''.join(ret)
1376 return ''.join(ret)
1377
1377
1378 class GitDiffRequired(Exception):
1378 class GitDiffRequired(Exception):
1379 pass
1379 pass
1380
1380
1381 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1381 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1382 losedatafn=None):
1382 losedatafn=None):
1383 '''yields diff of changes to files between two nodes, or node and
1383 '''yields diff of changes to files between two nodes, or node and
1384 working directory.
1384 working directory.
1385
1385
1386 if node1 is None, use first dirstate parent instead.
1386 if node1 is None, use first dirstate parent instead.
1387 if node2 is None, compare node1 with working directory.
1387 if node2 is None, compare node1 with working directory.
1388
1388
1389 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1389 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1390 every time some change cannot be represented with the current
1390 every time some change cannot be represented with the current
1391 patch format. Return False to upgrade to git patch format, True to
1391 patch format. Return False to upgrade to git patch format, True to
1392 accept the loss or raise an exception to abort the diff. It is
1392 accept the loss or raise an exception to abort the diff. It is
1393 called with the name of current file being diffed as 'fn'. If set
1393 called with the name of current file being diffed as 'fn'. If set
1394 to None, patches will always be upgraded to git format when
1394 to None, patches will always be upgraded to git format when
1395 necessary.
1395 necessary.
1396 '''
1396 '''
1397
1397
1398 if opts is None:
1398 if opts is None:
1399 opts = mdiff.defaultopts
1399 opts = mdiff.defaultopts
1400
1400
1401 if not node1 and not node2:
1401 if not node1 and not node2:
1402 node1 = repo.dirstate.parents()[0]
1402 node1 = repo.dirstate.parents()[0]
1403
1403
1404 def lrugetfilectx():
1404 def lrugetfilectx():
1405 cache = {}
1405 cache = {}
1406 order = []
1406 order = []
1407 def getfilectx(f, ctx):
1407 def getfilectx(f, ctx):
1408 fctx = ctx.filectx(f, filelog=cache.get(f))
1408 fctx = ctx.filectx(f, filelog=cache.get(f))
1409 if f not in cache:
1409 if f not in cache:
1410 if len(cache) > 20:
1410 if len(cache) > 20:
1411 del cache[order.pop(0)]
1411 del cache[order.pop(0)]
1412 cache[f] = fctx.filelog()
1412 cache[f] = fctx.filelog()
1413 else:
1413 else:
1414 order.remove(f)
1414 order.remove(f)
1415 order.append(f)
1415 order.append(f)
1416 return fctx
1416 return fctx
1417 return getfilectx
1417 return getfilectx
1418 getfilectx = lrugetfilectx()
1418 getfilectx = lrugetfilectx()
1419
1419
1420 ctx1 = repo[node1]
1420 ctx1 = repo[node1]
1421 ctx2 = repo[node2]
1421 ctx2 = repo[node2]
1422
1422
1423 if not changes:
1423 if not changes:
1424 changes = repo.status(ctx1, ctx2, match=match)
1424 changes = repo.status(ctx1, ctx2, match=match)
1425 modified, added, removed = changes[:3]
1425 modified, added, removed = changes[:3]
1426
1426
1427 if not modified and not added and not removed:
1427 if not modified and not added and not removed:
1428 return []
1428 return []
1429
1429
1430 revs = None
1430 revs = None
1431 if not repo.ui.quiet:
1431 if not repo.ui.quiet:
1432 hexfunc = repo.ui.debugflag and hex or short
1432 hexfunc = repo.ui.debugflag and hex or short
1433 revs = [hexfunc(node) for node in [node1, node2] if node]
1433 revs = [hexfunc(node) for node in [node1, node2] if node]
1434
1434
1435 copy = {}
1435 copy = {}
1436 if opts.git or opts.upgrade:
1436 if opts.git or opts.upgrade:
1437 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1437 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1438 copy = copy.copy()
1438 copy = copy.copy()
1439 for k, v in copy.items():
1440 copy[v] = k
1441
1439
1442 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1440 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1443 modified, added, removed, copy, getfilectx, opts, losedata)
1441 modified, added, removed, copy, getfilectx, opts, losedata)
1444 if opts.upgrade and not opts.git:
1442 if opts.upgrade and not opts.git:
1445 try:
1443 try:
1446 def losedata(fn):
1444 def losedata(fn):
1447 if not losedatafn or not losedatafn(fn=fn):
1445 if not losedatafn or not losedatafn(fn=fn):
1448 raise GitDiffRequired()
1446 raise GitDiffRequired()
1449 # Buffer the whole output until we are sure it can be generated
1447 # Buffer the whole output until we are sure it can be generated
1450 return list(difffn(opts.copy(git=False), losedata))
1448 return list(difffn(opts.copy(git=False), losedata))
1451 except GitDiffRequired:
1449 except GitDiffRequired:
1452 return difffn(opts.copy(git=True), None)
1450 return difffn(opts.copy(git=True), None)
1453 else:
1451 else:
1454 return difffn(opts, None)
1452 return difffn(opts, None)
1455
1453
1456 def _addmodehdr(header, omode, nmode):
1454 def _addmodehdr(header, omode, nmode):
1457 if omode != nmode:
1455 if omode != nmode:
1458 header.append('old mode %s\n' % omode)
1456 header.append('old mode %s\n' % omode)
1459 header.append('new mode %s\n' % nmode)
1457 header.append('new mode %s\n' % nmode)
1460
1458
1461 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1459 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1462 copy, getfilectx, opts, losedatafn):
1460 copy, getfilectx, opts, losedatafn):
1463
1461
1464 date1 = util.datestr(ctx1.date())
1462 date1 = util.datestr(ctx1.date())
1465 man1 = ctx1.manifest()
1463 man1 = ctx1.manifest()
1466
1464
1467 gone = set()
1465 gone = set()
1468 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1466 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1469
1467
1468 copyto = dict([(v, k) for k, v in copy.items()])
1469
1470 if opts.git:
1470 if opts.git:
1471 revs = None
1471 revs = None
1472
1472
1473 for f in sorted(modified + added + removed):
1473 for f in sorted(modified + added + removed):
1474 to = None
1474 to = None
1475 tn = None
1475 tn = None
1476 dodiff = True
1476 dodiff = True
1477 header = []
1477 header = []
1478 if f in man1:
1478 if f in man1:
1479 to = getfilectx(f, ctx1).data()
1479 to = getfilectx(f, ctx1).data()
1480 if f not in removed:
1480 if f not in removed:
1481 tn = getfilectx(f, ctx2).data()
1481 tn = getfilectx(f, ctx2).data()
1482 a, b = f, f
1482 a, b = f, f
1483 if opts.git or losedatafn:
1483 if opts.git or losedatafn:
1484 if f in added:
1484 if f in added:
1485 mode = gitmode[ctx2.flags(f)]
1485 mode = gitmode[ctx2.flags(f)]
1486 if f in copy:
1486 if f in copy or f in copyto:
1487 if opts.git:
1487 if opts.git:
1488 a = copy[f]
1488 if f in copy:
1489 a = copy[f]
1490 else:
1491 a = copyto[f]
1489 omode = gitmode[man1.flags(a)]
1492 omode = gitmode[man1.flags(a)]
1490 _addmodehdr(header, omode, mode)
1493 _addmodehdr(header, omode, mode)
1491 if a in removed and a not in gone:
1494 if a in removed and a not in gone:
1492 op = 'rename'
1495 op = 'rename'
1493 gone.add(a)
1496 gone.add(a)
1494 else:
1497 else:
1495 op = 'copy'
1498 op = 'copy'
1496 header.append('%s from %s\n' % (op, a))
1499 header.append('%s from %s\n' % (op, a))
1497 header.append('%s to %s\n' % (op, f))
1500 header.append('%s to %s\n' % (op, f))
1498 to = getfilectx(a, ctx1).data()
1501 to = getfilectx(a, ctx1).data()
1499 else:
1502 else:
1500 losedatafn(f)
1503 losedatafn(f)
1501 else:
1504 else:
1502 if opts.git:
1505 if opts.git:
1503 header.append('new file mode %s\n' % mode)
1506 header.append('new file mode %s\n' % mode)
1504 elif ctx2.flags(f):
1507 elif ctx2.flags(f):
1505 losedatafn(f)
1508 losedatafn(f)
1506 if util.binary(tn):
1509 if util.binary(tn):
1507 if opts.git:
1510 if opts.git:
1508 dodiff = 'binary'
1511 dodiff = 'binary'
1509 else:
1512 else:
1510 losedatafn(f)
1513 losedatafn(f)
1511 if not opts.git and not tn:
1514 if not opts.git and not tn:
1512 # regular diffs cannot represent new empty file
1515 # regular diffs cannot represent new empty file
1513 losedatafn(f)
1516 losedatafn(f)
1514 elif f in removed:
1517 elif f in removed:
1515 if opts.git:
1518 if opts.git:
1516 # have we already reported a copy above?
1519 # have we already reported a copy above?
1517 if f in copy and copy[f] in added and copy[copy[f]] == f:
1520 if f in copy and copy[f] in added and copyto[copy[f]] == f:
1521 dodiff = False
1522 elif f in copyto and copyto[f] in added and copy[copyto[f]] == f:
1518 dodiff = False
1523 dodiff = False
1519 else:
1524 else:
1520 header.append('deleted file mode %s\n' %
1525 header.append('deleted file mode %s\n' %
1521 gitmode[man1.flags(f)])
1526 gitmode[man1.flags(f)])
1522 elif not to:
1527 elif not to:
1523 # regular diffs cannot represent empty file deletion
1528 # regular diffs cannot represent empty file deletion
1524 losedatafn(f)
1529 losedatafn(f)
1525 else:
1530 else:
1526 oflag = man1.flags(f)
1531 oflag = man1.flags(f)
1527 nflag = ctx2.flags(f)
1532 nflag = ctx2.flags(f)
1528 binary = util.binary(to) or util.binary(tn)
1533 binary = util.binary(to) or util.binary(tn)
1529 if opts.git:
1534 if opts.git:
1530 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1535 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1531 if binary:
1536 if binary:
1532 dodiff = 'binary'
1537 dodiff = 'binary'
1533 elif binary or nflag != oflag:
1538 elif binary or nflag != oflag:
1534 losedatafn(f)
1539 losedatafn(f)
1535 if opts.git:
1540 if opts.git:
1536 header.insert(0, mdiff.diffline(revs, a, b, opts))
1541 header.insert(0, mdiff.diffline(revs, a, b, opts))
1537
1542
1538 if dodiff:
1543 if dodiff:
1539 if dodiff == 'binary':
1544 if dodiff == 'binary':
1540 text = b85diff(to, tn)
1545 text = b85diff(to, tn)
1541 else:
1546 else:
1542 text = mdiff.unidiff(to, date1,
1547 text = mdiff.unidiff(to, date1,
1543 # ctx2 date may be dynamic
1548 # ctx2 date may be dynamic
1544 tn, util.datestr(ctx2.date()),
1549 tn, util.datestr(ctx2.date()),
1545 a, b, revs, opts=opts)
1550 a, b, revs, opts=opts)
1546 if header and (text or len(header) > 1):
1551 if header and (text or len(header) > 1):
1547 yield ''.join(header)
1552 yield ''.join(header)
1548 if text:
1553 if text:
1549 yield text
1554 yield text
1550
1555
1551 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1556 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1552 opts=None):
1557 opts=None):
1553 '''export changesets as hg patches.'''
1558 '''export changesets as hg patches.'''
1554
1559
1555 total = len(revs)
1560 total = len(revs)
1556 revwidth = max([len(str(rev)) for rev in revs])
1561 revwidth = max([len(str(rev)) for rev in revs])
1557
1562
1558 def single(rev, seqno, fp):
1563 def single(rev, seqno, fp):
1559 ctx = repo[rev]
1564 ctx = repo[rev]
1560 node = ctx.node()
1565 node = ctx.node()
1561 parents = [p.node() for p in ctx.parents() if p]
1566 parents = [p.node() for p in ctx.parents() if p]
1562 branch = ctx.branch()
1567 branch = ctx.branch()
1563 if switch_parent:
1568 if switch_parent:
1564 parents.reverse()
1569 parents.reverse()
1565 prev = (parents and parents[0]) or nullid
1570 prev = (parents and parents[0]) or nullid
1566
1571
1567 if not fp:
1572 if not fp:
1568 fp = cmdutil.make_file(repo, template, node, total=total,
1573 fp = cmdutil.make_file(repo, template, node, total=total,
1569 seqno=seqno, revwidth=revwidth,
1574 seqno=seqno, revwidth=revwidth,
1570 mode='ab')
1575 mode='ab')
1571 if fp != sys.stdout and hasattr(fp, 'name'):
1576 if fp != sys.stdout and hasattr(fp, 'name'):
1572 repo.ui.note("%s\n" % fp.name)
1577 repo.ui.note("%s\n" % fp.name)
1573
1578
1574 fp.write("# HG changeset patch\n")
1579 fp.write("# HG changeset patch\n")
1575 fp.write("# User %s\n" % ctx.user())
1580 fp.write("# User %s\n" % ctx.user())
1576 fp.write("# Date %d %d\n" % ctx.date())
1581 fp.write("# Date %d %d\n" % ctx.date())
1577 if branch and (branch != 'default'):
1582 if branch and (branch != 'default'):
1578 fp.write("# Branch %s\n" % branch)
1583 fp.write("# Branch %s\n" % branch)
1579 fp.write("# Node ID %s\n" % hex(node))
1584 fp.write("# Node ID %s\n" % hex(node))
1580 fp.write("# Parent %s\n" % hex(prev))
1585 fp.write("# Parent %s\n" % hex(prev))
1581 if len(parents) > 1:
1586 if len(parents) > 1:
1582 fp.write("# Parent %s\n" % hex(parents[1]))
1587 fp.write("# Parent %s\n" % hex(parents[1]))
1583 fp.write(ctx.description().rstrip())
1588 fp.write(ctx.description().rstrip())
1584 fp.write("\n\n")
1589 fp.write("\n\n")
1585
1590
1586 for chunk in diff(repo, prev, node, opts=opts):
1591 for chunk in diff(repo, prev, node, opts=opts):
1587 fp.write(chunk)
1592 fp.write(chunk)
1588
1593
1589 for seqno, rev in enumerate(revs):
1594 for seqno, rev in enumerate(revs):
1590 single(rev, seqno + 1, fp)
1595 single(rev, seqno + 1, fp)
1591
1596
1592 def diffstatdata(lines):
1597 def diffstatdata(lines):
1593 filename, adds, removes = None, 0, 0
1598 filename, adds, removes = None, 0, 0
1594 for line in lines:
1599 for line in lines:
1595 if line.startswith('diff'):
1600 if line.startswith('diff'):
1596 if filename:
1601 if filename:
1597 isbinary = adds == 0 and removes == 0
1602 isbinary = adds == 0 and removes == 0
1598 yield (filename, adds, removes, isbinary)
1603 yield (filename, adds, removes, isbinary)
1599 # set numbers to 0 anyway when starting new file
1604 # set numbers to 0 anyway when starting new file
1600 adds, removes = 0, 0
1605 adds, removes = 0, 0
1601 if line.startswith('diff --git'):
1606 if line.startswith('diff --git'):
1602 filename = gitre.search(line).group(1)
1607 filename = gitre.search(line).group(1)
1603 else:
1608 else:
1604 # format: "diff -r ... -r ... filename"
1609 # format: "diff -r ... -r ... filename"
1605 filename = line.split(None, 5)[-1]
1610 filename = line.split(None, 5)[-1]
1606 elif line.startswith('+') and not line.startswith('+++'):
1611 elif line.startswith('+') and not line.startswith('+++'):
1607 adds += 1
1612 adds += 1
1608 elif line.startswith('-') and not line.startswith('---'):
1613 elif line.startswith('-') and not line.startswith('---'):
1609 removes += 1
1614 removes += 1
1610 if filename:
1615 if filename:
1611 isbinary = adds == 0 and removes == 0
1616 isbinary = adds == 0 and removes == 0
1612 yield (filename, adds, removes, isbinary)
1617 yield (filename, adds, removes, isbinary)
1613
1618
1614 def diffstat(lines, width=80, git=False):
1619 def diffstat(lines, width=80, git=False):
1615 output = []
1620 output = []
1616 stats = list(diffstatdata(lines))
1621 stats = list(diffstatdata(lines))
1617
1622
1618 maxtotal, maxname = 0, 0
1623 maxtotal, maxname = 0, 0
1619 totaladds, totalremoves = 0, 0
1624 totaladds, totalremoves = 0, 0
1620 hasbinary = False
1625 hasbinary = False
1621 for filename, adds, removes, isbinary in stats:
1626 for filename, adds, removes, isbinary in stats:
1622 totaladds += adds
1627 totaladds += adds
1623 totalremoves += removes
1628 totalremoves += removes
1624 maxname = max(maxname, len(filename))
1629 maxname = max(maxname, len(filename))
1625 maxtotal = max(maxtotal, adds + removes)
1630 maxtotal = max(maxtotal, adds + removes)
1626 if isbinary:
1631 if isbinary:
1627 hasbinary = True
1632 hasbinary = True
1628
1633
1629 countwidth = len(str(maxtotal))
1634 countwidth = len(str(maxtotal))
1630 if hasbinary and countwidth < 3:
1635 if hasbinary and countwidth < 3:
1631 countwidth = 3
1636 countwidth = 3
1632 graphwidth = width - countwidth - maxname - 6
1637 graphwidth = width - countwidth - maxname - 6
1633 if graphwidth < 10:
1638 if graphwidth < 10:
1634 graphwidth = 10
1639 graphwidth = 10
1635
1640
1636 def scale(i):
1641 def scale(i):
1637 if maxtotal <= graphwidth:
1642 if maxtotal <= graphwidth:
1638 return i
1643 return i
1639 # If diffstat runs out of room it doesn't print anything,
1644 # If diffstat runs out of room it doesn't print anything,
1640 # which isn't very useful, so always print at least one + or -
1645 # which isn't very useful, so always print at least one + or -
1641 # if there were at least some changes.
1646 # if there were at least some changes.
1642 return max(i * graphwidth // maxtotal, int(bool(i)))
1647 return max(i * graphwidth // maxtotal, int(bool(i)))
1643
1648
1644 for filename, adds, removes, isbinary in stats:
1649 for filename, adds, removes, isbinary in stats:
1645 if git and isbinary:
1650 if git and isbinary:
1646 count = 'Bin'
1651 count = 'Bin'
1647 else:
1652 else:
1648 count = adds + removes
1653 count = adds + removes
1649 pluses = '+' * scale(adds)
1654 pluses = '+' * scale(adds)
1650 minuses = '-' * scale(removes)
1655 minuses = '-' * scale(removes)
1651 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1656 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1652 count, pluses, minuses))
1657 count, pluses, minuses))
1653
1658
1654 if stats:
1659 if stats:
1655 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1660 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1656 % (len(stats), totaladds, totalremoves))
1661 % (len(stats), totaladds, totalremoves))
1657
1662
1658 return ''.join(output)
1663 return ''.join(output)
@@ -1,241 +1,248 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 hg init
3 hg init
4 mkdir d1 d1/d11 d2
4 mkdir d1 d1/d11 d2
5 echo d1/a > d1/a
5 echo d1/a > d1/a
6 echo d1/ba > d1/ba
6 echo d1/ba > d1/ba
7 echo d1/a1 > d1/d11/a1
7 echo d1/a1 > d1/d11/a1
8 echo d1/b > d1/b
8 echo d1/b > d1/b
9 echo d2/b > d2/b
9 echo d2/b > d2/b
10 hg add d1/a d1/b d1/ba d1/d11/a1 d2/b
10 hg add d1/a d1/b d1/ba d1/d11/a1 d2/b
11 hg commit -m "1" -d "1000000 0"
11 hg commit -m "1" -d "1000000 0"
12
12
13 echo "# rename a single file"
13 echo "# rename a single file"
14 hg rename d1/d11/a1 d2/c
14 hg rename d1/d11/a1 d2/c
15 hg status -C
15 hg status -C
16 hg update -C
16 hg update -C
17 rm d2/c
17 rm d2/c
18
18
19 echo "# rename --after a single file"
19 echo "# rename --after a single file"
20 mv d1/d11/a1 d2/c
20 mv d1/d11/a1 d2/c
21 hg rename --after d1/d11/a1 d2/c
21 hg rename --after d1/d11/a1 d2/c
22 hg status -C
22 hg status -C
23 hg update -C
23 hg update -C
24 rm d2/c
24 rm d2/c
25
25
26 echo "# move a single file to an existing directory"
26 echo "# move a single file to an existing directory"
27 hg rename d1/d11/a1 d2
27 hg rename d1/d11/a1 d2
28 hg status -C
28 hg status -C
29 hg update -C
29 hg update -C
30 rm d2/a1
30 rm d2/a1
31
31
32 echo "# move --after a single file to an existing directory"
32 echo "# move --after a single file to an existing directory"
33 mv d1/d11/a1 d2
33 mv d1/d11/a1 d2
34 hg rename --after d1/d11/a1 d2
34 hg rename --after d1/d11/a1 d2
35 hg status -C
35 hg status -C
36 hg update -C
36 hg update -C
37 rm d2/a1
37 rm d2/a1
38
38
39 echo "# rename a file using a relative path"
39 echo "# rename a file using a relative path"
40 (cd d1/d11; hg rename ../../d2/b e)
40 (cd d1/d11; hg rename ../../d2/b e)
41 hg status -C
41 hg status -C
42 hg update -C
42 hg update -C
43 rm d1/d11/e
43 rm d1/d11/e
44
44
45 echo "# rename --after a file using a relative path"
45 echo "# rename --after a file using a relative path"
46 (cd d1/d11; mv ../../d2/b e; hg rename --after ../../d2/b e)
46 (cd d1/d11; mv ../../d2/b e; hg rename --after ../../d2/b e)
47 hg status -C
47 hg status -C
48 hg update -C
48 hg update -C
49 rm d1/d11/e
49 rm d1/d11/e
50
50
51 echo "# rename directory d1 as d3"
51 echo "# rename directory d1 as d3"
52 hg rename d1/ d3
52 hg rename d1/ d3
53 hg status -C
53 hg status -C
54 hg update -C
54 hg update -C
55 rm -rf d3
55 rm -rf d3
56
56
57 echo "# rename --after directory d1 as d3"
57 echo "# rename --after directory d1 as d3"
58 mv d1 d3
58 mv d1 d3
59 hg rename --after d1 d3
59 hg rename --after d1 d3
60 hg status -C
60 hg status -C
61 hg update -C
61 hg update -C
62 rm -rf d3
62 rm -rf d3
63
63
64 echo "# move a directory using a relative path"
64 echo "# move a directory using a relative path"
65 (cd d2; mkdir d3; hg rename ../d1/d11 d3)
65 (cd d2; mkdir d3; hg rename ../d1/d11 d3)
66 hg status -C
66 hg status -C
67 hg update -C
67 hg update -C
68 rm -rf d2/d3
68 rm -rf d2/d3
69
69
70 echo "# move --after a directory using a relative path"
70 echo "# move --after a directory using a relative path"
71 (cd d2; mkdir d3; mv ../d1/d11 d3; hg rename --after ../d1/d11 d3)
71 (cd d2; mkdir d3; mv ../d1/d11 d3; hg rename --after ../d1/d11 d3)
72 hg status -C
72 hg status -C
73 hg update -C
73 hg update -C
74 rm -rf d2/d3
74 rm -rf d2/d3
75
75
76 echo "# move directory d1/d11 to an existing directory d2 (removes empty d1)"
76 echo "# move directory d1/d11 to an existing directory d2 (removes empty d1)"
77 hg rename d1/d11/ d2
77 hg rename d1/d11/ d2
78 hg status -C
78 hg status -C
79 hg update -C
79 hg update -C
80 rm -rf d2/d11
80 rm -rf d2/d11
81
81
82 echo "# move directories d1 and d2 to a new directory d3"
82 echo "# move directories d1 and d2 to a new directory d3"
83 mkdir d3
83 mkdir d3
84 hg rename d1 d2 d3
84 hg rename d1 d2 d3
85 hg status -C
85 hg status -C
86 hg update -C
86 hg update -C
87 rm -rf d3
87 rm -rf d3
88
88
89 echo "# move --after directories d1 and d2 to a new directory d3"
89 echo "# move --after directories d1 and d2 to a new directory d3"
90 mkdir d3
90 mkdir d3
91 mv d1 d2 d3
91 mv d1 d2 d3
92 hg rename --after d1 d2 d3
92 hg rename --after d1 d2 d3
93 hg status -C
93 hg status -C
94 hg update -C
94 hg update -C
95 rm -rf d3
95 rm -rf d3
96
96
97 echo "# move everything under directory d1 to existing directory d2, do not"
97 echo "# move everything under directory d1 to existing directory d2, do not"
98 echo "# overwrite existing files (d2/b)"
98 echo "# overwrite existing files (d2/b)"
99 hg rename d1/* d2
99 hg rename d1/* d2
100 hg status -C
100 hg status -C
101 diff d1/b d2/b
101 diff d1/b d2/b
102 hg update -C
102 hg update -C
103 rm d2/a d2/ba d2/d11/a1
103 rm d2/a d2/ba d2/d11/a1
104
104
105 echo "# attempt to move one file into a non-existent directory"
105 echo "# attempt to move one file into a non-existent directory"
106 hg rename d1/a dx/
106 hg rename d1/a dx/
107 hg status -C
107 hg status -C
108 hg update -C
108 hg update -C
109
109
110 echo "# attempt to move potentially more than one file into a non-existent"
110 echo "# attempt to move potentially more than one file into a non-existent"
111 echo "# directory"
111 echo "# directory"
112 hg rename 'glob:d1/**' dx
112 hg rename 'glob:d1/**' dx
113
113
114 echo "# move every file under d1 to d2/d21 (glob)"
114 echo "# move every file under d1 to d2/d21 (glob)"
115 mkdir d2/d21
115 mkdir d2/d21
116 hg rename 'glob:d1/**' d2/d21
116 hg rename 'glob:d1/**' d2/d21
117 hg status -C
117 hg status -C
118 hg update -C
118 hg update -C
119 rm -rf d2/d21
119 rm -rf d2/d21
120
120
121 echo "# move --after some files under d1 to d2/d21 (glob)"
121 echo "# move --after some files under d1 to d2/d21 (glob)"
122 mkdir d2/d21
122 mkdir d2/d21
123 mv d1/a d1/d11/a1 d2/d21
123 mv d1/a d1/d11/a1 d2/d21
124 hg rename --after 'glob:d1/**' d2/d21
124 hg rename --after 'glob:d1/**' d2/d21
125 hg status -C
125 hg status -C
126 hg update -C
126 hg update -C
127 rm -rf d2/d21
127 rm -rf d2/d21
128
128
129 echo "# move every file under d1 starting with an 'a' to d2/d21 (regexp)"
129 echo "# move every file under d1 starting with an 'a' to d2/d21 (regexp)"
130 mkdir d2/d21
130 mkdir d2/d21
131 hg rename 're:d1/([^a][^/]*/)*a.*' d2/d21
131 hg rename 're:d1/([^a][^/]*/)*a.*' d2/d21
132 hg status -C
132 hg status -C
133 hg update -C
133 hg update -C
134 rm -rf d2/d21
134 rm -rf d2/d21
135
135
136 echo "# attempt to overwrite an existing file"
136 echo "# attempt to overwrite an existing file"
137 echo "ca" > d1/ca
137 echo "ca" > d1/ca
138 hg rename d1/ba d1/ca
138 hg rename d1/ba d1/ca
139 hg status -C
139 hg status -C
140 hg update -C
140 hg update -C
141
141
142 echo "# forced overwrite of an existing file"
142 echo "# forced overwrite of an existing file"
143 echo "ca" > d1/ca
143 echo "ca" > d1/ca
144 hg rename --force d1/ba d1/ca
144 hg rename --force d1/ba d1/ca
145 hg status -C
145 hg status -C
146 hg update -C
146 hg update -C
147 rm d1/ca
147 rm d1/ca
148
148
149 echo "# replace a symlink with a file"
149 echo "# replace a symlink with a file"
150 ln -s ba d1/ca
150 ln -s ba d1/ca
151 hg rename --force d1/ba d1/ca
151 hg rename --force d1/ba d1/ca
152 hg status -C
152 hg status -C
153 hg update -C
153 hg update -C
154 rm d1/ca
154 rm d1/ca
155
155
156 echo "# do not copy more than one source file to the same destination file"
156 echo "# do not copy more than one source file to the same destination file"
157 mkdir d3
157 mkdir d3
158 hg rename d1/* d2/* d3
158 hg rename d1/* d2/* d3
159 hg status -C
159 hg status -C
160 hg update -C
160 hg update -C
161 rm -rf d3
161 rm -rf d3
162
162
163 echo "# move a whole subtree with \"hg rename .\""
163 echo "# move a whole subtree with \"hg rename .\""
164 mkdir d3
164 mkdir d3
165 (cd d1; hg rename . ../d3)
165 (cd d1; hg rename . ../d3)
166 hg status -C
166 hg status -C
167 hg update -C
167 hg update -C
168 rm -rf d3
168 rm -rf d3
169
169
170 echo "# move a whole subtree with \"hg rename --after .\""
170 echo "# move a whole subtree with \"hg rename --after .\""
171 mkdir d3
171 mkdir d3
172 mv d1/* d3
172 mv d1/* d3
173 (cd d1; hg rename --after . ../d3)
173 (cd d1; hg rename --after . ../d3)
174 hg status -C
174 hg status -C
175 hg update -C
175 hg update -C
176 rm -rf d3
176 rm -rf d3
177
177
178 echo "# move the parent tree with \"hg rename ..\""
178 echo "# move the parent tree with \"hg rename ..\""
179 (cd d1/d11; hg rename .. ../../d3)
179 (cd d1/d11; hg rename .. ../../d3)
180 hg status -C
180 hg status -C
181 hg update -C
181 hg update -C
182 rm -rf d3
182 rm -rf d3
183
183
184 echo "# skip removed files"
184 echo "# skip removed files"
185 hg remove d1/b
185 hg remove d1/b
186 hg rename d1 d3
186 hg rename d1 d3
187 hg status -C
187 hg status -C
188 hg update -C
188 hg update -C
189 rm -rf d3
189 rm -rf d3
190
190
191 echo "# transitive rename"
191 echo "# transitive rename"
192 hg rename d1/b d1/bb
192 hg rename d1/b d1/bb
193 hg rename d1/bb d1/bc
193 hg rename d1/bb d1/bc
194 hg status -C
194 hg status -C
195 hg update -C
195 hg update -C
196 rm d1/bc
196 rm d1/bc
197
197
198 echo "# transitive rename --after"
198 echo "# transitive rename --after"
199 hg rename d1/b d1/bb
199 hg rename d1/b d1/bb
200 mv d1/bb d1/bc
200 mv d1/bb d1/bc
201 hg rename --after d1/bb d1/bc
201 hg rename --after d1/bb d1/bc
202 hg status -C
202 hg status -C
203 hg update -C
203 hg update -C
204 rm d1/bc
204 rm d1/bc
205
205
206 echo "# idempotent renames (d1/b -> d1/bb followed by d1/bb -> d1/b)"
206 echo "# idempotent renames (d1/b -> d1/bb followed by d1/bb -> d1/b)"
207 hg rename d1/b d1/bb
207 hg rename d1/b d1/bb
208 echo "some stuff added to d1/bb" >> d1/bb
208 echo "some stuff added to d1/bb" >> d1/bb
209 hg rename d1/bb d1/b
209 hg rename d1/bb d1/b
210 hg status -C
210 hg status -C
211 hg update -C
211 hg update -C
212
212
213 echo '# overwriting with renames (issue1959)'
214 hg rename d1/a d1/c
215 hg rename d1/b d1/a
216 hg status -C
217 hg diff --git
218 hg update -C
219
213 echo "# check illegal path components"
220 echo "# check illegal path components"
214
221
215 hg rename d1/d11/a1 .hg/foo
222 hg rename d1/d11/a1 .hg/foo
216 hg status -C
223 hg status -C
217 hg rename d1/d11/a1 ../foo
224 hg rename d1/d11/a1 ../foo
218 hg status -C
225 hg status -C
219
226
220 mv d1/d11/a1 .hg/foo
227 mv d1/d11/a1 .hg/foo
221 hg rename --after d1/d11/a1 .hg/foo
228 hg rename --after d1/d11/a1 .hg/foo
222 hg status -C
229 hg status -C
223 hg update -C
230 hg update -C
224 rm .hg/foo
231 rm .hg/foo
225
232
226 hg rename d1/d11/a1 .hg
233 hg rename d1/d11/a1 .hg
227 hg status -C
234 hg status -C
228 hg rename d1/d11/a1 ..
235 hg rename d1/d11/a1 ..
229 hg status -C
236 hg status -C
230
237
231 mv d1/d11/a1 .hg
238 mv d1/d11/a1 .hg
232 hg rename --after d1/d11/a1 .hg
239 hg rename --after d1/d11/a1 .hg
233 hg status -C
240 hg status -C
234 hg update -C
241 hg update -C
235 rm .hg/a1
242 rm .hg/a1
236
243
237 (cd d1/d11; hg rename ../../d2/b ../../.hg/foo)
244 (cd d1/d11; hg rename ../../d2/b ../../.hg/foo)
238 hg status -C
245 hg status -C
239 (cd d1/d11; hg rename ../../d2/b ../../../foo)
246 (cd d1/d11; hg rename ../../d2/b ../../../foo)
240 hg status -C
247 hg status -C
241
248
@@ -1,315 +1,328 b''
1 # rename a single file
1 # rename a single file
2 A d2/c
2 A d2/c
3 d1/d11/a1
3 d1/d11/a1
4 R d1/d11/a1
4 R d1/d11/a1
5 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
5 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
6 # rename --after a single file
6 # rename --after a single file
7 A d2/c
7 A d2/c
8 d1/d11/a1
8 d1/d11/a1
9 R d1/d11/a1
9 R d1/d11/a1
10 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
10 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
11 # move a single file to an existing directory
11 # move a single file to an existing directory
12 A d2/a1
12 A d2/a1
13 d1/d11/a1
13 d1/d11/a1
14 R d1/d11/a1
14 R d1/d11/a1
15 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
15 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
16 # move --after a single file to an existing directory
16 # move --after a single file to an existing directory
17 A d2/a1
17 A d2/a1
18 d1/d11/a1
18 d1/d11/a1
19 R d1/d11/a1
19 R d1/d11/a1
20 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
20 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
21 # rename a file using a relative path
21 # rename a file using a relative path
22 A d1/d11/e
22 A d1/d11/e
23 d2/b
23 d2/b
24 R d2/b
24 R d2/b
25 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
25 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
26 # rename --after a file using a relative path
26 # rename --after a file using a relative path
27 A d1/d11/e
27 A d1/d11/e
28 d2/b
28 d2/b
29 R d2/b
29 R d2/b
30 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
30 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
31 # rename directory d1 as d3
31 # rename directory d1 as d3
32 moving d1/a to d3/a
32 moving d1/a to d3/a
33 moving d1/b to d3/b
33 moving d1/b to d3/b
34 moving d1/ba to d3/ba
34 moving d1/ba to d3/ba
35 moving d1/d11/a1 to d3/d11/a1
35 moving d1/d11/a1 to d3/d11/a1
36 A d3/a
36 A d3/a
37 d1/a
37 d1/a
38 A d3/b
38 A d3/b
39 d1/b
39 d1/b
40 A d3/ba
40 A d3/ba
41 d1/ba
41 d1/ba
42 A d3/d11/a1
42 A d3/d11/a1
43 d1/d11/a1
43 d1/d11/a1
44 R d1/a
44 R d1/a
45 R d1/b
45 R d1/b
46 R d1/ba
46 R d1/ba
47 R d1/d11/a1
47 R d1/d11/a1
48 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
48 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
49 # rename --after directory d1 as d3
49 # rename --after directory d1 as d3
50 moving d1/a to d3/a
50 moving d1/a to d3/a
51 moving d1/b to d3/b
51 moving d1/b to d3/b
52 moving d1/ba to d3/ba
52 moving d1/ba to d3/ba
53 moving d1/d11/a1 to d3/d11/a1
53 moving d1/d11/a1 to d3/d11/a1
54 A d3/a
54 A d3/a
55 d1/a
55 d1/a
56 A d3/b
56 A d3/b
57 d1/b
57 d1/b
58 A d3/ba
58 A d3/ba
59 d1/ba
59 d1/ba
60 A d3/d11/a1
60 A d3/d11/a1
61 d1/d11/a1
61 d1/d11/a1
62 R d1/a
62 R d1/a
63 R d1/b
63 R d1/b
64 R d1/ba
64 R d1/ba
65 R d1/d11/a1
65 R d1/d11/a1
66 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 # move a directory using a relative path
67 # move a directory using a relative path
68 moving ../d1/d11/a1 to d3/d11/a1
68 moving ../d1/d11/a1 to d3/d11/a1
69 A d2/d3/d11/a1
69 A d2/d3/d11/a1
70 d1/d11/a1
70 d1/d11/a1
71 R d1/d11/a1
71 R d1/d11/a1
72 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
72 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
73 # move --after a directory using a relative path
73 # move --after a directory using a relative path
74 moving ../d1/d11/a1 to d3/d11/a1
74 moving ../d1/d11/a1 to d3/d11/a1
75 A d2/d3/d11/a1
75 A d2/d3/d11/a1
76 d1/d11/a1
76 d1/d11/a1
77 R d1/d11/a1
77 R d1/d11/a1
78 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 # move directory d1/d11 to an existing directory d2 (removes empty d1)
79 # move directory d1/d11 to an existing directory d2 (removes empty d1)
80 moving d1/d11/a1 to d2/d11/a1
80 moving d1/d11/a1 to d2/d11/a1
81 A d2/d11/a1
81 A d2/d11/a1
82 d1/d11/a1
82 d1/d11/a1
83 R d1/d11/a1
83 R d1/d11/a1
84 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
84 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 # move directories d1 and d2 to a new directory d3
85 # move directories d1 and d2 to a new directory d3
86 moving d1/a to d3/d1/a
86 moving d1/a to d3/d1/a
87 moving d1/b to d3/d1/b
87 moving d1/b to d3/d1/b
88 moving d1/ba to d3/d1/ba
88 moving d1/ba to d3/d1/ba
89 moving d1/d11/a1 to d3/d1/d11/a1
89 moving d1/d11/a1 to d3/d1/d11/a1
90 moving d2/b to d3/d2/b
90 moving d2/b to d3/d2/b
91 A d3/d1/a
91 A d3/d1/a
92 d1/a
92 d1/a
93 A d3/d1/b
93 A d3/d1/b
94 d1/b
94 d1/b
95 A d3/d1/ba
95 A d3/d1/ba
96 d1/ba
96 d1/ba
97 A d3/d1/d11/a1
97 A d3/d1/d11/a1
98 d1/d11/a1
98 d1/d11/a1
99 A d3/d2/b
99 A d3/d2/b
100 d2/b
100 d2/b
101 R d1/a
101 R d1/a
102 R d1/b
102 R d1/b
103 R d1/ba
103 R d1/ba
104 R d1/d11/a1
104 R d1/d11/a1
105 R d2/b
105 R d2/b
106 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
106 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
107 # move --after directories d1 and d2 to a new directory d3
107 # move --after directories d1 and d2 to a new directory d3
108 moving d1/a to d3/d1/a
108 moving d1/a to d3/d1/a
109 moving d1/b to d3/d1/b
109 moving d1/b to d3/d1/b
110 moving d1/ba to d3/d1/ba
110 moving d1/ba to d3/d1/ba
111 moving d1/d11/a1 to d3/d1/d11/a1
111 moving d1/d11/a1 to d3/d1/d11/a1
112 moving d2/b to d3/d2/b
112 moving d2/b to d3/d2/b
113 A d3/d1/a
113 A d3/d1/a
114 d1/a
114 d1/a
115 A d3/d1/b
115 A d3/d1/b
116 d1/b
116 d1/b
117 A d3/d1/ba
117 A d3/d1/ba
118 d1/ba
118 d1/ba
119 A d3/d1/d11/a1
119 A d3/d1/d11/a1
120 d1/d11/a1
120 d1/d11/a1
121 A d3/d2/b
121 A d3/d2/b
122 d2/b
122 d2/b
123 R d1/a
123 R d1/a
124 R d1/b
124 R d1/b
125 R d1/ba
125 R d1/ba
126 R d1/d11/a1
126 R d1/d11/a1
127 R d2/b
127 R d2/b
128 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
128 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
129 # move everything under directory d1 to existing directory d2, do not
129 # move everything under directory d1 to existing directory d2, do not
130 # overwrite existing files (d2/b)
130 # overwrite existing files (d2/b)
131 d2/b: not overwriting - file exists
131 d2/b: not overwriting - file exists
132 moving d1/d11/a1 to d2/d11/a1
132 moving d1/d11/a1 to d2/d11/a1
133 A d2/a
133 A d2/a
134 d1/a
134 d1/a
135 A d2/ba
135 A d2/ba
136 d1/ba
136 d1/ba
137 A d2/d11/a1
137 A d2/d11/a1
138 d1/d11/a1
138 d1/d11/a1
139 R d1/a
139 R d1/a
140 R d1/ba
140 R d1/ba
141 R d1/d11/a1
141 R d1/d11/a1
142 1c1
142 1c1
143 < d1/b
143 < d1/b
144 ---
144 ---
145 > d2/b
145 > d2/b
146 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
146 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
147 # attempt to move one file into a non-existent directory
147 # attempt to move one file into a non-existent directory
148 abort: destination dx/ is not a directory
148 abort: destination dx/ is not a directory
149 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
149 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
150 # attempt to move potentially more than one file into a non-existent
150 # attempt to move potentially more than one file into a non-existent
151 # directory
151 # directory
152 abort: with multiple sources, destination must be an existing directory
152 abort: with multiple sources, destination must be an existing directory
153 # move every file under d1 to d2/d21 (glob)
153 # move every file under d1 to d2/d21 (glob)
154 moving d1/a to d2/d21/a
154 moving d1/a to d2/d21/a
155 moving d1/b to d2/d21/b
155 moving d1/b to d2/d21/b
156 moving d1/ba to d2/d21/ba
156 moving d1/ba to d2/d21/ba
157 moving d1/d11/a1 to d2/d21/a1
157 moving d1/d11/a1 to d2/d21/a1
158 A d2/d21/a
158 A d2/d21/a
159 d1/a
159 d1/a
160 A d2/d21/a1
160 A d2/d21/a1
161 d1/d11/a1
161 d1/d11/a1
162 A d2/d21/b
162 A d2/d21/b
163 d1/b
163 d1/b
164 A d2/d21/ba
164 A d2/d21/ba
165 d1/ba
165 d1/ba
166 R d1/a
166 R d1/a
167 R d1/b
167 R d1/b
168 R d1/ba
168 R d1/ba
169 R d1/d11/a1
169 R d1/d11/a1
170 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
170 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
171 # move --after some files under d1 to d2/d21 (glob)
171 # move --after some files under d1 to d2/d21 (glob)
172 moving d1/a to d2/d21/a
172 moving d1/a to d2/d21/a
173 moving d1/d11/a1 to d2/d21/a1
173 moving d1/d11/a1 to d2/d21/a1
174 A d2/d21/a
174 A d2/d21/a
175 d1/a
175 d1/a
176 A d2/d21/a1
176 A d2/d21/a1
177 d1/d11/a1
177 d1/d11/a1
178 R d1/a
178 R d1/a
179 R d1/d11/a1
179 R d1/d11/a1
180 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
180 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
181 # move every file under d1 starting with an 'a' to d2/d21 (regexp)
181 # move every file under d1 starting with an 'a' to d2/d21 (regexp)
182 moving d1/a to d2/d21/a
182 moving d1/a to d2/d21/a
183 moving d1/d11/a1 to d2/d21/a1
183 moving d1/d11/a1 to d2/d21/a1
184 A d2/d21/a
184 A d2/d21/a
185 d1/a
185 d1/a
186 A d2/d21/a1
186 A d2/d21/a1
187 d1/d11/a1
187 d1/d11/a1
188 R d1/a
188 R d1/a
189 R d1/d11/a1
189 R d1/d11/a1
190 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
190 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
191 # attempt to overwrite an existing file
191 # attempt to overwrite an existing file
192 d1/ca: not overwriting - file exists
192 d1/ca: not overwriting - file exists
193 ? d1/ca
193 ? d1/ca
194 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
194 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
195 # forced overwrite of an existing file
195 # forced overwrite of an existing file
196 A d1/ca
196 A d1/ca
197 d1/ba
197 d1/ba
198 R d1/ba
198 R d1/ba
199 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
199 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
200 # replace a symlink with a file
200 # replace a symlink with a file
201 A d1/ca
201 A d1/ca
202 d1/ba
202 d1/ba
203 R d1/ba
203 R d1/ba
204 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
204 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
205 # do not copy more than one source file to the same destination file
205 # do not copy more than one source file to the same destination file
206 moving d1/d11/a1 to d3/d11/a1
206 moving d1/d11/a1 to d3/d11/a1
207 d3/b: not overwriting - d2/b collides with d1/b
207 d3/b: not overwriting - d2/b collides with d1/b
208 A d3/a
208 A d3/a
209 d1/a
209 d1/a
210 A d3/b
210 A d3/b
211 d1/b
211 d1/b
212 A d3/ba
212 A d3/ba
213 d1/ba
213 d1/ba
214 A d3/d11/a1
214 A d3/d11/a1
215 d1/d11/a1
215 d1/d11/a1
216 R d1/a
216 R d1/a
217 R d1/b
217 R d1/b
218 R d1/ba
218 R d1/ba
219 R d1/d11/a1
219 R d1/d11/a1
220 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
220 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
221 # move a whole subtree with "hg rename ."
221 # move a whole subtree with "hg rename ."
222 moving a to ../d3/d1/a
222 moving a to ../d3/d1/a
223 moving b to ../d3/d1/b
223 moving b to ../d3/d1/b
224 moving ba to ../d3/d1/ba
224 moving ba to ../d3/d1/ba
225 moving d11/a1 to ../d3/d1/d11/a1
225 moving d11/a1 to ../d3/d1/d11/a1
226 A d3/d1/a
226 A d3/d1/a
227 d1/a
227 d1/a
228 A d3/d1/b
228 A d3/d1/b
229 d1/b
229 d1/b
230 A d3/d1/ba
230 A d3/d1/ba
231 d1/ba
231 d1/ba
232 A d3/d1/d11/a1
232 A d3/d1/d11/a1
233 d1/d11/a1
233 d1/d11/a1
234 R d1/a
234 R d1/a
235 R d1/b
235 R d1/b
236 R d1/ba
236 R d1/ba
237 R d1/d11/a1
237 R d1/d11/a1
238 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
238 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
239 # move a whole subtree with "hg rename --after ."
239 # move a whole subtree with "hg rename --after ."
240 moving a to ../d3/a
240 moving a to ../d3/a
241 moving b to ../d3/b
241 moving b to ../d3/b
242 moving ba to ../d3/ba
242 moving ba to ../d3/ba
243 moving d11/a1 to ../d3/d11/a1
243 moving d11/a1 to ../d3/d11/a1
244 A d3/a
244 A d3/a
245 d1/a
245 d1/a
246 A d3/b
246 A d3/b
247 d1/b
247 d1/b
248 A d3/ba
248 A d3/ba
249 d1/ba
249 d1/ba
250 A d3/d11/a1
250 A d3/d11/a1
251 d1/d11/a1
251 d1/d11/a1
252 R d1/a
252 R d1/a
253 R d1/b
253 R d1/b
254 R d1/ba
254 R d1/ba
255 R d1/d11/a1
255 R d1/d11/a1
256 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
256 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
257 # move the parent tree with "hg rename .."
257 # move the parent tree with "hg rename .."
258 moving ../a to ../../d3/a
258 moving ../a to ../../d3/a
259 moving ../b to ../../d3/b
259 moving ../b to ../../d3/b
260 moving ../ba to ../../d3/ba
260 moving ../ba to ../../d3/ba
261 moving a1 to ../../d3/d11/a1
261 moving a1 to ../../d3/d11/a1
262 A d3/a
262 A d3/a
263 d1/a
263 d1/a
264 A d3/b
264 A d3/b
265 d1/b
265 d1/b
266 A d3/ba
266 A d3/ba
267 d1/ba
267 d1/ba
268 A d3/d11/a1
268 A d3/d11/a1
269 d1/d11/a1
269 d1/d11/a1
270 R d1/a
270 R d1/a
271 R d1/b
271 R d1/b
272 R d1/ba
272 R d1/ba
273 R d1/d11/a1
273 R d1/d11/a1
274 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
274 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
275 # skip removed files
275 # skip removed files
276 moving d1/a to d3/a
276 moving d1/a to d3/a
277 moving d1/ba to d3/ba
277 moving d1/ba to d3/ba
278 moving d1/d11/a1 to d3/d11/a1
278 moving d1/d11/a1 to d3/d11/a1
279 A d3/a
279 A d3/a
280 d1/a
280 d1/a
281 A d3/ba
281 A d3/ba
282 d1/ba
282 d1/ba
283 A d3/d11/a1
283 A d3/d11/a1
284 d1/d11/a1
284 d1/d11/a1
285 R d1/a
285 R d1/a
286 R d1/b
286 R d1/b
287 R d1/ba
287 R d1/ba
288 R d1/d11/a1
288 R d1/d11/a1
289 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
289 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 # transitive rename
290 # transitive rename
291 A d1/bc
291 A d1/bc
292 d1/b
292 d1/b
293 R d1/b
293 R d1/b
294 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
294 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
295 # transitive rename --after
295 # transitive rename --after
296 A d1/bc
296 A d1/bc
297 d1/b
297 d1/b
298 R d1/b
298 R d1/b
299 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
299 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
300 # idempotent renames (d1/b -> d1/bb followed by d1/bb -> d1/b)
300 # idempotent renames (d1/b -> d1/bb followed by d1/bb -> d1/b)
301 M d1/b
301 M d1/b
302 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
302 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
303 # overwriting with renames (issue1959)
304 A d1/a
305 d1/b
306 A d1/c
307 d1/a
308 R d1/b
309 diff --git a/d1/b b/d1/a
310 rename from d1/b
311 rename to d1/a
312 diff --git a/d1/a b/d1/c
313 copy from d1/a
314 copy to d1/c
315 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
303 # check illegal path components
316 # check illegal path components
304 abort: path contains illegal component: .hg/foo
317 abort: path contains illegal component: .hg/foo
305 abort: ../foo not under root
318 abort: ../foo not under root
306 abort: path contains illegal component: .hg/foo
319 abort: path contains illegal component: .hg/foo
307 ! d1/d11/a1
320 ! d1/d11/a1
308 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
321 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
309 abort: path contains illegal component: .hg/a1
322 abort: path contains illegal component: .hg/a1
310 abort: ../a1 not under root
323 abort: ../a1 not under root
311 abort: path contains illegal component: .hg/a1
324 abort: path contains illegal component: .hg/a1
312 ! d1/d11/a1
325 ! d1/d11/a1
313 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
326 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
314 abort: path contains illegal component: .hg/foo
327 abort: path contains illegal component: .hg/foo
315 abort: ../../../foo not under root
328 abort: ../../../foo not under root
General Comments 0
You need to be logged in to leave comments. Login now