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