comparison hgext/patchbomb.py @ 1669:91d40fc959f0

turn patchbomb script into an extension module. command name is now 'hg email'.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Tue, 31 Jan 2006 08:06:35 -0800
parents contrib/patchbomb@da3f1121721b
children fe19c54ee403
comparison
equal deleted inserted replaced
1667:daff3ef0de8d 1669:91d40fc959f0
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
10 #
11 # The remainder of the changeset description.
12 #
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
15 #
16 # The patch itself, as generated by "hg export".
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
25 #
26 # It is best to run this script with the "-n" (test only) flag before
27 # firing it up "for real", in which case it will use your pager to
28 # display each of the messages that it would send.
29 #
30 # To configure a default mail host, add a section like this to your
31 # hgrc file:
32 #
33 # [smtp]
34 # host = my_mail_host
35 # port = 1025
36 # tls = yes # or omit if not needed
37 # username = user # if SMTP authentication required
38 # password = password # if SMTP authentication required - PLAINTEXT
39 #
40 # To configure other defaults, add a section like this to your hgrc
41 # file:
42 #
43 # [patchbomb]
44 # from = My Name <my@email>
45 # to = recipient1, recipient2, ...
46 # cc = cc1, cc2, ...
47
48 from email.MIMEMultipart import MIMEMultipart
49 from email.MIMEText import MIMEText
50 from mercurial import commands
51 from mercurial import hg
52 from mercurial import ui
53 import os
54 import popen2
55 import smtplib
56 import socket
57 import sys
58 import tempfile
59 import time
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
67 def diffstat(patch):
68 fd, name = tempfile.mkstemp()
69 try:
70 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
71 try:
72 for line in patch: print >> p.tochild, line
73 p.tochild.close()
74 if p.wait(): return
75 fp = os.fdopen(fd, 'r')
76 stat = []
77 for line in fp: stat.append(line.lstrip())
78 last = stat.pop()
79 stat.insert(0, last)
80 stat = ''.join(stat)
81 if stat.startswith('0 files'): raise ValueError
82 return stat
83 except: raise
84 finally:
85 try: os.unlink(name)
86 except: pass
87
88 def patchbomb(ui, repo, *revs, **opts):
89 '''send changesets as a series of patch emails'''
90 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
91 if default: prompt += ' [%s]' % default
92 prompt += rest
93 while True:
94 r = raw_input(prompt)
95 if r: return r
96 if default is not None: return default
97 if empty_ok: return r
98 ui.warn('Please enter a valid value.\n')
99
100 def confirm(s):
101 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
102 raise ValueError
103
104 def cdiffstat(summary, patch):
105 s = diffstat(patch)
106 if s:
107 if summary:
108 ui.write(summary, '\n')
109 ui.write(s, '\n')
110 confirm('Does the diffstat above look okay')
111 return s
112
113 def makepatch(patch, idx, total):
114 desc = []
115 node = None
116 body = ''
117 for line in patch:
118 if line.startswith('#'):
119 if line.startswith('# Node ID'): node = line.split()[-1]
120 continue
121 if line.startswith('diff -r'): break
122 desc.append(line)
123 if not node: raise ValueError
124
125 #body = ('\n'.join(desc[1:]).strip() or
126 # 'Patch subject is complete summary.')
127 #body += '\n\n\n'
128
129 if opts['plain']:
130 while patch and patch[0].startswith('# '): patch.pop(0)
131 if patch: patch.pop(0)
132 while patch and not patch[0].strip(): patch.pop(0)
133 if opts['diffstat']:
134 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
135 body += '\n'.join(patch)
136 msg = MIMEText(body)
137 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
138 if subj.endswith('.'): subj = subj[:-1]
139 msg['Subject'] = subj
140 msg['X-Mercurial-Node'] = node
141 return msg
142
143 start_time = int(time.time())
144
145 def genmsgid(id):
146 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
147
148 patches = []
149
150 class exportee:
151 def __init__(self, container):
152 self.lines = []
153 self.container = container
154 self.name = 'email'
155
156 def write(self, data):
157 self.lines.append(data)
158
159 def close(self):
160 self.container.append(''.join(self.lines).split('\n'))
161 self.lines = []
162
163 commands.export(ui, repo, *revs, **{'output': exportee(patches),
164 'switch_parent': False,
165 'text': None})
166
167 jumbo = []
168 msgs = []
169
170 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
171
172 for p, i in zip(patches, range(len(patches))):
173 jumbo.extend(p)
174 msgs.append(makepatch(p, i + 1, len(patches)))
175
176 ui.write('\nWrite the introductory message for the patch series.\n\n')
177
178 sender = (opts['from'] or ui.config('patchbomb', 'from') or
179 prompt('From', ui.username()))
180
181 msg = MIMEMultipart()
182 msg['Subject'] = '[PATCH 0 of %d] %s' % (
183 len(patches),
184 opts['subject'] or
185 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
186
187 def getaddrs(opt, prpt, default = None):
188 addrs = opts[opt] or (ui.config('patchbomb', opt) or
189 prompt(prpt, default = default)).split(',')
190 return [a.strip() for a in addrs if a.strip()]
191 to = getaddrs('to', 'To')
192 cc = getaddrs('cc', 'Cc', '')
193
194 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
195
196 body = []
197
198 while True:
199 try: l = raw_input()
200 except EOFError: break
201 if l == '.': break
202 body.append(l)
203
204 msg.attach(MIMEText('\n'.join(body) + '\n'))
205
206 ui.write('\n')
207
208 if opts['diffstat']:
209 d = cdiffstat('Final summary:\n', jumbo)
210 if d: msg.attach(MIMEText(d))
211
212 msgs.insert(0, msg)
213
214 if not opts['test']:
215 s = smtplib.SMTP()
216 s.connect(host = ui.config('smtp', 'host', 'mail'),
217 port = int(ui.config('smtp', 'port', 25)))
218 if ui.configbool('smtp', 'tls'):
219 s.ehlo()
220 s.starttls()
221 s.ehlo()
222 username = ui.config('smtp', 'username')
223 password = ui.config('smtp', 'password')
224 if username and password:
225 s.login(username, password)
226 parent = None
227 tz = time.strftime('%z')
228 for m in msgs:
229 try:
230 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
231 except TypeError:
232 m['Message-Id'] = genmsgid('patchbomb')
233 if parent:
234 m['In-Reply-To'] = parent
235 else:
236 parent = m['Message-Id']
237 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
238 start_time += 1
239 m['From'] = sender
240 m['To'] = ', '.join(to)
241 if cc: m['Cc'] = ', '.join(cc)
242 ui.status('Sending ', m['Subject'], ' ...\n')
243 if opts['test']:
244 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
245 fp.write(m.as_string(0))
246 fp.write('\n')
247 fp.close()
248 else:
249 s.sendmail(sender, to + cc, m.as_string(0))
250 if not opts['test']:
251 s.close()
252
253 cmdtable = {
254 'email':
255 (patchbomb,
256 [('c', 'cc', [], 'email addresses of copy recipients'),
257 ('d', 'diffstat', None, 'add diffstat output to messages'),
258 ('f', 'from', '', 'email address of sender'),
259 ('', 'plain', None, 'omit hg patch header'),
260 ('n', 'test', None, 'print messages that would be sent'),
261 ('s', 'subject', '', 'subject of introductory message'),
262 ('t', 'to', [], 'email addresses of recipients')],
263 "hg email [OPTION]... [REV]...")
264 }