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