##// END OF EJS Templates
patchbomb: ignore exception if pager quits.
Vadim Gelfer -
r1871:258e3a79 default
parent child Browse files
Show More
@@ -1,286 +1,290 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 mercurial.demandload import *
52 from mercurial.demandload import *
53 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
53 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
54 mercurial:commands,hg,ui
54 mercurial:commands,hg,ui
55 os popen2 smtplib socket sys tempfile time''')
55 os errno popen2 smtplib socket sys tempfile time''')
56 from mercurial.i18n import gettext as _
56 from mercurial.i18n import gettext as _
57
57
58 try:
58 try:
59 # readline gives raw_input editing capabilities, but is not
59 # readline gives raw_input editing capabilities, but is not
60 # present on windows
60 # present on windows
61 import readline
61 import readline
62 except ImportError: pass
62 except ImportError: pass
63
63
64 def diffstat(patch):
64 def diffstat(patch):
65 fd, name = tempfile.mkstemp()
65 fd, name = tempfile.mkstemp()
66 try:
66 try:
67 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
67 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
68 try:
68 try:
69 for line in patch: print >> p.tochild, line
69 for line in patch: print >> p.tochild, line
70 p.tochild.close()
70 p.tochild.close()
71 if p.wait(): return
71 if p.wait(): return
72 fp = os.fdopen(fd, 'r')
72 fp = os.fdopen(fd, 'r')
73 stat = []
73 stat = []
74 for line in fp: stat.append(line.lstrip())
74 for line in fp: stat.append(line.lstrip())
75 last = stat.pop()
75 last = stat.pop()
76 stat.insert(0, last)
76 stat.insert(0, last)
77 stat = ''.join(stat)
77 stat = ''.join(stat)
78 if stat.startswith('0 files'): raise ValueError
78 if stat.startswith('0 files'): raise ValueError
79 return stat
79 return stat
80 except: raise
80 except: raise
81 finally:
81 finally:
82 try: os.unlink(name)
82 try: os.unlink(name)
83 except: pass
83 except: pass
84
84
85 def patchbomb(ui, repo, *revs, **opts):
85 def patchbomb(ui, repo, *revs, **opts):
86 '''send changesets as a series of patch emails
86 '''send changesets as a series of patch emails
87
87
88 The series starts with a "[PATCH 0 of N]" introduction, which
88 The series starts with a "[PATCH 0 of N]" introduction, which
89 describes the series as a whole.
89 describes the series as a whole.
90
90
91 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
92 the first line of the changeset description as the subject text.
92 the first line of the changeset description as the subject text.
93 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
94 the changeset description. Next, (optionally) if the diffstat
94 the changeset description. Next, (optionally) if the diffstat
95 program is installed, the result of running diffstat on the patch.
95 program is installed, the result of running diffstat on the patch.
96 Finally, the patch itself, as generated by "hg export".'''
96 Finally, the patch itself, as generated by "hg export".'''
97 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
97 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
98 if default: prompt += ' [%s]' % default
98 if default: prompt += ' [%s]' % default
99 prompt += rest
99 prompt += rest
100 while True:
100 while True:
101 r = raw_input(prompt)
101 r = raw_input(prompt)
102 if r: return r
102 if r: return r
103 if default is not None: return default
103 if default is not None: return default
104 if empty_ok: return r
104 if empty_ok: return r
105 ui.warn(_('Please enter a valid value.\n'))
105 ui.warn(_('Please enter a valid value.\n'))
106
106
107 def confirm(s):
107 def confirm(s):
108 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
108 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
109 raise ValueError
109 raise ValueError
110
110
111 def cdiffstat(summary, patch):
111 def cdiffstat(summary, patch):
112 s = diffstat(patch)
112 s = diffstat(patch)
113 if s:
113 if s:
114 if summary:
114 if summary:
115 ui.write(summary, '\n')
115 ui.write(summary, '\n')
116 ui.write(s, '\n')
116 ui.write(s, '\n')
117 confirm(_('Does the diffstat above look okay'))
117 confirm(_('Does the diffstat above look okay'))
118 return s
118 return s
119
119
120 def makepatch(patch, idx, total):
120 def makepatch(patch, idx, total):
121 desc = []
121 desc = []
122 node = None
122 node = None
123 body = ''
123 body = ''
124 for line in patch:
124 for line in patch:
125 if line.startswith('#'):
125 if line.startswith('#'):
126 if line.startswith('# Node ID'): node = line.split()[-1]
126 if line.startswith('# Node ID'): node = line.split()[-1]
127 continue
127 continue
128 if line.startswith('diff -r'): break
128 if line.startswith('diff -r'): break
129 desc.append(line)
129 desc.append(line)
130 if not node: raise ValueError
130 if not node: raise ValueError
131
131
132 #body = ('\n'.join(desc[1:]).strip() or
132 #body = ('\n'.join(desc[1:]).strip() or
133 # 'Patch subject is complete summary.')
133 # 'Patch subject is complete summary.')
134 #body += '\n\n\n'
134 #body += '\n\n\n'
135
135
136 if opts['plain']:
136 if opts['plain']:
137 while patch and patch[0].startswith('# '): patch.pop(0)
137 while patch and patch[0].startswith('# '): patch.pop(0)
138 if patch: patch.pop(0)
138 if patch: patch.pop(0)
139 while patch and not patch[0].strip(): patch.pop(0)
139 while patch and not patch[0].strip(): patch.pop(0)
140 if opts['diffstat']:
140 if opts['diffstat']:
141 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
141 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
142 body += '\n'.join(patch)
142 body += '\n'.join(patch)
143 msg = email.MIMEText.MIMEText(body)
143 msg = email.MIMEText.MIMEText(body)
144 if total == 1:
144 if total == 1:
145 subj = '[PATCH] ' + desc[0].strip()
145 subj = '[PATCH] ' + desc[0].strip()
146 else:
146 else:
147 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
147 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
148 if subj.endswith('.'): subj = subj[:-1]
148 if subj.endswith('.'): subj = subj[:-1]
149 msg['Subject'] = subj
149 msg['Subject'] = subj
150 msg['X-Mercurial-Node'] = node
150 msg['X-Mercurial-Node'] = node
151 return msg
151 return msg
152
152
153 start_time = int(time.time())
153 start_time = int(time.time())
154
154
155 def genmsgid(id):
155 def genmsgid(id):
156 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
156 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
157
157
158 patches = []
158 patches = []
159
159
160 class exportee:
160 class exportee:
161 def __init__(self, container):
161 def __init__(self, container):
162 self.lines = []
162 self.lines = []
163 self.container = container
163 self.container = container
164 self.name = 'email'
164 self.name = 'email'
165
165
166 def write(self, data):
166 def write(self, data):
167 self.lines.append(data)
167 self.lines.append(data)
168
168
169 def close(self):
169 def close(self):
170 self.container.append(''.join(self.lines).split('\n'))
170 self.container.append(''.join(self.lines).split('\n'))
171 self.lines = []
171 self.lines = []
172
172
173 commands.export(ui, repo, *revs, **{'output': exportee(patches),
173 commands.export(ui, repo, *revs, **{'output': exportee(patches),
174 'switch_parent': False,
174 'switch_parent': False,
175 'text': None})
175 'text': None})
176
176
177 jumbo = []
177 jumbo = []
178 msgs = []
178 msgs = []
179
179
180 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
180 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
181
181
182 for p, i in zip(patches, range(len(patches))):
182 for p, i in zip(patches, range(len(patches))):
183 jumbo.extend(p)
183 jumbo.extend(p)
184 msgs.append(makepatch(p, i + 1, len(patches)))
184 msgs.append(makepatch(p, i + 1, len(patches)))
185
185
186 sender = (opts['from'] or ui.config('patchbomb', 'from') or
186 sender = (opts['from'] or ui.config('patchbomb', 'from') or
187 prompt('From', ui.username()))
187 prompt('From', ui.username()))
188
188
189 def getaddrs(opt, prpt, default = None):
189 def getaddrs(opt, prpt, default = None):
190 addrs = opts[opt] or (ui.config('patchbomb', opt) or
190 addrs = opts[opt] or (ui.config('patchbomb', opt) or
191 prompt(prpt, default = default)).split(',')
191 prompt(prpt, default = default)).split(',')
192 return [a.strip() for a in addrs if a.strip()]
192 return [a.strip() for a in addrs if a.strip()]
193 to = getaddrs('to', 'To')
193 to = getaddrs('to', 'To')
194 cc = getaddrs('cc', 'Cc', '')
194 cc = getaddrs('cc', 'Cc', '')
195
195
196 if len(patches) > 1:
196 if len(patches) > 1:
197 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
197 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
198
198
199 msg = email.MIMEMultipart.MIMEMultipart()
199 msg = email.MIMEMultipart.MIMEMultipart()
200 msg['Subject'] = '[PATCH 0 of %d] %s' % (
200 msg['Subject'] = '[PATCH 0 of %d] %s' % (
201 len(patches),
201 len(patches),
202 opts['subject'] or
202 opts['subject'] or
203 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
203 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
204
204
205 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
205 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
206
206
207 body = []
207 body = []
208
208
209 while True:
209 while True:
210 try: l = raw_input()
210 try: l = raw_input()
211 except EOFError: break
211 except EOFError: break
212 if l == '.': break
212 if l == '.': break
213 body.append(l)
213 body.append(l)
214
214
215 msg.attach(email.MIMEText.MIMEText('\n'.join(body) + '\n'))
215 msg.attach(email.MIMEText.MIMEText('\n'.join(body) + '\n'))
216
216
217 if opts['diffstat']:
217 if opts['diffstat']:
218 d = cdiffstat(_('Final summary:\n'), jumbo)
218 d = cdiffstat(_('Final summary:\n'), jumbo)
219 if d: msg.attach(email.MIMEText.MIMEText(d))
219 if d: msg.attach(email.MIMEText.MIMEText(d))
220
220
221 msgs.insert(0, msg)
221 msgs.insert(0, msg)
222
222
223 ui.write('\n')
223 ui.write('\n')
224
224
225 if not opts['test'] and not opts['mbox']:
225 if not opts['test'] and not opts['mbox']:
226 s = smtplib.SMTP()
226 s = smtplib.SMTP()
227 s.connect(host = ui.config('smtp', 'host', 'mail'),
227 s.connect(host = ui.config('smtp', 'host', 'mail'),
228 port = int(ui.config('smtp', 'port', 25)))
228 port = int(ui.config('smtp', 'port', 25)))
229 if ui.configbool('smtp', 'tls'):
229 if ui.configbool('smtp', 'tls'):
230 s.ehlo()
230 s.ehlo()
231 s.starttls()
231 s.starttls()
232 s.ehlo()
232 s.ehlo()
233 username = ui.config('smtp', 'username')
233 username = ui.config('smtp', 'username')
234 password = ui.config('smtp', 'password')
234 password = ui.config('smtp', 'password')
235 if username and password:
235 if username and password:
236 s.login(username, password)
236 s.login(username, password)
237 parent = None
237 parent = None
238 tz = time.strftime('%z')
238 tz = time.strftime('%z')
239 sender_addr = email.Utils.parseaddr(sender)[1]
239 sender_addr = email.Utils.parseaddr(sender)[1]
240 for m in msgs:
240 for m in msgs:
241 try:
241 try:
242 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
242 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
243 except TypeError:
243 except TypeError:
244 m['Message-Id'] = genmsgid('patchbomb')
244 m['Message-Id'] = genmsgid('patchbomb')
245 if parent:
245 if parent:
246 m['In-Reply-To'] = parent
246 m['In-Reply-To'] = parent
247 else:
247 else:
248 parent = m['Message-Id']
248 parent = m['Message-Id']
249 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
249 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
250 start_time += 1
250 start_time += 1
251 m['From'] = sender
251 m['From'] = sender
252 m['To'] = ', '.join(to)
252 m['To'] = ', '.join(to)
253 if cc: m['Cc'] = ', '.join(cc)
253 if cc: m['Cc'] = ', '.join(cc)
254 if opts['test']:
254 if opts['test']:
255 ui.status('Displaying ', m['Subject'], ' ...\n')
255 ui.status('Displaying ', m['Subject'], ' ...\n')
256 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
256 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
257 try:
257 fp.write(m.as_string(0))
258 fp.write(m.as_string(0))
258 fp.write('\n')
259 fp.write('\n')
260 except IOError, inst:
261 if inst.errno != errno.EPIPE:
262 raise
259 fp.close()
263 fp.close()
260 elif opts['mbox']:
264 elif opts['mbox']:
261 ui.status('Writing ', m['Subject'], ' ...\n')
265 ui.status('Writing ', m['Subject'], ' ...\n')
262 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
266 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
263 date = time.asctime(time.localtime(start_time))
267 date = time.asctime(time.localtime(start_time))
264 fp.write('From %s %s\n' % (sender_addr, date))
268 fp.write('From %s %s\n' % (sender_addr, date))
265 fp.write(m.as_string(0))
269 fp.write(m.as_string(0))
266 fp.write('\n\n')
270 fp.write('\n\n')
267 fp.close()
271 fp.close()
268 else:
272 else:
269 ui.status('Sending ', m['Subject'], ' ...\n')
273 ui.status('Sending ', m['Subject'], ' ...\n')
270 s.sendmail(sender, to + cc, m.as_string(0))
274 s.sendmail(sender, to + cc, m.as_string(0))
271 if not opts['test'] and not opts['mbox']:
275 if not opts['test'] and not opts['mbox']:
272 s.close()
276 s.close()
273
277
274 cmdtable = {
278 cmdtable = {
275 'email':
279 'email':
276 (patchbomb,
280 (patchbomb,
277 [('c', 'cc', [], 'email addresses of copy recipients'),
281 [('c', 'cc', [], 'email addresses of copy recipients'),
278 ('d', 'diffstat', None, 'add diffstat output to messages'),
282 ('d', 'diffstat', None, 'add diffstat output to messages'),
279 ('f', 'from', '', 'email address of sender'),
283 ('f', 'from', '', 'email address of sender'),
280 ('', 'plain', None, 'omit hg patch header'),
284 ('', 'plain', None, 'omit hg patch header'),
281 ('n', 'test', None, 'print messages that would be sent'),
285 ('n', 'test', None, 'print messages that would be sent'),
282 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
286 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
283 ('s', 'subject', '', 'subject of introductory message'),
287 ('s', 'subject', '', 'subject of introductory message'),
284 ('t', 'to', [], 'email addresses of recipients')],
288 ('t', 'to', [], 'email addresses of recipients')],
285 "hg email [OPTION]... [REV]...")
289 "hg email [OPTION]... [REV]...")
286 }
290 }
General Comments 0
You need to be logged in to leave comments. Login now