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