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