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