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