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