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