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