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