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