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