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