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