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