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