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