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