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