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