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