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