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