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