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