##// END OF EJS Templates
patch, i18n: avoid parameterized messages...
Wagner Bruna -
r10518:5fe51d34 stable
parent child Browse files
Show More
@@ -1,1663 +1,1660 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 line.startswith('--- ') and inheader:
160 elif line.startswith('--- ') and inheader:
161 # No evil headers seen by diff start, split by hand
161 # No evil headers seen by diff start, 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 offset = l - orig_start - fuzzlen
610 if fuzzlen:
611 if fuzzlen:
611 fuzzstr = "with fuzz %d " % fuzzlen
612 msg = _("Hunk #%d succeeded at %d "
612 f = self.ui.warn
613 "with fuzz %d "
614 "(offset %d lines).\n")
613 self.printfile(True)
615 self.printfile(True)
616 self.ui.warn(msg %
617 (h.number, l + 1, fuzzlen, offset))
614 else:
618 else:
615 fuzzstr = ""
619 msg = _("Hunk #%d succeeded at %d "
616 f = self.ui.note
617 offset = l - orig_start - fuzzlen
618 if offset == 1:
619 msg = _("Hunk #%d succeeded at %d %s"
620 "(offset %d line).\n")
621 else:
622 msg = _("Hunk #%d succeeded at %d %s"
623 "(offset %d lines).\n")
620 "(offset %d lines).\n")
624 f(msg % (h.number, l + 1, fuzzstr, offset))
621 self.ui.note(msg % (h.number, l + 1, offset))
625 return fuzzlen
622 return fuzzlen
626 self.printfile(True)
623 self.printfile(True)
627 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
624 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
628 self.rej.append(horig)
625 self.rej.append(horig)
629 return -1
626 return -1
630
627
631 class hunk(object):
628 class hunk(object):
632 def __init__(self, desc, num, lr, context, create=False, remove=False):
629 def __init__(self, desc, num, lr, context, create=False, remove=False):
633 self.number = num
630 self.number = num
634 self.desc = desc
631 self.desc = desc
635 self.hunk = [desc]
632 self.hunk = [desc]
636 self.a = []
633 self.a = []
637 self.b = []
634 self.b = []
638 self.starta = self.lena = None
635 self.starta = self.lena = None
639 self.startb = self.lenb = None
636 self.startb = self.lenb = None
640 if lr is not None:
637 if lr is not None:
641 if context:
638 if context:
642 self.read_context_hunk(lr)
639 self.read_context_hunk(lr)
643 else:
640 else:
644 self.read_unified_hunk(lr)
641 self.read_unified_hunk(lr)
645 self.create = create
642 self.create = create
646 self.remove = remove and not create
643 self.remove = remove and not create
647
644
648 def getnormalized(self):
645 def getnormalized(self):
649 """Return a copy with line endings normalized to LF."""
646 """Return a copy with line endings normalized to LF."""
650
647
651 def normalize(lines):
648 def normalize(lines):
652 nlines = []
649 nlines = []
653 for line in lines:
650 for line in lines:
654 if line.endswith('\r\n'):
651 if line.endswith('\r\n'):
655 line = line[:-2] + '\n'
652 line = line[:-2] + '\n'
656 nlines.append(line)
653 nlines.append(line)
657 return nlines
654 return nlines
658
655
659 # Dummy object, it is rebuilt manually
656 # Dummy object, it is rebuilt manually
660 nh = hunk(self.desc, self.number, None, None, False, False)
657 nh = hunk(self.desc, self.number, None, None, False, False)
661 nh.number = self.number
658 nh.number = self.number
662 nh.desc = self.desc
659 nh.desc = self.desc
663 nh.a = normalize(self.a)
660 nh.a = normalize(self.a)
664 nh.b = normalize(self.b)
661 nh.b = normalize(self.b)
665 nh.starta = self.starta
662 nh.starta = self.starta
666 nh.startb = self.startb
663 nh.startb = self.startb
667 nh.lena = self.lena
664 nh.lena = self.lena
668 nh.lenb = self.lenb
665 nh.lenb = self.lenb
669 nh.create = self.create
666 nh.create = self.create
670 nh.remove = self.remove
667 nh.remove = self.remove
671 return nh
668 return nh
672
669
673 def read_unified_hunk(self, lr):
670 def read_unified_hunk(self, lr):
674 m = unidesc.match(self.desc)
671 m = unidesc.match(self.desc)
675 if not m:
672 if not m:
676 raise PatchError(_("bad hunk #%d") % self.number)
673 raise PatchError(_("bad hunk #%d") % self.number)
677 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
674 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
678 if self.lena is None:
675 if self.lena is None:
679 self.lena = 1
676 self.lena = 1
680 else:
677 else:
681 self.lena = int(self.lena)
678 self.lena = int(self.lena)
682 if self.lenb is None:
679 if self.lenb is None:
683 self.lenb = 1
680 self.lenb = 1
684 else:
681 else:
685 self.lenb = int(self.lenb)
682 self.lenb = int(self.lenb)
686 self.starta = int(self.starta)
683 self.starta = int(self.starta)
687 self.startb = int(self.startb)
684 self.startb = int(self.startb)
688 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
685 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
686 # if we hit eof before finishing out the hunk, the last line will
690 # be zero length. Lets try to fix it up.
687 # be zero length. Lets try to fix it up.
691 while len(self.hunk[-1]) == 0:
688 while len(self.hunk[-1]) == 0:
692 del self.hunk[-1]
689 del self.hunk[-1]
693 del self.a[-1]
690 del self.a[-1]
694 del self.b[-1]
691 del self.b[-1]
695 self.lena -= 1
692 self.lena -= 1
696 self.lenb -= 1
693 self.lenb -= 1
697
694
698 def read_context_hunk(self, lr):
695 def read_context_hunk(self, lr):
699 self.desc = lr.readline()
696 self.desc = lr.readline()
700 m = contextdesc.match(self.desc)
697 m = contextdesc.match(self.desc)
701 if not m:
698 if not m:
702 raise PatchError(_("bad hunk #%d") % self.number)
699 raise PatchError(_("bad hunk #%d") % self.number)
703 foo, self.starta, foo2, aend, foo3 = m.groups()
700 foo, self.starta, foo2, aend, foo3 = m.groups()
704 self.starta = int(self.starta)
701 self.starta = int(self.starta)
705 if aend is None:
702 if aend is None:
706 aend = self.starta
703 aend = self.starta
707 self.lena = int(aend) - self.starta
704 self.lena = int(aend) - self.starta
708 if self.starta:
705 if self.starta:
709 self.lena += 1
706 self.lena += 1
710 for x in xrange(self.lena):
707 for x in xrange(self.lena):
711 l = lr.readline()
708 l = lr.readline()
712 if l.startswith('---'):
709 if l.startswith('---'):
713 lr.push(l)
710 lr.push(l)
714 break
711 break
715 s = l[2:]
712 s = l[2:]
716 if l.startswith('- ') or l.startswith('! '):
713 if l.startswith('- ') or l.startswith('! '):
717 u = '-' + s
714 u = '-' + s
718 elif l.startswith(' '):
715 elif l.startswith(' '):
719 u = ' ' + s
716 u = ' ' + s
720 else:
717 else:
721 raise PatchError(_("bad hunk #%d old text line %d") %
718 raise PatchError(_("bad hunk #%d old text line %d") %
722 (self.number, x))
719 (self.number, x))
723 self.a.append(u)
720 self.a.append(u)
724 self.hunk.append(u)
721 self.hunk.append(u)
725
722
726 l = lr.readline()
723 l = lr.readline()
727 if l.startswith('\ '):
724 if l.startswith('\ '):
728 s = self.a[-1][:-1]
725 s = self.a[-1][:-1]
729 self.a[-1] = s
726 self.a[-1] = s
730 self.hunk[-1] = s
727 self.hunk[-1] = s
731 l = lr.readline()
728 l = lr.readline()
732 m = contextdesc.match(l)
729 m = contextdesc.match(l)
733 if not m:
730 if not m:
734 raise PatchError(_("bad hunk #%d") % self.number)
731 raise PatchError(_("bad hunk #%d") % self.number)
735 foo, self.startb, foo2, bend, foo3 = m.groups()
732 foo, self.startb, foo2, bend, foo3 = m.groups()
736 self.startb = int(self.startb)
733 self.startb = int(self.startb)
737 if bend is None:
734 if bend is None:
738 bend = self.startb
735 bend = self.startb
739 self.lenb = int(bend) - self.startb
736 self.lenb = int(bend) - self.startb
740 if self.startb:
737 if self.startb:
741 self.lenb += 1
738 self.lenb += 1
742 hunki = 1
739 hunki = 1
743 for x in xrange(self.lenb):
740 for x in xrange(self.lenb):
744 l = lr.readline()
741 l = lr.readline()
745 if l.startswith('\ '):
742 if l.startswith('\ '):
746 s = self.b[-1][:-1]
743 s = self.b[-1][:-1]
747 self.b[-1] = s
744 self.b[-1] = s
748 self.hunk[hunki - 1] = s
745 self.hunk[hunki - 1] = s
749 continue
746 continue
750 if not l:
747 if not l:
751 lr.push(l)
748 lr.push(l)
752 break
749 break
753 s = l[2:]
750 s = l[2:]
754 if l.startswith('+ ') or l.startswith('! '):
751 if l.startswith('+ ') or l.startswith('! '):
755 u = '+' + s
752 u = '+' + s
756 elif l.startswith(' '):
753 elif l.startswith(' '):
757 u = ' ' + s
754 u = ' ' + s
758 elif len(self.b) == 0:
755 elif len(self.b) == 0:
759 # this can happen when the hunk does not add any lines
756 # this can happen when the hunk does not add any lines
760 lr.push(l)
757 lr.push(l)
761 break
758 break
762 else:
759 else:
763 raise PatchError(_("bad hunk #%d old text line %d") %
760 raise PatchError(_("bad hunk #%d old text line %d") %
764 (self.number, x))
761 (self.number, x))
765 self.b.append(s)
762 self.b.append(s)
766 while True:
763 while True:
767 if hunki >= len(self.hunk):
764 if hunki >= len(self.hunk):
768 h = ""
765 h = ""
769 else:
766 else:
770 h = self.hunk[hunki]
767 h = self.hunk[hunki]
771 hunki += 1
768 hunki += 1
772 if h == u:
769 if h == u:
773 break
770 break
774 elif h.startswith('-'):
771 elif h.startswith('-'):
775 continue
772 continue
776 else:
773 else:
777 self.hunk.insert(hunki - 1, u)
774 self.hunk.insert(hunki - 1, u)
778 break
775 break
779
776
780 if not self.a:
777 if not self.a:
781 # this happens when lines were only added to the hunk
778 # this happens when lines were only added to the hunk
782 for x in self.hunk:
779 for x in self.hunk:
783 if x.startswith('-') or x.startswith(' '):
780 if x.startswith('-') or x.startswith(' '):
784 self.a.append(x)
781 self.a.append(x)
785 if not self.b:
782 if not self.b:
786 # this happens when lines were only deleted from the hunk
783 # this happens when lines were only deleted from the hunk
787 for x in self.hunk:
784 for x in self.hunk:
788 if x.startswith('+') or x.startswith(' '):
785 if x.startswith('+') or x.startswith(' '):
789 self.b.append(x[1:])
786 self.b.append(x[1:])
790 # @@ -start,len +start,len @@
787 # @@ -start,len +start,len @@
791 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
788 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
792 self.startb, self.lenb)
789 self.startb, self.lenb)
793 self.hunk[0] = self.desc
790 self.hunk[0] = self.desc
794
791
795 def fix_newline(self):
792 def fix_newline(self):
796 diffhelpers.fix_newline(self.hunk, self.a, self.b)
793 diffhelpers.fix_newline(self.hunk, self.a, self.b)
797
794
798 def complete(self):
795 def complete(self):
799 return len(self.a) == self.lena and len(self.b) == self.lenb
796 return len(self.a) == self.lena and len(self.b) == self.lenb
800
797
801 def createfile(self):
798 def createfile(self):
802 return self.starta == 0 and self.lena == 0 and self.create
799 return self.starta == 0 and self.lena == 0 and self.create
803
800
804 def rmfile(self):
801 def rmfile(self):
805 return self.startb == 0 and self.lenb == 0 and self.remove
802 return self.startb == 0 and self.lenb == 0 and self.remove
806
803
807 def fuzzit(self, l, fuzz, toponly):
804 def fuzzit(self, l, fuzz, toponly):
808 # this removes context lines from the top and bottom of list 'l'. It
805 # 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
806 # checks the hunk to make sure only context lines are removed, and then
810 # returns a new shortened list of lines.
807 # returns a new shortened list of lines.
811 fuzz = min(fuzz, len(l)-1)
808 fuzz = min(fuzz, len(l)-1)
812 if fuzz:
809 if fuzz:
813 top = 0
810 top = 0
814 bot = 0
811 bot = 0
815 hlen = len(self.hunk)
812 hlen = len(self.hunk)
816 for x in xrange(hlen - 1):
813 for x in xrange(hlen - 1):
817 # the hunk starts with the @@ line, so use x+1
814 # the hunk starts with the @@ line, so use x+1
818 if self.hunk[x + 1][0] == ' ':
815 if self.hunk[x + 1][0] == ' ':
819 top += 1
816 top += 1
820 else:
817 else:
821 break
818 break
822 if not toponly:
819 if not toponly:
823 for x in xrange(hlen - 1):
820 for x in xrange(hlen - 1):
824 if self.hunk[hlen - bot - 1][0] == ' ':
821 if self.hunk[hlen - bot - 1][0] == ' ':
825 bot += 1
822 bot += 1
826 else:
823 else:
827 break
824 break
828
825
829 # top and bot now count context in the hunk
826 # top and bot now count context in the hunk
830 # adjust them if either one is short
827 # adjust them if either one is short
831 context = max(top, bot, 3)
828 context = max(top, bot, 3)
832 if bot < context:
829 if bot < context:
833 bot = max(0, fuzz - (context - bot))
830 bot = max(0, fuzz - (context - bot))
834 else:
831 else:
835 bot = min(fuzz, bot)
832 bot = min(fuzz, bot)
836 if top < context:
833 if top < context:
837 top = max(0, fuzz - (context - top))
834 top = max(0, fuzz - (context - top))
838 else:
835 else:
839 top = min(fuzz, top)
836 top = min(fuzz, top)
840
837
841 return l[top:len(l)-bot]
838 return l[top:len(l)-bot]
842 return l
839 return l
843
840
844 def old(self, fuzz=0, toponly=False):
841 def old(self, fuzz=0, toponly=False):
845 return self.fuzzit(self.a, fuzz, toponly)
842 return self.fuzzit(self.a, fuzz, toponly)
846
843
847 def new(self, fuzz=0, toponly=False):
844 def new(self, fuzz=0, toponly=False):
848 return self.fuzzit(self.b, fuzz, toponly)
845 return self.fuzzit(self.b, fuzz, toponly)
849
846
850 class binhunk:
847 class binhunk:
851 'A binary patch file. Only understands literals so far.'
848 'A binary patch file. Only understands literals so far.'
852 def __init__(self, gitpatch):
849 def __init__(self, gitpatch):
853 self.gitpatch = gitpatch
850 self.gitpatch = gitpatch
854 self.text = None
851 self.text = None
855 self.hunk = ['GIT binary patch\n']
852 self.hunk = ['GIT binary patch\n']
856
853
857 def createfile(self):
854 def createfile(self):
858 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
855 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
859
856
860 def rmfile(self):
857 def rmfile(self):
861 return self.gitpatch.op == 'DELETE'
858 return self.gitpatch.op == 'DELETE'
862
859
863 def complete(self):
860 def complete(self):
864 return self.text is not None
861 return self.text is not None
865
862
866 def new(self):
863 def new(self):
867 return [self.text]
864 return [self.text]
868
865
869 def extract(self, lr):
866 def extract(self, lr):
870 line = lr.readline()
867 line = lr.readline()
871 self.hunk.append(line)
868 self.hunk.append(line)
872 while line and not line.startswith('literal '):
869 while line and not line.startswith('literal '):
873 line = lr.readline()
870 line = lr.readline()
874 self.hunk.append(line)
871 self.hunk.append(line)
875 if not line:
872 if not line:
876 raise PatchError(_('could not extract binary patch'))
873 raise PatchError(_('could not extract binary patch'))
877 size = int(line[8:].rstrip())
874 size = int(line[8:].rstrip())
878 dec = []
875 dec = []
879 line = lr.readline()
876 line = lr.readline()
880 self.hunk.append(line)
877 self.hunk.append(line)
881 while len(line) > 1:
878 while len(line) > 1:
882 l = line[0]
879 l = line[0]
883 if l <= 'Z' and l >= 'A':
880 if l <= 'Z' and l >= 'A':
884 l = ord(l) - ord('A') + 1
881 l = ord(l) - ord('A') + 1
885 else:
882 else:
886 l = ord(l) - ord('a') + 27
883 l = ord(l) - ord('a') + 27
887 dec.append(base85.b85decode(line[1:-1])[:l])
884 dec.append(base85.b85decode(line[1:-1])[:l])
888 line = lr.readline()
885 line = lr.readline()
889 self.hunk.append(line)
886 self.hunk.append(line)
890 text = zlib.decompress(''.join(dec))
887 text = zlib.decompress(''.join(dec))
891 if len(text) != size:
888 if len(text) != size:
892 raise PatchError(_('binary patch is %d bytes, not %d') %
889 raise PatchError(_('binary patch is %d bytes, not %d') %
893 len(text), size)
890 len(text), size)
894 self.text = text
891 self.text = text
895
892
896 def parsefilename(str):
893 def parsefilename(str):
897 # --- filename \t|space stuff
894 # --- filename \t|space stuff
898 s = str[4:].rstrip('\r\n')
895 s = str[4:].rstrip('\r\n')
899 i = s.find('\t')
896 i = s.find('\t')
900 if i < 0:
897 if i < 0:
901 i = s.find(' ')
898 i = s.find(' ')
902 if i < 0:
899 if i < 0:
903 return s
900 return s
904 return s[:i]
901 return s[:i]
905
902
906 def selectfile(afile_orig, bfile_orig, hunk, strip):
903 def selectfile(afile_orig, bfile_orig, hunk, strip):
907 def pathstrip(path, count=1):
904 def pathstrip(path, count=1):
908 pathlen = len(path)
905 pathlen = len(path)
909 i = 0
906 i = 0
910 if count == 0:
907 if count == 0:
911 return '', path.rstrip()
908 return '', path.rstrip()
912 while count > 0:
909 while count > 0:
913 i = path.find('/', i)
910 i = path.find('/', i)
914 if i == -1:
911 if i == -1:
915 raise PatchError(_("unable to strip away %d dirs from %s") %
912 raise PatchError(_("unable to strip away %d dirs from %s") %
916 (count, path))
913 (count, path))
917 i += 1
914 i += 1
918 # consume '//' in the path
915 # consume '//' in the path
919 while i < pathlen - 1 and path[i] == '/':
916 while i < pathlen - 1 and path[i] == '/':
920 i += 1
917 i += 1
921 count -= 1
918 count -= 1
922 return path[:i].lstrip(), path[i:].rstrip()
919 return path[:i].lstrip(), path[i:].rstrip()
923
920
924 nulla = afile_orig == "/dev/null"
921 nulla = afile_orig == "/dev/null"
925 nullb = bfile_orig == "/dev/null"
922 nullb = bfile_orig == "/dev/null"
926 abase, afile = pathstrip(afile_orig, strip)
923 abase, afile = pathstrip(afile_orig, strip)
927 gooda = not nulla and util.lexists(afile)
924 gooda = not nulla and util.lexists(afile)
928 bbase, bfile = pathstrip(bfile_orig, strip)
925 bbase, bfile = pathstrip(bfile_orig, strip)
929 if afile == bfile:
926 if afile == bfile:
930 goodb = gooda
927 goodb = gooda
931 else:
928 else:
932 goodb = not nullb and os.path.exists(bfile)
929 goodb = not nullb and os.path.exists(bfile)
933 createfunc = hunk.createfile
930 createfunc = hunk.createfile
934 missing = not goodb and not gooda and not createfunc()
931 missing = not goodb and not gooda and not createfunc()
935
932
936 # some diff programs apparently produce create patches where the
933 # some diff programs apparently produce create patches where the
937 # afile is not /dev/null, but rather the same name as the bfile
934 # afile is not /dev/null, but rather the same name as the bfile
938 if missing and afile == bfile:
935 if missing and afile == bfile:
939 # this isn't very pretty
936 # this isn't very pretty
940 hunk.create = True
937 hunk.create = True
941 if createfunc():
938 if createfunc():
942 missing = False
939 missing = False
943 else:
940 else:
944 hunk.create = False
941 hunk.create = False
945
942
946 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
943 # 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
944 # diff is between a file and its backup. In this case, the original
948 # file should be patched (see original mpatch code).
945 # file should be patched (see original mpatch code).
949 isbackup = (abase == bbase and bfile.startswith(afile))
946 isbackup = (abase == bbase and bfile.startswith(afile))
950 fname = None
947 fname = None
951 if not missing:
948 if not missing:
952 if gooda and goodb:
949 if gooda and goodb:
953 fname = isbackup and afile or bfile
950 fname = isbackup and afile or bfile
954 elif gooda:
951 elif gooda:
955 fname = afile
952 fname = afile
956
953
957 if not fname:
954 if not fname:
958 if not nullb:
955 if not nullb:
959 fname = isbackup and afile or bfile
956 fname = isbackup and afile or bfile
960 elif not nulla:
957 elif not nulla:
961 fname = afile
958 fname = afile
962 else:
959 else:
963 raise PatchError(_("undefined source and destination files"))
960 raise PatchError(_("undefined source and destination files"))
964
961
965 return fname, missing
962 return fname, missing
966
963
967 def scangitpatch(lr, firstline):
964 def scangitpatch(lr, firstline):
968 """
965 """
969 Git patches can emit:
966 Git patches can emit:
970 - rename a to b
967 - rename a to b
971 - change b
968 - change b
972 - copy a to c
969 - copy a to c
973 - change c
970 - change c
974
971
975 We cannot apply this sequence as-is, the renamed 'a' could not be
972 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
973 found for it would have been renamed already. And we cannot copy
977 from 'b' instead because 'b' would have been changed already. So
974 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
975 we scan the git patch for copy and rename commands so we can
979 perform the copies ahead of time.
976 perform the copies ahead of time.
980 """
977 """
981 pos = 0
978 pos = 0
982 try:
979 try:
983 pos = lr.fp.tell()
980 pos = lr.fp.tell()
984 fp = lr.fp
981 fp = lr.fp
985 except IOError:
982 except IOError:
986 fp = cStringIO.StringIO(lr.fp.read())
983 fp = cStringIO.StringIO(lr.fp.read())
987 gitlr = linereader(fp, lr.textmode)
984 gitlr = linereader(fp, lr.textmode)
988 gitlr.push(firstline)
985 gitlr.push(firstline)
989 (dopatch, gitpatches) = readgitpatch(gitlr)
986 (dopatch, gitpatches) = readgitpatch(gitlr)
990 fp.seek(pos)
987 fp.seek(pos)
991 return dopatch, gitpatches
988 return dopatch, gitpatches
992
989
993 def iterhunks(ui, fp, sourcefile=None):
990 def iterhunks(ui, fp, sourcefile=None):
994 """Read a patch and yield the following events:
991 """Read a patch and yield the following events:
995 - ("file", afile, bfile, firsthunk): select a new target file.
992 - ("file", afile, bfile, firsthunk): select a new target file.
996 - ("hunk", hunk): a new hunk is ready to be applied, follows a
993 - ("hunk", hunk): a new hunk is ready to be applied, follows a
997 "file" event.
994 "file" event.
998 - ("git", gitchanges): current diff is in git format, gitchanges
995 - ("git", gitchanges): current diff is in git format, gitchanges
999 maps filenames to gitpatch records. Unique event.
996 maps filenames to gitpatch records. Unique event.
1000 """
997 """
1001 changed = {}
998 changed = {}
1002 current_hunk = None
999 current_hunk = None
1003 afile = ""
1000 afile = ""
1004 bfile = ""
1001 bfile = ""
1005 state = None
1002 state = None
1006 hunknum = 0
1003 hunknum = 0
1007 emitfile = False
1004 emitfile = False
1008 git = False
1005 git = False
1009
1006
1010 # our states
1007 # our states
1011 BFILE = 1
1008 BFILE = 1
1012 context = None
1009 context = None
1013 lr = linereader(fp)
1010 lr = linereader(fp)
1014 dopatch = True
1011 dopatch = True
1015 # gitworkdone is True if a git operation (copy, rename, ...) was
1012 # gitworkdone is True if a git operation (copy, rename, ...) was
1016 # performed already for the current file. Useful when the file
1013 # performed already for the current file. Useful when the file
1017 # section may have no hunk.
1014 # section may have no hunk.
1018 gitworkdone = False
1015 gitworkdone = False
1019
1016
1020 while True:
1017 while True:
1021 newfile = False
1018 newfile = False
1022 x = lr.readline()
1019 x = lr.readline()
1023 if not x:
1020 if not x:
1024 break
1021 break
1025 if current_hunk:
1022 if current_hunk:
1026 if x.startswith('\ '):
1023 if x.startswith('\ '):
1027 current_hunk.fix_newline()
1024 current_hunk.fix_newline()
1028 yield 'hunk', current_hunk
1025 yield 'hunk', current_hunk
1029 current_hunk = None
1026 current_hunk = None
1030 gitworkdone = False
1027 gitworkdone = False
1031 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1028 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1032 ((context is not False) and x.startswith('***************')))):
1029 ((context is not False) and x.startswith('***************')))):
1033 try:
1030 try:
1034 if context is None and x.startswith('***************'):
1031 if context is None and x.startswith('***************'):
1035 context = True
1032 context = True
1036 gpatch = changed.get(bfile)
1033 gpatch = changed.get(bfile)
1037 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1034 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1038 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1035 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1039 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1036 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1040 except PatchError, err:
1037 except PatchError, err:
1041 ui.debug(err)
1038 ui.debug(err)
1042 current_hunk = None
1039 current_hunk = None
1043 continue
1040 continue
1044 hunknum += 1
1041 hunknum += 1
1045 if emitfile:
1042 if emitfile:
1046 emitfile = False
1043 emitfile = False
1047 yield 'file', (afile, bfile, current_hunk)
1044 yield 'file', (afile, bfile, current_hunk)
1048 elif state == BFILE and x.startswith('GIT binary patch'):
1045 elif state == BFILE and x.startswith('GIT binary patch'):
1049 current_hunk = binhunk(changed[bfile])
1046 current_hunk = binhunk(changed[bfile])
1050 hunknum += 1
1047 hunknum += 1
1051 if emitfile:
1048 if emitfile:
1052 emitfile = False
1049 emitfile = False
1053 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1050 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1054 current_hunk.extract(lr)
1051 current_hunk.extract(lr)
1055 elif x.startswith('diff --git'):
1052 elif x.startswith('diff --git'):
1056 # check for git diff, scanning the whole patch file if needed
1053 # check for git diff, scanning the whole patch file if needed
1057 m = gitre.match(x)
1054 m = gitre.match(x)
1058 if m:
1055 if m:
1059 afile, bfile = m.group(1, 2)
1056 afile, bfile = m.group(1, 2)
1060 if not git:
1057 if not git:
1061 git = True
1058 git = True
1062 dopatch, gitpatches = scangitpatch(lr, x)
1059 dopatch, gitpatches = scangitpatch(lr, x)
1063 yield 'git', gitpatches
1060 yield 'git', gitpatches
1064 for gp in gitpatches:
1061 for gp in gitpatches:
1065 changed[gp.path] = gp
1062 changed[gp.path] = gp
1066 # else error?
1063 # else error?
1067 # copy/rename + modify should modify target, not source
1064 # copy/rename + modify should modify target, not source
1068 gp = changed.get(bfile)
1065 gp = changed.get(bfile)
1069 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
1066 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
1070 afile = bfile
1067 afile = bfile
1071 gitworkdone = True
1068 gitworkdone = True
1072 newfile = True
1069 newfile = True
1073 elif x.startswith('---'):
1070 elif x.startswith('---'):
1074 # check for a unified diff
1071 # check for a unified diff
1075 l2 = lr.readline()
1072 l2 = lr.readline()
1076 if not l2.startswith('+++'):
1073 if not l2.startswith('+++'):
1077 lr.push(l2)
1074 lr.push(l2)
1078 continue
1075 continue
1079 newfile = True
1076 newfile = True
1080 context = False
1077 context = False
1081 afile = parsefilename(x)
1078 afile = parsefilename(x)
1082 bfile = parsefilename(l2)
1079 bfile = parsefilename(l2)
1083 elif x.startswith('***'):
1080 elif x.startswith('***'):
1084 # check for a context diff
1081 # check for a context diff
1085 l2 = lr.readline()
1082 l2 = lr.readline()
1086 if not l2.startswith('---'):
1083 if not l2.startswith('---'):
1087 lr.push(l2)
1084 lr.push(l2)
1088 continue
1085 continue
1089 l3 = lr.readline()
1086 l3 = lr.readline()
1090 lr.push(l3)
1087 lr.push(l3)
1091 if not l3.startswith("***************"):
1088 if not l3.startswith("***************"):
1092 lr.push(l2)
1089 lr.push(l2)
1093 continue
1090 continue
1094 newfile = True
1091 newfile = True
1095 context = True
1092 context = True
1096 afile = parsefilename(x)
1093 afile = parsefilename(x)
1097 bfile = parsefilename(l2)
1094 bfile = parsefilename(l2)
1098
1095
1099 if newfile:
1096 if newfile:
1100 emitfile = True
1097 emitfile = True
1101 state = BFILE
1098 state = BFILE
1102 hunknum = 0
1099 hunknum = 0
1103 if current_hunk:
1100 if current_hunk:
1104 if current_hunk.complete():
1101 if current_hunk.complete():
1105 yield 'hunk', current_hunk
1102 yield 'hunk', current_hunk
1106 else:
1103 else:
1107 raise PatchError(_("malformed patch %s %s") % (afile,
1104 raise PatchError(_("malformed patch %s %s") % (afile,
1108 current_hunk.desc))
1105 current_hunk.desc))
1109
1106
1110 if hunknum == 0 and dopatch and not gitworkdone:
1107 if hunknum == 0 and dopatch and not gitworkdone:
1111 raise NoHunks
1108 raise NoHunks
1112
1109
1113 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1110 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1114 """
1111 """
1115 Reads a patch from fp and tries to apply it.
1112 Reads a patch from fp and tries to apply it.
1116
1113
1117 The dict 'changed' is filled in with all of the filenames changed
1114 The dict 'changed' is filled in with all of the filenames changed
1118 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1115 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1119 found and 1 if there was any fuzz.
1116 found and 1 if there was any fuzz.
1120
1117
1121 If 'eolmode' is 'strict', the patch content and patched file are
1118 If 'eolmode' is 'strict', the patch content and patched file are
1122 read in binary mode. Otherwise, line endings are ignored when
1119 read in binary mode. Otherwise, line endings are ignored when
1123 patching then normalized according to 'eolmode'.
1120 patching then normalized according to 'eolmode'.
1124 """
1121 """
1125 rejects = 0
1122 rejects = 0
1126 err = 0
1123 err = 0
1127 current_file = None
1124 current_file = None
1128 gitpatches = None
1125 gitpatches = None
1129 opener = util.opener(os.getcwd())
1126 opener = util.opener(os.getcwd())
1130
1127
1131 def closefile():
1128 def closefile():
1132 if not current_file:
1129 if not current_file:
1133 return 0
1130 return 0
1134 current_file.close()
1131 current_file.close()
1135 return len(current_file.rej)
1132 return len(current_file.rej)
1136
1133
1137 for state, values in iterhunks(ui, fp, sourcefile):
1134 for state, values in iterhunks(ui, fp, sourcefile):
1138 if state == 'hunk':
1135 if state == 'hunk':
1139 if not current_file:
1136 if not current_file:
1140 continue
1137 continue
1141 current_hunk = values
1138 current_hunk = values
1142 ret = current_file.apply(current_hunk)
1139 ret = current_file.apply(current_hunk)
1143 if ret >= 0:
1140 if ret >= 0:
1144 changed.setdefault(current_file.fname, None)
1141 changed.setdefault(current_file.fname, None)
1145 if ret > 0:
1142 if ret > 0:
1146 err = 1
1143 err = 1
1147 elif state == 'file':
1144 elif state == 'file':
1148 rejects += closefile()
1145 rejects += closefile()
1149 afile, bfile, first_hunk = values
1146 afile, bfile, first_hunk = values
1150 try:
1147 try:
1151 if sourcefile:
1148 if sourcefile:
1152 current_file = patchfile(ui, sourcefile, opener,
1149 current_file = patchfile(ui, sourcefile, opener,
1153 eolmode=eolmode)
1150 eolmode=eolmode)
1154 else:
1151 else:
1155 current_file, missing = selectfile(afile, bfile,
1152 current_file, missing = selectfile(afile, bfile,
1156 first_hunk, strip)
1153 first_hunk, strip)
1157 current_file = patchfile(ui, current_file, opener,
1154 current_file = patchfile(ui, current_file, opener,
1158 missing, eolmode)
1155 missing, eolmode)
1159 except PatchError, err:
1156 except PatchError, err:
1160 ui.warn(str(err) + '\n')
1157 ui.warn(str(err) + '\n')
1161 current_file, current_hunk = None, None
1158 current_file, current_hunk = None, None
1162 rejects += 1
1159 rejects += 1
1163 continue
1160 continue
1164 elif state == 'git':
1161 elif state == 'git':
1165 gitpatches = values
1162 gitpatches = values
1166 cwd = os.getcwd()
1163 cwd = os.getcwd()
1167 for gp in gitpatches:
1164 for gp in gitpatches:
1168 if gp.op in ('COPY', 'RENAME'):
1165 if gp.op in ('COPY', 'RENAME'):
1169 copyfile(gp.oldpath, gp.path, cwd)
1166 copyfile(gp.oldpath, gp.path, cwd)
1170 changed[gp.path] = gp
1167 changed[gp.path] = gp
1171 else:
1168 else:
1172 raise util.Abort(_('unsupported parser state: %s') % state)
1169 raise util.Abort(_('unsupported parser state: %s') % state)
1173
1170
1174 rejects += closefile()
1171 rejects += closefile()
1175
1172
1176 if rejects:
1173 if rejects:
1177 return -1
1174 return -1
1178 return err
1175 return err
1179
1176
1180 def diffopts(ui, opts=None, untrusted=False):
1177 def diffopts(ui, opts=None, untrusted=False):
1181 def get(key, name=None, getter=ui.configbool):
1178 def get(key, name=None, getter=ui.configbool):
1182 return ((opts and opts.get(key)) or
1179 return ((opts and opts.get(key)) or
1183 getter('diff', name or key, None, untrusted=untrusted))
1180 getter('diff', name or key, None, untrusted=untrusted))
1184 return mdiff.diffopts(
1181 return mdiff.diffopts(
1185 text=opts and opts.get('text'),
1182 text=opts and opts.get('text'),
1186 git=get('git'),
1183 git=get('git'),
1187 nodates=get('nodates'),
1184 nodates=get('nodates'),
1188 showfunc=get('show_function', 'showfunc'),
1185 showfunc=get('show_function', 'showfunc'),
1189 ignorews=get('ignore_all_space', 'ignorews'),
1186 ignorews=get('ignore_all_space', 'ignorews'),
1190 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1187 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1191 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1188 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1192 context=get('unified', getter=ui.config))
1189 context=get('unified', getter=ui.config))
1193
1190
1194 def updatedir(ui, repo, patches, similarity=0):
1191 def updatedir(ui, repo, patches, similarity=0):
1195 '''Update dirstate after patch application according to metadata'''
1192 '''Update dirstate after patch application according to metadata'''
1196 if not patches:
1193 if not patches:
1197 return
1194 return
1198 copies = []
1195 copies = []
1199 removes = set()
1196 removes = set()
1200 cfiles = patches.keys()
1197 cfiles = patches.keys()
1201 cwd = repo.getcwd()
1198 cwd = repo.getcwd()
1202 if cwd:
1199 if cwd:
1203 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1200 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1204 for f in patches:
1201 for f in patches:
1205 gp = patches[f]
1202 gp = patches[f]
1206 if not gp:
1203 if not gp:
1207 continue
1204 continue
1208 if gp.op == 'RENAME':
1205 if gp.op == 'RENAME':
1209 copies.append((gp.oldpath, gp.path))
1206 copies.append((gp.oldpath, gp.path))
1210 removes.add(gp.oldpath)
1207 removes.add(gp.oldpath)
1211 elif gp.op == 'COPY':
1208 elif gp.op == 'COPY':
1212 copies.append((gp.oldpath, gp.path))
1209 copies.append((gp.oldpath, gp.path))
1213 elif gp.op == 'DELETE':
1210 elif gp.op == 'DELETE':
1214 removes.add(gp.path)
1211 removes.add(gp.path)
1215 for src, dst in copies:
1212 for src, dst in copies:
1216 repo.copy(src, dst)
1213 repo.copy(src, dst)
1217 if (not similarity) and removes:
1214 if (not similarity) and removes:
1218 repo.remove(sorted(removes), True)
1215 repo.remove(sorted(removes), True)
1219 for f in patches:
1216 for f in patches:
1220 gp = patches[f]
1217 gp = patches[f]
1221 if gp and gp.mode:
1218 if gp and gp.mode:
1222 islink, isexec = gp.mode
1219 islink, isexec = gp.mode
1223 dst = repo.wjoin(gp.path)
1220 dst = repo.wjoin(gp.path)
1224 # patch won't create empty files
1221 # patch won't create empty files
1225 if gp.op == 'ADD' and not os.path.exists(dst):
1222 if gp.op == 'ADD' and not os.path.exists(dst):
1226 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1223 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1227 repo.wwrite(gp.path, '', flags)
1224 repo.wwrite(gp.path, '', flags)
1228 elif gp.op != 'DELETE':
1225 elif gp.op != 'DELETE':
1229 util.set_flags(dst, islink, isexec)
1226 util.set_flags(dst, islink, isexec)
1230 cmdutil.addremove(repo, cfiles, similarity=similarity)
1227 cmdutil.addremove(repo, cfiles, similarity=similarity)
1231 files = patches.keys()
1228 files = patches.keys()
1232 files.extend([r for r in removes if r not in files])
1229 files.extend([r for r in removes if r not in files])
1233 return sorted(files)
1230 return sorted(files)
1234
1231
1235 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1232 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1236 """use <patcher> to apply <patchname> to the working directory.
1233 """use <patcher> to apply <patchname> to the working directory.
1237 returns whether patch was applied with fuzz factor."""
1234 returns whether patch was applied with fuzz factor."""
1238
1235
1239 fuzz = False
1236 fuzz = False
1240 if cwd:
1237 if cwd:
1241 args.append('-d %s' % util.shellquote(cwd))
1238 args.append('-d %s' % util.shellquote(cwd))
1242 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1239 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1243 util.shellquote(patchname)))
1240 util.shellquote(patchname)))
1244
1241
1245 for line in fp:
1242 for line in fp:
1246 line = line.rstrip()
1243 line = line.rstrip()
1247 ui.note(line + '\n')
1244 ui.note(line + '\n')
1248 if line.startswith('patching file '):
1245 if line.startswith('patching file '):
1249 pf = util.parse_patch_output(line)
1246 pf = util.parse_patch_output(line)
1250 printed_file = False
1247 printed_file = False
1251 files.setdefault(pf, None)
1248 files.setdefault(pf, None)
1252 elif line.find('with fuzz') >= 0:
1249 elif line.find('with fuzz') >= 0:
1253 fuzz = True
1250 fuzz = True
1254 if not printed_file:
1251 if not printed_file:
1255 ui.warn(pf + '\n')
1252 ui.warn(pf + '\n')
1256 printed_file = True
1253 printed_file = True
1257 ui.warn(line + '\n')
1254 ui.warn(line + '\n')
1258 elif line.find('saving rejects to file') >= 0:
1255 elif line.find('saving rejects to file') >= 0:
1259 ui.warn(line + '\n')
1256 ui.warn(line + '\n')
1260 elif line.find('FAILED') >= 0:
1257 elif line.find('FAILED') >= 0:
1261 if not printed_file:
1258 if not printed_file:
1262 ui.warn(pf + '\n')
1259 ui.warn(pf + '\n')
1263 printed_file = True
1260 printed_file = True
1264 ui.warn(line + '\n')
1261 ui.warn(line + '\n')
1265 code = fp.close()
1262 code = fp.close()
1266 if code:
1263 if code:
1267 raise PatchError(_("patch command failed: %s") %
1264 raise PatchError(_("patch command failed: %s") %
1268 util.explain_exit(code)[0])
1265 util.explain_exit(code)[0])
1269 return fuzz
1266 return fuzz
1270
1267
1271 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1268 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1272 """use builtin patch to apply <patchobj> to the working directory.
1269 """use builtin patch to apply <patchobj> to the working directory.
1273 returns whether patch was applied with fuzz factor."""
1270 returns whether patch was applied with fuzz factor."""
1274
1271
1275 if files is None:
1272 if files is None:
1276 files = {}
1273 files = {}
1277 if eolmode is None:
1274 if eolmode is None:
1278 eolmode = ui.config('patch', 'eol', 'strict')
1275 eolmode = ui.config('patch', 'eol', 'strict')
1279 if eolmode.lower() not in eolmodes:
1276 if eolmode.lower() not in eolmodes:
1280 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1277 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1281 eolmode = eolmode.lower()
1278 eolmode = eolmode.lower()
1282
1279
1283 try:
1280 try:
1284 fp = open(patchobj, 'rb')
1281 fp = open(patchobj, 'rb')
1285 except TypeError:
1282 except TypeError:
1286 fp = patchobj
1283 fp = patchobj
1287 if cwd:
1284 if cwd:
1288 curdir = os.getcwd()
1285 curdir = os.getcwd()
1289 os.chdir(cwd)
1286 os.chdir(cwd)
1290 try:
1287 try:
1291 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1288 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1292 finally:
1289 finally:
1293 if cwd:
1290 if cwd:
1294 os.chdir(curdir)
1291 os.chdir(curdir)
1295 if fp != patchobj:
1292 if fp != patchobj:
1296 fp.close()
1293 fp.close()
1297 if ret < 0:
1294 if ret < 0:
1298 raise PatchError
1295 raise PatchError
1299 return ret > 0
1296 return ret > 0
1300
1297
1301 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1298 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1302 """Apply <patchname> to the working directory.
1299 """Apply <patchname> to the working directory.
1303
1300
1304 'eolmode' specifies how end of lines should be handled. It can be:
1301 'eolmode' specifies how end of lines should be handled. It can be:
1305 - 'strict': inputs are read in binary mode, EOLs are preserved
1302 - 'strict': inputs are read in binary mode, EOLs are preserved
1306 - 'crlf': EOLs are ignored when patching and reset to CRLF
1303 - 'crlf': EOLs are ignored when patching and reset to CRLF
1307 - 'lf': EOLs are ignored when patching and reset to LF
1304 - 'lf': EOLs are ignored when patching and reset to LF
1308 - None: get it from user settings, default to 'strict'
1305 - None: get it from user settings, default to 'strict'
1309 'eolmode' is ignored when using an external patcher program.
1306 'eolmode' is ignored when using an external patcher program.
1310
1307
1311 Returns whether patch was applied with fuzz factor.
1308 Returns whether patch was applied with fuzz factor.
1312 """
1309 """
1313 patcher = ui.config('ui', 'patch')
1310 patcher = ui.config('ui', 'patch')
1314 args = []
1311 args = []
1315 if files is None:
1312 if files is None:
1316 files = {}
1313 files = {}
1317 try:
1314 try:
1318 if patcher:
1315 if patcher:
1319 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1316 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1320 files)
1317 files)
1321 else:
1318 else:
1322 try:
1319 try:
1323 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1320 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1324 except NoHunks:
1321 except NoHunks:
1325 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1322 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1326 or 'patch')
1323 or 'patch')
1327 ui.debug('no valid hunks found; trying with %r instead\n' %
1324 ui.debug('no valid hunks found; trying with %r instead\n' %
1328 patcher)
1325 patcher)
1329 if util.needbinarypatch():
1326 if util.needbinarypatch():
1330 args.append('--binary')
1327 args.append('--binary')
1331 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1328 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1332 files)
1329 files)
1333 except PatchError, err:
1330 except PatchError, err:
1334 s = str(err)
1331 s = str(err)
1335 if s:
1332 if s:
1336 raise util.Abort(s)
1333 raise util.Abort(s)
1337 else:
1334 else:
1338 raise util.Abort(_('patch failed to apply'))
1335 raise util.Abort(_('patch failed to apply'))
1339
1336
1340 def b85diff(to, tn):
1337 def b85diff(to, tn):
1341 '''print base85-encoded binary diff'''
1338 '''print base85-encoded binary diff'''
1342 def gitindex(text):
1339 def gitindex(text):
1343 if not text:
1340 if not text:
1344 return '0' * 40
1341 return '0' * 40
1345 l = len(text)
1342 l = len(text)
1346 s = util.sha1('blob %d\0' % l)
1343 s = util.sha1('blob %d\0' % l)
1347 s.update(text)
1344 s.update(text)
1348 return s.hexdigest()
1345 return s.hexdigest()
1349
1346
1350 def fmtline(line):
1347 def fmtline(line):
1351 l = len(line)
1348 l = len(line)
1352 if l <= 26:
1349 if l <= 26:
1353 l = chr(ord('A') + l - 1)
1350 l = chr(ord('A') + l - 1)
1354 else:
1351 else:
1355 l = chr(l - 26 + ord('a') - 1)
1352 l = chr(l - 26 + ord('a') - 1)
1356 return '%c%s\n' % (l, base85.b85encode(line, True))
1353 return '%c%s\n' % (l, base85.b85encode(line, True))
1357
1354
1358 def chunk(text, csize=52):
1355 def chunk(text, csize=52):
1359 l = len(text)
1356 l = len(text)
1360 i = 0
1357 i = 0
1361 while i < l:
1358 while i < l:
1362 yield text[i:i + csize]
1359 yield text[i:i + csize]
1363 i += csize
1360 i += csize
1364
1361
1365 tohash = gitindex(to)
1362 tohash = gitindex(to)
1366 tnhash = gitindex(tn)
1363 tnhash = gitindex(tn)
1367 if tohash == tnhash:
1364 if tohash == tnhash:
1368 return ""
1365 return ""
1369
1366
1370 # TODO: deltas
1367 # TODO: deltas
1371 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1368 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1372 (tohash, tnhash, len(tn))]
1369 (tohash, tnhash, len(tn))]
1373 for l in chunk(zlib.compress(tn)):
1370 for l in chunk(zlib.compress(tn)):
1374 ret.append(fmtline(l))
1371 ret.append(fmtline(l))
1375 ret.append('\n')
1372 ret.append('\n')
1376 return ''.join(ret)
1373 return ''.join(ret)
1377
1374
1378 class GitDiffRequired(Exception):
1375 class GitDiffRequired(Exception):
1379 pass
1376 pass
1380
1377
1381 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1378 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1382 losedatafn=None):
1379 losedatafn=None):
1383 '''yields diff of changes to files between two nodes, or node and
1380 '''yields diff of changes to files between two nodes, or node and
1384 working directory.
1381 working directory.
1385
1382
1386 if node1 is None, use first dirstate parent instead.
1383 if node1 is None, use first dirstate parent instead.
1387 if node2 is None, compare node1 with working directory.
1384 if node2 is None, compare node1 with working directory.
1388
1385
1389 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1386 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1390 every time some change cannot be represented with the current
1387 every time some change cannot be represented with the current
1391 patch format. Return False to upgrade to git patch format, True to
1388 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
1389 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
1390 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
1391 to None, patches will always be upgraded to git format when
1395 necessary.
1392 necessary.
1396 '''
1393 '''
1397
1394
1398 if opts is None:
1395 if opts is None:
1399 opts = mdiff.defaultopts
1396 opts = mdiff.defaultopts
1400
1397
1401 if not node1 and not node2:
1398 if not node1 and not node2:
1402 node1 = repo.dirstate.parents()[0]
1399 node1 = repo.dirstate.parents()[0]
1403
1400
1404 def lrugetfilectx():
1401 def lrugetfilectx():
1405 cache = {}
1402 cache = {}
1406 order = []
1403 order = []
1407 def getfilectx(f, ctx):
1404 def getfilectx(f, ctx):
1408 fctx = ctx.filectx(f, filelog=cache.get(f))
1405 fctx = ctx.filectx(f, filelog=cache.get(f))
1409 if f not in cache:
1406 if f not in cache:
1410 if len(cache) > 20:
1407 if len(cache) > 20:
1411 del cache[order.pop(0)]
1408 del cache[order.pop(0)]
1412 cache[f] = fctx.filelog()
1409 cache[f] = fctx.filelog()
1413 else:
1410 else:
1414 order.remove(f)
1411 order.remove(f)
1415 order.append(f)
1412 order.append(f)
1416 return fctx
1413 return fctx
1417 return getfilectx
1414 return getfilectx
1418 getfilectx = lrugetfilectx()
1415 getfilectx = lrugetfilectx()
1419
1416
1420 ctx1 = repo[node1]
1417 ctx1 = repo[node1]
1421 ctx2 = repo[node2]
1418 ctx2 = repo[node2]
1422
1419
1423 if not changes:
1420 if not changes:
1424 changes = repo.status(ctx1, ctx2, match=match)
1421 changes = repo.status(ctx1, ctx2, match=match)
1425 modified, added, removed = changes[:3]
1422 modified, added, removed = changes[:3]
1426
1423
1427 if not modified and not added and not removed:
1424 if not modified and not added and not removed:
1428 return []
1425 return []
1429
1426
1430 revs = None
1427 revs = None
1431 if not repo.ui.quiet:
1428 if not repo.ui.quiet:
1432 hexfunc = repo.ui.debugflag and hex or short
1429 hexfunc = repo.ui.debugflag and hex or short
1433 revs = [hexfunc(node) for node in [node1, node2] if node]
1430 revs = [hexfunc(node) for node in [node1, node2] if node]
1434
1431
1435 copy = {}
1432 copy = {}
1436 if opts.git or opts.upgrade:
1433 if opts.git or opts.upgrade:
1437 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1434 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1438
1435
1439 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1436 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1440 modified, added, removed, copy, getfilectx, opts, losedata)
1437 modified, added, removed, copy, getfilectx, opts, losedata)
1441 if opts.upgrade and not opts.git:
1438 if opts.upgrade and not opts.git:
1442 try:
1439 try:
1443 def losedata(fn):
1440 def losedata(fn):
1444 if not losedatafn or not losedatafn(fn=fn):
1441 if not losedatafn or not losedatafn(fn=fn):
1445 raise GitDiffRequired()
1442 raise GitDiffRequired()
1446 # Buffer the whole output until we are sure it can be generated
1443 # Buffer the whole output until we are sure it can be generated
1447 return list(difffn(opts.copy(git=False), losedata))
1444 return list(difffn(opts.copy(git=False), losedata))
1448 except GitDiffRequired:
1445 except GitDiffRequired:
1449 return difffn(opts.copy(git=True), None)
1446 return difffn(opts.copy(git=True), None)
1450 else:
1447 else:
1451 return difffn(opts, None)
1448 return difffn(opts, None)
1452
1449
1453 def _addmodehdr(header, omode, nmode):
1450 def _addmodehdr(header, omode, nmode):
1454 if omode != nmode:
1451 if omode != nmode:
1455 header.append('old mode %s\n' % omode)
1452 header.append('old mode %s\n' % omode)
1456 header.append('new mode %s\n' % nmode)
1453 header.append('new mode %s\n' % nmode)
1457
1454
1458 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1455 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1459 copy, getfilectx, opts, losedatafn):
1456 copy, getfilectx, opts, losedatafn):
1460
1457
1461 date1 = util.datestr(ctx1.date())
1458 date1 = util.datestr(ctx1.date())
1462 man1 = ctx1.manifest()
1459 man1 = ctx1.manifest()
1463
1460
1464 gone = set()
1461 gone = set()
1465 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1462 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1466
1463
1467 copyto = dict([(v, k) for k, v in copy.items()])
1464 copyto = dict([(v, k) for k, v in copy.items()])
1468
1465
1469 if opts.git:
1466 if opts.git:
1470 revs = None
1467 revs = None
1471
1468
1472 for f in sorted(modified + added + removed):
1469 for f in sorted(modified + added + removed):
1473 to = None
1470 to = None
1474 tn = None
1471 tn = None
1475 dodiff = True
1472 dodiff = True
1476 header = []
1473 header = []
1477 if f in man1:
1474 if f in man1:
1478 to = getfilectx(f, ctx1).data()
1475 to = getfilectx(f, ctx1).data()
1479 if f not in removed:
1476 if f not in removed:
1480 tn = getfilectx(f, ctx2).data()
1477 tn = getfilectx(f, ctx2).data()
1481 a, b = f, f
1478 a, b = f, f
1482 if opts.git or losedatafn:
1479 if opts.git or losedatafn:
1483 if f in added:
1480 if f in added:
1484 mode = gitmode[ctx2.flags(f)]
1481 mode = gitmode[ctx2.flags(f)]
1485 if f in copy or f in copyto:
1482 if f in copy or f in copyto:
1486 if opts.git:
1483 if opts.git:
1487 if f in copy:
1484 if f in copy:
1488 a = copy[f]
1485 a = copy[f]
1489 else:
1486 else:
1490 a = copyto[f]
1487 a = copyto[f]
1491 omode = gitmode[man1.flags(a)]
1488 omode = gitmode[man1.flags(a)]
1492 _addmodehdr(header, omode, mode)
1489 _addmodehdr(header, omode, mode)
1493 if a in removed and a not in gone:
1490 if a in removed and a not in gone:
1494 op = 'rename'
1491 op = 'rename'
1495 gone.add(a)
1492 gone.add(a)
1496 else:
1493 else:
1497 op = 'copy'
1494 op = 'copy'
1498 header.append('%s from %s\n' % (op, a))
1495 header.append('%s from %s\n' % (op, a))
1499 header.append('%s to %s\n' % (op, f))
1496 header.append('%s to %s\n' % (op, f))
1500 to = getfilectx(a, ctx1).data()
1497 to = getfilectx(a, ctx1).data()
1501 else:
1498 else:
1502 losedatafn(f)
1499 losedatafn(f)
1503 else:
1500 else:
1504 if opts.git:
1501 if opts.git:
1505 header.append('new file mode %s\n' % mode)
1502 header.append('new file mode %s\n' % mode)
1506 elif ctx2.flags(f):
1503 elif ctx2.flags(f):
1507 losedatafn(f)
1504 losedatafn(f)
1508 if util.binary(tn):
1505 if util.binary(tn):
1509 if opts.git:
1506 if opts.git:
1510 dodiff = 'binary'
1507 dodiff = 'binary'
1511 else:
1508 else:
1512 losedatafn(f)
1509 losedatafn(f)
1513 if not opts.git and not tn:
1510 if not opts.git and not tn:
1514 # regular diffs cannot represent new empty file
1511 # regular diffs cannot represent new empty file
1515 losedatafn(f)
1512 losedatafn(f)
1516 elif f in removed:
1513 elif f in removed:
1517 if opts.git:
1514 if opts.git:
1518 # have we already reported a copy above?
1515 # have we already reported a copy above?
1519 if ((f in copy and copy[f] in added
1516 if ((f in copy and copy[f] in added
1520 and copyto[copy[f]] == f) or
1517 and copyto[copy[f]] == f) or
1521 (f in copyto and copyto[f] in added
1518 (f in copyto and copyto[f] in added
1522 and copy[copyto[f]] == f)):
1519 and copy[copyto[f]] == f)):
1523 dodiff = False
1520 dodiff = False
1524 else:
1521 else:
1525 header.append('deleted file mode %s\n' %
1522 header.append('deleted file mode %s\n' %
1526 gitmode[man1.flags(f)])
1523 gitmode[man1.flags(f)])
1527 elif not to:
1524 elif not to:
1528 # regular diffs cannot represent empty file deletion
1525 # regular diffs cannot represent empty file deletion
1529 losedatafn(f)
1526 losedatafn(f)
1530 else:
1527 else:
1531 oflag = man1.flags(f)
1528 oflag = man1.flags(f)
1532 nflag = ctx2.flags(f)
1529 nflag = ctx2.flags(f)
1533 binary = util.binary(to) or util.binary(tn)
1530 binary = util.binary(to) or util.binary(tn)
1534 if opts.git:
1531 if opts.git:
1535 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1532 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1536 if binary:
1533 if binary:
1537 dodiff = 'binary'
1534 dodiff = 'binary'
1538 elif binary or nflag != oflag:
1535 elif binary or nflag != oflag:
1539 losedatafn(f)
1536 losedatafn(f)
1540 if opts.git:
1537 if opts.git:
1541 header.insert(0, mdiff.diffline(revs, a, b, opts))
1538 header.insert(0, mdiff.diffline(revs, a, b, opts))
1542
1539
1543 if dodiff:
1540 if dodiff:
1544 if dodiff == 'binary':
1541 if dodiff == 'binary':
1545 text = b85diff(to, tn)
1542 text = b85diff(to, tn)
1546 else:
1543 else:
1547 text = mdiff.unidiff(to, date1,
1544 text = mdiff.unidiff(to, date1,
1548 # ctx2 date may be dynamic
1545 # ctx2 date may be dynamic
1549 tn, util.datestr(ctx2.date()),
1546 tn, util.datestr(ctx2.date()),
1550 a, b, revs, opts=opts)
1547 a, b, revs, opts=opts)
1551 if header and (text or len(header) > 1):
1548 if header and (text or len(header) > 1):
1552 yield ''.join(header)
1549 yield ''.join(header)
1553 if text:
1550 if text:
1554 yield text
1551 yield text
1555
1552
1556 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1553 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1557 opts=None):
1554 opts=None):
1558 '''export changesets as hg patches.'''
1555 '''export changesets as hg patches.'''
1559
1556
1560 total = len(revs)
1557 total = len(revs)
1561 revwidth = max([len(str(rev)) for rev in revs])
1558 revwidth = max([len(str(rev)) for rev in revs])
1562
1559
1563 def single(rev, seqno, fp):
1560 def single(rev, seqno, fp):
1564 ctx = repo[rev]
1561 ctx = repo[rev]
1565 node = ctx.node()
1562 node = ctx.node()
1566 parents = [p.node() for p in ctx.parents() if p]
1563 parents = [p.node() for p in ctx.parents() if p]
1567 branch = ctx.branch()
1564 branch = ctx.branch()
1568 if switch_parent:
1565 if switch_parent:
1569 parents.reverse()
1566 parents.reverse()
1570 prev = (parents and parents[0]) or nullid
1567 prev = (parents and parents[0]) or nullid
1571
1568
1572 if not fp:
1569 if not fp:
1573 fp = cmdutil.make_file(repo, template, node, total=total,
1570 fp = cmdutil.make_file(repo, template, node, total=total,
1574 seqno=seqno, revwidth=revwidth,
1571 seqno=seqno, revwidth=revwidth,
1575 mode='ab')
1572 mode='ab')
1576 if fp != sys.stdout and hasattr(fp, 'name'):
1573 if fp != sys.stdout and hasattr(fp, 'name'):
1577 repo.ui.note("%s\n" % fp.name)
1574 repo.ui.note("%s\n" % fp.name)
1578
1575
1579 fp.write("# HG changeset patch\n")
1576 fp.write("# HG changeset patch\n")
1580 fp.write("# User %s\n" % ctx.user())
1577 fp.write("# User %s\n" % ctx.user())
1581 fp.write("# Date %d %d\n" % ctx.date())
1578 fp.write("# Date %d %d\n" % ctx.date())
1582 if branch and (branch != 'default'):
1579 if branch and (branch != 'default'):
1583 fp.write("# Branch %s\n" % branch)
1580 fp.write("# Branch %s\n" % branch)
1584 fp.write("# Node ID %s\n" % hex(node))
1581 fp.write("# Node ID %s\n" % hex(node))
1585 fp.write("# Parent %s\n" % hex(prev))
1582 fp.write("# Parent %s\n" % hex(prev))
1586 if len(parents) > 1:
1583 if len(parents) > 1:
1587 fp.write("# Parent %s\n" % hex(parents[1]))
1584 fp.write("# Parent %s\n" % hex(parents[1]))
1588 fp.write(ctx.description().rstrip())
1585 fp.write(ctx.description().rstrip())
1589 fp.write("\n\n")
1586 fp.write("\n\n")
1590
1587
1591 for chunk in diff(repo, prev, node, opts=opts):
1588 for chunk in diff(repo, prev, node, opts=opts):
1592 fp.write(chunk)
1589 fp.write(chunk)
1593
1590
1594 for seqno, rev in enumerate(revs):
1591 for seqno, rev in enumerate(revs):
1595 single(rev, seqno + 1, fp)
1592 single(rev, seqno + 1, fp)
1596
1593
1597 def diffstatdata(lines):
1594 def diffstatdata(lines):
1598 filename, adds, removes = None, 0, 0
1595 filename, adds, removes = None, 0, 0
1599 for line in lines:
1596 for line in lines:
1600 if line.startswith('diff'):
1597 if line.startswith('diff'):
1601 if filename:
1598 if filename:
1602 isbinary = adds == 0 and removes == 0
1599 isbinary = adds == 0 and removes == 0
1603 yield (filename, adds, removes, isbinary)
1600 yield (filename, adds, removes, isbinary)
1604 # set numbers to 0 anyway when starting new file
1601 # set numbers to 0 anyway when starting new file
1605 adds, removes = 0, 0
1602 adds, removes = 0, 0
1606 if line.startswith('diff --git'):
1603 if line.startswith('diff --git'):
1607 filename = gitre.search(line).group(1)
1604 filename = gitre.search(line).group(1)
1608 else:
1605 else:
1609 # format: "diff -r ... -r ... filename"
1606 # format: "diff -r ... -r ... filename"
1610 filename = line.split(None, 5)[-1]
1607 filename = line.split(None, 5)[-1]
1611 elif line.startswith('+') and not line.startswith('+++'):
1608 elif line.startswith('+') and not line.startswith('+++'):
1612 adds += 1
1609 adds += 1
1613 elif line.startswith('-') and not line.startswith('---'):
1610 elif line.startswith('-') and not line.startswith('---'):
1614 removes += 1
1611 removes += 1
1615 if filename:
1612 if filename:
1616 isbinary = adds == 0 and removes == 0
1613 isbinary = adds == 0 and removes == 0
1617 yield (filename, adds, removes, isbinary)
1614 yield (filename, adds, removes, isbinary)
1618
1615
1619 def diffstat(lines, width=80, git=False):
1616 def diffstat(lines, width=80, git=False):
1620 output = []
1617 output = []
1621 stats = list(diffstatdata(lines))
1618 stats = list(diffstatdata(lines))
1622
1619
1623 maxtotal, maxname = 0, 0
1620 maxtotal, maxname = 0, 0
1624 totaladds, totalremoves = 0, 0
1621 totaladds, totalremoves = 0, 0
1625 hasbinary = False
1622 hasbinary = False
1626 for filename, adds, removes, isbinary in stats:
1623 for filename, adds, removes, isbinary in stats:
1627 totaladds += adds
1624 totaladds += adds
1628 totalremoves += removes
1625 totalremoves += removes
1629 maxname = max(maxname, len(filename))
1626 maxname = max(maxname, len(filename))
1630 maxtotal = max(maxtotal, adds + removes)
1627 maxtotal = max(maxtotal, adds + removes)
1631 if isbinary:
1628 if isbinary:
1632 hasbinary = True
1629 hasbinary = True
1633
1630
1634 countwidth = len(str(maxtotal))
1631 countwidth = len(str(maxtotal))
1635 if hasbinary and countwidth < 3:
1632 if hasbinary and countwidth < 3:
1636 countwidth = 3
1633 countwidth = 3
1637 graphwidth = width - countwidth - maxname - 6
1634 graphwidth = width - countwidth - maxname - 6
1638 if graphwidth < 10:
1635 if graphwidth < 10:
1639 graphwidth = 10
1636 graphwidth = 10
1640
1637
1641 def scale(i):
1638 def scale(i):
1642 if maxtotal <= graphwidth:
1639 if maxtotal <= graphwidth:
1643 return i
1640 return i
1644 # If diffstat runs out of room it doesn't print anything,
1641 # If diffstat runs out of room it doesn't print anything,
1645 # which isn't very useful, so always print at least one + or -
1642 # which isn't very useful, so always print at least one + or -
1646 # if there were at least some changes.
1643 # if there were at least some changes.
1647 return max(i * graphwidth // maxtotal, int(bool(i)))
1644 return max(i * graphwidth // maxtotal, int(bool(i)))
1648
1645
1649 for filename, adds, removes, isbinary in stats:
1646 for filename, adds, removes, isbinary in stats:
1650 if git and isbinary:
1647 if git and isbinary:
1651 count = 'Bin'
1648 count = 'Bin'
1652 else:
1649 else:
1653 count = adds + removes
1650 count = adds + removes
1654 pluses = '+' * scale(adds)
1651 pluses = '+' * scale(adds)
1655 minuses = '-' * scale(removes)
1652 minuses = '-' * scale(removes)
1656 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1653 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1657 count, pluses, minuses))
1654 count, pluses, minuses))
1658
1655
1659 if stats:
1656 if stats:
1660 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1657 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1661 % (len(stats), totaladds, totalremoves))
1658 % (len(stats), totaladds, totalremoves))
1662
1659
1663 return ''.join(output)
1660 return ''.join(output)
General Comments 0
You need to be logged in to leave comments. Login now