##// END OF EJS Templates
diffstat: fix parsing of filenames with spaces...
Gastón Kleiman -
r13395:104c9ed9 default
parent child Browse files
Show More
@@ -1,1622 +1,1624 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, errno, re
9 import cStringIO, email.Parser, os, errno, 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 st_mode = None
432 st_mode = None
433 islink = os.path.islink(fname)
433 islink = os.path.islink(fname)
434 if islink:
434 if islink:
435 fp = cStringIO.StringIO()
435 fp = cStringIO.StringIO()
436 else:
436 else:
437 try:
437 try:
438 st_mode = os.lstat(fname).st_mode & 0777
438 st_mode = os.lstat(fname).st_mode & 0777
439 except OSError, e:
439 except OSError, e:
440 if e.errno != errno.ENOENT:
440 if e.errno != errno.ENOENT:
441 raise
441 raise
442 fp = self.opener(fname, 'w')
442 fp = self.opener(fname, 'w')
443 try:
443 try:
444 if self.eolmode == 'auto':
444 if self.eolmode == 'auto':
445 eol = self.eol
445 eol = self.eol
446 elif self.eolmode == 'crlf':
446 elif self.eolmode == 'crlf':
447 eol = '\r\n'
447 eol = '\r\n'
448 else:
448 else:
449 eol = '\n'
449 eol = '\n'
450
450
451 if self.eolmode != 'strict' and eol and eol != '\n':
451 if self.eolmode != 'strict' and eol and eol != '\n':
452 for l in lines:
452 for l in lines:
453 if l and l[-1] == '\n':
453 if l and l[-1] == '\n':
454 l = l[:-1] + eol
454 l = l[:-1] + eol
455 fp.write(l)
455 fp.write(l)
456 else:
456 else:
457 fp.writelines(lines)
457 fp.writelines(lines)
458 if islink:
458 if islink:
459 self.opener.symlink(fp.getvalue(), fname)
459 self.opener.symlink(fp.getvalue(), fname)
460 if st_mode is not None:
460 if st_mode is not None:
461 os.chmod(fname, st_mode)
461 os.chmod(fname, st_mode)
462 finally:
462 finally:
463 fp.close()
463 fp.close()
464
464
465 def unlink(self, fname):
465 def unlink(self, fname):
466 os.unlink(fname)
466 os.unlink(fname)
467
467
468 def printfile(self, warn):
468 def printfile(self, warn):
469 if self.fileprinted:
469 if self.fileprinted:
470 return
470 return
471 if warn or self.ui.verbose:
471 if warn or self.ui.verbose:
472 self.fileprinted = True
472 self.fileprinted = True
473 s = _("patching file %s\n") % self.fname
473 s = _("patching file %s\n") % self.fname
474 if warn:
474 if warn:
475 self.ui.warn(s)
475 self.ui.warn(s)
476 else:
476 else:
477 self.ui.note(s)
477 self.ui.note(s)
478
478
479
479
480 def findlines(self, l, linenum):
480 def findlines(self, l, linenum):
481 # looks through the hash and finds candidate lines. The
481 # looks through the hash and finds candidate lines. The
482 # result is a list of line numbers sorted based on distance
482 # result is a list of line numbers sorted based on distance
483 # from linenum
483 # from linenum
484
484
485 cand = self.hash.get(l, [])
485 cand = self.hash.get(l, [])
486 if len(cand) > 1:
486 if len(cand) > 1:
487 # resort our list of potentials forward then back.
487 # resort our list of potentials forward then back.
488 cand.sort(key=lambda x: abs(x - linenum))
488 cand.sort(key=lambda x: abs(x - linenum))
489 return cand
489 return cand
490
490
491 def hashlines(self):
491 def hashlines(self):
492 self.hash = {}
492 self.hash = {}
493 for x, s in enumerate(self.lines):
493 for x, s in enumerate(self.lines):
494 self.hash.setdefault(s, []).append(x)
494 self.hash.setdefault(s, []).append(x)
495
495
496 def makerejlines(self, fname):
496 def makerejlines(self, fname):
497 base = os.path.basename(fname)
497 base = os.path.basename(fname)
498 yield "--- %s\n+++ %s\n" % (base, base)
498 yield "--- %s\n+++ %s\n" % (base, base)
499 for x in self.rej:
499 for x in self.rej:
500 for l in x.hunk:
500 for l in x.hunk:
501 yield l
501 yield l
502 if l[-1] != '\n':
502 if l[-1] != '\n':
503 yield "\n\ No newline at end of file\n"
503 yield "\n\ No newline at end of file\n"
504
504
505 def write_rej(self):
505 def write_rej(self):
506 # our rejects are a little different from patch(1). This always
506 # our rejects are a little different from patch(1). This always
507 # creates rejects in the same form as the original patch. A file
507 # creates rejects in the same form as the original patch. A file
508 # header is inserted so that you can run the reject through patch again
508 # header is inserted so that you can run the reject through patch again
509 # without having to type the filename.
509 # without having to type the filename.
510
510
511 if not self.rej:
511 if not self.rej:
512 return
512 return
513
513
514 fname = self.fname + ".rej"
514 fname = self.fname + ".rej"
515 self.ui.warn(
515 self.ui.warn(
516 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
516 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
517 (len(self.rej), self.hunks, fname))
517 (len(self.rej), self.hunks, fname))
518
518
519 fp = self.opener(fname, 'w')
519 fp = self.opener(fname, 'w')
520 fp.writelines(self.makerejlines(self.fname))
520 fp.writelines(self.makerejlines(self.fname))
521 fp.close()
521 fp.close()
522
522
523 def apply(self, h):
523 def apply(self, h):
524 if not h.complete():
524 if not h.complete():
525 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
525 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
526 (h.number, h.desc, len(h.a), h.lena, len(h.b),
526 (h.number, h.desc, len(h.a), h.lena, len(h.b),
527 h.lenb))
527 h.lenb))
528
528
529 self.hunks += 1
529 self.hunks += 1
530
530
531 if self.missing:
531 if self.missing:
532 self.rej.append(h)
532 self.rej.append(h)
533 return -1
533 return -1
534
534
535 if self.exists and h.createfile():
535 if self.exists and h.createfile():
536 self.ui.warn(_("file %s already exists\n") % self.fname)
536 self.ui.warn(_("file %s already exists\n") % self.fname)
537 self.rej.append(h)
537 self.rej.append(h)
538 return -1
538 return -1
539
539
540 if isinstance(h, binhunk):
540 if isinstance(h, binhunk):
541 if h.rmfile():
541 if h.rmfile():
542 self.unlink(self.fname)
542 self.unlink(self.fname)
543 else:
543 else:
544 self.lines[:] = h.new()
544 self.lines[:] = h.new()
545 self.offset += len(h.new())
545 self.offset += len(h.new())
546 self.dirty = 1
546 self.dirty = 1
547 return 0
547 return 0
548
548
549 horig = h
549 horig = h
550 if (self.eolmode in ('crlf', 'lf')
550 if (self.eolmode in ('crlf', 'lf')
551 or self.eolmode == 'auto' and self.eol):
551 or self.eolmode == 'auto' and self.eol):
552 # If new eols are going to be normalized, then normalize
552 # If new eols are going to be normalized, then normalize
553 # hunk data before patching. Otherwise, preserve input
553 # hunk data before patching. Otherwise, preserve input
554 # line-endings.
554 # line-endings.
555 h = h.getnormalized()
555 h = h.getnormalized()
556
556
557 # fast case first, no offsets, no fuzz
557 # fast case first, no offsets, no fuzz
558 old = h.old()
558 old = h.old()
559 # patch starts counting at 1 unless we are adding the file
559 # patch starts counting at 1 unless we are adding the file
560 if h.starta == 0:
560 if h.starta == 0:
561 start = 0
561 start = 0
562 else:
562 else:
563 start = h.starta + self.offset - 1
563 start = h.starta + self.offset - 1
564 orig_start = start
564 orig_start = start
565 # if there's skew we want to emit the "(offset %d lines)" even
565 # if there's skew we want to emit the "(offset %d lines)" even
566 # when the hunk cleanly applies at start + skew, so skip the
566 # when the hunk cleanly applies at start + skew, so skip the
567 # fast case code
567 # fast case code
568 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
568 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
569 if h.rmfile():
569 if h.rmfile():
570 self.unlink(self.fname)
570 self.unlink(self.fname)
571 else:
571 else:
572 self.lines[start : start + h.lena] = h.new()
572 self.lines[start : start + h.lena] = h.new()
573 self.offset += h.lenb - h.lena
573 self.offset += h.lenb - h.lena
574 self.dirty = 1
574 self.dirty = 1
575 return 0
575 return 0
576
576
577 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
577 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
578 self.hashlines()
578 self.hashlines()
579 if h.hunk[-1][0] != ' ':
579 if h.hunk[-1][0] != ' ':
580 # if the hunk tried to put something at the bottom of the file
580 # if the hunk tried to put something at the bottom of the file
581 # override the start line and use eof here
581 # override the start line and use eof here
582 search_start = len(self.lines)
582 search_start = len(self.lines)
583 else:
583 else:
584 search_start = orig_start + self.skew
584 search_start = orig_start + self.skew
585
585
586 for fuzzlen in xrange(3):
586 for fuzzlen in xrange(3):
587 for toponly in [True, False]:
587 for toponly in [True, False]:
588 old = h.old(fuzzlen, toponly)
588 old = h.old(fuzzlen, toponly)
589
589
590 cand = self.findlines(old[0][1:], search_start)
590 cand = self.findlines(old[0][1:], search_start)
591 for l in cand:
591 for l in cand:
592 if diffhelpers.testhunk(old, self.lines, l) == 0:
592 if diffhelpers.testhunk(old, self.lines, l) == 0:
593 newlines = h.new(fuzzlen, toponly)
593 newlines = h.new(fuzzlen, toponly)
594 self.lines[l : l + len(old)] = newlines
594 self.lines[l : l + len(old)] = newlines
595 self.offset += len(newlines) - len(old)
595 self.offset += len(newlines) - len(old)
596 self.skew = l - orig_start
596 self.skew = l - orig_start
597 self.dirty = 1
597 self.dirty = 1
598 offset = l - orig_start - fuzzlen
598 offset = l - orig_start - fuzzlen
599 if fuzzlen:
599 if fuzzlen:
600 msg = _("Hunk #%d succeeded at %d "
600 msg = _("Hunk #%d succeeded at %d "
601 "with fuzz %d "
601 "with fuzz %d "
602 "(offset %d lines).\n")
602 "(offset %d lines).\n")
603 self.printfile(True)
603 self.printfile(True)
604 self.ui.warn(msg %
604 self.ui.warn(msg %
605 (h.number, l + 1, fuzzlen, offset))
605 (h.number, l + 1, fuzzlen, offset))
606 else:
606 else:
607 msg = _("Hunk #%d succeeded at %d "
607 msg = _("Hunk #%d succeeded at %d "
608 "(offset %d lines).\n")
608 "(offset %d lines).\n")
609 self.ui.note(msg % (h.number, l + 1, offset))
609 self.ui.note(msg % (h.number, l + 1, offset))
610 return fuzzlen
610 return fuzzlen
611 self.printfile(True)
611 self.printfile(True)
612 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
612 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
613 self.rej.append(horig)
613 self.rej.append(horig)
614 return -1
614 return -1
615
615
616 class hunk(object):
616 class hunk(object):
617 def __init__(self, desc, num, lr, context, create=False, remove=False):
617 def __init__(self, desc, num, lr, context, create=False, remove=False):
618 self.number = num
618 self.number = num
619 self.desc = desc
619 self.desc = desc
620 self.hunk = [desc]
620 self.hunk = [desc]
621 self.a = []
621 self.a = []
622 self.b = []
622 self.b = []
623 self.starta = self.lena = None
623 self.starta = self.lena = None
624 self.startb = self.lenb = None
624 self.startb = self.lenb = None
625 if lr is not None:
625 if lr is not None:
626 if context:
626 if context:
627 self.read_context_hunk(lr)
627 self.read_context_hunk(lr)
628 else:
628 else:
629 self.read_unified_hunk(lr)
629 self.read_unified_hunk(lr)
630 self.create = create
630 self.create = create
631 self.remove = remove and not create
631 self.remove = remove and not create
632
632
633 def getnormalized(self):
633 def getnormalized(self):
634 """Return a copy with line endings normalized to LF."""
634 """Return a copy with line endings normalized to LF."""
635
635
636 def normalize(lines):
636 def normalize(lines):
637 nlines = []
637 nlines = []
638 for line in lines:
638 for line in lines:
639 if line.endswith('\r\n'):
639 if line.endswith('\r\n'):
640 line = line[:-2] + '\n'
640 line = line[:-2] + '\n'
641 nlines.append(line)
641 nlines.append(line)
642 return nlines
642 return nlines
643
643
644 # Dummy object, it is rebuilt manually
644 # Dummy object, it is rebuilt manually
645 nh = hunk(self.desc, self.number, None, None, False, False)
645 nh = hunk(self.desc, self.number, None, None, False, False)
646 nh.number = self.number
646 nh.number = self.number
647 nh.desc = self.desc
647 nh.desc = self.desc
648 nh.hunk = self.hunk
648 nh.hunk = self.hunk
649 nh.a = normalize(self.a)
649 nh.a = normalize(self.a)
650 nh.b = normalize(self.b)
650 nh.b = normalize(self.b)
651 nh.starta = self.starta
651 nh.starta = self.starta
652 nh.startb = self.startb
652 nh.startb = self.startb
653 nh.lena = self.lena
653 nh.lena = self.lena
654 nh.lenb = self.lenb
654 nh.lenb = self.lenb
655 nh.create = self.create
655 nh.create = self.create
656 nh.remove = self.remove
656 nh.remove = self.remove
657 return nh
657 return nh
658
658
659 def read_unified_hunk(self, lr):
659 def read_unified_hunk(self, lr):
660 m = unidesc.match(self.desc)
660 m = unidesc.match(self.desc)
661 if not m:
661 if not m:
662 raise PatchError(_("bad hunk #%d") % self.number)
662 raise PatchError(_("bad hunk #%d") % self.number)
663 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
663 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
664 if self.lena is None:
664 if self.lena is None:
665 self.lena = 1
665 self.lena = 1
666 else:
666 else:
667 self.lena = int(self.lena)
667 self.lena = int(self.lena)
668 if self.lenb is None:
668 if self.lenb is None:
669 self.lenb = 1
669 self.lenb = 1
670 else:
670 else:
671 self.lenb = int(self.lenb)
671 self.lenb = int(self.lenb)
672 self.starta = int(self.starta)
672 self.starta = int(self.starta)
673 self.startb = int(self.startb)
673 self.startb = int(self.startb)
674 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
674 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
675 # if we hit eof before finishing out the hunk, the last line will
675 # if we hit eof before finishing out the hunk, the last line will
676 # be zero length. Lets try to fix it up.
676 # be zero length. Lets try to fix it up.
677 while len(self.hunk[-1]) == 0:
677 while len(self.hunk[-1]) == 0:
678 del self.hunk[-1]
678 del self.hunk[-1]
679 del self.a[-1]
679 del self.a[-1]
680 del self.b[-1]
680 del self.b[-1]
681 self.lena -= 1
681 self.lena -= 1
682 self.lenb -= 1
682 self.lenb -= 1
683
683
684 def read_context_hunk(self, lr):
684 def read_context_hunk(self, lr):
685 self.desc = lr.readline()
685 self.desc = lr.readline()
686 m = contextdesc.match(self.desc)
686 m = contextdesc.match(self.desc)
687 if not m:
687 if not m:
688 raise PatchError(_("bad hunk #%d") % self.number)
688 raise PatchError(_("bad hunk #%d") % self.number)
689 foo, self.starta, foo2, aend, foo3 = m.groups()
689 foo, self.starta, foo2, aend, foo3 = m.groups()
690 self.starta = int(self.starta)
690 self.starta = int(self.starta)
691 if aend is None:
691 if aend is None:
692 aend = self.starta
692 aend = self.starta
693 self.lena = int(aend) - self.starta
693 self.lena = int(aend) - self.starta
694 if self.starta:
694 if self.starta:
695 self.lena += 1
695 self.lena += 1
696 for x in xrange(self.lena):
696 for x in xrange(self.lena):
697 l = lr.readline()
697 l = lr.readline()
698 if l.startswith('---'):
698 if l.startswith('---'):
699 # lines addition, old block is empty
699 # lines addition, old block is empty
700 lr.push(l)
700 lr.push(l)
701 break
701 break
702 s = l[2:]
702 s = l[2:]
703 if l.startswith('- ') or l.startswith('! '):
703 if l.startswith('- ') or l.startswith('! '):
704 u = '-' + s
704 u = '-' + s
705 elif l.startswith(' '):
705 elif l.startswith(' '):
706 u = ' ' + s
706 u = ' ' + s
707 else:
707 else:
708 raise PatchError(_("bad hunk #%d old text line %d") %
708 raise PatchError(_("bad hunk #%d old text line %d") %
709 (self.number, x))
709 (self.number, x))
710 self.a.append(u)
710 self.a.append(u)
711 self.hunk.append(u)
711 self.hunk.append(u)
712
712
713 l = lr.readline()
713 l = lr.readline()
714 if l.startswith('\ '):
714 if l.startswith('\ '):
715 s = self.a[-1][:-1]
715 s = self.a[-1][:-1]
716 self.a[-1] = s
716 self.a[-1] = s
717 self.hunk[-1] = s
717 self.hunk[-1] = s
718 l = lr.readline()
718 l = lr.readline()
719 m = contextdesc.match(l)
719 m = contextdesc.match(l)
720 if not m:
720 if not m:
721 raise PatchError(_("bad hunk #%d") % self.number)
721 raise PatchError(_("bad hunk #%d") % self.number)
722 foo, self.startb, foo2, bend, foo3 = m.groups()
722 foo, self.startb, foo2, bend, foo3 = m.groups()
723 self.startb = int(self.startb)
723 self.startb = int(self.startb)
724 if bend is None:
724 if bend is None:
725 bend = self.startb
725 bend = self.startb
726 self.lenb = int(bend) - self.startb
726 self.lenb = int(bend) - self.startb
727 if self.startb:
727 if self.startb:
728 self.lenb += 1
728 self.lenb += 1
729 hunki = 1
729 hunki = 1
730 for x in xrange(self.lenb):
730 for x in xrange(self.lenb):
731 l = lr.readline()
731 l = lr.readline()
732 if l.startswith('\ '):
732 if l.startswith('\ '):
733 # XXX: the only way to hit this is with an invalid line range.
733 # XXX: the only way to hit this is with an invalid line range.
734 # The no-eol marker is not counted in the line range, but I
734 # The no-eol marker is not counted in the line range, but I
735 # guess there are diff(1) out there which behave differently.
735 # guess there are diff(1) out there which behave differently.
736 s = self.b[-1][:-1]
736 s = self.b[-1][:-1]
737 self.b[-1] = s
737 self.b[-1] = s
738 self.hunk[hunki - 1] = s
738 self.hunk[hunki - 1] = s
739 continue
739 continue
740 if not l:
740 if not l:
741 # line deletions, new block is empty and we hit EOF
741 # line deletions, new block is empty and we hit EOF
742 lr.push(l)
742 lr.push(l)
743 break
743 break
744 s = l[2:]
744 s = l[2:]
745 if l.startswith('+ ') or l.startswith('! '):
745 if l.startswith('+ ') or l.startswith('! '):
746 u = '+' + s
746 u = '+' + s
747 elif l.startswith(' '):
747 elif l.startswith(' '):
748 u = ' ' + s
748 u = ' ' + s
749 elif len(self.b) == 0:
749 elif len(self.b) == 0:
750 # line deletions, new block is empty
750 # line deletions, new block is empty
751 lr.push(l)
751 lr.push(l)
752 break
752 break
753 else:
753 else:
754 raise PatchError(_("bad hunk #%d old text line %d") %
754 raise PatchError(_("bad hunk #%d old text line %d") %
755 (self.number, x))
755 (self.number, x))
756 self.b.append(s)
756 self.b.append(s)
757 while True:
757 while True:
758 if hunki >= len(self.hunk):
758 if hunki >= len(self.hunk):
759 h = ""
759 h = ""
760 else:
760 else:
761 h = self.hunk[hunki]
761 h = self.hunk[hunki]
762 hunki += 1
762 hunki += 1
763 if h == u:
763 if h == u:
764 break
764 break
765 elif h.startswith('-'):
765 elif h.startswith('-'):
766 continue
766 continue
767 else:
767 else:
768 self.hunk.insert(hunki - 1, u)
768 self.hunk.insert(hunki - 1, u)
769 break
769 break
770
770
771 if not self.a:
771 if not self.a:
772 # this happens when lines were only added to the hunk
772 # this happens when lines were only added to the hunk
773 for x in self.hunk:
773 for x in self.hunk:
774 if x.startswith('-') or x.startswith(' '):
774 if x.startswith('-') or x.startswith(' '):
775 self.a.append(x)
775 self.a.append(x)
776 if not self.b:
776 if not self.b:
777 # this happens when lines were only deleted from the hunk
777 # this happens when lines were only deleted from the hunk
778 for x in self.hunk:
778 for x in self.hunk:
779 if x.startswith('+') or x.startswith(' '):
779 if x.startswith('+') or x.startswith(' '):
780 self.b.append(x[1:])
780 self.b.append(x[1:])
781 # @@ -start,len +start,len @@
781 # @@ -start,len +start,len @@
782 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
782 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
783 self.startb, self.lenb)
783 self.startb, self.lenb)
784 self.hunk[0] = self.desc
784 self.hunk[0] = self.desc
785
785
786 def fix_newline(self):
786 def fix_newline(self):
787 diffhelpers.fix_newline(self.hunk, self.a, self.b)
787 diffhelpers.fix_newline(self.hunk, self.a, self.b)
788
788
789 def complete(self):
789 def complete(self):
790 return len(self.a) == self.lena and len(self.b) == self.lenb
790 return len(self.a) == self.lena and len(self.b) == self.lenb
791
791
792 def createfile(self):
792 def createfile(self):
793 return self.starta == 0 and self.lena == 0 and self.create
793 return self.starta == 0 and self.lena == 0 and self.create
794
794
795 def rmfile(self):
795 def rmfile(self):
796 return self.startb == 0 and self.lenb == 0 and self.remove
796 return self.startb == 0 and self.lenb == 0 and self.remove
797
797
798 def fuzzit(self, l, fuzz, toponly):
798 def fuzzit(self, l, fuzz, toponly):
799 # this removes context lines from the top and bottom of list 'l'. It
799 # this removes context lines from the top and bottom of list 'l'. It
800 # checks the hunk to make sure only context lines are removed, and then
800 # checks the hunk to make sure only context lines are removed, and then
801 # returns a new shortened list of lines.
801 # returns a new shortened list of lines.
802 fuzz = min(fuzz, len(l)-1)
802 fuzz = min(fuzz, len(l)-1)
803 if fuzz:
803 if fuzz:
804 top = 0
804 top = 0
805 bot = 0
805 bot = 0
806 hlen = len(self.hunk)
806 hlen = len(self.hunk)
807 for x in xrange(hlen - 1):
807 for x in xrange(hlen - 1):
808 # the hunk starts with the @@ line, so use x+1
808 # the hunk starts with the @@ line, so use x+1
809 if self.hunk[x + 1][0] == ' ':
809 if self.hunk[x + 1][0] == ' ':
810 top += 1
810 top += 1
811 else:
811 else:
812 break
812 break
813 if not toponly:
813 if not toponly:
814 for x in xrange(hlen - 1):
814 for x in xrange(hlen - 1):
815 if self.hunk[hlen - bot - 1][0] == ' ':
815 if self.hunk[hlen - bot - 1][0] == ' ':
816 bot += 1
816 bot += 1
817 else:
817 else:
818 break
818 break
819
819
820 # top and bot now count context in the hunk
820 # top and bot now count context in the hunk
821 # adjust them if either one is short
821 # adjust them if either one is short
822 context = max(top, bot, 3)
822 context = max(top, bot, 3)
823 if bot < context:
823 if bot < context:
824 bot = max(0, fuzz - (context - bot))
824 bot = max(0, fuzz - (context - bot))
825 else:
825 else:
826 bot = min(fuzz, bot)
826 bot = min(fuzz, bot)
827 if top < context:
827 if top < context:
828 top = max(0, fuzz - (context - top))
828 top = max(0, fuzz - (context - top))
829 else:
829 else:
830 top = min(fuzz, top)
830 top = min(fuzz, top)
831
831
832 return l[top:len(l)-bot]
832 return l[top:len(l)-bot]
833 return l
833 return l
834
834
835 def old(self, fuzz=0, toponly=False):
835 def old(self, fuzz=0, toponly=False):
836 return self.fuzzit(self.a, fuzz, toponly)
836 return self.fuzzit(self.a, fuzz, toponly)
837
837
838 def new(self, fuzz=0, toponly=False):
838 def new(self, fuzz=0, toponly=False):
839 return self.fuzzit(self.b, fuzz, toponly)
839 return self.fuzzit(self.b, fuzz, toponly)
840
840
841 class binhunk:
841 class binhunk:
842 'A binary patch file. Only understands literals so far.'
842 'A binary patch file. Only understands literals so far.'
843 def __init__(self, gitpatch):
843 def __init__(self, gitpatch):
844 self.gitpatch = gitpatch
844 self.gitpatch = gitpatch
845 self.text = None
845 self.text = None
846 self.hunk = ['GIT binary patch\n']
846 self.hunk = ['GIT binary patch\n']
847
847
848 def createfile(self):
848 def createfile(self):
849 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
849 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
850
850
851 def rmfile(self):
851 def rmfile(self):
852 return self.gitpatch.op == 'DELETE'
852 return self.gitpatch.op == 'DELETE'
853
853
854 def complete(self):
854 def complete(self):
855 return self.text is not None
855 return self.text is not None
856
856
857 def new(self):
857 def new(self):
858 return [self.text]
858 return [self.text]
859
859
860 def extract(self, lr):
860 def extract(self, lr):
861 line = lr.readline()
861 line = lr.readline()
862 self.hunk.append(line)
862 self.hunk.append(line)
863 while line and not line.startswith('literal '):
863 while line and not line.startswith('literal '):
864 line = lr.readline()
864 line = lr.readline()
865 self.hunk.append(line)
865 self.hunk.append(line)
866 if not line:
866 if not line:
867 raise PatchError(_('could not extract binary patch'))
867 raise PatchError(_('could not extract binary patch'))
868 size = int(line[8:].rstrip())
868 size = int(line[8:].rstrip())
869 dec = []
869 dec = []
870 line = lr.readline()
870 line = lr.readline()
871 self.hunk.append(line)
871 self.hunk.append(line)
872 while len(line) > 1:
872 while len(line) > 1:
873 l = line[0]
873 l = line[0]
874 if l <= 'Z' and l >= 'A':
874 if l <= 'Z' and l >= 'A':
875 l = ord(l) - ord('A') + 1
875 l = ord(l) - ord('A') + 1
876 else:
876 else:
877 l = ord(l) - ord('a') + 27
877 l = ord(l) - ord('a') + 27
878 dec.append(base85.b85decode(line[1:-1])[:l])
878 dec.append(base85.b85decode(line[1:-1])[:l])
879 line = lr.readline()
879 line = lr.readline()
880 self.hunk.append(line)
880 self.hunk.append(line)
881 text = zlib.decompress(''.join(dec))
881 text = zlib.decompress(''.join(dec))
882 if len(text) != size:
882 if len(text) != size:
883 raise PatchError(_('binary patch is %d bytes, not %d') %
883 raise PatchError(_('binary patch is %d bytes, not %d') %
884 len(text), size)
884 len(text), size)
885 self.text = text
885 self.text = text
886
886
887 def parsefilename(str):
887 def parsefilename(str):
888 # --- filename \t|space stuff
888 # --- filename \t|space stuff
889 s = str[4:].rstrip('\r\n')
889 s = str[4:].rstrip('\r\n')
890 i = s.find('\t')
890 i = s.find('\t')
891 if i < 0:
891 if i < 0:
892 i = s.find(' ')
892 i = s.find(' ')
893 if i < 0:
893 if i < 0:
894 return s
894 return s
895 return s[:i]
895 return s[:i]
896
896
897 def pathstrip(path, strip):
897 def pathstrip(path, strip):
898 pathlen = len(path)
898 pathlen = len(path)
899 i = 0
899 i = 0
900 if strip == 0:
900 if strip == 0:
901 return '', path.rstrip()
901 return '', path.rstrip()
902 count = strip
902 count = strip
903 while count > 0:
903 while count > 0:
904 i = path.find('/', i)
904 i = path.find('/', i)
905 if i == -1:
905 if i == -1:
906 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
906 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
907 (count, strip, path))
907 (count, strip, path))
908 i += 1
908 i += 1
909 # consume '//' in the path
909 # consume '//' in the path
910 while i < pathlen - 1 and path[i] == '/':
910 while i < pathlen - 1 and path[i] == '/':
911 i += 1
911 i += 1
912 count -= 1
912 count -= 1
913 return path[:i].lstrip(), path[i:].rstrip()
913 return path[:i].lstrip(), path[i:].rstrip()
914
914
915 def selectfile(afile_orig, bfile_orig, hunk, strip):
915 def selectfile(afile_orig, bfile_orig, hunk, strip):
916 nulla = afile_orig == "/dev/null"
916 nulla = afile_orig == "/dev/null"
917 nullb = bfile_orig == "/dev/null"
917 nullb = bfile_orig == "/dev/null"
918 abase, afile = pathstrip(afile_orig, strip)
918 abase, afile = pathstrip(afile_orig, strip)
919 gooda = not nulla and os.path.lexists(afile)
919 gooda = not nulla and os.path.lexists(afile)
920 bbase, bfile = pathstrip(bfile_orig, strip)
920 bbase, bfile = pathstrip(bfile_orig, strip)
921 if afile == bfile:
921 if afile == bfile:
922 goodb = gooda
922 goodb = gooda
923 else:
923 else:
924 goodb = not nullb and os.path.lexists(bfile)
924 goodb = not nullb and os.path.lexists(bfile)
925 createfunc = hunk.createfile
925 createfunc = hunk.createfile
926 missing = not goodb and not gooda and not createfunc()
926 missing = not goodb and not gooda and not createfunc()
927
927
928 # some diff programs apparently produce patches where the afile is
928 # some diff programs apparently produce patches where the afile is
929 # not /dev/null, but afile starts with bfile
929 # not /dev/null, but afile starts with bfile
930 abasedir = afile[:afile.rfind('/') + 1]
930 abasedir = afile[:afile.rfind('/') + 1]
931 bbasedir = bfile[:bfile.rfind('/') + 1]
931 bbasedir = bfile[:bfile.rfind('/') + 1]
932 if missing and abasedir == bbasedir and afile.startswith(bfile):
932 if missing and abasedir == bbasedir and afile.startswith(bfile):
933 # this isn't very pretty
933 # this isn't very pretty
934 hunk.create = True
934 hunk.create = True
935 if createfunc():
935 if createfunc():
936 missing = False
936 missing = False
937 else:
937 else:
938 hunk.create = False
938 hunk.create = False
939
939
940 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
940 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
941 # diff is between a file and its backup. In this case, the original
941 # diff is between a file and its backup. In this case, the original
942 # file should be patched (see original mpatch code).
942 # file should be patched (see original mpatch code).
943 isbackup = (abase == bbase and bfile.startswith(afile))
943 isbackup = (abase == bbase and bfile.startswith(afile))
944 fname = None
944 fname = None
945 if not missing:
945 if not missing:
946 if gooda and goodb:
946 if gooda and goodb:
947 fname = isbackup and afile or bfile
947 fname = isbackup and afile or bfile
948 elif gooda:
948 elif gooda:
949 fname = afile
949 fname = afile
950
950
951 if not fname:
951 if not fname:
952 if not nullb:
952 if not nullb:
953 fname = isbackup and afile or bfile
953 fname = isbackup and afile or bfile
954 elif not nulla:
954 elif not nulla:
955 fname = afile
955 fname = afile
956 else:
956 else:
957 raise PatchError(_("undefined source and destination files"))
957 raise PatchError(_("undefined source and destination files"))
958
958
959 return fname, missing
959 return fname, missing
960
960
961 def scangitpatch(lr, firstline):
961 def scangitpatch(lr, firstline):
962 """
962 """
963 Git patches can emit:
963 Git patches can emit:
964 - rename a to b
964 - rename a to b
965 - change b
965 - change b
966 - copy a to c
966 - copy a to c
967 - change c
967 - change c
968
968
969 We cannot apply this sequence as-is, the renamed 'a' could not be
969 We cannot apply this sequence as-is, the renamed 'a' could not be
970 found for it would have been renamed already. And we cannot copy
970 found for it would have been renamed already. And we cannot copy
971 from 'b' instead because 'b' would have been changed already. So
971 from 'b' instead because 'b' would have been changed already. So
972 we scan the git patch for copy and rename commands so we can
972 we scan the git patch for copy and rename commands so we can
973 perform the copies ahead of time.
973 perform the copies ahead of time.
974 """
974 """
975 pos = 0
975 pos = 0
976 try:
976 try:
977 pos = lr.fp.tell()
977 pos = lr.fp.tell()
978 fp = lr.fp
978 fp = lr.fp
979 except IOError:
979 except IOError:
980 fp = cStringIO.StringIO(lr.fp.read())
980 fp = cStringIO.StringIO(lr.fp.read())
981 gitlr = linereader(fp, lr.textmode)
981 gitlr = linereader(fp, lr.textmode)
982 gitlr.push(firstline)
982 gitlr.push(firstline)
983 gitpatches = readgitpatch(gitlr)
983 gitpatches = readgitpatch(gitlr)
984 fp.seek(pos)
984 fp.seek(pos)
985 return gitpatches
985 return gitpatches
986
986
987 def iterhunks(ui, fp):
987 def iterhunks(ui, fp):
988 """Read a patch and yield the following events:
988 """Read a patch and yield the following events:
989 - ("file", afile, bfile, firsthunk): select a new target file.
989 - ("file", afile, bfile, firsthunk): select a new target file.
990 - ("hunk", hunk): a new hunk is ready to be applied, follows a
990 - ("hunk", hunk): a new hunk is ready to be applied, follows a
991 "file" event.
991 "file" event.
992 - ("git", gitchanges): current diff is in git format, gitchanges
992 - ("git", gitchanges): current diff is in git format, gitchanges
993 maps filenames to gitpatch records. Unique event.
993 maps filenames to gitpatch records. Unique event.
994 """
994 """
995 changed = {}
995 changed = {}
996 current_hunk = None
996 current_hunk = None
997 afile = ""
997 afile = ""
998 bfile = ""
998 bfile = ""
999 state = None
999 state = None
1000 hunknum = 0
1000 hunknum = 0
1001 emitfile = False
1001 emitfile = False
1002 git = False
1002 git = False
1003
1003
1004 # our states
1004 # our states
1005 BFILE = 1
1005 BFILE = 1
1006 context = None
1006 context = None
1007 lr = linereader(fp)
1007 lr = linereader(fp)
1008
1008
1009 while True:
1009 while True:
1010 newfile = newgitfile = False
1010 newfile = newgitfile = False
1011 x = lr.readline()
1011 x = lr.readline()
1012 if not x:
1012 if not x:
1013 break
1013 break
1014 if current_hunk:
1014 if current_hunk:
1015 if x.startswith('\ '):
1015 if x.startswith('\ '):
1016 current_hunk.fix_newline()
1016 current_hunk.fix_newline()
1017 yield 'hunk', current_hunk
1017 yield 'hunk', current_hunk
1018 current_hunk = None
1018 current_hunk = None
1019 if (state == BFILE and ((not context and x[0] == '@') or
1019 if (state == BFILE and ((not context and x[0] == '@') or
1020 ((context is not False) and x.startswith('***************')))):
1020 ((context is not False) and x.startswith('***************')))):
1021 if context is None and x.startswith('***************'):
1021 if context is None and x.startswith('***************'):
1022 context = True
1022 context = True
1023 gpatch = changed.get(bfile)
1023 gpatch = changed.get(bfile)
1024 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1024 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1025 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1025 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1026 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1026 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1027 hunknum += 1
1027 hunknum += 1
1028 if emitfile:
1028 if emitfile:
1029 emitfile = False
1029 emitfile = False
1030 yield 'file', (afile, bfile, current_hunk)
1030 yield 'file', (afile, bfile, current_hunk)
1031 elif state == BFILE and x.startswith('GIT binary patch'):
1031 elif state == BFILE and x.startswith('GIT binary patch'):
1032 current_hunk = binhunk(changed[bfile])
1032 current_hunk = binhunk(changed[bfile])
1033 hunknum += 1
1033 hunknum += 1
1034 if emitfile:
1034 if emitfile:
1035 emitfile = False
1035 emitfile = False
1036 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1036 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1037 current_hunk.extract(lr)
1037 current_hunk.extract(lr)
1038 elif x.startswith('diff --git'):
1038 elif x.startswith('diff --git'):
1039 # check for git diff, scanning the whole patch file if needed
1039 # check for git diff, scanning the whole patch file if needed
1040 m = gitre.match(x)
1040 m = gitre.match(x)
1041 if m:
1041 if m:
1042 afile, bfile = m.group(1, 2)
1042 afile, bfile = m.group(1, 2)
1043 if not git:
1043 if not git:
1044 git = True
1044 git = True
1045 gitpatches = scangitpatch(lr, x)
1045 gitpatches = scangitpatch(lr, x)
1046 yield 'git', gitpatches
1046 yield 'git', gitpatches
1047 for gp in gitpatches:
1047 for gp in gitpatches:
1048 changed[gp.path] = gp
1048 changed[gp.path] = gp
1049 # else error?
1049 # else error?
1050 # copy/rename + modify should modify target, not source
1050 # copy/rename + modify should modify target, not source
1051 gp = changed.get(bfile)
1051 gp = changed.get(bfile)
1052 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1052 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1053 or gp.mode):
1053 or gp.mode):
1054 afile = bfile
1054 afile = bfile
1055 newgitfile = True
1055 newgitfile = True
1056 elif x.startswith('---'):
1056 elif x.startswith('---'):
1057 # check for a unified diff
1057 # check for a unified diff
1058 l2 = lr.readline()
1058 l2 = lr.readline()
1059 if not l2.startswith('+++'):
1059 if not l2.startswith('+++'):
1060 lr.push(l2)
1060 lr.push(l2)
1061 continue
1061 continue
1062 newfile = True
1062 newfile = True
1063 context = False
1063 context = False
1064 afile = parsefilename(x)
1064 afile = parsefilename(x)
1065 bfile = parsefilename(l2)
1065 bfile = parsefilename(l2)
1066 elif x.startswith('***'):
1066 elif x.startswith('***'):
1067 # check for a context diff
1067 # check for a context diff
1068 l2 = lr.readline()
1068 l2 = lr.readline()
1069 if not l2.startswith('---'):
1069 if not l2.startswith('---'):
1070 lr.push(l2)
1070 lr.push(l2)
1071 continue
1071 continue
1072 l3 = lr.readline()
1072 l3 = lr.readline()
1073 lr.push(l3)
1073 lr.push(l3)
1074 if not l3.startswith("***************"):
1074 if not l3.startswith("***************"):
1075 lr.push(l2)
1075 lr.push(l2)
1076 continue
1076 continue
1077 newfile = True
1077 newfile = True
1078 context = True
1078 context = True
1079 afile = parsefilename(x)
1079 afile = parsefilename(x)
1080 bfile = parsefilename(l2)
1080 bfile = parsefilename(l2)
1081
1081
1082 if newgitfile or newfile:
1082 if newgitfile or newfile:
1083 emitfile = True
1083 emitfile = True
1084 state = BFILE
1084 state = BFILE
1085 hunknum = 0
1085 hunknum = 0
1086 if current_hunk:
1086 if current_hunk:
1087 if current_hunk.complete():
1087 if current_hunk.complete():
1088 yield 'hunk', current_hunk
1088 yield 'hunk', current_hunk
1089 else:
1089 else:
1090 raise PatchError(_("malformed patch %s %s") % (afile,
1090 raise PatchError(_("malformed patch %s %s") % (afile,
1091 current_hunk.desc))
1091 current_hunk.desc))
1092
1092
1093 def applydiff(ui, fp, changed, strip=1, eolmode='strict'):
1093 def applydiff(ui, fp, changed, strip=1, eolmode='strict'):
1094 """Reads a patch from fp and tries to apply it.
1094 """Reads a patch from fp and tries to apply it.
1095
1095
1096 The dict 'changed' is filled in with all of the filenames changed
1096 The dict 'changed' is filled in with all of the filenames changed
1097 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1097 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1098 found and 1 if there was any fuzz.
1098 found and 1 if there was any fuzz.
1099
1099
1100 If 'eolmode' is 'strict', the patch content and patched file are
1100 If 'eolmode' is 'strict', the patch content and patched file are
1101 read in binary mode. Otherwise, line endings are ignored when
1101 read in binary mode. Otherwise, line endings are ignored when
1102 patching then normalized according to 'eolmode'.
1102 patching then normalized according to 'eolmode'.
1103
1103
1104 Callers probably want to call 'cmdutil.updatedir' after this to
1104 Callers probably want to call 'cmdutil.updatedir' after this to
1105 apply certain categories of changes not done by this function.
1105 apply certain categories of changes not done by this function.
1106 """
1106 """
1107 return _applydiff(ui, fp, patchfile, copyfile, changed, strip=strip,
1107 return _applydiff(ui, fp, patchfile, copyfile, changed, strip=strip,
1108 eolmode=eolmode)
1108 eolmode=eolmode)
1109
1109
1110 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1, eolmode='strict'):
1110 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1, eolmode='strict'):
1111 rejects = 0
1111 rejects = 0
1112 err = 0
1112 err = 0
1113 current_file = None
1113 current_file = None
1114 cwd = os.getcwd()
1114 cwd = os.getcwd()
1115 opener = util.opener(cwd)
1115 opener = util.opener(cwd)
1116
1116
1117 def closefile():
1117 def closefile():
1118 if not current_file:
1118 if not current_file:
1119 return 0
1119 return 0
1120 if current_file.dirty:
1120 if current_file.dirty:
1121 current_file.writelines(current_file.fname, current_file.lines)
1121 current_file.writelines(current_file.fname, current_file.lines)
1122 current_file.write_rej()
1122 current_file.write_rej()
1123 return len(current_file.rej)
1123 return len(current_file.rej)
1124
1124
1125 for state, values in iterhunks(ui, fp):
1125 for state, values in iterhunks(ui, fp):
1126 if state == 'hunk':
1126 if state == 'hunk':
1127 if not current_file:
1127 if not current_file:
1128 continue
1128 continue
1129 ret = current_file.apply(values)
1129 ret = current_file.apply(values)
1130 if ret >= 0:
1130 if ret >= 0:
1131 changed.setdefault(current_file.fname, None)
1131 changed.setdefault(current_file.fname, None)
1132 if ret > 0:
1132 if ret > 0:
1133 err = 1
1133 err = 1
1134 elif state == 'file':
1134 elif state == 'file':
1135 rejects += closefile()
1135 rejects += closefile()
1136 afile, bfile, first_hunk = values
1136 afile, bfile, first_hunk = values
1137 try:
1137 try:
1138 current_file, missing = selectfile(afile, bfile,
1138 current_file, missing = selectfile(afile, bfile,
1139 first_hunk, strip)
1139 first_hunk, strip)
1140 current_file = patcher(ui, current_file, opener,
1140 current_file = patcher(ui, current_file, opener,
1141 missing=missing, eolmode=eolmode)
1141 missing=missing, eolmode=eolmode)
1142 except PatchError, err:
1142 except PatchError, err:
1143 ui.warn(str(err) + '\n')
1143 ui.warn(str(err) + '\n')
1144 current_file = None
1144 current_file = None
1145 rejects += 1
1145 rejects += 1
1146 continue
1146 continue
1147 elif state == 'git':
1147 elif state == 'git':
1148 for gp in values:
1148 for gp in values:
1149 gp.path = pathstrip(gp.path, strip - 1)[1]
1149 gp.path = pathstrip(gp.path, strip - 1)[1]
1150 if gp.oldpath:
1150 if gp.oldpath:
1151 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1151 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1152 # Binary patches really overwrite target files, copying them
1152 # Binary patches really overwrite target files, copying them
1153 # will just make it fails with "target file exists"
1153 # will just make it fails with "target file exists"
1154 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1154 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1155 copyfn(gp.oldpath, gp.path, cwd)
1155 copyfn(gp.oldpath, gp.path, cwd)
1156 changed[gp.path] = gp
1156 changed[gp.path] = gp
1157 else:
1157 else:
1158 raise util.Abort(_('unsupported parser state: %s') % state)
1158 raise util.Abort(_('unsupported parser state: %s') % state)
1159
1159
1160 rejects += closefile()
1160 rejects += closefile()
1161
1161
1162 if rejects:
1162 if rejects:
1163 return -1
1163 return -1
1164 return err
1164 return err
1165
1165
1166 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1166 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1167 """use <patcher> to apply <patchname> to the working directory.
1167 """use <patcher> to apply <patchname> to the working directory.
1168 returns whether patch was applied with fuzz factor."""
1168 returns whether patch was applied with fuzz factor."""
1169
1169
1170 fuzz = False
1170 fuzz = False
1171 args = []
1171 args = []
1172 if cwd:
1172 if cwd:
1173 args.append('-d %s' % util.shellquote(cwd))
1173 args.append('-d %s' % util.shellquote(cwd))
1174 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1174 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1175 util.shellquote(patchname)))
1175 util.shellquote(patchname)))
1176
1176
1177 for line in fp:
1177 for line in fp:
1178 line = line.rstrip()
1178 line = line.rstrip()
1179 ui.note(line + '\n')
1179 ui.note(line + '\n')
1180 if line.startswith('patching file '):
1180 if line.startswith('patching file '):
1181 pf = util.parse_patch_output(line)
1181 pf = util.parse_patch_output(line)
1182 printed_file = False
1182 printed_file = False
1183 files.setdefault(pf, None)
1183 files.setdefault(pf, None)
1184 elif line.find('with fuzz') >= 0:
1184 elif line.find('with fuzz') >= 0:
1185 fuzz = True
1185 fuzz = True
1186 if not printed_file:
1186 if not printed_file:
1187 ui.warn(pf + '\n')
1187 ui.warn(pf + '\n')
1188 printed_file = True
1188 printed_file = True
1189 ui.warn(line + '\n')
1189 ui.warn(line + '\n')
1190 elif line.find('saving rejects to file') >= 0:
1190 elif line.find('saving rejects to file') >= 0:
1191 ui.warn(line + '\n')
1191 ui.warn(line + '\n')
1192 elif line.find('FAILED') >= 0:
1192 elif line.find('FAILED') >= 0:
1193 if not printed_file:
1193 if not printed_file:
1194 ui.warn(pf + '\n')
1194 ui.warn(pf + '\n')
1195 printed_file = True
1195 printed_file = True
1196 ui.warn(line + '\n')
1196 ui.warn(line + '\n')
1197 code = fp.close()
1197 code = fp.close()
1198 if code:
1198 if code:
1199 raise PatchError(_("patch command failed: %s") %
1199 raise PatchError(_("patch command failed: %s") %
1200 util.explain_exit(code)[0])
1200 util.explain_exit(code)[0])
1201 return fuzz
1201 return fuzz
1202
1202
1203 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1203 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1204 """use builtin patch to apply <patchobj> to the working directory.
1204 """use builtin patch to apply <patchobj> to the working directory.
1205 returns whether patch was applied with fuzz factor."""
1205 returns whether patch was applied with fuzz factor."""
1206
1206
1207 if files is None:
1207 if files is None:
1208 files = {}
1208 files = {}
1209 if eolmode is None:
1209 if eolmode is None:
1210 eolmode = ui.config('patch', 'eol', 'strict')
1210 eolmode = ui.config('patch', 'eol', 'strict')
1211 if eolmode.lower() not in eolmodes:
1211 if eolmode.lower() not in eolmodes:
1212 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1212 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1213 eolmode = eolmode.lower()
1213 eolmode = eolmode.lower()
1214
1214
1215 try:
1215 try:
1216 fp = open(patchobj, 'rb')
1216 fp = open(patchobj, 'rb')
1217 except TypeError:
1217 except TypeError:
1218 fp = patchobj
1218 fp = patchobj
1219 if cwd:
1219 if cwd:
1220 curdir = os.getcwd()
1220 curdir = os.getcwd()
1221 os.chdir(cwd)
1221 os.chdir(cwd)
1222 try:
1222 try:
1223 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1223 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1224 finally:
1224 finally:
1225 if cwd:
1225 if cwd:
1226 os.chdir(curdir)
1226 os.chdir(curdir)
1227 if fp != patchobj:
1227 if fp != patchobj:
1228 fp.close()
1228 fp.close()
1229 if ret < 0:
1229 if ret < 0:
1230 raise PatchError(_('patch failed to apply'))
1230 raise PatchError(_('patch failed to apply'))
1231 return ret > 0
1231 return ret > 0
1232
1232
1233 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1233 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1234 """Apply <patchname> to the working directory.
1234 """Apply <patchname> to the working directory.
1235
1235
1236 'eolmode' specifies how end of lines should be handled. It can be:
1236 'eolmode' specifies how end of lines should be handled. It can be:
1237 - 'strict': inputs are read in binary mode, EOLs are preserved
1237 - 'strict': inputs are read in binary mode, EOLs are preserved
1238 - 'crlf': EOLs are ignored when patching and reset to CRLF
1238 - 'crlf': EOLs are ignored when patching and reset to CRLF
1239 - 'lf': EOLs are ignored when patching and reset to LF
1239 - 'lf': EOLs are ignored when patching and reset to LF
1240 - None: get it from user settings, default to 'strict'
1240 - None: get it from user settings, default to 'strict'
1241 'eolmode' is ignored when using an external patcher program.
1241 'eolmode' is ignored when using an external patcher program.
1242
1242
1243 Returns whether patch was applied with fuzz factor.
1243 Returns whether patch was applied with fuzz factor.
1244 """
1244 """
1245 patcher = ui.config('ui', 'patch')
1245 patcher = ui.config('ui', 'patch')
1246 if files is None:
1246 if files is None:
1247 files = {}
1247 files = {}
1248 try:
1248 try:
1249 if patcher:
1249 if patcher:
1250 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1250 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1251 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1251 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1252 except PatchError, err:
1252 except PatchError, err:
1253 raise util.Abort(str(err))
1253 raise util.Abort(str(err))
1254
1254
1255 def b85diff(to, tn):
1255 def b85diff(to, tn):
1256 '''print base85-encoded binary diff'''
1256 '''print base85-encoded binary diff'''
1257 def gitindex(text):
1257 def gitindex(text):
1258 if not text:
1258 if not text:
1259 return hex(nullid)
1259 return hex(nullid)
1260 l = len(text)
1260 l = len(text)
1261 s = util.sha1('blob %d\0' % l)
1261 s = util.sha1('blob %d\0' % l)
1262 s.update(text)
1262 s.update(text)
1263 return s.hexdigest()
1263 return s.hexdigest()
1264
1264
1265 def fmtline(line):
1265 def fmtline(line):
1266 l = len(line)
1266 l = len(line)
1267 if l <= 26:
1267 if l <= 26:
1268 l = chr(ord('A') + l - 1)
1268 l = chr(ord('A') + l - 1)
1269 else:
1269 else:
1270 l = chr(l - 26 + ord('a') - 1)
1270 l = chr(l - 26 + ord('a') - 1)
1271 return '%c%s\n' % (l, base85.b85encode(line, True))
1271 return '%c%s\n' % (l, base85.b85encode(line, True))
1272
1272
1273 def chunk(text, csize=52):
1273 def chunk(text, csize=52):
1274 l = len(text)
1274 l = len(text)
1275 i = 0
1275 i = 0
1276 while i < l:
1276 while i < l:
1277 yield text[i:i + csize]
1277 yield text[i:i + csize]
1278 i += csize
1278 i += csize
1279
1279
1280 tohash = gitindex(to)
1280 tohash = gitindex(to)
1281 tnhash = gitindex(tn)
1281 tnhash = gitindex(tn)
1282 if tohash == tnhash:
1282 if tohash == tnhash:
1283 return ""
1283 return ""
1284
1284
1285 # TODO: deltas
1285 # TODO: deltas
1286 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1286 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1287 (tohash, tnhash, len(tn))]
1287 (tohash, tnhash, len(tn))]
1288 for l in chunk(zlib.compress(tn)):
1288 for l in chunk(zlib.compress(tn)):
1289 ret.append(fmtline(l))
1289 ret.append(fmtline(l))
1290 ret.append('\n')
1290 ret.append('\n')
1291 return ''.join(ret)
1291 return ''.join(ret)
1292
1292
1293 class GitDiffRequired(Exception):
1293 class GitDiffRequired(Exception):
1294 pass
1294 pass
1295
1295
1296 def diffopts(ui, opts=None, untrusted=False):
1296 def diffopts(ui, opts=None, untrusted=False):
1297 def get(key, name=None, getter=ui.configbool):
1297 def get(key, name=None, getter=ui.configbool):
1298 return ((opts and opts.get(key)) or
1298 return ((opts and opts.get(key)) or
1299 getter('diff', name or key, None, untrusted=untrusted))
1299 getter('diff', name or key, None, untrusted=untrusted))
1300 return mdiff.diffopts(
1300 return mdiff.diffopts(
1301 text=opts and opts.get('text'),
1301 text=opts and opts.get('text'),
1302 git=get('git'),
1302 git=get('git'),
1303 nodates=get('nodates'),
1303 nodates=get('nodates'),
1304 showfunc=get('show_function', 'showfunc'),
1304 showfunc=get('show_function', 'showfunc'),
1305 ignorews=get('ignore_all_space', 'ignorews'),
1305 ignorews=get('ignore_all_space', 'ignorews'),
1306 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1306 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1307 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1307 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1308 context=get('unified', getter=ui.config))
1308 context=get('unified', getter=ui.config))
1309
1309
1310 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1310 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1311 losedatafn=None, prefix=''):
1311 losedatafn=None, prefix=''):
1312 '''yields diff of changes to files between two nodes, or node and
1312 '''yields diff of changes to files between two nodes, or node and
1313 working directory.
1313 working directory.
1314
1314
1315 if node1 is None, use first dirstate parent instead.
1315 if node1 is None, use first dirstate parent instead.
1316 if node2 is None, compare node1 with working directory.
1316 if node2 is None, compare node1 with working directory.
1317
1317
1318 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1318 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1319 every time some change cannot be represented with the current
1319 every time some change cannot be represented with the current
1320 patch format. Return False to upgrade to git patch format, True to
1320 patch format. Return False to upgrade to git patch format, True to
1321 accept the loss or raise an exception to abort the diff. It is
1321 accept the loss or raise an exception to abort the diff. It is
1322 called with the name of current file being diffed as 'fn'. If set
1322 called with the name of current file being diffed as 'fn'. If set
1323 to None, patches will always be upgraded to git format when
1323 to None, patches will always be upgraded to git format when
1324 necessary.
1324 necessary.
1325
1325
1326 prefix is a filename prefix that is prepended to all filenames on
1326 prefix is a filename prefix that is prepended to all filenames on
1327 display (used for subrepos).
1327 display (used for subrepos).
1328 '''
1328 '''
1329
1329
1330 if opts is None:
1330 if opts is None:
1331 opts = mdiff.defaultopts
1331 opts = mdiff.defaultopts
1332
1332
1333 if not node1 and not node2:
1333 if not node1 and not node2:
1334 node1 = repo.dirstate.parents()[0]
1334 node1 = repo.dirstate.parents()[0]
1335
1335
1336 def lrugetfilectx():
1336 def lrugetfilectx():
1337 cache = {}
1337 cache = {}
1338 order = []
1338 order = []
1339 def getfilectx(f, ctx):
1339 def getfilectx(f, ctx):
1340 fctx = ctx.filectx(f, filelog=cache.get(f))
1340 fctx = ctx.filectx(f, filelog=cache.get(f))
1341 if f not in cache:
1341 if f not in cache:
1342 if len(cache) > 20:
1342 if len(cache) > 20:
1343 del cache[order.pop(0)]
1343 del cache[order.pop(0)]
1344 cache[f] = fctx.filelog()
1344 cache[f] = fctx.filelog()
1345 else:
1345 else:
1346 order.remove(f)
1346 order.remove(f)
1347 order.append(f)
1347 order.append(f)
1348 return fctx
1348 return fctx
1349 return getfilectx
1349 return getfilectx
1350 getfilectx = lrugetfilectx()
1350 getfilectx = lrugetfilectx()
1351
1351
1352 ctx1 = repo[node1]
1352 ctx1 = repo[node1]
1353 ctx2 = repo[node2]
1353 ctx2 = repo[node2]
1354
1354
1355 if not changes:
1355 if not changes:
1356 changes = repo.status(ctx1, ctx2, match=match)
1356 changes = repo.status(ctx1, ctx2, match=match)
1357 modified, added, removed = changes[:3]
1357 modified, added, removed = changes[:3]
1358
1358
1359 if not modified and not added and not removed:
1359 if not modified and not added and not removed:
1360 return []
1360 return []
1361
1361
1362 revs = None
1362 revs = None
1363 if not repo.ui.quiet:
1363 if not repo.ui.quiet:
1364 hexfunc = repo.ui.debugflag and hex or short
1364 hexfunc = repo.ui.debugflag and hex or short
1365 revs = [hexfunc(node) for node in [node1, node2] if node]
1365 revs = [hexfunc(node) for node in [node1, node2] if node]
1366
1366
1367 copy = {}
1367 copy = {}
1368 if opts.git or opts.upgrade:
1368 if opts.git or opts.upgrade:
1369 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1369 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1370
1370
1371 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1371 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1372 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1372 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1373 if opts.upgrade and not opts.git:
1373 if opts.upgrade and not opts.git:
1374 try:
1374 try:
1375 def losedata(fn):
1375 def losedata(fn):
1376 if not losedatafn or not losedatafn(fn=fn):
1376 if not losedatafn or not losedatafn(fn=fn):
1377 raise GitDiffRequired()
1377 raise GitDiffRequired()
1378 # Buffer the whole output until we are sure it can be generated
1378 # Buffer the whole output until we are sure it can be generated
1379 return list(difffn(opts.copy(git=False), losedata))
1379 return list(difffn(opts.copy(git=False), losedata))
1380 except GitDiffRequired:
1380 except GitDiffRequired:
1381 return difffn(opts.copy(git=True), None)
1381 return difffn(opts.copy(git=True), None)
1382 else:
1382 else:
1383 return difffn(opts, None)
1383 return difffn(opts, None)
1384
1384
1385 def difflabel(func, *args, **kw):
1385 def difflabel(func, *args, **kw):
1386 '''yields 2-tuples of (output, label) based on the output of func()'''
1386 '''yields 2-tuples of (output, label) based on the output of func()'''
1387 prefixes = [('diff', 'diff.diffline'),
1387 prefixes = [('diff', 'diff.diffline'),
1388 ('copy', 'diff.extended'),
1388 ('copy', 'diff.extended'),
1389 ('rename', 'diff.extended'),
1389 ('rename', 'diff.extended'),
1390 ('old', 'diff.extended'),
1390 ('old', 'diff.extended'),
1391 ('new', 'diff.extended'),
1391 ('new', 'diff.extended'),
1392 ('deleted', 'diff.extended'),
1392 ('deleted', 'diff.extended'),
1393 ('---', 'diff.file_a'),
1393 ('---', 'diff.file_a'),
1394 ('+++', 'diff.file_b'),
1394 ('+++', 'diff.file_b'),
1395 ('@@', 'diff.hunk'),
1395 ('@@', 'diff.hunk'),
1396 ('-', 'diff.deleted'),
1396 ('-', 'diff.deleted'),
1397 ('+', 'diff.inserted')]
1397 ('+', 'diff.inserted')]
1398
1398
1399 for chunk in func(*args, **kw):
1399 for chunk in func(*args, **kw):
1400 lines = chunk.split('\n')
1400 lines = chunk.split('\n')
1401 for i, line in enumerate(lines):
1401 for i, line in enumerate(lines):
1402 if i != 0:
1402 if i != 0:
1403 yield ('\n', '')
1403 yield ('\n', '')
1404 stripline = line
1404 stripline = line
1405 if line and line[0] in '+-':
1405 if line and line[0] in '+-':
1406 # highlight trailing whitespace, but only in changed lines
1406 # highlight trailing whitespace, but only in changed lines
1407 stripline = line.rstrip()
1407 stripline = line.rstrip()
1408 for prefix, label in prefixes:
1408 for prefix, label in prefixes:
1409 if stripline.startswith(prefix):
1409 if stripline.startswith(prefix):
1410 yield (stripline, label)
1410 yield (stripline, label)
1411 break
1411 break
1412 else:
1412 else:
1413 yield (line, '')
1413 yield (line, '')
1414 if line != stripline:
1414 if line != stripline:
1415 yield (line[len(stripline):], 'diff.trailingwhitespace')
1415 yield (line[len(stripline):], 'diff.trailingwhitespace')
1416
1416
1417 def diffui(*args, **kw):
1417 def diffui(*args, **kw):
1418 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1418 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1419 return difflabel(diff, *args, **kw)
1419 return difflabel(diff, *args, **kw)
1420
1420
1421
1421
1422 def _addmodehdr(header, omode, nmode):
1422 def _addmodehdr(header, omode, nmode):
1423 if omode != nmode:
1423 if omode != nmode:
1424 header.append('old mode %s\n' % omode)
1424 header.append('old mode %s\n' % omode)
1425 header.append('new mode %s\n' % nmode)
1425 header.append('new mode %s\n' % nmode)
1426
1426
1427 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1427 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1428 copy, getfilectx, opts, losedatafn, prefix):
1428 copy, getfilectx, opts, losedatafn, prefix):
1429
1429
1430 def join(f):
1430 def join(f):
1431 return os.path.join(prefix, f)
1431 return os.path.join(prefix, f)
1432
1432
1433 date1 = util.datestr(ctx1.date())
1433 date1 = util.datestr(ctx1.date())
1434 man1 = ctx1.manifest()
1434 man1 = ctx1.manifest()
1435
1435
1436 gone = set()
1436 gone = set()
1437 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1437 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1438
1438
1439 copyto = dict([(v, k) for k, v in copy.items()])
1439 copyto = dict([(v, k) for k, v in copy.items()])
1440
1440
1441 if opts.git:
1441 if opts.git:
1442 revs = None
1442 revs = None
1443
1443
1444 for f in sorted(modified + added + removed):
1444 for f in sorted(modified + added + removed):
1445 to = None
1445 to = None
1446 tn = None
1446 tn = None
1447 dodiff = True
1447 dodiff = True
1448 header = []
1448 header = []
1449 if f in man1:
1449 if f in man1:
1450 to = getfilectx(f, ctx1).data()
1450 to = getfilectx(f, ctx1).data()
1451 if f not in removed:
1451 if f not in removed:
1452 tn = getfilectx(f, ctx2).data()
1452 tn = getfilectx(f, ctx2).data()
1453 a, b = f, f
1453 a, b = f, f
1454 if opts.git or losedatafn:
1454 if opts.git or losedatafn:
1455 if f in added:
1455 if f in added:
1456 mode = gitmode[ctx2.flags(f)]
1456 mode = gitmode[ctx2.flags(f)]
1457 if f in copy or f in copyto:
1457 if f in copy or f in copyto:
1458 if opts.git:
1458 if opts.git:
1459 if f in copy:
1459 if f in copy:
1460 a = copy[f]
1460 a = copy[f]
1461 else:
1461 else:
1462 a = copyto[f]
1462 a = copyto[f]
1463 omode = gitmode[man1.flags(a)]
1463 omode = gitmode[man1.flags(a)]
1464 _addmodehdr(header, omode, mode)
1464 _addmodehdr(header, omode, mode)
1465 if a in removed and a not in gone:
1465 if a in removed and a not in gone:
1466 op = 'rename'
1466 op = 'rename'
1467 gone.add(a)
1467 gone.add(a)
1468 else:
1468 else:
1469 op = 'copy'
1469 op = 'copy'
1470 header.append('%s from %s\n' % (op, join(a)))
1470 header.append('%s from %s\n' % (op, join(a)))
1471 header.append('%s to %s\n' % (op, join(f)))
1471 header.append('%s to %s\n' % (op, join(f)))
1472 to = getfilectx(a, ctx1).data()
1472 to = getfilectx(a, ctx1).data()
1473 else:
1473 else:
1474 losedatafn(f)
1474 losedatafn(f)
1475 else:
1475 else:
1476 if opts.git:
1476 if opts.git:
1477 header.append('new file mode %s\n' % mode)
1477 header.append('new file mode %s\n' % mode)
1478 elif ctx2.flags(f):
1478 elif ctx2.flags(f):
1479 losedatafn(f)
1479 losedatafn(f)
1480 # In theory, if tn was copied or renamed we should check
1480 # In theory, if tn was copied or renamed we should check
1481 # if the source is binary too but the copy record already
1481 # if the source is binary too but the copy record already
1482 # forces git mode.
1482 # forces git mode.
1483 if util.binary(tn):
1483 if util.binary(tn):
1484 if opts.git:
1484 if opts.git:
1485 dodiff = 'binary'
1485 dodiff = 'binary'
1486 else:
1486 else:
1487 losedatafn(f)
1487 losedatafn(f)
1488 if not opts.git and not tn:
1488 if not opts.git and not tn:
1489 # regular diffs cannot represent new empty file
1489 # regular diffs cannot represent new empty file
1490 losedatafn(f)
1490 losedatafn(f)
1491 elif f in removed:
1491 elif f in removed:
1492 if opts.git:
1492 if opts.git:
1493 # have we already reported a copy above?
1493 # have we already reported a copy above?
1494 if ((f in copy and copy[f] in added
1494 if ((f in copy and copy[f] in added
1495 and copyto[copy[f]] == f) or
1495 and copyto[copy[f]] == f) or
1496 (f in copyto and copyto[f] in added
1496 (f in copyto and copyto[f] in added
1497 and copy[copyto[f]] == f)):
1497 and copy[copyto[f]] == f)):
1498 dodiff = False
1498 dodiff = False
1499 else:
1499 else:
1500 header.append('deleted file mode %s\n' %
1500 header.append('deleted file mode %s\n' %
1501 gitmode[man1.flags(f)])
1501 gitmode[man1.flags(f)])
1502 elif not to or util.binary(to):
1502 elif not to or util.binary(to):
1503 # regular diffs cannot represent empty file deletion
1503 # regular diffs cannot represent empty file deletion
1504 losedatafn(f)
1504 losedatafn(f)
1505 else:
1505 else:
1506 oflag = man1.flags(f)
1506 oflag = man1.flags(f)
1507 nflag = ctx2.flags(f)
1507 nflag = ctx2.flags(f)
1508 binary = util.binary(to) or util.binary(tn)
1508 binary = util.binary(to) or util.binary(tn)
1509 if opts.git:
1509 if opts.git:
1510 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1510 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1511 if binary:
1511 if binary:
1512 dodiff = 'binary'
1512 dodiff = 'binary'
1513 elif binary or nflag != oflag:
1513 elif binary or nflag != oflag:
1514 losedatafn(f)
1514 losedatafn(f)
1515 if opts.git:
1515 if opts.git:
1516 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1516 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1517
1517
1518 if dodiff:
1518 if dodiff:
1519 if dodiff == 'binary':
1519 if dodiff == 'binary':
1520 text = b85diff(to, tn)
1520 text = b85diff(to, tn)
1521 else:
1521 else:
1522 text = mdiff.unidiff(to, date1,
1522 text = mdiff.unidiff(to, date1,
1523 # ctx2 date may be dynamic
1523 # ctx2 date may be dynamic
1524 tn, util.datestr(ctx2.date()),
1524 tn, util.datestr(ctx2.date()),
1525 join(a), join(b), revs, opts=opts)
1525 join(a), join(b), revs, opts=opts)
1526 if header and (text or len(header) > 1):
1526 if header and (text or len(header) > 1):
1527 yield ''.join(header)
1527 yield ''.join(header)
1528 if text:
1528 if text:
1529 yield text
1529 yield text
1530
1530
1531 def diffstatdata(lines):
1531 def diffstatdata(lines):
1532 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1533
1532 filename, adds, removes = None, 0, 0
1534 filename, adds, removes = None, 0, 0
1533 for line in lines:
1535 for line in lines:
1534 if line.startswith('diff'):
1536 if line.startswith('diff'):
1535 if filename:
1537 if filename:
1536 isbinary = adds == 0 and removes == 0
1538 isbinary = adds == 0 and removes == 0
1537 yield (filename, adds, removes, isbinary)
1539 yield (filename, adds, removes, isbinary)
1538 # set numbers to 0 anyway when starting new file
1540 # set numbers to 0 anyway when starting new file
1539 adds, removes = 0, 0
1541 adds, removes = 0, 0
1540 if line.startswith('diff --git'):
1542 if line.startswith('diff --git'):
1541 filename = gitre.search(line).group(1)
1543 filename = gitre.search(line).group(1)
1542 else:
1544 elif line.startswith('diff -r'):
1543 # format: "diff -r ... -r ... filename"
1545 # format: "diff -r ... -r ... filename"
1544 filename = line.split(None, 5)[-1]
1546 filename = diffre.search(line).group(1)
1545 elif line.startswith('+') and not line.startswith('+++'):
1547 elif line.startswith('+') and not line.startswith('+++'):
1546 adds += 1
1548 adds += 1
1547 elif line.startswith('-') and not line.startswith('---'):
1549 elif line.startswith('-') and not line.startswith('---'):
1548 removes += 1
1550 removes += 1
1549 if filename:
1551 if filename:
1550 isbinary = adds == 0 and removes == 0
1552 isbinary = adds == 0 and removes == 0
1551 yield (filename, adds, removes, isbinary)
1553 yield (filename, adds, removes, isbinary)
1552
1554
1553 def diffstat(lines, width=80, git=False):
1555 def diffstat(lines, width=80, git=False):
1554 output = []
1556 output = []
1555 stats = list(diffstatdata(lines))
1557 stats = list(diffstatdata(lines))
1556
1558
1557 maxtotal, maxname = 0, 0
1559 maxtotal, maxname = 0, 0
1558 totaladds, totalremoves = 0, 0
1560 totaladds, totalremoves = 0, 0
1559 hasbinary = False
1561 hasbinary = False
1560
1562
1561 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1563 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1562 for filename, adds, removes, isbinary in stats]
1564 for filename, adds, removes, isbinary in stats]
1563
1565
1564 for filename, adds, removes, isbinary, namewidth in sized:
1566 for filename, adds, removes, isbinary, namewidth in sized:
1565 totaladds += adds
1567 totaladds += adds
1566 totalremoves += removes
1568 totalremoves += removes
1567 maxname = max(maxname, namewidth)
1569 maxname = max(maxname, namewidth)
1568 maxtotal = max(maxtotal, adds + removes)
1570 maxtotal = max(maxtotal, adds + removes)
1569 if isbinary:
1571 if isbinary:
1570 hasbinary = True
1572 hasbinary = True
1571
1573
1572 countwidth = len(str(maxtotal))
1574 countwidth = len(str(maxtotal))
1573 if hasbinary and countwidth < 3:
1575 if hasbinary and countwidth < 3:
1574 countwidth = 3
1576 countwidth = 3
1575 graphwidth = width - countwidth - maxname - 6
1577 graphwidth = width - countwidth - maxname - 6
1576 if graphwidth < 10:
1578 if graphwidth < 10:
1577 graphwidth = 10
1579 graphwidth = 10
1578
1580
1579 def scale(i):
1581 def scale(i):
1580 if maxtotal <= graphwidth:
1582 if maxtotal <= graphwidth:
1581 return i
1583 return i
1582 # If diffstat runs out of room it doesn't print anything,
1584 # If diffstat runs out of room it doesn't print anything,
1583 # which isn't very useful, so always print at least one + or -
1585 # which isn't very useful, so always print at least one + or -
1584 # if there were at least some changes.
1586 # if there were at least some changes.
1585 return max(i * graphwidth // maxtotal, int(bool(i)))
1587 return max(i * graphwidth // maxtotal, int(bool(i)))
1586
1588
1587 for filename, adds, removes, isbinary, namewidth in sized:
1589 for filename, adds, removes, isbinary, namewidth in sized:
1588 if git and isbinary:
1590 if git and isbinary:
1589 count = 'Bin'
1591 count = 'Bin'
1590 else:
1592 else:
1591 count = adds + removes
1593 count = adds + removes
1592 pluses = '+' * scale(adds)
1594 pluses = '+' * scale(adds)
1593 minuses = '-' * scale(removes)
1595 minuses = '-' * scale(removes)
1594 output.append(' %s%s | %*s %s%s\n' %
1596 output.append(' %s%s | %*s %s%s\n' %
1595 (filename, ' ' * (maxname - namewidth),
1597 (filename, ' ' * (maxname - namewidth),
1596 countwidth, count,
1598 countwidth, count,
1597 pluses, minuses))
1599 pluses, minuses))
1598
1600
1599 if stats:
1601 if stats:
1600 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1602 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1601 % (len(stats), totaladds, totalremoves))
1603 % (len(stats), totaladds, totalremoves))
1602
1604
1603 return ''.join(output)
1605 return ''.join(output)
1604
1606
1605 def diffstatui(*args, **kw):
1607 def diffstatui(*args, **kw):
1606 '''like diffstat(), but yields 2-tuples of (output, label) for
1608 '''like diffstat(), but yields 2-tuples of (output, label) for
1607 ui.write()
1609 ui.write()
1608 '''
1610 '''
1609
1611
1610 for line in diffstat(*args, **kw).splitlines():
1612 for line in diffstat(*args, **kw).splitlines():
1611 if line and line[-1] in '+-':
1613 if line and line[-1] in '+-':
1612 name, graph = line.rsplit(' ', 1)
1614 name, graph = line.rsplit(' ', 1)
1613 yield (name + ' ', '')
1615 yield (name + ' ', '')
1614 m = re.search(r'\++', graph)
1616 m = re.search(r'\++', graph)
1615 if m:
1617 if m:
1616 yield (m.group(0), 'diffstat.inserted')
1618 yield (m.group(0), 'diffstat.inserted')
1617 m = re.search(r'-+', graph)
1619 m = re.search(r'-+', graph)
1618 if m:
1620 if m:
1619 yield (m.group(0), 'diffstat.deleted')
1621 yield (m.group(0), 'diffstat.deleted')
1620 else:
1622 else:
1621 yield (line, '')
1623 yield (line, '')
1622 yield ('\n', '')
1624 yield ('\n', '')
@@ -1,48 +1,65 b''
1 $ hg init repo
1 $ hg init repo
2 $ cd repo
2 $ cd repo
3 $ i=0; while [ "$i" -lt 213 ]; do echo a >> a; i=`expr $i + 1`; done
3 $ i=0; while [ "$i" -lt 213 ]; do echo a >> a; i=`expr $i + 1`; done
4 $ hg add a
4 $ hg add a
5
5
6 Wide diffstat:
6 Wide diffstat:
7
7
8 $ hg diff --stat
8 $ hg diff --stat
9 a | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 a | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 1 files changed, 213 insertions(+), 0 deletions(-)
10 1 files changed, 213 insertions(+), 0 deletions(-)
11
11
12 diffstat width:
12 diffstat width:
13
13
14 $ COLUMNS=24 hg diff --config ui.interactive=true --stat
14 $ COLUMNS=24 hg diff --config ui.interactive=true --stat
15 a | 213 ++++++++++++++
15 a | 213 ++++++++++++++
16 1 files changed, 213 insertions(+), 0 deletions(-)
16 1 files changed, 213 insertions(+), 0 deletions(-)
17
17
18 $ hg ci -m adda
18 $ hg ci -m adda
19
19
20 $ cat >> a <<EOF
20 $ cat >> a <<EOF
21 > a
21 > a
22 > a
22 > a
23 > a
23 > a
24 > EOF
24 > EOF
25
25
26 Narrow diffstat:
26 Narrow diffstat:
27
27
28 $ hg diff --stat
28 $ hg diff --stat
29 a | 3 +++
29 a | 3 +++
30 1 files changed, 3 insertions(+), 0 deletions(-)
30 1 files changed, 3 insertions(+), 0 deletions(-)
31
31
32 $ hg ci -m appenda
32 $ hg ci -m appenda
33
33
34 $ printf '\0' > b
34 $ printf '\0' > b
35 $ hg add b
35 $ hg add b
36
36
37 Binary diffstat:
37 Binary diffstat:
38
38
39 $ hg diff --stat
39 $ hg diff --stat
40 b | 0
40 b | 0
41 1 files changed, 0 insertions(+), 0 deletions(-)
41 1 files changed, 0 insertions(+), 0 deletions(-)
42
42
43 Binary git diffstat:
43 Binary git diffstat:
44
44
45 $ hg diff --stat --git
45 $ hg diff --stat --git
46 b | Bin
46 b | Bin
47 1 files changed, 0 insertions(+), 0 deletions(-)
47 1 files changed, 0 insertions(+), 0 deletions(-)
48
48
49 $ hg ci -m createb
50
51 $ printf '\0' > "file with spaces"
52 $ hg add "file with spaces"
53
54 Filename with spaces diffstat:
55
56 $ hg diff --stat
57 file with spaces | 0
58 1 files changed, 0 insertions(+), 0 deletions(-)
59
60 Filename with spaces git diffstat:
61
62 $ hg diff --stat --git
63 file with spaces | Bin
64 1 files changed, 0 insertions(+), 0 deletions(-)
65
General Comments 0
You need to be logged in to leave comments. Login now