##// END OF EJS Templates
Get patchbomb script to not use MIME attachments....
Bryan O'Sullivan -
r876:14cfaaec default
parent child Browse files
Show More
@@ -1,242 +1,231 b''
1 #!/usr/bin/python
1 #!/usr/bin/python
2 #
2 #
3 # Interactive script for sending a collection of Mercurial changesets
3 # Interactive script for sending a collection of Mercurial changesets
4 # as a series of patch emails.
4 # as a series of patch emails.
5 #
5 #
6 # The series is started off with a "[PATCH 0 of N]" introduction,
6 # The series is started off with a "[PATCH 0 of N]" introduction,
7 # which describes the series as a whole.
7 # which describes the series as a whole.
8 #
8 #
9 # Each patch email has a Subject line of "[PATCH M of N] ...", using
9 # Each patch email has a Subject line of "[PATCH M of N] ...", using
10 # the first line of the changeset description as the subject text.
10 # the first line of the changeset description as the subject text.
11 # The message contains two or three body parts:
11 # The message contains two or three body parts:
12 #
12 #
13 # The remainder of the changeset description.
13 # The remainder of the changeset description.
14 #
14 #
15 # If the diffstat program is installed, the result of running
15 # If the diffstat program is installed, the result of running
16 # diffstat on the patch.
16 # diffstat on the patch.
17 #
17 #
18 # The patch itself, as generated by "hg export".
18 # The patch itself, as generated by "hg export".
19 #
19 #
20 # Each message refers to all of its predecessors using the In-Reply-To
20 # Each message refers to all of its predecessors using the In-Reply-To
21 # and References headers, so they will show up as a sequence in
21 # and References headers, so they will show up as a sequence in
22 # threaded mail and news readers, and in mail archives.
22 # threaded mail and news readers, and in mail archives.
23 #
23 #
24 # For each changeset, you will be prompted with a diffstat summary and
24 # For each changeset, you will be prompted with a diffstat summary and
25 # the changeset summary, so you can be sure you are sending the right
25 # the changeset summary, so you can be sure you are sending the right
26 # changes.
26 # changes.
27 #
27 #
28 # It is best to run this script with the "-n" (test only) flag before
28 # It is best to run this script with the "-n" (test only) flag before
29 # firing it up "for real", in which case it will display each of the
29 # firing it up "for real", in which case it will display each of the
30 # messages that it would send.
30 # messages that it would send.
31 #
31 #
32 # To configure a default mail host, add a section like this to your
32 # To configure a default mail host, add a section like this to your
33 # hgrc file:
33 # hgrc file:
34 #
34 #
35 # [smtp]
35 # [smtp]
36 # host = my_mail_host
36 # host = my_mail_host
37 # port = 1025
37 # port = 1025
38
38
39 from email.MIMEMultipart import MIMEMultipart
39 from email.MIMEMultipart import MIMEMultipart
40 from email.MIMEText import MIMEText
40 from email.MIMEText import MIMEText
41 from mercurial import commands
41 from mercurial import commands
42 from mercurial import fancyopts
42 from mercurial import fancyopts
43 from mercurial import hg
43 from mercurial import hg
44 from mercurial import ui
44 from mercurial import ui
45 import os
45 import os
46 import popen2
46 import popen2
47 import readline
47 import readline
48 import smtplib
48 import smtplib
49 import socket
49 import socket
50 import sys
50 import sys
51 import tempfile
51 import tempfile
52 import time
52 import time
53
53
54 def diffstat(patch):
54 def diffstat(patch):
55 fd, name = tempfile.mkstemp()
55 fd, name = tempfile.mkstemp()
56 try:
56 try:
57 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
57 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
58 try:
58 try:
59 for line in patch: print >> p.tochild, line
59 for line in patch: print >> p.tochild, line
60 p.tochild.close()
60 p.tochild.close()
61 if p.wait(): return
61 if p.wait(): return
62 fp = os.fdopen(fd, 'r')
62 fp = os.fdopen(fd, 'r')
63 stat = []
63 stat = []
64 for line in fp: stat.append(line.lstrip())
64 for line in fp: stat.append(line.lstrip())
65 last = stat.pop()
65 last = stat.pop()
66 stat.insert(0, last)
66 stat.insert(0, last)
67 stat = ''.join(stat)
67 stat = ''.join(stat)
68 if stat.startswith('0 files'): raise ValueError
68 if stat.startswith('0 files'): raise ValueError
69 return stat
69 return stat
70 except: raise
70 except: raise
71 finally:
71 finally:
72 try: os.unlink(name)
72 try: os.unlink(name)
73 except: pass
73 except: pass
74
74
75 def patchbomb(ui, repo, *revs, **opts):
75 def patchbomb(ui, repo, *revs, **opts):
76 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
76 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
77 try:
77 if default: prompt += ' [%s]' % default
78 if default: prompt += ' [%s]' % default
78 prompt += rest
79 prompt += rest
79 while True:
80 r = raw_input(prompt)
80 r = raw_input(prompt)
81 if not r and not empty_ok: raise EOFError
81 if r: return r
82 return r
82 if default is not None: return default
83 except EOFError:
83 if empty_ok: return r
84 if default is None: raise
84 print >> sys.stderr, 'Please enter a valid value.'
85 return default
86
85
87 def confirm(s):
86 def confirm(s):
88 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
87 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
89 raise ValueError
88 raise ValueError
90
89
91 def cdiffstat(summary, patch):
90 def cdiffstat(summary, patch):
92 s = diffstat(patch)
91 s = diffstat(patch)
93 if s:
92 if s:
94 if summary:
93 if summary:
95 ui.write(summary, '\n')
94 ui.write(summary, '\n')
96 ui.write(s, '\n')
95 ui.write(s, '\n')
97 confirm('Does the diffstat above look okay')
96 confirm('Does the diffstat above look okay')
98 return s
97 return s
99
98
100 def make_patch(patch, idx, total):
99 def makepatch(patch, idx, total):
101 desc = []
100 desc = []
102 node = None
101 node = None
103 for line in patch:
102 for line in patch:
104 if line.startswith('#'):
103 if line.startswith('#'):
105 if line.startswith('# Node ID'): node = line.split()[-1]
104 if line.startswith('# Node ID'): node = line.split()[-1]
106 continue
105 continue
107 if line.startswith('diff -r'): break
106 if line.startswith('diff -r'): break
108 desc.append(line)
107 desc.append(line)
109 if not node: raise ValueError
108 if not node: raise ValueError
110 msg = MIMEMultipart()
109 body = ('\n'.join(desc[1:]).strip() or
111 msg['X-Mercurial-Node'] = node
110 'Patch subject is complete summary.')
111 body += '\n\n\n' + cdiffstat('\n'.join(desc), patch) + '\n\n'
112 body += '\n'.join(patch)
113 msg = MIMEText(body)
112 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
114 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
113 if subj.endswith('.'): subj = subj[:-1]
115 if subj.endswith('.'): subj = subj[:-1]
114 msg['Subject'] = subj
116 msg['Subject'] = subj
115 body = '\n'.join(desc[1:]).strip() + '\n'
117 msg['X-Mercurial-Node'] = node
116 summary = subj
117 if body != '\n':
118 msg.attach(MIMEText(body))
119 summary += '\n\n' + body
120 else:
121 summary += '\n'
122 d = cdiffstat(summary, patch)
123 if d: msg.attach(MIMEText(d))
124 p = MIMEText('\n'.join(patch), 'x-patch')
125 p['Content-Disposition'] = commands.make_filename(repo, None,
126 'inline; filename=%b-%n.patch',
127 seqno = idx)
128 msg.attach(p)
129 return msg
118 return msg
130
119
131 start_time = int(time.time())
120 start_time = int(time.time())
132
121
133 def make_msgid(id):
122 def genmsgid(id):
134 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
123 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
135
124
136 patches = []
125 patches = []
137
126
138 class exportee:
127 class exportee:
139 def __init__(self, container):
128 def __init__(self, container):
140 self.lines = []
129 self.lines = []
141 self.container = container
130 self.container = container
131 self.name = 'email'
142
132
143 def write(self, data):
133 def write(self, data):
144 self.lines.append(data)
134 self.lines.append(data)
145
135
146 def close(self):
136 def close(self):
147 self.container.append(''.join(self.lines).split('\n'))
137 self.container.append(''.join(self.lines).split('\n'))
148 self.lines = []
138 self.lines = []
149
139
150 commands.export(ui, repo, *args, **{'output': exportee(patches)})
140 commands.export(ui, repo, *args, **{'output': exportee(patches)})
151
141
152 jumbo = []
142 jumbo = []
153 msgs = []
143 msgs = []
154
144
155 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
145 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
156
146
157 for p, i in zip(patches, range(len(patches))):
147 for p, i in zip(patches, range(len(patches))):
158 jumbo.extend(p)
148 jumbo.extend(p)
159 msgs.append(make_patch(p, i + 1, len(patches)))
149 msgs.append(makepatch(p, i + 1, len(patches)))
160
150
161 ui.write('\nWrite the introductory message for the patch series.\n\n')
151 ui.write('\nWrite the introductory message for the patch series.\n\n')
162
152
163 sender = opts['sender'] or prompt('From', ui.username())
153 sender = opts['sender'] or prompt('From', ui.username())
164
154
165 msg = MIMEMultipart()
155 msg = MIMEMultipart()
166 msg['Subject'] = '[PATCH 0 of %d] %s' % (
156 msg['Subject'] = '[PATCH 0 of %d] %s' % (
167 len(patches),
157 len(patches),
168 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
158 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
169 to = opts['to'] or [s.strip() for s in prompt('To').split(',')]
159 to = opts['to'] or [s.strip() for s in prompt('To').split(',')]
170 cc = opts['cc'] or [s.strip() for s in prompt('Cc', default = '').split(',')]
160 cc = opts['cc'] or [s.strip() for s in prompt('Cc', default = '').split(',')]
171
161
172 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
162 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
173
163
174 body = []
164 body = []
175
165
176 while True:
166 while True:
177 try: l = raw_input()
167 try: l = raw_input()
178 except EOFError: break
168 except EOFError: break
179 if l == '.': break
169 if l == '.': break
180 body.append(l)
170 body.append(l)
181
171
182 msg.attach(MIMEText('\n'.join(body) + '\n'))
172 msg.attach(MIMEText('\n'.join(body) + '\n'))
183
173
184 ui.write('\n')
174 ui.write('\n')
185
175
186 d = cdiffstat('Final summary:\n', jumbo)
176 d = cdiffstat('Final summary:\n', jumbo)
187 if d: msg.attach(MIMEText(d))
177 if d: msg.attach(MIMEText(d))
188
178
189 msgs.insert(0, msg)
179 msgs.insert(0, msg)
190
180
191 s = smtplib.SMTP()
181 if not opts['test']:
192 s.connect(host = ui.config('smtp', 'host', 'mail'),
182 s = smtplib.SMTP()
193 port = int(ui.config('smtp', 'port', 25)))
183 s.connect(host = ui.config('smtp', 'host', 'mail'),
184 port = int(ui.config('smtp', 'port', 25)))
194
185
195 refs = []
196 parent = None
186 parent = None
197 tz = time.strftime('%z')
187 tz = time.strftime('%z')
198 for m in msgs:
188 for m in msgs:
199 try:
189 try:
200 m['Message-Id'] = make_msgid(m['X-Mercurial-Node'])
190 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
201 except TypeError:
191 except TypeError:
202 m['Message-Id'] = make_msgid('patchbomb')
192 m['Message-Id'] = genmsgid('patchbomb')
203 if parent:
193 if parent:
204 m['In-Reply-To'] = parent
194 m['In-Reply-To'] = parent
205 parent = m['Message-Id']
195 else:
206 if len(refs) > 1:
196 parent = m['Message-Id']
207 m['References'] = ' '.join(refs[:-1])
208 refs.append(parent)
209 m['Date'] = time.strftime('%a, %m %b %Y %T ', time.localtime(start_time)) + tz
197 m['Date'] = time.strftime('%a, %m %b %Y %T ', time.localtime(start_time)) + tz
210 start_time += 1
198 start_time += 1
211 m['From'] = sender
199 m['From'] = sender
212 m['To'] = ', '.join(to)
200 m['To'] = ', '.join(to)
213 if cc: m['Cc'] = ', '.join(cc)
201 if cc: m['Cc'] = ', '.join(cc)
214 ui.status('Sending ', m['Subject'], ' ...\n')
202 ui.status('Sending ', m['Subject'], ' ...\n')
215 if opts['test']:
203 if opts['test']:
216 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
204 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
217 fp.write(m.as_string(0))
205 fp.write(m.as_string(0))
218 fp.write('\n')
206 fp.write('\n')
219 fp.close()
207 fp.close()
220 else:
208 else:
221 s.sendmail(sender, to + cc, m.as_string(0))
209 s.sendmail(sender, to + cc, m.as_string(0))
222 s.close()
210 if not opts['test']:
211 s.close()
223
212
224 if __name__ == '__main__':
213 if __name__ == '__main__':
225 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
214 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
226 ('n', 'test', None, 'print messages that would be sent'),
215 ('n', 'test', None, 'print messages that would be sent'),
227 ('s', 'sender', '', 'email address of sender'),
216 ('s', 'sender', '', 'email address of sender'),
228 ('t', 'to', [], 'email addresses of recipients')]
217 ('t', 'to', [], 'email addresses of recipients')]
229 options = {}
218 options = {}
230 try:
219 try:
231 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
220 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
232 options)
221 options)
233 except fancyopts.getopt.GetoptError, inst:
222 except fancyopts.getopt.GetoptError, inst:
234 u = ui.ui()
223 u = ui.ui()
235 u.warn('error: %s' % inst)
224 u.warn('error: %s' % inst)
236 sys.exit(1)
225 sys.exit(1)
237
226
238 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
227 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
239 not options["noninteractive"])
228 not options["noninteractive"])
240 repo = hg.repository(ui = u)
229 repo = hg.repository(ui = u)
241
230
242 patchbomb(u, repo, *args, **options)
231 patchbomb(u, repo, *args, **options)
General Comments 0
You need to be logged in to leave comments. Login now