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