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