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