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