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