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