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