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