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