##// END OF EJS Templates
patch: remove email import workaround for Python 2.4...
Gregory Szorc -
r25644:c99f9715 default
parent child Browse files
Show More
@@ -1,2553 +1,2549 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 collections
9 import collections
10 import cStringIO, email, os, errno, re, posixpath, copy
10 import cStringIO, email, os, errno, re, posixpath, copy
11 import tempfile, zlib, shutil
11 import tempfile, zlib, shutil
12 # On python2.4 you have to import these by name or they fail to
13 # load. This was not a problem on Python 2.7.
14 import email.Generator
15 import email.Parser
16
12
17 from i18n import _
13 from i18n import _
18 from node import hex, short
14 from node import hex, short
19 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
15 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
20 import pathutil
16 import pathutil
21
17
22 gitre = re.compile('diff --git a/(.*) b/(.*)')
18 gitre = re.compile('diff --git a/(.*) b/(.*)')
23 tabsplitter = re.compile(r'(\t+|[^\t]+)')
19 tabsplitter = re.compile(r'(\t+|[^\t]+)')
24
20
25 class PatchError(Exception):
21 class PatchError(Exception):
26 pass
22 pass
27
23
28
24
29 # public functions
25 # public functions
30
26
31 def split(stream):
27 def split(stream):
32 '''return an iterator of individual patches from a stream'''
28 '''return an iterator of individual patches from a stream'''
33 def isheader(line, inheader):
29 def isheader(line, inheader):
34 if inheader and line[0] in (' ', '\t'):
30 if inheader and line[0] in (' ', '\t'):
35 # continuation
31 # continuation
36 return True
32 return True
37 if line[0] in (' ', '-', '+'):
33 if line[0] in (' ', '-', '+'):
38 # diff line - don't check for header pattern in there
34 # diff line - don't check for header pattern in there
39 return False
35 return False
40 l = line.split(': ', 1)
36 l = line.split(': ', 1)
41 return len(l) == 2 and ' ' not in l[0]
37 return len(l) == 2 and ' ' not in l[0]
42
38
43 def chunk(lines):
39 def chunk(lines):
44 return cStringIO.StringIO(''.join(lines))
40 return cStringIO.StringIO(''.join(lines))
45
41
46 def hgsplit(stream, cur):
42 def hgsplit(stream, cur):
47 inheader = True
43 inheader = True
48
44
49 for line in stream:
45 for line in stream:
50 if not line.strip():
46 if not line.strip():
51 inheader = False
47 inheader = False
52 if not inheader and line.startswith('# HG changeset patch'):
48 if not inheader and line.startswith('# HG changeset patch'):
53 yield chunk(cur)
49 yield chunk(cur)
54 cur = []
50 cur = []
55 inheader = True
51 inheader = True
56
52
57 cur.append(line)
53 cur.append(line)
58
54
59 if cur:
55 if cur:
60 yield chunk(cur)
56 yield chunk(cur)
61
57
62 def mboxsplit(stream, cur):
58 def mboxsplit(stream, cur):
63 for line in stream:
59 for line in stream:
64 if line.startswith('From '):
60 if line.startswith('From '):
65 for c in split(chunk(cur[1:])):
61 for c in split(chunk(cur[1:])):
66 yield c
62 yield c
67 cur = []
63 cur = []
68
64
69 cur.append(line)
65 cur.append(line)
70
66
71 if cur:
67 if cur:
72 for c in split(chunk(cur[1:])):
68 for c in split(chunk(cur[1:])):
73 yield c
69 yield c
74
70
75 def mimesplit(stream, cur):
71 def mimesplit(stream, cur):
76 def msgfp(m):
72 def msgfp(m):
77 fp = cStringIO.StringIO()
73 fp = cStringIO.StringIO()
78 g = email.Generator.Generator(fp, mangle_from_=False)
74 g = email.Generator.Generator(fp, mangle_from_=False)
79 g.flatten(m)
75 g.flatten(m)
80 fp.seek(0)
76 fp.seek(0)
81 return fp
77 return fp
82
78
83 for line in stream:
79 for line in stream:
84 cur.append(line)
80 cur.append(line)
85 c = chunk(cur)
81 c = chunk(cur)
86
82
87 m = email.Parser.Parser().parse(c)
83 m = email.Parser.Parser().parse(c)
88 if not m.is_multipart():
84 if not m.is_multipart():
89 yield msgfp(m)
85 yield msgfp(m)
90 else:
86 else:
91 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
87 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
92 for part in m.walk():
88 for part in m.walk():
93 ct = part.get_content_type()
89 ct = part.get_content_type()
94 if ct not in ok_types:
90 if ct not in ok_types:
95 continue
91 continue
96 yield msgfp(part)
92 yield msgfp(part)
97
93
98 def headersplit(stream, cur):
94 def headersplit(stream, cur):
99 inheader = False
95 inheader = False
100
96
101 for line in stream:
97 for line in stream:
102 if not inheader and isheader(line, inheader):
98 if not inheader and isheader(line, inheader):
103 yield chunk(cur)
99 yield chunk(cur)
104 cur = []
100 cur = []
105 inheader = True
101 inheader = True
106 if inheader and not isheader(line, inheader):
102 if inheader and not isheader(line, inheader):
107 inheader = False
103 inheader = False
108
104
109 cur.append(line)
105 cur.append(line)
110
106
111 if cur:
107 if cur:
112 yield chunk(cur)
108 yield chunk(cur)
113
109
114 def remainder(cur):
110 def remainder(cur):
115 yield chunk(cur)
111 yield chunk(cur)
116
112
117 class fiter(object):
113 class fiter(object):
118 def __init__(self, fp):
114 def __init__(self, fp):
119 self.fp = fp
115 self.fp = fp
120
116
121 def __iter__(self):
117 def __iter__(self):
122 return self
118 return self
123
119
124 def next(self):
120 def next(self):
125 l = self.fp.readline()
121 l = self.fp.readline()
126 if not l:
122 if not l:
127 raise StopIteration
123 raise StopIteration
128 return l
124 return l
129
125
130 inheader = False
126 inheader = False
131 cur = []
127 cur = []
132
128
133 mimeheaders = ['content-type']
129 mimeheaders = ['content-type']
134
130
135 if not util.safehasattr(stream, 'next'):
131 if not util.safehasattr(stream, 'next'):
136 # http responses, for example, have readline but not next
132 # http responses, for example, have readline but not next
137 stream = fiter(stream)
133 stream = fiter(stream)
138
134
139 for line in stream:
135 for line in stream:
140 cur.append(line)
136 cur.append(line)
141 if line.startswith('# HG changeset patch'):
137 if line.startswith('# HG changeset patch'):
142 return hgsplit(stream, cur)
138 return hgsplit(stream, cur)
143 elif line.startswith('From '):
139 elif line.startswith('From '):
144 return mboxsplit(stream, cur)
140 return mboxsplit(stream, cur)
145 elif isheader(line, inheader):
141 elif isheader(line, inheader):
146 inheader = True
142 inheader = True
147 if line.split(':', 1)[0].lower() in mimeheaders:
143 if line.split(':', 1)[0].lower() in mimeheaders:
148 # let email parser handle this
144 # let email parser handle this
149 return mimesplit(stream, cur)
145 return mimesplit(stream, cur)
150 elif line.startswith('--- ') and inheader:
146 elif line.startswith('--- ') and inheader:
151 # No evil headers seen by diff start, split by hand
147 # No evil headers seen by diff start, split by hand
152 return headersplit(stream, cur)
148 return headersplit(stream, cur)
153 # Not enough info, keep reading
149 # Not enough info, keep reading
154
150
155 # if we are here, we have a very plain patch
151 # if we are here, we have a very plain patch
156 return remainder(cur)
152 return remainder(cur)
157
153
158 def extract(ui, fileobj):
154 def extract(ui, fileobj):
159 '''extract patch from data read from fileobj.
155 '''extract patch from data read from fileobj.
160
156
161 patch can be a normal patch or contained in an email message.
157 patch can be a normal patch or contained in an email message.
162
158
163 return tuple (filename, message, user, date, branch, node, p1, p2).
159 return tuple (filename, message, user, date, branch, node, p1, p2).
164 Any item in the returned tuple can be None. If filename is None,
160 Any item in the returned tuple can be None. If filename is None,
165 fileobj did not contain a patch. Caller must unlink filename when done.'''
161 fileobj did not contain a patch. Caller must unlink filename when done.'''
166
162
167 # attempt to detect the start of a patch
163 # attempt to detect the start of a patch
168 # (this heuristic is borrowed from quilt)
164 # (this heuristic is borrowed from quilt)
169 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
165 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
170 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
166 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
171 r'---[ \t].*?^\+\+\+[ \t]|'
167 r'---[ \t].*?^\+\+\+[ \t]|'
172 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
168 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
173
169
174 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
170 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
175 tmpfp = os.fdopen(fd, 'w')
171 tmpfp = os.fdopen(fd, 'w')
176 try:
172 try:
177 msg = email.Parser.Parser().parse(fileobj)
173 msg = email.Parser.Parser().parse(fileobj)
178
174
179 subject = msg['Subject']
175 subject = msg['Subject']
180 user = msg['From']
176 user = msg['From']
181 if not subject and not user:
177 if not subject and not user:
182 # Not an email, restore parsed headers if any
178 # Not an email, restore parsed headers if any
183 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
179 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
184
180
185 # should try to parse msg['Date']
181 # should try to parse msg['Date']
186 date = None
182 date = None
187 nodeid = None
183 nodeid = None
188 branch = None
184 branch = None
189 parents = []
185 parents = []
190
186
191 if subject:
187 if subject:
192 if subject.startswith('[PATCH'):
188 if subject.startswith('[PATCH'):
193 pend = subject.find(']')
189 pend = subject.find(']')
194 if pend >= 0:
190 if pend >= 0:
195 subject = subject[pend + 1:].lstrip()
191 subject = subject[pend + 1:].lstrip()
196 subject = re.sub(r'\n[ \t]+', ' ', subject)
192 subject = re.sub(r'\n[ \t]+', ' ', subject)
197 ui.debug('Subject: %s\n' % subject)
193 ui.debug('Subject: %s\n' % subject)
198 if user:
194 if user:
199 ui.debug('From: %s\n' % user)
195 ui.debug('From: %s\n' % user)
200 diffs_seen = 0
196 diffs_seen = 0
201 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
197 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
202 message = ''
198 message = ''
203 for part in msg.walk():
199 for part in msg.walk():
204 content_type = part.get_content_type()
200 content_type = part.get_content_type()
205 ui.debug('Content-Type: %s\n' % content_type)
201 ui.debug('Content-Type: %s\n' % content_type)
206 if content_type not in ok_types:
202 if content_type not in ok_types:
207 continue
203 continue
208 payload = part.get_payload(decode=True)
204 payload = part.get_payload(decode=True)
209 m = diffre.search(payload)
205 m = diffre.search(payload)
210 if m:
206 if m:
211 hgpatch = False
207 hgpatch = False
212 hgpatchheader = False
208 hgpatchheader = False
213 ignoretext = False
209 ignoretext = False
214
210
215 ui.debug('found patch at byte %d\n' % m.start(0))
211 ui.debug('found patch at byte %d\n' % m.start(0))
216 diffs_seen += 1
212 diffs_seen += 1
217 cfp = cStringIO.StringIO()
213 cfp = cStringIO.StringIO()
218 for line in payload[:m.start(0)].splitlines():
214 for line in payload[:m.start(0)].splitlines():
219 if line.startswith('# HG changeset patch') and not hgpatch:
215 if line.startswith('# HG changeset patch') and not hgpatch:
220 ui.debug('patch generated by hg export\n')
216 ui.debug('patch generated by hg export\n')
221 hgpatch = True
217 hgpatch = True
222 hgpatchheader = True
218 hgpatchheader = True
223 # drop earlier commit message content
219 # drop earlier commit message content
224 cfp.seek(0)
220 cfp.seek(0)
225 cfp.truncate()
221 cfp.truncate()
226 subject = None
222 subject = None
227 elif hgpatchheader:
223 elif hgpatchheader:
228 if line.startswith('# User '):
224 if line.startswith('# User '):
229 user = line[7:]
225 user = line[7:]
230 ui.debug('From: %s\n' % user)
226 ui.debug('From: %s\n' % user)
231 elif line.startswith("# Date "):
227 elif line.startswith("# Date "):
232 date = line[7:]
228 date = line[7:]
233 elif line.startswith("# Branch "):
229 elif line.startswith("# Branch "):
234 branch = line[9:]
230 branch = line[9:]
235 elif line.startswith("# Node ID "):
231 elif line.startswith("# Node ID "):
236 nodeid = line[10:]
232 nodeid = line[10:]
237 elif line.startswith("# Parent "):
233 elif line.startswith("# Parent "):
238 parents.append(line[9:].lstrip())
234 parents.append(line[9:].lstrip())
239 elif not line.startswith("# "):
235 elif not line.startswith("# "):
240 hgpatchheader = False
236 hgpatchheader = False
241 elif line == '---':
237 elif line == '---':
242 ignoretext = True
238 ignoretext = True
243 if not hgpatchheader and not ignoretext:
239 if not hgpatchheader and not ignoretext:
244 cfp.write(line)
240 cfp.write(line)
245 cfp.write('\n')
241 cfp.write('\n')
246 message = cfp.getvalue()
242 message = cfp.getvalue()
247 if tmpfp:
243 if tmpfp:
248 tmpfp.write(payload)
244 tmpfp.write(payload)
249 if not payload.endswith('\n'):
245 if not payload.endswith('\n'):
250 tmpfp.write('\n')
246 tmpfp.write('\n')
251 elif not diffs_seen and message and content_type == 'text/plain':
247 elif not diffs_seen and message and content_type == 'text/plain':
252 message += '\n' + payload
248 message += '\n' + payload
253 except: # re-raises
249 except: # re-raises
254 tmpfp.close()
250 tmpfp.close()
255 os.unlink(tmpname)
251 os.unlink(tmpname)
256 raise
252 raise
257
253
258 if subject and not message.startswith(subject):
254 if subject and not message.startswith(subject):
259 message = '%s\n%s' % (subject, message)
255 message = '%s\n%s' % (subject, message)
260 tmpfp.close()
256 tmpfp.close()
261 if not diffs_seen:
257 if not diffs_seen:
262 os.unlink(tmpname)
258 os.unlink(tmpname)
263 return None, message, user, date, branch, None, None, None
259 return None, message, user, date, branch, None, None, None
264
260
265 if parents:
261 if parents:
266 p1 = parents.pop(0)
262 p1 = parents.pop(0)
267 else:
263 else:
268 p1 = None
264 p1 = None
269
265
270 if parents:
266 if parents:
271 p2 = parents.pop(0)
267 p2 = parents.pop(0)
272 else:
268 else:
273 p2 = None
269 p2 = None
274
270
275 return tmpname, message, user, date, branch, nodeid, p1, p2
271 return tmpname, message, user, date, branch, nodeid, p1, p2
276
272
277 class patchmeta(object):
273 class patchmeta(object):
278 """Patched file metadata
274 """Patched file metadata
279
275
280 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
276 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 or COPY. 'path' is patched file path. 'oldpath' is set to the
277 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 origin file when 'op' is either COPY or RENAME, None otherwise. If
278 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 file mode is changed, 'mode' is a tuple (islink, isexec) where
279 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 'islink' is True if the file is a symlink and 'isexec' is True if
280 'islink' is True if the file is a symlink and 'isexec' is True if
285 the file is executable. Otherwise, 'mode' is None.
281 the file is executable. Otherwise, 'mode' is None.
286 """
282 """
287 def __init__(self, path):
283 def __init__(self, path):
288 self.path = path
284 self.path = path
289 self.oldpath = None
285 self.oldpath = None
290 self.mode = None
286 self.mode = None
291 self.op = 'MODIFY'
287 self.op = 'MODIFY'
292 self.binary = False
288 self.binary = False
293
289
294 def setmode(self, mode):
290 def setmode(self, mode):
295 islink = mode & 020000
291 islink = mode & 020000
296 isexec = mode & 0100
292 isexec = mode & 0100
297 self.mode = (islink, isexec)
293 self.mode = (islink, isexec)
298
294
299 def copy(self):
295 def copy(self):
300 other = patchmeta(self.path)
296 other = patchmeta(self.path)
301 other.oldpath = self.oldpath
297 other.oldpath = self.oldpath
302 other.mode = self.mode
298 other.mode = self.mode
303 other.op = self.op
299 other.op = self.op
304 other.binary = self.binary
300 other.binary = self.binary
305 return other
301 return other
306
302
307 def _ispatchinga(self, afile):
303 def _ispatchinga(self, afile):
308 if afile == '/dev/null':
304 if afile == '/dev/null':
309 return self.op == 'ADD'
305 return self.op == 'ADD'
310 return afile == 'a/' + (self.oldpath or self.path)
306 return afile == 'a/' + (self.oldpath or self.path)
311
307
312 def _ispatchingb(self, bfile):
308 def _ispatchingb(self, bfile):
313 if bfile == '/dev/null':
309 if bfile == '/dev/null':
314 return self.op == 'DELETE'
310 return self.op == 'DELETE'
315 return bfile == 'b/' + self.path
311 return bfile == 'b/' + self.path
316
312
317 def ispatching(self, afile, bfile):
313 def ispatching(self, afile, bfile):
318 return self._ispatchinga(afile) and self._ispatchingb(bfile)
314 return self._ispatchinga(afile) and self._ispatchingb(bfile)
319
315
320 def __repr__(self):
316 def __repr__(self):
321 return "<patchmeta %s %r>" % (self.op, self.path)
317 return "<patchmeta %s %r>" % (self.op, self.path)
322
318
323 def readgitpatch(lr):
319 def readgitpatch(lr):
324 """extract git-style metadata about patches from <patchname>"""
320 """extract git-style metadata about patches from <patchname>"""
325
321
326 # Filter patch for git information
322 # Filter patch for git information
327 gp = None
323 gp = None
328 gitpatches = []
324 gitpatches = []
329 for line in lr:
325 for line in lr:
330 line = line.rstrip(' \r\n')
326 line = line.rstrip(' \r\n')
331 if line.startswith('diff --git a/'):
327 if line.startswith('diff --git a/'):
332 m = gitre.match(line)
328 m = gitre.match(line)
333 if m:
329 if m:
334 if gp:
330 if gp:
335 gitpatches.append(gp)
331 gitpatches.append(gp)
336 dst = m.group(2)
332 dst = m.group(2)
337 gp = patchmeta(dst)
333 gp = patchmeta(dst)
338 elif gp:
334 elif gp:
339 if line.startswith('--- '):
335 if line.startswith('--- '):
340 gitpatches.append(gp)
336 gitpatches.append(gp)
341 gp = None
337 gp = None
342 continue
338 continue
343 if line.startswith('rename from '):
339 if line.startswith('rename from '):
344 gp.op = 'RENAME'
340 gp.op = 'RENAME'
345 gp.oldpath = line[12:]
341 gp.oldpath = line[12:]
346 elif line.startswith('rename to '):
342 elif line.startswith('rename to '):
347 gp.path = line[10:]
343 gp.path = line[10:]
348 elif line.startswith('copy from '):
344 elif line.startswith('copy from '):
349 gp.op = 'COPY'
345 gp.op = 'COPY'
350 gp.oldpath = line[10:]
346 gp.oldpath = line[10:]
351 elif line.startswith('copy to '):
347 elif line.startswith('copy to '):
352 gp.path = line[8:]
348 gp.path = line[8:]
353 elif line.startswith('deleted file'):
349 elif line.startswith('deleted file'):
354 gp.op = 'DELETE'
350 gp.op = 'DELETE'
355 elif line.startswith('new file mode '):
351 elif line.startswith('new file mode '):
356 gp.op = 'ADD'
352 gp.op = 'ADD'
357 gp.setmode(int(line[-6:], 8))
353 gp.setmode(int(line[-6:], 8))
358 elif line.startswith('new mode '):
354 elif line.startswith('new mode '):
359 gp.setmode(int(line[-6:], 8))
355 gp.setmode(int(line[-6:], 8))
360 elif line.startswith('GIT binary patch'):
356 elif line.startswith('GIT binary patch'):
361 gp.binary = True
357 gp.binary = True
362 if gp:
358 if gp:
363 gitpatches.append(gp)
359 gitpatches.append(gp)
364
360
365 return gitpatches
361 return gitpatches
366
362
367 class linereader(object):
363 class linereader(object):
368 # simple class to allow pushing lines back into the input stream
364 # simple class to allow pushing lines back into the input stream
369 def __init__(self, fp):
365 def __init__(self, fp):
370 self.fp = fp
366 self.fp = fp
371 self.buf = []
367 self.buf = []
372
368
373 def push(self, line):
369 def push(self, line):
374 if line is not None:
370 if line is not None:
375 self.buf.append(line)
371 self.buf.append(line)
376
372
377 def readline(self):
373 def readline(self):
378 if self.buf:
374 if self.buf:
379 l = self.buf[0]
375 l = self.buf[0]
380 del self.buf[0]
376 del self.buf[0]
381 return l
377 return l
382 return self.fp.readline()
378 return self.fp.readline()
383
379
384 def __iter__(self):
380 def __iter__(self):
385 while True:
381 while True:
386 l = self.readline()
382 l = self.readline()
387 if not l:
383 if not l:
388 break
384 break
389 yield l
385 yield l
390
386
391 class abstractbackend(object):
387 class abstractbackend(object):
392 def __init__(self, ui):
388 def __init__(self, ui):
393 self.ui = ui
389 self.ui = ui
394
390
395 def getfile(self, fname):
391 def getfile(self, fname):
396 """Return target file data and flags as a (data, (islink,
392 """Return target file data and flags as a (data, (islink,
397 isexec)) tuple. Data is None if file is missing/deleted.
393 isexec)) tuple. Data is None if file is missing/deleted.
398 """
394 """
399 raise NotImplementedError
395 raise NotImplementedError
400
396
401 def setfile(self, fname, data, mode, copysource):
397 def setfile(self, fname, data, mode, copysource):
402 """Write data to target file fname and set its mode. mode is a
398 """Write data to target file fname and set its mode. mode is a
403 (islink, isexec) tuple. If data is None, the file content should
399 (islink, isexec) tuple. If data is None, the file content should
404 be left unchanged. If the file is modified after being copied,
400 be left unchanged. If the file is modified after being copied,
405 copysource is set to the original file name.
401 copysource is set to the original file name.
406 """
402 """
407 raise NotImplementedError
403 raise NotImplementedError
408
404
409 def unlink(self, fname):
405 def unlink(self, fname):
410 """Unlink target file."""
406 """Unlink target file."""
411 raise NotImplementedError
407 raise NotImplementedError
412
408
413 def writerej(self, fname, failed, total, lines):
409 def writerej(self, fname, failed, total, lines):
414 """Write rejected lines for fname. total is the number of hunks
410 """Write rejected lines for fname. total is the number of hunks
415 which failed to apply and total the total number of hunks for this
411 which failed to apply and total the total number of hunks for this
416 files.
412 files.
417 """
413 """
418 pass
414 pass
419
415
420 def exists(self, fname):
416 def exists(self, fname):
421 raise NotImplementedError
417 raise NotImplementedError
422
418
423 class fsbackend(abstractbackend):
419 class fsbackend(abstractbackend):
424 def __init__(self, ui, basedir):
420 def __init__(self, ui, basedir):
425 super(fsbackend, self).__init__(ui)
421 super(fsbackend, self).__init__(ui)
426 self.opener = scmutil.opener(basedir)
422 self.opener = scmutil.opener(basedir)
427
423
428 def _join(self, f):
424 def _join(self, f):
429 return os.path.join(self.opener.base, f)
425 return os.path.join(self.opener.base, f)
430
426
431 def getfile(self, fname):
427 def getfile(self, fname):
432 if self.opener.islink(fname):
428 if self.opener.islink(fname):
433 return (self.opener.readlink(fname), (True, False))
429 return (self.opener.readlink(fname), (True, False))
434
430
435 isexec = False
431 isexec = False
436 try:
432 try:
437 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
433 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
438 except OSError, e:
434 except OSError, e:
439 if e.errno != errno.ENOENT:
435 if e.errno != errno.ENOENT:
440 raise
436 raise
441 try:
437 try:
442 return (self.opener.read(fname), (False, isexec))
438 return (self.opener.read(fname), (False, isexec))
443 except IOError, e:
439 except IOError, e:
444 if e.errno != errno.ENOENT:
440 if e.errno != errno.ENOENT:
445 raise
441 raise
446 return None, None
442 return None, None
447
443
448 def setfile(self, fname, data, mode, copysource):
444 def setfile(self, fname, data, mode, copysource):
449 islink, isexec = mode
445 islink, isexec = mode
450 if data is None:
446 if data is None:
451 self.opener.setflags(fname, islink, isexec)
447 self.opener.setflags(fname, islink, isexec)
452 return
448 return
453 if islink:
449 if islink:
454 self.opener.symlink(data, fname)
450 self.opener.symlink(data, fname)
455 else:
451 else:
456 self.opener.write(fname, data)
452 self.opener.write(fname, data)
457 if isexec:
453 if isexec:
458 self.opener.setflags(fname, False, True)
454 self.opener.setflags(fname, False, True)
459
455
460 def unlink(self, fname):
456 def unlink(self, fname):
461 self.opener.unlinkpath(fname, ignoremissing=True)
457 self.opener.unlinkpath(fname, ignoremissing=True)
462
458
463 def writerej(self, fname, failed, total, lines):
459 def writerej(self, fname, failed, total, lines):
464 fname = fname + ".rej"
460 fname = fname + ".rej"
465 self.ui.warn(
461 self.ui.warn(
466 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
462 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
467 (failed, total, fname))
463 (failed, total, fname))
468 fp = self.opener(fname, 'w')
464 fp = self.opener(fname, 'w')
469 fp.writelines(lines)
465 fp.writelines(lines)
470 fp.close()
466 fp.close()
471
467
472 def exists(self, fname):
468 def exists(self, fname):
473 return self.opener.lexists(fname)
469 return self.opener.lexists(fname)
474
470
475 class workingbackend(fsbackend):
471 class workingbackend(fsbackend):
476 def __init__(self, ui, repo, similarity):
472 def __init__(self, ui, repo, similarity):
477 super(workingbackend, self).__init__(ui, repo.root)
473 super(workingbackend, self).__init__(ui, repo.root)
478 self.repo = repo
474 self.repo = repo
479 self.similarity = similarity
475 self.similarity = similarity
480 self.removed = set()
476 self.removed = set()
481 self.changed = set()
477 self.changed = set()
482 self.copied = []
478 self.copied = []
483
479
484 def _checkknown(self, fname):
480 def _checkknown(self, fname):
485 if self.repo.dirstate[fname] == '?' and self.exists(fname):
481 if self.repo.dirstate[fname] == '?' and self.exists(fname):
486 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
482 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
487
483
488 def setfile(self, fname, data, mode, copysource):
484 def setfile(self, fname, data, mode, copysource):
489 self._checkknown(fname)
485 self._checkknown(fname)
490 super(workingbackend, self).setfile(fname, data, mode, copysource)
486 super(workingbackend, self).setfile(fname, data, mode, copysource)
491 if copysource is not None:
487 if copysource is not None:
492 self.copied.append((copysource, fname))
488 self.copied.append((copysource, fname))
493 self.changed.add(fname)
489 self.changed.add(fname)
494
490
495 def unlink(self, fname):
491 def unlink(self, fname):
496 self._checkknown(fname)
492 self._checkknown(fname)
497 super(workingbackend, self).unlink(fname)
493 super(workingbackend, self).unlink(fname)
498 self.removed.add(fname)
494 self.removed.add(fname)
499 self.changed.add(fname)
495 self.changed.add(fname)
500
496
501 def close(self):
497 def close(self):
502 wctx = self.repo[None]
498 wctx = self.repo[None]
503 changed = set(self.changed)
499 changed = set(self.changed)
504 for src, dst in self.copied:
500 for src, dst in self.copied:
505 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
501 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
506 if self.removed:
502 if self.removed:
507 wctx.forget(sorted(self.removed))
503 wctx.forget(sorted(self.removed))
508 for f in self.removed:
504 for f in self.removed:
509 if f not in self.repo.dirstate:
505 if f not in self.repo.dirstate:
510 # File was deleted and no longer belongs to the
506 # File was deleted and no longer belongs to the
511 # dirstate, it was probably marked added then
507 # dirstate, it was probably marked added then
512 # deleted, and should not be considered by
508 # deleted, and should not be considered by
513 # marktouched().
509 # marktouched().
514 changed.discard(f)
510 changed.discard(f)
515 if changed:
511 if changed:
516 scmutil.marktouched(self.repo, changed, self.similarity)
512 scmutil.marktouched(self.repo, changed, self.similarity)
517 return sorted(self.changed)
513 return sorted(self.changed)
518
514
519 class filestore(object):
515 class filestore(object):
520 def __init__(self, maxsize=None):
516 def __init__(self, maxsize=None):
521 self.opener = None
517 self.opener = None
522 self.files = {}
518 self.files = {}
523 self.created = 0
519 self.created = 0
524 self.maxsize = maxsize
520 self.maxsize = maxsize
525 if self.maxsize is None:
521 if self.maxsize is None:
526 self.maxsize = 4*(2**20)
522 self.maxsize = 4*(2**20)
527 self.size = 0
523 self.size = 0
528 self.data = {}
524 self.data = {}
529
525
530 def setfile(self, fname, data, mode, copied=None):
526 def setfile(self, fname, data, mode, copied=None):
531 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
527 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
532 self.data[fname] = (data, mode, copied)
528 self.data[fname] = (data, mode, copied)
533 self.size += len(data)
529 self.size += len(data)
534 else:
530 else:
535 if self.opener is None:
531 if self.opener is None:
536 root = tempfile.mkdtemp(prefix='hg-patch-')
532 root = tempfile.mkdtemp(prefix='hg-patch-')
537 self.opener = scmutil.opener(root)
533 self.opener = scmutil.opener(root)
538 # Avoid filename issues with these simple names
534 # Avoid filename issues with these simple names
539 fn = str(self.created)
535 fn = str(self.created)
540 self.opener.write(fn, data)
536 self.opener.write(fn, data)
541 self.created += 1
537 self.created += 1
542 self.files[fname] = (fn, mode, copied)
538 self.files[fname] = (fn, mode, copied)
543
539
544 def getfile(self, fname):
540 def getfile(self, fname):
545 if fname in self.data:
541 if fname in self.data:
546 return self.data[fname]
542 return self.data[fname]
547 if not self.opener or fname not in self.files:
543 if not self.opener or fname not in self.files:
548 return None, None, None
544 return None, None, None
549 fn, mode, copied = self.files[fname]
545 fn, mode, copied = self.files[fname]
550 return self.opener.read(fn), mode, copied
546 return self.opener.read(fn), mode, copied
551
547
552 def close(self):
548 def close(self):
553 if self.opener:
549 if self.opener:
554 shutil.rmtree(self.opener.base)
550 shutil.rmtree(self.opener.base)
555
551
556 class repobackend(abstractbackend):
552 class repobackend(abstractbackend):
557 def __init__(self, ui, repo, ctx, store):
553 def __init__(self, ui, repo, ctx, store):
558 super(repobackend, self).__init__(ui)
554 super(repobackend, self).__init__(ui)
559 self.repo = repo
555 self.repo = repo
560 self.ctx = ctx
556 self.ctx = ctx
561 self.store = store
557 self.store = store
562 self.changed = set()
558 self.changed = set()
563 self.removed = set()
559 self.removed = set()
564 self.copied = {}
560 self.copied = {}
565
561
566 def _checkknown(self, fname):
562 def _checkknown(self, fname):
567 if fname not in self.ctx:
563 if fname not in self.ctx:
568 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
564 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
569
565
570 def getfile(self, fname):
566 def getfile(self, fname):
571 try:
567 try:
572 fctx = self.ctx[fname]
568 fctx = self.ctx[fname]
573 except error.LookupError:
569 except error.LookupError:
574 return None, None
570 return None, None
575 flags = fctx.flags()
571 flags = fctx.flags()
576 return fctx.data(), ('l' in flags, 'x' in flags)
572 return fctx.data(), ('l' in flags, 'x' in flags)
577
573
578 def setfile(self, fname, data, mode, copysource):
574 def setfile(self, fname, data, mode, copysource):
579 if copysource:
575 if copysource:
580 self._checkknown(copysource)
576 self._checkknown(copysource)
581 if data is None:
577 if data is None:
582 data = self.ctx[fname].data()
578 data = self.ctx[fname].data()
583 self.store.setfile(fname, data, mode, copysource)
579 self.store.setfile(fname, data, mode, copysource)
584 self.changed.add(fname)
580 self.changed.add(fname)
585 if copysource:
581 if copysource:
586 self.copied[fname] = copysource
582 self.copied[fname] = copysource
587
583
588 def unlink(self, fname):
584 def unlink(self, fname):
589 self._checkknown(fname)
585 self._checkknown(fname)
590 self.removed.add(fname)
586 self.removed.add(fname)
591
587
592 def exists(self, fname):
588 def exists(self, fname):
593 return fname in self.ctx
589 return fname in self.ctx
594
590
595 def close(self):
591 def close(self):
596 return self.changed | self.removed
592 return self.changed | self.removed
597
593
598 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
594 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
599 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
595 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
600 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
596 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
601 eolmodes = ['strict', 'crlf', 'lf', 'auto']
597 eolmodes = ['strict', 'crlf', 'lf', 'auto']
602
598
603 class patchfile(object):
599 class patchfile(object):
604 def __init__(self, ui, gp, backend, store, eolmode='strict'):
600 def __init__(self, ui, gp, backend, store, eolmode='strict'):
605 self.fname = gp.path
601 self.fname = gp.path
606 self.eolmode = eolmode
602 self.eolmode = eolmode
607 self.eol = None
603 self.eol = None
608 self.backend = backend
604 self.backend = backend
609 self.ui = ui
605 self.ui = ui
610 self.lines = []
606 self.lines = []
611 self.exists = False
607 self.exists = False
612 self.missing = True
608 self.missing = True
613 self.mode = gp.mode
609 self.mode = gp.mode
614 self.copysource = gp.oldpath
610 self.copysource = gp.oldpath
615 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
611 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
616 self.remove = gp.op == 'DELETE'
612 self.remove = gp.op == 'DELETE'
617 if self.copysource is None:
613 if self.copysource is None:
618 data, mode = backend.getfile(self.fname)
614 data, mode = backend.getfile(self.fname)
619 else:
615 else:
620 data, mode = store.getfile(self.copysource)[:2]
616 data, mode = store.getfile(self.copysource)[:2]
621 if data is not None:
617 if data is not None:
622 self.exists = self.copysource is None or backend.exists(self.fname)
618 self.exists = self.copysource is None or backend.exists(self.fname)
623 self.missing = False
619 self.missing = False
624 if data:
620 if data:
625 self.lines = mdiff.splitnewlines(data)
621 self.lines = mdiff.splitnewlines(data)
626 if self.mode is None:
622 if self.mode is None:
627 self.mode = mode
623 self.mode = mode
628 if self.lines:
624 if self.lines:
629 # Normalize line endings
625 # Normalize line endings
630 if self.lines[0].endswith('\r\n'):
626 if self.lines[0].endswith('\r\n'):
631 self.eol = '\r\n'
627 self.eol = '\r\n'
632 elif self.lines[0].endswith('\n'):
628 elif self.lines[0].endswith('\n'):
633 self.eol = '\n'
629 self.eol = '\n'
634 if eolmode != 'strict':
630 if eolmode != 'strict':
635 nlines = []
631 nlines = []
636 for l in self.lines:
632 for l in self.lines:
637 if l.endswith('\r\n'):
633 if l.endswith('\r\n'):
638 l = l[:-2] + '\n'
634 l = l[:-2] + '\n'
639 nlines.append(l)
635 nlines.append(l)
640 self.lines = nlines
636 self.lines = nlines
641 else:
637 else:
642 if self.create:
638 if self.create:
643 self.missing = False
639 self.missing = False
644 if self.mode is None:
640 if self.mode is None:
645 self.mode = (False, False)
641 self.mode = (False, False)
646 if self.missing:
642 if self.missing:
647 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
643 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
648
644
649 self.hash = {}
645 self.hash = {}
650 self.dirty = 0
646 self.dirty = 0
651 self.offset = 0
647 self.offset = 0
652 self.skew = 0
648 self.skew = 0
653 self.rej = []
649 self.rej = []
654 self.fileprinted = False
650 self.fileprinted = False
655 self.printfile(False)
651 self.printfile(False)
656 self.hunks = 0
652 self.hunks = 0
657
653
658 def writelines(self, fname, lines, mode):
654 def writelines(self, fname, lines, mode):
659 if self.eolmode == 'auto':
655 if self.eolmode == 'auto':
660 eol = self.eol
656 eol = self.eol
661 elif self.eolmode == 'crlf':
657 elif self.eolmode == 'crlf':
662 eol = '\r\n'
658 eol = '\r\n'
663 else:
659 else:
664 eol = '\n'
660 eol = '\n'
665
661
666 if self.eolmode != 'strict' and eol and eol != '\n':
662 if self.eolmode != 'strict' and eol and eol != '\n':
667 rawlines = []
663 rawlines = []
668 for l in lines:
664 for l in lines:
669 if l and l[-1] == '\n':
665 if l and l[-1] == '\n':
670 l = l[:-1] + eol
666 l = l[:-1] + eol
671 rawlines.append(l)
667 rawlines.append(l)
672 lines = rawlines
668 lines = rawlines
673
669
674 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
670 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
675
671
676 def printfile(self, warn):
672 def printfile(self, warn):
677 if self.fileprinted:
673 if self.fileprinted:
678 return
674 return
679 if warn or self.ui.verbose:
675 if warn or self.ui.verbose:
680 self.fileprinted = True
676 self.fileprinted = True
681 s = _("patching file %s\n") % self.fname
677 s = _("patching file %s\n") % self.fname
682 if warn:
678 if warn:
683 self.ui.warn(s)
679 self.ui.warn(s)
684 else:
680 else:
685 self.ui.note(s)
681 self.ui.note(s)
686
682
687
683
688 def findlines(self, l, linenum):
684 def findlines(self, l, linenum):
689 # looks through the hash and finds candidate lines. The
685 # looks through the hash and finds candidate lines. The
690 # result is a list of line numbers sorted based on distance
686 # result is a list of line numbers sorted based on distance
691 # from linenum
687 # from linenum
692
688
693 cand = self.hash.get(l, [])
689 cand = self.hash.get(l, [])
694 if len(cand) > 1:
690 if len(cand) > 1:
695 # resort our list of potentials forward then back.
691 # resort our list of potentials forward then back.
696 cand.sort(key=lambda x: abs(x - linenum))
692 cand.sort(key=lambda x: abs(x - linenum))
697 return cand
693 return cand
698
694
699 def write_rej(self):
695 def write_rej(self):
700 # our rejects are a little different from patch(1). This always
696 # our rejects are a little different from patch(1). This always
701 # creates rejects in the same form as the original patch. A file
697 # creates rejects in the same form as the original patch. A file
702 # header is inserted so that you can run the reject through patch again
698 # header is inserted so that you can run the reject through patch again
703 # without having to type the filename.
699 # without having to type the filename.
704 if not self.rej:
700 if not self.rej:
705 return
701 return
706 base = os.path.basename(self.fname)
702 base = os.path.basename(self.fname)
707 lines = ["--- %s\n+++ %s\n" % (base, base)]
703 lines = ["--- %s\n+++ %s\n" % (base, base)]
708 for x in self.rej:
704 for x in self.rej:
709 for l in x.hunk:
705 for l in x.hunk:
710 lines.append(l)
706 lines.append(l)
711 if l[-1] != '\n':
707 if l[-1] != '\n':
712 lines.append("\n\ No newline at end of file\n")
708 lines.append("\n\ No newline at end of file\n")
713 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
709 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
714
710
715 def apply(self, h):
711 def apply(self, h):
716 if not h.complete():
712 if not h.complete():
717 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
713 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
718 (h.number, h.desc, len(h.a), h.lena, len(h.b),
714 (h.number, h.desc, len(h.a), h.lena, len(h.b),
719 h.lenb))
715 h.lenb))
720
716
721 self.hunks += 1
717 self.hunks += 1
722
718
723 if self.missing:
719 if self.missing:
724 self.rej.append(h)
720 self.rej.append(h)
725 return -1
721 return -1
726
722
727 if self.exists and self.create:
723 if self.exists and self.create:
728 if self.copysource:
724 if self.copysource:
729 self.ui.warn(_("cannot create %s: destination already "
725 self.ui.warn(_("cannot create %s: destination already "
730 "exists\n") % self.fname)
726 "exists\n") % self.fname)
731 else:
727 else:
732 self.ui.warn(_("file %s already exists\n") % self.fname)
728 self.ui.warn(_("file %s already exists\n") % self.fname)
733 self.rej.append(h)
729 self.rej.append(h)
734 return -1
730 return -1
735
731
736 if isinstance(h, binhunk):
732 if isinstance(h, binhunk):
737 if self.remove:
733 if self.remove:
738 self.backend.unlink(self.fname)
734 self.backend.unlink(self.fname)
739 else:
735 else:
740 l = h.new(self.lines)
736 l = h.new(self.lines)
741 self.lines[:] = l
737 self.lines[:] = l
742 self.offset += len(l)
738 self.offset += len(l)
743 self.dirty = True
739 self.dirty = True
744 return 0
740 return 0
745
741
746 horig = h
742 horig = h
747 if (self.eolmode in ('crlf', 'lf')
743 if (self.eolmode in ('crlf', 'lf')
748 or self.eolmode == 'auto' and self.eol):
744 or self.eolmode == 'auto' and self.eol):
749 # If new eols are going to be normalized, then normalize
745 # If new eols are going to be normalized, then normalize
750 # hunk data before patching. Otherwise, preserve input
746 # hunk data before patching. Otherwise, preserve input
751 # line-endings.
747 # line-endings.
752 h = h.getnormalized()
748 h = h.getnormalized()
753
749
754 # fast case first, no offsets, no fuzz
750 # fast case first, no offsets, no fuzz
755 old, oldstart, new, newstart = h.fuzzit(0, False)
751 old, oldstart, new, newstart = h.fuzzit(0, False)
756 oldstart += self.offset
752 oldstart += self.offset
757 orig_start = oldstart
753 orig_start = oldstart
758 # if there's skew we want to emit the "(offset %d lines)" even
754 # if there's skew we want to emit the "(offset %d lines)" even
759 # when the hunk cleanly applies at start + skew, so skip the
755 # when the hunk cleanly applies at start + skew, so skip the
760 # fast case code
756 # fast case code
761 if (self.skew == 0 and
757 if (self.skew == 0 and
762 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
758 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
763 if self.remove:
759 if self.remove:
764 self.backend.unlink(self.fname)
760 self.backend.unlink(self.fname)
765 else:
761 else:
766 self.lines[oldstart:oldstart + len(old)] = new
762 self.lines[oldstart:oldstart + len(old)] = new
767 self.offset += len(new) - len(old)
763 self.offset += len(new) - len(old)
768 self.dirty = True
764 self.dirty = True
769 return 0
765 return 0
770
766
771 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
767 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
772 self.hash = {}
768 self.hash = {}
773 for x, s in enumerate(self.lines):
769 for x, s in enumerate(self.lines):
774 self.hash.setdefault(s, []).append(x)
770 self.hash.setdefault(s, []).append(x)
775
771
776 for fuzzlen in xrange(self.ui.configint("patch", "fuzz", 2) + 1):
772 for fuzzlen in xrange(self.ui.configint("patch", "fuzz", 2) + 1):
777 for toponly in [True, False]:
773 for toponly in [True, False]:
778 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
774 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
779 oldstart = oldstart + self.offset + self.skew
775 oldstart = oldstart + self.offset + self.skew
780 oldstart = min(oldstart, len(self.lines))
776 oldstart = min(oldstart, len(self.lines))
781 if old:
777 if old:
782 cand = self.findlines(old[0][1:], oldstart)
778 cand = self.findlines(old[0][1:], oldstart)
783 else:
779 else:
784 # Only adding lines with no or fuzzed context, just
780 # Only adding lines with no or fuzzed context, just
785 # take the skew in account
781 # take the skew in account
786 cand = [oldstart]
782 cand = [oldstart]
787
783
788 for l in cand:
784 for l in cand:
789 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
785 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
790 self.lines[l : l + len(old)] = new
786 self.lines[l : l + len(old)] = new
791 self.offset += len(new) - len(old)
787 self.offset += len(new) - len(old)
792 self.skew = l - orig_start
788 self.skew = l - orig_start
793 self.dirty = True
789 self.dirty = True
794 offset = l - orig_start - fuzzlen
790 offset = l - orig_start - fuzzlen
795 if fuzzlen:
791 if fuzzlen:
796 msg = _("Hunk #%d succeeded at %d "
792 msg = _("Hunk #%d succeeded at %d "
797 "with fuzz %d "
793 "with fuzz %d "
798 "(offset %d lines).\n")
794 "(offset %d lines).\n")
799 self.printfile(True)
795 self.printfile(True)
800 self.ui.warn(msg %
796 self.ui.warn(msg %
801 (h.number, l + 1, fuzzlen, offset))
797 (h.number, l + 1, fuzzlen, offset))
802 else:
798 else:
803 msg = _("Hunk #%d succeeded at %d "
799 msg = _("Hunk #%d succeeded at %d "
804 "(offset %d lines).\n")
800 "(offset %d lines).\n")
805 self.ui.note(msg % (h.number, l + 1, offset))
801 self.ui.note(msg % (h.number, l + 1, offset))
806 return fuzzlen
802 return fuzzlen
807 self.printfile(True)
803 self.printfile(True)
808 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
804 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
809 self.rej.append(horig)
805 self.rej.append(horig)
810 return -1
806 return -1
811
807
812 def close(self):
808 def close(self):
813 if self.dirty:
809 if self.dirty:
814 self.writelines(self.fname, self.lines, self.mode)
810 self.writelines(self.fname, self.lines, self.mode)
815 self.write_rej()
811 self.write_rej()
816 return len(self.rej)
812 return len(self.rej)
817
813
818 class header(object):
814 class header(object):
819 """patch header
815 """patch header
820 """
816 """
821 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
817 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
822 diff_re = re.compile('diff -r .* (.*)$')
818 diff_re = re.compile('diff -r .* (.*)$')
823 allhunks_re = re.compile('(?:index|deleted file) ')
819 allhunks_re = re.compile('(?:index|deleted file) ')
824 pretty_re = re.compile('(?:new file|deleted file) ')
820 pretty_re = re.compile('(?:new file|deleted file) ')
825 special_re = re.compile('(?:index|deleted|copy|rename) ')
821 special_re = re.compile('(?:index|deleted|copy|rename) ')
826 newfile_re = re.compile('(?:new file)')
822 newfile_re = re.compile('(?:new file)')
827
823
828 def __init__(self, header):
824 def __init__(self, header):
829 self.header = header
825 self.header = header
830 self.hunks = []
826 self.hunks = []
831
827
832 def binary(self):
828 def binary(self):
833 return any(h.startswith('index ') for h in self.header)
829 return any(h.startswith('index ') for h in self.header)
834
830
835 def pretty(self, fp):
831 def pretty(self, fp):
836 for h in self.header:
832 for h in self.header:
837 if h.startswith('index '):
833 if h.startswith('index '):
838 fp.write(_('this modifies a binary file (all or nothing)\n'))
834 fp.write(_('this modifies a binary file (all or nothing)\n'))
839 break
835 break
840 if self.pretty_re.match(h):
836 if self.pretty_re.match(h):
841 fp.write(h)
837 fp.write(h)
842 if self.binary():
838 if self.binary():
843 fp.write(_('this is a binary file\n'))
839 fp.write(_('this is a binary file\n'))
844 break
840 break
845 if h.startswith('---'):
841 if h.startswith('---'):
846 fp.write(_('%d hunks, %d lines changed\n') %
842 fp.write(_('%d hunks, %d lines changed\n') %
847 (len(self.hunks),
843 (len(self.hunks),
848 sum([max(h.added, h.removed) for h in self.hunks])))
844 sum([max(h.added, h.removed) for h in self.hunks])))
849 break
845 break
850 fp.write(h)
846 fp.write(h)
851
847
852 def write(self, fp):
848 def write(self, fp):
853 fp.write(''.join(self.header))
849 fp.write(''.join(self.header))
854
850
855 def allhunks(self):
851 def allhunks(self):
856 return any(self.allhunks_re.match(h) for h in self.header)
852 return any(self.allhunks_re.match(h) for h in self.header)
857
853
858 def files(self):
854 def files(self):
859 match = self.diffgit_re.match(self.header[0])
855 match = self.diffgit_re.match(self.header[0])
860 if match:
856 if match:
861 fromfile, tofile = match.groups()
857 fromfile, tofile = match.groups()
862 if fromfile == tofile:
858 if fromfile == tofile:
863 return [fromfile]
859 return [fromfile]
864 return [fromfile, tofile]
860 return [fromfile, tofile]
865 else:
861 else:
866 return self.diff_re.match(self.header[0]).groups()
862 return self.diff_re.match(self.header[0]).groups()
867
863
868 def filename(self):
864 def filename(self):
869 return self.files()[-1]
865 return self.files()[-1]
870
866
871 def __repr__(self):
867 def __repr__(self):
872 return '<header %s>' % (' '.join(map(repr, self.files())))
868 return '<header %s>' % (' '.join(map(repr, self.files())))
873
869
874 def isnewfile(self):
870 def isnewfile(self):
875 return any(self.newfile_re.match(h) for h in self.header)
871 return any(self.newfile_re.match(h) for h in self.header)
876
872
877 def special(self):
873 def special(self):
878 # Special files are shown only at the header level and not at the hunk
874 # Special files are shown only at the header level and not at the hunk
879 # level for example a file that has been deleted is a special file.
875 # level for example a file that has been deleted is a special file.
880 # The user cannot change the content of the operation, in the case of
876 # The user cannot change the content of the operation, in the case of
881 # the deleted file he has to take the deletion or not take it, he
877 # the deleted file he has to take the deletion or not take it, he
882 # cannot take some of it.
878 # cannot take some of it.
883 # Newly added files are special if they are empty, they are not special
879 # Newly added files are special if they are empty, they are not special
884 # if they have some content as we want to be able to change it
880 # if they have some content as we want to be able to change it
885 nocontent = len(self.header) == 2
881 nocontent = len(self.header) == 2
886 emptynewfile = self.isnewfile() and nocontent
882 emptynewfile = self.isnewfile() and nocontent
887 return emptynewfile or \
883 return emptynewfile or \
888 any(self.special_re.match(h) for h in self.header)
884 any(self.special_re.match(h) for h in self.header)
889
885
890 class recordhunk(object):
886 class recordhunk(object):
891 """patch hunk
887 """patch hunk
892
888
893 XXX shouldn't we merge this with the other hunk class?
889 XXX shouldn't we merge this with the other hunk class?
894 """
890 """
895 maxcontext = 3
891 maxcontext = 3
896
892
897 def __init__(self, header, fromline, toline, proc, before, hunk, after):
893 def __init__(self, header, fromline, toline, proc, before, hunk, after):
898 def trimcontext(number, lines):
894 def trimcontext(number, lines):
899 delta = len(lines) - self.maxcontext
895 delta = len(lines) - self.maxcontext
900 if False and delta > 0:
896 if False and delta > 0:
901 return number + delta, lines[:self.maxcontext]
897 return number + delta, lines[:self.maxcontext]
902 return number, lines
898 return number, lines
903
899
904 self.header = header
900 self.header = header
905 self.fromline, self.before = trimcontext(fromline, before)
901 self.fromline, self.before = trimcontext(fromline, before)
906 self.toline, self.after = trimcontext(toline, after)
902 self.toline, self.after = trimcontext(toline, after)
907 self.proc = proc
903 self.proc = proc
908 self.hunk = hunk
904 self.hunk = hunk
909 self.added, self.removed = self.countchanges(self.hunk)
905 self.added, self.removed = self.countchanges(self.hunk)
910
906
911 def __eq__(self, v):
907 def __eq__(self, v):
912 if not isinstance(v, recordhunk):
908 if not isinstance(v, recordhunk):
913 return False
909 return False
914
910
915 return ((v.hunk == self.hunk) and
911 return ((v.hunk == self.hunk) and
916 (v.proc == self.proc) and
912 (v.proc == self.proc) and
917 (self.fromline == v.fromline) and
913 (self.fromline == v.fromline) and
918 (self.header.files() == v.header.files()))
914 (self.header.files() == v.header.files()))
919
915
920 def __hash__(self):
916 def __hash__(self):
921 return hash((tuple(self.hunk),
917 return hash((tuple(self.hunk),
922 tuple(self.header.files()),
918 tuple(self.header.files()),
923 self.fromline,
919 self.fromline,
924 self.proc))
920 self.proc))
925
921
926 def countchanges(self, hunk):
922 def countchanges(self, hunk):
927 """hunk -> (n+,n-)"""
923 """hunk -> (n+,n-)"""
928 add = len([h for h in hunk if h[0] == '+'])
924 add = len([h for h in hunk if h[0] == '+'])
929 rem = len([h for h in hunk if h[0] == '-'])
925 rem = len([h for h in hunk if h[0] == '-'])
930 return add, rem
926 return add, rem
931
927
932 def write(self, fp):
928 def write(self, fp):
933 delta = len(self.before) + len(self.after)
929 delta = len(self.before) + len(self.after)
934 if self.after and self.after[-1] == '\\ No newline at end of file\n':
930 if self.after and self.after[-1] == '\\ No newline at end of file\n':
935 delta -= 1
931 delta -= 1
936 fromlen = delta + self.removed
932 fromlen = delta + self.removed
937 tolen = delta + self.added
933 tolen = delta + self.added
938 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
934 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
939 (self.fromline, fromlen, self.toline, tolen,
935 (self.fromline, fromlen, self.toline, tolen,
940 self.proc and (' ' + self.proc)))
936 self.proc and (' ' + self.proc)))
941 fp.write(''.join(self.before + self.hunk + self.after))
937 fp.write(''.join(self.before + self.hunk + self.after))
942
938
943 pretty = write
939 pretty = write
944
940
945 def filename(self):
941 def filename(self):
946 return self.header.filename()
942 return self.header.filename()
947
943
948 def __repr__(self):
944 def __repr__(self):
949 return '<hunk %r@%d>' % (self.filename(), self.fromline)
945 return '<hunk %r@%d>' % (self.filename(), self.fromline)
950
946
951 def filterpatch(ui, headers, operation=None):
947 def filterpatch(ui, headers, operation=None):
952 """Interactively filter patch chunks into applied-only chunks"""
948 """Interactively filter patch chunks into applied-only chunks"""
953 if operation is None:
949 if operation is None:
954 operation = _('record')
950 operation = _('record')
955
951
956 def prompt(skipfile, skipall, query, chunk):
952 def prompt(skipfile, skipall, query, chunk):
957 """prompt query, and process base inputs
953 """prompt query, and process base inputs
958
954
959 - y/n for the rest of file
955 - y/n for the rest of file
960 - y/n for the rest
956 - y/n for the rest
961 - ? (help)
957 - ? (help)
962 - q (quit)
958 - q (quit)
963
959
964 Return True/False and possibly updated skipfile and skipall.
960 Return True/False and possibly updated skipfile and skipall.
965 """
961 """
966 newpatches = None
962 newpatches = None
967 if skipall is not None:
963 if skipall is not None:
968 return skipall, skipfile, skipall, newpatches
964 return skipall, skipfile, skipall, newpatches
969 if skipfile is not None:
965 if skipfile is not None:
970 return skipfile, skipfile, skipall, newpatches
966 return skipfile, skipfile, skipall, newpatches
971 while True:
967 while True:
972 resps = _('[Ynesfdaq?]'
968 resps = _('[Ynesfdaq?]'
973 '$$ &Yes, record this change'
969 '$$ &Yes, record this change'
974 '$$ &No, skip this change'
970 '$$ &No, skip this change'
975 '$$ &Edit this change manually'
971 '$$ &Edit this change manually'
976 '$$ &Skip remaining changes to this file'
972 '$$ &Skip remaining changes to this file'
977 '$$ Record remaining changes to this &file'
973 '$$ Record remaining changes to this &file'
978 '$$ &Done, skip remaining changes and files'
974 '$$ &Done, skip remaining changes and files'
979 '$$ Record &all changes to all remaining files'
975 '$$ Record &all changes to all remaining files'
980 '$$ &Quit, recording no changes'
976 '$$ &Quit, recording no changes'
981 '$$ &? (display help)')
977 '$$ &? (display help)')
982 r = ui.promptchoice("%s %s" % (query, resps))
978 r = ui.promptchoice("%s %s" % (query, resps))
983 ui.write("\n")
979 ui.write("\n")
984 if r == 8: # ?
980 if r == 8: # ?
985 for c, t in ui.extractchoices(resps)[1]:
981 for c, t in ui.extractchoices(resps)[1]:
986 ui.write('%s - %s\n' % (c, t.lower()))
982 ui.write('%s - %s\n' % (c, t.lower()))
987 continue
983 continue
988 elif r == 0: # yes
984 elif r == 0: # yes
989 ret = True
985 ret = True
990 elif r == 1: # no
986 elif r == 1: # no
991 ret = False
987 ret = False
992 elif r == 2: # Edit patch
988 elif r == 2: # Edit patch
993 if chunk is None:
989 if chunk is None:
994 ui.write(_('cannot edit patch for whole file'))
990 ui.write(_('cannot edit patch for whole file'))
995 ui.write("\n")
991 ui.write("\n")
996 continue
992 continue
997 if chunk.header.binary():
993 if chunk.header.binary():
998 ui.write(_('cannot edit patch for binary file'))
994 ui.write(_('cannot edit patch for binary file'))
999 ui.write("\n")
995 ui.write("\n")
1000 continue
996 continue
1001 # Patch comment based on the Git one (based on comment at end of
997 # Patch comment based on the Git one (based on comment at end of
1002 # http://mercurial.selenic.com/wiki/RecordExtension)
998 # http://mercurial.selenic.com/wiki/RecordExtension)
1003 phelp = '---' + _("""
999 phelp = '---' + _("""
1004 To remove '-' lines, make them ' ' lines (context).
1000 To remove '-' lines, make them ' ' lines (context).
1005 To remove '+' lines, delete them.
1001 To remove '+' lines, delete them.
1006 Lines starting with # will be removed from the patch.
1002 Lines starting with # will be removed from the patch.
1007
1003
1008 If the patch applies cleanly, the edited hunk will immediately be
1004 If the patch applies cleanly, the edited hunk will immediately be
1009 added to the record list. If it does not apply cleanly, a rejects
1005 added to the record list. If it does not apply cleanly, a rejects
1010 file will be generated: you can use that when you try again. If
1006 file will be generated: you can use that when you try again. If
1011 all lines of the hunk are removed, then the edit is aborted and
1007 all lines of the hunk are removed, then the edit is aborted and
1012 the hunk is left unchanged.
1008 the hunk is left unchanged.
1013 """)
1009 """)
1014 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1010 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1015 suffix=".diff", text=True)
1011 suffix=".diff", text=True)
1016 ncpatchfp = None
1012 ncpatchfp = None
1017 try:
1013 try:
1018 # Write the initial patch
1014 # Write the initial patch
1019 f = os.fdopen(patchfd, "w")
1015 f = os.fdopen(patchfd, "w")
1020 chunk.header.write(f)
1016 chunk.header.write(f)
1021 chunk.write(f)
1017 chunk.write(f)
1022 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1018 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1023 f.close()
1019 f.close()
1024 # Start the editor and wait for it to complete
1020 # Start the editor and wait for it to complete
1025 editor = ui.geteditor()
1021 editor = ui.geteditor()
1026 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1022 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1027 environ={'HGUSER': ui.username()})
1023 environ={'HGUSER': ui.username()})
1028 if ret != 0:
1024 if ret != 0:
1029 ui.warn(_("editor exited with exit code %d\n") % ret)
1025 ui.warn(_("editor exited with exit code %d\n") % ret)
1030 continue
1026 continue
1031 # Remove comment lines
1027 # Remove comment lines
1032 patchfp = open(patchfn)
1028 patchfp = open(patchfn)
1033 ncpatchfp = cStringIO.StringIO()
1029 ncpatchfp = cStringIO.StringIO()
1034 for line in patchfp:
1030 for line in patchfp:
1035 if not line.startswith('#'):
1031 if not line.startswith('#'):
1036 ncpatchfp.write(line)
1032 ncpatchfp.write(line)
1037 patchfp.close()
1033 patchfp.close()
1038 ncpatchfp.seek(0)
1034 ncpatchfp.seek(0)
1039 newpatches = parsepatch(ncpatchfp)
1035 newpatches = parsepatch(ncpatchfp)
1040 finally:
1036 finally:
1041 os.unlink(patchfn)
1037 os.unlink(patchfn)
1042 del ncpatchfp
1038 del ncpatchfp
1043 # Signal that the chunk shouldn't be applied as-is, but
1039 # Signal that the chunk shouldn't be applied as-is, but
1044 # provide the new patch to be used instead.
1040 # provide the new patch to be used instead.
1045 ret = False
1041 ret = False
1046 elif r == 3: # Skip
1042 elif r == 3: # Skip
1047 ret = skipfile = False
1043 ret = skipfile = False
1048 elif r == 4: # file (Record remaining)
1044 elif r == 4: # file (Record remaining)
1049 ret = skipfile = True
1045 ret = skipfile = True
1050 elif r == 5: # done, skip remaining
1046 elif r == 5: # done, skip remaining
1051 ret = skipall = False
1047 ret = skipall = False
1052 elif r == 6: # all
1048 elif r == 6: # all
1053 ret = skipall = True
1049 ret = skipall = True
1054 elif r == 7: # quit
1050 elif r == 7: # quit
1055 raise util.Abort(_('user quit'))
1051 raise util.Abort(_('user quit'))
1056 return ret, skipfile, skipall, newpatches
1052 return ret, skipfile, skipall, newpatches
1057
1053
1058 seen = set()
1054 seen = set()
1059 applied = {} # 'filename' -> [] of chunks
1055 applied = {} # 'filename' -> [] of chunks
1060 skipfile, skipall = None, None
1056 skipfile, skipall = None, None
1061 pos, total = 1, sum(len(h.hunks) for h in headers)
1057 pos, total = 1, sum(len(h.hunks) for h in headers)
1062 for h in headers:
1058 for h in headers:
1063 pos += len(h.hunks)
1059 pos += len(h.hunks)
1064 skipfile = None
1060 skipfile = None
1065 fixoffset = 0
1061 fixoffset = 0
1066 hdr = ''.join(h.header)
1062 hdr = ''.join(h.header)
1067 if hdr in seen:
1063 if hdr in seen:
1068 continue
1064 continue
1069 seen.add(hdr)
1065 seen.add(hdr)
1070 if skipall is None:
1066 if skipall is None:
1071 h.pretty(ui)
1067 h.pretty(ui)
1072 msg = (_('examine changes to %s?') %
1068 msg = (_('examine changes to %s?') %
1073 _(' and ').join("'%s'" % f for f in h.files()))
1069 _(' and ').join("'%s'" % f for f in h.files()))
1074 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1070 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1075 if not r:
1071 if not r:
1076 continue
1072 continue
1077 applied[h.filename()] = [h]
1073 applied[h.filename()] = [h]
1078 if h.allhunks():
1074 if h.allhunks():
1079 applied[h.filename()] += h.hunks
1075 applied[h.filename()] += h.hunks
1080 continue
1076 continue
1081 for i, chunk in enumerate(h.hunks):
1077 for i, chunk in enumerate(h.hunks):
1082 if skipfile is None and skipall is None:
1078 if skipfile is None and skipall is None:
1083 chunk.pretty(ui)
1079 chunk.pretty(ui)
1084 if total == 1:
1080 if total == 1:
1085 msg = _("record this change to '%s'?") % chunk.filename()
1081 msg = _("record this change to '%s'?") % chunk.filename()
1086 else:
1082 else:
1087 idx = pos - len(h.hunks) + i
1083 idx = pos - len(h.hunks) + i
1088 msg = _("record change %d/%d to '%s'?") % (idx, total,
1084 msg = _("record change %d/%d to '%s'?") % (idx, total,
1089 chunk.filename())
1085 chunk.filename())
1090 r, skipfile, skipall, newpatches = prompt(skipfile,
1086 r, skipfile, skipall, newpatches = prompt(skipfile,
1091 skipall, msg, chunk)
1087 skipall, msg, chunk)
1092 if r:
1088 if r:
1093 if fixoffset:
1089 if fixoffset:
1094 chunk = copy.copy(chunk)
1090 chunk = copy.copy(chunk)
1095 chunk.toline += fixoffset
1091 chunk.toline += fixoffset
1096 applied[chunk.filename()].append(chunk)
1092 applied[chunk.filename()].append(chunk)
1097 elif newpatches is not None:
1093 elif newpatches is not None:
1098 for newpatch in newpatches:
1094 for newpatch in newpatches:
1099 for newhunk in newpatch.hunks:
1095 for newhunk in newpatch.hunks:
1100 if fixoffset:
1096 if fixoffset:
1101 newhunk.toline += fixoffset
1097 newhunk.toline += fixoffset
1102 applied[newhunk.filename()].append(newhunk)
1098 applied[newhunk.filename()].append(newhunk)
1103 else:
1099 else:
1104 fixoffset += chunk.removed - chunk.added
1100 fixoffset += chunk.removed - chunk.added
1105 return sum([h for h in applied.itervalues()
1101 return sum([h for h in applied.itervalues()
1106 if h[0].special() or len(h) > 1], [])
1102 if h[0].special() or len(h) > 1], [])
1107 class hunk(object):
1103 class hunk(object):
1108 def __init__(self, desc, num, lr, context):
1104 def __init__(self, desc, num, lr, context):
1109 self.number = num
1105 self.number = num
1110 self.desc = desc
1106 self.desc = desc
1111 self.hunk = [desc]
1107 self.hunk = [desc]
1112 self.a = []
1108 self.a = []
1113 self.b = []
1109 self.b = []
1114 self.starta = self.lena = None
1110 self.starta = self.lena = None
1115 self.startb = self.lenb = None
1111 self.startb = self.lenb = None
1116 if lr is not None:
1112 if lr is not None:
1117 if context:
1113 if context:
1118 self.read_context_hunk(lr)
1114 self.read_context_hunk(lr)
1119 else:
1115 else:
1120 self.read_unified_hunk(lr)
1116 self.read_unified_hunk(lr)
1121
1117
1122 def getnormalized(self):
1118 def getnormalized(self):
1123 """Return a copy with line endings normalized to LF."""
1119 """Return a copy with line endings normalized to LF."""
1124
1120
1125 def normalize(lines):
1121 def normalize(lines):
1126 nlines = []
1122 nlines = []
1127 for line in lines:
1123 for line in lines:
1128 if line.endswith('\r\n'):
1124 if line.endswith('\r\n'):
1129 line = line[:-2] + '\n'
1125 line = line[:-2] + '\n'
1130 nlines.append(line)
1126 nlines.append(line)
1131 return nlines
1127 return nlines
1132
1128
1133 # Dummy object, it is rebuilt manually
1129 # Dummy object, it is rebuilt manually
1134 nh = hunk(self.desc, self.number, None, None)
1130 nh = hunk(self.desc, self.number, None, None)
1135 nh.number = self.number
1131 nh.number = self.number
1136 nh.desc = self.desc
1132 nh.desc = self.desc
1137 nh.hunk = self.hunk
1133 nh.hunk = self.hunk
1138 nh.a = normalize(self.a)
1134 nh.a = normalize(self.a)
1139 nh.b = normalize(self.b)
1135 nh.b = normalize(self.b)
1140 nh.starta = self.starta
1136 nh.starta = self.starta
1141 nh.startb = self.startb
1137 nh.startb = self.startb
1142 nh.lena = self.lena
1138 nh.lena = self.lena
1143 nh.lenb = self.lenb
1139 nh.lenb = self.lenb
1144 return nh
1140 return nh
1145
1141
1146 def read_unified_hunk(self, lr):
1142 def read_unified_hunk(self, lr):
1147 m = unidesc.match(self.desc)
1143 m = unidesc.match(self.desc)
1148 if not m:
1144 if not m:
1149 raise PatchError(_("bad hunk #%d") % self.number)
1145 raise PatchError(_("bad hunk #%d") % self.number)
1150 self.starta, self.lena, self.startb, self.lenb = m.groups()
1146 self.starta, self.lena, self.startb, self.lenb = m.groups()
1151 if self.lena is None:
1147 if self.lena is None:
1152 self.lena = 1
1148 self.lena = 1
1153 else:
1149 else:
1154 self.lena = int(self.lena)
1150 self.lena = int(self.lena)
1155 if self.lenb is None:
1151 if self.lenb is None:
1156 self.lenb = 1
1152 self.lenb = 1
1157 else:
1153 else:
1158 self.lenb = int(self.lenb)
1154 self.lenb = int(self.lenb)
1159 self.starta = int(self.starta)
1155 self.starta = int(self.starta)
1160 self.startb = int(self.startb)
1156 self.startb = int(self.startb)
1161 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1157 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1162 self.b)
1158 self.b)
1163 # if we hit eof before finishing out the hunk, the last line will
1159 # if we hit eof before finishing out the hunk, the last line will
1164 # be zero length. Lets try to fix it up.
1160 # be zero length. Lets try to fix it up.
1165 while len(self.hunk[-1]) == 0:
1161 while len(self.hunk[-1]) == 0:
1166 del self.hunk[-1]
1162 del self.hunk[-1]
1167 del self.a[-1]
1163 del self.a[-1]
1168 del self.b[-1]
1164 del self.b[-1]
1169 self.lena -= 1
1165 self.lena -= 1
1170 self.lenb -= 1
1166 self.lenb -= 1
1171 self._fixnewline(lr)
1167 self._fixnewline(lr)
1172
1168
1173 def read_context_hunk(self, lr):
1169 def read_context_hunk(self, lr):
1174 self.desc = lr.readline()
1170 self.desc = lr.readline()
1175 m = contextdesc.match(self.desc)
1171 m = contextdesc.match(self.desc)
1176 if not m:
1172 if not m:
1177 raise PatchError(_("bad hunk #%d") % self.number)
1173 raise PatchError(_("bad hunk #%d") % self.number)
1178 self.starta, aend = m.groups()
1174 self.starta, aend = m.groups()
1179 self.starta = int(self.starta)
1175 self.starta = int(self.starta)
1180 if aend is None:
1176 if aend is None:
1181 aend = self.starta
1177 aend = self.starta
1182 self.lena = int(aend) - self.starta
1178 self.lena = int(aend) - self.starta
1183 if self.starta:
1179 if self.starta:
1184 self.lena += 1
1180 self.lena += 1
1185 for x in xrange(self.lena):
1181 for x in xrange(self.lena):
1186 l = lr.readline()
1182 l = lr.readline()
1187 if l.startswith('---'):
1183 if l.startswith('---'):
1188 # lines addition, old block is empty
1184 # lines addition, old block is empty
1189 lr.push(l)
1185 lr.push(l)
1190 break
1186 break
1191 s = l[2:]
1187 s = l[2:]
1192 if l.startswith('- ') or l.startswith('! '):
1188 if l.startswith('- ') or l.startswith('! '):
1193 u = '-' + s
1189 u = '-' + s
1194 elif l.startswith(' '):
1190 elif l.startswith(' '):
1195 u = ' ' + s
1191 u = ' ' + s
1196 else:
1192 else:
1197 raise PatchError(_("bad hunk #%d old text line %d") %
1193 raise PatchError(_("bad hunk #%d old text line %d") %
1198 (self.number, x))
1194 (self.number, x))
1199 self.a.append(u)
1195 self.a.append(u)
1200 self.hunk.append(u)
1196 self.hunk.append(u)
1201
1197
1202 l = lr.readline()
1198 l = lr.readline()
1203 if l.startswith('\ '):
1199 if l.startswith('\ '):
1204 s = self.a[-1][:-1]
1200 s = self.a[-1][:-1]
1205 self.a[-1] = s
1201 self.a[-1] = s
1206 self.hunk[-1] = s
1202 self.hunk[-1] = s
1207 l = lr.readline()
1203 l = lr.readline()
1208 m = contextdesc.match(l)
1204 m = contextdesc.match(l)
1209 if not m:
1205 if not m:
1210 raise PatchError(_("bad hunk #%d") % self.number)
1206 raise PatchError(_("bad hunk #%d") % self.number)
1211 self.startb, bend = m.groups()
1207 self.startb, bend = m.groups()
1212 self.startb = int(self.startb)
1208 self.startb = int(self.startb)
1213 if bend is None:
1209 if bend is None:
1214 bend = self.startb
1210 bend = self.startb
1215 self.lenb = int(bend) - self.startb
1211 self.lenb = int(bend) - self.startb
1216 if self.startb:
1212 if self.startb:
1217 self.lenb += 1
1213 self.lenb += 1
1218 hunki = 1
1214 hunki = 1
1219 for x in xrange(self.lenb):
1215 for x in xrange(self.lenb):
1220 l = lr.readline()
1216 l = lr.readline()
1221 if l.startswith('\ '):
1217 if l.startswith('\ '):
1222 # XXX: the only way to hit this is with an invalid line range.
1218 # XXX: the only way to hit this is with an invalid line range.
1223 # The no-eol marker is not counted in the line range, but I
1219 # The no-eol marker is not counted in the line range, but I
1224 # guess there are diff(1) out there which behave differently.
1220 # guess there are diff(1) out there which behave differently.
1225 s = self.b[-1][:-1]
1221 s = self.b[-1][:-1]
1226 self.b[-1] = s
1222 self.b[-1] = s
1227 self.hunk[hunki - 1] = s
1223 self.hunk[hunki - 1] = s
1228 continue
1224 continue
1229 if not l:
1225 if not l:
1230 # line deletions, new block is empty and we hit EOF
1226 # line deletions, new block is empty and we hit EOF
1231 lr.push(l)
1227 lr.push(l)
1232 break
1228 break
1233 s = l[2:]
1229 s = l[2:]
1234 if l.startswith('+ ') or l.startswith('! '):
1230 if l.startswith('+ ') or l.startswith('! '):
1235 u = '+' + s
1231 u = '+' + s
1236 elif l.startswith(' '):
1232 elif l.startswith(' '):
1237 u = ' ' + s
1233 u = ' ' + s
1238 elif len(self.b) == 0:
1234 elif len(self.b) == 0:
1239 # line deletions, new block is empty
1235 # line deletions, new block is empty
1240 lr.push(l)
1236 lr.push(l)
1241 break
1237 break
1242 else:
1238 else:
1243 raise PatchError(_("bad hunk #%d old text line %d") %
1239 raise PatchError(_("bad hunk #%d old text line %d") %
1244 (self.number, x))
1240 (self.number, x))
1245 self.b.append(s)
1241 self.b.append(s)
1246 while True:
1242 while True:
1247 if hunki >= len(self.hunk):
1243 if hunki >= len(self.hunk):
1248 h = ""
1244 h = ""
1249 else:
1245 else:
1250 h = self.hunk[hunki]
1246 h = self.hunk[hunki]
1251 hunki += 1
1247 hunki += 1
1252 if h == u:
1248 if h == u:
1253 break
1249 break
1254 elif h.startswith('-'):
1250 elif h.startswith('-'):
1255 continue
1251 continue
1256 else:
1252 else:
1257 self.hunk.insert(hunki - 1, u)
1253 self.hunk.insert(hunki - 1, u)
1258 break
1254 break
1259
1255
1260 if not self.a:
1256 if not self.a:
1261 # this happens when lines were only added to the hunk
1257 # this happens when lines were only added to the hunk
1262 for x in self.hunk:
1258 for x in self.hunk:
1263 if x.startswith('-') or x.startswith(' '):
1259 if x.startswith('-') or x.startswith(' '):
1264 self.a.append(x)
1260 self.a.append(x)
1265 if not self.b:
1261 if not self.b:
1266 # this happens when lines were only deleted from the hunk
1262 # this happens when lines were only deleted from the hunk
1267 for x in self.hunk:
1263 for x in self.hunk:
1268 if x.startswith('+') or x.startswith(' '):
1264 if x.startswith('+') or x.startswith(' '):
1269 self.b.append(x[1:])
1265 self.b.append(x[1:])
1270 # @@ -start,len +start,len @@
1266 # @@ -start,len +start,len @@
1271 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1267 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1272 self.startb, self.lenb)
1268 self.startb, self.lenb)
1273 self.hunk[0] = self.desc
1269 self.hunk[0] = self.desc
1274 self._fixnewline(lr)
1270 self._fixnewline(lr)
1275
1271
1276 def _fixnewline(self, lr):
1272 def _fixnewline(self, lr):
1277 l = lr.readline()
1273 l = lr.readline()
1278 if l.startswith('\ '):
1274 if l.startswith('\ '):
1279 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1275 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1280 else:
1276 else:
1281 lr.push(l)
1277 lr.push(l)
1282
1278
1283 def complete(self):
1279 def complete(self):
1284 return len(self.a) == self.lena and len(self.b) == self.lenb
1280 return len(self.a) == self.lena and len(self.b) == self.lenb
1285
1281
1286 def _fuzzit(self, old, new, fuzz, toponly):
1282 def _fuzzit(self, old, new, fuzz, toponly):
1287 # this removes context lines from the top and bottom of list 'l'. It
1283 # this removes context lines from the top and bottom of list 'l'. It
1288 # checks the hunk to make sure only context lines are removed, and then
1284 # checks the hunk to make sure only context lines are removed, and then
1289 # returns a new shortened list of lines.
1285 # returns a new shortened list of lines.
1290 fuzz = min(fuzz, len(old))
1286 fuzz = min(fuzz, len(old))
1291 if fuzz:
1287 if fuzz:
1292 top = 0
1288 top = 0
1293 bot = 0
1289 bot = 0
1294 hlen = len(self.hunk)
1290 hlen = len(self.hunk)
1295 for x in xrange(hlen - 1):
1291 for x in xrange(hlen - 1):
1296 # the hunk starts with the @@ line, so use x+1
1292 # the hunk starts with the @@ line, so use x+1
1297 if self.hunk[x + 1][0] == ' ':
1293 if self.hunk[x + 1][0] == ' ':
1298 top += 1
1294 top += 1
1299 else:
1295 else:
1300 break
1296 break
1301 if not toponly:
1297 if not toponly:
1302 for x in xrange(hlen - 1):
1298 for x in xrange(hlen - 1):
1303 if self.hunk[hlen - bot - 1][0] == ' ':
1299 if self.hunk[hlen - bot - 1][0] == ' ':
1304 bot += 1
1300 bot += 1
1305 else:
1301 else:
1306 break
1302 break
1307
1303
1308 bot = min(fuzz, bot)
1304 bot = min(fuzz, bot)
1309 top = min(fuzz, top)
1305 top = min(fuzz, top)
1310 return old[top:len(old) - bot], new[top:len(new) - bot], top
1306 return old[top:len(old) - bot], new[top:len(new) - bot], top
1311 return old, new, 0
1307 return old, new, 0
1312
1308
1313 def fuzzit(self, fuzz, toponly):
1309 def fuzzit(self, fuzz, toponly):
1314 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1310 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1315 oldstart = self.starta + top
1311 oldstart = self.starta + top
1316 newstart = self.startb + top
1312 newstart = self.startb + top
1317 # zero length hunk ranges already have their start decremented
1313 # zero length hunk ranges already have their start decremented
1318 if self.lena and oldstart > 0:
1314 if self.lena and oldstart > 0:
1319 oldstart -= 1
1315 oldstart -= 1
1320 if self.lenb and newstart > 0:
1316 if self.lenb and newstart > 0:
1321 newstart -= 1
1317 newstart -= 1
1322 return old, oldstart, new, newstart
1318 return old, oldstart, new, newstart
1323
1319
1324 class binhunk(object):
1320 class binhunk(object):
1325 'A binary patch file.'
1321 'A binary patch file.'
1326 def __init__(self, lr, fname):
1322 def __init__(self, lr, fname):
1327 self.text = None
1323 self.text = None
1328 self.delta = False
1324 self.delta = False
1329 self.hunk = ['GIT binary patch\n']
1325 self.hunk = ['GIT binary patch\n']
1330 self._fname = fname
1326 self._fname = fname
1331 self._read(lr)
1327 self._read(lr)
1332
1328
1333 def complete(self):
1329 def complete(self):
1334 return self.text is not None
1330 return self.text is not None
1335
1331
1336 def new(self, lines):
1332 def new(self, lines):
1337 if self.delta:
1333 if self.delta:
1338 return [applybindelta(self.text, ''.join(lines))]
1334 return [applybindelta(self.text, ''.join(lines))]
1339 return [self.text]
1335 return [self.text]
1340
1336
1341 def _read(self, lr):
1337 def _read(self, lr):
1342 def getline(lr, hunk):
1338 def getline(lr, hunk):
1343 l = lr.readline()
1339 l = lr.readline()
1344 hunk.append(l)
1340 hunk.append(l)
1345 return l.rstrip('\r\n')
1341 return l.rstrip('\r\n')
1346
1342
1347 size = 0
1343 size = 0
1348 while True:
1344 while True:
1349 line = getline(lr, self.hunk)
1345 line = getline(lr, self.hunk)
1350 if not line:
1346 if not line:
1351 raise PatchError(_('could not extract "%s" binary data')
1347 raise PatchError(_('could not extract "%s" binary data')
1352 % self._fname)
1348 % self._fname)
1353 if line.startswith('literal '):
1349 if line.startswith('literal '):
1354 size = int(line[8:].rstrip())
1350 size = int(line[8:].rstrip())
1355 break
1351 break
1356 if line.startswith('delta '):
1352 if line.startswith('delta '):
1357 size = int(line[6:].rstrip())
1353 size = int(line[6:].rstrip())
1358 self.delta = True
1354 self.delta = True
1359 break
1355 break
1360 dec = []
1356 dec = []
1361 line = getline(lr, self.hunk)
1357 line = getline(lr, self.hunk)
1362 while len(line) > 1:
1358 while len(line) > 1:
1363 l = line[0]
1359 l = line[0]
1364 if l <= 'Z' and l >= 'A':
1360 if l <= 'Z' and l >= 'A':
1365 l = ord(l) - ord('A') + 1
1361 l = ord(l) - ord('A') + 1
1366 else:
1362 else:
1367 l = ord(l) - ord('a') + 27
1363 l = ord(l) - ord('a') + 27
1368 try:
1364 try:
1369 dec.append(base85.b85decode(line[1:])[:l])
1365 dec.append(base85.b85decode(line[1:])[:l])
1370 except ValueError, e:
1366 except ValueError, e:
1371 raise PatchError(_('could not decode "%s" binary patch: %s')
1367 raise PatchError(_('could not decode "%s" binary patch: %s')
1372 % (self._fname, str(e)))
1368 % (self._fname, str(e)))
1373 line = getline(lr, self.hunk)
1369 line = getline(lr, self.hunk)
1374 text = zlib.decompress(''.join(dec))
1370 text = zlib.decompress(''.join(dec))
1375 if len(text) != size:
1371 if len(text) != size:
1376 raise PatchError(_('"%s" length is %d bytes, should be %d')
1372 raise PatchError(_('"%s" length is %d bytes, should be %d')
1377 % (self._fname, len(text), size))
1373 % (self._fname, len(text), size))
1378 self.text = text
1374 self.text = text
1379
1375
1380 def parsefilename(str):
1376 def parsefilename(str):
1381 # --- filename \t|space stuff
1377 # --- filename \t|space stuff
1382 s = str[4:].rstrip('\r\n')
1378 s = str[4:].rstrip('\r\n')
1383 i = s.find('\t')
1379 i = s.find('\t')
1384 if i < 0:
1380 if i < 0:
1385 i = s.find(' ')
1381 i = s.find(' ')
1386 if i < 0:
1382 if i < 0:
1387 return s
1383 return s
1388 return s[:i]
1384 return s[:i]
1389
1385
1390 def reversehunks(hunks):
1386 def reversehunks(hunks):
1391 '''reverse the signs in the hunks given as argument
1387 '''reverse the signs in the hunks given as argument
1392
1388
1393 This function operates on hunks coming out of patch.filterpatch, that is
1389 This function operates on hunks coming out of patch.filterpatch, that is
1394 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1390 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1395
1391
1396 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1392 >>> rawpatch = """diff --git a/folder1/g b/folder1/g
1397 ... --- a/folder1/g
1393 ... --- a/folder1/g
1398 ... +++ b/folder1/g
1394 ... +++ b/folder1/g
1399 ... @@ -1,7 +1,7 @@
1395 ... @@ -1,7 +1,7 @@
1400 ... +firstline
1396 ... +firstline
1401 ... c
1397 ... c
1402 ... 1
1398 ... 1
1403 ... 2
1399 ... 2
1404 ... + 3
1400 ... + 3
1405 ... -4
1401 ... -4
1406 ... 5
1402 ... 5
1407 ... d
1403 ... d
1408 ... +lastline"""
1404 ... +lastline"""
1409 >>> hunks = parsepatch(rawpatch)
1405 >>> hunks = parsepatch(rawpatch)
1410 >>> hunkscomingfromfilterpatch = []
1406 >>> hunkscomingfromfilterpatch = []
1411 >>> for h in hunks:
1407 >>> for h in hunks:
1412 ... hunkscomingfromfilterpatch.append(h)
1408 ... hunkscomingfromfilterpatch.append(h)
1413 ... hunkscomingfromfilterpatch.extend(h.hunks)
1409 ... hunkscomingfromfilterpatch.extend(h.hunks)
1414
1410
1415 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1411 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1416 >>> fp = cStringIO.StringIO()
1412 >>> fp = cStringIO.StringIO()
1417 >>> for c in reversedhunks:
1413 >>> for c in reversedhunks:
1418 ... c.write(fp)
1414 ... c.write(fp)
1419 >>> fp.seek(0)
1415 >>> fp.seek(0)
1420 >>> reversedpatch = fp.read()
1416 >>> reversedpatch = fp.read()
1421 >>> print reversedpatch
1417 >>> print reversedpatch
1422 diff --git a/folder1/g b/folder1/g
1418 diff --git a/folder1/g b/folder1/g
1423 --- a/folder1/g
1419 --- a/folder1/g
1424 +++ b/folder1/g
1420 +++ b/folder1/g
1425 @@ -1,4 +1,3 @@
1421 @@ -1,4 +1,3 @@
1426 -firstline
1422 -firstline
1427 c
1423 c
1428 1
1424 1
1429 2
1425 2
1430 @@ -1,6 +2,6 @@
1426 @@ -1,6 +2,6 @@
1431 c
1427 c
1432 1
1428 1
1433 2
1429 2
1434 - 3
1430 - 3
1435 +4
1431 +4
1436 5
1432 5
1437 d
1433 d
1438 @@ -5,3 +6,2 @@
1434 @@ -5,3 +6,2 @@
1439 5
1435 5
1440 d
1436 d
1441 -lastline
1437 -lastline
1442
1438
1443 '''
1439 '''
1444
1440
1445 import crecord as crecordmod
1441 import crecord as crecordmod
1446 newhunks = []
1442 newhunks = []
1447 for c in hunks:
1443 for c in hunks:
1448 if isinstance(c, crecordmod.uihunk):
1444 if isinstance(c, crecordmod.uihunk):
1449 # curses hunks encapsulate the record hunk in _hunk
1445 # curses hunks encapsulate the record hunk in _hunk
1450 c = c._hunk
1446 c = c._hunk
1451 if isinstance(c, recordhunk):
1447 if isinstance(c, recordhunk):
1452 for j, line in enumerate(c.hunk):
1448 for j, line in enumerate(c.hunk):
1453 if line.startswith("-"):
1449 if line.startswith("-"):
1454 c.hunk[j] = "+" + c.hunk[j][1:]
1450 c.hunk[j] = "+" + c.hunk[j][1:]
1455 elif line.startswith("+"):
1451 elif line.startswith("+"):
1456 c.hunk[j] = "-" + c.hunk[j][1:]
1452 c.hunk[j] = "-" + c.hunk[j][1:]
1457 c.added, c.removed = c.removed, c.added
1453 c.added, c.removed = c.removed, c.added
1458 newhunks.append(c)
1454 newhunks.append(c)
1459 return newhunks
1455 return newhunks
1460
1456
1461 def parsepatch(originalchunks):
1457 def parsepatch(originalchunks):
1462 """patch -> [] of headers -> [] of hunks """
1458 """patch -> [] of headers -> [] of hunks """
1463 class parser(object):
1459 class parser(object):
1464 """patch parsing state machine"""
1460 """patch parsing state machine"""
1465 def __init__(self):
1461 def __init__(self):
1466 self.fromline = 0
1462 self.fromline = 0
1467 self.toline = 0
1463 self.toline = 0
1468 self.proc = ''
1464 self.proc = ''
1469 self.header = None
1465 self.header = None
1470 self.context = []
1466 self.context = []
1471 self.before = []
1467 self.before = []
1472 self.hunk = []
1468 self.hunk = []
1473 self.headers = []
1469 self.headers = []
1474
1470
1475 def addrange(self, limits):
1471 def addrange(self, limits):
1476 fromstart, fromend, tostart, toend, proc = limits
1472 fromstart, fromend, tostart, toend, proc = limits
1477 self.fromline = int(fromstart)
1473 self.fromline = int(fromstart)
1478 self.toline = int(tostart)
1474 self.toline = int(tostart)
1479 self.proc = proc
1475 self.proc = proc
1480
1476
1481 def addcontext(self, context):
1477 def addcontext(self, context):
1482 if self.hunk:
1478 if self.hunk:
1483 h = recordhunk(self.header, self.fromline, self.toline,
1479 h = recordhunk(self.header, self.fromline, self.toline,
1484 self.proc, self.before, self.hunk, context)
1480 self.proc, self.before, self.hunk, context)
1485 self.header.hunks.append(h)
1481 self.header.hunks.append(h)
1486 self.fromline += len(self.before) + h.removed
1482 self.fromline += len(self.before) + h.removed
1487 self.toline += len(self.before) + h.added
1483 self.toline += len(self.before) + h.added
1488 self.before = []
1484 self.before = []
1489 self.hunk = []
1485 self.hunk = []
1490 self.proc = ''
1486 self.proc = ''
1491 self.context = context
1487 self.context = context
1492
1488
1493 def addhunk(self, hunk):
1489 def addhunk(self, hunk):
1494 if self.context:
1490 if self.context:
1495 self.before = self.context
1491 self.before = self.context
1496 self.context = []
1492 self.context = []
1497 self.hunk = hunk
1493 self.hunk = hunk
1498
1494
1499 def newfile(self, hdr):
1495 def newfile(self, hdr):
1500 self.addcontext([])
1496 self.addcontext([])
1501 h = header(hdr)
1497 h = header(hdr)
1502 self.headers.append(h)
1498 self.headers.append(h)
1503 self.header = h
1499 self.header = h
1504
1500
1505 def addother(self, line):
1501 def addother(self, line):
1506 pass # 'other' lines are ignored
1502 pass # 'other' lines are ignored
1507
1503
1508 def finished(self):
1504 def finished(self):
1509 self.addcontext([])
1505 self.addcontext([])
1510 return self.headers
1506 return self.headers
1511
1507
1512 transitions = {
1508 transitions = {
1513 'file': {'context': addcontext,
1509 'file': {'context': addcontext,
1514 'file': newfile,
1510 'file': newfile,
1515 'hunk': addhunk,
1511 'hunk': addhunk,
1516 'range': addrange},
1512 'range': addrange},
1517 'context': {'file': newfile,
1513 'context': {'file': newfile,
1518 'hunk': addhunk,
1514 'hunk': addhunk,
1519 'range': addrange,
1515 'range': addrange,
1520 'other': addother},
1516 'other': addother},
1521 'hunk': {'context': addcontext,
1517 'hunk': {'context': addcontext,
1522 'file': newfile,
1518 'file': newfile,
1523 'range': addrange},
1519 'range': addrange},
1524 'range': {'context': addcontext,
1520 'range': {'context': addcontext,
1525 'hunk': addhunk},
1521 'hunk': addhunk},
1526 'other': {'other': addother},
1522 'other': {'other': addother},
1527 }
1523 }
1528
1524
1529 p = parser()
1525 p = parser()
1530 fp = cStringIO.StringIO()
1526 fp = cStringIO.StringIO()
1531 fp.write(''.join(originalchunks))
1527 fp.write(''.join(originalchunks))
1532 fp.seek(0)
1528 fp.seek(0)
1533
1529
1534 state = 'context'
1530 state = 'context'
1535 for newstate, data in scanpatch(fp):
1531 for newstate, data in scanpatch(fp):
1536 try:
1532 try:
1537 p.transitions[state][newstate](p, data)
1533 p.transitions[state][newstate](p, data)
1538 except KeyError:
1534 except KeyError:
1539 raise PatchError('unhandled transition: %s -> %s' %
1535 raise PatchError('unhandled transition: %s -> %s' %
1540 (state, newstate))
1536 (state, newstate))
1541 state = newstate
1537 state = newstate
1542 del fp
1538 del fp
1543 return p.finished()
1539 return p.finished()
1544
1540
1545 def pathtransform(path, strip, prefix):
1541 def pathtransform(path, strip, prefix):
1546 '''turn a path from a patch into a path suitable for the repository
1542 '''turn a path from a patch into a path suitable for the repository
1547
1543
1548 prefix, if not empty, is expected to be normalized with a / at the end.
1544 prefix, if not empty, is expected to be normalized with a / at the end.
1549
1545
1550 Returns (stripped components, path in repository).
1546 Returns (stripped components, path in repository).
1551
1547
1552 >>> pathtransform('a/b/c', 0, '')
1548 >>> pathtransform('a/b/c', 0, '')
1553 ('', 'a/b/c')
1549 ('', 'a/b/c')
1554 >>> pathtransform(' a/b/c ', 0, '')
1550 >>> pathtransform(' a/b/c ', 0, '')
1555 ('', ' a/b/c')
1551 ('', ' a/b/c')
1556 >>> pathtransform(' a/b/c ', 2, '')
1552 >>> pathtransform(' a/b/c ', 2, '')
1557 ('a/b/', 'c')
1553 ('a/b/', 'c')
1558 >>> pathtransform('a/b/c', 0, 'd/e/')
1554 >>> pathtransform('a/b/c', 0, 'd/e/')
1559 ('', 'd/e/a/b/c')
1555 ('', 'd/e/a/b/c')
1560 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1556 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1561 ('a//b/', 'd/e/c')
1557 ('a//b/', 'd/e/c')
1562 >>> pathtransform('a/b/c', 3, '')
1558 >>> pathtransform('a/b/c', 3, '')
1563 Traceback (most recent call last):
1559 Traceback (most recent call last):
1564 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1560 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1565 '''
1561 '''
1566 pathlen = len(path)
1562 pathlen = len(path)
1567 i = 0
1563 i = 0
1568 if strip == 0:
1564 if strip == 0:
1569 return '', prefix + path.rstrip()
1565 return '', prefix + path.rstrip()
1570 count = strip
1566 count = strip
1571 while count > 0:
1567 while count > 0:
1572 i = path.find('/', i)
1568 i = path.find('/', i)
1573 if i == -1:
1569 if i == -1:
1574 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1570 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1575 (count, strip, path))
1571 (count, strip, path))
1576 i += 1
1572 i += 1
1577 # consume '//' in the path
1573 # consume '//' in the path
1578 while i < pathlen - 1 and path[i] == '/':
1574 while i < pathlen - 1 and path[i] == '/':
1579 i += 1
1575 i += 1
1580 count -= 1
1576 count -= 1
1581 return path[:i].lstrip(), prefix + path[i:].rstrip()
1577 return path[:i].lstrip(), prefix + path[i:].rstrip()
1582
1578
1583 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1579 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1584 nulla = afile_orig == "/dev/null"
1580 nulla = afile_orig == "/dev/null"
1585 nullb = bfile_orig == "/dev/null"
1581 nullb = bfile_orig == "/dev/null"
1586 create = nulla and hunk.starta == 0 and hunk.lena == 0
1582 create = nulla and hunk.starta == 0 and hunk.lena == 0
1587 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1583 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1588 abase, afile = pathtransform(afile_orig, strip, prefix)
1584 abase, afile = pathtransform(afile_orig, strip, prefix)
1589 gooda = not nulla and backend.exists(afile)
1585 gooda = not nulla and backend.exists(afile)
1590 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1586 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1591 if afile == bfile:
1587 if afile == bfile:
1592 goodb = gooda
1588 goodb = gooda
1593 else:
1589 else:
1594 goodb = not nullb and backend.exists(bfile)
1590 goodb = not nullb and backend.exists(bfile)
1595 missing = not goodb and not gooda and not create
1591 missing = not goodb and not gooda and not create
1596
1592
1597 # some diff programs apparently produce patches where the afile is
1593 # some diff programs apparently produce patches where the afile is
1598 # not /dev/null, but afile starts with bfile
1594 # not /dev/null, but afile starts with bfile
1599 abasedir = afile[:afile.rfind('/') + 1]
1595 abasedir = afile[:afile.rfind('/') + 1]
1600 bbasedir = bfile[:bfile.rfind('/') + 1]
1596 bbasedir = bfile[:bfile.rfind('/') + 1]
1601 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1597 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1602 and hunk.starta == 0 and hunk.lena == 0):
1598 and hunk.starta == 0 and hunk.lena == 0):
1603 create = True
1599 create = True
1604 missing = False
1600 missing = False
1605
1601
1606 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1602 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1607 # diff is between a file and its backup. In this case, the original
1603 # diff is between a file and its backup. In this case, the original
1608 # file should be patched (see original mpatch code).
1604 # file should be patched (see original mpatch code).
1609 isbackup = (abase == bbase and bfile.startswith(afile))
1605 isbackup = (abase == bbase and bfile.startswith(afile))
1610 fname = None
1606 fname = None
1611 if not missing:
1607 if not missing:
1612 if gooda and goodb:
1608 if gooda and goodb:
1613 if isbackup:
1609 if isbackup:
1614 fname = afile
1610 fname = afile
1615 else:
1611 else:
1616 fname = bfile
1612 fname = bfile
1617 elif gooda:
1613 elif gooda:
1618 fname = afile
1614 fname = afile
1619
1615
1620 if not fname:
1616 if not fname:
1621 if not nullb:
1617 if not nullb:
1622 if isbackup:
1618 if isbackup:
1623 fname = afile
1619 fname = afile
1624 else:
1620 else:
1625 fname = bfile
1621 fname = bfile
1626 elif not nulla:
1622 elif not nulla:
1627 fname = afile
1623 fname = afile
1628 else:
1624 else:
1629 raise PatchError(_("undefined source and destination files"))
1625 raise PatchError(_("undefined source and destination files"))
1630
1626
1631 gp = patchmeta(fname)
1627 gp = patchmeta(fname)
1632 if create:
1628 if create:
1633 gp.op = 'ADD'
1629 gp.op = 'ADD'
1634 elif remove:
1630 elif remove:
1635 gp.op = 'DELETE'
1631 gp.op = 'DELETE'
1636 return gp
1632 return gp
1637
1633
1638 def scanpatch(fp):
1634 def scanpatch(fp):
1639 """like patch.iterhunks, but yield different events
1635 """like patch.iterhunks, but yield different events
1640
1636
1641 - ('file', [header_lines + fromfile + tofile])
1637 - ('file', [header_lines + fromfile + tofile])
1642 - ('context', [context_lines])
1638 - ('context', [context_lines])
1643 - ('hunk', [hunk_lines])
1639 - ('hunk', [hunk_lines])
1644 - ('range', (-start,len, +start,len, proc))
1640 - ('range', (-start,len, +start,len, proc))
1645 """
1641 """
1646 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1642 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1647 lr = linereader(fp)
1643 lr = linereader(fp)
1648
1644
1649 def scanwhile(first, p):
1645 def scanwhile(first, p):
1650 """scan lr while predicate holds"""
1646 """scan lr while predicate holds"""
1651 lines = [first]
1647 lines = [first]
1652 while True:
1648 while True:
1653 line = lr.readline()
1649 line = lr.readline()
1654 if not line:
1650 if not line:
1655 break
1651 break
1656 if p(line):
1652 if p(line):
1657 lines.append(line)
1653 lines.append(line)
1658 else:
1654 else:
1659 lr.push(line)
1655 lr.push(line)
1660 break
1656 break
1661 return lines
1657 return lines
1662
1658
1663 while True:
1659 while True:
1664 line = lr.readline()
1660 line = lr.readline()
1665 if not line:
1661 if not line:
1666 break
1662 break
1667 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1663 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1668 def notheader(line):
1664 def notheader(line):
1669 s = line.split(None, 1)
1665 s = line.split(None, 1)
1670 return not s or s[0] not in ('---', 'diff')
1666 return not s or s[0] not in ('---', 'diff')
1671 header = scanwhile(line, notheader)
1667 header = scanwhile(line, notheader)
1672 fromfile = lr.readline()
1668 fromfile = lr.readline()
1673 if fromfile.startswith('---'):
1669 if fromfile.startswith('---'):
1674 tofile = lr.readline()
1670 tofile = lr.readline()
1675 header += [fromfile, tofile]
1671 header += [fromfile, tofile]
1676 else:
1672 else:
1677 lr.push(fromfile)
1673 lr.push(fromfile)
1678 yield 'file', header
1674 yield 'file', header
1679 elif line[0] == ' ':
1675 elif line[0] == ' ':
1680 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1676 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1681 elif line[0] in '-+':
1677 elif line[0] in '-+':
1682 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1678 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1683 else:
1679 else:
1684 m = lines_re.match(line)
1680 m = lines_re.match(line)
1685 if m:
1681 if m:
1686 yield 'range', m.groups()
1682 yield 'range', m.groups()
1687 else:
1683 else:
1688 yield 'other', line
1684 yield 'other', line
1689
1685
1690 def scangitpatch(lr, firstline):
1686 def scangitpatch(lr, firstline):
1691 """
1687 """
1692 Git patches can emit:
1688 Git patches can emit:
1693 - rename a to b
1689 - rename a to b
1694 - change b
1690 - change b
1695 - copy a to c
1691 - copy a to c
1696 - change c
1692 - change c
1697
1693
1698 We cannot apply this sequence as-is, the renamed 'a' could not be
1694 We cannot apply this sequence as-is, the renamed 'a' could not be
1699 found for it would have been renamed already. And we cannot copy
1695 found for it would have been renamed already. And we cannot copy
1700 from 'b' instead because 'b' would have been changed already. So
1696 from 'b' instead because 'b' would have been changed already. So
1701 we scan the git patch for copy and rename commands so we can
1697 we scan the git patch for copy and rename commands so we can
1702 perform the copies ahead of time.
1698 perform the copies ahead of time.
1703 """
1699 """
1704 pos = 0
1700 pos = 0
1705 try:
1701 try:
1706 pos = lr.fp.tell()
1702 pos = lr.fp.tell()
1707 fp = lr.fp
1703 fp = lr.fp
1708 except IOError:
1704 except IOError:
1709 fp = cStringIO.StringIO(lr.fp.read())
1705 fp = cStringIO.StringIO(lr.fp.read())
1710 gitlr = linereader(fp)
1706 gitlr = linereader(fp)
1711 gitlr.push(firstline)
1707 gitlr.push(firstline)
1712 gitpatches = readgitpatch(gitlr)
1708 gitpatches = readgitpatch(gitlr)
1713 fp.seek(pos)
1709 fp.seek(pos)
1714 return gitpatches
1710 return gitpatches
1715
1711
1716 def iterhunks(fp):
1712 def iterhunks(fp):
1717 """Read a patch and yield the following events:
1713 """Read a patch and yield the following events:
1718 - ("file", afile, bfile, firsthunk): select a new target file.
1714 - ("file", afile, bfile, firsthunk): select a new target file.
1719 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1715 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1720 "file" event.
1716 "file" event.
1721 - ("git", gitchanges): current diff is in git format, gitchanges
1717 - ("git", gitchanges): current diff is in git format, gitchanges
1722 maps filenames to gitpatch records. Unique event.
1718 maps filenames to gitpatch records. Unique event.
1723 """
1719 """
1724 afile = ""
1720 afile = ""
1725 bfile = ""
1721 bfile = ""
1726 state = None
1722 state = None
1727 hunknum = 0
1723 hunknum = 0
1728 emitfile = newfile = False
1724 emitfile = newfile = False
1729 gitpatches = None
1725 gitpatches = None
1730
1726
1731 # our states
1727 # our states
1732 BFILE = 1
1728 BFILE = 1
1733 context = None
1729 context = None
1734 lr = linereader(fp)
1730 lr = linereader(fp)
1735
1731
1736 while True:
1732 while True:
1737 x = lr.readline()
1733 x = lr.readline()
1738 if not x:
1734 if not x:
1739 break
1735 break
1740 if state == BFILE and (
1736 if state == BFILE and (
1741 (not context and x[0] == '@')
1737 (not context and x[0] == '@')
1742 or (context is not False and x.startswith('***************'))
1738 or (context is not False and x.startswith('***************'))
1743 or x.startswith('GIT binary patch')):
1739 or x.startswith('GIT binary patch')):
1744 gp = None
1740 gp = None
1745 if (gitpatches and
1741 if (gitpatches and
1746 gitpatches[-1].ispatching(afile, bfile)):
1742 gitpatches[-1].ispatching(afile, bfile)):
1747 gp = gitpatches.pop()
1743 gp = gitpatches.pop()
1748 if x.startswith('GIT binary patch'):
1744 if x.startswith('GIT binary patch'):
1749 h = binhunk(lr, gp.path)
1745 h = binhunk(lr, gp.path)
1750 else:
1746 else:
1751 if context is None and x.startswith('***************'):
1747 if context is None and x.startswith('***************'):
1752 context = True
1748 context = True
1753 h = hunk(x, hunknum + 1, lr, context)
1749 h = hunk(x, hunknum + 1, lr, context)
1754 hunknum += 1
1750 hunknum += 1
1755 if emitfile:
1751 if emitfile:
1756 emitfile = False
1752 emitfile = False
1757 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1753 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1758 yield 'hunk', h
1754 yield 'hunk', h
1759 elif x.startswith('diff --git a/'):
1755 elif x.startswith('diff --git a/'):
1760 m = gitre.match(x.rstrip(' \r\n'))
1756 m = gitre.match(x.rstrip(' \r\n'))
1761 if not m:
1757 if not m:
1762 continue
1758 continue
1763 if gitpatches is None:
1759 if gitpatches is None:
1764 # scan whole input for git metadata
1760 # scan whole input for git metadata
1765 gitpatches = scangitpatch(lr, x)
1761 gitpatches = scangitpatch(lr, x)
1766 yield 'git', [g.copy() for g in gitpatches
1762 yield 'git', [g.copy() for g in gitpatches
1767 if g.op in ('COPY', 'RENAME')]
1763 if g.op in ('COPY', 'RENAME')]
1768 gitpatches.reverse()
1764 gitpatches.reverse()
1769 afile = 'a/' + m.group(1)
1765 afile = 'a/' + m.group(1)
1770 bfile = 'b/' + m.group(2)
1766 bfile = 'b/' + m.group(2)
1771 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1767 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1772 gp = gitpatches.pop()
1768 gp = gitpatches.pop()
1773 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1769 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1774 if not gitpatches:
1770 if not gitpatches:
1775 raise PatchError(_('failed to synchronize metadata for "%s"')
1771 raise PatchError(_('failed to synchronize metadata for "%s"')
1776 % afile[2:])
1772 % afile[2:])
1777 gp = gitpatches[-1]
1773 gp = gitpatches[-1]
1778 newfile = True
1774 newfile = True
1779 elif x.startswith('---'):
1775 elif x.startswith('---'):
1780 # check for a unified diff
1776 # check for a unified diff
1781 l2 = lr.readline()
1777 l2 = lr.readline()
1782 if not l2.startswith('+++'):
1778 if not l2.startswith('+++'):
1783 lr.push(l2)
1779 lr.push(l2)
1784 continue
1780 continue
1785 newfile = True
1781 newfile = True
1786 context = False
1782 context = False
1787 afile = parsefilename(x)
1783 afile = parsefilename(x)
1788 bfile = parsefilename(l2)
1784 bfile = parsefilename(l2)
1789 elif x.startswith('***'):
1785 elif x.startswith('***'):
1790 # check for a context diff
1786 # check for a context diff
1791 l2 = lr.readline()
1787 l2 = lr.readline()
1792 if not l2.startswith('---'):
1788 if not l2.startswith('---'):
1793 lr.push(l2)
1789 lr.push(l2)
1794 continue
1790 continue
1795 l3 = lr.readline()
1791 l3 = lr.readline()
1796 lr.push(l3)
1792 lr.push(l3)
1797 if not l3.startswith("***************"):
1793 if not l3.startswith("***************"):
1798 lr.push(l2)
1794 lr.push(l2)
1799 continue
1795 continue
1800 newfile = True
1796 newfile = True
1801 context = True
1797 context = True
1802 afile = parsefilename(x)
1798 afile = parsefilename(x)
1803 bfile = parsefilename(l2)
1799 bfile = parsefilename(l2)
1804
1800
1805 if newfile:
1801 if newfile:
1806 newfile = False
1802 newfile = False
1807 emitfile = True
1803 emitfile = True
1808 state = BFILE
1804 state = BFILE
1809 hunknum = 0
1805 hunknum = 0
1810
1806
1811 while gitpatches:
1807 while gitpatches:
1812 gp = gitpatches.pop()
1808 gp = gitpatches.pop()
1813 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1809 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1814
1810
1815 def applybindelta(binchunk, data):
1811 def applybindelta(binchunk, data):
1816 """Apply a binary delta hunk
1812 """Apply a binary delta hunk
1817 The algorithm used is the algorithm from git's patch-delta.c
1813 The algorithm used is the algorithm from git's patch-delta.c
1818 """
1814 """
1819 def deltahead(binchunk):
1815 def deltahead(binchunk):
1820 i = 0
1816 i = 0
1821 for c in binchunk:
1817 for c in binchunk:
1822 i += 1
1818 i += 1
1823 if not (ord(c) & 0x80):
1819 if not (ord(c) & 0x80):
1824 return i
1820 return i
1825 return i
1821 return i
1826 out = ""
1822 out = ""
1827 s = deltahead(binchunk)
1823 s = deltahead(binchunk)
1828 binchunk = binchunk[s:]
1824 binchunk = binchunk[s:]
1829 s = deltahead(binchunk)
1825 s = deltahead(binchunk)
1830 binchunk = binchunk[s:]
1826 binchunk = binchunk[s:]
1831 i = 0
1827 i = 0
1832 while i < len(binchunk):
1828 while i < len(binchunk):
1833 cmd = ord(binchunk[i])
1829 cmd = ord(binchunk[i])
1834 i += 1
1830 i += 1
1835 if (cmd & 0x80):
1831 if (cmd & 0x80):
1836 offset = 0
1832 offset = 0
1837 size = 0
1833 size = 0
1838 if (cmd & 0x01):
1834 if (cmd & 0x01):
1839 offset = ord(binchunk[i])
1835 offset = ord(binchunk[i])
1840 i += 1
1836 i += 1
1841 if (cmd & 0x02):
1837 if (cmd & 0x02):
1842 offset |= ord(binchunk[i]) << 8
1838 offset |= ord(binchunk[i]) << 8
1843 i += 1
1839 i += 1
1844 if (cmd & 0x04):
1840 if (cmd & 0x04):
1845 offset |= ord(binchunk[i]) << 16
1841 offset |= ord(binchunk[i]) << 16
1846 i += 1
1842 i += 1
1847 if (cmd & 0x08):
1843 if (cmd & 0x08):
1848 offset |= ord(binchunk[i]) << 24
1844 offset |= ord(binchunk[i]) << 24
1849 i += 1
1845 i += 1
1850 if (cmd & 0x10):
1846 if (cmd & 0x10):
1851 size = ord(binchunk[i])
1847 size = ord(binchunk[i])
1852 i += 1
1848 i += 1
1853 if (cmd & 0x20):
1849 if (cmd & 0x20):
1854 size |= ord(binchunk[i]) << 8
1850 size |= ord(binchunk[i]) << 8
1855 i += 1
1851 i += 1
1856 if (cmd & 0x40):
1852 if (cmd & 0x40):
1857 size |= ord(binchunk[i]) << 16
1853 size |= ord(binchunk[i]) << 16
1858 i += 1
1854 i += 1
1859 if size == 0:
1855 if size == 0:
1860 size = 0x10000
1856 size = 0x10000
1861 offset_end = offset + size
1857 offset_end = offset + size
1862 out += data[offset:offset_end]
1858 out += data[offset:offset_end]
1863 elif cmd != 0:
1859 elif cmd != 0:
1864 offset_end = i + cmd
1860 offset_end = i + cmd
1865 out += binchunk[i:offset_end]
1861 out += binchunk[i:offset_end]
1866 i += cmd
1862 i += cmd
1867 else:
1863 else:
1868 raise PatchError(_('unexpected delta opcode 0'))
1864 raise PatchError(_('unexpected delta opcode 0'))
1869 return out
1865 return out
1870
1866
1871 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1867 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1872 """Reads a patch from fp and tries to apply it.
1868 """Reads a patch from fp and tries to apply it.
1873
1869
1874 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1870 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1875 there was any fuzz.
1871 there was any fuzz.
1876
1872
1877 If 'eolmode' is 'strict', the patch content and patched file are
1873 If 'eolmode' is 'strict', the patch content and patched file are
1878 read in binary mode. Otherwise, line endings are ignored when
1874 read in binary mode. Otherwise, line endings are ignored when
1879 patching then normalized according to 'eolmode'.
1875 patching then normalized according to 'eolmode'.
1880 """
1876 """
1881 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1877 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1882 prefix=prefix, eolmode=eolmode)
1878 prefix=prefix, eolmode=eolmode)
1883
1879
1884 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1880 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1885 eolmode='strict'):
1881 eolmode='strict'):
1886
1882
1887 if prefix:
1883 if prefix:
1888 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1884 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1889 prefix)
1885 prefix)
1890 if prefix != '':
1886 if prefix != '':
1891 prefix += '/'
1887 prefix += '/'
1892 def pstrip(p):
1888 def pstrip(p):
1893 return pathtransform(p, strip - 1, prefix)[1]
1889 return pathtransform(p, strip - 1, prefix)[1]
1894
1890
1895 rejects = 0
1891 rejects = 0
1896 err = 0
1892 err = 0
1897 current_file = None
1893 current_file = None
1898
1894
1899 for state, values in iterhunks(fp):
1895 for state, values in iterhunks(fp):
1900 if state == 'hunk':
1896 if state == 'hunk':
1901 if not current_file:
1897 if not current_file:
1902 continue
1898 continue
1903 ret = current_file.apply(values)
1899 ret = current_file.apply(values)
1904 if ret > 0:
1900 if ret > 0:
1905 err = 1
1901 err = 1
1906 elif state == 'file':
1902 elif state == 'file':
1907 if current_file:
1903 if current_file:
1908 rejects += current_file.close()
1904 rejects += current_file.close()
1909 current_file = None
1905 current_file = None
1910 afile, bfile, first_hunk, gp = values
1906 afile, bfile, first_hunk, gp = values
1911 if gp:
1907 if gp:
1912 gp.path = pstrip(gp.path)
1908 gp.path = pstrip(gp.path)
1913 if gp.oldpath:
1909 if gp.oldpath:
1914 gp.oldpath = pstrip(gp.oldpath)
1910 gp.oldpath = pstrip(gp.oldpath)
1915 else:
1911 else:
1916 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1912 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1917 prefix)
1913 prefix)
1918 if gp.op == 'RENAME':
1914 if gp.op == 'RENAME':
1919 backend.unlink(gp.oldpath)
1915 backend.unlink(gp.oldpath)
1920 if not first_hunk:
1916 if not first_hunk:
1921 if gp.op == 'DELETE':
1917 if gp.op == 'DELETE':
1922 backend.unlink(gp.path)
1918 backend.unlink(gp.path)
1923 continue
1919 continue
1924 data, mode = None, None
1920 data, mode = None, None
1925 if gp.op in ('RENAME', 'COPY'):
1921 if gp.op in ('RENAME', 'COPY'):
1926 data, mode = store.getfile(gp.oldpath)[:2]
1922 data, mode = store.getfile(gp.oldpath)[:2]
1927 # FIXME: failing getfile has never been handled here
1923 # FIXME: failing getfile has never been handled here
1928 assert data is not None
1924 assert data is not None
1929 if gp.mode:
1925 if gp.mode:
1930 mode = gp.mode
1926 mode = gp.mode
1931 if gp.op == 'ADD':
1927 if gp.op == 'ADD':
1932 # Added files without content have no hunk and
1928 # Added files without content have no hunk and
1933 # must be created
1929 # must be created
1934 data = ''
1930 data = ''
1935 if data or mode:
1931 if data or mode:
1936 if (gp.op in ('ADD', 'RENAME', 'COPY')
1932 if (gp.op in ('ADD', 'RENAME', 'COPY')
1937 and backend.exists(gp.path)):
1933 and backend.exists(gp.path)):
1938 raise PatchError(_("cannot create %s: destination "
1934 raise PatchError(_("cannot create %s: destination "
1939 "already exists") % gp.path)
1935 "already exists") % gp.path)
1940 backend.setfile(gp.path, data, mode, gp.oldpath)
1936 backend.setfile(gp.path, data, mode, gp.oldpath)
1941 continue
1937 continue
1942 try:
1938 try:
1943 current_file = patcher(ui, gp, backend, store,
1939 current_file = patcher(ui, gp, backend, store,
1944 eolmode=eolmode)
1940 eolmode=eolmode)
1945 except PatchError, inst:
1941 except PatchError, inst:
1946 ui.warn(str(inst) + '\n')
1942 ui.warn(str(inst) + '\n')
1947 current_file = None
1943 current_file = None
1948 rejects += 1
1944 rejects += 1
1949 continue
1945 continue
1950 elif state == 'git':
1946 elif state == 'git':
1951 for gp in values:
1947 for gp in values:
1952 path = pstrip(gp.oldpath)
1948 path = pstrip(gp.oldpath)
1953 data, mode = backend.getfile(path)
1949 data, mode = backend.getfile(path)
1954 if data is None:
1950 if data is None:
1955 # The error ignored here will trigger a getfile()
1951 # The error ignored here will trigger a getfile()
1956 # error in a place more appropriate for error
1952 # error in a place more appropriate for error
1957 # handling, and will not interrupt the patching
1953 # handling, and will not interrupt the patching
1958 # process.
1954 # process.
1959 pass
1955 pass
1960 else:
1956 else:
1961 store.setfile(path, data, mode)
1957 store.setfile(path, data, mode)
1962 else:
1958 else:
1963 raise util.Abort(_('unsupported parser state: %s') % state)
1959 raise util.Abort(_('unsupported parser state: %s') % state)
1964
1960
1965 if current_file:
1961 if current_file:
1966 rejects += current_file.close()
1962 rejects += current_file.close()
1967
1963
1968 if rejects:
1964 if rejects:
1969 return -1
1965 return -1
1970 return err
1966 return err
1971
1967
1972 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1968 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1973 similarity):
1969 similarity):
1974 """use <patcher> to apply <patchname> to the working directory.
1970 """use <patcher> to apply <patchname> to the working directory.
1975 returns whether patch was applied with fuzz factor."""
1971 returns whether patch was applied with fuzz factor."""
1976
1972
1977 fuzz = False
1973 fuzz = False
1978 args = []
1974 args = []
1979 cwd = repo.root
1975 cwd = repo.root
1980 if cwd:
1976 if cwd:
1981 args.append('-d %s' % util.shellquote(cwd))
1977 args.append('-d %s' % util.shellquote(cwd))
1982 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1978 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1983 util.shellquote(patchname)))
1979 util.shellquote(patchname)))
1984 try:
1980 try:
1985 for line in fp:
1981 for line in fp:
1986 line = line.rstrip()
1982 line = line.rstrip()
1987 ui.note(line + '\n')
1983 ui.note(line + '\n')
1988 if line.startswith('patching file '):
1984 if line.startswith('patching file '):
1989 pf = util.parsepatchoutput(line)
1985 pf = util.parsepatchoutput(line)
1990 printed_file = False
1986 printed_file = False
1991 files.add(pf)
1987 files.add(pf)
1992 elif line.find('with fuzz') >= 0:
1988 elif line.find('with fuzz') >= 0:
1993 fuzz = True
1989 fuzz = True
1994 if not printed_file:
1990 if not printed_file:
1995 ui.warn(pf + '\n')
1991 ui.warn(pf + '\n')
1996 printed_file = True
1992 printed_file = True
1997 ui.warn(line + '\n')
1993 ui.warn(line + '\n')
1998 elif line.find('saving rejects to file') >= 0:
1994 elif line.find('saving rejects to file') >= 0:
1999 ui.warn(line + '\n')
1995 ui.warn(line + '\n')
2000 elif line.find('FAILED') >= 0:
1996 elif line.find('FAILED') >= 0:
2001 if not printed_file:
1997 if not printed_file:
2002 ui.warn(pf + '\n')
1998 ui.warn(pf + '\n')
2003 printed_file = True
1999 printed_file = True
2004 ui.warn(line + '\n')
2000 ui.warn(line + '\n')
2005 finally:
2001 finally:
2006 if files:
2002 if files:
2007 scmutil.marktouched(repo, files, similarity)
2003 scmutil.marktouched(repo, files, similarity)
2008 code = fp.close()
2004 code = fp.close()
2009 if code:
2005 if code:
2010 raise PatchError(_("patch command failed: %s") %
2006 raise PatchError(_("patch command failed: %s") %
2011 util.explainexit(code)[0])
2007 util.explainexit(code)[0])
2012 return fuzz
2008 return fuzz
2013
2009
2014 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2010 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2015 eolmode='strict'):
2011 eolmode='strict'):
2016 if files is None:
2012 if files is None:
2017 files = set()
2013 files = set()
2018 if eolmode is None:
2014 if eolmode is None:
2019 eolmode = ui.config('patch', 'eol', 'strict')
2015 eolmode = ui.config('patch', 'eol', 'strict')
2020 if eolmode.lower() not in eolmodes:
2016 if eolmode.lower() not in eolmodes:
2021 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
2017 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
2022 eolmode = eolmode.lower()
2018 eolmode = eolmode.lower()
2023
2019
2024 store = filestore()
2020 store = filestore()
2025 try:
2021 try:
2026 fp = open(patchobj, 'rb')
2022 fp = open(patchobj, 'rb')
2027 except TypeError:
2023 except TypeError:
2028 fp = patchobj
2024 fp = patchobj
2029 try:
2025 try:
2030 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2026 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2031 eolmode=eolmode)
2027 eolmode=eolmode)
2032 finally:
2028 finally:
2033 if fp != patchobj:
2029 if fp != patchobj:
2034 fp.close()
2030 fp.close()
2035 files.update(backend.close())
2031 files.update(backend.close())
2036 store.close()
2032 store.close()
2037 if ret < 0:
2033 if ret < 0:
2038 raise PatchError(_('patch failed to apply'))
2034 raise PatchError(_('patch failed to apply'))
2039 return ret > 0
2035 return ret > 0
2040
2036
2041 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2037 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2042 eolmode='strict', similarity=0):
2038 eolmode='strict', similarity=0):
2043 """use builtin patch to apply <patchobj> to the working directory.
2039 """use builtin patch to apply <patchobj> to the working directory.
2044 returns whether patch was applied with fuzz factor."""
2040 returns whether patch was applied with fuzz factor."""
2045 backend = workingbackend(ui, repo, similarity)
2041 backend = workingbackend(ui, repo, similarity)
2046 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2042 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2047
2043
2048 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2044 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2049 eolmode='strict'):
2045 eolmode='strict'):
2050 backend = repobackend(ui, repo, ctx, store)
2046 backend = repobackend(ui, repo, ctx, store)
2051 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2047 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2052
2048
2053 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2049 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2054 similarity=0):
2050 similarity=0):
2055 """Apply <patchname> to the working directory.
2051 """Apply <patchname> to the working directory.
2056
2052
2057 'eolmode' specifies how end of lines should be handled. It can be:
2053 'eolmode' specifies how end of lines should be handled. It can be:
2058 - 'strict': inputs are read in binary mode, EOLs are preserved
2054 - 'strict': inputs are read in binary mode, EOLs are preserved
2059 - 'crlf': EOLs are ignored when patching and reset to CRLF
2055 - 'crlf': EOLs are ignored when patching and reset to CRLF
2060 - 'lf': EOLs are ignored when patching and reset to LF
2056 - 'lf': EOLs are ignored when patching and reset to LF
2061 - None: get it from user settings, default to 'strict'
2057 - None: get it from user settings, default to 'strict'
2062 'eolmode' is ignored when using an external patcher program.
2058 'eolmode' is ignored when using an external patcher program.
2063
2059
2064 Returns whether patch was applied with fuzz factor.
2060 Returns whether patch was applied with fuzz factor.
2065 """
2061 """
2066 patcher = ui.config('ui', 'patch')
2062 patcher = ui.config('ui', 'patch')
2067 if files is None:
2063 if files is None:
2068 files = set()
2064 files = set()
2069 if patcher:
2065 if patcher:
2070 return _externalpatch(ui, repo, patcher, patchname, strip,
2066 return _externalpatch(ui, repo, patcher, patchname, strip,
2071 files, similarity)
2067 files, similarity)
2072 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2068 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2073 similarity)
2069 similarity)
2074
2070
2075 def changedfiles(ui, repo, patchpath, strip=1):
2071 def changedfiles(ui, repo, patchpath, strip=1):
2076 backend = fsbackend(ui, repo.root)
2072 backend = fsbackend(ui, repo.root)
2077 fp = open(patchpath, 'rb')
2073 fp = open(patchpath, 'rb')
2078 try:
2074 try:
2079 changed = set()
2075 changed = set()
2080 for state, values in iterhunks(fp):
2076 for state, values in iterhunks(fp):
2081 if state == 'file':
2077 if state == 'file':
2082 afile, bfile, first_hunk, gp = values
2078 afile, bfile, first_hunk, gp = values
2083 if gp:
2079 if gp:
2084 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2080 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2085 if gp.oldpath:
2081 if gp.oldpath:
2086 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2082 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2087 else:
2083 else:
2088 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2084 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2089 '')
2085 '')
2090 changed.add(gp.path)
2086 changed.add(gp.path)
2091 if gp.op == 'RENAME':
2087 if gp.op == 'RENAME':
2092 changed.add(gp.oldpath)
2088 changed.add(gp.oldpath)
2093 elif state not in ('hunk', 'git'):
2089 elif state not in ('hunk', 'git'):
2094 raise util.Abort(_('unsupported parser state: %s') % state)
2090 raise util.Abort(_('unsupported parser state: %s') % state)
2095 return changed
2091 return changed
2096 finally:
2092 finally:
2097 fp.close()
2093 fp.close()
2098
2094
2099 class GitDiffRequired(Exception):
2095 class GitDiffRequired(Exception):
2100 pass
2096 pass
2101
2097
2102 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2098 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2103 '''return diffopts with all features supported and parsed'''
2099 '''return diffopts with all features supported and parsed'''
2104 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2100 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2105 git=True, whitespace=True, formatchanging=True)
2101 git=True, whitespace=True, formatchanging=True)
2106
2102
2107 diffopts = diffallopts
2103 diffopts = diffallopts
2108
2104
2109 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2105 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2110 whitespace=False, formatchanging=False):
2106 whitespace=False, formatchanging=False):
2111 '''return diffopts with only opted-in features parsed
2107 '''return diffopts with only opted-in features parsed
2112
2108
2113 Features:
2109 Features:
2114 - git: git-style diffs
2110 - git: git-style diffs
2115 - whitespace: whitespace options like ignoreblanklines and ignorews
2111 - whitespace: whitespace options like ignoreblanklines and ignorews
2116 - formatchanging: options that will likely break or cause correctness issues
2112 - formatchanging: options that will likely break or cause correctness issues
2117 with most diff parsers
2113 with most diff parsers
2118 '''
2114 '''
2119 def get(key, name=None, getter=ui.configbool, forceplain=None):
2115 def get(key, name=None, getter=ui.configbool, forceplain=None):
2120 if opts:
2116 if opts:
2121 v = opts.get(key)
2117 v = opts.get(key)
2122 if v:
2118 if v:
2123 return v
2119 return v
2124 if forceplain is not None and ui.plain():
2120 if forceplain is not None and ui.plain():
2125 return forceplain
2121 return forceplain
2126 return getter(section, name or key, None, untrusted=untrusted)
2122 return getter(section, name or key, None, untrusted=untrusted)
2127
2123
2128 # core options, expected to be understood by every diff parser
2124 # core options, expected to be understood by every diff parser
2129 buildopts = {
2125 buildopts = {
2130 'nodates': get('nodates'),
2126 'nodates': get('nodates'),
2131 'showfunc': get('show_function', 'showfunc'),
2127 'showfunc': get('show_function', 'showfunc'),
2132 'context': get('unified', getter=ui.config),
2128 'context': get('unified', getter=ui.config),
2133 }
2129 }
2134
2130
2135 if git:
2131 if git:
2136 buildopts['git'] = get('git')
2132 buildopts['git'] = get('git')
2137 if whitespace:
2133 if whitespace:
2138 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2134 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2139 buildopts['ignorewsamount'] = get('ignore_space_change',
2135 buildopts['ignorewsamount'] = get('ignore_space_change',
2140 'ignorewsamount')
2136 'ignorewsamount')
2141 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2137 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2142 'ignoreblanklines')
2138 'ignoreblanklines')
2143 if formatchanging:
2139 if formatchanging:
2144 buildopts['text'] = opts and opts.get('text')
2140 buildopts['text'] = opts and opts.get('text')
2145 buildopts['nobinary'] = get('nobinary')
2141 buildopts['nobinary'] = get('nobinary')
2146 buildopts['noprefix'] = get('noprefix', forceplain=False)
2142 buildopts['noprefix'] = get('noprefix', forceplain=False)
2147
2143
2148 return mdiff.diffopts(**buildopts)
2144 return mdiff.diffopts(**buildopts)
2149
2145
2150 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2146 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2151 losedatafn=None, prefix='', relroot=''):
2147 losedatafn=None, prefix='', relroot=''):
2152 '''yields diff of changes to files between two nodes, or node and
2148 '''yields diff of changes to files between two nodes, or node and
2153 working directory.
2149 working directory.
2154
2150
2155 if node1 is None, use first dirstate parent instead.
2151 if node1 is None, use first dirstate parent instead.
2156 if node2 is None, compare node1 with working directory.
2152 if node2 is None, compare node1 with working directory.
2157
2153
2158 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2154 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2159 every time some change cannot be represented with the current
2155 every time some change cannot be represented with the current
2160 patch format. Return False to upgrade to git patch format, True to
2156 patch format. Return False to upgrade to git patch format, True to
2161 accept the loss or raise an exception to abort the diff. It is
2157 accept the loss or raise an exception to abort the diff. It is
2162 called with the name of current file being diffed as 'fn'. If set
2158 called with the name of current file being diffed as 'fn'. If set
2163 to None, patches will always be upgraded to git format when
2159 to None, patches will always be upgraded to git format when
2164 necessary.
2160 necessary.
2165
2161
2166 prefix is a filename prefix that is prepended to all filenames on
2162 prefix is a filename prefix that is prepended to all filenames on
2167 display (used for subrepos).
2163 display (used for subrepos).
2168
2164
2169 relroot, if not empty, must be normalized with a trailing /. Any match
2165 relroot, if not empty, must be normalized with a trailing /. Any match
2170 patterns that fall outside it will be ignored.'''
2166 patterns that fall outside it will be ignored.'''
2171
2167
2172 if opts is None:
2168 if opts is None:
2173 opts = mdiff.defaultopts
2169 opts = mdiff.defaultopts
2174
2170
2175 if not node1 and not node2:
2171 if not node1 and not node2:
2176 node1 = repo.dirstate.p1()
2172 node1 = repo.dirstate.p1()
2177
2173
2178 def lrugetfilectx():
2174 def lrugetfilectx():
2179 cache = {}
2175 cache = {}
2180 order = collections.deque()
2176 order = collections.deque()
2181 def getfilectx(f, ctx):
2177 def getfilectx(f, ctx):
2182 fctx = ctx.filectx(f, filelog=cache.get(f))
2178 fctx = ctx.filectx(f, filelog=cache.get(f))
2183 if f not in cache:
2179 if f not in cache:
2184 if len(cache) > 20:
2180 if len(cache) > 20:
2185 del cache[order.popleft()]
2181 del cache[order.popleft()]
2186 cache[f] = fctx.filelog()
2182 cache[f] = fctx.filelog()
2187 else:
2183 else:
2188 order.remove(f)
2184 order.remove(f)
2189 order.append(f)
2185 order.append(f)
2190 return fctx
2186 return fctx
2191 return getfilectx
2187 return getfilectx
2192 getfilectx = lrugetfilectx()
2188 getfilectx = lrugetfilectx()
2193
2189
2194 ctx1 = repo[node1]
2190 ctx1 = repo[node1]
2195 ctx2 = repo[node2]
2191 ctx2 = repo[node2]
2196
2192
2197 relfiltered = False
2193 relfiltered = False
2198 if relroot != '' and match.always():
2194 if relroot != '' and match.always():
2199 # as a special case, create a new matcher with just the relroot
2195 # as a special case, create a new matcher with just the relroot
2200 pats = [relroot]
2196 pats = [relroot]
2201 match = scmutil.match(ctx2, pats, default='path')
2197 match = scmutil.match(ctx2, pats, default='path')
2202 relfiltered = True
2198 relfiltered = True
2203
2199
2204 if not changes:
2200 if not changes:
2205 changes = repo.status(ctx1, ctx2, match=match)
2201 changes = repo.status(ctx1, ctx2, match=match)
2206 modified, added, removed = changes[:3]
2202 modified, added, removed = changes[:3]
2207
2203
2208 if not modified and not added and not removed:
2204 if not modified and not added and not removed:
2209 return []
2205 return []
2210
2206
2211 if repo.ui.debugflag:
2207 if repo.ui.debugflag:
2212 hexfunc = hex
2208 hexfunc = hex
2213 else:
2209 else:
2214 hexfunc = short
2210 hexfunc = short
2215 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2211 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2216
2212
2217 copy = {}
2213 copy = {}
2218 if opts.git or opts.upgrade:
2214 if opts.git or opts.upgrade:
2219 copy = copies.pathcopies(ctx1, ctx2, match=match)
2215 copy = copies.pathcopies(ctx1, ctx2, match=match)
2220
2216
2221 if relroot is not None:
2217 if relroot is not None:
2222 if not relfiltered:
2218 if not relfiltered:
2223 # XXX this would ideally be done in the matcher, but that is
2219 # XXX this would ideally be done in the matcher, but that is
2224 # generally meant to 'or' patterns, not 'and' them. In this case we
2220 # generally meant to 'or' patterns, not 'and' them. In this case we
2225 # need to 'and' all the patterns from the matcher with relroot.
2221 # need to 'and' all the patterns from the matcher with relroot.
2226 def filterrel(l):
2222 def filterrel(l):
2227 return [f for f in l if f.startswith(relroot)]
2223 return [f for f in l if f.startswith(relroot)]
2228 modified = filterrel(modified)
2224 modified = filterrel(modified)
2229 added = filterrel(added)
2225 added = filterrel(added)
2230 removed = filterrel(removed)
2226 removed = filterrel(removed)
2231 relfiltered = True
2227 relfiltered = True
2232 # filter out copies where either side isn't inside the relative root
2228 # filter out copies where either side isn't inside the relative root
2233 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2229 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2234 if dst.startswith(relroot)
2230 if dst.startswith(relroot)
2235 and src.startswith(relroot)))
2231 and src.startswith(relroot)))
2236
2232
2237 def difffn(opts, losedata):
2233 def difffn(opts, losedata):
2238 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2234 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2239 copy, getfilectx, opts, losedata, prefix, relroot)
2235 copy, getfilectx, opts, losedata, prefix, relroot)
2240 if opts.upgrade and not opts.git:
2236 if opts.upgrade and not opts.git:
2241 try:
2237 try:
2242 def losedata(fn):
2238 def losedata(fn):
2243 if not losedatafn or not losedatafn(fn=fn):
2239 if not losedatafn or not losedatafn(fn=fn):
2244 raise GitDiffRequired
2240 raise GitDiffRequired
2245 # Buffer the whole output until we are sure it can be generated
2241 # Buffer the whole output until we are sure it can be generated
2246 return list(difffn(opts.copy(git=False), losedata))
2242 return list(difffn(opts.copy(git=False), losedata))
2247 except GitDiffRequired:
2243 except GitDiffRequired:
2248 return difffn(opts.copy(git=True), None)
2244 return difffn(opts.copy(git=True), None)
2249 else:
2245 else:
2250 return difffn(opts, None)
2246 return difffn(opts, None)
2251
2247
2252 def difflabel(func, *args, **kw):
2248 def difflabel(func, *args, **kw):
2253 '''yields 2-tuples of (output, label) based on the output of func()'''
2249 '''yields 2-tuples of (output, label) based on the output of func()'''
2254 headprefixes = [('diff', 'diff.diffline'),
2250 headprefixes = [('diff', 'diff.diffline'),
2255 ('copy', 'diff.extended'),
2251 ('copy', 'diff.extended'),
2256 ('rename', 'diff.extended'),
2252 ('rename', 'diff.extended'),
2257 ('old', 'diff.extended'),
2253 ('old', 'diff.extended'),
2258 ('new', 'diff.extended'),
2254 ('new', 'diff.extended'),
2259 ('deleted', 'diff.extended'),
2255 ('deleted', 'diff.extended'),
2260 ('---', 'diff.file_a'),
2256 ('---', 'diff.file_a'),
2261 ('+++', 'diff.file_b')]
2257 ('+++', 'diff.file_b')]
2262 textprefixes = [('@', 'diff.hunk'),
2258 textprefixes = [('@', 'diff.hunk'),
2263 ('-', 'diff.deleted'),
2259 ('-', 'diff.deleted'),
2264 ('+', 'diff.inserted')]
2260 ('+', 'diff.inserted')]
2265 head = False
2261 head = False
2266 for chunk in func(*args, **kw):
2262 for chunk in func(*args, **kw):
2267 lines = chunk.split('\n')
2263 lines = chunk.split('\n')
2268 for i, line in enumerate(lines):
2264 for i, line in enumerate(lines):
2269 if i != 0:
2265 if i != 0:
2270 yield ('\n', '')
2266 yield ('\n', '')
2271 if head:
2267 if head:
2272 if line.startswith('@'):
2268 if line.startswith('@'):
2273 head = False
2269 head = False
2274 else:
2270 else:
2275 if line and line[0] not in ' +-@\\':
2271 if line and line[0] not in ' +-@\\':
2276 head = True
2272 head = True
2277 stripline = line
2273 stripline = line
2278 diffline = False
2274 diffline = False
2279 if not head and line and line[0] in '+-':
2275 if not head and line and line[0] in '+-':
2280 # highlight tabs and trailing whitespace, but only in
2276 # highlight tabs and trailing whitespace, but only in
2281 # changed lines
2277 # changed lines
2282 stripline = line.rstrip()
2278 stripline = line.rstrip()
2283 diffline = True
2279 diffline = True
2284
2280
2285 prefixes = textprefixes
2281 prefixes = textprefixes
2286 if head:
2282 if head:
2287 prefixes = headprefixes
2283 prefixes = headprefixes
2288 for prefix, label in prefixes:
2284 for prefix, label in prefixes:
2289 if stripline.startswith(prefix):
2285 if stripline.startswith(prefix):
2290 if diffline:
2286 if diffline:
2291 for token in tabsplitter.findall(stripline):
2287 for token in tabsplitter.findall(stripline):
2292 if '\t' == token[0]:
2288 if '\t' == token[0]:
2293 yield (token, 'diff.tab')
2289 yield (token, 'diff.tab')
2294 else:
2290 else:
2295 yield (token, label)
2291 yield (token, label)
2296 else:
2292 else:
2297 yield (stripline, label)
2293 yield (stripline, label)
2298 break
2294 break
2299 else:
2295 else:
2300 yield (line, '')
2296 yield (line, '')
2301 if line != stripline:
2297 if line != stripline:
2302 yield (line[len(stripline):], 'diff.trailingwhitespace')
2298 yield (line[len(stripline):], 'diff.trailingwhitespace')
2303
2299
2304 def diffui(*args, **kw):
2300 def diffui(*args, **kw):
2305 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2301 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2306 return difflabel(diff, *args, **kw)
2302 return difflabel(diff, *args, **kw)
2307
2303
2308 def _filepairs(ctx1, modified, added, removed, copy, opts):
2304 def _filepairs(ctx1, modified, added, removed, copy, opts):
2309 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2305 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2310 before and f2 is the the name after. For added files, f1 will be None,
2306 before and f2 is the the name after. For added files, f1 will be None,
2311 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2307 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2312 or 'rename' (the latter two only if opts.git is set).'''
2308 or 'rename' (the latter two only if opts.git is set).'''
2313 gone = set()
2309 gone = set()
2314
2310
2315 copyto = dict([(v, k) for k, v in copy.items()])
2311 copyto = dict([(v, k) for k, v in copy.items()])
2316
2312
2317 addedset, removedset = set(added), set(removed)
2313 addedset, removedset = set(added), set(removed)
2318 # Fix up added, since merged-in additions appear as
2314 # Fix up added, since merged-in additions appear as
2319 # modifications during merges
2315 # modifications during merges
2320 for f in modified:
2316 for f in modified:
2321 if f not in ctx1:
2317 if f not in ctx1:
2322 addedset.add(f)
2318 addedset.add(f)
2323
2319
2324 for f in sorted(modified + added + removed):
2320 for f in sorted(modified + added + removed):
2325 copyop = None
2321 copyop = None
2326 f1, f2 = f, f
2322 f1, f2 = f, f
2327 if f in addedset:
2323 if f in addedset:
2328 f1 = None
2324 f1 = None
2329 if f in copy:
2325 if f in copy:
2330 if opts.git:
2326 if opts.git:
2331 f1 = copy[f]
2327 f1 = copy[f]
2332 if f1 in removedset and f1 not in gone:
2328 if f1 in removedset and f1 not in gone:
2333 copyop = 'rename'
2329 copyop = 'rename'
2334 gone.add(f1)
2330 gone.add(f1)
2335 else:
2331 else:
2336 copyop = 'copy'
2332 copyop = 'copy'
2337 elif f in removedset:
2333 elif f in removedset:
2338 f2 = None
2334 f2 = None
2339 if opts.git:
2335 if opts.git:
2340 # have we already reported a copy above?
2336 # have we already reported a copy above?
2341 if (f in copyto and copyto[f] in addedset
2337 if (f in copyto and copyto[f] in addedset
2342 and copy[copyto[f]] == f):
2338 and copy[copyto[f]] == f):
2343 continue
2339 continue
2344 yield f1, f2, copyop
2340 yield f1, f2, copyop
2345
2341
2346 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2342 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2347 copy, getfilectx, opts, losedatafn, prefix, relroot):
2343 copy, getfilectx, opts, losedatafn, prefix, relroot):
2348 '''given input data, generate a diff and yield it in blocks
2344 '''given input data, generate a diff and yield it in blocks
2349
2345
2350 If generating a diff would lose data like flags or binary data and
2346 If generating a diff would lose data like flags or binary data and
2351 losedatafn is not None, it will be called.
2347 losedatafn is not None, it will be called.
2352
2348
2353 relroot is removed and prefix is added to every path in the diff output.
2349 relroot is removed and prefix is added to every path in the diff output.
2354
2350
2355 If relroot is not empty, this function expects every path in modified,
2351 If relroot is not empty, this function expects every path in modified,
2356 added, removed and copy to start with it.'''
2352 added, removed and copy to start with it.'''
2357
2353
2358 def gitindex(text):
2354 def gitindex(text):
2359 if not text:
2355 if not text:
2360 text = ""
2356 text = ""
2361 l = len(text)
2357 l = len(text)
2362 s = util.sha1('blob %d\0' % l)
2358 s = util.sha1('blob %d\0' % l)
2363 s.update(text)
2359 s.update(text)
2364 return s.hexdigest()
2360 return s.hexdigest()
2365
2361
2366 if opts.noprefix:
2362 if opts.noprefix:
2367 aprefix = bprefix = ''
2363 aprefix = bprefix = ''
2368 else:
2364 else:
2369 aprefix = 'a/'
2365 aprefix = 'a/'
2370 bprefix = 'b/'
2366 bprefix = 'b/'
2371
2367
2372 def diffline(f, revs):
2368 def diffline(f, revs):
2373 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2369 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2374 return 'diff %s %s' % (revinfo, f)
2370 return 'diff %s %s' % (revinfo, f)
2375
2371
2376 date1 = util.datestr(ctx1.date())
2372 date1 = util.datestr(ctx1.date())
2377 date2 = util.datestr(ctx2.date())
2373 date2 = util.datestr(ctx2.date())
2378
2374
2379 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2375 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2380
2376
2381 if relroot != '' and (repo.ui.configbool('devel', 'all')
2377 if relroot != '' and (repo.ui.configbool('devel', 'all')
2382 or repo.ui.configbool('devel', 'check-relroot')):
2378 or repo.ui.configbool('devel', 'check-relroot')):
2383 for f in modified + added + removed + copy.keys() + copy.values():
2379 for f in modified + added + removed + copy.keys() + copy.values():
2384 if f is not None and not f.startswith(relroot):
2380 if f is not None and not f.startswith(relroot):
2385 raise AssertionError(
2381 raise AssertionError(
2386 "file %s doesn't start with relroot %s" % (f, relroot))
2382 "file %s doesn't start with relroot %s" % (f, relroot))
2387
2383
2388 for f1, f2, copyop in _filepairs(
2384 for f1, f2, copyop in _filepairs(
2389 ctx1, modified, added, removed, copy, opts):
2385 ctx1, modified, added, removed, copy, opts):
2390 content1 = None
2386 content1 = None
2391 content2 = None
2387 content2 = None
2392 flag1 = None
2388 flag1 = None
2393 flag2 = None
2389 flag2 = None
2394 if f1:
2390 if f1:
2395 content1 = getfilectx(f1, ctx1).data()
2391 content1 = getfilectx(f1, ctx1).data()
2396 if opts.git or losedatafn:
2392 if opts.git or losedatafn:
2397 flag1 = ctx1.flags(f1)
2393 flag1 = ctx1.flags(f1)
2398 if f2:
2394 if f2:
2399 content2 = getfilectx(f2, ctx2).data()
2395 content2 = getfilectx(f2, ctx2).data()
2400 if opts.git or losedatafn:
2396 if opts.git or losedatafn:
2401 flag2 = ctx2.flags(f2)
2397 flag2 = ctx2.flags(f2)
2402 binary = False
2398 binary = False
2403 if opts.git or losedatafn:
2399 if opts.git or losedatafn:
2404 binary = util.binary(content1) or util.binary(content2)
2400 binary = util.binary(content1) or util.binary(content2)
2405
2401
2406 if losedatafn and not opts.git:
2402 if losedatafn and not opts.git:
2407 if (binary or
2403 if (binary or
2408 # copy/rename
2404 # copy/rename
2409 f2 in copy or
2405 f2 in copy or
2410 # empty file creation
2406 # empty file creation
2411 (not f1 and not content2) or
2407 (not f1 and not content2) or
2412 # empty file deletion
2408 # empty file deletion
2413 (not content1 and not f2) or
2409 (not content1 and not f2) or
2414 # create with flags
2410 # create with flags
2415 (not f1 and flag2) or
2411 (not f1 and flag2) or
2416 # change flags
2412 # change flags
2417 (f1 and f2 and flag1 != flag2)):
2413 (f1 and f2 and flag1 != flag2)):
2418 losedatafn(f2 or f1)
2414 losedatafn(f2 or f1)
2419
2415
2420 path1 = f1 or f2
2416 path1 = f1 or f2
2421 path2 = f2 or f1
2417 path2 = f2 or f1
2422 path1 = posixpath.join(prefix, path1[len(relroot):])
2418 path1 = posixpath.join(prefix, path1[len(relroot):])
2423 path2 = posixpath.join(prefix, path2[len(relroot):])
2419 path2 = posixpath.join(prefix, path2[len(relroot):])
2424 header = []
2420 header = []
2425 if opts.git:
2421 if opts.git:
2426 header.append('diff --git %s%s %s%s' %
2422 header.append('diff --git %s%s %s%s' %
2427 (aprefix, path1, bprefix, path2))
2423 (aprefix, path1, bprefix, path2))
2428 if not f1: # added
2424 if not f1: # added
2429 header.append('new file mode %s' % gitmode[flag2])
2425 header.append('new file mode %s' % gitmode[flag2])
2430 elif not f2: # removed
2426 elif not f2: # removed
2431 header.append('deleted file mode %s' % gitmode[flag1])
2427 header.append('deleted file mode %s' % gitmode[flag1])
2432 else: # modified/copied/renamed
2428 else: # modified/copied/renamed
2433 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2429 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2434 if mode1 != mode2:
2430 if mode1 != mode2:
2435 header.append('old mode %s' % mode1)
2431 header.append('old mode %s' % mode1)
2436 header.append('new mode %s' % mode2)
2432 header.append('new mode %s' % mode2)
2437 if copyop is not None:
2433 if copyop is not None:
2438 header.append('%s from %s' % (copyop, path1))
2434 header.append('%s from %s' % (copyop, path1))
2439 header.append('%s to %s' % (copyop, path2))
2435 header.append('%s to %s' % (copyop, path2))
2440 elif revs and not repo.ui.quiet:
2436 elif revs and not repo.ui.quiet:
2441 header.append(diffline(path1, revs))
2437 header.append(diffline(path1, revs))
2442
2438
2443 if binary and opts.git and not opts.nobinary:
2439 if binary and opts.git and not opts.nobinary:
2444 text = mdiff.b85diff(content1, content2)
2440 text = mdiff.b85diff(content1, content2)
2445 if text:
2441 if text:
2446 header.append('index %s..%s' %
2442 header.append('index %s..%s' %
2447 (gitindex(content1), gitindex(content2)))
2443 (gitindex(content1), gitindex(content2)))
2448 else:
2444 else:
2449 text = mdiff.unidiff(content1, date1,
2445 text = mdiff.unidiff(content1, date1,
2450 content2, date2,
2446 content2, date2,
2451 path1, path2, opts=opts)
2447 path1, path2, opts=opts)
2452 if header and (text or len(header) > 1):
2448 if header and (text or len(header) > 1):
2453 yield '\n'.join(header) + '\n'
2449 yield '\n'.join(header) + '\n'
2454 if text:
2450 if text:
2455 yield text
2451 yield text
2456
2452
2457 def diffstatsum(stats):
2453 def diffstatsum(stats):
2458 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2454 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2459 for f, a, r, b in stats:
2455 for f, a, r, b in stats:
2460 maxfile = max(maxfile, encoding.colwidth(f))
2456 maxfile = max(maxfile, encoding.colwidth(f))
2461 maxtotal = max(maxtotal, a + r)
2457 maxtotal = max(maxtotal, a + r)
2462 addtotal += a
2458 addtotal += a
2463 removetotal += r
2459 removetotal += r
2464 binary = binary or b
2460 binary = binary or b
2465
2461
2466 return maxfile, maxtotal, addtotal, removetotal, binary
2462 return maxfile, maxtotal, addtotal, removetotal, binary
2467
2463
2468 def diffstatdata(lines):
2464 def diffstatdata(lines):
2469 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2465 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2470
2466
2471 results = []
2467 results = []
2472 filename, adds, removes, isbinary = None, 0, 0, False
2468 filename, adds, removes, isbinary = None, 0, 0, False
2473
2469
2474 def addresult():
2470 def addresult():
2475 if filename:
2471 if filename:
2476 results.append((filename, adds, removes, isbinary))
2472 results.append((filename, adds, removes, isbinary))
2477
2473
2478 for line in lines:
2474 for line in lines:
2479 if line.startswith('diff'):
2475 if line.startswith('diff'):
2480 addresult()
2476 addresult()
2481 # set numbers to 0 anyway when starting new file
2477 # set numbers to 0 anyway when starting new file
2482 adds, removes, isbinary = 0, 0, False
2478 adds, removes, isbinary = 0, 0, False
2483 if line.startswith('diff --git a/'):
2479 if line.startswith('diff --git a/'):
2484 filename = gitre.search(line).group(2)
2480 filename = gitre.search(line).group(2)
2485 elif line.startswith('diff -r'):
2481 elif line.startswith('diff -r'):
2486 # format: "diff -r ... -r ... filename"
2482 # format: "diff -r ... -r ... filename"
2487 filename = diffre.search(line).group(1)
2483 filename = diffre.search(line).group(1)
2488 elif line.startswith('+') and not line.startswith('+++ '):
2484 elif line.startswith('+') and not line.startswith('+++ '):
2489 adds += 1
2485 adds += 1
2490 elif line.startswith('-') and not line.startswith('--- '):
2486 elif line.startswith('-') and not line.startswith('--- '):
2491 removes += 1
2487 removes += 1
2492 elif (line.startswith('GIT binary patch') or
2488 elif (line.startswith('GIT binary patch') or
2493 line.startswith('Binary file')):
2489 line.startswith('Binary file')):
2494 isbinary = True
2490 isbinary = True
2495 addresult()
2491 addresult()
2496 return results
2492 return results
2497
2493
2498 def diffstat(lines, width=80, git=False):
2494 def diffstat(lines, width=80, git=False):
2499 output = []
2495 output = []
2500 stats = diffstatdata(lines)
2496 stats = diffstatdata(lines)
2501 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2497 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2502
2498
2503 countwidth = len(str(maxtotal))
2499 countwidth = len(str(maxtotal))
2504 if hasbinary and countwidth < 3:
2500 if hasbinary and countwidth < 3:
2505 countwidth = 3
2501 countwidth = 3
2506 graphwidth = width - countwidth - maxname - 6
2502 graphwidth = width - countwidth - maxname - 6
2507 if graphwidth < 10:
2503 if graphwidth < 10:
2508 graphwidth = 10
2504 graphwidth = 10
2509
2505
2510 def scale(i):
2506 def scale(i):
2511 if maxtotal <= graphwidth:
2507 if maxtotal <= graphwidth:
2512 return i
2508 return i
2513 # If diffstat runs out of room it doesn't print anything,
2509 # If diffstat runs out of room it doesn't print anything,
2514 # which isn't very useful, so always print at least one + or -
2510 # which isn't very useful, so always print at least one + or -
2515 # if there were at least some changes.
2511 # if there were at least some changes.
2516 return max(i * graphwidth // maxtotal, int(bool(i)))
2512 return max(i * graphwidth // maxtotal, int(bool(i)))
2517
2513
2518 for filename, adds, removes, isbinary in stats:
2514 for filename, adds, removes, isbinary in stats:
2519 if isbinary:
2515 if isbinary:
2520 count = 'Bin'
2516 count = 'Bin'
2521 else:
2517 else:
2522 count = adds + removes
2518 count = adds + removes
2523 pluses = '+' * scale(adds)
2519 pluses = '+' * scale(adds)
2524 minuses = '-' * scale(removes)
2520 minuses = '-' * scale(removes)
2525 output.append(' %s%s | %*s %s%s\n' %
2521 output.append(' %s%s | %*s %s%s\n' %
2526 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2522 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2527 countwidth, count, pluses, minuses))
2523 countwidth, count, pluses, minuses))
2528
2524
2529 if stats:
2525 if stats:
2530 output.append(_(' %d files changed, %d insertions(+), '
2526 output.append(_(' %d files changed, %d insertions(+), '
2531 '%d deletions(-)\n')
2527 '%d deletions(-)\n')
2532 % (len(stats), totaladds, totalremoves))
2528 % (len(stats), totaladds, totalremoves))
2533
2529
2534 return ''.join(output)
2530 return ''.join(output)
2535
2531
2536 def diffstatui(*args, **kw):
2532 def diffstatui(*args, **kw):
2537 '''like diffstat(), but yields 2-tuples of (output, label) for
2533 '''like diffstat(), but yields 2-tuples of (output, label) for
2538 ui.write()
2534 ui.write()
2539 '''
2535 '''
2540
2536
2541 for line in diffstat(*args, **kw).splitlines():
2537 for line in diffstat(*args, **kw).splitlines():
2542 if line and line[-1] in '+-':
2538 if line and line[-1] in '+-':
2543 name, graph = line.rsplit(' ', 1)
2539 name, graph = line.rsplit(' ', 1)
2544 yield (name + ' ', '')
2540 yield (name + ' ', '')
2545 m = re.search(r'\++', graph)
2541 m = re.search(r'\++', graph)
2546 if m:
2542 if m:
2547 yield (m.group(0), 'diffstat.inserted')
2543 yield (m.group(0), 'diffstat.inserted')
2548 m = re.search(r'-+', graph)
2544 m = re.search(r'-+', graph)
2549 if m:
2545 if m:
2550 yield (m.group(0), 'diffstat.deleted')
2546 yield (m.group(0), 'diffstat.deleted')
2551 else:
2547 else:
2552 yield (line, '')
2548 yield (line, '')
2553 yield ('\n', '')
2549 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now