##// END OF EJS Templates
Variable 'body' was missing in patchbomb script.
Thomas Arendsen Hein -
r1135:e455d91f default
parent child Browse files
Show More
@@ -1,251 +1,252 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 body = ''
110 for line in patch:
111 for line in patch:
111 if line.startswith('#'):
112 if line.startswith('#'):
112 if line.startswith('# Node ID'): node = line.split()[-1]
113 if line.startswith('# Node ID'): node = line.split()[-1]
113 continue
114 continue
114 if line.startswith('diff -r'): break
115 if line.startswith('diff -r'): break
115 desc.append(line)
116 desc.append(line)
116 if not node: raise ValueError
117 if not node: raise ValueError
117
118
118 #body = ('\n'.join(desc[1:]).strip() or
119 #body = ('\n'.join(desc[1:]).strip() or
119 # 'Patch subject is complete summary.')
120 # 'Patch subject is complete summary.')
120 #body += '\n\n\n'
121 #body += '\n\n\n'
121
122
122 if opts['diffstat']:
123 if opts['diffstat']:
123 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
124 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
124 body += '\n'.join(patch)
125 body += '\n'.join(patch)
125 msg = MIMEText(body)
126 msg = MIMEText(body)
126 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
127 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
127 if subj.endswith('.'): subj = subj[:-1]
128 if subj.endswith('.'): subj = subj[:-1]
128 msg['Subject'] = subj
129 msg['Subject'] = subj
129 msg['X-Mercurial-Node'] = node
130 msg['X-Mercurial-Node'] = node
130 return msg
131 return msg
131
132
132 start_time = int(time.time())
133 start_time = int(time.time())
133
134
134 def genmsgid(id):
135 def genmsgid(id):
135 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
136 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
136
137
137 patches = []
138 patches = []
138
139
139 class exportee:
140 class exportee:
140 def __init__(self, container):
141 def __init__(self, container):
141 self.lines = []
142 self.lines = []
142 self.container = container
143 self.container = container
143 self.name = 'email'
144 self.name = 'email'
144
145
145 def write(self, data):
146 def write(self, data):
146 self.lines.append(data)
147 self.lines.append(data)
147
148
148 def close(self):
149 def close(self):
149 self.container.append(''.join(self.lines).split('\n'))
150 self.container.append(''.join(self.lines).split('\n'))
150 self.lines = []
151 self.lines = []
151
152
152 commands.export(ui, repo, *args, **{'output': exportee(patches),
153 commands.export(ui, repo, *args, **{'output': exportee(patches),
153 'text': None})
154 'text': None})
154
155
155 jumbo = []
156 jumbo = []
156 msgs = []
157 msgs = []
157
158
158 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
159 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
159
160
160 for p, i in zip(patches, range(len(patches))):
161 for p, i in zip(patches, range(len(patches))):
161 jumbo.extend(p)
162 jumbo.extend(p)
162 msgs.append(makepatch(p, i + 1, len(patches)))
163 msgs.append(makepatch(p, i + 1, len(patches)))
163
164
164 ui.write('\nWrite the introductory message for the patch series.\n\n')
165 ui.write('\nWrite the introductory message for the patch series.\n\n')
165
166
166 sender = (opts['from'] or ui.config('patchbomb', 'from') or
167 sender = (opts['from'] or ui.config('patchbomb', 'from') or
167 prompt('From', ui.username()))
168 prompt('From', ui.username()))
168
169
169 msg = MIMEMultipart()
170 msg = MIMEMultipart()
170 msg['Subject'] = '[PATCH 0 of %d] %s' % (
171 msg['Subject'] = '[PATCH 0 of %d] %s' % (
171 len(patches),
172 len(patches),
172 opts['subject'] or
173 opts['subject'] or
173 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
174 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
174 to = opts['to'] or ui.config('patchbomb', 'to') or prompt('To')
175 to = opts['to'] or ui.config('patchbomb', 'to') or prompt('To')
175 to = [t.strip() for t in to.split(',')]
176 to = [t.strip() for t in to.split(',')]
176 cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
177 cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
177 prompt('Cc', default = ''))
178 prompt('Cc', default = ''))
178 cc = (cc and [c.strip() for c in cc.split(',')]) or []
179 cc = (cc and [c.strip() for c in cc.split(',')]) or []
179
180
180 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
181 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
181
182
182 body = []
183 body = []
183
184
184 while True:
185 while True:
185 try: l = raw_input()
186 try: l = raw_input()
186 except EOFError: break
187 except EOFError: break
187 if l == '.': break
188 if l == '.': break
188 body.append(l)
189 body.append(l)
189
190
190 msg.attach(MIMEText('\n'.join(body) + '\n'))
191 msg.attach(MIMEText('\n'.join(body) + '\n'))
191
192
192 ui.write('\n')
193 ui.write('\n')
193
194
194 d = cdiffstat('Final summary:\n', jumbo)
195 d = cdiffstat('Final summary:\n', jumbo)
195 if d: msg.attach(MIMEText(d))
196 if d: msg.attach(MIMEText(d))
196
197
197 msgs.insert(0, msg)
198 msgs.insert(0, msg)
198
199
199 if not opts['test']:
200 if not opts['test']:
200 s = smtplib.SMTP()
201 s = smtplib.SMTP()
201 s.connect(host = ui.config('smtp', 'host', 'mail'),
202 s.connect(host = ui.config('smtp', 'host', 'mail'),
202 port = int(ui.config('smtp', 'port', 25)))
203 port = int(ui.config('smtp', 'port', 25)))
203
204
204 parent = None
205 parent = None
205 tz = time.strftime('%z')
206 tz = time.strftime('%z')
206 for m in msgs:
207 for m in msgs:
207 try:
208 try:
208 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
209 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
209 except TypeError:
210 except TypeError:
210 m['Message-Id'] = genmsgid('patchbomb')
211 m['Message-Id'] = genmsgid('patchbomb')
211 if parent:
212 if parent:
212 m['In-Reply-To'] = parent
213 m['In-Reply-To'] = parent
213 else:
214 else:
214 parent = m['Message-Id']
215 parent = m['Message-Id']
215 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
216 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
216 start_time += 1
217 start_time += 1
217 m['From'] = sender
218 m['From'] = sender
218 m['To'] = ', '.join(to)
219 m['To'] = ', '.join(to)
219 if cc: m['Cc'] = ', '.join(cc)
220 if cc: m['Cc'] = ', '.join(cc)
220 ui.status('Sending ', m['Subject'], ' ...\n')
221 ui.status('Sending ', m['Subject'], ' ...\n')
221 if opts['test']:
222 if opts['test']:
222 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
223 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
223 fp.write(m.as_string(0))
224 fp.write(m.as_string(0))
224 fp.write('\n')
225 fp.write('\n')
225 fp.close()
226 fp.close()
226 else:
227 else:
227 s.sendmail(sender, to + cc, m.as_string(0))
228 s.sendmail(sender, to + cc, m.as_string(0))
228 if not opts['test']:
229 if not opts['test']:
229 s.close()
230 s.close()
230
231
231 if __name__ == '__main__':
232 if __name__ == '__main__':
232 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
233 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
233 ('d', 'diffstat', None, 'add diffstat output to messages'),
234 ('d', 'diffstat', None, 'add diffstat output to messages'),
234 ('f', 'from', '', 'email address of sender'),
235 ('f', 'from', '', 'email address of sender'),
235 ('n', 'test', None, 'print messages that would be sent'),
236 ('n', 'test', None, 'print messages that would be sent'),
236 ('s', 'subject', '', 'subject of introductory message'),
237 ('s', 'subject', '', 'subject of introductory message'),
237 ('t', 'to', [], 'email addresses of recipients')]
238 ('t', 'to', [], 'email addresses of recipients')]
238 options = {}
239 options = {}
239 try:
240 try:
240 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
241 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
241 options)
242 options)
242 except fancyopts.getopt.GetoptError, inst:
243 except fancyopts.getopt.GetoptError, inst:
243 u = ui.ui()
244 u = ui.ui()
244 u.warn('error: %s' % inst)
245 u.warn('error: %s' % inst)
245 sys.exit(1)
246 sys.exit(1)
246
247
247 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
248 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
248 not options["noninteractive"])
249 not options["noninteractive"])
249 repo = hg.repository(ui = u)
250 repo = hg.repository(ui = u)
250
251
251 patchbomb(u, repo, *args, **options)
252 patchbomb(u, repo, *args, **options)
General Comments 0
You need to be logged in to leave comments. Login now