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