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