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