##// END OF EJS Templates
patchbomb: move command option help from the extension (patchbomb) to the command (email)
timeless -
r12749:338b4b61 default
parent child Browse files
Show More
@@ -1,556 +1,553 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 With the -d/--diffstat or -c/--confirm options, you will be presented
26 with a final summary of all messages and asked for confirmation before
27 the messages are sent.
28
29 To configure other defaults, add a section like this to your hgrc
25 To configure other defaults, add a section like this to your hgrc
30 file::
26 file::
31
27
32 [email]
28 [email]
33 from = My Name <my@email>
29 from = My Name <my@email>
34 to = recipient1, recipient2, ...
30 to = recipient1, recipient2, ...
35 cc = cc1, cc2, ...
31 cc = cc1, cc2, ...
36 bcc = bcc1, bcc2, ...
32 bcc = bcc1, bcc2, ...
37 reply-to = address1, address2, ...
33 reply-to = address1, address2, ...
38
34
39 Use ``[patchbomb]`` as configuration section name if you need to
35 Use ``[patchbomb]`` as configuration section name if you need to
40 override global ``[email]`` address settings.
36 override global ``[email]`` address settings.
41
37
42 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
43 changesets as a patchbomb.
39 changesets as a patchbomb.
44
40
45 To avoid sending patches prematurely, it is a good idea to first run
46 the :hg:`email` command with the "-n" option (test only). You will be
47 prompted for an email recipient address, a subject and an introductory
48 message describing the patches of your patchbomb. Then when all is
49 done, patchbomb messages are displayed. If the PAGER environment
50 variable is set, your pager will be fired up once for each patchbomb
51 message, so you can verify everything is alright.
52
53 The -m/--mbox option is also very useful. Instead of previewing each
54 patchbomb message in a pager or sending the messages directly, it will
55 create a UNIX mailbox file with the patch emails. This mailbox file
56 can be previewed with any mail user agent which supports UNIX mbox
57 files, e.g. with mutt::
58
59 % mutt -R -f mbox
60
61 When you are previewing the patchbomb messages, you can use ``formail``
62 (a utility that is commonly installed as part of the procmail
63 package), to send each message out::
64
65 % formail -s sendmail -bm -t < mbox
66
67 That should be all. Now your patchbomb is on its way out.
68
69 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
70 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
71 that the patchbomb extension can automatically send patchbombs
43 that the patchbomb extension can automatically send patchbombs
72 directly from the commandline. See the [email] and [smtp] sections in
44 directly from the commandline. See the [email] and [smtp] sections in
73 hgrc(5) for details.
45 hgrc(5) for details.
74 '''
46 '''
75
47
76 import os, errno, socket, tempfile, cStringIO, time
48 import os, errno, socket, tempfile, cStringIO, time
77 import email.MIMEMultipart, email.MIMEBase
49 import email.MIMEMultipart, email.MIMEBase
78 import email.Utils, email.Encoders, email.Generator
50 import email.Utils, email.Encoders, email.Generator
79 from mercurial import cmdutil, commands, hg, mail, patch, util, discovery
51 from mercurial import cmdutil, commands, hg, mail, patch, util, discovery
80 from mercurial.i18n import _
52 from mercurial.i18n import _
81 from mercurial.node import bin
53 from mercurial.node import bin
82
54
83 def prompt(ui, prompt, default=None, rest=':'):
55 def prompt(ui, prompt, default=None, rest=':'):
84 if not ui.interactive() and default is None:
56 if not ui.interactive() and default is None:
85 raise util.Abort(_("%s Please enter a valid value" % (prompt + rest)))
57 raise util.Abort(_("%s Please enter a valid value" % (prompt + rest)))
86 if default:
58 if default:
87 prompt += ' [%s]' % default
59 prompt += ' [%s]' % default
88 prompt += rest
60 prompt += rest
89 while True:
61 while True:
90 r = ui.prompt(prompt, default=default)
62 r = ui.prompt(prompt, default=default)
91 if r:
63 if r:
92 return r
64 return r
93 if default is not None:
65 if default is not None:
94 return default
66 return default
95 ui.warn(_('Please enter a valid value.\n'))
67 ui.warn(_('Please enter a valid value.\n'))
96
68
97 def introneeded(opts, number):
69 def introneeded(opts, number):
98 '''is an introductory message required?'''
70 '''is an introductory message required?'''
99 return number > 1 or opts.get('intro') or opts.get('desc')
71 return number > 1 or opts.get('intro') or opts.get('desc')
100
72
101 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total,
73 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total,
102 patchname=None):
74 patchname=None):
103
75
104 desc = []
76 desc = []
105 node = None
77 node = None
106 body = ''
78 body = ''
107
79
108 for line in patchlines:
80 for line in patchlines:
109 if line.startswith('#'):
81 if line.startswith('#'):
110 if line.startswith('# Node ID'):
82 if line.startswith('# Node ID'):
111 node = line.split()[-1]
83 node = line.split()[-1]
112 continue
84 continue
113 if line.startswith('diff -r') or line.startswith('diff --git'):
85 if line.startswith('diff -r') or line.startswith('diff --git'):
114 break
86 break
115 desc.append(line)
87 desc.append(line)
116
88
117 if not patchname and not node:
89 if not patchname and not node:
118 raise ValueError
90 raise ValueError
119
91
120 if opts.get('attach'):
92 if opts.get('attach'):
121 body = ('\n'.join(desc[1:]).strip() or
93 body = ('\n'.join(desc[1:]).strip() or
122 'Patch subject is complete summary.')
94 'Patch subject is complete summary.')
123 body += '\n\n\n'
95 body += '\n\n\n'
124
96
125 if opts.get('plain'):
97 if opts.get('plain'):
126 while patchlines and patchlines[0].startswith('# '):
98 while patchlines and patchlines[0].startswith('# '):
127 patchlines.pop(0)
99 patchlines.pop(0)
128 if patchlines:
100 if patchlines:
129 patchlines.pop(0)
101 patchlines.pop(0)
130 while patchlines and not patchlines[0].strip():
102 while patchlines and not patchlines[0].strip():
131 patchlines.pop(0)
103 patchlines.pop(0)
132
104
133 ds = patch.diffstat(patchlines)
105 ds = patch.diffstat(patchlines)
134 if opts.get('diffstat'):
106 if opts.get('diffstat'):
135 body += ds + '\n\n'
107 body += ds + '\n\n'
136
108
137 if opts.get('attach') or opts.get('inline'):
109 if opts.get('attach') or opts.get('inline'):
138 msg = email.MIMEMultipart.MIMEMultipart()
110 msg = email.MIMEMultipart.MIMEMultipart()
139 if body:
111 if body:
140 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
112 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
141 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', opts.get('test'))
113 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', opts.get('test'))
142 binnode = bin(node)
114 binnode = bin(node)
143 # if node is mq patch, it will have the patch file's name as a tag
115 # if node is mq patch, it will have the patch file's name as a tag
144 if not patchname:
116 if not patchname:
145 patchtags = [t for t in repo.nodetags(binnode)
117 patchtags = [t for t in repo.nodetags(binnode)
146 if t.endswith('.patch') or t.endswith('.diff')]
118 if t.endswith('.patch') or t.endswith('.diff')]
147 if patchtags:
119 if patchtags:
148 patchname = patchtags[0]
120 patchname = patchtags[0]
149 elif total > 1:
121 elif total > 1:
150 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
122 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
151 binnode, seqno=idx, total=total)
123 binnode, seqno=idx, total=total)
152 else:
124 else:
153 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
125 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
154 disposition = 'inline'
126 disposition = 'inline'
155 if opts.get('attach'):
127 if opts.get('attach'):
156 disposition = 'attachment'
128 disposition = 'attachment'
157 p['Content-Disposition'] = disposition + '; filename=' + patchname
129 p['Content-Disposition'] = disposition + '; filename=' + patchname
158 msg.attach(p)
130 msg.attach(p)
159 else:
131 else:
160 body += '\n'.join(patchlines)
132 body += '\n'.join(patchlines)
161 msg = mail.mimetextpatch(body, display=opts.get('test'))
133 msg = mail.mimetextpatch(body, display=opts.get('test'))
162
134
163 flag = ' '.join(opts.get('flag'))
135 flag = ' '.join(opts.get('flag'))
164 if flag:
136 if flag:
165 flag = ' ' + flag
137 flag = ' ' + flag
166
138
167 subj = desc[0].strip().rstrip('. ')
139 subj = desc[0].strip().rstrip('. ')
168 if not introneeded(opts, total):
140 if not introneeded(opts, total):
169 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
141 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
170 else:
142 else:
171 tlen = len(str(total))
143 tlen = len(str(total))
172 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
144 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
173 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
145 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
174 msg['X-Mercurial-Node'] = node
146 msg['X-Mercurial-Node'] = node
175 return msg, subj, ds
147 return msg, subj, ds
176
148
177 def patchbomb(ui, repo, *revs, **opts):
149 def patchbomb(ui, repo, *revs, **opts):
178 '''send changesets by email
150 '''send changesets by email
179
151
180 By default, diffs are sent in the format generated by
152 By default, diffs are sent in the format generated by
181 :hg:`export`, one per message. The series starts with a "[PATCH 0
153 :hg:`export`, one per message. The series starts with a "[PATCH 0
182 of N]" introduction, which describes the series as a whole.
154 of N]" introduction, which describes the series as a whole.
183
155
184 Each patch email has a Subject line of "[PATCH M of N] ...", using
156 Each patch email has a Subject line of "[PATCH M of N] ...", using
185 the first line of the changeset description as the subject text.
157 the first line of the changeset description as the subject text.
186 The message contains two or three parts. First, the changeset
158 The message contains two or three parts. First, the changeset
187 description. Next, (optionally) if the diffstat program is
159 description.
188 installed and -d/--diffstat is used, the result of running
160
189 diffstat on the patch. Finally, the patch itself, as generated by
161 With the -d/--diffstat option, if the diffstat program is
190 :hg:`export`.
162 installed, the result of running diffstat on the patch is inserted.
163
164 Finally, the patch itself, as generated by :hg:`export`.
165
166 With the -d/--diffstat or -c/--confirm options, you will be presented
167 with a final summary of all messages and asked for confirmation before
168 the messages are sent.
191
169
192 By default the patch is included as text in the email body for
170 By default the patch is included as text in the email body for
193 easy reviewing. Using the -a/--attach option will instead create
171 easy reviewing. Using the -a/--attach option will instead create
194 an attachment for the patch. With -i/--inline an inline attachment
172 an attachment for the patch. With -i/--inline an inline attachment
195 will be created.
173 will be created.
196
174
197 With -o/--outgoing, emails will be generated for patches not found
175 With -o/--outgoing, emails will be generated for patches not found
198 in the destination repository (or only those which are ancestors
176 in the destination repository (or only those which are ancestors
199 of the specified revisions if any are provided)
177 of the specified revisions if any are provided)
200
178
201 With -b/--bundle, changesets are selected as for --outgoing, but a
179 With -b/--bundle, changesets are selected as for --outgoing, but a
202 single email containing a binary Mercurial bundle as an attachment
180 single email containing a binary Mercurial bundle as an attachment
203 will be sent.
181 will be sent.
204
182
183 With -m/--mbox, instead of previewing each patchbomb message in a
184 pager or sending the messages directly, it will create a UNIX
185 mailbox file with the patch emails. This mailbox file can be
186 previewed with any mail user agent which supports UNIX mbox
187 files.
188
189 With -n/--test, all steps will run, but mail will not be sent.
190 You will be prompted for an email recipient address, a subject and
191 an introductory message describing the patches of your patchbomb.
192 Then when all is done, patchbomb messages are displayed. If the
193 PAGER environment variable is set, your pager will be fired up once
194 for each patchbomb message, so you can verify everything is alright.
195
205 Examples::
196 Examples::
206
197
207 hg email -r 3000 # send patch 3000 only
198 hg email -r 3000 # send patch 3000 only
208 hg email -r 3000 -r 3001 # send patches 3000 and 3001
199 hg email -r 3000 -r 3001 # send patches 3000 and 3001
209 hg email -r 3000:3005 # send patches 3000 through 3005
200 hg email -r 3000:3005 # send patches 3000 through 3005
210 hg email 3000 # send patch 3000 (deprecated)
201 hg email 3000 # send patch 3000 (deprecated)
211
202
212 hg email -o # send all patches not in default
203 hg email -o # send all patches not in default
213 hg email -o DEST # send all patches not in DEST
204 hg email -o DEST # send all patches not in DEST
214 hg email -o -r 3000 # send all ancestors of 3000 not in default
205 hg email -o -r 3000 # send all ancestors of 3000 not in default
215 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
206 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
216
207
217 hg email -b # send bundle of all patches not in default
208 hg email -b # send bundle of all patches not in default
218 hg email -b DEST # send bundle of all patches not in DEST
209 hg email -b DEST # send bundle of all patches not in DEST
219 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
210 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
220 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
211 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
221
212
213 hg email -o -m mbox && # generate an mbox file...
214 mutt -R -f mbox # ... and view it with mutt
215 hg email -o -m mbox && # generate an mbox file ...
216 formail -s sendmail \ # ... and use formail to send from the mbox
217 -bm -t < mbox # ... using sendmail
218
222 Before using this command, you will need to enable email in your
219 Before using this command, you will need to enable email in your
223 hgrc. See the [email] section in hgrc(5) for details.
220 hgrc. See the [email] section in hgrc(5) for details.
224 '''
221 '''
225
222
226 _charsets = mail._charsets(ui)
223 _charsets = mail._charsets(ui)
227
224
228 bundle = opts.get('bundle')
225 bundle = opts.get('bundle')
229 date = opts.get('date')
226 date = opts.get('date')
230 mbox = opts.get('mbox')
227 mbox = opts.get('mbox')
231 outgoing = opts.get('outgoing')
228 outgoing = opts.get('outgoing')
232 rev = opts.get('rev')
229 rev = opts.get('rev')
233 # internal option used by pbranches
230 # internal option used by pbranches
234 patches = opts.get('patches')
231 patches = opts.get('patches')
235
232
236 def getoutgoing(dest, revs):
233 def getoutgoing(dest, revs):
237 '''Return the revisions present locally but not in dest'''
234 '''Return the revisions present locally but not in dest'''
238 dest = ui.expandpath(dest or 'default-push', dest or 'default')
235 dest = ui.expandpath(dest or 'default-push', dest or 'default')
239 dest, branches = hg.parseurl(dest)
236 dest, branches = hg.parseurl(dest)
240 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
237 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
241 if revs:
238 if revs:
242 revs = [repo.lookup(rev) for rev in revs]
239 revs = [repo.lookup(rev) for rev in revs]
243 other = hg.repository(hg.remoteui(repo, opts), dest)
240 other = hg.repository(hg.remoteui(repo, opts), dest)
244 ui.status(_('comparing with %s\n') % dest)
241 ui.status(_('comparing with %s\n') % dest)
245 o = discovery.findoutgoing(repo, other)
242 o = discovery.findoutgoing(repo, other)
246 if not o:
243 if not o:
247 ui.status(_("no changes found\n"))
244 ui.status(_("no changes found\n"))
248 return []
245 return []
249 o = repo.changelog.nodesbetween(o, revs)[0]
246 o = repo.changelog.nodesbetween(o, revs)[0]
250 return [str(repo.changelog.rev(r)) for r in o]
247 return [str(repo.changelog.rev(r)) for r in o]
251
248
252 def getpatches(revs):
249 def getpatches(revs):
253 for r in cmdutil.revrange(repo, revs):
250 for r in cmdutil.revrange(repo, revs):
254 output = cStringIO.StringIO()
251 output = cStringIO.StringIO()
255 cmdutil.export(repo, [r], fp=output,
252 cmdutil.export(repo, [r], fp=output,
256 opts=patch.diffopts(ui, opts))
253 opts=patch.diffopts(ui, opts))
257 yield output.getvalue().split('\n')
254 yield output.getvalue().split('\n')
258
255
259 def getbundle(dest):
256 def getbundle(dest):
260 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
257 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
261 tmpfn = os.path.join(tmpdir, 'bundle')
258 tmpfn = os.path.join(tmpdir, 'bundle')
262 try:
259 try:
263 commands.bundle(ui, repo, tmpfn, dest, **opts)
260 commands.bundle(ui, repo, tmpfn, dest, **opts)
264 return open(tmpfn, 'rb').read()
261 return open(tmpfn, 'rb').read()
265 finally:
262 finally:
266 try:
263 try:
267 os.unlink(tmpfn)
264 os.unlink(tmpfn)
268 except:
265 except:
269 pass
266 pass
270 os.rmdir(tmpdir)
267 os.rmdir(tmpdir)
271
268
272 if not (opts.get('test') or mbox):
269 if not (opts.get('test') or mbox):
273 # really sending
270 # really sending
274 mail.validateconfig(ui)
271 mail.validateconfig(ui)
275
272
276 if not (revs or rev or outgoing or bundle or patches):
273 if not (revs or rev or outgoing or bundle or patches):
277 raise util.Abort(_('specify at least one changeset with -r or -o'))
274 raise util.Abort(_('specify at least one changeset with -r or -o'))
278
275
279 if outgoing and bundle:
276 if outgoing and bundle:
280 raise util.Abort(_("--outgoing mode always on with --bundle;"
277 raise util.Abort(_("--outgoing mode always on with --bundle;"
281 " do not re-specify --outgoing"))
278 " do not re-specify --outgoing"))
282
279
283 if outgoing or bundle:
280 if outgoing or bundle:
284 if len(revs) > 1:
281 if len(revs) > 1:
285 raise util.Abort(_("too many destinations"))
282 raise util.Abort(_("too many destinations"))
286 dest = revs and revs[0] or None
283 dest = revs and revs[0] or None
287 revs = []
284 revs = []
288
285
289 if rev:
286 if rev:
290 if revs:
287 if revs:
291 raise util.Abort(_('use only one form to specify the revision'))
288 raise util.Abort(_('use only one form to specify the revision'))
292 revs = rev
289 revs = rev
293
290
294 if outgoing:
291 if outgoing:
295 revs = getoutgoing(dest, rev)
292 revs = getoutgoing(dest, rev)
296 if bundle:
293 if bundle:
297 opts['revs'] = revs
294 opts['revs'] = revs
298
295
299 # start
296 # start
300 if date:
297 if date:
301 start_time = util.parsedate(date)
298 start_time = util.parsedate(date)
302 else:
299 else:
303 start_time = util.makedate()
300 start_time = util.makedate()
304
301
305 def genmsgid(id):
302 def genmsgid(id):
306 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
303 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
307
304
308 def getdescription(body, sender):
305 def getdescription(body, sender):
309 if opts.get('desc'):
306 if opts.get('desc'):
310 body = open(opts.get('desc')).read()
307 body = open(opts.get('desc')).read()
311 else:
308 else:
312 ui.write(_('\nWrite the introductory message for the '
309 ui.write(_('\nWrite the introductory message for the '
313 'patch series.\n\n'))
310 'patch series.\n\n'))
314 body = ui.edit(body, sender)
311 body = ui.edit(body, sender)
315 return body
312 return body
316
313
317 def getpatchmsgs(patches, patchnames=None):
314 def getpatchmsgs(patches, patchnames=None):
318 jumbo = []
315 jumbo = []
319 msgs = []
316 msgs = []
320
317
321 ui.write(_('This patch series consists of %d patches.\n\n')
318 ui.write(_('This patch series consists of %d patches.\n\n')
322 % len(patches))
319 % len(patches))
323
320
324 name = None
321 name = None
325 for i, p in enumerate(patches):
322 for i, p in enumerate(patches):
326 jumbo.extend(p)
323 jumbo.extend(p)
327 if patchnames:
324 if patchnames:
328 name = patchnames[i]
325 name = patchnames[i]
329 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
326 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
330 len(patches), name)
327 len(patches), name)
331 msgs.append(msg)
328 msgs.append(msg)
332
329
333 if introneeded(opts, len(patches)):
330 if introneeded(opts, len(patches)):
334 tlen = len(str(len(patches)))
331 tlen = len(str(len(patches)))
335
332
336 flag = ' '.join(opts.get('flag'))
333 flag = ' '.join(opts.get('flag'))
337 if flag:
334 if flag:
338 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
335 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
339 else:
336 else:
340 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
337 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
341 subj += ' ' + (opts.get('subject') or
338 subj += ' ' + (opts.get('subject') or
342 prompt(ui, 'Subject: ', rest=subj))
339 prompt(ui, 'Subject: ', rest=subj))
343
340
344 body = ''
341 body = ''
345 ds = patch.diffstat(jumbo)
342 ds = patch.diffstat(jumbo)
346 if ds and opts.get('diffstat'):
343 if ds and opts.get('diffstat'):
347 body = '\n' + ds
344 body = '\n' + ds
348
345
349 body = getdescription(body, sender)
346 body = getdescription(body, sender)
350 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
347 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
351 msg['Subject'] = mail.headencode(ui, subj, _charsets,
348 msg['Subject'] = mail.headencode(ui, subj, _charsets,
352 opts.get('test'))
349 opts.get('test'))
353
350
354 msgs.insert(0, (msg, subj, ds))
351 msgs.insert(0, (msg, subj, ds))
355 return msgs
352 return msgs
356
353
357 def getbundlemsgs(bundle):
354 def getbundlemsgs(bundle):
358 subj = (opts.get('subject')
355 subj = (opts.get('subject')
359 or prompt(ui, 'Subject:', 'A bundle for your repository'))
356 or prompt(ui, 'Subject:', 'A bundle for your repository'))
360
357
361 body = getdescription('', sender)
358 body = getdescription('', sender)
362 msg = email.MIMEMultipart.MIMEMultipart()
359 msg = email.MIMEMultipart.MIMEMultipart()
363 if body:
360 if body:
364 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
361 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
365 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
362 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
366 datapart.set_payload(bundle)
363 datapart.set_payload(bundle)
367 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
364 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
368 datapart.add_header('Content-Disposition', 'attachment',
365 datapart.add_header('Content-Disposition', 'attachment',
369 filename=bundlename)
366 filename=bundlename)
370 email.Encoders.encode_base64(datapart)
367 email.Encoders.encode_base64(datapart)
371 msg.attach(datapart)
368 msg.attach(datapart)
372 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
369 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
373 return [(msg, subj, None)]
370 return [(msg, subj, None)]
374
371
375 sender = (opts.get('from') or ui.config('email', 'from') or
372 sender = (opts.get('from') or ui.config('email', 'from') or
376 ui.config('patchbomb', 'from') or
373 ui.config('patchbomb', 'from') or
377 prompt(ui, 'From', ui.username()))
374 prompt(ui, 'From', ui.username()))
378
375
379 if patches:
376 if patches:
380 msgs = getpatchmsgs(patches, opts.get('patchnames'))
377 msgs = getpatchmsgs(patches, opts.get('patchnames'))
381 elif bundle:
378 elif bundle:
382 msgs = getbundlemsgs(getbundle(dest))
379 msgs = getbundlemsgs(getbundle(dest))
383 else:
380 else:
384 msgs = getpatchmsgs(list(getpatches(revs)))
381 msgs = getpatchmsgs(list(getpatches(revs)))
385
382
386 showaddrs = []
383 showaddrs = []
387
384
388 def getaddrs(opt, prpt=None, default=None):
385 def getaddrs(opt, prpt=None, default=None):
389 addrs = opts.get(opt.replace('-', '_'))
386 addrs = opts.get(opt.replace('-', '_'))
390 if opt != 'reply-to':
387 if opt != 'reply-to':
391 showaddr = '%s:' % opt.capitalize()
388 showaddr = '%s:' % opt.capitalize()
392 else:
389 else:
393 showaddr = 'Reply-To:'
390 showaddr = 'Reply-To:'
394
391
395 if addrs:
392 if addrs:
396 showaddrs.append('%s %s' % (showaddr, ', '.join(addrs)))
393 showaddrs.append('%s %s' % (showaddr, ', '.join(addrs)))
397 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
394 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
398
395
399 addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
396 addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
400 if not addrs and prpt:
397 if not addrs and prpt:
401 addrs = prompt(ui, prpt, default)
398 addrs = prompt(ui, prpt, default)
402
399
403 if addrs:
400 if addrs:
404 showaddrs.append('%s %s' % (showaddr, addrs))
401 showaddrs.append('%s %s' % (showaddr, addrs))
405 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
402 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
406
403
407 to = getaddrs('to', 'To')
404 to = getaddrs('to', 'To')
408 cc = getaddrs('cc', 'Cc', '')
405 cc = getaddrs('cc', 'Cc', '')
409 bcc = getaddrs('bcc')
406 bcc = getaddrs('bcc')
410 replyto = getaddrs('reply-to')
407 replyto = getaddrs('reply-to')
411
408
412 if opts.get('diffstat') or opts.get('confirm'):
409 if opts.get('diffstat') or opts.get('confirm'):
413 ui.write(_('\nFinal summary:\n\n'))
410 ui.write(_('\nFinal summary:\n\n'))
414 ui.write('From: %s\n' % sender)
411 ui.write('From: %s\n' % sender)
415 for addr in showaddrs:
412 for addr in showaddrs:
416 ui.write('%s\n' % addr)
413 ui.write('%s\n' % addr)
417 for m, subj, ds in msgs:
414 for m, subj, ds in msgs:
418 ui.write('Subject: %s\n' % subj)
415 ui.write('Subject: %s\n' % subj)
419 if ds:
416 if ds:
420 ui.write(ds)
417 ui.write(ds)
421 ui.write('\n')
418 ui.write('\n')
422 if ui.promptchoice(_('are you sure you want to send (yn)?'),
419 if ui.promptchoice(_('are you sure you want to send (yn)?'),
423 (_('&Yes'), _('&No'))):
420 (_('&Yes'), _('&No'))):
424 raise util.Abort(_('patchbomb canceled'))
421 raise util.Abort(_('patchbomb canceled'))
425
422
426 ui.write('\n')
423 ui.write('\n')
427
424
428 parent = opts.get('in_reply_to') or None
425 parent = opts.get('in_reply_to') or None
429 # angle brackets may be omitted, they're not semantically part of the msg-id
426 # angle brackets may be omitted, they're not semantically part of the msg-id
430 if parent is not None:
427 if parent is not None:
431 if not parent.startswith('<'):
428 if not parent.startswith('<'):
432 parent = '<' + parent
429 parent = '<' + parent
433 if not parent.endswith('>'):
430 if not parent.endswith('>'):
434 parent += '>'
431 parent += '>'
435
432
436 first = True
433 first = True
437
434
438 sender_addr = email.Utils.parseaddr(sender)[1]
435 sender_addr = email.Utils.parseaddr(sender)[1]
439 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
436 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
440 sendmail = None
437 sendmail = None
441 for i, (m, subj, ds) in enumerate(msgs):
438 for i, (m, subj, ds) in enumerate(msgs):
442 try:
439 try:
443 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
440 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
444 except TypeError:
441 except TypeError:
445 m['Message-Id'] = genmsgid('patchbomb')
442 m['Message-Id'] = genmsgid('patchbomb')
446 if parent:
443 if parent:
447 m['In-Reply-To'] = parent
444 m['In-Reply-To'] = parent
448 m['References'] = parent
445 m['References'] = parent
449 if first:
446 if first:
450 parent = m['Message-Id']
447 parent = m['Message-Id']
451 first = False
448 first = False
452
449
453 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
450 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
454 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
451 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
455
452
456 start_time = (start_time[0] + 1, start_time[1])
453 start_time = (start_time[0] + 1, start_time[1])
457 m['From'] = sender
454 m['From'] = sender
458 m['To'] = ', '.join(to)
455 m['To'] = ', '.join(to)
459 if cc:
456 if cc:
460 m['Cc'] = ', '.join(cc)
457 m['Cc'] = ', '.join(cc)
461 if bcc:
458 if bcc:
462 m['Bcc'] = ', '.join(bcc)
459 m['Bcc'] = ', '.join(bcc)
463 if replyto:
460 if replyto:
464 m['Reply-To'] = ', '.join(replyto)
461 m['Reply-To'] = ', '.join(replyto)
465 if opts.get('test'):
462 if opts.get('test'):
466 ui.status(_('Displaying '), subj, ' ...\n')
463 ui.status(_('Displaying '), subj, ' ...\n')
467 ui.flush()
464 ui.flush()
468 if 'PAGER' in os.environ and not ui.plain():
465 if 'PAGER' in os.environ and not ui.plain():
469 fp = util.popen(os.environ['PAGER'], 'w')
466 fp = util.popen(os.environ['PAGER'], 'w')
470 else:
467 else:
471 fp = ui
468 fp = ui
472 generator = email.Generator.Generator(fp, mangle_from_=False)
469 generator = email.Generator.Generator(fp, mangle_from_=False)
473 try:
470 try:
474 generator.flatten(m, 0)
471 generator.flatten(m, 0)
475 fp.write('\n')
472 fp.write('\n')
476 except IOError, inst:
473 except IOError, inst:
477 if inst.errno != errno.EPIPE:
474 if inst.errno != errno.EPIPE:
478 raise
475 raise
479 if fp is not ui:
476 if fp is not ui:
480 fp.close()
477 fp.close()
481 elif mbox:
478 elif mbox:
482 ui.status(_('Writing '), subj, ' ...\n')
479 ui.status(_('Writing '), subj, ' ...\n')
483 ui.progress(_('writing'), i, item=subj, total=len(msgs))
480 ui.progress(_('writing'), i, item=subj, total=len(msgs))
484 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
481 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
485 generator = email.Generator.Generator(fp, mangle_from_=True)
482 generator = email.Generator.Generator(fp, mangle_from_=True)
486 # Should be time.asctime(), but Windows prints 2-characters day
483 # Should be time.asctime(), but Windows prints 2-characters day
487 # of month instead of one. Make them print the same thing.
484 # of month instead of one. Make them print the same thing.
488 date = time.strftime('%a %b %d %H:%M:%S %Y',
485 date = time.strftime('%a %b %d %H:%M:%S %Y',
489 time.localtime(start_time[0]))
486 time.localtime(start_time[0]))
490 fp.write('From %s %s\n' % (sender_addr, date))
487 fp.write('From %s %s\n' % (sender_addr, date))
491 generator.flatten(m, 0)
488 generator.flatten(m, 0)
492 fp.write('\n\n')
489 fp.write('\n\n')
493 fp.close()
490 fp.close()
494 else:
491 else:
495 if not sendmail:
492 if not sendmail:
496 sendmail = mail.connect(ui)
493 sendmail = mail.connect(ui)
497 ui.status(_('Sending '), subj, ' ...\n')
494 ui.status(_('Sending '), subj, ' ...\n')
498 ui.progress(_('sending'), i, item=subj, total=len(msgs))
495 ui.progress(_('sending'), i, item=subj, total=len(msgs))
499 # Exim does not remove the Bcc field
496 # Exim does not remove the Bcc field
500 del m['Bcc']
497 del m['Bcc']
501 fp = cStringIO.StringIO()
498 fp = cStringIO.StringIO()
502 generator = email.Generator.Generator(fp, mangle_from_=False)
499 generator = email.Generator.Generator(fp, mangle_from_=False)
503 generator.flatten(m, 0)
500 generator.flatten(m, 0)
504 sendmail(sender, to + bcc + cc, fp.getvalue())
501 sendmail(sender, to + bcc + cc, fp.getvalue())
505
502
506 ui.progress(_('writing'), None)
503 ui.progress(_('writing'), None)
507 ui.progress(_('sending'), None)
504 ui.progress(_('sending'), None)
508
505
509 emailopts = [
506 emailopts = [
510 ('a', 'attach', None, _('send patches as attachments')),
507 ('a', 'attach', None, _('send patches as attachments')),
511 ('i', 'inline', None, _('send patches as inline attachments')),
508 ('i', 'inline', None, _('send patches as inline attachments')),
512 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
509 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
513 ('c', 'cc', [], _('email addresses of copy recipients')),
510 ('c', 'cc', [], _('email addresses of copy recipients')),
514 ('', 'confirm', None, _('ask for confirmation before sending')),
511 ('', 'confirm', None, _('ask for confirmation before sending')),
515 ('d', 'diffstat', None, _('add diffstat output to messages')),
512 ('d', 'diffstat', None, _('add diffstat output to messages')),
516 ('', 'date', '', _('use the given date as the sending date')),
513 ('', 'date', '', _('use the given date as the sending date')),
517 ('', 'desc', '', _('use the given file as the series description')),
514 ('', 'desc', '', _('use the given file as the series description')),
518 ('f', 'from', '', _('email address of sender')),
515 ('f', 'from', '', _('email address of sender')),
519 ('n', 'test', None, _('print messages that would be sent')),
516 ('n', 'test', None, _('print messages that would be sent')),
520 ('m', 'mbox', '',
517 ('m', 'mbox', '',
521 _('write messages to mbox file instead of sending them')),
518 _('write messages to mbox file instead of sending them')),
522 ('', 'reply-to', [], _('email addresses replies should be sent to')),
519 ('', 'reply-to', [], _('email addresses replies should be sent to')),
523 ('s', 'subject', '',
520 ('s', 'subject', '',
524 _('subject of first message (intro or single patch)')),
521 _('subject of first message (intro or single patch)')),
525 ('', 'in-reply-to', '',
522 ('', 'in-reply-to', '',
526 _('message identifier to reply to')),
523 _('message identifier to reply to')),
527 ('', 'flag', [], _('flags to add in subject prefixes')),
524 ('', 'flag', [], _('flags to add in subject prefixes')),
528 ('t', 'to', [], _('email addresses of recipients')),
525 ('t', 'to', [], _('email addresses of recipients')),
529 ]
526 ]
530
527
531
528
532 cmdtable = {
529 cmdtable = {
533 "email":
530 "email":
534 (patchbomb,
531 (patchbomb,
535 [('g', 'git', None, _('use git extended diff format')),
532 [('g', 'git', None, _('use git extended diff format')),
536 ('', 'plain', None, _('omit hg patch header')),
533 ('', 'plain', None, _('omit hg patch header')),
537 ('o', 'outgoing', None,
534 ('o', 'outgoing', None,
538 _('send changes not found in the target repository')),
535 _('send changes not found in the target repository')),
539 ('b', 'bundle', None,
536 ('b', 'bundle', None,
540 _('send changes not in target as a binary bundle')),
537 _('send changes not in target as a binary bundle')),
541 ('', 'bundlename', 'bundle',
538 ('', 'bundlename', 'bundle',
542 _('name of the bundle attachment file'), _('NAME')),
539 _('name of the bundle attachment file'), _('NAME')),
543 ('r', 'rev', [],
540 ('r', 'rev', [],
544 _('a revision to send'), _('REV')),
541 _('a revision to send'), _('REV')),
545 ('', 'force', None,
542 ('', 'force', None,
546 _('run even when remote repository is unrelated '
543 _('run even when remote repository is unrelated '
547 '(with -b/--bundle)')),
544 '(with -b/--bundle)')),
548 ('', 'base', [],
545 ('', 'base', [],
549 _('a base changeset to specify instead of a destination '
546 _('a base changeset to specify instead of a destination '
550 '(with -b/--bundle)'),
547 '(with -b/--bundle)'),
551 _('REV')),
548 _('REV')),
552 ('', 'intro', None,
549 ('', 'intro', None,
553 _('send an introduction email for a single patch')),
550 _('send an introduction email for a single patch')),
554 ] + emailopts + commands.remoteopts,
551 ] + emailopts + commands.remoteopts,
555 _('hg email [OPTION]... [DEST]...'))
552 _('hg email [OPTION]... [DEST]...'))
556 }
553 }
General Comments 0
You need to be logged in to leave comments. Login now