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