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