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