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