##// END OF EJS Templates
patchbomb: pass --git argument to diffstat
Idan Kamara -
r14397:0368ad79 default
parent child Browse files
Show More
@@ -1,551 +1,551
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 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
58
59 def prompt(ui, prompt, default=None, rest=':'):
59 def prompt(ui, prompt, default=None, rest=':'):
60 if not ui.interactive() and default is None:
60 if not ui.interactive() and default is None:
61 raise util.Abort(_("%s Please enter a valid value" % (prompt + rest)))
61 raise util.Abort(_("%s Please enter a valid value" % (prompt + rest)))
62 if default:
62 if default:
63 prompt += ' [%s]' % default
63 prompt += ' [%s]' % default
64 prompt += rest
64 prompt += rest
65 while True:
65 while True:
66 r = ui.prompt(prompt, default=default)
66 r = ui.prompt(prompt, default=default)
67 if r:
67 if r:
68 return r
68 return r
69 if default is not None:
69 if default is not None:
70 return default
70 return default
71 ui.warn(_('Please enter a valid value.\n'))
71 ui.warn(_('Please enter a valid value.\n'))
72
72
73 def introneeded(opts, number):
73 def introneeded(opts, number):
74 '''is an introductory message required?'''
74 '''is an introductory message required?'''
75 return number > 1 or opts.get('intro') or opts.get('desc')
75 return number > 1 or opts.get('intro') or opts.get('desc')
76
76
77 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total,
77 def makepatch(ui, repo, patchlines, opts, _charsets, idx, total,
78 patchname=None):
78 patchname=None):
79
79
80 desc = []
80 desc = []
81 node = None
81 node = None
82 body = ''
82 body = ''
83
83
84 for line in patchlines:
84 for line in patchlines:
85 if line.startswith('#'):
85 if line.startswith('#'):
86 if line.startswith('# Node ID'):
86 if line.startswith('# Node ID'):
87 node = line.split()[-1]
87 node = line.split()[-1]
88 continue
88 continue
89 if line.startswith('diff -r') or line.startswith('diff --git'):
89 if line.startswith('diff -r') or line.startswith('diff --git'):
90 break
90 break
91 desc.append(line)
91 desc.append(line)
92
92
93 if not patchname and not node:
93 if not patchname and not node:
94 raise ValueError
94 raise ValueError
95
95
96 if opts.get('attach'):
96 if opts.get('attach'):
97 body = ('\n'.join(desc[1:]).strip() or
97 body = ('\n'.join(desc[1:]).strip() or
98 'Patch subject is complete summary.')
98 'Patch subject is complete summary.')
99 body += '\n\n\n'
99 body += '\n\n\n'
100
100
101 if opts.get('plain'):
101 if opts.get('plain'):
102 while patchlines and patchlines[0].startswith('# '):
102 while patchlines and patchlines[0].startswith('# '):
103 patchlines.pop(0)
103 patchlines.pop(0)
104 if patchlines:
104 if patchlines:
105 patchlines.pop(0)
105 patchlines.pop(0)
106 while patchlines and not patchlines[0].strip():
106 while patchlines and not patchlines[0].strip():
107 patchlines.pop(0)
107 patchlines.pop(0)
108
108
109 ds = patch.diffstat(patchlines)
109 ds = patch.diffstat(patchlines, git=opts.get('git'))
110 if opts.get('diffstat'):
110 if opts.get('diffstat'):
111 body += ds + '\n\n'
111 body += ds + '\n\n'
112
112
113 if opts.get('attach') or opts.get('inline'):
113 if opts.get('attach') or opts.get('inline'):
114 msg = email.MIMEMultipart.MIMEMultipart()
114 msg = email.MIMEMultipart.MIMEMultipart()
115 if body:
115 if body:
116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
116 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', opts.get('test'))
117 p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', opts.get('test'))
118 binnode = bin(node)
118 binnode = bin(node)
119 # if node is mq patch, it will have the patch file's name as a tag
119 # if node is mq patch, it will have the patch file's name as a tag
120 if not patchname:
120 if not patchname:
121 patchtags = [t for t in repo.nodetags(binnode)
121 patchtags = [t for t in repo.nodetags(binnode)
122 if t.endswith('.patch') or t.endswith('.diff')]
122 if t.endswith('.patch') or t.endswith('.diff')]
123 if patchtags:
123 if patchtags:
124 patchname = patchtags[0]
124 patchname = patchtags[0]
125 elif total > 1:
125 elif total > 1:
126 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
126 patchname = cmdutil.makefilename(repo, '%b-%n.patch',
127 binnode, seqno=idx, total=total)
127 binnode, seqno=idx, total=total)
128 else:
128 else:
129 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
129 patchname = cmdutil.makefilename(repo, '%b.patch', binnode)
130 disposition = 'inline'
130 disposition = 'inline'
131 if opts.get('attach'):
131 if opts.get('attach'):
132 disposition = 'attachment'
132 disposition = 'attachment'
133 p['Content-Disposition'] = disposition + '; filename=' + patchname
133 p['Content-Disposition'] = disposition + '; filename=' + patchname
134 msg.attach(p)
134 msg.attach(p)
135 else:
135 else:
136 body += '\n'.join(patchlines)
136 body += '\n'.join(patchlines)
137 msg = mail.mimetextpatch(body, display=opts.get('test'))
137 msg = mail.mimetextpatch(body, display=opts.get('test'))
138
138
139 flag = ' '.join(opts.get('flag'))
139 flag = ' '.join(opts.get('flag'))
140 if flag:
140 if flag:
141 flag = ' ' + flag
141 flag = ' ' + flag
142
142
143 subj = desc[0].strip().rstrip('. ')
143 subj = desc[0].strip().rstrip('. ')
144 if not introneeded(opts, total):
144 if not introneeded(opts, total):
145 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
145 subj = '[PATCH%s] %s' % (flag, opts.get('subject') or subj)
146 else:
146 else:
147 tlen = len(str(total))
147 tlen = len(str(total))
148 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
148 subj = '[PATCH %0*d of %d%s] %s' % (tlen, idx, total, flag, subj)
149 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
149 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
150 msg['X-Mercurial-Node'] = node
150 msg['X-Mercurial-Node'] = node
151 return msg, subj, ds
151 return msg, subj, ds
152
152
153 emailopts = [
153 emailopts = [
154 ('a', 'attach', None, _('send patches as attachments')),
154 ('a', 'attach', None, _('send patches as attachments')),
155 ('i', 'inline', None, _('send patches as inline attachments')),
155 ('i', 'inline', None, _('send patches as inline attachments')),
156 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
156 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
157 ('c', 'cc', [], _('email addresses of copy recipients')),
157 ('c', 'cc', [], _('email addresses of copy recipients')),
158 ('', 'confirm', None, _('ask for confirmation before sending')),
158 ('', 'confirm', None, _('ask for confirmation before sending')),
159 ('d', 'diffstat', None, _('add diffstat output to messages')),
159 ('d', 'diffstat', None, _('add diffstat output to messages')),
160 ('', 'date', '', _('use the given date as the sending date')),
160 ('', 'date', '', _('use the given date as the sending date')),
161 ('', 'desc', '', _('use the given file as the series description')),
161 ('', 'desc', '', _('use the given file as the series description')),
162 ('f', 'from', '', _('email address of sender')),
162 ('f', 'from', '', _('email address of sender')),
163 ('n', 'test', None, _('print messages that would be sent')),
163 ('n', 'test', None, _('print messages that would be sent')),
164 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
164 ('m', 'mbox', '', _('write messages to mbox file instead of sending them')),
165 ('', 'reply-to', [], _('email addresses replies should be sent to')),
165 ('', 'reply-to', [], _('email addresses replies should be sent to')),
166 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
166 ('s', 'subject', '', _('subject of first message (intro or single patch)')),
167 ('', 'in-reply-to', '', _('message identifier to reply to')),
167 ('', 'in-reply-to', '', _('message identifier to reply to')),
168 ('', 'flag', [], _('flags to add in subject prefixes')),
168 ('', 'flag', [], _('flags to add in subject prefixes')),
169 ('t', 'to', [], _('email addresses of recipients'))]
169 ('t', 'to', [], _('email addresses of recipients'))]
170
170
171 @command('email',
171 @command('email',
172 [('g', 'git', None, _('use git extended diff format')),
172 [('g', 'git', None, _('use git extended diff format')),
173 ('', 'plain', None, _('omit hg patch header')),
173 ('', 'plain', None, _('omit hg patch header')),
174 ('o', 'outgoing', None,
174 ('o', 'outgoing', None,
175 _('send changes not found in the target repository')),
175 _('send changes not found in the target repository')),
176 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
176 ('b', 'bundle', None, _('send changes not in target as a binary bundle')),
177 ('', 'bundlename', 'bundle',
177 ('', 'bundlename', 'bundle',
178 _('name of the bundle attachment file'), _('NAME')),
178 _('name of the bundle attachment file'), _('NAME')),
179 ('r', 'rev', [], _('a revision to send'), _('REV')),
179 ('r', 'rev', [], _('a revision to send'), _('REV')),
180 ('', 'force', None, _('run even when remote repository is unrelated '
180 ('', 'force', None, _('run even when remote repository is unrelated '
181 '(with -b/--bundle)')),
181 '(with -b/--bundle)')),
182 ('', 'base', [], _('a base changeset to specify instead of a destination '
182 ('', 'base', [], _('a base changeset to specify instead of a destination '
183 '(with -b/--bundle)'), _('REV')),
183 '(with -b/--bundle)'), _('REV')),
184 ('', 'intro', None, _('send an introduction email for a single patch')),
184 ('', 'intro', None, _('send an introduction email for a single patch')),
185 ] + emailopts + commands.remoteopts,
185 ] + emailopts + commands.remoteopts,
186 _('hg email [OPTION]... [DEST]...'))
186 _('hg email [OPTION]... [DEST]...'))
187 def patchbomb(ui, repo, *revs, **opts):
187 def patchbomb(ui, repo, *revs, **opts):
188 '''send changesets by email
188 '''send changesets by email
189
189
190 By default, diffs are sent in the format generated by
190 By default, diffs are sent in the format generated by
191 :hg:`export`, one per message. The series starts with a "[PATCH 0
191 :hg:`export`, one per message. The series starts with a "[PATCH 0
192 of N]" introduction, which describes the series as a whole.
192 of N]" introduction, which describes the series as a whole.
193
193
194 Each patch email has a Subject line of "[PATCH M of N] ...", using
194 Each patch email has a Subject line of "[PATCH M of N] ...", using
195 the first line of the changeset description as the subject text.
195 the first line of the changeset description as the subject text.
196 The message contains two or three parts. First, the changeset
196 The message contains two or three parts. First, the changeset
197 description.
197 description.
198
198
199 With the -d/--diffstat option, if the diffstat program is
199 With the -d/--diffstat option, if the diffstat program is
200 installed, the result of running diffstat on the patch is inserted.
200 installed, the result of running diffstat on the patch is inserted.
201
201
202 Finally, the patch itself, as generated by :hg:`export`.
202 Finally, the patch itself, as generated by :hg:`export`.
203
203
204 With the -d/--diffstat or -c/--confirm options, you will be presented
204 With the -d/--diffstat or -c/--confirm options, you will be presented
205 with a final summary of all messages and asked for confirmation before
205 with a final summary of all messages and asked for confirmation before
206 the messages are sent.
206 the messages are sent.
207
207
208 By default the patch is included as text in the email body for
208 By default the patch is included as text in the email body for
209 easy reviewing. Using the -a/--attach option will instead create
209 easy reviewing. Using the -a/--attach option will instead create
210 an attachment for the patch. With -i/--inline an inline attachment
210 an attachment for the patch. With -i/--inline an inline attachment
211 will be created.
211 will be created.
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 dest = ui.expandpath(dest or 'default-push', dest or 'default')
277 dest, branches = hg.parseurl(dest)
277 dest, branches = hg.parseurl(dest)
278 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
278 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
279 other = hg.repository(hg.remoteui(repo, opts), dest)
279 other = hg.repository(hg.remoteui(repo, opts), dest)
280 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
280 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
281 common, _anyinc, _heads = discovery.findcommonincoming(repo, other)
281 common, _anyinc, _heads = discovery.findcommonincoming(repo, other)
282 nodes = revs and map(repo.lookup, revs) or revs
282 nodes = revs and map(repo.lookup, revs) or revs
283 o = repo.changelog.findmissing(common, heads=nodes)
283 o = repo.changelog.findmissing(common, heads=nodes)
284 if not o:
284 if not o:
285 ui.status(_("no changes found\n"))
285 ui.status(_("no changes found\n"))
286 return []
286 return []
287 return [str(repo.changelog.rev(r)) for r in o]
287 return [str(repo.changelog.rev(r)) for r in o]
288
288
289 def getpatches(revs):
289 def getpatches(revs):
290 for r in scmutil.revrange(repo, revs):
290 for r in scmutil.revrange(repo, revs):
291 output = cStringIO.StringIO()
291 output = cStringIO.StringIO()
292 cmdutil.export(repo, [r], fp=output,
292 cmdutil.export(repo, [r], fp=output,
293 opts=patch.diffopts(ui, opts))
293 opts=patch.diffopts(ui, opts))
294 yield output.getvalue().split('\n')
294 yield output.getvalue().split('\n')
295
295
296 def getbundle(dest):
296 def getbundle(dest):
297 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
297 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
298 tmpfn = os.path.join(tmpdir, 'bundle')
298 tmpfn = os.path.join(tmpdir, 'bundle')
299 try:
299 try:
300 commands.bundle(ui, repo, tmpfn, dest, **opts)
300 commands.bundle(ui, repo, tmpfn, dest, **opts)
301 fp = open(tmpfn, 'rb')
301 fp = open(tmpfn, 'rb')
302 data = fp.read()
302 data = fp.read()
303 fp.close()
303 fp.close()
304 return data
304 return data
305 finally:
305 finally:
306 try:
306 try:
307 os.unlink(tmpfn)
307 os.unlink(tmpfn)
308 except:
308 except:
309 pass
309 pass
310 os.rmdir(tmpdir)
310 os.rmdir(tmpdir)
311
311
312 if not (opts.get('test') or mbox):
312 if not (opts.get('test') or mbox):
313 # really sending
313 # really sending
314 mail.validateconfig(ui)
314 mail.validateconfig(ui)
315
315
316 if not (revs or rev or outgoing or bundle or patches):
316 if not (revs or rev or outgoing or bundle or patches):
317 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'))
318
318
319 if outgoing and bundle:
319 if outgoing and bundle:
320 raise util.Abort(_("--outgoing mode always on with --bundle;"
320 raise util.Abort(_("--outgoing mode always on with --bundle;"
321 " do not re-specify --outgoing"))
321 " do not re-specify --outgoing"))
322
322
323 if outgoing or bundle:
323 if outgoing or bundle:
324 if len(revs) > 1:
324 if len(revs) > 1:
325 raise util.Abort(_("too many destinations"))
325 raise util.Abort(_("too many destinations"))
326 dest = revs and revs[0] or None
326 dest = revs and revs[0] or None
327 revs = []
327 revs = []
328
328
329 if rev:
329 if rev:
330 if revs:
330 if revs:
331 raise util.Abort(_('use only one form to specify the revision'))
331 raise util.Abort(_('use only one form to specify the revision'))
332 revs = rev
332 revs = rev
333
333
334 if outgoing:
334 if outgoing:
335 revs = getoutgoing(dest, rev)
335 revs = getoutgoing(dest, rev)
336 if bundle:
336 if bundle:
337 opts['revs'] = revs
337 opts['revs'] = revs
338
338
339 # start
339 # start
340 if date:
340 if date:
341 start_time = util.parsedate(date)
341 start_time = util.parsedate(date)
342 else:
342 else:
343 start_time = util.makedate()
343 start_time = util.makedate()
344
344
345 def genmsgid(id):
345 def genmsgid(id):
346 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())
347
347
348 def getdescription(body, sender):
348 def getdescription(body, sender):
349 if opts.get('desc'):
349 if opts.get('desc'):
350 body = open(opts.get('desc')).read()
350 body = open(opts.get('desc')).read()
351 else:
351 else:
352 ui.write(_('\nWrite the introductory message for the '
352 ui.write(_('\nWrite the introductory message for the '
353 'patch series.\n\n'))
353 'patch series.\n\n'))
354 body = ui.edit(body, sender)
354 body = ui.edit(body, sender)
355 # Save serie description in case sendmail fails
355 # Save serie description in case sendmail fails
356 msgfile = repo.opener('last-email.txt', 'wb')
356 msgfile = repo.opener('last-email.txt', 'wb')
357 msgfile.write(body)
357 msgfile.write(body)
358 msgfile.close()
358 msgfile.close()
359 return body
359 return body
360
360
361 def getpatchmsgs(patches, patchnames=None):
361 def getpatchmsgs(patches, patchnames=None):
362 jumbo = []
362 jumbo = []
363 msgs = []
363 msgs = []
364
364
365 ui.write(_('This patch series consists of %d patches.\n\n')
365 ui.write(_('This patch series consists of %d patches.\n\n')
366 % len(patches))
366 % len(patches))
367
367
368 name = None
368 name = None
369 for i, p in enumerate(patches):
369 for i, p in enumerate(patches):
370 jumbo.extend(p)
370 jumbo.extend(p)
371 if patchnames:
371 if patchnames:
372 name = patchnames[i]
372 name = patchnames[i]
373 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
373 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
374 len(patches), name)
374 len(patches), name)
375 msgs.append(msg)
375 msgs.append(msg)
376
376
377 if introneeded(opts, len(patches)):
377 if introneeded(opts, len(patches)):
378 tlen = len(str(len(patches)))
378 tlen = len(str(len(patches)))
379
379
380 flag = ' '.join(opts.get('flag'))
380 flag = ' '.join(opts.get('flag'))
381 if flag:
381 if flag:
382 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
382 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
383 else:
383 else:
384 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
384 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
385 subj += ' ' + (opts.get('subject') or
385 subj += ' ' + (opts.get('subject') or
386 prompt(ui, 'Subject: ', rest=subj))
386 prompt(ui, 'Subject: ', rest=subj))
387
387
388 body = ''
388 body = ''
389 ds = patch.diffstat(jumbo)
389 ds = patch.diffstat(jumbo)
390 if ds and opts.get('diffstat'):
390 if ds and opts.get('diffstat'):
391 body = '\n' + ds
391 body = '\n' + ds
392
392
393 body = getdescription(body, sender)
393 body = getdescription(body, sender)
394 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
394 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
395 msg['Subject'] = mail.headencode(ui, subj, _charsets,
395 msg['Subject'] = mail.headencode(ui, subj, _charsets,
396 opts.get('test'))
396 opts.get('test'))
397
397
398 msgs.insert(0, (msg, subj, ds))
398 msgs.insert(0, (msg, subj, ds))
399 return msgs
399 return msgs
400
400
401 def getbundlemsgs(bundle):
401 def getbundlemsgs(bundle):
402 subj = (opts.get('subject')
402 subj = (opts.get('subject')
403 or prompt(ui, 'Subject:', 'A bundle for your repository'))
403 or prompt(ui, 'Subject:', 'A bundle for your repository'))
404
404
405 body = getdescription('', sender)
405 body = getdescription('', sender)
406 msg = email.MIMEMultipart.MIMEMultipart()
406 msg = email.MIMEMultipart.MIMEMultipart()
407 if body:
407 if body:
408 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
408 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
409 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
409 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
410 datapart.set_payload(bundle)
410 datapart.set_payload(bundle)
411 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
411 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
412 datapart.add_header('Content-Disposition', 'attachment',
412 datapart.add_header('Content-Disposition', 'attachment',
413 filename=bundlename)
413 filename=bundlename)
414 email.Encoders.encode_base64(datapart)
414 email.Encoders.encode_base64(datapart)
415 msg.attach(datapart)
415 msg.attach(datapart)
416 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
416 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
417 return [(msg, subj, None)]
417 return [(msg, subj, None)]
418
418
419 sender = (opts.get('from') or ui.config('email', 'from') or
419 sender = (opts.get('from') or ui.config('email', 'from') or
420 ui.config('patchbomb', 'from') or
420 ui.config('patchbomb', 'from') or
421 prompt(ui, 'From', ui.username()))
421 prompt(ui, 'From', ui.username()))
422
422
423 if patches:
423 if patches:
424 msgs = getpatchmsgs(patches, opts.get('patchnames'))
424 msgs = getpatchmsgs(patches, opts.get('patchnames'))
425 elif bundle:
425 elif bundle:
426 msgs = getbundlemsgs(getbundle(dest))
426 msgs = getbundlemsgs(getbundle(dest))
427 else:
427 else:
428 msgs = getpatchmsgs(list(getpatches(revs)))
428 msgs = getpatchmsgs(list(getpatches(revs)))
429
429
430 showaddrs = []
430 showaddrs = []
431
431
432 def getaddrs(opt, prpt=None, default=None):
432 def getaddrs(opt, prpt=None, default=None):
433 addrs = opts.get(opt.replace('-', '_'))
433 addrs = opts.get(opt.replace('-', '_'))
434 if opt != 'reply-to':
434 if opt != 'reply-to':
435 showaddr = '%s:' % opt.capitalize()
435 showaddr = '%s:' % opt.capitalize()
436 else:
436 else:
437 showaddr = 'Reply-To:'
437 showaddr = 'Reply-To:'
438
438
439 if addrs:
439 if addrs:
440 showaddrs.append('%s %s' % (showaddr, ', '.join(addrs)))
440 showaddrs.append('%s %s' % (showaddr, ', '.join(addrs)))
441 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
441 return mail.addrlistencode(ui, addrs, _charsets, opts.get('test'))
442
442
443 addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
443 addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
444 if not addrs and prpt:
444 if not addrs and prpt:
445 addrs = prompt(ui, prpt, default)
445 addrs = prompt(ui, prpt, default)
446
446
447 if addrs:
447 if addrs:
448 showaddrs.append('%s %s' % (showaddr, addrs))
448 showaddrs.append('%s %s' % (showaddr, addrs))
449 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
449 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
450
450
451 to = getaddrs('to', 'To')
451 to = getaddrs('to', 'To')
452 cc = getaddrs('cc', 'Cc', '')
452 cc = getaddrs('cc', 'Cc', '')
453 bcc = getaddrs('bcc')
453 bcc = getaddrs('bcc')
454 replyto = getaddrs('reply-to')
454 replyto = getaddrs('reply-to')
455
455
456 if opts.get('diffstat') or opts.get('confirm'):
456 if opts.get('diffstat') or opts.get('confirm'):
457 ui.write(_('\nFinal summary:\n\n'))
457 ui.write(_('\nFinal summary:\n\n'))
458 ui.write('From: %s\n' % sender)
458 ui.write('From: %s\n' % sender)
459 for addr in showaddrs:
459 for addr in showaddrs:
460 ui.write('%s\n' % addr)
460 ui.write('%s\n' % addr)
461 for m, subj, ds in msgs:
461 for m, subj, ds in msgs:
462 ui.write('Subject: %s\n' % subj)
462 ui.write('Subject: %s\n' % subj)
463 if ds:
463 if ds:
464 ui.write(ds)
464 ui.write(ds)
465 ui.write('\n')
465 ui.write('\n')
466 if ui.promptchoice(_('are you sure you want to send (yn)?'),
466 if ui.promptchoice(_('are you sure you want to send (yn)?'),
467 (_('&Yes'), _('&No'))):
467 (_('&Yes'), _('&No'))):
468 raise util.Abort(_('patchbomb canceled'))
468 raise util.Abort(_('patchbomb canceled'))
469
469
470 ui.write('\n')
470 ui.write('\n')
471
471
472 parent = opts.get('in_reply_to') or None
472 parent = opts.get('in_reply_to') or None
473 # angle brackets may be omitted, they're not semantically part of the msg-id
473 # angle brackets may be omitted, they're not semantically part of the msg-id
474 if parent is not None:
474 if parent is not None:
475 if not parent.startswith('<'):
475 if not parent.startswith('<'):
476 parent = '<' + parent
476 parent = '<' + parent
477 if not parent.endswith('>'):
477 if not parent.endswith('>'):
478 parent += '>'
478 parent += '>'
479
479
480 first = True
480 first = True
481
481
482 sender_addr = email.Utils.parseaddr(sender)[1]
482 sender_addr = email.Utils.parseaddr(sender)[1]
483 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
483 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
484 sendmail = None
484 sendmail = None
485 for i, (m, subj, ds) in enumerate(msgs):
485 for i, (m, subj, ds) in enumerate(msgs):
486 try:
486 try:
487 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
487 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
488 except TypeError:
488 except TypeError:
489 m['Message-Id'] = genmsgid('patchbomb')
489 m['Message-Id'] = genmsgid('patchbomb')
490 if parent:
490 if parent:
491 m['In-Reply-To'] = parent
491 m['In-Reply-To'] = parent
492 m['References'] = parent
492 m['References'] = parent
493 if first:
493 if first:
494 parent = m['Message-Id']
494 parent = m['Message-Id']
495 first = False
495 first = False
496
496
497 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
497 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
498 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
498 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
499
499
500 start_time = (start_time[0] + 1, start_time[1])
500 start_time = (start_time[0] + 1, start_time[1])
501 m['From'] = sender
501 m['From'] = sender
502 m['To'] = ', '.join(to)
502 m['To'] = ', '.join(to)
503 if cc:
503 if cc:
504 m['Cc'] = ', '.join(cc)
504 m['Cc'] = ', '.join(cc)
505 if bcc:
505 if bcc:
506 m['Bcc'] = ', '.join(bcc)
506 m['Bcc'] = ', '.join(bcc)
507 if replyto:
507 if replyto:
508 m['Reply-To'] = ', '.join(replyto)
508 m['Reply-To'] = ', '.join(replyto)
509 if opts.get('test'):
509 if opts.get('test'):
510 ui.status(_('Displaying '), subj, ' ...\n')
510 ui.status(_('Displaying '), subj, ' ...\n')
511 ui.flush()
511 ui.flush()
512 if 'PAGER' in os.environ and not ui.plain():
512 if 'PAGER' in os.environ and not ui.plain():
513 fp = util.popen(os.environ['PAGER'], 'w')
513 fp = util.popen(os.environ['PAGER'], 'w')
514 else:
514 else:
515 fp = ui
515 fp = ui
516 generator = email.Generator.Generator(fp, mangle_from_=False)
516 generator = email.Generator.Generator(fp, mangle_from_=False)
517 try:
517 try:
518 generator.flatten(m, 0)
518 generator.flatten(m, 0)
519 fp.write('\n')
519 fp.write('\n')
520 except IOError, inst:
520 except IOError, inst:
521 if inst.errno != errno.EPIPE:
521 if inst.errno != errno.EPIPE:
522 raise
522 raise
523 if fp is not ui:
523 if fp is not ui:
524 fp.close()
524 fp.close()
525 elif mbox:
525 elif mbox:
526 ui.status(_('Writing '), subj, ' ...\n')
526 ui.status(_('Writing '), subj, ' ...\n')
527 ui.progress(_('writing'), i, item=subj, total=len(msgs))
527 ui.progress(_('writing'), i, item=subj, total=len(msgs))
528 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
528 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
529 generator = email.Generator.Generator(fp, mangle_from_=True)
529 generator = email.Generator.Generator(fp, mangle_from_=True)
530 # Should be time.asctime(), but Windows prints 2-characters day
530 # Should be time.asctime(), but Windows prints 2-characters day
531 # of month instead of one. Make them print the same thing.
531 # of month instead of one. Make them print the same thing.
532 date = time.strftime('%a %b %d %H:%M:%S %Y',
532 date = time.strftime('%a %b %d %H:%M:%S %Y',
533 time.localtime(start_time[0]))
533 time.localtime(start_time[0]))
534 fp.write('From %s %s\n' % (sender_addr, date))
534 fp.write('From %s %s\n' % (sender_addr, date))
535 generator.flatten(m, 0)
535 generator.flatten(m, 0)
536 fp.write('\n\n')
536 fp.write('\n\n')
537 fp.close()
537 fp.close()
538 else:
538 else:
539 if not sendmail:
539 if not sendmail:
540 sendmail = mail.connect(ui)
540 sendmail = mail.connect(ui)
541 ui.status(_('Sending '), subj, ' ...\n')
541 ui.status(_('Sending '), subj, ' ...\n')
542 ui.progress(_('sending'), i, item=subj, total=len(msgs))
542 ui.progress(_('sending'), i, item=subj, total=len(msgs))
543 # Exim does not remove the Bcc field
543 # Exim does not remove the Bcc field
544 del m['Bcc']
544 del m['Bcc']
545 fp = cStringIO.StringIO()
545 fp = cStringIO.StringIO()
546 generator = email.Generator.Generator(fp, mangle_from_=False)
546 generator = email.Generator.Generator(fp, mangle_from_=False)
547 generator.flatten(m, 0)
547 generator.flatten(m, 0)
548 sendmail(sender, to + bcc + cc, fp.getvalue())
548 sendmail(sender, to + bcc + cc, fp.getvalue())
549
549
550 ui.progress(_('writing'), None)
550 ui.progress(_('writing'), None)
551 ui.progress(_('sending'), None)
551 ui.progress(_('sending'), None)
General Comments 0
You need to be logged in to leave comments. Login now