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