##// END OF EJS Templates
patchbomb: extract 'getpatchmsgs' closure into its own function...
Pierre-Yves David -
r23215:83a19103 default
parent child Browse files
Show More
@@ -1,611 +1,620 b''
1 # patchbomb.py - sending Mercurial changesets as patch emails
1 # patchbomb.py - sending Mercurial changesets as patch emails
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to send changesets as (a series of) patch emails
8 '''command to send changesets as (a series of) patch emails
9
9
10 The series is started off with a "[PATCH 0 of N]" introduction, which
10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 describes the series as a whole.
11 describes the series as a whole.
12
12
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 first line of the changeset description as the subject text. The
14 first line of the changeset description as the subject text. The
15 message contains two or three body parts:
15 message contains two or three body parts:
16
16
17 - The changeset description.
17 - The changeset description.
18 - [Optional] The result of running diffstat on the patch.
18 - [Optional] The result of running diffstat on the patch.
19 - The patch itself, as generated by :hg:`export`.
19 - The patch itself, as generated by :hg:`export`.
20
20
21 Each message refers to the first in the series using the In-Reply-To
21 Each message refers to the first in the series using the In-Reply-To
22 and References headers, so they will show up as a sequence in threaded
22 and References headers, so they will show up as a sequence in threaded
23 mail and news readers, and in mail archives.
23 mail and news readers, and in mail archives.
24
24
25 To configure other defaults, add a section like this to your
25 To configure other defaults, add a section like this to your
26 configuration file::
26 configuration file::
27
27
28 [email]
28 [email]
29 from = My Name <my@email>
29 from = My Name <my@email>
30 to = recipient1, recipient2, ...
30 to = recipient1, recipient2, ...
31 cc = cc1, cc2, ...
31 cc = cc1, cc2, ...
32 bcc = bcc1, bcc2, ...
32 bcc = bcc1, bcc2, ...
33 reply-to = address1, address2, ...
33 reply-to = address1, address2, ...
34
34
35 Use ``[patchbomb]`` as configuration section name if you need to
35 Use ``[patchbomb]`` as configuration section name if you need to
36 override global ``[email]`` address settings.
36 override global ``[email]`` address settings.
37
37
38 Then you can use the :hg:`email` command to mail a series of
38 Then you can use the :hg:`email` command to mail a series of
39 changesets as a patchbomb.
39 changesets as a patchbomb.
40
40
41 You can also either configure the method option in the email section
41 You can also either configure the method option in the email section
42 to be a sendmail compatible mailer or fill out the [smtp] section so
42 to be a sendmail compatible mailer or fill out the [smtp] section so
43 that the patchbomb extension can automatically send patchbombs
43 that the patchbomb extension can automatically send patchbombs
44 directly from the commandline. See the [email] and [smtp] sections in
44 directly from the commandline. See the [email] and [smtp] sections in
45 hgrc(5) for details.
45 hgrc(5) for details.
46 '''
46 '''
47
47
48 import os, errno, socket, tempfile, cStringIO
48 import os, errno, socket, tempfile, cStringIO
49 import email
49 import email
50 # On python2.4 you have to import these by name or they fail to
50 # On python2.4 you have to import these by name or they fail to
51 # load. This was not a problem on Python 2.7.
51 # load. This was not a problem on Python 2.7.
52 import email.Generator
52 import email.Generator
53 import email.MIMEMultipart
53 import email.MIMEMultipart
54
54
55 from mercurial import cmdutil, commands, hg, mail, patch, util
55 from mercurial import cmdutil, commands, hg, mail, patch, util
56 from mercurial import scmutil
56 from mercurial import scmutil
57 from mercurial.i18n import _
57 from mercurial.i18n import _
58 from mercurial.node import bin
58 from mercurial.node import bin
59
59
60 cmdtable = {}
60 cmdtable = {}
61 command = cmdutil.command(cmdtable)
61 command = cmdutil.command(cmdtable)
62 testedwith = 'internal'
62 testedwith = 'internal'
63
63
64 def prompt(ui, prompt, default=None, rest=':'):
64 def prompt(ui, prompt, default=None, rest=':'):
65 if default:
65 if default:
66 prompt += ' [%s]' % default
66 prompt += ' [%s]' % default
67 return ui.prompt(prompt + rest, default)
67 return ui.prompt(prompt + rest, default)
68
68
69 def introwanted(opts, number):
69 def introwanted(opts, number):
70 '''is an introductory message apparently wanted?'''
70 '''is an introductory message apparently wanted?'''
71 return number > 1 or opts.get('intro') or opts.get('desc')
71 return number > 1 or opts.get('intro') or opts.get('desc')
72
72
73 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total, numbered,
73 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total, numbered,
74 patchname=None):
74 patchname=None):
75
75
76 desc = []
76 desc = []
77 node = None
77 node = None
78 body = ''
78 body = ''
79
79
80 for line in patchlines:
80 for line in patchlines:
81 if line.startswith('#'):
81 if line.startswith('#'):
82 if line.startswith('# Node ID'):
82 if line.startswith('# Node ID'):
83 node = line.split()[-1]
83 node = line.split()[-1]
84 continue
84 continue
85 if line.startswith('diff -r') or line.startswith('diff --git'):
85 if line.startswith('diff -r') or line.startswith('diff --git'):
86 break
86 break
87 desc.append(line)
87 desc.append(line)
88
88
89 if not patchname and not node:
89 if not patchname and not node:
90 raise ValueError
90 raise ValueError
91
91
92 if opts.get('attach') and not opts.get('body'):
92 if opts.get('attach') and not opts.get('body'):
93 body = ('\n'.join(desc[1:]).strip() or
93 body = ('\n'.join(desc[1:]).strip() or
94 'Patch subject is complete summary.')
94 'Patch subject is complete summary.')
95 body += '\n\n\n'
95 body += '\n\n\n'
96
96
97 if opts.get('plain'):
97 if opts.get('plain'):
98 while patchlines and patchlines[0].startswith('# '):
98 while patchlines and patchlines[0].startswith('# '):
99 patchlines.pop(0)
99 patchlines.pop(0)
100 if patchlines:
100 if patchlines:
101 patchlines.pop(0)
101 patchlines.pop(0)
102 while patchlines and not patchlines[0].strip():
102 while patchlines and not patchlines[0].strip():
103 patchlines.pop(0)
103 patchlines.pop(0)
104
104
105 ds = patch.diffstat(patchlines, git=opts.get('git'))
105 ds = patch.diffstat(patchlines, git=opts.get('git'))
106 if opts.get('diffstat'):
106 if opts.get('diffstat'):
107 body += ds + '\n\n'
107 body += ds + '\n\n'
108
108
109 addattachment = opts.get('attach') or opts.get('inline')
109 addattachment = opts.get('attach') or opts.get('inline')
110 if not addattachment or opts.get('body'):
110 if not addattachment or opts.get('body'):
111 body += '\n'.join(patchlines)
111 body += '\n'.join(patchlines)
112
112
113 if addattachment:
113 if addattachment:
114 msg = email.MIMEMultipart.MIMEMultipart()
114 msg = email.MIMEMultipart.MIMEMultipart()
115 if body:
115 if body:
116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch',
118 opts.get('test'))
118 opts.get('test'))
119 binnode = bin(node)
119 binnode = bin(node)
120 # if node is mq patch, it will have the patch file's name as a tag
120 # if node is mq patch, it will have the patch file's name as a tag
121 if not patchname:
121 if not patchname:
122 patchtags = [t for t in repo.nodetags(binnode)
122 patchtags = [t for t in repo.nodetags(binnode)
123 if t.endswith('.patch') or t.endswith('.diff')]
123 if t.endswith('.patch') or t.endswith('.diff')]
124 if patchtags:
124 if patchtags:
125 patchname = patchtags[0]
125 patchname = patchtags[0]
126 elif total > 1:
126 elif total > 1:
127 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
127 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
128 binnode, seqno=idx,
128 binnode, seqno=idx,
129 total=total)
129 total=total)
130 else:
130 else:
131 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
131 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
132 disposition = 'inline'
132 disposition = 'inline'
133 if opts.get('attach'):
133 if opts.get('attach'):
134 disposition = 'attachment'
134 disposition = 'attachment'
135 p['Content-Disposition'] = disposition + '; filename=' + patchname
135 p['Content-Disposition'] = disposition + '; filename=' + patchname
136 msg.attach(p)
136 msg.attach(p)
137 else:
137 else:
138 msg = mail.mimetextpatch(body, display=opts.get('test'))
138 msg = mail.mimetextpatch(body, display=opts.get('test'))
139
139
140 flag = ' '.join(opts.get('flag'))
140 flag = ' '.join(opts.get('flag'))
141 if flag:
141 if flag:
142 flag = ' ' + flag
142 flag = ' ' + flag
143
143
144 subj = desc[0].strip().rstrip('. ')
144 subj = desc[0].strip().rstrip('. ')
145 if not numbered:
145 if not numbered:
146 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
146 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
147 else:
147 else:
148 tlen = len(str(total))
148 tlen = len(str(total))
149 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
149 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
150 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
150 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
151 msg['X-Mercurial-Node'] = node
151 msg['X-Mercurial-Node'] = node
152 msg['X-Mercurial-Series-Index'] = '%i' % idx
152 msg['X-Mercurial-Series-Index'] = '%i' % idx
153 msg['X-Mercurial-Series-Total'] = '%i' % total
153 msg['X-Mercurial-Series-Total'] = '%i' % total
154 return msg, subj, ds
154 return msg, subj, ds
155
155
156 def _getpatches(repo, revs, **opts):
156 def _getpatches(repo, revs, **opts):
157 """return a list of patches for a list of revisions
157 """return a list of patches for a list of revisions
158
158
159 Each patch in the list is itself a list of lines.
159 Each patch in the list is itself a list of lines.
160 """
160 """
161 ui = repo.ui
161 ui = repo.ui
162 prev = repo['.'].rev()
162 prev = repo['.'].rev()
163 for r in scmutil.revrange(repo, revs):
163 for r in scmutil.revrange(repo, revs):
164 if r == prev and (repo[None].files() or repo[None].deleted()):
164 if r == prev and (repo[None].files() or repo[None].deleted()):
165 ui.warn(_('warning: working directory has '
165 ui.warn(_('warning: working directory has '
166 'uncommitted changes\n'))
166 'uncommitted changes\n'))
167 output = cStringIO.StringIO()
167 output = cStringIO.StringIO()
168 cmdutil.export(repo, [r], fp=output,
168 cmdutil.export(repo, [r], fp=output,
169 opts=patch.diffopts(ui, opts))
169 opts=patch.diffopts(ui, opts))
170 yield output.getvalue().split('\n')
170 yield output.getvalue().split('\n')
171 def _getbundle(repo, dest, **opts):
171 def _getbundle(repo, dest, **opts):
172 """return a bundle containing changesets missing in "dest"
172 """return a bundle containing changesets missing in "dest"
173
173
174 The `opts` keyword-arguments are the same as the one accepted by the
174 The `opts` keyword-arguments are the same as the one accepted by the
175 `bundle` command.
175 `bundle` command.
176
176
177 The bundle is a returned as a single in-memory binary blob.
177 The bundle is a returned as a single in-memory binary blob.
178 """
178 """
179 ui = repo.ui
179 ui = repo.ui
180 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
180 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
181 tmpfn = os.path.join(tmpdir, 'bundle')
181 tmpfn = os.path.join(tmpdir, 'bundle')
182 try:
182 try:
183 commands.bundle(ui, repo, tmpfn, dest, **opts)
183 commands.bundle(ui, repo, tmpfn, dest, **opts)
184 fp = open(tmpfn, 'rb')
184 fp = open(tmpfn, 'rb')
185 data = fp.read()
185 data = fp.read()
186 fp.close()
186 fp.close()
187 return data
187 return data
188 finally:
188 finally:
189 try:
189 try:
190 os.unlink(tmpfn)
190 os.unlink(tmpfn)
191 except OSError:
191 except OSError:
192 pass
192 pass
193 os.rmdir(tmpdir)
193 os.rmdir(tmpdir)
194
194
195 def _getdescription(repo, defaultbody, sender, **opts):
195 def _getdescription(repo, defaultbody, sender, **opts):
196 """obtain the body of the introduction message and return it
196 """obtain the body of the introduction message and return it
197
197
198 This is also used for the body of email with an attached bundle.
198 This is also used for the body of email with an attached bundle.
199
199
200 The body can be obtained either from the command line option or entered by
200 The body can be obtained either from the command line option or entered by
201 the user through the editor.
201 the user through the editor.
202 """
202 """
203 ui = repo.ui
203 ui = repo.ui
204 if opts.get('desc'):
204 if opts.get('desc'):
205 body = open(opts.get('desc')).read()
205 body = open(opts.get('desc')).read()
206 else:
206 else:
207 ui.write(_('\nWrite the introductory message for the '
207 ui.write(_('\nWrite the introductory message for the '
208 'patch series.\n\n'))
208 'patch series.\n\n'))
209 body = ui.edit(defaultbody, sender)
209 body = ui.edit(defaultbody, sender)
210 # Save series description in case sendmail fails
210 # Save series description in case sendmail fails
211 msgfile = repo.opener('last-email.txt', 'wb')
211 msgfile = repo.opener('last-email.txt', 'wb')
212 msgfile.write(body)
212 msgfile.write(body)
213 msgfile.close()
213 msgfile.close()
214 return body
214 return body
215
215
216 def _getbundlemsgs(repo, sender, bundle, **opts):
216 def _getbundlemsgs(repo, sender, bundle, **opts):
217 """Get the full email for sending a given bundle
217 """Get the full email for sending a given bundle
218
218
219 This function returns a list of "email" tuples (subject, content, None).
219 This function returns a list of "email" tuples (subject, content, None).
220 The list is always one message long in that case.
220 The list is always one message long in that case.
221 """
221 """
222 ui = repo.ui
222 ui = repo.ui
223 _charsets = mail._charsets(ui)
223 _charsets = mail._charsets(ui)
224 subj = (opts.get('subject')
224 subj = (opts.get('subject')
225 or prompt(ui, 'Subject:', 'A bundle for your repository'))
225 or prompt(ui, 'Subject:', 'A bundle for your repository'))
226
226
227 body = _getdescription(repo, '', sender, **opts)
227 body = _getdescription(repo, '', sender, **opts)
228 msg = email.MIMEMultipart.MIMEMultipart()
228 msg = email.MIMEMultipart.MIMEMultipart()
229 if body:
229 if body:
230 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
230 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
231 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
231 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
232 datapart.set_payload(bundle)
232 datapart.set_payload(bundle)
233 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
233 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
234 datapart.add_header('Content-Disposition', 'attachment',
234 datapart.add_header('Content-Disposition', 'attachment',
235 filename=bundlename)
235 filename=bundlename)
236 email.Encoders.encode_base64(datapart)
236 email.Encoders.encode_base64(datapart)
237 msg.attach(datapart)
237 msg.attach(datapart)
238 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
238 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
239 return [(msg, subj, None)]
239 return [(msg, subj, None)]
240
240
241 def _makeintro(repo, sender, patches, **opts):
241 def _makeintro(repo, sender, patches, **opts):
242 """make an introduction email, asking the user for content if needed
242 """make an introduction email, asking the user for content if needed
243
243
244 email is returned as (subject, body, cumulative-diffstat)"""
244 email is returned as (subject, body, cumulative-diffstat)"""
245 ui = repo.ui
245 ui = repo.ui
246 _charsets = mail._charsets(ui)
246 _charsets = mail._charsets(ui)
247 tlen = len(str(len(patches)))
247 tlen = len(str(len(patches)))
248
248
249 flag = opts.get('flag') or ''
249 flag = opts.get('flag') or ''
250 if flag:
250 if flag:
251 flag = ' ' + ' '.join(flag)
251 flag = ' ' + ' '.join(flag)
252 prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag)
252 prefix = '[PATCH %0*d of %d%s]' % (tlen, 0, len(patches), flag)
253
253
254 subj = (opts.get('subject') or
254 subj = (opts.get('subject') or
255 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
255 prompt(ui, '(optional) Subject: ', rest=prefix, default=''))
256 if not subj:
256 if not subj:
257 return None # skip intro if the user doesn't bother
257 return None # skip intro if the user doesn't bother
258
258
259 subj = prefix + ' ' + subj
259 subj = prefix + ' ' + subj
260
260
261 body = ''
261 body = ''
262 if opts.get('diffstat'):
262 if opts.get('diffstat'):
263 # generate a cumulative diffstat of the whole patch series
263 # generate a cumulative diffstat of the whole patch series
264 diffstat = patch.diffstat(sum(patches, []))
264 diffstat = patch.diffstat(sum(patches, []))
265 body = '\n' + diffstat
265 body = '\n' + diffstat
266 else:
266 else:
267 diffstat = None
267 diffstat = None
268
268
269 body = _getdescription(repo, body, sender, **opts)
269 body = _getdescription(repo, body, sender, **opts)
270 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
270 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
271 msg['Subject'] = mail.headencode(ui, subj, _charsets,
271 msg['Subject'] = mail.headencode(ui, subj, _charsets,
272 opts.get('test'))
272 opts.get('test'))
273 return (msg, subj, diffstat)
273 return (msg, subj, diffstat)
274
274
275 def _getpatchmsgs(repo, sender, patches, patchnames=None, **opts):
276 """return a list of emails from a list of patches
277
278 This involves introduction message creation if necessary.
279
280 This function returns a list of "email" tuples (subject, content, None).
281 """
282 ui = repo.ui
283 _charsets = mail._charsets(ui)
284 msgs = []
285
286 ui.write(_('this patch series consists of %d patches.\n\n')
287 % len(patches))
288
289 # build the intro message, or skip it if the user declines
290 if introwanted(opts, len(patches)):
291 msg = _makeintro(repo, sender, patches, **opts)
292 if msg:
293 msgs.append(msg)
294
295 # are we going to send more than one message?
296 numbered = len(msgs) + len(patches) > 1
297
298 # now generate the actual patch messages
299 name = None
300 for i, p in enumerate(patches):
301 if patchnames:
302 name = patchnames[i]
303 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
304 len(patches), numbered, name)
305 msgs.append(msg)
306
307 return msgs
308
275 emailopts = [
309 emailopts = [
276 ('', 'body', None, _('send patches as inline message text (default)')),
310 ('', 'body', None, _('send patches as inline message text (default)')),
277 ('a', 'attach', None, _('send patches as attachments')),
311 ('a', 'attach', None, _('send patches as attachments')),
278 ('i', 'inline', None, _('send patches as inline attachments')),
312 ('i', 'inline', None, _('send patches as inline attachments')),
279 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
313 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
280 ('c', 'cc', [], _('email addresses of copy recipients')),
314 ('c', 'cc', [], _('email addresses of copy recipients')),
281 ('', 'confirm', None, _('ask for confirmation before sending')),
315 ('', 'confirm', None, _('ask for confirmation before sending')),
282 ('d', 'diffstat', None, _('add diffstat output to messages')),
316 ('d', 'diffstat', None, _('add diffstat output to messages')),
283 ('', 'date', '', _('use the given date as the sending date')),
317 ('', 'date', '', _('use the given date as the sending date')),
284 ('', 'desc', '', _('use the given file as the series description')),
318 ('', 'desc', '', _('use the given file as the series description')),
285 ('f', 'from', '', _('email address of sender')),
319 ('f', 'from', '', _('email address of sender')),
286 ('n', 'test', None, _('print messages that would be sent')),
320 ('n', 'test', None, _('print messages that would be sent')),
287 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
321 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
288 ('', 'reply-to', [], _('email addresses replies should be sent to')),
322 ('', 'reply-to', [], _('email addresses replies should be sent to')),
289 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
323 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
290 ('', 'in-reply-to', '', _('message identifier to reply to')),
324 ('', 'in-reply-to', '', _('message identifier to reply to')),
291 ('', 'flag', [], _('flags to add in subject prefixes')),
325 ('', 'flag', [], _('flags to add in subject prefixes')),
292 ('t', 'to', [], _('email addresses of recipients'))]
326 ('t', 'to', [], _('email addresses of recipients'))]
293
327
294 @command('email',
328 @command('email',
295 [('g', 'git', None, _('use git extended diff format')),
329 [('g', 'git', None, _('use git extended diff format')),
296 ('', 'plain', None, _('omit hg patch header')),
330 ('', 'plain', None, _('omit hg patch header')),
297 ('o', 'outgoing', None,
331 ('o', 'outgoing', None,
298 _('send changes not found in the target repository')),
332 _('send changes not found in the target repository')),
299 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
333 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
300 ('', 'bundlename', 'bundle',
334 ('', 'bundlename', 'bundle',
301 _('name of the bundle attachment file'), _('NAME')),
335 _('name of the bundle attachment file'), _('NAME')),
302 ('r', 'rev', [], _('a revision to send'), _('REV')),
336 ('r', 'rev', [], _('a revision to send'), _('REV')),
303 ('', 'force', None, _('run even when remote repository is unrelated '
337 ('', 'force', None, _('run even when remote repository is unrelated '
304 '(with -b/--bundle)')),
338 '(with -b/--bundle)')),
305 ('', 'base', [], _('a base changeset to specify instead of a destination '
339 ('', 'base', [], _('a base changeset to specify instead of a destination '
306 '(with -b/--bundle)'), _('REV')),
340 '(with -b/--bundle)'), _('REV')),
307 ('', 'intro', None, _('send an introduction email for a single patch')),
341 ('', 'intro', None, _('send an introduction email for a single patch')),
308 ] + emailopts + commands.remoteopts,
342 ] + emailopts + commands.remoteopts,
309 _('hg email [OPTION]... [DEST]...'))
343 _('hg email [OPTION]... [DEST]...'))
310 def patchbomb(ui, repo, *revs, **opts):
344 def patchbomb(ui, repo, *revs, **opts):
311 '''send changesets by email
345 '''send changesets by email
312
346
313 By default, diffs are sent in the format generated by
347 By default, diffs are sent in the format generated by
314 :hg:`export`, one per message. The series starts with a "[PATCH 0
348 :hg:`export`, one per message. The series starts with a "[PATCH 0
315 of N]" introduction, which describes the series as a whole.
349 of N]" introduction, which describes the series as a whole.
316
350
317 Each patch email has a Subject line of "[PATCH M of N] ...", using
351 Each patch email has a Subject line of "[PATCH M of N] ...", using
318 the first line of the changeset description as the subject text.
352 the first line of the changeset description as the subject text.
319 The message contains two or three parts. First, the changeset
353 The message contains two or three parts. First, the changeset
320 description.
354 description.
321
355
322 With the -d/--diffstat option, if the diffstat program is
356 With the -d/--diffstat option, if the diffstat program is
323 installed, the result of running diffstat on the patch is inserted.
357 installed, the result of running diffstat on the patch is inserted.
324
358
325 Finally, the patch itself, as generated by :hg:`export`.
359 Finally, the patch itself, as generated by :hg:`export`.
326
360
327 With the -d/--diffstat or --confirm options, you will be presented
361 With the -d/--diffstat or --confirm options, you will be presented
328 with a final summary of all messages and asked for confirmation before
362 with a final summary of all messages and asked for confirmation before
329 the messages are sent.
363 the messages are sent.
330
364
331 By default the patch is included as text in the email body for
365 By default the patch is included as text in the email body for
332 easy reviewing. Using the -a/--attach option will instead create
366 easy reviewing. Using the -a/--attach option will instead create
333 an attachment for the patch. With -i/--inline an inline attachment
367 an attachment for the patch. With -i/--inline an inline attachment
334 will be created. You can include a patch both as text in the email
368 will be created. You can include a patch both as text in the email
335 body and as a regular or an inline attachment by combining the
369 body and as a regular or an inline attachment by combining the
336 -a/--attach or -i/--inline with the --body option.
370 -a/--attach or -i/--inline with the --body option.
337
371
338 With -o/--outgoing, emails will be generated for patches not found
372 With -o/--outgoing, emails will be generated for patches not found
339 in the destination repository (or only those which are ancestors
373 in the destination repository (or only those which are ancestors
340 of the specified revisions if any are provided)
374 of the specified revisions if any are provided)
341
375
342 With -b/--bundle, changesets are selected as for --outgoing, but a
376 With -b/--bundle, changesets are selected as for --outgoing, but a
343 single email containing a binary Mercurial bundle as an attachment
377 single email containing a binary Mercurial bundle as an attachment
344 will be sent.
378 will be sent.
345
379
346 With -m/--mbox, instead of previewing each patchbomb message in a
380 With -m/--mbox, instead of previewing each patchbomb message in a
347 pager or sending the messages directly, it will create a UNIX
381 pager or sending the messages directly, it will create a UNIX
348 mailbox file with the patch emails. This mailbox file can be
382 mailbox file with the patch emails. This mailbox file can be
349 previewed with any mail user agent which supports UNIX mbox
383 previewed with any mail user agent which supports UNIX mbox
350 files.
384 files.
351
385
352 With -n/--test, all steps will run, but mail will not be sent.
386 With -n/--test, all steps will run, but mail will not be sent.
353 You will be prompted for an email recipient address, a subject and
387 You will be prompted for an email recipient address, a subject and
354 an introductory message describing the patches of your patchbomb.
388 an introductory message describing the patches of your patchbomb.
355 Then when all is done, patchbomb messages are displayed. If the
389 Then when all is done, patchbomb messages are displayed. If the
356 PAGER environment variable is set, your pager will be fired up once
390 PAGER environment variable is set, your pager will be fired up once
357 for each patchbomb message, so you can verify everything is alright.
391 for each patchbomb message, so you can verify everything is alright.
358
392
359 In case email sending fails, you will find a backup of your series
393 In case email sending fails, you will find a backup of your series
360 introductory message in ``.hg/last-email.txt``.
394 introductory message in ``.hg/last-email.txt``.
361
395
362 Examples::
396 Examples::
363
397
364 hg email -r 3000 # send patch 3000 only
398 hg email -r 3000 # send patch 3000 only
365 hg email -r 3000 -r 3001 # send patches 3000 and 3001
399 hg email -r 3000 -r 3001 # send patches 3000 and 3001
366 hg email -r 3000:3005 # send patches 3000 through 3005
400 hg email -r 3000:3005 # send patches 3000 through 3005
367 hg email 3000 # send patch 3000 (deprecated)
401 hg email 3000 # send patch 3000 (deprecated)
368
402
369 hg email -o # send all patches not in default
403 hg email -o # send all patches not in default
370 hg email -o DEST # send all patches not in DEST
404 hg email -o DEST # send all patches not in DEST
371 hg email -o -r 3000 # send all ancestors of 3000 not in default
405 hg email -o -r 3000 # send all ancestors of 3000 not in default
372 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
406 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
373
407
374 hg email -b # send bundle of all patches not in default
408 hg email -b # send bundle of all patches not in default
375 hg email -b DEST # send bundle of all patches not in DEST
409 hg email -b DEST # send bundle of all patches not in DEST
376 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
410 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
377 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
411 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
378
412
379 hg email -o -m mbox && # generate an mbox file...
413 hg email -o -m mbox && # generate an mbox file...
380 mutt -R -f mbox # ... and view it with mutt
414 mutt -R -f mbox # ... and view it with mutt
381 hg email -o -m mbox && # generate an mbox file ...
415 hg email -o -m mbox && # generate an mbox file ...
382 formail -s sendmail \\ # ... and use formail to send from the mbox
416 formail -s sendmail \\ # ... and use formail to send from the mbox
383 -bm -t < mbox # ... using sendmail
417 -bm -t < mbox # ... using sendmail
384
418
385 Before using this command, you will need to enable email in your
419 Before using this command, you will need to enable email in your
386 hgrc. See the [email] section in hgrc(5) for details.
420 hgrc. See the [email] section in hgrc(5) for details.
387 '''
421 '''
388
422
389 _charsets = mail._charsets(ui)
423 _charsets = mail._charsets(ui)
390
424
391 bundle = opts.get('bundle')
425 bundle = opts.get('bundle')
392 date = opts.get('date')
426 date = opts.get('date')
393 mbox = opts.get('mbox')
427 mbox = opts.get('mbox')
394 outgoing = opts.get('outgoing')
428 outgoing = opts.get('outgoing')
395 rev = opts.get('rev')
429 rev = opts.get('rev')
396 # internal option used by pbranches
430 # internal option used by pbranches
397 patches = opts.get('patches')
431 patches = opts.get('patches')
398
432
399 def getoutgoing(dest, revs):
433 def getoutgoing(dest, revs):
400 '''Return the revisions present locally but not in dest'''
434 '''Return the revisions present locally but not in dest'''
401 url = ui.expandpath(dest or 'default-push', dest or 'default')
435 url = ui.expandpath(dest or 'default-push', dest or 'default')
402 url = hg.parseurl(url)[0]
436 url = hg.parseurl(url)[0]
403 ui.status(_('comparing with %s\n') % util.hidepassword(url))
437 ui.status(_('comparing with %s\n') % util.hidepassword(url))
404
438
405 revs = [r for r in scmutil.revrange(repo, revs) if r >= 0]
439 revs = [r for r in scmutil.revrange(repo, revs) if r >= 0]
406 if not revs:
440 if not revs:
407 revs = [len(repo) - 1]
441 revs = [len(repo) - 1]
408 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
442 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
409 if not revs:
443 if not revs:
410 ui.status(_("no changes found\n"))
444 ui.status(_("no changes found\n"))
411 return []
445 return []
412 return [str(r) for r in revs]
446 return [str(r) for r in revs]
413
447
414 if not (opts.get('test') or mbox):
448 if not (opts.get('test') or mbox):
415 # really sending
449 # really sending
416 mail.validateconfig(ui)
450 mail.validateconfig(ui)
417
451
418 if not (revs or rev or outgoing or bundle or patches):
452 if not (revs or rev or outgoing or bundle or patches):
419 raise util.Abort(_('specify at least one changeset with -r or -o'))
453 raise util.Abort(_('specify at least one changeset with -r or -o'))
420
454
421 if outgoing and bundle:
455 if outgoing and bundle:
422 raise util.Abort(_("--outgoing mode always on with --bundle;"
456 raise util.Abort(_("--outgoing mode always on with --bundle;"
423 " do not re-specify --outgoing"))
457 " do not re-specify --outgoing"))
424
458
425 if outgoing or bundle:
459 if outgoing or bundle:
426 if len(revs) > 1:
460 if len(revs) > 1:
427 raise util.Abort(_("too many destinations"))
461 raise util.Abort(_("too many destinations"))
428 dest = revs and revs[0] or None
462 dest = revs and revs[0] or None
429 revs = []
463 revs = []
430
464
431 if rev:
465 if rev:
432 if revs:
466 if revs:
433 raise util.Abort(_('use only one form to specify the revision'))
467 raise util.Abort(_('use only one form to specify the revision'))
434 revs = rev
468 revs = rev
435
469
436 if outgoing:
470 if outgoing:
437 revs = getoutgoing(dest, rev)
471 revs = getoutgoing(dest, rev)
438 if bundle:
472 if bundle:
439 opts['revs'] = revs
473 opts['revs'] = revs
440
474
441 # start
475 # start
442 if date:
476 if date:
443 start_time = util.parsedate(date)
477 start_time = util.parsedate(date)
444 else:
478 else:
445 start_time = util.makedate()
479 start_time = util.makedate()
446
480
447 def genmsgid(id):
481 def genmsgid(id):
448 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
482 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
449
483
450 def getpatchmsgs(patches, patchnames=None):
451 msgs = []
452
453 ui.write(_('this patch series consists of %d patches.\n\n')
454 % len(patches))
455
456 # build the intro message, or skip it if the user declines
457 if introwanted(opts, len(patches)):
458 msg = _makeintro(repo, sender, patches, **opts)
459 if msg:
460 msgs.append(msg)
461
462 # are we going to send more than one message?
463 numbered = len(msgs) + len(patches) > 1
464
465 # now generate the actual patch messages
466 name = None
467 for i, p in enumerate(patches):
468 if patchnames:
469 name = patchnames[i]
470 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
471 len(patches), numbered, name)
472 msgs.append(msg)
473
474 return msgs
475
476
477 sender = (opts.get('from') or ui.config('email', 'from') or
484 sender = (opts.get('from') or ui.config('email', 'from') or
478 ui.config('patchbomb', 'from') or
485 ui.config('patchbomb', 'from') or
479 prompt(ui, 'From', ui.username()))
486 prompt(ui, 'From', ui.username()))
480
487
481 if patches:
488 if patches:
482 msgs = getpatchmsgs(patches, opts.get('patchnames'))
489 msgs = _getpatchmsgs(repo, sender, patches, opts.get('patchnames'),
490 **opts)
483 elif bundle:
491 elif bundle:
484 bundledata = _getbundle(repo, dest, **opts)
492 bundledata = _getbundle(repo, dest, **opts)
485 bundleopts = opts.copy()
493 bundleopts = opts.copy()
486 bundleopts.pop('bundle', None) # already processed
494 bundleopts.pop('bundle', None) # already processed
487 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
495 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
488 else:
496 else:
489 msgs = getpatchmsgs(list(_getpatches(repo, revs, **opts)))
497 _patches = list(_getpatches(repo, revs, **opts))
498 msgs = _getpatchmsgs(repo, sender, _patches, **opts)
490
499
491 showaddrs = []
500 showaddrs = []
492
501
493 def getaddrs(header, ask=False, default=None):
502 def getaddrs(header, ask=False, default=None):
494 configkey = header.lower()
503 configkey = header.lower()
495 opt = header.replace('-', '_').lower()
504 opt = header.replace('-', '_').lower()
496 addrs = opts.get(opt)
505 addrs = opts.get(opt)
497 if addrs:
506 if addrs:
498 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
507 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
499 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
508 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
500
509
501 # not on the command line: fallback to config and then maybe ask
510 # not on the command line: fallback to config and then maybe ask
502 addr = (ui.config('email', configkey) or
511 addr = (ui.config('email', configkey) or
503 ui.config('patchbomb', configkey) or
512 ui.config('patchbomb', configkey) or
504 '')
513 '')
505 if not addr and ask:
514 if not addr and ask:
506 addr = prompt(ui, header, default=default)
515 addr = prompt(ui, header, default=default)
507 if addr:
516 if addr:
508 showaddrs.append('%s: %s' % (header, addr))
517 showaddrs.append('%s: %s' % (header, addr))
509 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
518 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
510 else:
519 else:
511 return default
520 return default
512
521
513 to = getaddrs('To', ask=True)
522 to = getaddrs('To', ask=True)
514 if not to:
523 if not to:
515 # we can get here in non-interactive mode
524 # we can get here in non-interactive mode
516 raise util.Abort(_('no recipient addresses provided'))
525 raise util.Abort(_('no recipient addresses provided'))
517 cc = getaddrs('Cc', ask=True, default='') or []
526 cc = getaddrs('Cc', ask=True, default='') or []
518 bcc = getaddrs('Bcc') or []
527 bcc = getaddrs('Bcc') or []
519 replyto = getaddrs('Reply-To')
528 replyto = getaddrs('Reply-To')
520
529
521 if opts.get('diffstat') or opts.get('confirm'):
530 if opts.get('diffstat') or opts.get('confirm'):
522 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
531 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
523 ui.write(('From: %s\n' % sender), label='patchbomb.from')
532 ui.write(('From: %s\n' % sender), label='patchbomb.from')
524 for addr in showaddrs:
533 for addr in showaddrs:
525 ui.write('%s\n' % addr, label='patchbomb.to')
534 ui.write('%s\n' % addr, label='patchbomb.to')
526 for m, subj, ds in msgs:
535 for m, subj, ds in msgs:
527 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
536 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
528 if ds:
537 if ds:
529 ui.write(ds, label='patchbomb.diffstats')
538 ui.write(ds, label='patchbomb.diffstats')
530 ui.write('\n')
539 ui.write('\n')
531 if ui.promptchoice(_('are you sure you want to send (yn)?'
540 if ui.promptchoice(_('are you sure you want to send (yn)?'
532 '$$ &Yes $$ &No')):
541 '$$ &Yes $$ &No')):
533 raise util.Abort(_('patchbomb canceled'))
542 raise util.Abort(_('patchbomb canceled'))
534
543
535 ui.write('\n')
544 ui.write('\n')
536
545
537 parent = opts.get('in_reply_to') or None
546 parent = opts.get('in_reply_to') or None
538 # angle brackets may be omitted, they're not semantically part of the msg-id
547 # angle brackets may be omitted, they're not semantically part of the msg-id
539 if parent is not None:
548 if parent is not None:
540 if not parent.startswith('<'):
549 if not parent.startswith('<'):
541 parent = '<' + parent
550 parent = '<' + parent
542 if not parent.endswith('>'):
551 if not parent.endswith('>'):
543 parent += '>'
552 parent += '>'
544
553
545 sender_addr = email.Utils.parseaddr(sender)[1]
554 sender_addr = email.Utils.parseaddr(sender)[1]
546 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
555 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
547 sendmail = None
556 sendmail = None
548 firstpatch = None
557 firstpatch = None
549 for i, (m, subj, ds) in enumerate(msgs):
558 for i, (m, subj, ds) in enumerate(msgs):
550 try:
559 try:
551 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
560 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
552 if not firstpatch:
561 if not firstpatch:
553 firstpatch = m['Message-Id']
562 firstpatch = m['Message-Id']
554 m['X-Mercurial-Series-Id'] = firstpatch
563 m['X-Mercurial-Series-Id'] = firstpatch
555 except TypeError:
564 except TypeError:
556 m['Message-Id'] = genmsgid('patchbomb')
565 m['Message-Id'] = genmsgid('patchbomb')
557 if parent:
566 if parent:
558 m['In-Reply-To'] = parent
567 m['In-Reply-To'] = parent
559 m['References'] = parent
568 m['References'] = parent
560 if not parent or 'X-Mercurial-Node' not in m:
569 if not parent or 'X-Mercurial-Node' not in m:
561 parent = m['Message-Id']
570 parent = m['Message-Id']
562
571
563 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
572 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
564 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
573 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
565
574
566 start_time = (start_time[0] + 1, start_time[1])
575 start_time = (start_time[0] + 1, start_time[1])
567 m['From'] = sender
576 m['From'] = sender
568 m['To'] = ', '.join(to)
577 m['To'] = ', '.join(to)
569 if cc:
578 if cc:
570 m['Cc'] = ', '.join(cc)
579 m['Cc'] = ', '.join(cc)
571 if bcc:
580 if bcc:
572 m['Bcc'] = ', '.join(bcc)
581 m['Bcc'] = ', '.join(bcc)
573 if replyto:
582 if replyto:
574 m['Reply-To'] = ', '.join(replyto)
583 m['Reply-To'] = ', '.join(replyto)
575 if opts.get('test'):
584 if opts.get('test'):
576 ui.status(_('displaying '), subj, ' ...\n')
585 ui.status(_('displaying '), subj, ' ...\n')
577 ui.flush()
586 ui.flush()
578 if 'PAGER' in os.environ and not ui.plain():
587 if 'PAGER' in os.environ and not ui.plain():
579 fp = util.popen(os.environ['PAGER'], 'w')
588 fp = util.popen(os.environ['PAGER'], 'w')
580 else:
589 else:
581 fp = ui
590 fp = ui
582 generator = email.Generator.Generator(fp, mangle_from_=False)
591 generator = email.Generator.Generator(fp, mangle_from_=False)
583 try:
592 try:
584 generator.flatten(m, 0)
593 generator.flatten(m, 0)
585 fp.write('\n')
594 fp.write('\n')
586 except IOError, inst:
595 except IOError, inst:
587 if inst.errno != errno.EPIPE:
596 if inst.errno != errno.EPIPE:
588 raise
597 raise
589 if fp is not ui:
598 if fp is not ui:
590 fp.close()
599 fp.close()
591 else:
600 else:
592 if not sendmail:
601 if not sendmail:
593 verifycert = ui.config('smtp', 'verifycert')
602 verifycert = ui.config('smtp', 'verifycert')
594 if opts.get('insecure'):
603 if opts.get('insecure'):
595 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
604 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
596 try:
605 try:
597 sendmail = mail.connect(ui, mbox=mbox)
606 sendmail = mail.connect(ui, mbox=mbox)
598 finally:
607 finally:
599 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
608 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
600 ui.status(_('sending '), subj, ' ...\n')
609 ui.status(_('sending '), subj, ' ...\n')
601 ui.progress(_('sending'), i, item=subj, total=len(msgs))
610 ui.progress(_('sending'), i, item=subj, total=len(msgs))
602 if not mbox:
611 if not mbox:
603 # Exim does not remove the Bcc field
612 # Exim does not remove the Bcc field
604 del m['Bcc']
613 del m['Bcc']
605 fp = cStringIO.StringIO()
614 fp = cStringIO.StringIO()
606 generator = email.Generator.Generator(fp, mangle_from_=False)
615 generator = email.Generator.Generator(fp, mangle_from_=False)
607 generator.flatten(m, 0)
616 generator.flatten(m, 0)
608 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
617 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
609
618
610 ui.progress(_('writing'), None)
619 ui.progress(_('writing'), None)
611 ui.progress(_('sending'), None)
620 ui.progress(_('sending'), None)
General Comments 0
You need to be logged in to leave comments. Login now