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