##// END OF EJS Templates
patchbomb: remove unneeded line continuation
Christian Ebert -
r12064:6449be7b default
parent child Browse files
Show More
@@ -1,539 +1,538 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 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, discovery
79 from mercurial import cmdutil, commands, hg, mail, patch, util, discovery
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
190 By default, diffs are sent in the format generated by
191 :hg:`export`, one per message. The series starts with a "[PATCH 0
191 :hg:`export`, one per message. The series starts with a "[PATCH 0
192 of N]" introduction, which describes the series as a whole.
192 of N]" 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 bundle = opts.get('bundle')
238 bundle = opts.get('bundle')
239 date = opts.get('date')
239 date = opts.get('date')
240 mbox = opts.get('mbox')
240 mbox = opts.get('mbox')
241 outgoing = opts.get('outgoing')
241 outgoing = opts.get('outgoing')
242 rev = opts.get('rev')
242 rev = opts.get('rev')
243 # internal option used by pbranches
243 # internal option used by pbranches
244 patches = opts.get('patches')
244 patches = opts.get('patches')
245
245
246 def getoutgoing(dest, revs):
246 def getoutgoing(dest, revs):
247 '''Return the revisions present locally but not in dest'''
247 '''Return the revisions present locally but not in dest'''
248 dest = ui.expandpath(dest or 'default-push', dest or 'default')
248 dest = ui.expandpath(dest or 'default-push', dest or 'default')
249 dest, branches = hg.parseurl(dest)
249 dest, branches = hg.parseurl(dest)
250 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
250 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
251 if revs:
251 if revs:
252 revs = [repo.lookup(rev) for rev in revs]
252 revs = [repo.lookup(rev) for rev in revs]
253 other = hg.repository(hg.remoteui(repo, opts), dest)
253 other = hg.repository(hg.remoteui(repo, opts), dest)
254 ui.status(_('comparing with %s\n') % dest)
254 ui.status(_('comparing with %s\n') % dest)
255 o = discovery.findoutgoing(repo, other)
255 o = discovery.findoutgoing(repo, other)
256 if not o:
256 if not o:
257 ui.status(_("no changes found\n"))
257 ui.status(_("no changes found\n"))
258 return []
258 return []
259 o = repo.changelog.nodesbetween(o, revs)[0]
259 o = repo.changelog.nodesbetween(o, revs)[0]
260 return [str(repo.changelog.rev(r)) for r in o]
260 return [str(repo.changelog.rev(r)) for r in o]
261
261
262 def getpatches(revs):
262 def getpatches(revs):
263 for r in cmdutil.revrange(repo, revs):
263 for r in cmdutil.revrange(repo, revs):
264 output = cStringIO.StringIO()
264 output = cStringIO.StringIO()
265 cmdutil.export(repo, [r], fp=output,
265 cmdutil.export(repo, [r], fp=output,
266 opts=patch.diffopts(ui, opts))
266 opts=patch.diffopts(ui, opts))
267 yield output.getvalue().split('\n')
267 yield output.getvalue().split('\n')
268
268
269 def getbundle(dest):
269 def getbundle(dest):
270 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
270 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
271 tmpfn = os.path.join(tmpdir, 'bundle')
271 tmpfn = os.path.join(tmpdir, 'bundle')
272 try:
272 try:
273 commands.bundle(ui, repo, tmpfn, dest, **opts)
273 commands.bundle(ui, repo, tmpfn, dest, **opts)
274 return open(tmpfn, 'rb').read()
274 return open(tmpfn, 'rb').read()
275 finally:
275 finally:
276 try:
276 try:
277 os.unlink(tmpfn)
277 os.unlink(tmpfn)
278 except:
278 except:
279 pass
279 pass
280 os.rmdir(tmpdir)
280 os.rmdir(tmpdir)
281
281
282 if not (opts.get('test') or mbox):
282 if not (opts.get('test') or mbox):
283 # really sending
283 # really sending
284 mail.validateconfig(ui)
284 mail.validateconfig(ui)
285
285
286 if not (revs or rev or outgoing or bundle or patches):
286 if not (revs or rev or outgoing or bundle or patches):
287 raise util.Abort(_('specify at least one changeset with -r or -o'))
287 raise util.Abort(_('specify at least one changeset with -r or -o'))
288
288
289 if outgoing and bundle:
289 if outgoing and bundle:
290 raise util.Abort(_("--outgoing mode always on with --bundle;"
290 raise util.Abort(_("--outgoing mode always on with --bundle;"
291 " do not re-specify --outgoing"))
291 " do not re-specify --outgoing"))
292
292
293 if outgoing or bundle:
293 if outgoing or bundle:
294 if len(revs) > 1:
294 if len(revs) > 1:
295 raise util.Abort(_("too many destinations"))
295 raise util.Abort(_("too many destinations"))
296 dest = revs and revs[0] or None
296 dest = revs and revs[0] or None
297 revs = []
297 revs = []
298
298
299 if rev:
299 if rev:
300 if revs:
300 if revs:
301 raise util.Abort(_('use only one form to specify the revision'))
301 raise util.Abort(_('use only one form to specify the revision'))
302 revs = rev
302 revs = rev
303
303
304 if outgoing:
304 if outgoing:
305 revs = getoutgoing(dest, rev)
305 revs = getoutgoing(dest, rev)
306 if bundle:
306 if bundle:
307 opts['revs'] = revs
307 opts['revs'] = revs
308
308
309 # start
309 # start
310 if date:
310 if date:
311 start_time = util.parsedate(date)
311 start_time = util.parsedate(date)
312 else:
312 else:
313 start_time = util.makedate()
313 start_time = util.makedate()
314
314
315 def genmsgid(id):
315 def genmsgid(id):
316 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
316 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
317
317
318 def getdescription(body, sender):
318 def getdescription(body, sender):
319 if opts.get('desc'):
319 if opts.get('desc'):
320 body = open(opts.get('desc')).read()
320 body = open(opts.get('desc')).read()
321 else:
321 else:
322 ui.write(_('\nWrite the introductory message for the '
322 ui.write(_('\nWrite the introductory message for the '
323 'patch series.\n\n'))
323 'patch series.\n\n'))
324 body = ui.edit(body, sender)
324 body = ui.edit(body, sender)
325 return body
325 return body
326
326
327 def getpatchmsgs(patches, patchnames=None):
327 def getpatchmsgs(patches, patchnames=None):
328 jumbo = []
328 jumbo = []
329 msgs = []
329 msgs = []
330
330
331 ui.write(_('This patch series consists of %d patches.\n\n')
331 ui.write(_('This patch series consists of %d patches.\n\n')
332 % len(patches))
332 % len(patches))
333
333
334 name = None
334 name = None
335 for i, p in enumerate(patches):
335 for i, p in enumerate(patches):
336 jumbo.extend(p)
336 jumbo.extend(p)
337 if patchnames:
337 if patchnames:
338 name = patchnames[i]
338 name = patchnames[i]
339 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
339 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
340 len(patches), name)
340 len(patches), name)
341 msgs.append(msg)
341 msgs.append(msg)
342
342
343 if introneeded(opts, len(patches)):
343 if introneeded(opts, len(patches)):
344 tlen = len(str(len(patches)))
344 tlen = len(str(len(patches)))
345
345
346 flag = ' '.join(opts.get('flag'))
346 flag = ' '.join(opts.get('flag'))
347 if flag:
347 if flag:
348 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
348 subj = '[PATCH %0*d of %d %s]' % (tlen, 0, len(patches), flag)
349 else:
349 else:
350 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
350 subj = '[PATCH %0*d of %d]' % (tlen, 0, len(patches))
351 subj += ' ' + (opts.get('subject') or
351 subj += ' ' + (opts.get('subject') or
352 prompt(ui, 'Subject: ', rest=subj))
352 prompt(ui, 'Subject: ', rest=subj))
353
353
354 body = ''
354 body = ''
355 if opts.get('diffstat'):
355 if opts.get('diffstat'):
356 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
356 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
357 if d:
357 if d:
358 body = '\n' + d
358 body = '\n' + d
359
359
360 body = getdescription(body, sender)
360 body = getdescription(body, sender)
361 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
361 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
362 msg['Subject'] = mail.headencode(ui, subj, _charsets,
362 msg['Subject'] = mail.headencode(ui, subj, _charsets,
363 opts.get('test'))
363 opts.get('test'))
364
364
365 msgs.insert(0, (msg, subj))
365 msgs.insert(0, (msg, subj))
366 return msgs
366 return msgs
367
367
368 def getbundlemsgs(bundle):
368 def getbundlemsgs(bundle):
369 subj = (opts.get('subject')
369 subj = (opts.get('subject')
370 or prompt(ui, 'Subject:', 'A bundle for your repository'))
370 or prompt(ui, 'Subject:', 'A bundle for your repository'))
371
371
372 body = getdescription('', sender)
372 body = getdescription('', sender)
373 msg = email.MIMEMultipart.MIMEMultipart()
373 msg = email.MIMEMultipart.MIMEMultipart()
374 if body:
374 if body:
375 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
375 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
376 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
376 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
377 datapart.set_payload(bundle)
377 datapart.set_payload(bundle)
378 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
378 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
379 datapart.add_header('Content-Disposition', 'attachment',
379 datapart.add_header('Content-Disposition', 'attachment',
380 filename=bundlename)
380 filename=bundlename)
381 email.Encoders.encode_base64(datapart)
381 email.Encoders.encode_base64(datapart)
382 msg.attach(datapart)
382 msg.attach(datapart)
383 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
383 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
384 return [(msg, subj)]
384 return [(msg, subj)]
385
385
386 sender = (opts.get('from') or ui.config('email', 'from') or
386 sender = (opts.get('from') or ui.config('email', 'from') or
387 ui.config('patchbomb', 'from') or
387 ui.config('patchbomb', 'from') or
388 prompt(ui, 'From', ui.username()))
388 prompt(ui, 'From', ui.username()))
389
389
390 if patches:
390 if patches:
391 msgs = getpatchmsgs(patches, opts.get('patchnames'))
391 msgs = getpatchmsgs(patches, opts.get('patchnames'))
392 elif bundle:
392 elif bundle:
393 msgs = getbundlemsgs(getbundle(dest))
393 msgs = getbundlemsgs(getbundle(dest))
394 else:
394 else:
395 msgs = getpatchmsgs(list(getpatches(revs)))
395 msgs = getpatchmsgs(list(getpatches(revs)))
396
396
397 def getaddrs(opt, prpt=None, default=None):
397 def getaddrs(opt, prpt=None, default=None):
398 addrs = opts.get(opt.replace('-', '_'))
398 addrs = opts.get(opt.replace('-', '_'))
399 if addrs:
399 if addrs:
400 return mail.addrlistencode(ui, addrs, _charsets,
400 return mail.addrlistencode(ui, addrs, _charsets,
401 opts.get('test'))
401 opts.get('test'))
402
402
403 addrs = (ui.config('email', opt) or
403 addrs = ui.config('email', opt) or ui.config('patchbomb', opt) or ''
404 ui.config('patchbomb', opt) or '')
405 if not addrs and prpt:
404 if not addrs and prpt:
406 addrs = prompt(ui, prpt, default)
405 addrs = prompt(ui, prpt, default)
407
406
408 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
407 return mail.addrlistencode(ui, [addrs], _charsets, opts.get('test'))
409
408
410 to = getaddrs('to', 'To')
409 to = getaddrs('to', 'To')
411 cc = getaddrs('cc', 'Cc', '')
410 cc = getaddrs('cc', 'Cc', '')
412 bcc = getaddrs('bcc')
411 bcc = getaddrs('bcc')
413 replyto = getaddrs('reply-to')
412 replyto = getaddrs('reply-to')
414
413
415 ui.write('\n')
414 ui.write('\n')
416
415
417 parent = opts.get('in_reply_to') or None
416 parent = opts.get('in_reply_to') or None
418 # angle brackets may be omitted, they're not semantically part of the msg-id
417 # angle brackets may be omitted, they're not semantically part of the msg-id
419 if parent is not None:
418 if parent is not None:
420 if not parent.startswith('<'):
419 if not parent.startswith('<'):
421 parent = '<' + parent
420 parent = '<' + parent
422 if not parent.endswith('>'):
421 if not parent.endswith('>'):
423 parent += '>'
422 parent += '>'
424
423
425 first = True
424 first = True
426
425
427 sender_addr = email.Utils.parseaddr(sender)[1]
426 sender_addr = email.Utils.parseaddr(sender)[1]
428 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
427 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
429 sendmail = None
428 sendmail = None
430 for m, subj in msgs:
429 for m, subj in msgs:
431 try:
430 try:
432 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
431 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
433 except TypeError:
432 except TypeError:
434 m['Message-Id'] = genmsgid('patchbomb')
433 m['Message-Id'] = genmsgid('patchbomb')
435 if parent:
434 if parent:
436 m['In-Reply-To'] = parent
435 m['In-Reply-To'] = parent
437 m['References'] = parent
436 m['References'] = parent
438 if first:
437 if first:
439 parent = m['Message-Id']
438 parent = m['Message-Id']
440 first = False
439 first = False
441
440
442 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
441 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
443 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
442 m['Date'] = email.Utils.formatdate(start_time[0], localtime=True)
444
443
445 start_time = (start_time[0] + 1, start_time[1])
444 start_time = (start_time[0] + 1, start_time[1])
446 m['From'] = sender
445 m['From'] = sender
447 m['To'] = ', '.join(to)
446 m['To'] = ', '.join(to)
448 if cc:
447 if cc:
449 m['Cc'] = ', '.join(cc)
448 m['Cc'] = ', '.join(cc)
450 if bcc:
449 if bcc:
451 m['Bcc'] = ', '.join(bcc)
450 m['Bcc'] = ', '.join(bcc)
452 if replyto:
451 if replyto:
453 m['Reply-To'] = ', '.join(replyto)
452 m['Reply-To'] = ', '.join(replyto)
454 if opts.get('test'):
453 if opts.get('test'):
455 ui.status(_('Displaying '), subj, ' ...\n')
454 ui.status(_('Displaying '), subj, ' ...\n')
456 ui.flush()
455 ui.flush()
457 if 'PAGER' in os.environ and not ui.plain():
456 if 'PAGER' in os.environ and not ui.plain():
458 fp = util.popen(os.environ['PAGER'], 'w')
457 fp = util.popen(os.environ['PAGER'], 'w')
459 else:
458 else:
460 fp = ui
459 fp = ui
461 generator = email.Generator.Generator(fp, mangle_from_=False)
460 generator = email.Generator.Generator(fp, mangle_from_=False)
462 try:
461 try:
463 generator.flatten(m, 0)
462 generator.flatten(m, 0)
464 fp.write('\n')
463 fp.write('\n')
465 except IOError, inst:
464 except IOError, inst:
466 if inst.errno != errno.EPIPE:
465 if inst.errno != errno.EPIPE:
467 raise
466 raise
468 if fp is not ui:
467 if fp is not ui:
469 fp.close()
468 fp.close()
470 elif mbox:
469 elif mbox:
471 ui.status(_('Writing '), subj, ' ...\n')
470 ui.status(_('Writing '), subj, ' ...\n')
472 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
471 fp = open(mbox, 'In-Reply-To' in m and 'ab+' or 'wb+')
473 generator = email.Generator.Generator(fp, mangle_from_=True)
472 generator = email.Generator.Generator(fp, mangle_from_=True)
474 # Should be time.asctime(), but Windows prints 2-characters day
473 # Should be time.asctime(), but Windows prints 2-characters day
475 # of month instead of one. Make them print the same thing.
474 # of month instead of one. Make them print the same thing.
476 date = time.strftime('%a %b %d %H:%M:%S %Y',
475 date = time.strftime('%a %b %d %H:%M:%S %Y',
477 time.localtime(start_time[0]))
476 time.localtime(start_time[0]))
478 fp.write('From %s %s\n' % (sender_addr, date))
477 fp.write('From %s %s\n' % (sender_addr, date))
479 generator.flatten(m, 0)
478 generator.flatten(m, 0)
480 fp.write('\n\n')
479 fp.write('\n\n')
481 fp.close()
480 fp.close()
482 else:
481 else:
483 if not sendmail:
482 if not sendmail:
484 sendmail = mail.connect(ui)
483 sendmail = mail.connect(ui)
485 ui.status(_('Sending '), subj, ' ...\n')
484 ui.status(_('Sending '), subj, ' ...\n')
486 # Exim does not remove the Bcc field
485 # Exim does not remove the Bcc field
487 del m['Bcc']
486 del m['Bcc']
488 fp = cStringIO.StringIO()
487 fp = cStringIO.StringIO()
489 generator = email.Generator.Generator(fp, mangle_from_=False)
488 generator = email.Generator.Generator(fp, mangle_from_=False)
490 generator.flatten(m, 0)
489 generator.flatten(m, 0)
491 sendmail(sender, to + bcc + cc, fp.getvalue())
490 sendmail(sender, to + bcc + cc, fp.getvalue())
492
491
493 emailopts = [
492 emailopts = [
494 ('a', 'attach', None, _('send patches as attachments')),
493 ('a', 'attach', None, _('send patches as attachments')),
495 ('i', 'inline', None, _('send patches as inline attachments')),
494 ('i', 'inline', None, _('send patches as inline attachments')),
496 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
495 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
497 ('c', 'cc', [], _('email addresses of copy recipients')),
496 ('c', 'cc', [], _('email addresses of copy recipients')),
498 ('d', 'diffstat', None, _('add diffstat output to messages')),
497 ('d', 'diffstat', None, _('add diffstat output to messages')),
499 ('', 'date', '', _('use the given date as the sending date')),
498 ('', 'date', '', _('use the given date as the sending date')),
500 ('', 'desc', '', _('use the given file as the series description')),
499 ('', 'desc', '', _('use the given file as the series description')),
501 ('f', 'from', '', _('email address of sender')),
500 ('f', 'from', '', _('email address of sender')),
502 ('n', 'test', None, _('print messages that would be sent')),
501 ('n', 'test', None, _('print messages that would be sent')),
503 ('m', 'mbox', '',
502 ('m', 'mbox', '',
504 _('write messages to mbox file instead of sending them')),
503 _('write messages to mbox file instead of sending them')),
505 ('', 'reply-to', [], _('email addresses replies should be sent to')),
504 ('', 'reply-to', [], _('email addresses replies should be sent to')),
506 ('s', 'subject', '',
505 ('s', 'subject', '',
507 _('subject of first message (intro or single patch)')),
506 _('subject of first message (intro or single patch)')),
508 ('', 'in-reply-to', '',
507 ('', 'in-reply-to', '',
509 _('message identifier to reply to')),
508 _('message identifier to reply to')),
510 ('', 'flag', [], _('flags to add in subject prefixes')),
509 ('', 'flag', [], _('flags to add in subject prefixes')),
511 ('t', 'to', [], _('email addresses of recipients')),
510 ('t', 'to', [], _('email addresses of recipients')),
512 ]
511 ]
513
512
514
513
515 cmdtable = {
514 cmdtable = {
516 "email":
515 "email":
517 (patchbomb,
516 (patchbomb,
518 [('g', 'git', None, _('use git extended diff format')),
517 [('g', 'git', None, _('use git extended diff format')),
519 ('', 'plain', None, _('omit hg patch header')),
518 ('', 'plain', None, _('omit hg patch header')),
520 ('o', 'outgoing', None,
519 ('o', 'outgoing', None,
521 _('send changes not found in the target repository')),
520 _('send changes not found in the target repository')),
522 ('b', 'bundle', None,
521 ('b', 'bundle', None,
523 _('send changes not in target as a binary bundle')),
522 _('send changes not in target as a binary bundle')),
524 ('', 'bundlename', 'bundle',
523 ('', 'bundlename', 'bundle',
525 _('name of the bundle attachment file'), _('NAME')),
524 _('name of the bundle attachment file'), _('NAME')),
526 ('r', 'rev', [],
525 ('r', 'rev', [],
527 _('a revision to send'), _('REV')),
526 _('a revision to send'), _('REV')),
528 ('', 'force', None,
527 ('', 'force', None,
529 _('run even when remote repository is unrelated '
528 _('run even when remote repository is unrelated '
530 '(with -b/--bundle)')),
529 '(with -b/--bundle)')),
531 ('', 'base', [],
530 ('', 'base', [],
532 _('a base changeset to specify instead of a destination '
531 _('a base changeset to specify instead of a destination '
533 '(with -b/--bundle)'),
532 '(with -b/--bundle)'),
534 _('REV')),
533 _('REV')),
535 ('', 'intro', None,
534 ('', 'intro', None,
536 _('send an introduction email for a single patch')),
535 _('send an introduction email for a single patch')),
537 ] + emailopts + commands.remoteopts,
536 ] + emailopts + commands.remoteopts,
538 _('hg email [OPTION]... [DEST]...'))
537 _('hg email [OPTION]... [DEST]...'))
539 }
538 }
General Comments 0
You need to be logged in to leave comments. Login now