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