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