##// END OF EJS Templates
Make [PATCH] removal slightly more robust
Brendan Cully -
r4208:bd9b84b9 default
parent child Browse files
Show More
@@ -1,645 +1,645 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 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 from node import *
9 from node import *
10 import base85, cmdutil, mdiff, util, context, revlog
10 import base85, cmdutil, mdiff, util, context, revlog
11 import cStringIO, email.Parser, os, popen2, re, sha
11 import cStringIO, email.Parser, os, popen2, re, sha
12 import sys, tempfile, zlib
12 import sys, tempfile, zlib
13
13
14 # helper functions
14 # helper functions
15
15
16 def copyfile(src, dst, basedir=None):
16 def copyfile(src, dst, basedir=None):
17 if not basedir:
17 if not basedir:
18 basedir = os.getcwd()
18 basedir = os.getcwd()
19
19
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 if os.path.exists(absdst):
21 if os.path.exists(absdst):
22 raise util.Abort(_("cannot create %s: destination already exists") %
22 raise util.Abort(_("cannot create %s: destination already exists") %
23 dst)
23 dst)
24
24
25 targetdir = os.path.dirname(absdst)
25 targetdir = os.path.dirname(absdst)
26 if not os.path.isdir(targetdir):
26 if not os.path.isdir(targetdir):
27 os.makedirs(targetdir)
27 os.makedirs(targetdir)
28
28
29 util.copyfile(abssrc, absdst)
29 util.copyfile(abssrc, absdst)
30
30
31 # public functions
31 # public functions
32
32
33 def extract(ui, fileobj):
33 def extract(ui, fileobj):
34 '''extract patch from data read from fileobj.
34 '''extract patch from data read from fileobj.
35
35
36 patch can be normal patch or contained in email message.
36 patch can be normal patch or contained in email message.
37
37
38 return tuple (filename, message, user, date). any item in returned
38 return tuple (filename, message, user, date). any item in returned
39 tuple can be None. if filename is None, fileobj did not contain
39 tuple can be None. if filename is None, fileobj did not contain
40 patch. caller must unlink filename when done.'''
40 patch. caller must unlink filename when done.'''
41
41
42 # attempt to detect the start of a patch
42 # attempt to detect the start of a patch
43 # (this heuristic is borrowed from quilt)
43 # (this heuristic is borrowed from quilt)
44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 '(---|\*\*\*)[ \t])', re.MULTILINE)
46 '(---|\*\*\*)[ \t])', re.MULTILINE)
47
47
48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 tmpfp = os.fdopen(fd, 'w')
49 tmpfp = os.fdopen(fd, 'w')
50 try:
50 try:
51 hgpatch = False
51 hgpatch = False
52
52
53 msg = email.Parser.Parser().parse(fileobj)
53 msg = email.Parser.Parser().parse(fileobj)
54
54
55 message = msg['Subject']
55 message = msg['Subject']
56 user = msg['From']
56 user = msg['From']
57 # should try to parse msg['Date']
57 # should try to parse msg['Date']
58 date = None
58 date = None
59
59
60 if message:
60 if message:
61 if message.startswith('[PATCH'):
61 if message.startswith('[PATCH'):
62 pend = message.find(']')
62 pend = message.find(']')
63 if pend:
63 if pend >= 0:
64 message = message[pend+2:]
64 message = message[pend+1:].lstrip()
65 message = message.replace('\n\t', ' ')
65 message = message.replace('\n\t', ' ')
66 ui.debug('Subject: %s\n' % message)
66 ui.debug('Subject: %s\n' % message)
67 if user:
67 if user:
68 ui.debug('From: %s\n' % user)
68 ui.debug('From: %s\n' % user)
69 diffs_seen = 0
69 diffs_seen = 0
70 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
70 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
71
71
72 for part in msg.walk():
72 for part in msg.walk():
73 content_type = part.get_content_type()
73 content_type = part.get_content_type()
74 ui.debug('Content-Type: %s\n' % content_type)
74 ui.debug('Content-Type: %s\n' % content_type)
75 if content_type not in ok_types:
75 if content_type not in ok_types:
76 continue
76 continue
77 payload = part.get_payload(decode=True)
77 payload = part.get_payload(decode=True)
78 m = diffre.search(payload)
78 m = diffre.search(payload)
79 if m:
79 if m:
80 ui.debug(_('found patch at byte %d\n') % m.start(0))
80 ui.debug(_('found patch at byte %d\n') % m.start(0))
81 diffs_seen += 1
81 diffs_seen += 1
82 cfp = cStringIO.StringIO()
82 cfp = cStringIO.StringIO()
83 if message:
83 if message:
84 cfp.write(message)
84 cfp.write(message)
85 cfp.write('\n')
85 cfp.write('\n')
86 for line in payload[:m.start(0)].splitlines():
86 for line in payload[:m.start(0)].splitlines():
87 if line.startswith('# HG changeset patch'):
87 if line.startswith('# HG changeset patch'):
88 ui.debug(_('patch generated by hg export\n'))
88 ui.debug(_('patch generated by hg export\n'))
89 hgpatch = True
89 hgpatch = True
90 # drop earlier commit message content
90 # drop earlier commit message content
91 cfp.seek(0)
91 cfp.seek(0)
92 cfp.truncate()
92 cfp.truncate()
93 elif hgpatch:
93 elif hgpatch:
94 if line.startswith('# User '):
94 if line.startswith('# User '):
95 user = line[7:]
95 user = line[7:]
96 ui.debug('From: %s\n' % user)
96 ui.debug('From: %s\n' % user)
97 elif line.startswith("# Date "):
97 elif line.startswith("# Date "):
98 date = line[7:]
98 date = line[7:]
99 if not line.startswith('# '):
99 if not line.startswith('# '):
100 cfp.write(line)
100 cfp.write(line)
101 cfp.write('\n')
101 cfp.write('\n')
102 message = cfp.getvalue()
102 message = cfp.getvalue()
103 if tmpfp:
103 if tmpfp:
104 tmpfp.write(payload)
104 tmpfp.write(payload)
105 if not payload.endswith('\n'):
105 if not payload.endswith('\n'):
106 tmpfp.write('\n')
106 tmpfp.write('\n')
107 elif not diffs_seen and message and content_type == 'text/plain':
107 elif not diffs_seen and message and content_type == 'text/plain':
108 message += '\n' + payload
108 message += '\n' + payload
109 except:
109 except:
110 tmpfp.close()
110 tmpfp.close()
111 os.unlink(tmpname)
111 os.unlink(tmpname)
112 raise
112 raise
113
113
114 tmpfp.close()
114 tmpfp.close()
115 if not diffs_seen:
115 if not diffs_seen:
116 os.unlink(tmpname)
116 os.unlink(tmpname)
117 return None, message, user, date
117 return None, message, user, date
118 return tmpname, message, user, date
118 return tmpname, message, user, date
119
119
120 GP_PATCH = 1 << 0 # we have to run patch
120 GP_PATCH = 1 << 0 # we have to run patch
121 GP_FILTER = 1 << 1 # there's some copy/rename operation
121 GP_FILTER = 1 << 1 # there's some copy/rename operation
122 GP_BINARY = 1 << 2 # there's a binary patch
122 GP_BINARY = 1 << 2 # there's a binary patch
123
123
124 def readgitpatch(patchname):
124 def readgitpatch(patchname):
125 """extract git-style metadata about patches from <patchname>"""
125 """extract git-style metadata about patches from <patchname>"""
126 class gitpatch:
126 class gitpatch:
127 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
127 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
128 def __init__(self, path):
128 def __init__(self, path):
129 self.path = path
129 self.path = path
130 self.oldpath = None
130 self.oldpath = None
131 self.mode = None
131 self.mode = None
132 self.op = 'MODIFY'
132 self.op = 'MODIFY'
133 self.copymod = False
133 self.copymod = False
134 self.lineno = 0
134 self.lineno = 0
135 self.binary = False
135 self.binary = False
136
136
137 # Filter patch for git information
137 # Filter patch for git information
138 gitre = re.compile('diff --git a/(.*) b/(.*)')
138 gitre = re.compile('diff --git a/(.*) b/(.*)')
139 pf = file(patchname)
139 pf = file(patchname)
140 gp = None
140 gp = None
141 gitpatches = []
141 gitpatches = []
142 # Can have a git patch with only metadata, causing patch to complain
142 # Can have a git patch with only metadata, causing patch to complain
143 dopatch = 0
143 dopatch = 0
144
144
145 lineno = 0
145 lineno = 0
146 for line in pf:
146 for line in pf:
147 lineno += 1
147 lineno += 1
148 if line.startswith('diff --git'):
148 if line.startswith('diff --git'):
149 m = gitre.match(line)
149 m = gitre.match(line)
150 if m:
150 if m:
151 if gp:
151 if gp:
152 gitpatches.append(gp)
152 gitpatches.append(gp)
153 src, dst = m.group(1, 2)
153 src, dst = m.group(1, 2)
154 gp = gitpatch(dst)
154 gp = gitpatch(dst)
155 gp.lineno = lineno
155 gp.lineno = lineno
156 elif gp:
156 elif gp:
157 if line.startswith('--- '):
157 if line.startswith('--- '):
158 if gp.op in ('COPY', 'RENAME'):
158 if gp.op in ('COPY', 'RENAME'):
159 gp.copymod = True
159 gp.copymod = True
160 dopatch |= GP_FILTER
160 dopatch |= GP_FILTER
161 gitpatches.append(gp)
161 gitpatches.append(gp)
162 gp = None
162 gp = None
163 dopatch |= GP_PATCH
163 dopatch |= GP_PATCH
164 continue
164 continue
165 if line.startswith('rename from '):
165 if line.startswith('rename from '):
166 gp.op = 'RENAME'
166 gp.op = 'RENAME'
167 gp.oldpath = line[12:].rstrip()
167 gp.oldpath = line[12:].rstrip()
168 elif line.startswith('rename to '):
168 elif line.startswith('rename to '):
169 gp.path = line[10:].rstrip()
169 gp.path = line[10:].rstrip()
170 elif line.startswith('copy from '):
170 elif line.startswith('copy from '):
171 gp.op = 'COPY'
171 gp.op = 'COPY'
172 gp.oldpath = line[10:].rstrip()
172 gp.oldpath = line[10:].rstrip()
173 elif line.startswith('copy to '):
173 elif line.startswith('copy to '):
174 gp.path = line[8:].rstrip()
174 gp.path = line[8:].rstrip()
175 elif line.startswith('deleted file'):
175 elif line.startswith('deleted file'):
176 gp.op = 'DELETE'
176 gp.op = 'DELETE'
177 elif line.startswith('new file mode '):
177 elif line.startswith('new file mode '):
178 gp.op = 'ADD'
178 gp.op = 'ADD'
179 gp.mode = int(line.rstrip()[-3:], 8)
179 gp.mode = int(line.rstrip()[-3:], 8)
180 elif line.startswith('new mode '):
180 elif line.startswith('new mode '):
181 gp.mode = int(line.rstrip()[-3:], 8)
181 gp.mode = int(line.rstrip()[-3:], 8)
182 elif line.startswith('GIT binary patch'):
182 elif line.startswith('GIT binary patch'):
183 dopatch |= GP_BINARY
183 dopatch |= GP_BINARY
184 gp.binary = True
184 gp.binary = True
185 if gp:
185 if gp:
186 gitpatches.append(gp)
186 gitpatches.append(gp)
187
187
188 if not gitpatches:
188 if not gitpatches:
189 dopatch = GP_PATCH
189 dopatch = GP_PATCH
190
190
191 return (dopatch, gitpatches)
191 return (dopatch, gitpatches)
192
192
193 def dogitpatch(patchname, gitpatches, cwd=None):
193 def dogitpatch(patchname, gitpatches, cwd=None):
194 """Preprocess git patch so that vanilla patch can handle it"""
194 """Preprocess git patch so that vanilla patch can handle it"""
195 def extractbin(fp):
195 def extractbin(fp):
196 i = [0] # yuck
196 i = [0] # yuck
197 def readline():
197 def readline():
198 i[0] += 1
198 i[0] += 1
199 return fp.readline().rstrip()
199 return fp.readline().rstrip()
200 line = readline()
200 line = readline()
201 while line and not line.startswith('literal '):
201 while line and not line.startswith('literal '):
202 line = readline()
202 line = readline()
203 if not line:
203 if not line:
204 return None, i[0]
204 return None, i[0]
205 size = int(line[8:])
205 size = int(line[8:])
206 dec = []
206 dec = []
207 line = readline()
207 line = readline()
208 while line:
208 while line:
209 l = line[0]
209 l = line[0]
210 if l <= 'Z' and l >= 'A':
210 if l <= 'Z' and l >= 'A':
211 l = ord(l) - ord('A') + 1
211 l = ord(l) - ord('A') + 1
212 else:
212 else:
213 l = ord(l) - ord('a') + 27
213 l = ord(l) - ord('a') + 27
214 dec.append(base85.b85decode(line[1:])[:l])
214 dec.append(base85.b85decode(line[1:])[:l])
215 line = readline()
215 line = readline()
216 text = zlib.decompress(''.join(dec))
216 text = zlib.decompress(''.join(dec))
217 if len(text) != size:
217 if len(text) != size:
218 raise util.Abort(_('binary patch is %d bytes, not %d') %
218 raise util.Abort(_('binary patch is %d bytes, not %d') %
219 (len(text), size))
219 (len(text), size))
220 return text, i[0]
220 return text, i[0]
221
221
222 pf = file(patchname)
222 pf = file(patchname)
223 pfline = 1
223 pfline = 1
224
224
225 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
225 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
226 tmpfp = os.fdopen(fd, 'w')
226 tmpfp = os.fdopen(fd, 'w')
227
227
228 try:
228 try:
229 for i in xrange(len(gitpatches)):
229 for i in xrange(len(gitpatches)):
230 p = gitpatches[i]
230 p = gitpatches[i]
231 if not p.copymod and not p.binary:
231 if not p.copymod and not p.binary:
232 continue
232 continue
233
233
234 # rewrite patch hunk
234 # rewrite patch hunk
235 while pfline < p.lineno:
235 while pfline < p.lineno:
236 tmpfp.write(pf.readline())
236 tmpfp.write(pf.readline())
237 pfline += 1
237 pfline += 1
238
238
239 if p.binary:
239 if p.binary:
240 text, delta = extractbin(pf)
240 text, delta = extractbin(pf)
241 if not text:
241 if not text:
242 raise util.Abort(_('binary patch extraction failed'))
242 raise util.Abort(_('binary patch extraction failed'))
243 pfline += delta
243 pfline += delta
244 if not cwd:
244 if not cwd:
245 cwd = os.getcwd()
245 cwd = os.getcwd()
246 absdst = os.path.join(cwd, p.path)
246 absdst = os.path.join(cwd, p.path)
247 basedir = os.path.dirname(absdst)
247 basedir = os.path.dirname(absdst)
248 if not os.path.isdir(basedir):
248 if not os.path.isdir(basedir):
249 os.makedirs(basedir)
249 os.makedirs(basedir)
250 out = file(absdst, 'wb')
250 out = file(absdst, 'wb')
251 out.write(text)
251 out.write(text)
252 out.close()
252 out.close()
253 elif p.copymod:
253 elif p.copymod:
254 copyfile(p.oldpath, p.path, basedir=cwd)
254 copyfile(p.oldpath, p.path, basedir=cwd)
255 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
255 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
256 line = pf.readline()
256 line = pf.readline()
257 pfline += 1
257 pfline += 1
258 while not line.startswith('--- a/'):
258 while not line.startswith('--- a/'):
259 tmpfp.write(line)
259 tmpfp.write(line)
260 line = pf.readline()
260 line = pf.readline()
261 pfline += 1
261 pfline += 1
262 tmpfp.write('--- a/%s\n' % p.path)
262 tmpfp.write('--- a/%s\n' % p.path)
263
263
264 line = pf.readline()
264 line = pf.readline()
265 while line:
265 while line:
266 tmpfp.write(line)
266 tmpfp.write(line)
267 line = pf.readline()
267 line = pf.readline()
268 except:
268 except:
269 tmpfp.close()
269 tmpfp.close()
270 os.unlink(patchname)
270 os.unlink(patchname)
271 raise
271 raise
272
272
273 tmpfp.close()
273 tmpfp.close()
274 return patchname
274 return patchname
275
275
276 def patch(patchname, ui, strip=1, cwd=None, files={}):
276 def patch(patchname, ui, strip=1, cwd=None, files={}):
277 """apply the patch <patchname> to the working directory.
277 """apply the patch <patchname> to the working directory.
278 a list of patched files is returned"""
278 a list of patched files is returned"""
279
279
280 # helper function
280 # helper function
281 def __patch(patchname):
281 def __patch(patchname):
282 """patch and updates the files and fuzz variables"""
282 """patch and updates the files and fuzz variables"""
283 fuzz = False
283 fuzz = False
284
284
285 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
285 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
286 'patch')
286 'patch')
287 args = []
287 args = []
288 if cwd:
288 if cwd:
289 args.append('-d %s' % util.shellquote(cwd))
289 args.append('-d %s' % util.shellquote(cwd))
290 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
290 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
291 util.shellquote(patchname)))
291 util.shellquote(patchname)))
292
292
293 for line in fp:
293 for line in fp:
294 line = line.rstrip()
294 line = line.rstrip()
295 ui.note(line + '\n')
295 ui.note(line + '\n')
296 if line.startswith('patching file '):
296 if line.startswith('patching file '):
297 pf = util.parse_patch_output(line)
297 pf = util.parse_patch_output(line)
298 printed_file = False
298 printed_file = False
299 files.setdefault(pf, (None, None))
299 files.setdefault(pf, (None, None))
300 elif line.find('with fuzz') >= 0:
300 elif line.find('with fuzz') >= 0:
301 fuzz = True
301 fuzz = True
302 if not printed_file:
302 if not printed_file:
303 ui.warn(pf + '\n')
303 ui.warn(pf + '\n')
304 printed_file = True
304 printed_file = True
305 ui.warn(line + '\n')
305 ui.warn(line + '\n')
306 elif line.find('saving rejects to file') >= 0:
306 elif line.find('saving rejects to file') >= 0:
307 ui.warn(line + '\n')
307 ui.warn(line + '\n')
308 elif line.find('FAILED') >= 0:
308 elif line.find('FAILED') >= 0:
309 if not printed_file:
309 if not printed_file:
310 ui.warn(pf + '\n')
310 ui.warn(pf + '\n')
311 printed_file = True
311 printed_file = True
312 ui.warn(line + '\n')
312 ui.warn(line + '\n')
313 code = fp.close()
313 code = fp.close()
314 if code:
314 if code:
315 raise util.Abort(_("patch command failed: %s") %
315 raise util.Abort(_("patch command failed: %s") %
316 util.explain_exit(code)[0])
316 util.explain_exit(code)[0])
317 return fuzz
317 return fuzz
318
318
319 (dopatch, gitpatches) = readgitpatch(patchname)
319 (dopatch, gitpatches) = readgitpatch(patchname)
320 for gp in gitpatches:
320 for gp in gitpatches:
321 files[gp.path] = (gp.op, gp)
321 files[gp.path] = (gp.op, gp)
322
322
323 fuzz = False
323 fuzz = False
324 if dopatch:
324 if dopatch:
325 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
325 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
326 if filterpatch:
326 if filterpatch:
327 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
327 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
328 try:
328 try:
329 if dopatch & GP_PATCH:
329 if dopatch & GP_PATCH:
330 fuzz = __patch(patchname)
330 fuzz = __patch(patchname)
331 finally:
331 finally:
332 if filterpatch:
332 if filterpatch:
333 os.unlink(patchname)
333 os.unlink(patchname)
334
334
335 return fuzz
335 return fuzz
336
336
337 def diffopts(ui, opts={}, untrusted=False):
337 def diffopts(ui, opts={}, untrusted=False):
338 def get(key, name=None):
338 def get(key, name=None):
339 return (opts.get(key) or
339 return (opts.get(key) or
340 ui.configbool('diff', name or key, None, untrusted=untrusted))
340 ui.configbool('diff', name or key, None, untrusted=untrusted))
341 return mdiff.diffopts(
341 return mdiff.diffopts(
342 text=opts.get('text'),
342 text=opts.get('text'),
343 git=get('git'),
343 git=get('git'),
344 nodates=get('nodates'),
344 nodates=get('nodates'),
345 showfunc=get('show_function', 'showfunc'),
345 showfunc=get('show_function', 'showfunc'),
346 ignorews=get('ignore_all_space', 'ignorews'),
346 ignorews=get('ignore_all_space', 'ignorews'),
347 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
347 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
348 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
348 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
349
349
350 def updatedir(ui, repo, patches, wlock=None):
350 def updatedir(ui, repo, patches, wlock=None):
351 '''Update dirstate after patch application according to metadata'''
351 '''Update dirstate after patch application according to metadata'''
352 if not patches:
352 if not patches:
353 return
353 return
354 copies = []
354 copies = []
355 removes = {}
355 removes = {}
356 cfiles = patches.keys()
356 cfiles = patches.keys()
357 cwd = repo.getcwd()
357 cwd = repo.getcwd()
358 if cwd:
358 if cwd:
359 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
359 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
360 for f in patches:
360 for f in patches:
361 ctype, gp = patches[f]
361 ctype, gp = patches[f]
362 if ctype == 'RENAME':
362 if ctype == 'RENAME':
363 copies.append((gp.oldpath, gp.path, gp.copymod))
363 copies.append((gp.oldpath, gp.path, gp.copymod))
364 removes[gp.oldpath] = 1
364 removes[gp.oldpath] = 1
365 elif ctype == 'COPY':
365 elif ctype == 'COPY':
366 copies.append((gp.oldpath, gp.path, gp.copymod))
366 copies.append((gp.oldpath, gp.path, gp.copymod))
367 elif ctype == 'DELETE':
367 elif ctype == 'DELETE':
368 removes[gp.path] = 1
368 removes[gp.path] = 1
369 for src, dst, after in copies:
369 for src, dst, after in copies:
370 if not after:
370 if not after:
371 copyfile(src, dst, repo.root)
371 copyfile(src, dst, repo.root)
372 repo.copy(src, dst, wlock=wlock)
372 repo.copy(src, dst, wlock=wlock)
373 removes = removes.keys()
373 removes = removes.keys()
374 if removes:
374 if removes:
375 removes.sort()
375 removes.sort()
376 repo.remove(removes, True, wlock=wlock)
376 repo.remove(removes, True, wlock=wlock)
377 for f in patches:
377 for f in patches:
378 ctype, gp = patches[f]
378 ctype, gp = patches[f]
379 if gp and gp.mode:
379 if gp and gp.mode:
380 x = gp.mode & 0100 != 0
380 x = gp.mode & 0100 != 0
381 dst = os.path.join(repo.root, gp.path)
381 dst = os.path.join(repo.root, gp.path)
382 # patch won't create empty files
382 # patch won't create empty files
383 if ctype == 'ADD' and not os.path.exists(dst):
383 if ctype == 'ADD' and not os.path.exists(dst):
384 repo.wwrite(gp.path, '', x and 'x' or '')
384 repo.wwrite(gp.path, '', x and 'x' or '')
385 else:
385 else:
386 util.set_exec(dst, x)
386 util.set_exec(dst, x)
387 cmdutil.addremove(repo, cfiles, wlock=wlock)
387 cmdutil.addremove(repo, cfiles, wlock=wlock)
388 files = patches.keys()
388 files = patches.keys()
389 files.extend([r for r in removes if r not in files])
389 files.extend([r for r in removes if r not in files])
390 files.sort()
390 files.sort()
391
391
392 return files
392 return files
393
393
394 def b85diff(fp, to, tn):
394 def b85diff(fp, to, tn):
395 '''print base85-encoded binary diff'''
395 '''print base85-encoded binary diff'''
396 def gitindex(text):
396 def gitindex(text):
397 if not text:
397 if not text:
398 return '0' * 40
398 return '0' * 40
399 l = len(text)
399 l = len(text)
400 s = sha.new('blob %d\0' % l)
400 s = sha.new('blob %d\0' % l)
401 s.update(text)
401 s.update(text)
402 return s.hexdigest()
402 return s.hexdigest()
403
403
404 def fmtline(line):
404 def fmtline(line):
405 l = len(line)
405 l = len(line)
406 if l <= 26:
406 if l <= 26:
407 l = chr(ord('A') + l - 1)
407 l = chr(ord('A') + l - 1)
408 else:
408 else:
409 l = chr(l - 26 + ord('a') - 1)
409 l = chr(l - 26 + ord('a') - 1)
410 return '%c%s\n' % (l, base85.b85encode(line, True))
410 return '%c%s\n' % (l, base85.b85encode(line, True))
411
411
412 def chunk(text, csize=52):
412 def chunk(text, csize=52):
413 l = len(text)
413 l = len(text)
414 i = 0
414 i = 0
415 while i < l:
415 while i < l:
416 yield text[i:i+csize]
416 yield text[i:i+csize]
417 i += csize
417 i += csize
418
418
419 tohash = gitindex(to)
419 tohash = gitindex(to)
420 tnhash = gitindex(tn)
420 tnhash = gitindex(tn)
421 if tohash == tnhash:
421 if tohash == tnhash:
422 return ""
422 return ""
423
423
424 # TODO: deltas
424 # TODO: deltas
425 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
425 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
426 (tohash, tnhash, len(tn))]
426 (tohash, tnhash, len(tn))]
427 for l in chunk(zlib.compress(tn)):
427 for l in chunk(zlib.compress(tn)):
428 ret.append(fmtline(l))
428 ret.append(fmtline(l))
429 ret.append('\n')
429 ret.append('\n')
430 return ''.join(ret)
430 return ''.join(ret)
431
431
432 def diff(repo, node1=None, node2=None, files=None, match=util.always,
432 def diff(repo, node1=None, node2=None, files=None, match=util.always,
433 fp=None, changes=None, opts=None):
433 fp=None, changes=None, opts=None):
434 '''print diff of changes to files between two nodes, or node and
434 '''print diff of changes to files between two nodes, or node and
435 working directory.
435 working directory.
436
436
437 if node1 is None, use first dirstate parent instead.
437 if node1 is None, use first dirstate parent instead.
438 if node2 is None, compare node1 with working directory.'''
438 if node2 is None, compare node1 with working directory.'''
439
439
440 if opts is None:
440 if opts is None:
441 opts = mdiff.defaultopts
441 opts = mdiff.defaultopts
442 if fp is None:
442 if fp is None:
443 fp = repo.ui
443 fp = repo.ui
444
444
445 if not node1:
445 if not node1:
446 node1 = repo.dirstate.parents()[0]
446 node1 = repo.dirstate.parents()[0]
447
447
448 ccache = {}
448 ccache = {}
449 def getctx(r):
449 def getctx(r):
450 if r not in ccache:
450 if r not in ccache:
451 ccache[r] = context.changectx(repo, r)
451 ccache[r] = context.changectx(repo, r)
452 return ccache[r]
452 return ccache[r]
453
453
454 flcache = {}
454 flcache = {}
455 def getfilectx(f, ctx):
455 def getfilectx(f, ctx):
456 flctx = ctx.filectx(f, filelog=flcache.get(f))
456 flctx = ctx.filectx(f, filelog=flcache.get(f))
457 if f not in flcache:
457 if f not in flcache:
458 flcache[f] = flctx._filelog
458 flcache[f] = flctx._filelog
459 return flctx
459 return flctx
460
460
461 # reading the data for node1 early allows it to play nicely
461 # reading the data for node1 early allows it to play nicely
462 # with repo.status and the revlog cache.
462 # with repo.status and the revlog cache.
463 ctx1 = context.changectx(repo, node1)
463 ctx1 = context.changectx(repo, node1)
464 # force manifest reading
464 # force manifest reading
465 man1 = ctx1.manifest()
465 man1 = ctx1.manifest()
466 date1 = util.datestr(ctx1.date())
466 date1 = util.datestr(ctx1.date())
467
467
468 if not changes:
468 if not changes:
469 changes = repo.status(node1, node2, files, match=match)[:5]
469 changes = repo.status(node1, node2, files, match=match)[:5]
470 modified, added, removed, deleted, unknown = changes
470 modified, added, removed, deleted, unknown = changes
471
471
472 if not modified and not added and not removed:
472 if not modified and not added and not removed:
473 return
473 return
474
474
475 if node2:
475 if node2:
476 ctx2 = context.changectx(repo, node2)
476 ctx2 = context.changectx(repo, node2)
477 else:
477 else:
478 ctx2 = context.workingctx(repo)
478 ctx2 = context.workingctx(repo)
479 man2 = ctx2.manifest()
479 man2 = ctx2.manifest()
480
480
481 # returns False if there was no rename between ctx1 and ctx2
481 # returns False if there was no rename between ctx1 and ctx2
482 # returns None if the file was created between ctx1 and ctx2
482 # returns None if the file was created between ctx1 and ctx2
483 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
483 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
484 def renamed(f):
484 def renamed(f):
485 startrev = ctx1.rev()
485 startrev = ctx1.rev()
486 c = ctx2
486 c = ctx2
487 crev = c.rev()
487 crev = c.rev()
488 if crev is None:
488 if crev is None:
489 crev = repo.changelog.count()
489 crev = repo.changelog.count()
490 orig = f
490 orig = f
491 while crev > startrev:
491 while crev > startrev:
492 if f in c.files():
492 if f in c.files():
493 try:
493 try:
494 src = getfilectx(f, c).renamed()
494 src = getfilectx(f, c).renamed()
495 except revlog.LookupError:
495 except revlog.LookupError:
496 return None
496 return None
497 if src:
497 if src:
498 f = src[0]
498 f = src[0]
499 crev = c.parents()[0].rev()
499 crev = c.parents()[0].rev()
500 # try to reuse
500 # try to reuse
501 c = getctx(crev)
501 c = getctx(crev)
502 if f not in man1:
502 if f not in man1:
503 return None
503 return None
504 if f == orig:
504 if f == orig:
505 return False
505 return False
506 return f
506 return f
507
507
508 if repo.ui.quiet:
508 if repo.ui.quiet:
509 r = None
509 r = None
510 else:
510 else:
511 hexfunc = repo.ui.debugflag and hex or short
511 hexfunc = repo.ui.debugflag and hex or short
512 r = [hexfunc(node) for node in [node1, node2] if node]
512 r = [hexfunc(node) for node in [node1, node2] if node]
513
513
514 if opts.git:
514 if opts.git:
515 copied = {}
515 copied = {}
516 for f in added:
516 for f in added:
517 src = renamed(f)
517 src = renamed(f)
518 if src:
518 if src:
519 copied[f] = src
519 copied[f] = src
520 srcs = [x[1] for x in copied.items()]
520 srcs = [x[1] for x in copied.items()]
521
521
522 all = modified + added + removed
522 all = modified + added + removed
523 all.sort()
523 all.sort()
524 gone = {}
524 gone = {}
525
525
526 for f in all:
526 for f in all:
527 to = None
527 to = None
528 tn = None
528 tn = None
529 dodiff = True
529 dodiff = True
530 header = []
530 header = []
531 if f in man1:
531 if f in man1:
532 to = getfilectx(f, ctx1).data()
532 to = getfilectx(f, ctx1).data()
533 if f not in removed:
533 if f not in removed:
534 tn = getfilectx(f, ctx2).data()
534 tn = getfilectx(f, ctx2).data()
535 if opts.git:
535 if opts.git:
536 def gitmode(x):
536 def gitmode(x):
537 return x and '100755' or '100644'
537 return x and '100755' or '100644'
538 def addmodehdr(header, omode, nmode):
538 def addmodehdr(header, omode, nmode):
539 if omode != nmode:
539 if omode != nmode:
540 header.append('old mode %s\n' % omode)
540 header.append('old mode %s\n' % omode)
541 header.append('new mode %s\n' % nmode)
541 header.append('new mode %s\n' % nmode)
542
542
543 a, b = f, f
543 a, b = f, f
544 if f in added:
544 if f in added:
545 mode = gitmode(man2.execf(f))
545 mode = gitmode(man2.execf(f))
546 if f in copied:
546 if f in copied:
547 a = copied[f]
547 a = copied[f]
548 omode = gitmode(man1.execf(a))
548 omode = gitmode(man1.execf(a))
549 addmodehdr(header, omode, mode)
549 addmodehdr(header, omode, mode)
550 if a in removed and a not in gone:
550 if a in removed and a not in gone:
551 op = 'rename'
551 op = 'rename'
552 gone[a] = 1
552 gone[a] = 1
553 else:
553 else:
554 op = 'copy'
554 op = 'copy'
555 header.append('%s from %s\n' % (op, a))
555 header.append('%s from %s\n' % (op, a))
556 header.append('%s to %s\n' % (op, f))
556 header.append('%s to %s\n' % (op, f))
557 to = getfilectx(a, ctx1).data()
557 to = getfilectx(a, ctx1).data()
558 else:
558 else:
559 header.append('new file mode %s\n' % mode)
559 header.append('new file mode %s\n' % mode)
560 if util.binary(tn):
560 if util.binary(tn):
561 dodiff = 'binary'
561 dodiff = 'binary'
562 elif f in removed:
562 elif f in removed:
563 if f in srcs:
563 if f in srcs:
564 dodiff = False
564 dodiff = False
565 else:
565 else:
566 mode = gitmode(man1.execf(f))
566 mode = gitmode(man1.execf(f))
567 header.append('deleted file mode %s\n' % mode)
567 header.append('deleted file mode %s\n' % mode)
568 else:
568 else:
569 omode = gitmode(man1.execf(f))
569 omode = gitmode(man1.execf(f))
570 nmode = gitmode(man2.execf(f))
570 nmode = gitmode(man2.execf(f))
571 addmodehdr(header, omode, nmode)
571 addmodehdr(header, omode, nmode)
572 if util.binary(to) or util.binary(tn):
572 if util.binary(to) or util.binary(tn):
573 dodiff = 'binary'
573 dodiff = 'binary'
574 r = None
574 r = None
575 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
575 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
576 if dodiff:
576 if dodiff:
577 if dodiff == 'binary':
577 if dodiff == 'binary':
578 text = b85diff(fp, to, tn)
578 text = b85diff(fp, to, tn)
579 else:
579 else:
580 text = mdiff.unidiff(to, date1,
580 text = mdiff.unidiff(to, date1,
581 # ctx2 date may be dynamic
581 # ctx2 date may be dynamic
582 tn, util.datestr(ctx2.date()),
582 tn, util.datestr(ctx2.date()),
583 f, r, opts=opts)
583 f, r, opts=opts)
584 if text or len(header) > 1:
584 if text or len(header) > 1:
585 fp.write(''.join(header))
585 fp.write(''.join(header))
586 fp.write(text)
586 fp.write(text)
587
587
588 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
588 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
589 opts=None):
589 opts=None):
590 '''export changesets as hg patches.'''
590 '''export changesets as hg patches.'''
591
591
592 total = len(revs)
592 total = len(revs)
593 revwidth = max([len(str(rev)) for rev in revs])
593 revwidth = max([len(str(rev)) for rev in revs])
594
594
595 def single(rev, seqno, fp):
595 def single(rev, seqno, fp):
596 ctx = repo.changectx(rev)
596 ctx = repo.changectx(rev)
597 node = ctx.node()
597 node = ctx.node()
598 parents = [p.node() for p in ctx.parents() if p]
598 parents = [p.node() for p in ctx.parents() if p]
599 if switch_parent:
599 if switch_parent:
600 parents.reverse()
600 parents.reverse()
601 prev = (parents and parents[0]) or nullid
601 prev = (parents and parents[0]) or nullid
602
602
603 if not fp:
603 if not fp:
604 fp = cmdutil.make_file(repo, template, node, total=total,
604 fp = cmdutil.make_file(repo, template, node, total=total,
605 seqno=seqno, revwidth=revwidth)
605 seqno=seqno, revwidth=revwidth)
606 if fp != sys.stdout and hasattr(fp, 'name'):
606 if fp != sys.stdout and hasattr(fp, 'name'):
607 repo.ui.note("%s\n" % fp.name)
607 repo.ui.note("%s\n" % fp.name)
608
608
609 fp.write("# HG changeset patch\n")
609 fp.write("# HG changeset patch\n")
610 fp.write("# User %s\n" % ctx.user())
610 fp.write("# User %s\n" % ctx.user())
611 fp.write("# Date %d %d\n" % ctx.date())
611 fp.write("# Date %d %d\n" % ctx.date())
612 fp.write("# Node ID %s\n" % hex(node))
612 fp.write("# Node ID %s\n" % hex(node))
613 fp.write("# Parent %s\n" % hex(prev))
613 fp.write("# Parent %s\n" % hex(prev))
614 if len(parents) > 1:
614 if len(parents) > 1:
615 fp.write("# Parent %s\n" % hex(parents[1]))
615 fp.write("# Parent %s\n" % hex(parents[1]))
616 fp.write(ctx.description().rstrip())
616 fp.write(ctx.description().rstrip())
617 fp.write("\n\n")
617 fp.write("\n\n")
618
618
619 diff(repo, prev, node, fp=fp, opts=opts)
619 diff(repo, prev, node, fp=fp, opts=opts)
620 if fp not in (sys.stdout, repo.ui):
620 if fp not in (sys.stdout, repo.ui):
621 fp.close()
621 fp.close()
622
622
623 for seqno, rev in enumerate(revs):
623 for seqno, rev in enumerate(revs):
624 single(rev, seqno+1, fp)
624 single(rev, seqno+1, fp)
625
625
626 def diffstat(patchlines):
626 def diffstat(patchlines):
627 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
627 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
628 try:
628 try:
629 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
629 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
630 try:
630 try:
631 for line in patchlines: print >> p.tochild, line
631 for line in patchlines: print >> p.tochild, line
632 p.tochild.close()
632 p.tochild.close()
633 if p.wait(): return
633 if p.wait(): return
634 fp = os.fdopen(fd, 'r')
634 fp = os.fdopen(fd, 'r')
635 stat = []
635 stat = []
636 for line in fp: stat.append(line.lstrip())
636 for line in fp: stat.append(line.lstrip())
637 last = stat.pop()
637 last = stat.pop()
638 stat.insert(0, last)
638 stat.insert(0, last)
639 stat = ''.join(stat)
639 stat = ''.join(stat)
640 if stat.startswith('0 files'): raise ValueError
640 if stat.startswith('0 files'): raise ValueError
641 return stat
641 return stat
642 except: raise
642 except: raise
643 finally:
643 finally:
644 try: os.unlink(name)
644 try: os.unlink(name)
645 except: pass
645 except: pass
General Comments 0
You need to be logged in to leave comments. Login now