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