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