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