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