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