##// END OF EJS Templates
patchbomb: continue if we can't import readline.
Bryan O'Sullivan -
r1204:b0f6053d default
parent child Browse files
Show More
@@ -1,255 +1,260
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 import readline
56 55 import smtplib
57 56 import socket
58 57 import sys
59 58 import tempfile
60 59 import time
61 60
61 try:
62 # readline gives raw_input editing capabilities, but is not
63 # present on windows
64 import readline
65 except ImportError: pass
66
62 67 def diffstat(patch):
63 68 fd, name = tempfile.mkstemp()
64 69 try:
65 70 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
66 71 try:
67 72 for line in patch: print >> p.tochild, line
68 73 p.tochild.close()
69 74 if p.wait(): return
70 75 fp = os.fdopen(fd, 'r')
71 76 stat = []
72 77 for line in fp: stat.append(line.lstrip())
73 78 last = stat.pop()
74 79 stat.insert(0, last)
75 80 stat = ''.join(stat)
76 81 if stat.startswith('0 files'): raise ValueError
77 82 return stat
78 83 except: raise
79 84 finally:
80 85 try: os.unlink(name)
81 86 except: pass
82 87
83 88 def patchbomb(ui, repo, *revs, **opts):
84 89 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
85 90 if default: prompt += ' [%s]' % default
86 91 prompt += rest
87 92 while True:
88 93 r = raw_input(prompt)
89 94 if r: return r
90 95 if default is not None: return default
91 96 if empty_ok: return r
92 97 ui.warn('Please enter a valid value.\n')
93 98
94 99 def confirm(s):
95 100 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
96 101 raise ValueError
97 102
98 103 def cdiffstat(summary, patch):
99 104 s = diffstat(patch)
100 105 if s:
101 106 if summary:
102 107 ui.write(summary, '\n')
103 108 ui.write(s, '\n')
104 109 confirm('Does the diffstat above look okay')
105 110 return s
106 111
107 112 def makepatch(patch, idx, total):
108 113 desc = []
109 114 node = None
110 115 body = ''
111 116 for line in patch:
112 117 if line.startswith('#'):
113 118 if line.startswith('# Node ID'): node = line.split()[-1]
114 119 continue
115 120 if line.startswith('diff -r'): break
116 121 desc.append(line)
117 122 if not node: raise ValueError
118 123
119 124 #body = ('\n'.join(desc[1:]).strip() or
120 125 # 'Patch subject is complete summary.')
121 126 #body += '\n\n\n'
122 127
123 128 if opts['diffstat']:
124 129 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
125 130 body += '\n'.join(patch)
126 131 msg = MIMEText(body)
127 132 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
128 133 if subj.endswith('.'): subj = subj[:-1]
129 134 msg['Subject'] = subj
130 135 msg['X-Mercurial-Node'] = node
131 136 return msg
132 137
133 138 start_time = int(time.time())
134 139
135 140 def genmsgid(id):
136 141 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
137 142
138 143 patches = []
139 144
140 145 class exportee:
141 146 def __init__(self, container):
142 147 self.lines = []
143 148 self.container = container
144 149 self.name = 'email'
145 150
146 151 def write(self, data):
147 152 self.lines.append(data)
148 153
149 154 def close(self):
150 155 self.container.append(''.join(self.lines).split('\n'))
151 156 self.lines = []
152 157
153 158 commands.export(ui, repo, *args, **{'output': exportee(patches),
154 159 'text': None})
155 160
156 161 jumbo = []
157 162 msgs = []
158 163
159 164 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
160 165
161 166 for p, i in zip(patches, range(len(patches))):
162 167 jumbo.extend(p)
163 168 msgs.append(makepatch(p, i + 1, len(patches)))
164 169
165 170 ui.write('\nWrite the introductory message for the patch series.\n\n')
166 171
167 172 sender = (opts['from'] or ui.config('patchbomb', 'from') or
168 173 prompt('From', ui.username()))
169 174
170 175 msg = MIMEMultipart()
171 176 msg['Subject'] = '[PATCH 0 of %d] %s' % (
172 177 len(patches),
173 178 opts['subject'] or
174 179 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
175 180
176 181 def getaddrs(opt, prpt, default = None):
177 182 addrs = opts[opt] or (ui.config('patchbomb', opt) or
178 183 prompt(prpt, default = default)).split(',')
179 184 return [a.strip() for a in addrs if a.strip()]
180 185 to = getaddrs('to', 'To')
181 186 cc = getaddrs('cc', 'Cc', '')
182 187
183 188 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
184 189
185 190 body = []
186 191
187 192 while True:
188 193 try: l = raw_input()
189 194 except EOFError: break
190 195 if l == '.': break
191 196 body.append(l)
192 197
193 198 msg.attach(MIMEText('\n'.join(body) + '\n'))
194 199
195 200 ui.write('\n')
196 201
197 202 if opts['diffstat']:
198 203 d = cdiffstat('Final summary:\n', jumbo)
199 204 if d: msg.attach(MIMEText(d))
200 205
201 206 msgs.insert(0, msg)
202 207
203 208 if not opts['test']:
204 209 s = smtplib.SMTP()
205 210 s.connect(host = ui.config('smtp', 'host', 'mail'),
206 211 port = int(ui.config('smtp', 'port', 25)))
207 212
208 213 parent = None
209 214 tz = time.strftime('%z')
210 215 for m in msgs:
211 216 try:
212 217 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
213 218 except TypeError:
214 219 m['Message-Id'] = genmsgid('patchbomb')
215 220 if parent:
216 221 m['In-Reply-To'] = parent
217 222 else:
218 223 parent = m['Message-Id']
219 224 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
220 225 start_time += 1
221 226 m['From'] = sender
222 227 m['To'] = ', '.join(to)
223 228 if cc: m['Cc'] = ', '.join(cc)
224 229 ui.status('Sending ', m['Subject'], ' ...\n')
225 230 if opts['test']:
226 231 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
227 232 fp.write(m.as_string(0))
228 233 fp.write('\n')
229 234 fp.close()
230 235 else:
231 236 s.sendmail(sender, to + cc, m.as_string(0))
232 237 if not opts['test']:
233 238 s.close()
234 239
235 240 if __name__ == '__main__':
236 241 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
237 242 ('d', 'diffstat', None, 'add diffstat output to messages'),
238 243 ('f', 'from', '', 'email address of sender'),
239 244 ('n', 'test', None, 'print messages that would be sent'),
240 245 ('s', 'subject', '', 'subject of introductory message'),
241 246 ('t', 'to', [], 'email addresses of recipients')]
242 247 options = {}
243 248 try:
244 249 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
245 250 options)
246 251 except fancyopts.getopt.GetoptError, inst:
247 252 u = ui.ui()
248 253 u.warn('error: %s' % inst)
249 254 sys.exit(1)
250 255
251 256 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
252 257 not options["noninteractive"])
253 258 repo = hg.repository(ui = u)
254 259
255 260 patchbomb(u, repo, *args, **options)
General Comments 0
You need to be logged in to leave comments. Login now