##// END OF EJS Templates
get patchbomb extension to use demandload. speeds up hg startup by 50%.
Vadim Gelfer -
r1827:26dd4ae7 default
parent child Browse files
Show More
@@ -1,291 +1,282 b''
1 # Command for sending a collection of Mercurial changesets as a series
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
2 # of patch emails.
3 #
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
5 # which describes the series as a whole.
6 #
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
9 # The message contains two or three body parts:
10 #
10 #
11 # The remainder of the changeset description.
11 # The remainder of the changeset description.
12 #
12 #
13 # [Optional] If the diffstat program is installed, the result of
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
14 # running diffstat on the patch.
15 #
15 #
16 # The patch itself, as generated by "hg export".
16 # The patch itself, as generated by "hg export".
17 #
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
20 # threaded mail and news readers, and in mail archives.
21 #
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
24 # changes.
25 #
25 #
26 # It is best to run this script with the "-n" (test only) flag before
26 # It is best to run this script with the "-n" (test only) flag before
27 # firing it up "for real", in which case it will use your pager to
27 # firing it up "for real", in which case it will use your pager to
28 # display each of the messages that it would send.
28 # display each of the messages that it would send.
29 #
29 #
30 # The "-m" (mbox) option will create an mbox file instead of sending
30 # The "-m" (mbox) option will create an mbox file instead of sending
31 # the messages directly. This can be reviewed e.g. with "mutt -R -f mbox",
31 # the messages directly. This can be reviewed e.g. with "mutt -R -f mbox",
32 # and finally sent with "formail -s sendmail -bm -t < mbox".
32 # and finally sent with "formail -s sendmail -bm -t < mbox".
33 #
33 #
34 # To configure a default mail host, add a section like this to your
34 # To configure a default mail host, add a section like this to your
35 # hgrc file:
35 # hgrc file:
36 #
36 #
37 # [smtp]
37 # [smtp]
38 # host = my_mail_host
38 # host = my_mail_host
39 # port = 1025
39 # port = 1025
40 # tls = yes # or omit if not needed
40 # tls = yes # or omit if not needed
41 # username = user # if SMTP authentication required
41 # username = user # if SMTP authentication required
42 # password = password # if SMTP authentication required - PLAINTEXT
42 # password = password # if SMTP authentication required - PLAINTEXT
43 #
43 #
44 # To configure other defaults, add a section like this to your hgrc
44 # To configure other defaults, add a section like this to your hgrc
45 # file:
45 # file:
46 #
46 #
47 # [patchbomb]
47 # [patchbomb]
48 # from = My Name <my@email>
48 # from = My Name <my@email>
49 # to = recipient1, recipient2, ...
49 # to = recipient1, recipient2, ...
50 # cc = cc1, cc2, ...
50 # cc = cc1, cc2, ...
51
51
52 from email.MIMEMultipart import MIMEMultipart
52 from mercurial.demandload import *
53 from email.MIMEText import MIMEText
53 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
54 from email.Utils import parseaddr
54 mercurial:commands,hg,ui
55 from mercurial import commands
55 os popen2 smtplib socket sys tempfile time''')
56 from mercurial import hg
57 from mercurial import ui
58 from mercurial.i18n import gettext as _
56 from mercurial.i18n import gettext as _
59 import os
60 import popen2
61 import smtplib
62 import socket
63 import sys
64 import tempfile
65 import time
66
57
67 try:
58 try:
68 # readline gives raw_input editing capabilities, but is not
59 # readline gives raw_input editing capabilities, but is not
69 # present on windows
60 # present on windows
70 import readline
61 import readline
71 except ImportError: pass
62 except ImportError: pass
72
63
73 def diffstat(patch):
64 def diffstat(patch):
74 fd, name = tempfile.mkstemp()
65 fd, name = tempfile.mkstemp()
75 try:
66 try:
76 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
67 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
77 try:
68 try:
78 for line in patch: print >> p.tochild, line
69 for line in patch: print >> p.tochild, line
79 p.tochild.close()
70 p.tochild.close()
80 if p.wait(): return
71 if p.wait(): return
81 fp = os.fdopen(fd, 'r')
72 fp = os.fdopen(fd, 'r')
82 stat = []
73 stat = []
83 for line in fp: stat.append(line.lstrip())
74 for line in fp: stat.append(line.lstrip())
84 last = stat.pop()
75 last = stat.pop()
85 stat.insert(0, last)
76 stat.insert(0, last)
86 stat = ''.join(stat)
77 stat = ''.join(stat)
87 if stat.startswith('0 files'): raise ValueError
78 if stat.startswith('0 files'): raise ValueError
88 return stat
79 return stat
89 except: raise
80 except: raise
90 finally:
81 finally:
91 try: os.unlink(name)
82 try: os.unlink(name)
92 except: pass
83 except: pass
93
84
94 def patchbomb(ui, repo, *revs, **opts):
85 def patchbomb(ui, repo, *revs, **opts):
95 '''send changesets as a series of patch emails
86 '''send changesets as a series of patch emails
96
87
97 The series starts with a "[PATCH 0 of N]" introduction, which
88 The series starts with a "[PATCH 0 of N]" introduction, which
98 describes the series as a whole.
89 describes the series as a whole.
99
90
100 Each patch email has a Subject line of "[PATCH M of N] ...", using
91 Each patch email has a Subject line of "[PATCH M of N] ...", using
101 the first line of the changeset description as the subject text.
92 the first line of the changeset description as the subject text.
102 The message contains two or three body parts. First, the rest of
93 The message contains two or three body parts. First, the rest of
103 the changeset description. Next, (optionally) if the diffstat
94 the changeset description. Next, (optionally) if the diffstat
104 program is installed, the result of running diffstat on the patch.
95 program is installed, the result of running diffstat on the patch.
105 Finally, the patch itself, as generated by "hg export".'''
96 Finally, the patch itself, as generated by "hg export".'''
106 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
97 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
107 if default: prompt += ' [%s]' % default
98 if default: prompt += ' [%s]' % default
108 prompt += rest
99 prompt += rest
109 while True:
100 while True:
110 r = raw_input(prompt)
101 r = raw_input(prompt)
111 if r: return r
102 if r: return r
112 if default is not None: return default
103 if default is not None: return default
113 if empty_ok: return r
104 if empty_ok: return r
114 ui.warn(_('Please enter a valid value.\n'))
105 ui.warn(_('Please enter a valid value.\n'))
115
106
116 def confirm(s):
107 def confirm(s):
117 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
108 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
118 raise ValueError
109 raise ValueError
119
110
120 def cdiffstat(summary, patch):
111 def cdiffstat(summary, patch):
121 s = diffstat(patch)
112 s = diffstat(patch)
122 if s:
113 if s:
123 if summary:
114 if summary:
124 ui.write(summary, '\n')
115 ui.write(summary, '\n')
125 ui.write(s, '\n')
116 ui.write(s, '\n')
126 confirm(_('Does the diffstat above look okay'))
117 confirm(_('Does the diffstat above look okay'))
127 return s
118 return s
128
119
129 def makepatch(patch, idx, total):
120 def makepatch(patch, idx, total):
130 desc = []
121 desc = []
131 node = None
122 node = None
132 body = ''
123 body = ''
133 for line in patch:
124 for line in patch:
134 if line.startswith('#'):
125 if line.startswith('#'):
135 if line.startswith('# Node ID'): node = line.split()[-1]
126 if line.startswith('# Node ID'): node = line.split()[-1]
136 continue
127 continue
137 if line.startswith('diff -r'): break
128 if line.startswith('diff -r'): break
138 desc.append(line)
129 desc.append(line)
139 if not node: raise ValueError
130 if not node: raise ValueError
140
131
141 #body = ('\n'.join(desc[1:]).strip() or
132 #body = ('\n'.join(desc[1:]).strip() or
142 # 'Patch subject is complete summary.')
133 # 'Patch subject is complete summary.')
143 #body += '\n\n\n'
134 #body += '\n\n\n'
144
135
145 if opts['plain']:
136 if opts['plain']:
146 while patch and patch[0].startswith('# '): patch.pop(0)
137 while patch and patch[0].startswith('# '): patch.pop(0)
147 if patch: patch.pop(0)
138 if patch: patch.pop(0)
148 while patch and not patch[0].strip(): patch.pop(0)
139 while patch and not patch[0].strip(): patch.pop(0)
149 if opts['diffstat']:
140 if opts['diffstat']:
150 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
141 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
151 body += '\n'.join(patch)
142 body += '\n'.join(patch)
152 msg = MIMEText(body)
143 msg = email.MIMEText.MIMEText(body)
153 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
144 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
154 if subj.endswith('.'): subj = subj[:-1]
145 if subj.endswith('.'): subj = subj[:-1]
155 msg['Subject'] = subj
146 msg['Subject'] = subj
156 msg['X-Mercurial-Node'] = node
147 msg['X-Mercurial-Node'] = node
157 return msg
148 return msg
158
149
159 start_time = int(time.time())
150 start_time = int(time.time())
160
151
161 def genmsgid(id):
152 def genmsgid(id):
162 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
153 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
163
154
164 patches = []
155 patches = []
165
156
166 class exportee:
157 class exportee:
167 def __init__(self, container):
158 def __init__(self, container):
168 self.lines = []
159 self.lines = []
169 self.container = container
160 self.container = container
170 self.name = 'email'
161 self.name = 'email'
171
162
172 def write(self, data):
163 def write(self, data):
173 self.lines.append(data)
164 self.lines.append(data)
174
165
175 def close(self):
166 def close(self):
176 self.container.append(''.join(self.lines).split('\n'))
167 self.container.append(''.join(self.lines).split('\n'))
177 self.lines = []
168 self.lines = []
178
169
179 commands.export(ui, repo, *revs, **{'output': exportee(patches),
170 commands.export(ui, repo, *revs, **{'output': exportee(patches),
180 'switch_parent': False,
171 'switch_parent': False,
181 'text': None})
172 'text': None})
182
173
183 jumbo = []
174 jumbo = []
184 msgs = []
175 msgs = []
185
176
186 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
177 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
187
178
188 for p, i in zip(patches, range(len(patches))):
179 for p, i in zip(patches, range(len(patches))):
189 jumbo.extend(p)
180 jumbo.extend(p)
190 msgs.append(makepatch(p, i + 1, len(patches)))
181 msgs.append(makepatch(p, i + 1, len(patches)))
191
182
192 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
183 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
193
184
194 sender = (opts['from'] or ui.config('patchbomb', 'from') or
185 sender = (opts['from'] or ui.config('patchbomb', 'from') or
195 prompt('From', ui.username()))
186 prompt('From', ui.username()))
196
187
197 msg = MIMEMultipart()
188 msg = email.MIMEMultipart.MIMEMultipart()
198 msg['Subject'] = '[PATCH 0 of %d] %s' % (
189 msg['Subject'] = '[PATCH 0 of %d] %s' % (
199 len(patches),
190 len(patches),
200 opts['subject'] or
191 opts['subject'] or
201 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
192 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
202
193
203 def getaddrs(opt, prpt, default = None):
194 def getaddrs(opt, prpt, default = None):
204 addrs = opts[opt] or (ui.config('patchbomb', opt) or
195 addrs = opts[opt] or (ui.config('patchbomb', opt) or
205 prompt(prpt, default = default)).split(',')
196 prompt(prpt, default = default)).split(',')
206 return [a.strip() for a in addrs if a.strip()]
197 return [a.strip() for a in addrs if a.strip()]
207 to = getaddrs('to', 'To')
198 to = getaddrs('to', 'To')
208 cc = getaddrs('cc', 'Cc', '')
199 cc = getaddrs('cc', 'Cc', '')
209
200
210 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
201 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
211
202
212 body = []
203 body = []
213
204
214 while True:
205 while True:
215 try: l = raw_input()
206 try: l = raw_input()
216 except EOFError: break
207 except EOFError: break
217 if l == '.': break
208 if l == '.': break
218 body.append(l)
209 body.append(l)
219
210
220 msg.attach(MIMEText('\n'.join(body) + '\n'))
211 msg.attach(email.MIMEText.MIMEText('\n'.join(body) + '\n'))
221
212
222 ui.write('\n')
213 ui.write('\n')
223
214
224 if opts['diffstat']:
215 if opts['diffstat']:
225 d = cdiffstat(_('Final summary:\n'), jumbo)
216 d = cdiffstat(_('Final summary:\n'), jumbo)
226 if d: msg.attach(MIMEText(d))
217 if d: msg.attach(email.MIMEText.MIMEText(d))
227
218
228 msgs.insert(0, msg)
219 msgs.insert(0, msg)
229
220
230 if not opts['test'] and not opts['mbox']:
221 if not opts['test'] and not opts['mbox']:
231 s = smtplib.SMTP()
222 s = smtplib.SMTP()
232 s.connect(host = ui.config('smtp', 'host', 'mail'),
223 s.connect(host = ui.config('smtp', 'host', 'mail'),
233 port = int(ui.config('smtp', 'port', 25)))
224 port = int(ui.config('smtp', 'port', 25)))
234 if ui.configbool('smtp', 'tls'):
225 if ui.configbool('smtp', 'tls'):
235 s.ehlo()
226 s.ehlo()
236 s.starttls()
227 s.starttls()
237 s.ehlo()
228 s.ehlo()
238 username = ui.config('smtp', 'username')
229 username = ui.config('smtp', 'username')
239 password = ui.config('smtp', 'password')
230 password = ui.config('smtp', 'password')
240 if username and password:
231 if username and password:
241 s.login(username, password)
232 s.login(username, password)
242 parent = None
233 parent = None
243 tz = time.strftime('%z')
234 tz = time.strftime('%z')
244 sender_addr = parseaddr(sender)[1]
235 sender_addr = email.Utils.parseaddr(sender)[1]
245 for m in msgs:
236 for m in msgs:
246 try:
237 try:
247 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
238 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
248 except TypeError:
239 except TypeError:
249 m['Message-Id'] = genmsgid('patchbomb')
240 m['Message-Id'] = genmsgid('patchbomb')
250 if parent:
241 if parent:
251 m['In-Reply-To'] = parent
242 m['In-Reply-To'] = parent
252 else:
243 else:
253 parent = m['Message-Id']
244 parent = m['Message-Id']
254 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
245 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
255 start_time += 1
246 start_time += 1
256 m['From'] = sender
247 m['From'] = sender
257 m['To'] = ', '.join(to)
248 m['To'] = ', '.join(to)
258 if cc: m['Cc'] = ', '.join(cc)
249 if cc: m['Cc'] = ', '.join(cc)
259 if opts['test']:
250 if opts['test']:
260 ui.status('Displaying ', m['Subject'], ' ...\n')
251 ui.status('Displaying ', m['Subject'], ' ...\n')
261 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
252 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
262 fp.write(m.as_string(0))
253 fp.write(m.as_string(0))
263 fp.write('\n')
254 fp.write('\n')
264 fp.close()
255 fp.close()
265 elif opts['mbox']:
256 elif opts['mbox']:
266 ui.status('Writing ', m['Subject'], ' ...\n')
257 ui.status('Writing ', m['Subject'], ' ...\n')
267 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
258 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
268 date = time.asctime(time.localtime(start_time))
259 date = time.asctime(time.localtime(start_time))
269 fp.write('From %s %s\n' % (sender_addr, date))
260 fp.write('From %s %s\n' % (sender_addr, date))
270 fp.write(m.as_string(0))
261 fp.write(m.as_string(0))
271 fp.write('\n\n')
262 fp.write('\n\n')
272 fp.close()
263 fp.close()
273 else:
264 else:
274 ui.status('Sending ', m['Subject'], ' ...\n')
265 ui.status('Sending ', m['Subject'], ' ...\n')
275 s.sendmail(sender, to + cc, m.as_string(0))
266 s.sendmail(sender, to + cc, m.as_string(0))
276 if not opts['test'] and not opts['mbox']:
267 if not opts['test'] and not opts['mbox']:
277 s.close()
268 s.close()
278
269
279 cmdtable = {
270 cmdtable = {
280 'email':
271 'email':
281 (patchbomb,
272 (patchbomb,
282 [('c', 'cc', [], 'email addresses of copy recipients'),
273 [('c', 'cc', [], 'email addresses of copy recipients'),
283 ('d', 'diffstat', None, 'add diffstat output to messages'),
274 ('d', 'diffstat', None, 'add diffstat output to messages'),
284 ('f', 'from', '', 'email address of sender'),
275 ('f', 'from', '', 'email address of sender'),
285 ('', 'plain', None, 'omit hg patch header'),
276 ('', 'plain', None, 'omit hg patch header'),
286 ('n', 'test', None, 'print messages that would be sent'),
277 ('n', 'test', None, 'print messages that would be sent'),
287 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
278 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
288 ('s', 'subject', '', 'subject of introductory message'),
279 ('s', 'subject', '', 'subject of introductory message'),
289 ('t', 'to', [], 'email addresses of recipients')],
280 ('t', 'to', [], 'email addresses of recipients')],
290 "hg email [OPTION]... [REV]...")
281 "hg email [OPTION]... [REV]...")
291 }
282 }
General Comments 0
You need to be logged in to leave comments. Login now