Mercurial > hg > mercurial-crew-with-dirclash
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 } |