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