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