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