comparison contrib/patchbomb @ 878:781266a78fe1

Merge patchbomb script.
author Bryan O'Sullivan <bos@serpentine.com>
date Fri, 12 Aug 2005 10:17:12 -0800
parents 25430c523677
children 01215ad04283
comparison
equal deleted inserted replaced
874:d4cb383e7de7 878:781266a78fe1
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 # [Optional] If the diffstat program is installed, the result of
16 # running 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 use your pager to
30 # display each of the 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 # To configure other defaults, add a section like this to your hgrc
40 # file:
41 #
42 # [patchbomb]
43 # from = My Name <my@email>
44 # to = recipient1, recipient2, ...
45 # cc = cc1, cc2, ...
46
47 from email.MIMEMultipart import MIMEMultipart
48 from email.MIMEText import MIMEText
49 from mercurial import commands
50 from mercurial import fancyopts
51 from mercurial import hg
52 from mercurial import ui
53 import os
54 import popen2
55 import readline
56 import smtplib
57 import socket
58 import sys
59 import tempfile
60 import time
61
62 def diffstat(patch):
63 fd, name = tempfile.mkstemp()
64 try:
65 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
66 try:
67 for line in patch: print >> p.tochild, line
68 p.tochild.close()
69 if p.wait(): return
70 fp = os.fdopen(fd, 'r')
71 stat = []
72 for line in fp: stat.append(line.lstrip())
73 last = stat.pop()
74 stat.insert(0, last)
75 stat = ''.join(stat)
76 if stat.startswith('0 files'): raise ValueError
77 return stat
78 except: raise
79 finally:
80 try: os.unlink(name)
81 except: pass
82
83 def patchbomb(ui, repo, *revs, **opts):
84 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
85 if default: prompt += ' [%s]' % default
86 prompt += rest
87 while True:
88 r = raw_input(prompt)
89 if r: return r
90 if default is not None: return default
91 if empty_ok: return r
92 ui.warn('Please enter a valid value.\n')
93
94 def confirm(s):
95 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
96 raise ValueError
97
98 def cdiffstat(summary, patch):
99 s = diffstat(patch)
100 if s:
101 if summary:
102 ui.write(summary, '\n')
103 ui.write(s, '\n')
104 confirm('Does the diffstat above look okay')
105 return s
106
107 def makepatch(patch, idx, total):
108 desc = []
109 node = None
110 for line in patch:
111 if line.startswith('#'):
112 if line.startswith('# Node ID'): node = line.split()[-1]
113 continue
114 if line.startswith('diff -r'): break
115 desc.append(line)
116 if not node: raise ValueError
117 body = ('\n'.join(desc[1:]).strip() or
118 'Patch subject is complete summary.')
119 body += '\n\n\n'
120 if opts['diffstat']:
121 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
122 body += '\n'.join(patch)
123 msg = MIMEText(body)
124 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
125 if subj.endswith('.'): subj = subj[:-1]
126 msg['Subject'] = subj
127 msg['X-Mercurial-Node'] = node
128 return msg
129
130 start_time = int(time.time())
131
132 def genmsgid(id):
133 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
134
135 patches = []
136
137 class exportee:
138 def __init__(self, container):
139 self.lines = []
140 self.container = container
141 self.name = 'email'
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(makepatch(p, i + 1, len(patches)))
160
161 ui.write('\nWrite the introductory message for the patch series.\n\n')
162
163 sender = (opts['from'] or ui.config('patchbomb', 'from') or
164 prompt('From', ui.username()))
165
166 msg = MIMEMultipart()
167 msg['Subject'] = '[PATCH 0 of %d] %s' % (
168 len(patches),
169 opts['subject'] or
170 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
171 to = (opts['to'] or ui.config('patchbomb', 'to') or
172 [s.strip() for s in prompt('To').split(',')])
173 cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
174 [s.strip() for s in prompt('Cc', default = '').split(',')])
175
176 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
177
178 body = []
179
180 while True:
181 try: l = raw_input()
182 except EOFError: break
183 if l == '.': break
184 body.append(l)
185
186 msg.attach(MIMEText('\n'.join(body) + '\n'))
187
188 ui.write('\n')
189
190 d = cdiffstat('Final summary:\n', jumbo)
191 if d: msg.attach(MIMEText(d))
192
193 msgs.insert(0, msg)
194
195 if not opts['test']:
196 s = smtplib.SMTP()
197 s.connect(host = ui.config('smtp', 'host', 'mail'),
198 port = int(ui.config('smtp', 'port', 25)))
199
200 parent = None
201 tz = time.strftime('%z')
202 for m in msgs:
203 try:
204 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
205 except TypeError:
206 m['Message-Id'] = genmsgid('patchbomb')
207 if parent:
208 m['In-Reply-To'] = parent
209 else:
210 parent = m['Message-Id']
211 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
212 start_time += 1
213 m['From'] = sender
214 m['To'] = ', '.join(to)
215 if cc: m['Cc'] = ', '.join(cc)
216 ui.status('Sending ', m['Subject'], ' ...\n')
217 if opts['test']:
218 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
219 fp.write(m.as_string(0))
220 fp.write('\n')
221 fp.close()
222 else:
223 s.sendmail(sender, to + cc, m.as_string(0))
224 if not opts['test']:
225 s.close()
226
227 if __name__ == '__main__':
228 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
229 ('d', 'diffstat', None, 'add diffstat output to messages'),
230 ('f', 'from', '', 'email address of sender'),
231 ('n', 'test', None, 'print messages that would be sent'),
232 ('s', 'subject', '', 'subject of introductory message'),
233 ('t', 'to', [], 'email addresses of recipients')]
234 options = {}
235 try:
236 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
237 options)
238 except fancyopts.getopt.GetoptError, inst:
239 u = ui.ui()
240 u.warn('error: %s' % inst)
241 sys.exit(1)
242
243 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
244 not options["noninteractive"])
245 repo = hg.repository(ui = u)
246
247 patchbomb(u, repo, *args, **options)