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