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