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