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