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