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