comparison hgext/patchbomb.py @ 1675:03191e1a4230

merge with crew
author Benoit Boissinot <benoit.boissinot@ens-lyon.org>
date Wed, 01 Feb 2006 08:50:45 +0100
parents bd53710c7780
children e70e1ed66093
comparison
equal deleted inserted replaced
1674:dee55c4a4963 1675:03191e1a4230
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 from mercurial.i18n import gettext as _
54 import os
55 import popen2
56 import smtplib
57 import socket
58 import sys
59 import tempfile
60 import time
61
62 try:
63 # readline gives raw_input editing capabilities, but is not
64 # present on windows
65 import readline
66 except ImportError: pass
67
68 def diffstat(patch):
69 fd, name = tempfile.mkstemp()
70 try:
71 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
72 try:
73 for line in patch: print >> p.tochild, line
74 p.tochild.close()
75 if p.wait(): return
76 fp = os.fdopen(fd, 'r')
77 stat = []
78 for line in fp: stat.append(line.lstrip())
79 last = stat.pop()
80 stat.insert(0, last)
81 stat = ''.join(stat)
82 if stat.startswith('0 files'): raise ValueError
83 return stat
84 except: raise
85 finally:
86 try: os.unlink(name)
87 except: pass
88
89 def patchbomb(ui, repo, *revs, **opts):
90 '''send changesets as a series of patch emails
91
92 The series starts with a "[PATCH 0 of N]" introduction, which
93 describes the series as a whole.
94
95 Each patch email has a Subject line of "[PATCH M of N] ...", using
96 the first line of the changeset description as the subject text.
97 The message contains two or three body parts. First, the rest of
98 the changeset description. Next, (optionally) if the diffstat
99 program is installed, the result of running diffstat on the patch.
100 Finally, the patch itself, as generated by "hg export".'''
101 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
102 if default: prompt += ' [%s]' % default
103 prompt += rest
104 while True:
105 r = raw_input(prompt)
106 if r: return r
107 if default is not None: return default
108 if empty_ok: return r
109 ui.warn(_('Please enter a valid value.\n'))
110
111 def confirm(s):
112 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
113 raise ValueError
114
115 def cdiffstat(summary, patch):
116 s = diffstat(patch)
117 if s:
118 if summary:
119 ui.write(summary, '\n')
120 ui.write(s, '\n')
121 confirm(_('Does the diffstat above look okay'))
122 return s
123
124 def makepatch(patch, idx, total):
125 desc = []
126 node = None
127 body = ''
128 for line in patch:
129 if line.startswith('#'):
130 if line.startswith('# Node ID'): node = line.split()[-1]
131 continue
132 if line.startswith('diff -r'): break
133 desc.append(line)
134 if not node: raise ValueError
135
136 #body = ('\n'.join(desc[1:]).strip() or
137 # 'Patch subject is complete summary.')
138 #body += '\n\n\n'
139
140 if opts['plain']:
141 while patch and patch[0].startswith('# '): patch.pop(0)
142 if patch: patch.pop(0)
143 while patch and not patch[0].strip(): patch.pop(0)
144 if opts['diffstat']:
145 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
146 body += '\n'.join(patch)
147 msg = MIMEText(body)
148 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
149 if subj.endswith('.'): subj = subj[:-1]
150 msg['Subject'] = subj
151 msg['X-Mercurial-Node'] = node
152 return msg
153
154 start_time = int(time.time())
155
156 def genmsgid(id):
157 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
158
159 patches = []
160
161 class exportee:
162 def __init__(self, container):
163 self.lines = []
164 self.container = container
165 self.name = 'email'
166
167 def write(self, data):
168 self.lines.append(data)
169
170 def close(self):
171 self.container.append(''.join(self.lines).split('\n'))
172 self.lines = []
173
174 commands.export(ui, repo, *revs, **{'output': exportee(patches),
175 'switch_parent': False,
176 'text': None})
177
178 jumbo = []
179 msgs = []
180
181 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
182
183 for p, i in zip(patches, range(len(patches))):
184 jumbo.extend(p)
185 msgs.append(makepatch(p, i + 1, len(patches)))
186
187 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
188
189 sender = (opts['from'] or ui.config('patchbomb', 'from') or
190 prompt('From', ui.username()))
191
192 msg = MIMEMultipart()
193 msg['Subject'] = '[PATCH 0 of %d] %s' % (
194 len(patches),
195 opts['subject'] or
196 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
197
198 def getaddrs(opt, prpt, default = None):
199 addrs = opts[opt] or (ui.config('patchbomb', opt) or
200 prompt(prpt, default = default)).split(',')
201 return [a.strip() for a in addrs if a.strip()]
202 to = getaddrs('to', 'To')
203 cc = getaddrs('cc', 'Cc', '')
204
205 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
206
207 body = []
208
209 while True:
210 try: l = raw_input()
211 except EOFError: break
212 if l == '.': break
213 body.append(l)
214
215 msg.attach(MIMEText('\n'.join(body) + '\n'))
216
217 ui.write('\n')
218
219 if opts['diffstat']:
220 d = cdiffstat(_('Final summary:\n'), jumbo)
221 if d: msg.attach(MIMEText(d))
222
223 msgs.insert(0, msg)
224
225 if not opts['test']:
226 s = smtplib.SMTP()
227 s.connect(host = ui.config('smtp', 'host', 'mail'),
228 port = int(ui.config('smtp', 'port', 25)))
229 if ui.configbool('smtp', 'tls'):
230 s.ehlo()
231 s.starttls()
232 s.ehlo()
233 username = ui.config('smtp', 'username')
234 password = ui.config('smtp', 'password')
235 if username and password:
236 s.login(username, password)
237 parent = None
238 tz = time.strftime('%z')
239 for m in msgs:
240 try:
241 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
242 except TypeError:
243 m['Message-Id'] = genmsgid('patchbomb')
244 if parent:
245 m['In-Reply-To'] = parent
246 else:
247 parent = m['Message-Id']
248 m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
249 start_time += 1
250 m['From'] = sender
251 m['To'] = ', '.join(to)
252 if cc: m['Cc'] = ', '.join(cc)
253 ui.status('Sending ', m['Subject'], ' ...\n')
254 if opts['test']:
255 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
256 fp.write(m.as_string(0))
257 fp.write('\n')
258 fp.close()
259 else:
260 s.sendmail(sender, to + cc, m.as_string(0))
261 if not opts['test']:
262 s.close()
263
264 cmdtable = {
265 'email':
266 (patchbomb,
267 [('c', 'cc', [], 'email addresses of copy recipients'),
268 ('d', 'diffstat', None, 'add diffstat output to messages'),
269 ('f', 'from', '', 'email address of sender'),
270 ('', 'plain', None, 'omit hg patch header'),
271 ('n', 'test', None, 'print messages that would be sent'),
272 ('s', 'subject', '', 'subject of introductory message'),
273 ('t', 'to', [], 'email addresses of recipients')],
274 "hg email [OPTION]... [REV]...")
275 }