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