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