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