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