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