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