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