##// END OF EJS Templates
patchbomb: extract 'getoutgoing' closure into its own function...
Pierre-Yves David -
r23486:1de21483 default
parent child Browse files
Show More
@@ -1,620 +1,621
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.difffeatureopts(ui, opts, git=True))
169 opts=patch.difffeatureopts(ui, opts, git=True))
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):
275 def _getpatchmsgs(repo, sender, patches, patchnames=None, **opts):
276 """return a list of emails from a list of patches
276 """return a list of emails from a list of patches
277
277
278 This involves introduction message creation if necessary.
278 This involves introduction message creation if necessary.
279
279
280 This function returns a list of "email" tuples (subject, content, None).
280 This function returns a list of "email" tuples (subject, content, None).
281 """
281 """
282 ui = repo.ui
282 ui = repo.ui
283 _charsets = mail._charsets(ui)
283 _charsets = mail._charsets(ui)
284 msgs = []
284 msgs = []
285
285
286 ui.write(_('this patch series consists of %d patches.\n\n')
286 ui.write(_('this patch series consists of %d patches.\n\n')
287 % len(patches))
287 % len(patches))
288
288
289 # build the intro message, or skip it if the user declines
289 # build the intro message, or skip it if the user declines
290 if introwanted(opts, len(patches)):
290 if introwanted(opts, len(patches)):
291 msg = _makeintro(repo, sender, patches, **opts)
291 msg = _makeintro(repo, sender, patches, **opts)
292 if msg:
292 if msg:
293 msgs.append(msg)
293 msgs.append(msg)
294
294
295 # are we going to send more than one message?
295 # are we going to send more than one message?
296 numbered = len(msgs) + len(patches) > 1
296 numbered = len(msgs) + len(patches) > 1
297
297
298 # now generate the actual patch messages
298 # now generate the actual patch messages
299 name = None
299 name = None
300 for i, p in enumerate(patches):
300 for i, p in enumerate(patches):
301 if patchnames:
301 if patchnames:
302 name = patchnames[i]
302 name = patchnames[i]
303 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
303 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
304 len(patches), numbered, name)
304 len(patches), numbered, name)
305 msgs.append(msg)
305 msgs.append(msg)
306
306
307 return msgs
307 return msgs
308
308
309 def _getoutgoing(repo, dest, revs):
310 '''Return the revisions present locally but not in dest'''
311 ui = repo.ui
312 url = ui.expandpath(dest or 'default-push', dest or 'default')
313 url = hg.parseurl(url)[0]
314 ui.status(_('comparing with %s\n') % util.hidepassword(url))
315
316 revs = [r for r in scmutil.revrange(repo, revs) if r >= 0]
317 if not revs:
318 revs = [len(repo) - 1]
319 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
320 if not revs:
321 ui.status(_("no changes found\n"))
322 return []
323 return [str(r) for r in revs]
324
309 emailopts = [
325 emailopts = [
310 ('', 'body', None, _('send patches as inline message text (default)')),
326 ('', 'body', None, _('send patches as inline message text (default)')),
311 ('a', 'attach', None, _('send patches as attachments')),
327 ('a', 'attach', None, _('send patches as attachments')),
312 ('i', 'inline', None, _('send patches as inline attachments')),
328 ('i', 'inline', None, _('send patches as inline attachments')),
313 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
329 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
314 ('c', 'cc', [], _('email addresses of copy recipients')),
330 ('c', 'cc', [], _('email addresses of copy recipients')),
315 ('', 'confirm', None, _('ask for confirmation before sending')),
331 ('', 'confirm', None, _('ask for confirmation before sending')),
316 ('d', 'diffstat', None, _('add diffstat output to messages')),
332 ('d', 'diffstat', None, _('add diffstat output to messages')),
317 ('', 'date', '', _('use the given date as the sending date')),
333 ('', 'date', '', _('use the given date as the sending date')),
318 ('', 'desc', '', _('use the given file as the series description')),
334 ('', 'desc', '', _('use the given file as the series description')),
319 ('f', 'from', '', _('email address of sender')),
335 ('f', 'from', '', _('email address of sender')),
320 ('n', 'test', None, _('print messages that would be sent')),
336 ('n', 'test', None, _('print messages that would be sent')),
321 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
337 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
322 ('', 'reply-to', [], _('email addresses replies should be sent to')),
338 ('', 'reply-to', [], _('email addresses replies should be sent to')),
323 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
339 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
324 ('', 'in-reply-to', '', _('message identifier to reply to')),
340 ('', 'in-reply-to', '', _('message identifier to reply to')),
325 ('', 'flag', [], _('flags to add in subject prefixes')),
341 ('', 'flag', [], _('flags to add in subject prefixes')),
326 ('t', 'to', [], _('email addresses of recipients'))]
342 ('t', 'to', [], _('email addresses of recipients'))]
327
343
328 @command('email',
344 @command('email',
329 [('g', 'git', None, _('use git extended diff format')),
345 [('g', 'git', None, _('use git extended diff format')),
330 ('', 'plain', None, _('omit hg patch header')),
346 ('', 'plain', None, _('omit hg patch header')),
331 ('o', 'outgoing', None,
347 ('o', 'outgoing', None,
332 _('send changes not found in the target repository')),
348 _('send changes not found in the target repository')),
333 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
349 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
334 ('', 'bundlename', 'bundle',
350 ('', 'bundlename', 'bundle',
335 _('name of the bundle attachment file'), _('NAME')),
351 _('name of the bundle attachment file'), _('NAME')),
336 ('r', 'rev', [], _('a revision to send'), _('REV')),
352 ('r', 'rev', [], _('a revision to send'), _('REV')),
337 ('', 'force', None, _('run even when remote repository is unrelated '
353 ('', 'force', None, _('run even when remote repository is unrelated '
338 '(with -b/--bundle)')),
354 '(with -b/--bundle)')),
339 ('', 'base', [], _('a base changeset to specify instead of a destination '
355 ('', 'base', [], _('a base changeset to specify instead of a destination '
340 '(with -b/--bundle)'), _('REV')),
356 '(with -b/--bundle)'), _('REV')),
341 ('', 'intro', None, _('send an introduction email for a single patch')),
357 ('', 'intro', None, _('send an introduction email for a single patch')),
342 ] + emailopts + commands.remoteopts,
358 ] + emailopts + commands.remoteopts,
343 _('hg email [OPTION]... [DEST]...'))
359 _('hg email [OPTION]... [DEST]...'))
344 def patchbomb(ui, repo, *revs, **opts):
360 def patchbomb(ui, repo, *revs, **opts):
345 '''send changesets by email
361 '''send changesets by email
346
362
347 By default, diffs are sent in the format generated by
363 By default, diffs are sent in the format generated by
348 :hg:`export`, one per message. The series starts with a "[PATCH 0
364 :hg:`export`, one per message. The series starts with a "[PATCH 0
349 of N]" introduction, which describes the series as a whole.
365 of N]" introduction, which describes the series as a whole.
350
366
351 Each patch email has a Subject line of "[PATCH M of N] ...", using
367 Each patch email has a Subject line of "[PATCH M of N] ...", using
352 the first line of the changeset description as the subject text.
368 the first line of the changeset description as the subject text.
353 The message contains two or three parts. First, the changeset
369 The message contains two or three parts. First, the changeset
354 description.
370 description.
355
371
356 With the -d/--diffstat option, if the diffstat program is
372 With the -d/--diffstat option, if the diffstat program is
357 installed, the result of running diffstat on the patch is inserted.
373 installed, the result of running diffstat on the patch is inserted.
358
374
359 Finally, the patch itself, as generated by :hg:`export`.
375 Finally, the patch itself, as generated by :hg:`export`.
360
376
361 With the -d/--diffstat or --confirm options, you will be presented
377 With the -d/--diffstat or --confirm options, you will be presented
362 with a final summary of all messages and asked for confirmation before
378 with a final summary of all messages and asked for confirmation before
363 the messages are sent.
379 the messages are sent.
364
380
365 By default the patch is included as text in the email body for
381 By default the patch is included as text in the email body for
366 easy reviewing. Using the -a/--attach option will instead create
382 easy reviewing. Using the -a/--attach option will instead create
367 an attachment for the patch. With -i/--inline an inline attachment
383 an attachment for the patch. With -i/--inline an inline attachment
368 will be created. You can include a patch both as text in the email
384 will be created. You can include a patch both as text in the email
369 body and as a regular or an inline attachment by combining the
385 body and as a regular or an inline attachment by combining the
370 -a/--attach or -i/--inline with the --body option.
386 -a/--attach or -i/--inline with the --body option.
371
387
372 With -o/--outgoing, emails will be generated for patches not found
388 With -o/--outgoing, emails will be generated for patches not found
373 in the destination repository (or only those which are ancestors
389 in the destination repository (or only those which are ancestors
374 of the specified revisions if any are provided)
390 of the specified revisions if any are provided)
375
391
376 With -b/--bundle, changesets are selected as for --outgoing, but a
392 With -b/--bundle, changesets are selected as for --outgoing, but a
377 single email containing a binary Mercurial bundle as an attachment
393 single email containing a binary Mercurial bundle as an attachment
378 will be sent.
394 will be sent.
379
395
380 With -m/--mbox, instead of previewing each patchbomb message in a
396 With -m/--mbox, instead of previewing each patchbomb message in a
381 pager or sending the messages directly, it will create a UNIX
397 pager or sending the messages directly, it will create a UNIX
382 mailbox file with the patch emails. This mailbox file can be
398 mailbox file with the patch emails. This mailbox file can be
383 previewed with any mail user agent which supports UNIX mbox
399 previewed with any mail user agent which supports UNIX mbox
384 files.
400 files.
385
401
386 With -n/--test, all steps will run, but mail will not be sent.
402 With -n/--test, all steps will run, but mail will not be sent.
387 You will be prompted for an email recipient address, a subject and
403 You will be prompted for an email recipient address, a subject and
388 an introductory message describing the patches of your patchbomb.
404 an introductory message describing the patches of your patchbomb.
389 Then when all is done, patchbomb messages are displayed. If the
405 Then when all is done, patchbomb messages are displayed. If the
390 PAGER environment variable is set, your pager will be fired up once
406 PAGER environment variable is set, your pager will be fired up once
391 for each patchbomb message, so you can verify everything is alright.
407 for each patchbomb message, so you can verify everything is alright.
392
408
393 In case email sending fails, you will find a backup of your series
409 In case email sending fails, you will find a backup of your series
394 introductory message in ``.hg/last-email.txt``.
410 introductory message in ``.hg/last-email.txt``.
395
411
396 Examples::
412 Examples::
397
413
398 hg email -r 3000 # send patch 3000 only
414 hg email -r 3000 # send patch 3000 only
399 hg email -r 3000 -r 3001 # send patches 3000 and 3001
415 hg email -r 3000 -r 3001 # send patches 3000 and 3001
400 hg email -r 3000:3005 # send patches 3000 through 3005
416 hg email -r 3000:3005 # send patches 3000 through 3005
401 hg email 3000 # send patch 3000 (deprecated)
417 hg email 3000 # send patch 3000 (deprecated)
402
418
403 hg email -o # send all patches not in default
419 hg email -o # send all patches not in default
404 hg email -o DEST # send all patches not in DEST
420 hg email -o DEST # send all patches not in DEST
405 hg email -o -r 3000 # send all ancestors of 3000 not in default
421 hg email -o -r 3000 # send all ancestors of 3000 not in default
406 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
422 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
407
423
408 hg email -b # send bundle of all patches not in default
424 hg email -b # send bundle of all patches not in default
409 hg email -b DEST # send bundle of all patches not in DEST
425 hg email -b DEST # send bundle of all patches not in DEST
410 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
426 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
411 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
427 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
412
428
413 hg email -o -m mbox && # generate an mbox file...
429 hg email -o -m mbox && # generate an mbox file...
414 mutt -R -f mbox # ... and view it with mutt
430 mutt -R -f mbox # ... and view it with mutt
415 hg email -o -m mbox && # generate an mbox file ...
431 hg email -o -m mbox && # generate an mbox file ...
416 formail -s sendmail \\ # ... and use formail to send from the mbox
432 formail -s sendmail \\ # ... and use formail to send from the mbox
417 -bm -t < mbox # ... using sendmail
433 -bm -t < mbox # ... using sendmail
418
434
419 Before using this command, you will need to enable email in your
435 Before using this command, you will need to enable email in your
420 hgrc. See the [email] section in hgrc(5) for details.
436 hgrc. See the [email] section in hgrc(5) for details.
421 '''
437 '''
422
438
423 _charsets = mail._charsets(ui)
439 _charsets = mail._charsets(ui)
424
440
425 bundle = opts.get('bundle')
441 bundle = opts.get('bundle')
426 date = opts.get('date')
442 date = opts.get('date')
427 mbox = opts.get('mbox')
443 mbox = opts.get('mbox')
428 outgoing = opts.get('outgoing')
444 outgoing = opts.get('outgoing')
429 rev = opts.get('rev')
445 rev = opts.get('rev')
430 # internal option used by pbranches
446 # internal option used by pbranches
431 patches = opts.get('patches')
447 patches = opts.get('patches')
432
448
433 def getoutgoing(dest, revs):
434 '''Return the revisions present locally but not in dest'''
435 url = ui.expandpath(dest or 'default-push', dest or 'default')
436 url = hg.parseurl(url)[0]
437 ui.status(_('comparing with %s\n') % util.hidepassword(url))
438
439 revs = [r for r in scmutil.revrange(repo, revs) if r >= 0]
440 if not revs:
441 revs = [len(repo) - 1]
442 revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs)
443 if not revs:
444 ui.status(_("no changes found\n"))
445 return []
446 return [str(r) for r in revs]
447
448 if not (opts.get('test') or mbox):
449 if not (opts.get('test') or mbox):
449 # really sending
450 # really sending
450 mail.validateconfig(ui)
451 mail.validateconfig(ui)
451
452
452 if not (revs or rev or outgoing or bundle or patches):
453 if not (revs or rev or outgoing or bundle or patches):
453 raise util.Abort(_('specify at least one changeset with -r or -o'))
454 raise util.Abort(_('specify at least one changeset with -r or -o'))
454
455
455 if outgoing and bundle:
456 if outgoing and bundle:
456 raise util.Abort(_("--outgoing mode always on with --bundle;"
457 raise util.Abort(_("--outgoing mode always on with --bundle;"
457 " do not re-specify --outgoing"))
458 " do not re-specify --outgoing"))
458
459
459 if outgoing or bundle:
460 if outgoing or bundle:
460 if len(revs) > 1:
461 if len(revs) > 1:
461 raise util.Abort(_("too many destinations"))
462 raise util.Abort(_("too many destinations"))
462 dest = revs and revs[0] or None
463 dest = revs and revs[0] or None
463 revs = []
464 revs = []
464
465
465 if rev:
466 if rev:
466 if revs:
467 if revs:
467 raise util.Abort(_('use only one form to specify the revision'))
468 raise util.Abort(_('use only one form to specify the revision'))
468 revs = rev
469 revs = rev
469
470
470 if outgoing:
471 if outgoing:
471 revs = getoutgoing(dest, rev)
472 revs = _getoutgoing(repo, dest, rev)
472 if bundle:
473 if bundle:
473 opts['revs'] = revs
474 opts['revs'] = revs
474
475
475 # start
476 # start
476 if date:
477 if date:
477 start_time = util.parsedate(date)
478 start_time = util.parsedate(date)
478 else:
479 else:
479 start_time = util.makedate()
480 start_time = util.makedate()
480
481
481 def genmsgid(id):
482 def genmsgid(id):
482 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
483 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
483
484
484 sender = (opts.get('from') or ui.config('email', 'from') or
485 sender = (opts.get('from') or ui.config('email', 'from') or
485 ui.config('patchbomb', 'from') or
486 ui.config('patchbomb', 'from') or
486 prompt(ui, 'From', ui.username()))
487 prompt(ui, 'From', ui.username()))
487
488
488 if patches:
489 if patches:
489 msgs = _getpatchmsgs(repo, sender, patches, opts.get('patchnames'),
490 msgs = _getpatchmsgs(repo, sender, patches, opts.get('patchnames'),
490 **opts)
491 **opts)
491 elif bundle:
492 elif bundle:
492 bundledata = _getbundle(repo, dest, **opts)
493 bundledata = _getbundle(repo, dest, **opts)
493 bundleopts = opts.copy()
494 bundleopts = opts.copy()
494 bundleopts.pop('bundle', None) # already processed
495 bundleopts.pop('bundle', None) # already processed
495 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
496 msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
496 else:
497 else:
497 _patches = list(_getpatches(repo, revs, **opts))
498 _patches = list(_getpatches(repo, revs, **opts))
498 msgs = _getpatchmsgs(repo, sender, _patches, **opts)
499 msgs = _getpatchmsgs(repo, sender, _patches, **opts)
499
500
500 showaddrs = []
501 showaddrs = []
501
502
502 def getaddrs(header, ask=False, default=None):
503 def getaddrs(header, ask=False, default=None):
503 configkey = header.lower()
504 configkey = header.lower()
504 opt = header.replace('-', '_').lower()
505 opt = header.replace('-', '_').lower()
505 addrs = opts.get(opt)
506 addrs = opts.get(opt)
506 if addrs:
507 if addrs:
507 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
508 showaddrs.append('%s: %s' % (header, ', '.join(addrs)))
508 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
509 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
509
510
510 # not on the command line: fallback to config and then maybe ask
511 # not on the command line: fallback to config and then maybe ask
511 addr = (ui.config('email', configkey) or
512 addr = (ui.config('email', configkey) or
512 ui.config('patchbomb', configkey) or
513 ui.config('patchbomb', configkey) or
513 '')
514 '')
514 if not addr and ask:
515 if not addr and ask:
515 addr = prompt(ui, header, default=default)
516 addr = prompt(ui, header, default=default)
516 if addr:
517 if addr:
517 showaddrs.append('%s: %s' % (header, addr))
518 showaddrs.append('%s: %s' % (header, addr))
518 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
519 return mail.addrlistencode(ui, [addr], _charsets, opts.get('test'))
519 else:
520 else:
520 return default
521 return default
521
522
522 to = getaddrs('To', ask=True)
523 to = getaddrs('To', ask=True)
523 if not to:
524 if not to:
524 # we can get here in non-interactive mode
525 # we can get here in non-interactive mode
525 raise util.Abort(_('no recipient addresses provided'))
526 raise util.Abort(_('no recipient addresses provided'))
526 cc = getaddrs('Cc', ask=True, default='') or []
527 cc = getaddrs('Cc', ask=True, default='') or []
527 bcc = getaddrs('Bcc') or []
528 bcc = getaddrs('Bcc') or []
528 replyto = getaddrs('Reply-To')
529 replyto = getaddrs('Reply-To')
529
530
530 if opts.get('diffstat') or opts.get('confirm'):
531 if opts.get('diffstat') or opts.get('confirm'):
531 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
532 ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary')
532 ui.write(('From: %s\n' % sender), label='patchbomb.from')
533 ui.write(('From: %s\n' % sender), label='patchbomb.from')
533 for addr in showaddrs:
534 for addr in showaddrs:
534 ui.write('%s\n' % addr, label='patchbomb.to')
535 ui.write('%s\n' % addr, label='patchbomb.to')
535 for m, subj, ds in msgs:
536 for m, subj, ds in msgs:
536 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
537 ui.write(('Subject: %s\n' % subj), label='patchbomb.subject')
537 if ds:
538 if ds:
538 ui.write(ds, label='patchbomb.diffstats')
539 ui.write(ds, label='patchbomb.diffstats')
539 ui.write('\n')
540 ui.write('\n')
540 if ui.promptchoice(_('are you sure you want to send (yn)?'
541 if ui.promptchoice(_('are you sure you want to send (yn)?'
541 '$$ &Yes $$ &No')):
542 '$$ &Yes $$ &No')):
542 raise util.Abort(_('patchbomb canceled'))
543 raise util.Abort(_('patchbomb canceled'))
543
544
544 ui.write('\n')
545 ui.write('\n')
545
546
546 parent = opts.get('in_reply_to') or None
547 parent = opts.get('in_reply_to') or None
547 # angle brackets may be omitted, they're not semantically part of the msg-id
548 # angle brackets may be omitted, they're not semantically part of the msg-id
548 if parent is not None:
549 if parent is not None:
549 if not parent.startswith('<'):
550 if not parent.startswith('<'):
550 parent = '<' + parent
551 parent = '<' + parent
551 if not parent.endswith('>'):
552 if not parent.endswith('>'):
552 parent += '>'
553 parent += '>'
553
554
554 sender_addr = email.Utils.parseaddr(sender)[1]
555 sender_addr = email.Utils.parseaddr(sender)[1]
555 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
556 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
556 sendmail = None
557 sendmail = None
557 firstpatch = None
558 firstpatch = None
558 for i, (m, subj, ds) in enumerate(msgs):
559 for i, (m, subj, ds) in enumerate(msgs):
559 try:
560 try:
560 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
561 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
561 if not firstpatch:
562 if not firstpatch:
562 firstpatch = m['Message-Id']
563 firstpatch = m['Message-Id']
563 m['X-Mercurial-Series-Id'] = firstpatch
564 m['X-Mercurial-Series-Id'] = firstpatch
564 except TypeError:
565 except TypeError:
565 m['Message-Id'] = genmsgid('patchbomb')
566 m['Message-Id'] = genmsgid('patchbomb')
566 if parent:
567 if parent:
567 m['In-Reply-To'] = parent
568 m['In-Reply-To'] = parent
568 m['References'] = parent
569 m['References'] = parent
569 if not parent or 'X-Mercurial-Node' not in m:
570 if not parent or 'X-Mercurial-Node' not in m:
570 parent = m['Message-Id']
571 parent = m['Message-Id']
571
572
572 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
573 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
573 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
574 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
574
575
575 start_time = (start_time[0] + 1, start_time[1])
576 start_time = (start_time[0] + 1, start_time[1])
576 m['From'] = sender
577 m['From'] = sender
577 m['To'] = ', '.join(to)
578 m['To'] = ', '.join(to)
578 if cc:
579 if cc:
579 m['Cc'] = ', '.join(cc)
580 m['Cc'] = ', '.join(cc)
580 if bcc:
581 if bcc:
581 m['Bcc'] = ', '.join(bcc)
582 m['Bcc'] = ', '.join(bcc)
582 if replyto:
583 if replyto:
583 m['Reply-To'] = ', '.join(replyto)
584 m['Reply-To'] = ', '.join(replyto)
584 if opts.get('test'):
585 if opts.get('test'):
585 ui.status(_('displaying '), subj, ' ...\n')
586 ui.status(_('displaying '), subj, ' ...\n')
586 ui.flush()
587 ui.flush()
587 if 'PAGER' in os.environ and not ui.plain():
588 if 'PAGER' in os.environ and not ui.plain():
588 fp = util.popen(os.environ['PAGER'], 'w')
589 fp = util.popen(os.environ['PAGER'], 'w')
589 else:
590 else:
590 fp = ui
591 fp = ui
591 generator = email.Generator.Generator(fp, mangle_from_=False)
592 generator = email.Generator.Generator(fp, mangle_from_=False)
592 try:
593 try:
593 generator.flatten(m, 0)
594 generator.flatten(m, 0)
594 fp.write('\n')
595 fp.write('\n')
595 except IOError, inst:
596 except IOError, inst:
596 if inst.errno != errno.EPIPE:
597 if inst.errno != errno.EPIPE:
597 raise
598 raise
598 if fp is not ui:
599 if fp is not ui:
599 fp.close()
600 fp.close()
600 else:
601 else:
601 if not sendmail:
602 if not sendmail:
602 verifycert = ui.config('smtp', 'verifycert')
603 verifycert = ui.config('smtp', 'verifycert')
603 if opts.get('insecure'):
604 if opts.get('insecure'):
604 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
605 ui.setconfig('smtp', 'verifycert', 'loose', 'patchbomb')
605 try:
606 try:
606 sendmail = mail.connect(ui, mbox=mbox)
607 sendmail = mail.connect(ui, mbox=mbox)
607 finally:
608 finally:
608 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
609 ui.setconfig('smtp', 'verifycert', verifycert, 'patchbomb')
609 ui.status(_('sending '), subj, ' ...\n')
610 ui.status(_('sending '), subj, ' ...\n')
610 ui.progress(_('sending'), i, item=subj, total=len(msgs))
611 ui.progress(_('sending'), i, item=subj, total=len(msgs))
611 if not mbox:
612 if not mbox:
612 # Exim does not remove the Bcc field
613 # Exim does not remove the Bcc field
613 del m['Bcc']
614 del m['Bcc']
614 fp = cStringIO.StringIO()
615 fp = cStringIO.StringIO()
615 generator = email.Generator.Generator(fp, mangle_from_=False)
616 generator = email.Generator.Generator(fp, mangle_from_=False)
616 generator.flatten(m, 0)
617 generator.flatten(m, 0)
617 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
618 sendmail(sender_addr, to + bcc + cc, fp.getvalue())
618
619
619 ui.progress(_('writing'), None)
620 ui.progress(_('writing'), None)
620 ui.progress(_('sending'), None)
621 ui.progress(_('sending'), None)
General Comments 0
You need to be logged in to leave comments. Login now