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