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