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