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