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