##// END OF EJS Templates
patch: minor cleanup of _applydiff
Mads Kiilerich -
r11021:c47a1cfa default
parent child Browse files
Show More
@@ -1,1709 +1,1706 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, cmdutil, mdiff, util, diffhelpers, copies
14 import base85, cmdutil, mdiff, util, diffhelpers, copies
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.exists(absdst):
28 if os.path.exists(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, node, p1, p2).
177 return tuple (filename, message, user, date, 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. updatedir() will -too magically- take care of
447 # a symlink. updatedir() will -too magically- take care of
448 # setting it to the proper type afterwards.
448 # 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 write(self, dest=None):
530 def write(self, dest=None):
531 if not self.dirty:
531 if not self.dirty:
532 return
532 return
533 if not dest:
533 if not dest:
534 dest = self.fname
534 dest = self.fname
535 self.writelines(dest, self.lines)
535 self.writelines(dest, self.lines)
536
536
537 def close(self):
537 def close(self):
538 self.write()
538 self.write()
539 self.write_rej()
539 self.write_rej()
540
540
541 def apply(self, h):
541 def apply(self, h):
542 if not h.complete():
542 if not h.complete():
543 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
543 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
544 (h.number, h.desc, len(h.a), h.lena, len(h.b),
544 (h.number, h.desc, len(h.a), h.lena, len(h.b),
545 h.lenb))
545 h.lenb))
546
546
547 self.hunks += 1
547 self.hunks += 1
548
548
549 if self.missing:
549 if self.missing:
550 self.rej.append(h)
550 self.rej.append(h)
551 return -1
551 return -1
552
552
553 if self.exists and h.createfile():
553 if self.exists and h.createfile():
554 self.ui.warn(_("file %s already exists\n") % self.fname)
554 self.ui.warn(_("file %s already exists\n") % self.fname)
555 self.rej.append(h)
555 self.rej.append(h)
556 return -1
556 return -1
557
557
558 if isinstance(h, binhunk):
558 if isinstance(h, binhunk):
559 if h.rmfile():
559 if h.rmfile():
560 self.unlink(self.fname)
560 self.unlink(self.fname)
561 else:
561 else:
562 self.lines[:] = h.new()
562 self.lines[:] = h.new()
563 self.offset += len(h.new())
563 self.offset += len(h.new())
564 self.dirty = 1
564 self.dirty = 1
565 return 0
565 return 0
566
566
567 horig = h
567 horig = h
568 if (self.eolmode in ('crlf', 'lf')
568 if (self.eolmode in ('crlf', 'lf')
569 or self.eolmode == 'auto' and self.eol):
569 or self.eolmode == 'auto' and self.eol):
570 # If new eols are going to be normalized, then normalize
570 # If new eols are going to be normalized, then normalize
571 # hunk data before patching. Otherwise, preserve input
571 # hunk data before patching. Otherwise, preserve input
572 # line-endings.
572 # line-endings.
573 h = h.getnormalized()
573 h = h.getnormalized()
574
574
575 # fast case first, no offsets, no fuzz
575 # fast case first, no offsets, no fuzz
576 old = h.old()
576 old = h.old()
577 # patch starts counting at 1 unless we are adding the file
577 # patch starts counting at 1 unless we are adding the file
578 if h.starta == 0:
578 if h.starta == 0:
579 start = 0
579 start = 0
580 else:
580 else:
581 start = h.starta + self.offset - 1
581 start = h.starta + self.offset - 1
582 orig_start = start
582 orig_start = start
583 # if there's skew we want to emit the "(offset %d lines)" even
583 # if there's skew we want to emit the "(offset %d lines)" even
584 # when the hunk cleanly applies at start + skew, so skip the
584 # when the hunk cleanly applies at start + skew, so skip the
585 # fast case code
585 # fast case code
586 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
586 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
587 if h.rmfile():
587 if h.rmfile():
588 self.unlink(self.fname)
588 self.unlink(self.fname)
589 else:
589 else:
590 self.lines[start : start + h.lena] = h.new()
590 self.lines[start : start + h.lena] = h.new()
591 self.offset += h.lenb - h.lena
591 self.offset += h.lenb - h.lena
592 self.dirty = 1
592 self.dirty = 1
593 return 0
593 return 0
594
594
595 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
595 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
596 self.hashlines()
596 self.hashlines()
597 if h.hunk[-1][0] != ' ':
597 if h.hunk[-1][0] != ' ':
598 # if the hunk tried to put something at the bottom of the file
598 # if the hunk tried to put something at the bottom of the file
599 # override the start line and use eof here
599 # override the start line and use eof here
600 search_start = len(self.lines)
600 search_start = len(self.lines)
601 else:
601 else:
602 search_start = orig_start + self.skew
602 search_start = orig_start + self.skew
603
603
604 for fuzzlen in xrange(3):
604 for fuzzlen in xrange(3):
605 for toponly in [True, False]:
605 for toponly in [True, False]:
606 old = h.old(fuzzlen, toponly)
606 old = h.old(fuzzlen, toponly)
607
607
608 cand = self.findlines(old[0][1:], search_start)
608 cand = self.findlines(old[0][1:], search_start)
609 for l in cand:
609 for l in cand:
610 if diffhelpers.testhunk(old, self.lines, l) == 0:
610 if diffhelpers.testhunk(old, self.lines, l) == 0:
611 newlines = h.new(fuzzlen, toponly)
611 newlines = h.new(fuzzlen, toponly)
612 self.lines[l : l + len(old)] = newlines
612 self.lines[l : l + len(old)] = newlines
613 self.offset += len(newlines) - len(old)
613 self.offset += len(newlines) - len(old)
614 self.skew = l - orig_start
614 self.skew = l - orig_start
615 self.dirty = 1
615 self.dirty = 1
616 offset = l - orig_start - fuzzlen
616 offset = l - orig_start - fuzzlen
617 if fuzzlen:
617 if fuzzlen:
618 msg = _("Hunk #%d succeeded at %d "
618 msg = _("Hunk #%d succeeded at %d "
619 "with fuzz %d "
619 "with fuzz %d "
620 "(offset %d lines).\n")
620 "(offset %d lines).\n")
621 self.printfile(True)
621 self.printfile(True)
622 self.ui.warn(msg %
622 self.ui.warn(msg %
623 (h.number, l + 1, fuzzlen, offset))
623 (h.number, l + 1, fuzzlen, offset))
624 else:
624 else:
625 msg = _("Hunk #%d succeeded at %d "
625 msg = _("Hunk #%d succeeded at %d "
626 "(offset %d lines).\n")
626 "(offset %d lines).\n")
627 self.ui.note(msg % (h.number, l + 1, offset))
627 self.ui.note(msg % (h.number, l + 1, offset))
628 return fuzzlen
628 return fuzzlen
629 self.printfile(True)
629 self.printfile(True)
630 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
630 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
631 self.rej.append(horig)
631 self.rej.append(horig)
632 return -1
632 return -1
633
633
634 class hunk(object):
634 class hunk(object):
635 def __init__(self, desc, num, lr, context, create=False, remove=False):
635 def __init__(self, desc, num, lr, context, create=False, remove=False):
636 self.number = num
636 self.number = num
637 self.desc = desc
637 self.desc = desc
638 self.hunk = [desc]
638 self.hunk = [desc]
639 self.a = []
639 self.a = []
640 self.b = []
640 self.b = []
641 self.starta = self.lena = None
641 self.starta = self.lena = None
642 self.startb = self.lenb = None
642 self.startb = self.lenb = None
643 if lr is not None:
643 if lr is not None:
644 if context:
644 if context:
645 self.read_context_hunk(lr)
645 self.read_context_hunk(lr)
646 else:
646 else:
647 self.read_unified_hunk(lr)
647 self.read_unified_hunk(lr)
648 self.create = create
648 self.create = create
649 self.remove = remove and not create
649 self.remove = remove and not create
650
650
651 def getnormalized(self):
651 def getnormalized(self):
652 """Return a copy with line endings normalized to LF."""
652 """Return a copy with line endings normalized to LF."""
653
653
654 def normalize(lines):
654 def normalize(lines):
655 nlines = []
655 nlines = []
656 for line in lines:
656 for line in lines:
657 if line.endswith('\r\n'):
657 if line.endswith('\r\n'):
658 line = line[:-2] + '\n'
658 line = line[:-2] + '\n'
659 nlines.append(line)
659 nlines.append(line)
660 return nlines
660 return nlines
661
661
662 # Dummy object, it is rebuilt manually
662 # Dummy object, it is rebuilt manually
663 nh = hunk(self.desc, self.number, None, None, False, False)
663 nh = hunk(self.desc, self.number, None, None, False, False)
664 nh.number = self.number
664 nh.number = self.number
665 nh.desc = self.desc
665 nh.desc = self.desc
666 nh.hunk = self.hunk
666 nh.hunk = self.hunk
667 nh.a = normalize(self.a)
667 nh.a = normalize(self.a)
668 nh.b = normalize(self.b)
668 nh.b = normalize(self.b)
669 nh.starta = self.starta
669 nh.starta = self.starta
670 nh.startb = self.startb
670 nh.startb = self.startb
671 nh.lena = self.lena
671 nh.lena = self.lena
672 nh.lenb = self.lenb
672 nh.lenb = self.lenb
673 nh.create = self.create
673 nh.create = self.create
674 nh.remove = self.remove
674 nh.remove = self.remove
675 return nh
675 return nh
676
676
677 def read_unified_hunk(self, lr):
677 def read_unified_hunk(self, lr):
678 m = unidesc.match(self.desc)
678 m = unidesc.match(self.desc)
679 if not m:
679 if not m:
680 raise PatchError(_("bad hunk #%d") % self.number)
680 raise PatchError(_("bad hunk #%d") % self.number)
681 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
681 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
682 if self.lena is None:
682 if self.lena is None:
683 self.lena = 1
683 self.lena = 1
684 else:
684 else:
685 self.lena = int(self.lena)
685 self.lena = int(self.lena)
686 if self.lenb is None:
686 if self.lenb is None:
687 self.lenb = 1
687 self.lenb = 1
688 else:
688 else:
689 self.lenb = int(self.lenb)
689 self.lenb = int(self.lenb)
690 self.starta = int(self.starta)
690 self.starta = int(self.starta)
691 self.startb = int(self.startb)
691 self.startb = int(self.startb)
692 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
692 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
693 # if we hit eof before finishing out the hunk, the last line will
693 # if we hit eof before finishing out the hunk, the last line will
694 # be zero length. Lets try to fix it up.
694 # be zero length. Lets try to fix it up.
695 while len(self.hunk[-1]) == 0:
695 while len(self.hunk[-1]) == 0:
696 del self.hunk[-1]
696 del self.hunk[-1]
697 del self.a[-1]
697 del self.a[-1]
698 del self.b[-1]
698 del self.b[-1]
699 self.lena -= 1
699 self.lena -= 1
700 self.lenb -= 1
700 self.lenb -= 1
701
701
702 def read_context_hunk(self, lr):
702 def read_context_hunk(self, lr):
703 self.desc = lr.readline()
703 self.desc = lr.readline()
704 m = contextdesc.match(self.desc)
704 m = contextdesc.match(self.desc)
705 if not m:
705 if not m:
706 raise PatchError(_("bad hunk #%d") % self.number)
706 raise PatchError(_("bad hunk #%d") % self.number)
707 foo, self.starta, foo2, aend, foo3 = m.groups()
707 foo, self.starta, foo2, aend, foo3 = m.groups()
708 self.starta = int(self.starta)
708 self.starta = int(self.starta)
709 if aend is None:
709 if aend is None:
710 aend = self.starta
710 aend = self.starta
711 self.lena = int(aend) - self.starta
711 self.lena = int(aend) - self.starta
712 if self.starta:
712 if self.starta:
713 self.lena += 1
713 self.lena += 1
714 for x in xrange(self.lena):
714 for x in xrange(self.lena):
715 l = lr.readline()
715 l = lr.readline()
716 if l.startswith('---'):
716 if l.startswith('---'):
717 lr.push(l)
717 lr.push(l)
718 break
718 break
719 s = l[2:]
719 s = l[2:]
720 if l.startswith('- ') or l.startswith('! '):
720 if l.startswith('- ') or l.startswith('! '):
721 u = '-' + s
721 u = '-' + s
722 elif l.startswith(' '):
722 elif l.startswith(' '):
723 u = ' ' + s
723 u = ' ' + s
724 else:
724 else:
725 raise PatchError(_("bad hunk #%d old text line %d") %
725 raise PatchError(_("bad hunk #%d old text line %d") %
726 (self.number, x))
726 (self.number, x))
727 self.a.append(u)
727 self.a.append(u)
728 self.hunk.append(u)
728 self.hunk.append(u)
729
729
730 l = lr.readline()
730 l = lr.readline()
731 if l.startswith('\ '):
731 if l.startswith('\ '):
732 s = self.a[-1][:-1]
732 s = self.a[-1][:-1]
733 self.a[-1] = s
733 self.a[-1] = s
734 self.hunk[-1] = s
734 self.hunk[-1] = s
735 l = lr.readline()
735 l = lr.readline()
736 m = contextdesc.match(l)
736 m = contextdesc.match(l)
737 if not m:
737 if not m:
738 raise PatchError(_("bad hunk #%d") % self.number)
738 raise PatchError(_("bad hunk #%d") % self.number)
739 foo, self.startb, foo2, bend, foo3 = m.groups()
739 foo, self.startb, foo2, bend, foo3 = m.groups()
740 self.startb = int(self.startb)
740 self.startb = int(self.startb)
741 if bend is None:
741 if bend is None:
742 bend = self.startb
742 bend = self.startb
743 self.lenb = int(bend) - self.startb
743 self.lenb = int(bend) - self.startb
744 if self.startb:
744 if self.startb:
745 self.lenb += 1
745 self.lenb += 1
746 hunki = 1
746 hunki = 1
747 for x in xrange(self.lenb):
747 for x in xrange(self.lenb):
748 l = lr.readline()
748 l = lr.readline()
749 if l.startswith('\ '):
749 if l.startswith('\ '):
750 s = self.b[-1][:-1]
750 s = self.b[-1][:-1]
751 self.b[-1] = s
751 self.b[-1] = s
752 self.hunk[hunki - 1] = s
752 self.hunk[hunki - 1] = s
753 continue
753 continue
754 if not l:
754 if not l:
755 lr.push(l)
755 lr.push(l)
756 break
756 break
757 s = l[2:]
757 s = l[2:]
758 if l.startswith('+ ') or l.startswith('! '):
758 if l.startswith('+ ') or l.startswith('! '):
759 u = '+' + s
759 u = '+' + s
760 elif l.startswith(' '):
760 elif l.startswith(' '):
761 u = ' ' + s
761 u = ' ' + s
762 elif len(self.b) == 0:
762 elif len(self.b) == 0:
763 # this can happen when the hunk does not add any lines
763 # this can happen when the hunk does not add any lines
764 lr.push(l)
764 lr.push(l)
765 break
765 break
766 else:
766 else:
767 raise PatchError(_("bad hunk #%d old text line %d") %
767 raise PatchError(_("bad hunk #%d old text line %d") %
768 (self.number, x))
768 (self.number, x))
769 self.b.append(s)
769 self.b.append(s)
770 while True:
770 while True:
771 if hunki >= len(self.hunk):
771 if hunki >= len(self.hunk):
772 h = ""
772 h = ""
773 else:
773 else:
774 h = self.hunk[hunki]
774 h = self.hunk[hunki]
775 hunki += 1
775 hunki += 1
776 if h == u:
776 if h == u:
777 break
777 break
778 elif h.startswith('-'):
778 elif h.startswith('-'):
779 continue
779 continue
780 else:
780 else:
781 self.hunk.insert(hunki - 1, u)
781 self.hunk.insert(hunki - 1, u)
782 break
782 break
783
783
784 if not self.a:
784 if not self.a:
785 # this happens when lines were only added to the hunk
785 # this happens when lines were only added to the hunk
786 for x in self.hunk:
786 for x in self.hunk:
787 if x.startswith('-') or x.startswith(' '):
787 if x.startswith('-') or x.startswith(' '):
788 self.a.append(x)
788 self.a.append(x)
789 if not self.b:
789 if not self.b:
790 # this happens when lines were only deleted from the hunk
790 # this happens when lines were only deleted from the hunk
791 for x in self.hunk:
791 for x in self.hunk:
792 if x.startswith('+') or x.startswith(' '):
792 if x.startswith('+') or x.startswith(' '):
793 self.b.append(x[1:])
793 self.b.append(x[1:])
794 # @@ -start,len +start,len @@
794 # @@ -start,len +start,len @@
795 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
795 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
796 self.startb, self.lenb)
796 self.startb, self.lenb)
797 self.hunk[0] = self.desc
797 self.hunk[0] = self.desc
798
798
799 def fix_newline(self):
799 def fix_newline(self):
800 diffhelpers.fix_newline(self.hunk, self.a, self.b)
800 diffhelpers.fix_newline(self.hunk, self.a, self.b)
801
801
802 def complete(self):
802 def complete(self):
803 return len(self.a) == self.lena and len(self.b) == self.lenb
803 return len(self.a) == self.lena and len(self.b) == self.lenb
804
804
805 def createfile(self):
805 def createfile(self):
806 return self.starta == 0 and self.lena == 0 and self.create
806 return self.starta == 0 and self.lena == 0 and self.create
807
807
808 def rmfile(self):
808 def rmfile(self):
809 return self.startb == 0 and self.lenb == 0 and self.remove
809 return self.startb == 0 and self.lenb == 0 and self.remove
810
810
811 def fuzzit(self, l, fuzz, toponly):
811 def fuzzit(self, l, fuzz, toponly):
812 # this removes context lines from the top and bottom of list 'l'. It
812 # this removes context lines from the top and bottom of list 'l'. It
813 # checks the hunk to make sure only context lines are removed, and then
813 # checks the hunk to make sure only context lines are removed, and then
814 # returns a new shortened list of lines.
814 # returns a new shortened list of lines.
815 fuzz = min(fuzz, len(l)-1)
815 fuzz = min(fuzz, len(l)-1)
816 if fuzz:
816 if fuzz:
817 top = 0
817 top = 0
818 bot = 0
818 bot = 0
819 hlen = len(self.hunk)
819 hlen = len(self.hunk)
820 for x in xrange(hlen - 1):
820 for x in xrange(hlen - 1):
821 # the hunk starts with the @@ line, so use x+1
821 # the hunk starts with the @@ line, so use x+1
822 if self.hunk[x + 1][0] == ' ':
822 if self.hunk[x + 1][0] == ' ':
823 top += 1
823 top += 1
824 else:
824 else:
825 break
825 break
826 if not toponly:
826 if not toponly:
827 for x in xrange(hlen - 1):
827 for x in xrange(hlen - 1):
828 if self.hunk[hlen - bot - 1][0] == ' ':
828 if self.hunk[hlen - bot - 1][0] == ' ':
829 bot += 1
829 bot += 1
830 else:
830 else:
831 break
831 break
832
832
833 # top and bot now count context in the hunk
833 # top and bot now count context in the hunk
834 # adjust them if either one is short
834 # adjust them if either one is short
835 context = max(top, bot, 3)
835 context = max(top, bot, 3)
836 if bot < context:
836 if bot < context:
837 bot = max(0, fuzz - (context - bot))
837 bot = max(0, fuzz - (context - bot))
838 else:
838 else:
839 bot = min(fuzz, bot)
839 bot = min(fuzz, bot)
840 if top < context:
840 if top < context:
841 top = max(0, fuzz - (context - top))
841 top = max(0, fuzz - (context - top))
842 else:
842 else:
843 top = min(fuzz, top)
843 top = min(fuzz, top)
844
844
845 return l[top:len(l)-bot]
845 return l[top:len(l)-bot]
846 return l
846 return l
847
847
848 def old(self, fuzz=0, toponly=False):
848 def old(self, fuzz=0, toponly=False):
849 return self.fuzzit(self.a, fuzz, toponly)
849 return self.fuzzit(self.a, fuzz, toponly)
850
850
851 def new(self, fuzz=0, toponly=False):
851 def new(self, fuzz=0, toponly=False):
852 return self.fuzzit(self.b, fuzz, toponly)
852 return self.fuzzit(self.b, fuzz, toponly)
853
853
854 class binhunk:
854 class binhunk:
855 'A binary patch file. Only understands literals so far.'
855 'A binary patch file. Only understands literals so far.'
856 def __init__(self, gitpatch):
856 def __init__(self, gitpatch):
857 self.gitpatch = gitpatch
857 self.gitpatch = gitpatch
858 self.text = None
858 self.text = None
859 self.hunk = ['GIT binary patch\n']
859 self.hunk = ['GIT binary patch\n']
860
860
861 def createfile(self):
861 def createfile(self):
862 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
862 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
863
863
864 def rmfile(self):
864 def rmfile(self):
865 return self.gitpatch.op == 'DELETE'
865 return self.gitpatch.op == 'DELETE'
866
866
867 def complete(self):
867 def complete(self):
868 return self.text is not None
868 return self.text is not None
869
869
870 def new(self):
870 def new(self):
871 return [self.text]
871 return [self.text]
872
872
873 def extract(self, lr):
873 def extract(self, lr):
874 line = lr.readline()
874 line = lr.readline()
875 self.hunk.append(line)
875 self.hunk.append(line)
876 while line and not line.startswith('literal '):
876 while line and not line.startswith('literal '):
877 line = lr.readline()
877 line = lr.readline()
878 self.hunk.append(line)
878 self.hunk.append(line)
879 if not line:
879 if not line:
880 raise PatchError(_('could not extract binary patch'))
880 raise PatchError(_('could not extract binary patch'))
881 size = int(line[8:].rstrip())
881 size = int(line[8:].rstrip())
882 dec = []
882 dec = []
883 line = lr.readline()
883 line = lr.readline()
884 self.hunk.append(line)
884 self.hunk.append(line)
885 while len(line) > 1:
885 while len(line) > 1:
886 l = line[0]
886 l = line[0]
887 if l <= 'Z' and l >= 'A':
887 if l <= 'Z' and l >= 'A':
888 l = ord(l) - ord('A') + 1
888 l = ord(l) - ord('A') + 1
889 else:
889 else:
890 l = ord(l) - ord('a') + 27
890 l = ord(l) - ord('a') + 27
891 dec.append(base85.b85decode(line[1:-1])[:l])
891 dec.append(base85.b85decode(line[1:-1])[:l])
892 line = lr.readline()
892 line = lr.readline()
893 self.hunk.append(line)
893 self.hunk.append(line)
894 text = zlib.decompress(''.join(dec))
894 text = zlib.decompress(''.join(dec))
895 if len(text) != size:
895 if len(text) != size:
896 raise PatchError(_('binary patch is %d bytes, not %d') %
896 raise PatchError(_('binary patch is %d bytes, not %d') %
897 len(text), size)
897 len(text), size)
898 self.text = text
898 self.text = text
899
899
900 def parsefilename(str):
900 def parsefilename(str):
901 # --- filename \t|space stuff
901 # --- filename \t|space stuff
902 s = str[4:].rstrip('\r\n')
902 s = str[4:].rstrip('\r\n')
903 i = s.find('\t')
903 i = s.find('\t')
904 if i < 0:
904 if i < 0:
905 i = s.find(' ')
905 i = s.find(' ')
906 if i < 0:
906 if i < 0:
907 return s
907 return s
908 return s[:i]
908 return s[:i]
909
909
910 def selectfile(afile_orig, bfile_orig, hunk, strip):
910 def selectfile(afile_orig, bfile_orig, hunk, strip):
911 def pathstrip(path, strip):
911 def pathstrip(path, strip):
912 pathlen = len(path)
912 pathlen = len(path)
913 i = 0
913 i = 0
914 if strip == 0:
914 if strip == 0:
915 return '', path.rstrip()
915 return '', path.rstrip()
916 count = strip
916 count = strip
917 while count > 0:
917 while count > 0:
918 i = path.find('/', i)
918 i = path.find('/', i)
919 if i == -1:
919 if i == -1:
920 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
920 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
921 (count, strip, path))
921 (count, strip, path))
922 i += 1
922 i += 1
923 # consume '//' in the path
923 # consume '//' in the path
924 while i < pathlen - 1 and path[i] == '/':
924 while i < pathlen - 1 and path[i] == '/':
925 i += 1
925 i += 1
926 count -= 1
926 count -= 1
927 return path[:i].lstrip(), path[i:].rstrip()
927 return path[:i].lstrip(), path[i:].rstrip()
928
928
929 nulla = afile_orig == "/dev/null"
929 nulla = afile_orig == "/dev/null"
930 nullb = bfile_orig == "/dev/null"
930 nullb = bfile_orig == "/dev/null"
931 abase, afile = pathstrip(afile_orig, strip)
931 abase, afile = pathstrip(afile_orig, strip)
932 gooda = not nulla and util.lexists(afile)
932 gooda = not nulla and util.lexists(afile)
933 bbase, bfile = pathstrip(bfile_orig, strip)
933 bbase, bfile = pathstrip(bfile_orig, strip)
934 if afile == bfile:
934 if afile == bfile:
935 goodb = gooda
935 goodb = gooda
936 else:
936 else:
937 goodb = not nullb and os.path.exists(bfile)
937 goodb = not nullb and os.path.exists(bfile)
938 createfunc = hunk.createfile
938 createfunc = hunk.createfile
939 missing = not goodb and not gooda and not createfunc()
939 missing = not goodb and not gooda and not createfunc()
940
940
941 # some diff programs apparently produce create patches where the
941 # some diff programs apparently produce create patches where the
942 # afile is not /dev/null, but afile starts with bfile
942 # afile is not /dev/null, but afile starts with bfile
943 abasedir = afile[:afile.rfind('/') + 1]
943 abasedir = afile[:afile.rfind('/') + 1]
944 bbasedir = bfile[:bfile.rfind('/') + 1]
944 bbasedir = bfile[:bfile.rfind('/') + 1]
945 if missing and abasedir == bbasedir and afile.startswith(bfile):
945 if missing and abasedir == bbasedir and afile.startswith(bfile):
946 # this isn't very pretty
946 # this isn't very pretty
947 hunk.create = True
947 hunk.create = True
948 if createfunc():
948 if createfunc():
949 missing = False
949 missing = False
950 else:
950 else:
951 hunk.create = False
951 hunk.create = False
952
952
953 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
953 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
954 # diff is between a file and its backup. In this case, the original
954 # diff is between a file and its backup. In this case, the original
955 # file should be patched (see original mpatch code).
955 # file should be patched (see original mpatch code).
956 isbackup = (abase == bbase and bfile.startswith(afile))
956 isbackup = (abase == bbase and bfile.startswith(afile))
957 fname = None
957 fname = None
958 if not missing:
958 if not missing:
959 if gooda and goodb:
959 if gooda and goodb:
960 fname = isbackup and afile or bfile
960 fname = isbackup and afile or bfile
961 elif gooda:
961 elif gooda:
962 fname = afile
962 fname = afile
963
963
964 if not fname:
964 if not fname:
965 if not nullb:
965 if not nullb:
966 fname = isbackup and afile or bfile
966 fname = isbackup and afile or bfile
967 elif not nulla:
967 elif not nulla:
968 fname = afile
968 fname = afile
969 else:
969 else:
970 raise PatchError(_("undefined source and destination files"))
970 raise PatchError(_("undefined source and destination files"))
971
971
972 return fname, missing
972 return fname, missing
973
973
974 def scangitpatch(lr, firstline):
974 def scangitpatch(lr, firstline):
975 """
975 """
976 Git patches can emit:
976 Git patches can emit:
977 - rename a to b
977 - rename a to b
978 - change b
978 - change b
979 - copy a to c
979 - copy a to c
980 - change c
980 - change c
981
981
982 We cannot apply this sequence as-is, the renamed 'a' could not be
982 We cannot apply this sequence as-is, the renamed 'a' could not be
983 found for it would have been renamed already. And we cannot copy
983 found for it would have been renamed already. And we cannot copy
984 from 'b' instead because 'b' would have been changed already. So
984 from 'b' instead because 'b' would have been changed already. So
985 we scan the git patch for copy and rename commands so we can
985 we scan the git patch for copy and rename commands so we can
986 perform the copies ahead of time.
986 perform the copies ahead of time.
987 """
987 """
988 pos = 0
988 pos = 0
989 try:
989 try:
990 pos = lr.fp.tell()
990 pos = lr.fp.tell()
991 fp = lr.fp
991 fp = lr.fp
992 except IOError:
992 except IOError:
993 fp = cStringIO.StringIO(lr.fp.read())
993 fp = cStringIO.StringIO(lr.fp.read())
994 gitlr = linereader(fp, lr.textmode)
994 gitlr = linereader(fp, lr.textmode)
995 gitlr.push(firstline)
995 gitlr.push(firstline)
996 (dopatch, gitpatches) = readgitpatch(gitlr)
996 (dopatch, gitpatches) = readgitpatch(gitlr)
997 fp.seek(pos)
997 fp.seek(pos)
998 return dopatch, gitpatches
998 return dopatch, gitpatches
999
999
1000 def iterhunks(ui, fp, sourcefile=None):
1000 def iterhunks(ui, fp, sourcefile=None):
1001 """Read a patch and yield the following events:
1001 """Read a patch and yield the following events:
1002 - ("file", afile, bfile, firsthunk): select a new target file.
1002 - ("file", afile, bfile, firsthunk): select a new target file.
1003 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1003 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1004 "file" event.
1004 "file" event.
1005 - ("git", gitchanges): current diff is in git format, gitchanges
1005 - ("git", gitchanges): current diff is in git format, gitchanges
1006 maps filenames to gitpatch records. Unique event.
1006 maps filenames to gitpatch records. Unique event.
1007 """
1007 """
1008 changed = {}
1008 changed = {}
1009 current_hunk = None
1009 current_hunk = None
1010 afile = ""
1010 afile = ""
1011 bfile = ""
1011 bfile = ""
1012 state = None
1012 state = None
1013 hunknum = 0
1013 hunknum = 0
1014 emitfile = False
1014 emitfile = False
1015 git = False
1015 git = False
1016
1016
1017 # our states
1017 # our states
1018 BFILE = 1
1018 BFILE = 1
1019 context = None
1019 context = None
1020 lr = linereader(fp)
1020 lr = linereader(fp)
1021 # gitworkdone is True if a git operation (copy, rename, ...) was
1021 # gitworkdone is True if a git operation (copy, rename, ...) was
1022 # performed already for the current file. Useful when the file
1022 # performed already for the current file. Useful when the file
1023 # section may have no hunk.
1023 # section may have no hunk.
1024 gitworkdone = False
1024 gitworkdone = False
1025 empty = None
1025 empty = None
1026
1026
1027 while True:
1027 while True:
1028 newfile = newgitfile = False
1028 newfile = newgitfile = False
1029 x = lr.readline()
1029 x = lr.readline()
1030 if not x:
1030 if not x:
1031 break
1031 break
1032 if current_hunk:
1032 if current_hunk:
1033 if x.startswith('\ '):
1033 if x.startswith('\ '):
1034 current_hunk.fix_newline()
1034 current_hunk.fix_newline()
1035 yield 'hunk', current_hunk
1035 yield 'hunk', current_hunk
1036 current_hunk = None
1036 current_hunk = None
1037 empty = False
1037 empty = False
1038 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1038 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1039 ((context is not False) and x.startswith('***************')))):
1039 ((context is not False) and x.startswith('***************')))):
1040 try:
1040 try:
1041 if context is None and x.startswith('***************'):
1041 if context is None and x.startswith('***************'):
1042 context = True
1042 context = True
1043 gpatch = changed.get(bfile)
1043 gpatch = changed.get(bfile)
1044 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1044 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1045 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1045 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1046 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1046 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1047 except PatchError, err:
1047 except PatchError, err:
1048 ui.debug(err)
1048 ui.debug(err)
1049 current_hunk = None
1049 current_hunk = None
1050 continue
1050 continue
1051 hunknum += 1
1051 hunknum += 1
1052 if emitfile:
1052 if emitfile:
1053 emitfile = False
1053 emitfile = False
1054 yield 'file', (afile, bfile, current_hunk)
1054 yield 'file', (afile, bfile, current_hunk)
1055 empty = False
1055 empty = False
1056 elif state == BFILE and x.startswith('GIT binary patch'):
1056 elif state == BFILE and x.startswith('GIT binary patch'):
1057 current_hunk = binhunk(changed[bfile])
1057 current_hunk = binhunk(changed[bfile])
1058 hunknum += 1
1058 hunknum += 1
1059 if emitfile:
1059 if emitfile:
1060 emitfile = False
1060 emitfile = False
1061 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1061 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1062 empty = False
1062 empty = False
1063 current_hunk.extract(lr)
1063 current_hunk.extract(lr)
1064 elif x.startswith('diff --git'):
1064 elif x.startswith('diff --git'):
1065 # check for git diff, scanning the whole patch file if needed
1065 # check for git diff, scanning the whole patch file if needed
1066 m = gitre.match(x)
1066 m = gitre.match(x)
1067 gitworkdone = False
1067 gitworkdone = False
1068 if m:
1068 if m:
1069 afile, bfile = m.group(1, 2)
1069 afile, bfile = m.group(1, 2)
1070 if not git:
1070 if not git:
1071 git = True
1071 git = True
1072 gitpatches = scangitpatch(lr, x)[1]
1072 gitpatches = scangitpatch(lr, x)[1]
1073 yield 'git', gitpatches
1073 yield 'git', gitpatches
1074 for gp in gitpatches:
1074 for gp in gitpatches:
1075 changed[gp.path] = gp
1075 changed[gp.path] = gp
1076 # else error?
1076 # else error?
1077 # copy/rename + modify should modify target, not source
1077 # copy/rename + modify should modify target, not source
1078 gp = changed.get(bfile)
1078 gp = changed.get(bfile)
1079 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1079 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1080 or gp.mode):
1080 or gp.mode):
1081 afile = bfile
1081 afile = bfile
1082 gitworkdone = True
1082 gitworkdone = True
1083 newgitfile = True
1083 newgitfile = True
1084 elif x.startswith('---'):
1084 elif x.startswith('---'):
1085 # check for a unified diff
1085 # check for a unified diff
1086 l2 = lr.readline()
1086 l2 = lr.readline()
1087 if not l2.startswith('+++'):
1087 if not l2.startswith('+++'):
1088 lr.push(l2)
1088 lr.push(l2)
1089 continue
1089 continue
1090 newfile = True
1090 newfile = True
1091 context = False
1091 context = False
1092 afile = parsefilename(x)
1092 afile = parsefilename(x)
1093 bfile = parsefilename(l2)
1093 bfile = parsefilename(l2)
1094 elif x.startswith('***'):
1094 elif x.startswith('***'):
1095 # check for a context diff
1095 # check for a context diff
1096 l2 = lr.readline()
1096 l2 = lr.readline()
1097 if not l2.startswith('---'):
1097 if not l2.startswith('---'):
1098 lr.push(l2)
1098 lr.push(l2)
1099 continue
1099 continue
1100 l3 = lr.readline()
1100 l3 = lr.readline()
1101 lr.push(l3)
1101 lr.push(l3)
1102 if not l3.startswith("***************"):
1102 if not l3.startswith("***************"):
1103 lr.push(l2)
1103 lr.push(l2)
1104 continue
1104 continue
1105 newfile = True
1105 newfile = True
1106 context = True
1106 context = True
1107 afile = parsefilename(x)
1107 afile = parsefilename(x)
1108 bfile = parsefilename(l2)
1108 bfile = parsefilename(l2)
1109
1109
1110 if newfile:
1110 if newfile:
1111 if empty:
1111 if empty:
1112 raise NoHunks
1112 raise NoHunks
1113 empty = not gitworkdone
1113 empty = not gitworkdone
1114 gitworkdone = False
1114 gitworkdone = False
1115
1115
1116 if newgitfile or newfile:
1116 if newgitfile or newfile:
1117 emitfile = True
1117 emitfile = True
1118 state = BFILE
1118 state = BFILE
1119 hunknum = 0
1119 hunknum = 0
1120 if current_hunk:
1120 if current_hunk:
1121 if current_hunk.complete():
1121 if current_hunk.complete():
1122 yield 'hunk', current_hunk
1122 yield 'hunk', current_hunk
1123 empty = False
1123 empty = False
1124 else:
1124 else:
1125 raise PatchError(_("malformed patch %s %s") % (afile,
1125 raise PatchError(_("malformed patch %s %s") % (afile,
1126 current_hunk.desc))
1126 current_hunk.desc))
1127
1127
1128 if (empty is None and not gitworkdone) or empty:
1128 if (empty is None and not gitworkdone) or empty:
1129 raise NoHunks
1129 raise NoHunks
1130
1130
1131
1131
1132 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1132 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1133 """Reads a patch from fp and tries to apply it.
1133 """Reads a patch from fp and tries to apply it.
1134
1134
1135 The dict 'changed' is filled in with all of the filenames changed
1135 The dict 'changed' is filled in with all of the filenames changed
1136 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1136 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1137 found and 1 if there was any fuzz.
1137 found and 1 if there was any fuzz.
1138
1138
1139 If 'eolmode' is 'strict', the patch content and patched file are
1139 If 'eolmode' is 'strict', the patch content and patched file are
1140 read in binary mode. Otherwise, line endings are ignored when
1140 read in binary mode. Otherwise, line endings are ignored when
1141 patching then normalized according to 'eolmode'.
1141 patching then normalized according to 'eolmode'.
1142
1142
1143 Callers probably want to call 'updatedir' after this to apply
1143 Callers probably want to call 'updatedir' after this to apply
1144 certain categories of changes not done by this function.
1144 certain categories of changes not done by this function.
1145 """
1145 """
1146 return _applydiff(
1146 return _applydiff(
1147 ui, fp, patchfile, copyfile,
1147 ui, fp, patchfile, copyfile,
1148 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1148 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1149
1149
1150
1150
1151 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1151 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1152 sourcefile=None, eolmode='strict'):
1152 sourcefile=None, eolmode='strict'):
1153 rejects = 0
1153 rejects = 0
1154 err = 0
1154 err = 0
1155 current_file = None
1155 current_file = None
1156 gitpatches = None
1156 cwd = os.getcwd()
1157 opener = util.opener(os.getcwd())
1157 opener = util.opener(cwd)
1158
1158
1159 def closefile():
1159 def closefile():
1160 if not current_file:
1160 if not current_file:
1161 return 0
1161 return 0
1162 current_file.close()
1162 current_file.close()
1163 return len(current_file.rej)
1163 return len(current_file.rej)
1164
1164
1165 for state, values in iterhunks(ui, fp, sourcefile):
1165 for state, values in iterhunks(ui, fp, sourcefile):
1166 if state == 'hunk':
1166 if state == 'hunk':
1167 if not current_file:
1167 if not current_file:
1168 continue
1168 continue
1169 current_hunk = values
1169 ret = current_file.apply(values)
1170 ret = current_file.apply(current_hunk)
1171 if ret >= 0:
1170 if ret >= 0:
1172 changed.setdefault(current_file.fname, None)
1171 changed.setdefault(current_file.fname, None)
1173 if ret > 0:
1172 if ret > 0:
1174 err = 1
1173 err = 1
1175 elif state == 'file':
1174 elif state == 'file':
1176 rejects += closefile()
1175 rejects += closefile()
1177 afile, bfile, first_hunk = values
1176 afile, bfile, first_hunk = values
1178 try:
1177 try:
1179 if sourcefile:
1178 if sourcefile:
1180 current_file = patcher(ui, sourcefile, opener,
1179 current_file = patcher(ui, sourcefile, opener,
1181 eolmode=eolmode)
1180 eolmode=eolmode)
1182 else:
1181 else:
1183 current_file, missing = selectfile(afile, bfile,
1182 current_file, missing = selectfile(afile, bfile,
1184 first_hunk, strip)
1183 first_hunk, strip)
1185 current_file = patcher(ui, current_file, opener,
1184 current_file = patcher(ui, current_file, opener,
1186 missing=missing, eolmode=eolmode)
1185 missing=missing, eolmode=eolmode)
1187 except PatchError, err:
1186 except PatchError, err:
1188 ui.warn(str(err) + '\n')
1187 ui.warn(str(err) + '\n')
1189 current_file, current_hunk = None, None
1188 current_file = None
1190 rejects += 1
1189 rejects += 1
1191 continue
1190 continue
1192 elif state == 'git':
1191 elif state == 'git':
1193 gitpatches = values
1192 for gp in values:
1194 cwd = os.getcwd()
1195 for gp in gitpatches:
1196 if gp.op in ('COPY', 'RENAME'):
1193 if gp.op in ('COPY', 'RENAME'):
1197 copyfn(gp.oldpath, gp.path, cwd)
1194 copyfn(gp.oldpath, gp.path, cwd)
1198 changed[gp.path] = gp
1195 changed[gp.path] = gp
1199 else:
1196 else:
1200 raise util.Abort(_('unsupported parser state: %s') % state)
1197 raise util.Abort(_('unsupported parser state: %s') % state)
1201
1198
1202 rejects += closefile()
1199 rejects += closefile()
1203
1200
1204 if rejects:
1201 if rejects:
1205 return -1
1202 return -1
1206 return err
1203 return err
1207
1204
1208 def updatedir(ui, repo, patches, similarity=0):
1205 def updatedir(ui, repo, patches, similarity=0):
1209 '''Update dirstate after patch application according to metadata'''
1206 '''Update dirstate after patch application according to metadata'''
1210 if not patches:
1207 if not patches:
1211 return
1208 return
1212 copies = []
1209 copies = []
1213 removes = set()
1210 removes = set()
1214 cfiles = patches.keys()
1211 cfiles = patches.keys()
1215 cwd = repo.getcwd()
1212 cwd = repo.getcwd()
1216 if cwd:
1213 if cwd:
1217 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1214 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1218 for f in patches:
1215 for f in patches:
1219 gp = patches[f]
1216 gp = patches[f]
1220 if not gp:
1217 if not gp:
1221 continue
1218 continue
1222 if gp.op == 'RENAME':
1219 if gp.op == 'RENAME':
1223 copies.append((gp.oldpath, gp.path))
1220 copies.append((gp.oldpath, gp.path))
1224 removes.add(gp.oldpath)
1221 removes.add(gp.oldpath)
1225 elif gp.op == 'COPY':
1222 elif gp.op == 'COPY':
1226 copies.append((gp.oldpath, gp.path))
1223 copies.append((gp.oldpath, gp.path))
1227 elif gp.op == 'DELETE':
1224 elif gp.op == 'DELETE':
1228 removes.add(gp.path)
1225 removes.add(gp.path)
1229 for src, dst in copies:
1226 for src, dst in copies:
1230 repo.copy(src, dst)
1227 repo.copy(src, dst)
1231 if (not similarity) and removes:
1228 if (not similarity) and removes:
1232 repo.remove(sorted(removes), True)
1229 repo.remove(sorted(removes), True)
1233 for f in patches:
1230 for f in patches:
1234 gp = patches[f]
1231 gp = patches[f]
1235 if gp and gp.mode:
1232 if gp and gp.mode:
1236 islink, isexec = gp.mode
1233 islink, isexec = gp.mode
1237 dst = repo.wjoin(gp.path)
1234 dst = repo.wjoin(gp.path)
1238 # patch won't create empty files
1235 # patch won't create empty files
1239 if gp.op == 'ADD' and not os.path.exists(dst):
1236 if gp.op == 'ADD' and not os.path.exists(dst):
1240 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1237 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1241 repo.wwrite(gp.path, '', flags)
1238 repo.wwrite(gp.path, '', flags)
1242 util.set_flags(dst, islink, isexec)
1239 util.set_flags(dst, islink, isexec)
1243 cmdutil.addremove(repo, cfiles, similarity=similarity)
1240 cmdutil.addremove(repo, cfiles, similarity=similarity)
1244 files = patches.keys()
1241 files = patches.keys()
1245 files.extend([r for r in removes if r not in files])
1242 files.extend([r for r in removes if r not in files])
1246 return sorted(files)
1243 return sorted(files)
1247
1244
1248 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1245 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1249 """use <patcher> to apply <patchname> to the working directory.
1246 """use <patcher> to apply <patchname> to the working directory.
1250 returns whether patch was applied with fuzz factor."""
1247 returns whether patch was applied with fuzz factor."""
1251
1248
1252 fuzz = False
1249 fuzz = False
1253 if cwd:
1250 if cwd:
1254 args.append('-d %s' % util.shellquote(cwd))
1251 args.append('-d %s' % util.shellquote(cwd))
1255 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1252 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1256 util.shellquote(patchname)))
1253 util.shellquote(patchname)))
1257
1254
1258 for line in fp:
1255 for line in fp:
1259 line = line.rstrip()
1256 line = line.rstrip()
1260 ui.note(line + '\n')
1257 ui.note(line + '\n')
1261 if line.startswith('patching file '):
1258 if line.startswith('patching file '):
1262 pf = util.parse_patch_output(line)
1259 pf = util.parse_patch_output(line)
1263 printed_file = False
1260 printed_file = False
1264 files.setdefault(pf, None)
1261 files.setdefault(pf, None)
1265 elif line.find('with fuzz') >= 0:
1262 elif line.find('with fuzz') >= 0:
1266 fuzz = True
1263 fuzz = True
1267 if not printed_file:
1264 if not printed_file:
1268 ui.warn(pf + '\n')
1265 ui.warn(pf + '\n')
1269 printed_file = True
1266 printed_file = True
1270 ui.warn(line + '\n')
1267 ui.warn(line + '\n')
1271 elif line.find('saving rejects to file') >= 0:
1268 elif line.find('saving rejects to file') >= 0:
1272 ui.warn(line + '\n')
1269 ui.warn(line + '\n')
1273 elif line.find('FAILED') >= 0:
1270 elif line.find('FAILED') >= 0:
1274 if not printed_file:
1271 if not printed_file:
1275 ui.warn(pf + '\n')
1272 ui.warn(pf + '\n')
1276 printed_file = True
1273 printed_file = True
1277 ui.warn(line + '\n')
1274 ui.warn(line + '\n')
1278 code = fp.close()
1275 code = fp.close()
1279 if code:
1276 if code:
1280 raise PatchError(_("patch command failed: %s") %
1277 raise PatchError(_("patch command failed: %s") %
1281 util.explain_exit(code)[0])
1278 util.explain_exit(code)[0])
1282 return fuzz
1279 return fuzz
1283
1280
1284 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1281 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1285 """use builtin patch to apply <patchobj> to the working directory.
1282 """use builtin patch to apply <patchobj> to the working directory.
1286 returns whether patch was applied with fuzz factor."""
1283 returns whether patch was applied with fuzz factor."""
1287
1284
1288 if files is None:
1285 if files is None:
1289 files = {}
1286 files = {}
1290 if eolmode is None:
1287 if eolmode is None:
1291 eolmode = ui.config('patch', 'eol', 'strict')
1288 eolmode = ui.config('patch', 'eol', 'strict')
1292 if eolmode.lower() not in eolmodes:
1289 if eolmode.lower() not in eolmodes:
1293 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1290 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1294 eolmode = eolmode.lower()
1291 eolmode = eolmode.lower()
1295
1292
1296 try:
1293 try:
1297 fp = open(patchobj, 'rb')
1294 fp = open(patchobj, 'rb')
1298 except TypeError:
1295 except TypeError:
1299 fp = patchobj
1296 fp = patchobj
1300 if cwd:
1297 if cwd:
1301 curdir = os.getcwd()
1298 curdir = os.getcwd()
1302 os.chdir(cwd)
1299 os.chdir(cwd)
1303 try:
1300 try:
1304 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1301 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1305 finally:
1302 finally:
1306 if cwd:
1303 if cwd:
1307 os.chdir(curdir)
1304 os.chdir(curdir)
1308 if fp != patchobj:
1305 if fp != patchobj:
1309 fp.close()
1306 fp.close()
1310 if ret < 0:
1307 if ret < 0:
1311 raise PatchError
1308 raise PatchError
1312 return ret > 0
1309 return ret > 0
1313
1310
1314 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1311 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1315 """Apply <patchname> to the working directory.
1312 """Apply <patchname> to the working directory.
1316
1313
1317 'eolmode' specifies how end of lines should be handled. It can be:
1314 'eolmode' specifies how end of lines should be handled. It can be:
1318 - 'strict': inputs are read in binary mode, EOLs are preserved
1315 - 'strict': inputs are read in binary mode, EOLs are preserved
1319 - 'crlf': EOLs are ignored when patching and reset to CRLF
1316 - 'crlf': EOLs are ignored when patching and reset to CRLF
1320 - 'lf': EOLs are ignored when patching and reset to LF
1317 - 'lf': EOLs are ignored when patching and reset to LF
1321 - None: get it from user settings, default to 'strict'
1318 - None: get it from user settings, default to 'strict'
1322 'eolmode' is ignored when using an external patcher program.
1319 'eolmode' is ignored when using an external patcher program.
1323
1320
1324 Returns whether patch was applied with fuzz factor.
1321 Returns whether patch was applied with fuzz factor.
1325 """
1322 """
1326 patcher = ui.config('ui', 'patch')
1323 patcher = ui.config('ui', 'patch')
1327 args = []
1324 args = []
1328 if files is None:
1325 if files is None:
1329 files = {}
1326 files = {}
1330 try:
1327 try:
1331 if patcher:
1328 if patcher:
1332 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1329 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1333 files)
1330 files)
1334 else:
1331 else:
1335 try:
1332 try:
1336 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1333 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1337 except NoHunks:
1334 except NoHunks:
1338 ui.warn(_('internal patcher failed\n'
1335 ui.warn(_('internal patcher failed\n'
1339 'please report details to '
1336 'please report details to '
1340 'http://mercurial.selenic.com/bts/\n'
1337 'http://mercurial.selenic.com/bts/\n'
1341 'or mercurial@selenic.com\n'))
1338 'or mercurial@selenic.com\n'))
1342 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1339 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1343 or 'patch')
1340 or 'patch')
1344 ui.debug('no valid hunks found; trying with %r instead\n' %
1341 ui.debug('no valid hunks found; trying with %r instead\n' %
1345 patcher)
1342 patcher)
1346 if util.needbinarypatch():
1343 if util.needbinarypatch():
1347 args.append('--binary')
1344 args.append('--binary')
1348 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1345 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1349 files)
1346 files)
1350 except PatchError, err:
1347 except PatchError, err:
1351 s = str(err)
1348 s = str(err)
1352 if s:
1349 if s:
1353 raise util.Abort(s)
1350 raise util.Abort(s)
1354 else:
1351 else:
1355 raise util.Abort(_('patch failed to apply'))
1352 raise util.Abort(_('patch failed to apply'))
1356
1353
1357 def b85diff(to, tn):
1354 def b85diff(to, tn):
1358 '''print base85-encoded binary diff'''
1355 '''print base85-encoded binary diff'''
1359 def gitindex(text):
1356 def gitindex(text):
1360 if not text:
1357 if not text:
1361 return '0' * 40
1358 return '0' * 40
1362 l = len(text)
1359 l = len(text)
1363 s = util.sha1('blob %d\0' % l)
1360 s = util.sha1('blob %d\0' % l)
1364 s.update(text)
1361 s.update(text)
1365 return s.hexdigest()
1362 return s.hexdigest()
1366
1363
1367 def fmtline(line):
1364 def fmtline(line):
1368 l = len(line)
1365 l = len(line)
1369 if l <= 26:
1366 if l <= 26:
1370 l = chr(ord('A') + l - 1)
1367 l = chr(ord('A') + l - 1)
1371 else:
1368 else:
1372 l = chr(l - 26 + ord('a') - 1)
1369 l = chr(l - 26 + ord('a') - 1)
1373 return '%c%s\n' % (l, base85.b85encode(line, True))
1370 return '%c%s\n' % (l, base85.b85encode(line, True))
1374
1371
1375 def chunk(text, csize=52):
1372 def chunk(text, csize=52):
1376 l = len(text)
1373 l = len(text)
1377 i = 0
1374 i = 0
1378 while i < l:
1375 while i < l:
1379 yield text[i:i + csize]
1376 yield text[i:i + csize]
1380 i += csize
1377 i += csize
1381
1378
1382 tohash = gitindex(to)
1379 tohash = gitindex(to)
1383 tnhash = gitindex(tn)
1380 tnhash = gitindex(tn)
1384 if tohash == tnhash:
1381 if tohash == tnhash:
1385 return ""
1382 return ""
1386
1383
1387 # TODO: deltas
1384 # TODO: deltas
1388 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1385 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1389 (tohash, tnhash, len(tn))]
1386 (tohash, tnhash, len(tn))]
1390 for l in chunk(zlib.compress(tn)):
1387 for l in chunk(zlib.compress(tn)):
1391 ret.append(fmtline(l))
1388 ret.append(fmtline(l))
1392 ret.append('\n')
1389 ret.append('\n')
1393 return ''.join(ret)
1390 return ''.join(ret)
1394
1391
1395 class GitDiffRequired(Exception):
1392 class GitDiffRequired(Exception):
1396 pass
1393 pass
1397
1394
1398 def diffopts(ui, opts=None, untrusted=False):
1395 def diffopts(ui, opts=None, untrusted=False):
1399 def get(key, name=None, getter=ui.configbool):
1396 def get(key, name=None, getter=ui.configbool):
1400 return ((opts and opts.get(key)) or
1397 return ((opts and opts.get(key)) or
1401 getter('diff', name or key, None, untrusted=untrusted))
1398 getter('diff', name or key, None, untrusted=untrusted))
1402 return mdiff.diffopts(
1399 return mdiff.diffopts(
1403 text=opts and opts.get('text'),
1400 text=opts and opts.get('text'),
1404 git=get('git'),
1401 git=get('git'),
1405 nodates=get('nodates'),
1402 nodates=get('nodates'),
1406 showfunc=get('show_function', 'showfunc'),
1403 showfunc=get('show_function', 'showfunc'),
1407 ignorews=get('ignore_all_space', 'ignorews'),
1404 ignorews=get('ignore_all_space', 'ignorews'),
1408 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1405 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1409 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1406 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1410 context=get('unified', getter=ui.config))
1407 context=get('unified', getter=ui.config))
1411
1408
1412 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1409 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1413 losedatafn=None):
1410 losedatafn=None):
1414 '''yields diff of changes to files between two nodes, or node and
1411 '''yields diff of changes to files between two nodes, or node and
1415 working directory.
1412 working directory.
1416
1413
1417 if node1 is None, use first dirstate parent instead.
1414 if node1 is None, use first dirstate parent instead.
1418 if node2 is None, compare node1 with working directory.
1415 if node2 is None, compare node1 with working directory.
1419
1416
1420 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1417 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1421 every time some change cannot be represented with the current
1418 every time some change cannot be represented with the current
1422 patch format. Return False to upgrade to git patch format, True to
1419 patch format. Return False to upgrade to git patch format, True to
1423 accept the loss or raise an exception to abort the diff. It is
1420 accept the loss or raise an exception to abort the diff. It is
1424 called with the name of current file being diffed as 'fn'. If set
1421 called with the name of current file being diffed as 'fn'. If set
1425 to None, patches will always be upgraded to git format when
1422 to None, patches will always be upgraded to git format when
1426 necessary.
1423 necessary.
1427 '''
1424 '''
1428
1425
1429 if opts is None:
1426 if opts is None:
1430 opts = mdiff.defaultopts
1427 opts = mdiff.defaultopts
1431
1428
1432 if not node1 and not node2:
1429 if not node1 and not node2:
1433 node1 = repo.dirstate.parents()[0]
1430 node1 = repo.dirstate.parents()[0]
1434
1431
1435 def lrugetfilectx():
1432 def lrugetfilectx():
1436 cache = {}
1433 cache = {}
1437 order = []
1434 order = []
1438 def getfilectx(f, ctx):
1435 def getfilectx(f, ctx):
1439 fctx = ctx.filectx(f, filelog=cache.get(f))
1436 fctx = ctx.filectx(f, filelog=cache.get(f))
1440 if f not in cache:
1437 if f not in cache:
1441 if len(cache) > 20:
1438 if len(cache) > 20:
1442 del cache[order.pop(0)]
1439 del cache[order.pop(0)]
1443 cache[f] = fctx.filelog()
1440 cache[f] = fctx.filelog()
1444 else:
1441 else:
1445 order.remove(f)
1442 order.remove(f)
1446 order.append(f)
1443 order.append(f)
1447 return fctx
1444 return fctx
1448 return getfilectx
1445 return getfilectx
1449 getfilectx = lrugetfilectx()
1446 getfilectx = lrugetfilectx()
1450
1447
1451 ctx1 = repo[node1]
1448 ctx1 = repo[node1]
1452 ctx2 = repo[node2]
1449 ctx2 = repo[node2]
1453
1450
1454 if not changes:
1451 if not changes:
1455 changes = repo.status(ctx1, ctx2, match=match)
1452 changes = repo.status(ctx1, ctx2, match=match)
1456 modified, added, removed = changes[:3]
1453 modified, added, removed = changes[:3]
1457
1454
1458 if not modified and not added and not removed:
1455 if not modified and not added and not removed:
1459 return []
1456 return []
1460
1457
1461 revs = None
1458 revs = None
1462 if not repo.ui.quiet:
1459 if not repo.ui.quiet:
1463 hexfunc = repo.ui.debugflag and hex or short
1460 hexfunc = repo.ui.debugflag and hex or short
1464 revs = [hexfunc(node) for node in [node1, node2] if node]
1461 revs = [hexfunc(node) for node in [node1, node2] if node]
1465
1462
1466 copy = {}
1463 copy = {}
1467 if opts.git or opts.upgrade:
1464 if opts.git or opts.upgrade:
1468 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1465 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1469
1466
1470 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1467 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1471 modified, added, removed, copy, getfilectx, opts, losedata)
1468 modified, added, removed, copy, getfilectx, opts, losedata)
1472 if opts.upgrade and not opts.git:
1469 if opts.upgrade and not opts.git:
1473 try:
1470 try:
1474 def losedata(fn):
1471 def losedata(fn):
1475 if not losedatafn or not losedatafn(fn=fn):
1472 if not losedatafn or not losedatafn(fn=fn):
1476 raise GitDiffRequired()
1473 raise GitDiffRequired()
1477 # Buffer the whole output until we are sure it can be generated
1474 # Buffer the whole output until we are sure it can be generated
1478 return list(difffn(opts.copy(git=False), losedata))
1475 return list(difffn(opts.copy(git=False), losedata))
1479 except GitDiffRequired:
1476 except GitDiffRequired:
1480 return difffn(opts.copy(git=True), None)
1477 return difffn(opts.copy(git=True), None)
1481 else:
1478 else:
1482 return difffn(opts, None)
1479 return difffn(opts, None)
1483
1480
1484 def difflabel(func, *args, **kw):
1481 def difflabel(func, *args, **kw):
1485 '''yields 2-tuples of (output, label) based on the output of func()'''
1482 '''yields 2-tuples of (output, label) based on the output of func()'''
1486 prefixes = [('diff', 'diff.diffline'),
1483 prefixes = [('diff', 'diff.diffline'),
1487 ('copy', 'diff.extended'),
1484 ('copy', 'diff.extended'),
1488 ('rename', 'diff.extended'),
1485 ('rename', 'diff.extended'),
1489 ('old', 'diff.extended'),
1486 ('old', 'diff.extended'),
1490 ('new', 'diff.extended'),
1487 ('new', 'diff.extended'),
1491 ('deleted', 'diff.extended'),
1488 ('deleted', 'diff.extended'),
1492 ('---', 'diff.file_a'),
1489 ('---', 'diff.file_a'),
1493 ('+++', 'diff.file_b'),
1490 ('+++', 'diff.file_b'),
1494 ('@@', 'diff.hunk'),
1491 ('@@', 'diff.hunk'),
1495 ('-', 'diff.deleted'),
1492 ('-', 'diff.deleted'),
1496 ('+', 'diff.inserted')]
1493 ('+', 'diff.inserted')]
1497
1494
1498 for chunk in func(*args, **kw):
1495 for chunk in func(*args, **kw):
1499 lines = chunk.split('\n')
1496 lines = chunk.split('\n')
1500 for i, line in enumerate(lines):
1497 for i, line in enumerate(lines):
1501 if i != 0:
1498 if i != 0:
1502 yield ('\n', '')
1499 yield ('\n', '')
1503 stripline = line
1500 stripline = line
1504 if line and line[0] in '+-':
1501 if line and line[0] in '+-':
1505 # highlight trailing whitespace, but only in changed lines
1502 # highlight trailing whitespace, but only in changed lines
1506 stripline = line.rstrip()
1503 stripline = line.rstrip()
1507 for prefix, label in prefixes:
1504 for prefix, label in prefixes:
1508 if stripline.startswith(prefix):
1505 if stripline.startswith(prefix):
1509 yield (stripline, label)
1506 yield (stripline, label)
1510 break
1507 break
1511 else:
1508 else:
1512 yield (line, '')
1509 yield (line, '')
1513 if line != stripline:
1510 if line != stripline:
1514 yield (line[len(stripline):], 'diff.trailingwhitespace')
1511 yield (line[len(stripline):], 'diff.trailingwhitespace')
1515
1512
1516 def diffui(*args, **kw):
1513 def diffui(*args, **kw):
1517 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1514 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1518 return difflabel(diff, *args, **kw)
1515 return difflabel(diff, *args, **kw)
1519
1516
1520
1517
1521 def _addmodehdr(header, omode, nmode):
1518 def _addmodehdr(header, omode, nmode):
1522 if omode != nmode:
1519 if omode != nmode:
1523 header.append('old mode %s\n' % omode)
1520 header.append('old mode %s\n' % omode)
1524 header.append('new mode %s\n' % nmode)
1521 header.append('new mode %s\n' % nmode)
1525
1522
1526 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1523 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1527 copy, getfilectx, opts, losedatafn):
1524 copy, getfilectx, opts, losedatafn):
1528
1525
1529 date1 = util.datestr(ctx1.date())
1526 date1 = util.datestr(ctx1.date())
1530 man1 = ctx1.manifest()
1527 man1 = ctx1.manifest()
1531
1528
1532 gone = set()
1529 gone = set()
1533 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1530 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1534
1531
1535 copyto = dict([(v, k) for k, v in copy.items()])
1532 copyto = dict([(v, k) for k, v in copy.items()])
1536
1533
1537 if opts.git:
1534 if opts.git:
1538 revs = None
1535 revs = None
1539
1536
1540 for f in sorted(modified + added + removed):
1537 for f in sorted(modified + added + removed):
1541 to = None
1538 to = None
1542 tn = None
1539 tn = None
1543 dodiff = True
1540 dodiff = True
1544 header = []
1541 header = []
1545 if f in man1:
1542 if f in man1:
1546 to = getfilectx(f, ctx1).data()
1543 to = getfilectx(f, ctx1).data()
1547 if f not in removed:
1544 if f not in removed:
1548 tn = getfilectx(f, ctx2).data()
1545 tn = getfilectx(f, ctx2).data()
1549 a, b = f, f
1546 a, b = f, f
1550 if opts.git or losedatafn:
1547 if opts.git or losedatafn:
1551 if f in added:
1548 if f in added:
1552 mode = gitmode[ctx2.flags(f)]
1549 mode = gitmode[ctx2.flags(f)]
1553 if f in copy or f in copyto:
1550 if f in copy or f in copyto:
1554 if opts.git:
1551 if opts.git:
1555 if f in copy:
1552 if f in copy:
1556 a = copy[f]
1553 a = copy[f]
1557 else:
1554 else:
1558 a = copyto[f]
1555 a = copyto[f]
1559 omode = gitmode[man1.flags(a)]
1556 omode = gitmode[man1.flags(a)]
1560 _addmodehdr(header, omode, mode)
1557 _addmodehdr(header, omode, mode)
1561 if a in removed and a not in gone:
1558 if a in removed and a not in gone:
1562 op = 'rename'
1559 op = 'rename'
1563 gone.add(a)
1560 gone.add(a)
1564 else:
1561 else:
1565 op = 'copy'
1562 op = 'copy'
1566 header.append('%s from %s\n' % (op, a))
1563 header.append('%s from %s\n' % (op, a))
1567 header.append('%s to %s\n' % (op, f))
1564 header.append('%s to %s\n' % (op, f))
1568 to = getfilectx(a, ctx1).data()
1565 to = getfilectx(a, ctx1).data()
1569 else:
1566 else:
1570 losedatafn(f)
1567 losedatafn(f)
1571 else:
1568 else:
1572 if opts.git:
1569 if opts.git:
1573 header.append('new file mode %s\n' % mode)
1570 header.append('new file mode %s\n' % mode)
1574 elif ctx2.flags(f):
1571 elif ctx2.flags(f):
1575 losedatafn(f)
1572 losedatafn(f)
1576 if util.binary(tn):
1573 if util.binary(tn):
1577 if opts.git:
1574 if opts.git:
1578 dodiff = 'binary'
1575 dodiff = 'binary'
1579 else:
1576 else:
1580 losedatafn(f)
1577 losedatafn(f)
1581 if not opts.git and not tn:
1578 if not opts.git and not tn:
1582 # regular diffs cannot represent new empty file
1579 # regular diffs cannot represent new empty file
1583 losedatafn(f)
1580 losedatafn(f)
1584 elif f in removed:
1581 elif f in removed:
1585 if opts.git:
1582 if opts.git:
1586 # have we already reported a copy above?
1583 # have we already reported a copy above?
1587 if ((f in copy and copy[f] in added
1584 if ((f in copy and copy[f] in added
1588 and copyto[copy[f]] == f) or
1585 and copyto[copy[f]] == f) or
1589 (f in copyto and copyto[f] in added
1586 (f in copyto and copyto[f] in added
1590 and copy[copyto[f]] == f)):
1587 and copy[copyto[f]] == f)):
1591 dodiff = False
1588 dodiff = False
1592 else:
1589 else:
1593 header.append('deleted file mode %s\n' %
1590 header.append('deleted file mode %s\n' %
1594 gitmode[man1.flags(f)])
1591 gitmode[man1.flags(f)])
1595 elif not to:
1592 elif not to:
1596 # regular diffs cannot represent empty file deletion
1593 # regular diffs cannot represent empty file deletion
1597 losedatafn(f)
1594 losedatafn(f)
1598 else:
1595 else:
1599 oflag = man1.flags(f)
1596 oflag = man1.flags(f)
1600 nflag = ctx2.flags(f)
1597 nflag = ctx2.flags(f)
1601 binary = util.binary(to) or util.binary(tn)
1598 binary = util.binary(to) or util.binary(tn)
1602 if opts.git:
1599 if opts.git:
1603 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1600 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1604 if binary:
1601 if binary:
1605 dodiff = 'binary'
1602 dodiff = 'binary'
1606 elif binary or nflag != oflag:
1603 elif binary or nflag != oflag:
1607 losedatafn(f)
1604 losedatafn(f)
1608 if opts.git:
1605 if opts.git:
1609 header.insert(0, mdiff.diffline(revs, a, b, opts))
1606 header.insert(0, mdiff.diffline(revs, a, b, opts))
1610
1607
1611 if dodiff:
1608 if dodiff:
1612 if dodiff == 'binary':
1609 if dodiff == 'binary':
1613 text = b85diff(to, tn)
1610 text = b85diff(to, tn)
1614 else:
1611 else:
1615 text = mdiff.unidiff(to, date1,
1612 text = mdiff.unidiff(to, date1,
1616 # ctx2 date may be dynamic
1613 # ctx2 date may be dynamic
1617 tn, util.datestr(ctx2.date()),
1614 tn, util.datestr(ctx2.date()),
1618 a, b, revs, opts=opts)
1615 a, b, revs, opts=opts)
1619 if header and (text or len(header) > 1):
1616 if header and (text or len(header) > 1):
1620 yield ''.join(header)
1617 yield ''.join(header)
1621 if text:
1618 if text:
1622 yield text
1619 yield text
1623
1620
1624 def diffstatdata(lines):
1621 def diffstatdata(lines):
1625 filename, adds, removes = None, 0, 0
1622 filename, adds, removes = None, 0, 0
1626 for line in lines:
1623 for line in lines:
1627 if line.startswith('diff'):
1624 if line.startswith('diff'):
1628 if filename:
1625 if filename:
1629 isbinary = adds == 0 and removes == 0
1626 isbinary = adds == 0 and removes == 0
1630 yield (filename, adds, removes, isbinary)
1627 yield (filename, adds, removes, isbinary)
1631 # set numbers to 0 anyway when starting new file
1628 # set numbers to 0 anyway when starting new file
1632 adds, removes = 0, 0
1629 adds, removes = 0, 0
1633 if line.startswith('diff --git'):
1630 if line.startswith('diff --git'):
1634 filename = gitre.search(line).group(1)
1631 filename = gitre.search(line).group(1)
1635 else:
1632 else:
1636 # format: "diff -r ... -r ... filename"
1633 # format: "diff -r ... -r ... filename"
1637 filename = line.split(None, 5)[-1]
1634 filename = line.split(None, 5)[-1]
1638 elif line.startswith('+') and not line.startswith('+++'):
1635 elif line.startswith('+') and not line.startswith('+++'):
1639 adds += 1
1636 adds += 1
1640 elif line.startswith('-') and not line.startswith('---'):
1637 elif line.startswith('-') and not line.startswith('---'):
1641 removes += 1
1638 removes += 1
1642 if filename:
1639 if filename:
1643 isbinary = adds == 0 and removes == 0
1640 isbinary = adds == 0 and removes == 0
1644 yield (filename, adds, removes, isbinary)
1641 yield (filename, adds, removes, isbinary)
1645
1642
1646 def diffstat(lines, width=80, git=False):
1643 def diffstat(lines, width=80, git=False):
1647 output = []
1644 output = []
1648 stats = list(diffstatdata(lines))
1645 stats = list(diffstatdata(lines))
1649
1646
1650 maxtotal, maxname = 0, 0
1647 maxtotal, maxname = 0, 0
1651 totaladds, totalremoves = 0, 0
1648 totaladds, totalremoves = 0, 0
1652 hasbinary = False
1649 hasbinary = False
1653 for filename, adds, removes, isbinary in stats:
1650 for filename, adds, removes, isbinary in stats:
1654 totaladds += adds
1651 totaladds += adds
1655 totalremoves += removes
1652 totalremoves += removes
1656 maxname = max(maxname, len(filename))
1653 maxname = max(maxname, len(filename))
1657 maxtotal = max(maxtotal, adds + removes)
1654 maxtotal = max(maxtotal, adds + removes)
1658 if isbinary:
1655 if isbinary:
1659 hasbinary = True
1656 hasbinary = True
1660
1657
1661 countwidth = len(str(maxtotal))
1658 countwidth = len(str(maxtotal))
1662 if hasbinary and countwidth < 3:
1659 if hasbinary and countwidth < 3:
1663 countwidth = 3
1660 countwidth = 3
1664 graphwidth = width - countwidth - maxname - 6
1661 graphwidth = width - countwidth - maxname - 6
1665 if graphwidth < 10:
1662 if graphwidth < 10:
1666 graphwidth = 10
1663 graphwidth = 10
1667
1664
1668 def scale(i):
1665 def scale(i):
1669 if maxtotal <= graphwidth:
1666 if maxtotal <= graphwidth:
1670 return i
1667 return i
1671 # If diffstat runs out of room it doesn't print anything,
1668 # If diffstat runs out of room it doesn't print anything,
1672 # which isn't very useful, so always print at least one + or -
1669 # which isn't very useful, so always print at least one + or -
1673 # if there were at least some changes.
1670 # if there were at least some changes.
1674 return max(i * graphwidth // maxtotal, int(bool(i)))
1671 return max(i * graphwidth // maxtotal, int(bool(i)))
1675
1672
1676 for filename, adds, removes, isbinary in stats:
1673 for filename, adds, removes, isbinary in stats:
1677 if git and isbinary:
1674 if git and isbinary:
1678 count = 'Bin'
1675 count = 'Bin'
1679 else:
1676 else:
1680 count = adds + removes
1677 count = adds + removes
1681 pluses = '+' * scale(adds)
1678 pluses = '+' * scale(adds)
1682 minuses = '-' * scale(removes)
1679 minuses = '-' * scale(removes)
1683 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1680 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1684 count, pluses, minuses))
1681 count, pluses, minuses))
1685
1682
1686 if stats:
1683 if stats:
1687 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1684 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1688 % (len(stats), totaladds, totalremoves))
1685 % (len(stats), totaladds, totalremoves))
1689
1686
1690 return ''.join(output)
1687 return ''.join(output)
1691
1688
1692 def diffstatui(*args, **kw):
1689 def diffstatui(*args, **kw):
1693 '''like diffstat(), but yields 2-tuples of (output, label) for
1690 '''like diffstat(), but yields 2-tuples of (output, label) for
1694 ui.write()
1691 ui.write()
1695 '''
1692 '''
1696
1693
1697 for line in diffstat(*args, **kw).splitlines():
1694 for line in diffstat(*args, **kw).splitlines():
1698 if line and line[-1] in '+-':
1695 if line and line[-1] in '+-':
1699 name, graph = line.rsplit(' ', 1)
1696 name, graph = line.rsplit(' ', 1)
1700 yield (name + ' ', '')
1697 yield (name + ' ', '')
1701 m = re.search(r'\++', graph)
1698 m = re.search(r'\++', graph)
1702 if m:
1699 if m:
1703 yield (m.group(0), 'diffstat.inserted')
1700 yield (m.group(0), 'diffstat.inserted')
1704 m = re.search(r'-+', graph)
1701 m = re.search(r'-+', graph)
1705 if m:
1702 if m:
1706 yield (m.group(0), 'diffstat.deleted')
1703 yield (m.group(0), 'diffstat.deleted')
1707 else:
1704 else:
1708 yield (line, '')
1705 yield (line, '')
1709 yield ('\n', '')
1706 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now