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