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