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