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