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