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