##// END OF EJS Templates
patch: simplify externalpatch() arguments
Patrick Mezard -
r12673:9ad16d1b default
parent child Browse files
Show More
@@ -1,1633 +1,1632 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'):
231 if line.startswith('# HG changeset patch'):
232 ui.debug('patch generated by hg export\n')
232 ui.debug('patch generated by hg export\n')
233 hgpatchheader = True
233 hgpatchheader = True
234 # drop earlier commit message content
234 # drop earlier commit message content
235 cfp.seek(0)
235 cfp.seek(0)
236 cfp.truncate()
236 cfp.truncate()
237 subject = None
237 subject = None
238 elif hgpatchheader:
238 elif hgpatchheader:
239 if line.startswith('# User '):
239 if line.startswith('# User '):
240 user = line[7:]
240 user = line[7:]
241 ui.debug('From: %s\n' % user)
241 ui.debug('From: %s\n' % user)
242 elif line.startswith("# Date "):
242 elif line.startswith("# Date "):
243 date = line[7:]
243 date = line[7:]
244 elif line.startswith("# Branch "):
244 elif line.startswith("# Branch "):
245 branch = line[9:]
245 branch = line[9:]
246 elif line.startswith("# Node ID "):
246 elif line.startswith("# Node ID "):
247 nodeid = line[10:]
247 nodeid = line[10:]
248 elif line.startswith("# Parent "):
248 elif line.startswith("# Parent "):
249 parents.append(line[10:])
249 parents.append(line[10:])
250 elif not line.startswith("# "):
250 elif not line.startswith("# "):
251 hgpatchheader = False
251 hgpatchheader = False
252 hgpatch = True
252 hgpatch = True
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 lr.push(l)
689 lr.push(l)
690 break
690 break
691 s = l[2:]
691 s = l[2:]
692 if l.startswith('- ') or l.startswith('! '):
692 if l.startswith('- ') or l.startswith('! '):
693 u = '-' + s
693 u = '-' + s
694 elif l.startswith(' '):
694 elif l.startswith(' '):
695 u = ' ' + s
695 u = ' ' + s
696 else:
696 else:
697 raise PatchError(_("bad hunk #%d old text line %d") %
697 raise PatchError(_("bad hunk #%d old text line %d") %
698 (self.number, x))
698 (self.number, x))
699 self.a.append(u)
699 self.a.append(u)
700 self.hunk.append(u)
700 self.hunk.append(u)
701
701
702 l = lr.readline()
702 l = lr.readline()
703 if l.startswith('\ '):
703 if l.startswith('\ '):
704 s = self.a[-1][:-1]
704 s = self.a[-1][:-1]
705 self.a[-1] = s
705 self.a[-1] = s
706 self.hunk[-1] = s
706 self.hunk[-1] = s
707 l = lr.readline()
707 l = lr.readline()
708 m = contextdesc.match(l)
708 m = contextdesc.match(l)
709 if not m:
709 if not m:
710 raise PatchError(_("bad hunk #%d") % self.number)
710 raise PatchError(_("bad hunk #%d") % self.number)
711 foo, self.startb, foo2, bend, foo3 = m.groups()
711 foo, self.startb, foo2, bend, foo3 = m.groups()
712 self.startb = int(self.startb)
712 self.startb = int(self.startb)
713 if bend is None:
713 if bend is None:
714 bend = self.startb
714 bend = self.startb
715 self.lenb = int(bend) - self.startb
715 self.lenb = int(bend) - self.startb
716 if self.startb:
716 if self.startb:
717 self.lenb += 1
717 self.lenb += 1
718 hunki = 1
718 hunki = 1
719 for x in xrange(self.lenb):
719 for x in xrange(self.lenb):
720 l = lr.readline()
720 l = lr.readline()
721 if l.startswith('\ '):
721 if l.startswith('\ '):
722 s = self.b[-1][:-1]
722 s = self.b[-1][:-1]
723 self.b[-1] = s
723 self.b[-1] = s
724 self.hunk[hunki - 1] = s
724 self.hunk[hunki - 1] = s
725 continue
725 continue
726 if not l:
726 if not l:
727 lr.push(l)
727 lr.push(l)
728 break
728 break
729 s = l[2:]
729 s = l[2:]
730 if l.startswith('+ ') or l.startswith('! '):
730 if l.startswith('+ ') or l.startswith('! '):
731 u = '+' + s
731 u = '+' + s
732 elif l.startswith(' '):
732 elif l.startswith(' '):
733 u = ' ' + s
733 u = ' ' + s
734 elif len(self.b) == 0:
734 elif len(self.b) == 0:
735 # this can happen when the hunk does not add any lines
735 # this can happen when the hunk does not add any lines
736 lr.push(l)
736 lr.push(l)
737 break
737 break
738 else:
738 else:
739 raise PatchError(_("bad hunk #%d old text line %d") %
739 raise PatchError(_("bad hunk #%d old text line %d") %
740 (self.number, x))
740 (self.number, x))
741 self.b.append(s)
741 self.b.append(s)
742 while True:
742 while True:
743 if hunki >= len(self.hunk):
743 if hunki >= len(self.hunk):
744 h = ""
744 h = ""
745 else:
745 else:
746 h = self.hunk[hunki]
746 h = self.hunk[hunki]
747 hunki += 1
747 hunki += 1
748 if h == u:
748 if h == u:
749 break
749 break
750 elif h.startswith('-'):
750 elif h.startswith('-'):
751 continue
751 continue
752 else:
752 else:
753 self.hunk.insert(hunki - 1, u)
753 self.hunk.insert(hunki - 1, u)
754 break
754 break
755
755
756 if not self.a:
756 if not self.a:
757 # this happens when lines were only added to the hunk
757 # this happens when lines were only added to the hunk
758 for x in self.hunk:
758 for x in self.hunk:
759 if x.startswith('-') or x.startswith(' '):
759 if x.startswith('-') or x.startswith(' '):
760 self.a.append(x)
760 self.a.append(x)
761 if not self.b:
761 if not self.b:
762 # this happens when lines were only deleted from the hunk
762 # this happens when lines were only deleted from 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.b.append(x[1:])
765 self.b.append(x[1:])
766 # @@ -start,len +start,len @@
766 # @@ -start,len +start,len @@
767 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
767 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
768 self.startb, self.lenb)
768 self.startb, self.lenb)
769 self.hunk[0] = self.desc
769 self.hunk[0] = self.desc
770
770
771 def fix_newline(self):
771 def fix_newline(self):
772 diffhelpers.fix_newline(self.hunk, self.a, self.b)
772 diffhelpers.fix_newline(self.hunk, self.a, self.b)
773
773
774 def complete(self):
774 def complete(self):
775 return len(self.a) == self.lena and len(self.b) == self.lenb
775 return len(self.a) == self.lena and len(self.b) == self.lenb
776
776
777 def createfile(self):
777 def createfile(self):
778 return self.starta == 0 and self.lena == 0 and self.create
778 return self.starta == 0 and self.lena == 0 and self.create
779
779
780 def rmfile(self):
780 def rmfile(self):
781 return self.startb == 0 and self.lenb == 0 and self.remove
781 return self.startb == 0 and self.lenb == 0 and self.remove
782
782
783 def fuzzit(self, l, fuzz, toponly):
783 def fuzzit(self, l, fuzz, toponly):
784 # this removes context lines from the top and bottom of list 'l'. It
784 # this removes context lines from the top and bottom of list 'l'. It
785 # checks the hunk to make sure only context lines are removed, and then
785 # checks the hunk to make sure only context lines are removed, and then
786 # returns a new shortened list of lines.
786 # returns a new shortened list of lines.
787 fuzz = min(fuzz, len(l)-1)
787 fuzz = min(fuzz, len(l)-1)
788 if fuzz:
788 if fuzz:
789 top = 0
789 top = 0
790 bot = 0
790 bot = 0
791 hlen = len(self.hunk)
791 hlen = len(self.hunk)
792 for x in xrange(hlen - 1):
792 for x in xrange(hlen - 1):
793 # the hunk starts with the @@ line, so use x+1
793 # the hunk starts with the @@ line, so use x+1
794 if self.hunk[x + 1][0] == ' ':
794 if self.hunk[x + 1][0] == ' ':
795 top += 1
795 top += 1
796 else:
796 else:
797 break
797 break
798 if not toponly:
798 if not toponly:
799 for x in xrange(hlen - 1):
799 for x in xrange(hlen - 1):
800 if self.hunk[hlen - bot - 1][0] == ' ':
800 if self.hunk[hlen - bot - 1][0] == ' ':
801 bot += 1
801 bot += 1
802 else:
802 else:
803 break
803 break
804
804
805 # top and bot now count context in the hunk
805 # top and bot now count context in the hunk
806 # adjust them if either one is short
806 # adjust them if either one is short
807 context = max(top, bot, 3)
807 context = max(top, bot, 3)
808 if bot < context:
808 if bot < context:
809 bot = max(0, fuzz - (context - bot))
809 bot = max(0, fuzz - (context - bot))
810 else:
810 else:
811 bot = min(fuzz, bot)
811 bot = min(fuzz, bot)
812 if top < context:
812 if top < context:
813 top = max(0, fuzz - (context - top))
813 top = max(0, fuzz - (context - top))
814 else:
814 else:
815 top = min(fuzz, top)
815 top = min(fuzz, top)
816
816
817 return l[top:len(l)-bot]
817 return l[top:len(l)-bot]
818 return l
818 return l
819
819
820 def old(self, fuzz=0, toponly=False):
820 def old(self, fuzz=0, toponly=False):
821 return self.fuzzit(self.a, fuzz, toponly)
821 return self.fuzzit(self.a, fuzz, toponly)
822
822
823 def new(self, fuzz=0, toponly=False):
823 def new(self, fuzz=0, toponly=False):
824 return self.fuzzit(self.b, fuzz, toponly)
824 return self.fuzzit(self.b, fuzz, toponly)
825
825
826 class binhunk:
826 class binhunk:
827 'A binary patch file. Only understands literals so far.'
827 'A binary patch file. Only understands literals so far.'
828 def __init__(self, gitpatch):
828 def __init__(self, gitpatch):
829 self.gitpatch = gitpatch
829 self.gitpatch = gitpatch
830 self.text = None
830 self.text = None
831 self.hunk = ['GIT binary patch\n']
831 self.hunk = ['GIT binary patch\n']
832
832
833 def createfile(self):
833 def createfile(self):
834 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
834 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
835
835
836 def rmfile(self):
836 def rmfile(self):
837 return self.gitpatch.op == 'DELETE'
837 return self.gitpatch.op == 'DELETE'
838
838
839 def complete(self):
839 def complete(self):
840 return self.text is not None
840 return self.text is not None
841
841
842 def new(self):
842 def new(self):
843 return [self.text]
843 return [self.text]
844
844
845 def extract(self, lr):
845 def extract(self, lr):
846 line = lr.readline()
846 line = lr.readline()
847 self.hunk.append(line)
847 self.hunk.append(line)
848 while line and not line.startswith('literal '):
848 while line and not line.startswith('literal '):
849 line = lr.readline()
849 line = lr.readline()
850 self.hunk.append(line)
850 self.hunk.append(line)
851 if not line:
851 if not line:
852 raise PatchError(_('could not extract binary patch'))
852 raise PatchError(_('could not extract binary patch'))
853 size = int(line[8:].rstrip())
853 size = int(line[8:].rstrip())
854 dec = []
854 dec = []
855 line = lr.readline()
855 line = lr.readline()
856 self.hunk.append(line)
856 self.hunk.append(line)
857 while len(line) > 1:
857 while len(line) > 1:
858 l = line[0]
858 l = line[0]
859 if l <= 'Z' and l >= 'A':
859 if l <= 'Z' and l >= 'A':
860 l = ord(l) - ord('A') + 1
860 l = ord(l) - ord('A') + 1
861 else:
861 else:
862 l = ord(l) - ord('a') + 27
862 l = ord(l) - ord('a') + 27
863 dec.append(base85.b85decode(line[1:-1])[:l])
863 dec.append(base85.b85decode(line[1:-1])[:l])
864 line = lr.readline()
864 line = lr.readline()
865 self.hunk.append(line)
865 self.hunk.append(line)
866 text = zlib.decompress(''.join(dec))
866 text = zlib.decompress(''.join(dec))
867 if len(text) != size:
867 if len(text) != size:
868 raise PatchError(_('binary patch is %d bytes, not %d') %
868 raise PatchError(_('binary patch is %d bytes, not %d') %
869 len(text), size)
869 len(text), size)
870 self.text = text
870 self.text = text
871
871
872 def parsefilename(str):
872 def parsefilename(str):
873 # --- filename \t|space stuff
873 # --- filename \t|space stuff
874 s = str[4:].rstrip('\r\n')
874 s = str[4:].rstrip('\r\n')
875 i = s.find('\t')
875 i = s.find('\t')
876 if i < 0:
876 if i < 0:
877 i = s.find(' ')
877 i = s.find(' ')
878 if i < 0:
878 if i < 0:
879 return s
879 return s
880 return s[:i]
880 return s[:i]
881
881
882 def pathstrip(path, strip):
882 def pathstrip(path, strip):
883 pathlen = len(path)
883 pathlen = len(path)
884 i = 0
884 i = 0
885 if strip == 0:
885 if strip == 0:
886 return '', path.rstrip()
886 return '', path.rstrip()
887 count = strip
887 count = strip
888 while count > 0:
888 while count > 0:
889 i = path.find('/', i)
889 i = path.find('/', i)
890 if i == -1:
890 if i == -1:
891 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
891 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
892 (count, strip, path))
892 (count, strip, path))
893 i += 1
893 i += 1
894 # consume '//' in the path
894 # consume '//' in the path
895 while i < pathlen - 1 and path[i] == '/':
895 while i < pathlen - 1 and path[i] == '/':
896 i += 1
896 i += 1
897 count -= 1
897 count -= 1
898 return path[:i].lstrip(), path[i:].rstrip()
898 return path[:i].lstrip(), path[i:].rstrip()
899
899
900 def selectfile(afile_orig, bfile_orig, hunk, strip):
900 def selectfile(afile_orig, bfile_orig, hunk, strip):
901 nulla = afile_orig == "/dev/null"
901 nulla = afile_orig == "/dev/null"
902 nullb = bfile_orig == "/dev/null"
902 nullb = bfile_orig == "/dev/null"
903 abase, afile = pathstrip(afile_orig, strip)
903 abase, afile = pathstrip(afile_orig, strip)
904 gooda = not nulla and os.path.lexists(afile)
904 gooda = not nulla and os.path.lexists(afile)
905 bbase, bfile = pathstrip(bfile_orig, strip)
905 bbase, bfile = pathstrip(bfile_orig, strip)
906 if afile == bfile:
906 if afile == bfile:
907 goodb = gooda
907 goodb = gooda
908 else:
908 else:
909 goodb = not nullb and os.path.lexists(bfile)
909 goodb = not nullb and os.path.lexists(bfile)
910 createfunc = hunk.createfile
910 createfunc = hunk.createfile
911 missing = not goodb and not gooda and not createfunc()
911 missing = not goodb and not gooda and not createfunc()
912
912
913 # some diff programs apparently produce patches where the afile is
913 # some diff programs apparently produce patches where the afile is
914 # not /dev/null, but afile starts with bfile
914 # not /dev/null, but afile starts with bfile
915 abasedir = afile[:afile.rfind('/') + 1]
915 abasedir = afile[:afile.rfind('/') + 1]
916 bbasedir = bfile[:bfile.rfind('/') + 1]
916 bbasedir = bfile[:bfile.rfind('/') + 1]
917 if missing and abasedir == bbasedir and afile.startswith(bfile):
917 if missing and abasedir == bbasedir and afile.startswith(bfile):
918 # this isn't very pretty
918 # this isn't very pretty
919 hunk.create = True
919 hunk.create = True
920 if createfunc():
920 if createfunc():
921 missing = False
921 missing = False
922 else:
922 else:
923 hunk.create = False
923 hunk.create = False
924
924
925 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
925 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
926 # diff is between a file and its backup. In this case, the original
926 # diff is between a file and its backup. In this case, the original
927 # file should be patched (see original mpatch code).
927 # file should be patched (see original mpatch code).
928 isbackup = (abase == bbase and bfile.startswith(afile))
928 isbackup = (abase == bbase and bfile.startswith(afile))
929 fname = None
929 fname = None
930 if not missing:
930 if not missing:
931 if gooda and goodb:
931 if gooda and goodb:
932 fname = isbackup and afile or bfile
932 fname = isbackup and afile or bfile
933 elif gooda:
933 elif gooda:
934 fname = afile
934 fname = afile
935
935
936 if not fname:
936 if not fname:
937 if not nullb:
937 if not nullb:
938 fname = isbackup and afile or bfile
938 fname = isbackup and afile or bfile
939 elif not nulla:
939 elif not nulla:
940 fname = afile
940 fname = afile
941 else:
941 else:
942 raise PatchError(_("undefined source and destination files"))
942 raise PatchError(_("undefined source and destination files"))
943
943
944 return fname, missing
944 return fname, missing
945
945
946 def scangitpatch(lr, firstline):
946 def scangitpatch(lr, firstline):
947 """
947 """
948 Git patches can emit:
948 Git patches can emit:
949 - rename a to b
949 - rename a to b
950 - change b
950 - change b
951 - copy a to c
951 - copy a to c
952 - change c
952 - change c
953
953
954 We cannot apply this sequence as-is, the renamed 'a' could not be
954 We cannot apply this sequence as-is, the renamed 'a' could not be
955 found for it would have been renamed already. And we cannot copy
955 found for it would have been renamed already. And we cannot copy
956 from 'b' instead because 'b' would have been changed already. So
956 from 'b' instead because 'b' would have been changed already. So
957 we scan the git patch for copy and rename commands so we can
957 we scan the git patch for copy and rename commands so we can
958 perform the copies ahead of time.
958 perform the copies ahead of time.
959 """
959 """
960 pos = 0
960 pos = 0
961 try:
961 try:
962 pos = lr.fp.tell()
962 pos = lr.fp.tell()
963 fp = lr.fp
963 fp = lr.fp
964 except IOError:
964 except IOError:
965 fp = cStringIO.StringIO(lr.fp.read())
965 fp = cStringIO.StringIO(lr.fp.read())
966 gitlr = linereader(fp, lr.textmode)
966 gitlr = linereader(fp, lr.textmode)
967 gitlr.push(firstline)
967 gitlr.push(firstline)
968 gitpatches = readgitpatch(gitlr)
968 gitpatches = readgitpatch(gitlr)
969 fp.seek(pos)
969 fp.seek(pos)
970 return gitpatches
970 return gitpatches
971
971
972 def iterhunks(ui, fp, sourcefile=None):
972 def iterhunks(ui, fp, sourcefile=None):
973 """Read a patch and yield the following events:
973 """Read a patch and yield the following events:
974 - ("file", afile, bfile, firsthunk): select a new target file.
974 - ("file", afile, bfile, firsthunk): select a new target file.
975 - ("hunk", hunk): a new hunk is ready to be applied, follows a
975 - ("hunk", hunk): a new hunk is ready to be applied, follows a
976 "file" event.
976 "file" event.
977 - ("git", gitchanges): current diff is in git format, gitchanges
977 - ("git", gitchanges): current diff is in git format, gitchanges
978 maps filenames to gitpatch records. Unique event.
978 maps filenames to gitpatch records. Unique event.
979 """
979 """
980 changed = {}
980 changed = {}
981 current_hunk = None
981 current_hunk = None
982 afile = ""
982 afile = ""
983 bfile = ""
983 bfile = ""
984 state = None
984 state = None
985 hunknum = 0
985 hunknum = 0
986 emitfile = False
986 emitfile = False
987 git = False
987 git = False
988
988
989 # our states
989 # our states
990 BFILE = 1
990 BFILE = 1
991 context = None
991 context = None
992 lr = linereader(fp)
992 lr = linereader(fp)
993 # gitworkdone is True if a git operation (copy, rename, ...) was
993 # gitworkdone is True if a git operation (copy, rename, ...) was
994 # performed already for the current file. Useful when the file
994 # performed already for the current file. Useful when the file
995 # section may have no hunk.
995 # section may have no hunk.
996 gitworkdone = False
996 gitworkdone = False
997
997
998 while True:
998 while True:
999 newfile = newgitfile = False
999 newfile = newgitfile = False
1000 x = lr.readline()
1000 x = lr.readline()
1001 if not x:
1001 if not x:
1002 break
1002 break
1003 if current_hunk:
1003 if current_hunk:
1004 if x.startswith('\ '):
1004 if x.startswith('\ '):
1005 current_hunk.fix_newline()
1005 current_hunk.fix_newline()
1006 yield 'hunk', current_hunk
1006 yield 'hunk', current_hunk
1007 current_hunk = None
1007 current_hunk = None
1008 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1008 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1009 ((context is not False) and x.startswith('***************')))):
1009 ((context is not False) and x.startswith('***************')))):
1010 try:
1010 try:
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 except PatchError, err:
1017 except PatchError, err:
1018 ui.debug(err)
1018 ui.debug(err)
1019 current_hunk = None
1019 current_hunk = None
1020 continue
1020 continue
1021 hunknum += 1
1021 hunknum += 1
1022 if emitfile:
1022 if emitfile:
1023 emitfile = False
1023 emitfile = False
1024 yield 'file', (afile, bfile, current_hunk)
1024 yield 'file', (afile, bfile, current_hunk)
1025 elif state == BFILE and x.startswith('GIT binary patch'):
1025 elif state == BFILE and x.startswith('GIT binary patch'):
1026 current_hunk = binhunk(changed[bfile])
1026 current_hunk = binhunk(changed[bfile])
1027 hunknum += 1
1027 hunknum += 1
1028 if emitfile:
1028 if emitfile:
1029 emitfile = False
1029 emitfile = False
1030 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1030 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1031 current_hunk.extract(lr)
1031 current_hunk.extract(lr)
1032 elif x.startswith('diff --git'):
1032 elif x.startswith('diff --git'):
1033 # check for git diff, scanning the whole patch file if needed
1033 # check for git diff, scanning the whole patch file if needed
1034 m = gitre.match(x)
1034 m = gitre.match(x)
1035 gitworkdone = False
1035 gitworkdone = False
1036 if m:
1036 if m:
1037 afile, bfile = m.group(1, 2)
1037 afile, bfile = m.group(1, 2)
1038 if not git:
1038 if not git:
1039 git = True
1039 git = True
1040 gitpatches = scangitpatch(lr, x)
1040 gitpatches = scangitpatch(lr, x)
1041 yield 'git', gitpatches
1041 yield 'git', gitpatches
1042 for gp in gitpatches:
1042 for gp in gitpatches:
1043 changed[gp.path] = gp
1043 changed[gp.path] = gp
1044 # else error?
1044 # else error?
1045 # copy/rename + modify should modify target, not source
1045 # copy/rename + modify should modify target, not source
1046 gp = changed.get(bfile)
1046 gp = changed.get(bfile)
1047 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1047 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1048 or gp.mode):
1048 or gp.mode):
1049 afile = bfile
1049 afile = bfile
1050 gitworkdone = True
1050 gitworkdone = True
1051 newgitfile = True
1051 newgitfile = True
1052 elif x.startswith('---'):
1052 elif x.startswith('---'):
1053 # check for a unified diff
1053 # check for a unified diff
1054 l2 = lr.readline()
1054 l2 = lr.readline()
1055 if not l2.startswith('+++'):
1055 if not l2.startswith('+++'):
1056 lr.push(l2)
1056 lr.push(l2)
1057 continue
1057 continue
1058 newfile = True
1058 newfile = True
1059 context = False
1059 context = False
1060 afile = parsefilename(x)
1060 afile = parsefilename(x)
1061 bfile = parsefilename(l2)
1061 bfile = parsefilename(l2)
1062 elif x.startswith('***'):
1062 elif x.startswith('***'):
1063 # check for a context diff
1063 # check for a context diff
1064 l2 = lr.readline()
1064 l2 = lr.readline()
1065 if not l2.startswith('---'):
1065 if not l2.startswith('---'):
1066 lr.push(l2)
1066 lr.push(l2)
1067 continue
1067 continue
1068 l3 = lr.readline()
1068 l3 = lr.readline()
1069 lr.push(l3)
1069 lr.push(l3)
1070 if not l3.startswith("***************"):
1070 if not l3.startswith("***************"):
1071 lr.push(l2)
1071 lr.push(l2)
1072 continue
1072 continue
1073 newfile = True
1073 newfile = True
1074 context = True
1074 context = True
1075 afile = parsefilename(x)
1075 afile = parsefilename(x)
1076 bfile = parsefilename(l2)
1076 bfile = parsefilename(l2)
1077
1077
1078 if newfile:
1078 if newfile:
1079 gitworkdone = False
1079 gitworkdone = False
1080
1080
1081 if newgitfile or newfile:
1081 if newgitfile or newfile:
1082 emitfile = True
1082 emitfile = True
1083 state = BFILE
1083 state = BFILE
1084 hunknum = 0
1084 hunknum = 0
1085 if current_hunk:
1085 if current_hunk:
1086 if current_hunk.complete():
1086 if current_hunk.complete():
1087 yield 'hunk', current_hunk
1087 yield 'hunk', current_hunk
1088 else:
1088 else:
1089 raise PatchError(_("malformed patch %s %s") % (afile,
1089 raise PatchError(_("malformed patch %s %s") % (afile,
1090 current_hunk.desc))
1090 current_hunk.desc))
1091
1091
1092 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1092 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1093 """Reads a patch from fp and tries to apply it.
1093 """Reads a patch from fp and tries to apply it.
1094
1094
1095 The dict 'changed' is filled in with all of the filenames changed
1095 The dict 'changed' is filled in with all of the filenames changed
1096 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1096 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1097 found and 1 if there was any fuzz.
1097 found and 1 if there was any fuzz.
1098
1098
1099 If 'eolmode' is 'strict', the patch content and patched file are
1099 If 'eolmode' is 'strict', the patch content and patched file are
1100 read in binary mode. Otherwise, line endings are ignored when
1100 read in binary mode. Otherwise, line endings are ignored when
1101 patching then normalized according to 'eolmode'.
1101 patching then normalized according to 'eolmode'.
1102
1102
1103 Callers probably want to call 'cmdutil.updatedir' after this to
1103 Callers probably want to call 'cmdutil.updatedir' after this to
1104 apply certain categories of changes not done by this function.
1104 apply certain categories of changes not done by this function.
1105 """
1105 """
1106 return _applydiff(
1106 return _applydiff(
1107 ui, fp, patchfile, copyfile,
1107 ui, fp, patchfile, copyfile,
1108 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1108 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1109
1109
1110
1110
1111 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1111 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1112 sourcefile=None, eolmode='strict'):
1112 sourcefile=None, eolmode='strict'):
1113 rejects = 0
1113 rejects = 0
1114 err = 0
1114 err = 0
1115 current_file = None
1115 current_file = None
1116 cwd = os.getcwd()
1116 cwd = os.getcwd()
1117 opener = util.opener(cwd)
1117 opener = util.opener(cwd)
1118
1118
1119 def closefile():
1119 def closefile():
1120 if not current_file:
1120 if not current_file:
1121 return 0
1121 return 0
1122 if current_file.dirty:
1122 if current_file.dirty:
1123 current_file.writelines(current_file.fname, current_file.lines)
1123 current_file.writelines(current_file.fname, current_file.lines)
1124 current_file.write_rej()
1124 current_file.write_rej()
1125 return len(current_file.rej)
1125 return len(current_file.rej)
1126
1126
1127 for state, values in iterhunks(ui, fp, sourcefile):
1127 for state, values in iterhunks(ui, fp, sourcefile):
1128 if state == 'hunk':
1128 if state == 'hunk':
1129 if not current_file:
1129 if not current_file:
1130 continue
1130 continue
1131 ret = current_file.apply(values)
1131 ret = current_file.apply(values)
1132 if ret >= 0:
1132 if ret >= 0:
1133 changed.setdefault(current_file.fname, None)
1133 changed.setdefault(current_file.fname, None)
1134 if ret > 0:
1134 if ret > 0:
1135 err = 1
1135 err = 1
1136 elif state == 'file':
1136 elif state == 'file':
1137 rejects += closefile()
1137 rejects += closefile()
1138 afile, bfile, first_hunk = values
1138 afile, bfile, first_hunk = values
1139 try:
1139 try:
1140 if sourcefile:
1140 if sourcefile:
1141 current_file = patcher(ui, sourcefile, opener,
1141 current_file = patcher(ui, sourcefile, opener,
1142 eolmode=eolmode)
1142 eolmode=eolmode)
1143 else:
1143 else:
1144 current_file, missing = selectfile(afile, bfile,
1144 current_file, missing = selectfile(afile, bfile,
1145 first_hunk, strip)
1145 first_hunk, strip)
1146 current_file = patcher(ui, current_file, opener,
1146 current_file = patcher(ui, current_file, opener,
1147 missing=missing, eolmode=eolmode)
1147 missing=missing, eolmode=eolmode)
1148 except PatchError, err:
1148 except PatchError, err:
1149 ui.warn(str(err) + '\n')
1149 ui.warn(str(err) + '\n')
1150 current_file = None
1150 current_file = None
1151 rejects += 1
1151 rejects += 1
1152 continue
1152 continue
1153 elif state == 'git':
1153 elif state == 'git':
1154 for gp in values:
1154 for gp in values:
1155 gp.path = pathstrip(gp.path, strip - 1)[1]
1155 gp.path = pathstrip(gp.path, strip - 1)[1]
1156 if gp.oldpath:
1156 if gp.oldpath:
1157 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1157 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1158 # Binary patches really overwrite target files, copying them
1158 # Binary patches really overwrite target files, copying them
1159 # will just make it fails with "target file exists"
1159 # will just make it fails with "target file exists"
1160 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1160 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1161 copyfn(gp.oldpath, gp.path, cwd)
1161 copyfn(gp.oldpath, gp.path, cwd)
1162 changed[gp.path] = gp
1162 changed[gp.path] = gp
1163 else:
1163 else:
1164 raise util.Abort(_('unsupported parser state: %s') % state)
1164 raise util.Abort(_('unsupported parser state: %s') % state)
1165
1165
1166 rejects += closefile()
1166 rejects += closefile()
1167
1167
1168 if rejects:
1168 if rejects:
1169 return -1
1169 return -1
1170 return err
1170 return err
1171
1171
1172 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1172 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1173 """use <patcher> to apply <patchname> to the working directory.
1173 """use <patcher> to apply <patchname> to the working directory.
1174 returns whether patch was applied with fuzz factor."""
1174 returns whether patch was applied with fuzz factor."""
1175
1175
1176 fuzz = False
1176 fuzz = False
1177 args = []
1177 if cwd:
1178 if cwd:
1178 args.append('-d %s' % util.shellquote(cwd))
1179 args.append('-d %s' % util.shellquote(cwd))
1179 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1180 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1180 util.shellquote(patchname)))
1181 util.shellquote(patchname)))
1181
1182
1182 for line in fp:
1183 for line in fp:
1183 line = line.rstrip()
1184 line = line.rstrip()
1184 ui.note(line + '\n')
1185 ui.note(line + '\n')
1185 if line.startswith('patching file '):
1186 if line.startswith('patching file '):
1186 pf = util.parse_patch_output(line)
1187 pf = util.parse_patch_output(line)
1187 printed_file = False
1188 printed_file = False
1188 files.setdefault(pf, None)
1189 files.setdefault(pf, None)
1189 elif line.find('with fuzz') >= 0:
1190 elif line.find('with fuzz') >= 0:
1190 fuzz = True
1191 fuzz = True
1191 if not printed_file:
1192 if not printed_file:
1192 ui.warn(pf + '\n')
1193 ui.warn(pf + '\n')
1193 printed_file = True
1194 printed_file = True
1194 ui.warn(line + '\n')
1195 ui.warn(line + '\n')
1195 elif line.find('saving rejects to file') >= 0:
1196 elif line.find('saving rejects to file') >= 0:
1196 ui.warn(line + '\n')
1197 ui.warn(line + '\n')
1197 elif line.find('FAILED') >= 0:
1198 elif line.find('FAILED') >= 0:
1198 if not printed_file:
1199 if not printed_file:
1199 ui.warn(pf + '\n')
1200 ui.warn(pf + '\n')
1200 printed_file = True
1201 printed_file = True
1201 ui.warn(line + '\n')
1202 ui.warn(line + '\n')
1202 code = fp.close()
1203 code = fp.close()
1203 if code:
1204 if code:
1204 raise PatchError(_("patch command failed: %s") %
1205 raise PatchError(_("patch command failed: %s") %
1205 util.explain_exit(code)[0])
1206 util.explain_exit(code)[0])
1206 return fuzz
1207 return fuzz
1207
1208
1208 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1209 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1209 """use builtin patch to apply <patchobj> to the working directory.
1210 """use builtin patch to apply <patchobj> to the working directory.
1210 returns whether patch was applied with fuzz factor."""
1211 returns whether patch was applied with fuzz factor."""
1211
1212
1212 if files is None:
1213 if files is None:
1213 files = {}
1214 files = {}
1214 if eolmode is None:
1215 if eolmode is None:
1215 eolmode = ui.config('patch', 'eol', 'strict')
1216 eolmode = ui.config('patch', 'eol', 'strict')
1216 if eolmode.lower() not in eolmodes:
1217 if eolmode.lower() not in eolmodes:
1217 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1218 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1218 eolmode = eolmode.lower()
1219 eolmode = eolmode.lower()
1219
1220
1220 try:
1221 try:
1221 fp = open(patchobj, 'rb')
1222 fp = open(patchobj, 'rb')
1222 except TypeError:
1223 except TypeError:
1223 fp = patchobj
1224 fp = patchobj
1224 if cwd:
1225 if cwd:
1225 curdir = os.getcwd()
1226 curdir = os.getcwd()
1226 os.chdir(cwd)
1227 os.chdir(cwd)
1227 try:
1228 try:
1228 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1229 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1229 finally:
1230 finally:
1230 if cwd:
1231 if cwd:
1231 os.chdir(curdir)
1232 os.chdir(curdir)
1232 if fp != patchobj:
1233 if fp != patchobj:
1233 fp.close()
1234 fp.close()
1234 if ret < 0:
1235 if ret < 0:
1235 raise PatchError
1236 raise PatchError
1236 return ret > 0
1237 return ret > 0
1237
1238
1238 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1239 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1239 """Apply <patchname> to the working directory.
1240 """Apply <patchname> to the working directory.
1240
1241
1241 'eolmode' specifies how end of lines should be handled. It can be:
1242 'eolmode' specifies how end of lines should be handled. It can be:
1242 - 'strict': inputs are read in binary mode, EOLs are preserved
1243 - 'strict': inputs are read in binary mode, EOLs are preserved
1243 - 'crlf': EOLs are ignored when patching and reset to CRLF
1244 - 'crlf': EOLs are ignored when patching and reset to CRLF
1244 - 'lf': EOLs are ignored when patching and reset to LF
1245 - 'lf': EOLs are ignored when patching and reset to LF
1245 - None: get it from user settings, default to 'strict'
1246 - None: get it from user settings, default to 'strict'
1246 'eolmode' is ignored when using an external patcher program.
1247 'eolmode' is ignored when using an external patcher program.
1247
1248
1248 Returns whether patch was applied with fuzz factor.
1249 Returns whether patch was applied with fuzz factor.
1249 """
1250 """
1250 patcher = ui.config('ui', 'patch')
1251 patcher = ui.config('ui', 'patch')
1251 args = []
1252 if files is None:
1252 if files is None:
1253 files = {}
1253 files = {}
1254 try:
1254 try:
1255 if patcher:
1255 if patcher:
1256 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1256 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1257 files)
1258 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1257 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1259 except PatchError, err:
1258 except PatchError, err:
1260 s = str(err)
1259 s = str(err)
1261 if s:
1260 if s:
1262 raise util.Abort(s)
1261 raise util.Abort(s)
1263 else:
1262 else:
1264 raise util.Abort(_('patch failed to apply'))
1263 raise util.Abort(_('patch failed to apply'))
1265
1264
1266 def b85diff(to, tn):
1265 def b85diff(to, tn):
1267 '''print base85-encoded binary diff'''
1266 '''print base85-encoded binary diff'''
1268 def gitindex(text):
1267 def gitindex(text):
1269 if not text:
1268 if not text:
1270 return hex(nullid)
1269 return hex(nullid)
1271 l = len(text)
1270 l = len(text)
1272 s = util.sha1('blob %d\0' % l)
1271 s = util.sha1('blob %d\0' % l)
1273 s.update(text)
1272 s.update(text)
1274 return s.hexdigest()
1273 return s.hexdigest()
1275
1274
1276 def fmtline(line):
1275 def fmtline(line):
1277 l = len(line)
1276 l = len(line)
1278 if l <= 26:
1277 if l <= 26:
1279 l = chr(ord('A') + l - 1)
1278 l = chr(ord('A') + l - 1)
1280 else:
1279 else:
1281 l = chr(l - 26 + ord('a') - 1)
1280 l = chr(l - 26 + ord('a') - 1)
1282 return '%c%s\n' % (l, base85.b85encode(line, True))
1281 return '%c%s\n' % (l, base85.b85encode(line, True))
1283
1282
1284 def chunk(text, csize=52):
1283 def chunk(text, csize=52):
1285 l = len(text)
1284 l = len(text)
1286 i = 0
1285 i = 0
1287 while i < l:
1286 while i < l:
1288 yield text[i:i + csize]
1287 yield text[i:i + csize]
1289 i += csize
1288 i += csize
1290
1289
1291 tohash = gitindex(to)
1290 tohash = gitindex(to)
1292 tnhash = gitindex(tn)
1291 tnhash = gitindex(tn)
1293 if tohash == tnhash:
1292 if tohash == tnhash:
1294 return ""
1293 return ""
1295
1294
1296 # TODO: deltas
1295 # TODO: deltas
1297 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1296 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1298 (tohash, tnhash, len(tn))]
1297 (tohash, tnhash, len(tn))]
1299 for l in chunk(zlib.compress(tn)):
1298 for l in chunk(zlib.compress(tn)):
1300 ret.append(fmtline(l))
1299 ret.append(fmtline(l))
1301 ret.append('\n')
1300 ret.append('\n')
1302 return ''.join(ret)
1301 return ''.join(ret)
1303
1302
1304 class GitDiffRequired(Exception):
1303 class GitDiffRequired(Exception):
1305 pass
1304 pass
1306
1305
1307 def diffopts(ui, opts=None, untrusted=False):
1306 def diffopts(ui, opts=None, untrusted=False):
1308 def get(key, name=None, getter=ui.configbool):
1307 def get(key, name=None, getter=ui.configbool):
1309 return ((opts and opts.get(key)) or
1308 return ((opts and opts.get(key)) or
1310 getter('diff', name or key, None, untrusted=untrusted))
1309 getter('diff', name or key, None, untrusted=untrusted))
1311 return mdiff.diffopts(
1310 return mdiff.diffopts(
1312 text=opts and opts.get('text'),
1311 text=opts and opts.get('text'),
1313 git=get('git'),
1312 git=get('git'),
1314 nodates=get('nodates'),
1313 nodates=get('nodates'),
1315 showfunc=get('show_function', 'showfunc'),
1314 showfunc=get('show_function', 'showfunc'),
1316 ignorews=get('ignore_all_space', 'ignorews'),
1315 ignorews=get('ignore_all_space', 'ignorews'),
1317 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1316 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1318 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1317 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1319 context=get('unified', getter=ui.config))
1318 context=get('unified', getter=ui.config))
1320
1319
1321 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1320 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1322 losedatafn=None, prefix=''):
1321 losedatafn=None, prefix=''):
1323 '''yields diff of changes to files between two nodes, or node and
1322 '''yields diff of changes to files between two nodes, or node and
1324 working directory.
1323 working directory.
1325
1324
1326 if node1 is None, use first dirstate parent instead.
1325 if node1 is None, use first dirstate parent instead.
1327 if node2 is None, compare node1 with working directory.
1326 if node2 is None, compare node1 with working directory.
1328
1327
1329 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1328 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1330 every time some change cannot be represented with the current
1329 every time some change cannot be represented with the current
1331 patch format. Return False to upgrade to git patch format, True to
1330 patch format. Return False to upgrade to git patch format, True to
1332 accept the loss or raise an exception to abort the diff. It is
1331 accept the loss or raise an exception to abort the diff. It is
1333 called with the name of current file being diffed as 'fn'. If set
1332 called with the name of current file being diffed as 'fn'. If set
1334 to None, patches will always be upgraded to git format when
1333 to None, patches will always be upgraded to git format when
1335 necessary.
1334 necessary.
1336
1335
1337 prefix is a filename prefix that is prepended to all filenames on
1336 prefix is a filename prefix that is prepended to all filenames on
1338 display (used for subrepos).
1337 display (used for subrepos).
1339 '''
1338 '''
1340
1339
1341 if opts is None:
1340 if opts is None:
1342 opts = mdiff.defaultopts
1341 opts = mdiff.defaultopts
1343
1342
1344 if not node1 and not node2:
1343 if not node1 and not node2:
1345 node1 = repo.dirstate.parents()[0]
1344 node1 = repo.dirstate.parents()[0]
1346
1345
1347 def lrugetfilectx():
1346 def lrugetfilectx():
1348 cache = {}
1347 cache = {}
1349 order = []
1348 order = []
1350 def getfilectx(f, ctx):
1349 def getfilectx(f, ctx):
1351 fctx = ctx.filectx(f, filelog=cache.get(f))
1350 fctx = ctx.filectx(f, filelog=cache.get(f))
1352 if f not in cache:
1351 if f not in cache:
1353 if len(cache) > 20:
1352 if len(cache) > 20:
1354 del cache[order.pop(0)]
1353 del cache[order.pop(0)]
1355 cache[f] = fctx.filelog()
1354 cache[f] = fctx.filelog()
1356 else:
1355 else:
1357 order.remove(f)
1356 order.remove(f)
1358 order.append(f)
1357 order.append(f)
1359 return fctx
1358 return fctx
1360 return getfilectx
1359 return getfilectx
1361 getfilectx = lrugetfilectx()
1360 getfilectx = lrugetfilectx()
1362
1361
1363 ctx1 = repo[node1]
1362 ctx1 = repo[node1]
1364 ctx2 = repo[node2]
1363 ctx2 = repo[node2]
1365
1364
1366 if not changes:
1365 if not changes:
1367 changes = repo.status(ctx1, ctx2, match=match)
1366 changes = repo.status(ctx1, ctx2, match=match)
1368 modified, added, removed = changes[:3]
1367 modified, added, removed = changes[:3]
1369
1368
1370 if not modified and not added and not removed:
1369 if not modified and not added and not removed:
1371 return []
1370 return []
1372
1371
1373 revs = None
1372 revs = None
1374 if not repo.ui.quiet:
1373 if not repo.ui.quiet:
1375 hexfunc = repo.ui.debugflag and hex or short
1374 hexfunc = repo.ui.debugflag and hex or short
1376 revs = [hexfunc(node) for node in [node1, node2] if node]
1375 revs = [hexfunc(node) for node in [node1, node2] if node]
1377
1376
1378 copy = {}
1377 copy = {}
1379 if opts.git or opts.upgrade:
1378 if opts.git or opts.upgrade:
1380 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1379 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1381
1380
1382 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1381 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1383 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1382 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1384 if opts.upgrade and not opts.git:
1383 if opts.upgrade and not opts.git:
1385 try:
1384 try:
1386 def losedata(fn):
1385 def losedata(fn):
1387 if not losedatafn or not losedatafn(fn=fn):
1386 if not losedatafn or not losedatafn(fn=fn):
1388 raise GitDiffRequired()
1387 raise GitDiffRequired()
1389 # Buffer the whole output until we are sure it can be generated
1388 # Buffer the whole output until we are sure it can be generated
1390 return list(difffn(opts.copy(git=False), losedata))
1389 return list(difffn(opts.copy(git=False), losedata))
1391 except GitDiffRequired:
1390 except GitDiffRequired:
1392 return difffn(opts.copy(git=True), None)
1391 return difffn(opts.copy(git=True), None)
1393 else:
1392 else:
1394 return difffn(opts, None)
1393 return difffn(opts, None)
1395
1394
1396 def difflabel(func, *args, **kw):
1395 def difflabel(func, *args, **kw):
1397 '''yields 2-tuples of (output, label) based on the output of func()'''
1396 '''yields 2-tuples of (output, label) based on the output of func()'''
1398 prefixes = [('diff', 'diff.diffline'),
1397 prefixes = [('diff', 'diff.diffline'),
1399 ('copy', 'diff.extended'),
1398 ('copy', 'diff.extended'),
1400 ('rename', 'diff.extended'),
1399 ('rename', 'diff.extended'),
1401 ('old', 'diff.extended'),
1400 ('old', 'diff.extended'),
1402 ('new', 'diff.extended'),
1401 ('new', 'diff.extended'),
1403 ('deleted', 'diff.extended'),
1402 ('deleted', 'diff.extended'),
1404 ('---', 'diff.file_a'),
1403 ('---', 'diff.file_a'),
1405 ('+++', 'diff.file_b'),
1404 ('+++', 'diff.file_b'),
1406 ('@@', 'diff.hunk'),
1405 ('@@', 'diff.hunk'),
1407 ('-', 'diff.deleted'),
1406 ('-', 'diff.deleted'),
1408 ('+', 'diff.inserted')]
1407 ('+', 'diff.inserted')]
1409
1408
1410 for chunk in func(*args, **kw):
1409 for chunk in func(*args, **kw):
1411 lines = chunk.split('\n')
1410 lines = chunk.split('\n')
1412 for i, line in enumerate(lines):
1411 for i, line in enumerate(lines):
1413 if i != 0:
1412 if i != 0:
1414 yield ('\n', '')
1413 yield ('\n', '')
1415 stripline = line
1414 stripline = line
1416 if line and line[0] in '+-':
1415 if line and line[0] in '+-':
1417 # highlight trailing whitespace, but only in changed lines
1416 # highlight trailing whitespace, but only in changed lines
1418 stripline = line.rstrip()
1417 stripline = line.rstrip()
1419 for prefix, label in prefixes:
1418 for prefix, label in prefixes:
1420 if stripline.startswith(prefix):
1419 if stripline.startswith(prefix):
1421 yield (stripline, label)
1420 yield (stripline, label)
1422 break
1421 break
1423 else:
1422 else:
1424 yield (line, '')
1423 yield (line, '')
1425 if line != stripline:
1424 if line != stripline:
1426 yield (line[len(stripline):], 'diff.trailingwhitespace')
1425 yield (line[len(stripline):], 'diff.trailingwhitespace')
1427
1426
1428 def diffui(*args, **kw):
1427 def diffui(*args, **kw):
1429 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1428 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1430 return difflabel(diff, *args, **kw)
1429 return difflabel(diff, *args, **kw)
1431
1430
1432
1431
1433 def _addmodehdr(header, omode, nmode):
1432 def _addmodehdr(header, omode, nmode):
1434 if omode != nmode:
1433 if omode != nmode:
1435 header.append('old mode %s\n' % omode)
1434 header.append('old mode %s\n' % omode)
1436 header.append('new mode %s\n' % nmode)
1435 header.append('new mode %s\n' % nmode)
1437
1436
1438 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1437 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1439 copy, getfilectx, opts, losedatafn, prefix):
1438 copy, getfilectx, opts, losedatafn, prefix):
1440
1439
1441 def join(f):
1440 def join(f):
1442 return os.path.join(prefix, f)
1441 return os.path.join(prefix, f)
1443
1442
1444 date1 = util.datestr(ctx1.date())
1443 date1 = util.datestr(ctx1.date())
1445 man1 = ctx1.manifest()
1444 man1 = ctx1.manifest()
1446
1445
1447 gone = set()
1446 gone = set()
1448 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1447 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1449
1448
1450 copyto = dict([(v, k) for k, v in copy.items()])
1449 copyto = dict([(v, k) for k, v in copy.items()])
1451
1450
1452 if opts.git:
1451 if opts.git:
1453 revs = None
1452 revs = None
1454
1453
1455 for f in sorted(modified + added + removed):
1454 for f in sorted(modified + added + removed):
1456 to = None
1455 to = None
1457 tn = None
1456 tn = None
1458 dodiff = True
1457 dodiff = True
1459 header = []
1458 header = []
1460 if f in man1:
1459 if f in man1:
1461 to = getfilectx(f, ctx1).data()
1460 to = getfilectx(f, ctx1).data()
1462 if f not in removed:
1461 if f not in removed:
1463 tn = getfilectx(f, ctx2).data()
1462 tn = getfilectx(f, ctx2).data()
1464 a, b = f, f
1463 a, b = f, f
1465 if opts.git or losedatafn:
1464 if opts.git or losedatafn:
1466 if f in added:
1465 if f in added:
1467 mode = gitmode[ctx2.flags(f)]
1466 mode = gitmode[ctx2.flags(f)]
1468 if f in copy or f in copyto:
1467 if f in copy or f in copyto:
1469 if opts.git:
1468 if opts.git:
1470 if f in copy:
1469 if f in copy:
1471 a = copy[f]
1470 a = copy[f]
1472 else:
1471 else:
1473 a = copyto[f]
1472 a = copyto[f]
1474 omode = gitmode[man1.flags(a)]
1473 omode = gitmode[man1.flags(a)]
1475 _addmodehdr(header, omode, mode)
1474 _addmodehdr(header, omode, mode)
1476 if a in removed and a not in gone:
1475 if a in removed and a not in gone:
1477 op = 'rename'
1476 op = 'rename'
1478 gone.add(a)
1477 gone.add(a)
1479 else:
1478 else:
1480 op = 'copy'
1479 op = 'copy'
1481 header.append('%s from %s\n' % (op, join(a)))
1480 header.append('%s from %s\n' % (op, join(a)))
1482 header.append('%s to %s\n' % (op, join(f)))
1481 header.append('%s to %s\n' % (op, join(f)))
1483 to = getfilectx(a, ctx1).data()
1482 to = getfilectx(a, ctx1).data()
1484 else:
1483 else:
1485 losedatafn(f)
1484 losedatafn(f)
1486 else:
1485 else:
1487 if opts.git:
1486 if opts.git:
1488 header.append('new file mode %s\n' % mode)
1487 header.append('new file mode %s\n' % mode)
1489 elif ctx2.flags(f):
1488 elif ctx2.flags(f):
1490 losedatafn(f)
1489 losedatafn(f)
1491 # In theory, if tn was copied or renamed we should check
1490 # In theory, if tn was copied or renamed we should check
1492 # if the source is binary too but the copy record already
1491 # if the source is binary too but the copy record already
1493 # forces git mode.
1492 # forces git mode.
1494 if util.binary(tn):
1493 if util.binary(tn):
1495 if opts.git:
1494 if opts.git:
1496 dodiff = 'binary'
1495 dodiff = 'binary'
1497 else:
1496 else:
1498 losedatafn(f)
1497 losedatafn(f)
1499 if not opts.git and not tn:
1498 if not opts.git and not tn:
1500 # regular diffs cannot represent new empty file
1499 # regular diffs cannot represent new empty file
1501 losedatafn(f)
1500 losedatafn(f)
1502 elif f in removed:
1501 elif f in removed:
1503 if opts.git:
1502 if opts.git:
1504 # have we already reported a copy above?
1503 # have we already reported a copy above?
1505 if ((f in copy and copy[f] in added
1504 if ((f in copy and copy[f] in added
1506 and copyto[copy[f]] == f) or
1505 and copyto[copy[f]] == f) or
1507 (f in copyto and copyto[f] in added
1506 (f in copyto and copyto[f] in added
1508 and copy[copyto[f]] == f)):
1507 and copy[copyto[f]] == f)):
1509 dodiff = False
1508 dodiff = False
1510 else:
1509 else:
1511 header.append('deleted file mode %s\n' %
1510 header.append('deleted file mode %s\n' %
1512 gitmode[man1.flags(f)])
1511 gitmode[man1.flags(f)])
1513 elif not to or util.binary(to):
1512 elif not to or util.binary(to):
1514 # regular diffs cannot represent empty file deletion
1513 # regular diffs cannot represent empty file deletion
1515 losedatafn(f)
1514 losedatafn(f)
1516 else:
1515 else:
1517 oflag = man1.flags(f)
1516 oflag = man1.flags(f)
1518 nflag = ctx2.flags(f)
1517 nflag = ctx2.flags(f)
1519 binary = util.binary(to) or util.binary(tn)
1518 binary = util.binary(to) or util.binary(tn)
1520 if opts.git:
1519 if opts.git:
1521 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1520 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1522 if binary:
1521 if binary:
1523 dodiff = 'binary'
1522 dodiff = 'binary'
1524 elif binary or nflag != oflag:
1523 elif binary or nflag != oflag:
1525 losedatafn(f)
1524 losedatafn(f)
1526 if opts.git:
1525 if opts.git:
1527 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1526 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1528
1527
1529 if dodiff:
1528 if dodiff:
1530 if dodiff == 'binary':
1529 if dodiff == 'binary':
1531 text = b85diff(to, tn)
1530 text = b85diff(to, tn)
1532 else:
1531 else:
1533 text = mdiff.unidiff(to, date1,
1532 text = mdiff.unidiff(to, date1,
1534 # ctx2 date may be dynamic
1533 # ctx2 date may be dynamic
1535 tn, util.datestr(ctx2.date()),
1534 tn, util.datestr(ctx2.date()),
1536 join(a), join(b), revs, opts=opts)
1535 join(a), join(b), revs, opts=opts)
1537 if header and (text or len(header) > 1):
1536 if header and (text or len(header) > 1):
1538 yield ''.join(header)
1537 yield ''.join(header)
1539 if text:
1538 if text:
1540 yield text
1539 yield text
1541
1540
1542 def diffstatdata(lines):
1541 def diffstatdata(lines):
1543 filename, adds, removes = None, 0, 0
1542 filename, adds, removes = None, 0, 0
1544 for line in lines:
1543 for line in lines:
1545 if line.startswith('diff'):
1544 if line.startswith('diff'):
1546 if filename:
1545 if filename:
1547 isbinary = adds == 0 and removes == 0
1546 isbinary = adds == 0 and removes == 0
1548 yield (filename, adds, removes, isbinary)
1547 yield (filename, adds, removes, isbinary)
1549 # set numbers to 0 anyway when starting new file
1548 # set numbers to 0 anyway when starting new file
1550 adds, removes = 0, 0
1549 adds, removes = 0, 0
1551 if line.startswith('diff --git'):
1550 if line.startswith('diff --git'):
1552 filename = gitre.search(line).group(1)
1551 filename = gitre.search(line).group(1)
1553 else:
1552 else:
1554 # format: "diff -r ... -r ... filename"
1553 # format: "diff -r ... -r ... filename"
1555 filename = line.split(None, 5)[-1]
1554 filename = line.split(None, 5)[-1]
1556 elif line.startswith('+') and not line.startswith('+++'):
1555 elif line.startswith('+') and not line.startswith('+++'):
1557 adds += 1
1556 adds += 1
1558 elif line.startswith('-') and not line.startswith('---'):
1557 elif line.startswith('-') and not line.startswith('---'):
1559 removes += 1
1558 removes += 1
1560 if filename:
1559 if filename:
1561 isbinary = adds == 0 and removes == 0
1560 isbinary = adds == 0 and removes == 0
1562 yield (filename, adds, removes, isbinary)
1561 yield (filename, adds, removes, isbinary)
1563
1562
1564 def diffstat(lines, width=80, git=False):
1563 def diffstat(lines, width=80, git=False):
1565 output = []
1564 output = []
1566 stats = list(diffstatdata(lines))
1565 stats = list(diffstatdata(lines))
1567
1566
1568 maxtotal, maxname = 0, 0
1567 maxtotal, maxname = 0, 0
1569 totaladds, totalremoves = 0, 0
1568 totaladds, totalremoves = 0, 0
1570 hasbinary = False
1569 hasbinary = False
1571
1570
1572 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1571 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1573 for filename, adds, removes, isbinary in stats]
1572 for filename, adds, removes, isbinary in stats]
1574
1573
1575 for filename, adds, removes, isbinary, namewidth in sized:
1574 for filename, adds, removes, isbinary, namewidth in sized:
1576 totaladds += adds
1575 totaladds += adds
1577 totalremoves += removes
1576 totalremoves += removes
1578 maxname = max(maxname, namewidth)
1577 maxname = max(maxname, namewidth)
1579 maxtotal = max(maxtotal, adds + removes)
1578 maxtotal = max(maxtotal, adds + removes)
1580 if isbinary:
1579 if isbinary:
1581 hasbinary = True
1580 hasbinary = True
1582
1581
1583 countwidth = len(str(maxtotal))
1582 countwidth = len(str(maxtotal))
1584 if hasbinary and countwidth < 3:
1583 if hasbinary and countwidth < 3:
1585 countwidth = 3
1584 countwidth = 3
1586 graphwidth = width - countwidth - maxname - 6
1585 graphwidth = width - countwidth - maxname - 6
1587 if graphwidth < 10:
1586 if graphwidth < 10:
1588 graphwidth = 10
1587 graphwidth = 10
1589
1588
1590 def scale(i):
1589 def scale(i):
1591 if maxtotal <= graphwidth:
1590 if maxtotal <= graphwidth:
1592 return i
1591 return i
1593 # If diffstat runs out of room it doesn't print anything,
1592 # If diffstat runs out of room it doesn't print anything,
1594 # which isn't very useful, so always print at least one + or -
1593 # which isn't very useful, so always print at least one + or -
1595 # if there were at least some changes.
1594 # if there were at least some changes.
1596 return max(i * graphwidth // maxtotal, int(bool(i)))
1595 return max(i * graphwidth // maxtotal, int(bool(i)))
1597
1596
1598 for filename, adds, removes, isbinary, namewidth in sized:
1597 for filename, adds, removes, isbinary, namewidth in sized:
1599 if git and isbinary:
1598 if git and isbinary:
1600 count = 'Bin'
1599 count = 'Bin'
1601 else:
1600 else:
1602 count = adds + removes
1601 count = adds + removes
1603 pluses = '+' * scale(adds)
1602 pluses = '+' * scale(adds)
1604 minuses = '-' * scale(removes)
1603 minuses = '-' * scale(removes)
1605 output.append(' %s%s | %*s %s%s\n' %
1604 output.append(' %s%s | %*s %s%s\n' %
1606 (filename, ' ' * (maxname - namewidth),
1605 (filename, ' ' * (maxname - namewidth),
1607 countwidth, count,
1606 countwidth, count,
1608 pluses, minuses))
1607 pluses, minuses))
1609
1608
1610 if stats:
1609 if stats:
1611 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1610 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1612 % (len(stats), totaladds, totalremoves))
1611 % (len(stats), totaladds, totalremoves))
1613
1612
1614 return ''.join(output)
1613 return ''.join(output)
1615
1614
1616 def diffstatui(*args, **kw):
1615 def diffstatui(*args, **kw):
1617 '''like diffstat(), but yields 2-tuples of (output, label) for
1616 '''like diffstat(), but yields 2-tuples of (output, label) for
1618 ui.write()
1617 ui.write()
1619 '''
1618 '''
1620
1619
1621 for line in diffstat(*args, **kw).splitlines():
1620 for line in diffstat(*args, **kw).splitlines():
1622 if line and line[-1] in '+-':
1621 if line and line[-1] in '+-':
1623 name, graph = line.rsplit(' ', 1)
1622 name, graph = line.rsplit(' ', 1)
1624 yield (name + ' ', '')
1623 yield (name + ' ', '')
1625 m = re.search(r'\++', graph)
1624 m = re.search(r'\++', graph)
1626 if m:
1625 if m:
1627 yield (m.group(0), 'diffstat.inserted')
1626 yield (m.group(0), 'diffstat.inserted')
1628 m = re.search(r'-+', graph)
1627 m = re.search(r'-+', graph)
1629 if m:
1628 if m:
1630 yield (m.group(0), 'diffstat.deleted')
1629 yield (m.group(0), 'diffstat.deleted')
1631 else:
1630 else:
1632 yield (line, '')
1631 yield (line, '')
1633 yield ('\n', '')
1632 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now