comparison contrib/patchbomb @ 875:d3f836bf6cc1

Add patchbomb script.
author Bryan O'Sullivan <bos@serpentine.com>
date Tue, 09 Aug 2005 20:18:58 -0800
parents
children 14cfaaec2e8e
comparison
equal deleted inserted replaced
871:c2e77581bc84 875:d3f836bf6cc1
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)