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