875
|
1 #!/usr/bin/python
|
|
2 #
|
|
3 # Interactive script for sending a collection of Mercurial changesets
|
|
4 # as a series of patch emails.
|
|
5 #
|
|
6 # The series is started off with a "[PATCH 0 of N]" introduction,
|
|
7 # which describes the series as a whole.
|
|
8 #
|
|
9 # Each patch email has a Subject line of "[PATCH M of N] ...", using
|
|
10 # the first line of the changeset description as the subject text.
|
|
11 # The message contains two or three body parts:
|
|
12 #
|
|
13 # The remainder of the changeset description.
|
|
14 #
|
|
15 # If the diffstat program is installed, the result of running
|
|
16 # diffstat on the patch.
|
|
17 #
|
|
18 # The patch itself, as generated by "hg export".
|
|
19 #
|
|
20 # Each message refers to all of its predecessors using the In-Reply-To
|
|
21 # and References headers, so they will show up as a sequence in
|
|
22 # threaded mail and news readers, and in mail archives.
|
|
23 #
|
|
24 # For each changeset, you will be prompted with a diffstat summary and
|
|
25 # the changeset summary, so you can be sure you are sending the right
|
|
26 # changes.
|
|
27 #
|
|
28 # It is best to run this script with the "-n" (test only) flag before
|
|
29 # firing it up "for real", in which case it will display each of the
|
|
30 # messages that it would send.
|
|
31 #
|
|
32 # To configure a default mail host, add a section like this to your
|
|
33 # hgrc file:
|
|
34 #
|
|
35 # [smtp]
|
|
36 # host = my_mail_host
|
|
37 # port = 1025
|
|
38
|
|
39 from email.MIMEMultipart import MIMEMultipart
|
|
40 from email.MIMEText import MIMEText
|
|
41 from mercurial import commands
|
|
42 from mercurial import fancyopts
|
|
43 from mercurial import hg
|
|
44 from mercurial import ui
|
|
45 import os
|
|
46 import popen2
|
|
47 import readline
|
|
48 import smtplib
|
|
49 import socket
|
|
50 import sys
|
|
51 import tempfile
|
|
52 import time
|
|
53
|
|
54 def diffstat(patch):
|
|
55 fd, name = tempfile.mkstemp()
|
|
56 try:
|
|
57 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
|
|
58 try:
|
|
59 for line in patch: print >> p.tochild, line
|
|
60 p.tochild.close()
|
|
61 if p.wait(): return
|
|
62 fp = os.fdopen(fd, 'r')
|
|
63 stat = []
|
|
64 for line in fp: stat.append(line.lstrip())
|
|
65 last = stat.pop()
|
|
66 stat.insert(0, last)
|
|
67 stat = ''.join(stat)
|
|
68 if stat.startswith('0 files'): raise ValueError
|
|
69 return stat
|
|
70 except: raise
|
|
71 finally:
|
|
72 try: os.unlink(name)
|
|
73 except: pass
|
|
74
|
|
75 def patchbomb(ui, repo, *revs, **opts):
|
|
76 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
|
|
77 try:
|
|
78 if default: prompt += ' [%s]' % default
|
|
79 prompt += rest
|
|
80 r = raw_input(prompt)
|
|
81 if not r and not empty_ok: raise EOFError
|
|
82 return r
|
|
83 except EOFError:
|
|
84 if default is None: raise
|
|
85 return default
|
|
86
|
|
87 def confirm(s):
|
|
88 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
|
|
89 raise ValueError
|
|
90
|
|
91 def cdiffstat(summary, patch):
|
|
92 s = diffstat(patch)
|
|
93 if s:
|
|
94 if summary:
|
|
95 ui.write(summary, '\n')
|
|
96 ui.write(s, '\n')
|
|
97 confirm('Does the diffstat above look okay')
|
|
98 return s
|
|
99
|
|
100 def make_patch(patch, idx, total):
|
|
101 desc = []
|
|
102 node = None
|
|
103 for line in patch:
|
|
104 if line.startswith('#'):
|
|
105 if line.startswith('# Node ID'): node = line.split()[-1]
|
|
106 continue
|
|
107 if line.startswith('diff -r'): break
|
|
108 desc.append(line)
|
|
109 if not node: raise ValueError
|
|
110 msg = MIMEMultipart()
|
|
111 msg['X-Mercurial-Node'] = node
|
|
112 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
|
|
113 if subj.endswith('.'): subj = subj[:-1]
|
|
114 msg['Subject'] = subj
|
|
115 body = '\n'.join(desc[1:]).strip() + '\n'
|
|
116 summary = subj
|
|
117 if body != '\n':
|
|
118 msg.attach(MIMEText(body))
|
|
119 summary += '\n\n' + body
|
|
120 else:
|
|
121 summary += '\n'
|
|
122 d = cdiffstat(summary, patch)
|
|
123 if d: msg.attach(MIMEText(d))
|
|
124 p = MIMEText('\n'.join(patch), 'x-patch')
|
|
125 p['Content-Disposition'] = commands.make_filename(repo, None,
|
|
126 'inline; filename=%b-%n.patch',
|
|
127 seqno = idx)
|
|
128 msg.attach(p)
|
|
129 return msg
|
|
130
|
|
131 start_time = int(time.time())
|
|
132
|
|
133 def make_msgid(id):
|
|
134 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
|
|
135
|
|
136 patches = []
|
|
137
|
|
138 class exportee:
|
|
139 def __init__(self, container):
|
|
140 self.lines = []
|
|
141 self.container = container
|
|
142
|
|
143 def write(self, data):
|
|
144 self.lines.append(data)
|
|
145
|
|
146 def close(self):
|
|
147 self.container.append(''.join(self.lines).split('\n'))
|
|
148 self.lines = []
|
|
149
|
|
150 commands.export(ui, repo, *args, **{'output': exportee(patches)})
|
|
151
|
|
152 jumbo = []
|
|
153 msgs = []
|
|
154
|
|
155 ui.write('This patch series consists of %d patches.\n\n' % len(patches))
|
|
156
|
|
157 for p, i in zip(patches, range(len(patches))):
|
|
158 jumbo.extend(p)
|
|
159 msgs.append(make_patch(p, i + 1, len(patches)))
|
|
160
|
|
161 ui.write('\nWrite the introductory message for the patch series.\n\n')
|
|
162
|
|
163 sender = opts['sender'] or prompt('From', ui.username())
|
|
164
|
|
165 msg = MIMEMultipart()
|
|
166 msg['Subject'] = '[PATCH 0 of %d] %s' % (
|
|
167 len(patches),
|
|
168 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
|
|
169 to = opts['to'] or [s.strip() for s in prompt('To').split(',')]
|
|
170 cc = opts['cc'] or [s.strip() for s in prompt('Cc', default = '').split(',')]
|
|
171
|
|
172 ui.write('Finish with ^D or a dot on a line by itself.\n\n')
|
|
173
|
|
174 body = []
|
|
175
|
|
176 while True:
|
|
177 try: l = raw_input()
|
|
178 except EOFError: break
|
|
179 if l == '.': break
|
|
180 body.append(l)
|
|
181
|
|
182 msg.attach(MIMEText('\n'.join(body) + '\n'))
|
|
183
|
|
184 ui.write('\n')
|
|
185
|
|
186 d = cdiffstat('Final summary:\n', jumbo)
|
|
187 if d: msg.attach(MIMEText(d))
|
|
188
|
|
189 msgs.insert(0, msg)
|
|
190
|
|
191 s = smtplib.SMTP()
|
|
192 s.connect(host = ui.config('smtp', 'host', 'mail'),
|
|
193 port = int(ui.config('smtp', 'port', 25)))
|
|
194
|
|
195 refs = []
|
|
196 parent = None
|
|
197 tz = time.strftime('%z')
|
|
198 for m in msgs:
|
|
199 try:
|
|
200 m['Message-Id'] = make_msgid(m['X-Mercurial-Node'])
|
|
201 except TypeError:
|
|
202 m['Message-Id'] = make_msgid('patchbomb')
|
|
203 if parent:
|
|
204 m['In-Reply-To'] = parent
|
|
205 parent = m['Message-Id']
|
|
206 if len(refs) > 1:
|
|
207 m['References'] = ' '.join(refs[:-1])
|
|
208 refs.append(parent)
|
|
209 m['Date'] = time.strftime('%a, %m %b %Y %T ', time.localtime(start_time)) + tz
|
|
210 start_time += 1
|
|
211 m['From'] = sender
|
|
212 m['To'] = ', '.join(to)
|
|
213 if cc: m['Cc'] = ', '.join(cc)
|
|
214 ui.status('Sending ', m['Subject'], ' ...\n')
|
|
215 if opts['test']:
|
|
216 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
|
|
217 fp.write(m.as_string(0))
|
|
218 fp.write('\n')
|
|
219 fp.close()
|
|
220 else:
|
|
221 s.sendmail(sender, to + cc, m.as_string(0))
|
|
222 s.close()
|
|
223
|
|
224 if __name__ == '__main__':
|
|
225 optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
|
|
226 ('n', 'test', None, 'print messages that would be sent'),
|
|
227 ('s', 'sender', '', 'email address of sender'),
|
|
228 ('t', 'to', [], 'email addresses of recipients')]
|
|
229 options = {}
|
|
230 try:
|
|
231 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
|
|
232 options)
|
|
233 except fancyopts.getopt.GetoptError, inst:
|
|
234 u = ui.ui()
|
|
235 u.warn('error: %s' % inst)
|
|
236 sys.exit(1)
|
|
237
|
|
238 u = ui.ui(options["verbose"], options["debug"], options["quiet"],
|
|
239 not options["noninteractive"])
|
|
240 repo = hg.repository(ui = u)
|
|
241
|
|
242 patchbomb(u, repo, *args, **options)
|