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