patchbomb.py
762 lines
| 28.0 KiB
| text/x-python
|
PythonLexer
/ hgext / patchbomb.py
|
r8252 | # patchbomb.py - sending Mercurial changesets as patch emails | ||
# | ||||
# Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r8252 | |||
|
r8935 | '''command to send changesets as (a series of) patch emails | ||
|
r6666 | |||
|
r7997 | The series is started off with a "[PATCH 0 of N]" introduction, which | ||
describes the series as a whole. | ||||
|
r6666 | |||
|
r9269 | Each patch email has a Subject line of "[PATCH M of N] ...", using the | ||
first line of the changeset description as the subject text. The | ||||
message contains two or three body parts: | ||||
|
r6666 | |||
|
r9300 | - The changeset description. | ||
- [Optional] The result of running diffstat on the patch. | ||||
|
r10973 | - The patch itself, as generated by :hg:`export`. | ||
|
r6666 | |||
|
r9269 | Each message refers to the first in the series using the In-Reply-To | ||
and References headers, so they will show up as a sequence in threaded | ||||
mail and news readers, and in mail archives. | ||||
|
r6666 | |||
|
r13838 | To configure other defaults, add a section like this to your | ||
configuration file:: | ||||
|
r6666 | |||
[email] | ||||
from = My Name <my@email> | ||||
to = recipient1, recipient2, ... | ||||
cc = cc1, cc2, ... | ||||
bcc = bcc1, bcc2, ... | ||||
|
r11150 | reply-to = address1, address2, ... | ||
|
r6666 | |||
|
r10284 | Use ``[patchbomb]`` as configuration section name if you need to | ||
override global ``[email]`` address settings. | ||||
|
r10973 | Then you can use the :hg:`email` command to mail a series of | ||
changesets as a patchbomb. | ||||
|
r6666 | |||
|
r9269 | You can also either configure the method option in the email section | ||
to be a sendmail compatible mailer or fill out the [smtp] section so | ||||
that the patchbomb extension can automatically send patchbombs | ||||
directly from the commandline. See the [email] and [smtp] sections in | ||||
hgrc(5) for details. | ||||
|
r23487 | |||
|
r27697 | By default, :hg:`email` will prompt for a ``To`` or ``CC`` header if | ||
you do not supply one via configuration or the command line. You can | ||||
override this to never prompt by configuring an empty value:: | ||||
[email] | ||||
cc = | ||||
|
r23487 | You can control the default inclusion of an introduction message with the | ||
``patchbomb.intro`` configuration option. The configuration is always | ||||
overwritten by command line flags like --intro and --desc:: | ||||
[patchbomb] | ||||
intro=auto # include introduction message if more than 1 patch (default) | ||||
intro=never # never include an introduction message | ||||
intro=always # always include an introduction message | ||||
|
r23488 | |||
|
r31187 | You can specify a template for flags to be added in subject prefixes. Flags | ||
specified by --flag option are exported as ``{flags}`` keyword:: | ||||
[patchbomb] | ||||
flagtemplate = "{separate(' ', | ||||
ifeq(branch, 'default', '', branch|upper), | ||||
flags)}" | ||||
|
r23488 | You can set patchbomb to always ask for confirmation by setting | ||
``patchbomb.confirm`` to true. | ||||
|
r9071 | ''' | ||
|
r28415 | from __future__ import absolute_import | ||
|
r1669 | |||
|
r27150 | import email as emailmod | ||
|
r28415 | import errno | ||
import os | ||||
import socket | ||||
import tempfile | ||||
|
r19810 | |||
|
r29205 | from mercurial.i18n import _ | ||
|
r28415 | from mercurial import ( | ||
cmdutil, | ||||
commands, | ||||
error, | ||||
|
r31187 | formatter, | ||
|
r28415 | hg, | ||
mail, | ||||
node as nodemod, | ||||
patch, | ||||
|
r32337 | registrar, | ||
|
r32639 | repair, | ||
|
r28415 | scmutil, | ||
|
r31187 | templater, | ||
|
r28415 | util, | ||
) | ||||
|
r28861 | stringio = util.stringio | ||
|
r1669 | |||
|
r14309 | cmdtable = {} | ||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r29841 | testedwith = 'ships-with-hg-core' | ||
|
r14309 | |||
|
r26546 | def _addpullheader(seq, ctx): | ||
"""Add a header pointing to a public URL where the changeset is available | ||||
""" | ||||
repo = ctx.repo() | ||||
# experimental config: patchbomb.publicurl | ||||
# waiting for some logic that check that the changeset are available on the | ||||
# destination before patchbombing anything. | ||||
|
r32824 | publicurl = repo.ui.config('patchbomb', 'publicurl') | ||
|
r32825 | if publicurl: | ||
|
r26546 | return ('Available At %s\n' | ||
|
r32824 | '# hg pull %s -r %s' % (publicurl, publicurl, ctx)) | ||
|
r26546 | return None | ||
def uisetup(ui): | ||||
cmdutil.extraexport.append('pullurl') | ||||
cmdutil.extraexportmap['pullurl'] = _addpullheader | ||||
|
r33436 | def reposetup(ui, repo): | ||
if not repo.local(): | ||||
return | ||||
repo._wlockfreeprefix.add('last-email.txt') | ||||
|
r26546 | |||
|
r9648 | def prompt(ui, prompt, default=None, rest=':'): | ||
|
r7354 | if default: | ||
prompt += ' [%s]' % default | ||||
|
r15166 | return ui.prompt(prompt + rest, default) | ||
|
r7354 | |||
|
r23487 | def introwanted(ui, opts, number): | ||
|
r15164 | '''is an introductory message apparently wanted?''' | ||
|
r23487 | introconfig = ui.config('patchbomb', 'intro', 'auto') | ||
if opts.get('intro') or opts.get('desc'): | ||||
intro = True | ||||
elif introconfig == 'always': | ||||
intro = True | ||||
elif introconfig == 'never': | ||||
intro = False | ||||
elif introconfig == 'auto': | ||||
intro = 1 < number | ||||
else: | ||||
ui.write_err(_('warning: invalid patchbomb.intro value "%s"\n') | ||||
% introconfig) | ||||
ui.write_err(_('(should be one of always, never, auto)\n')) | ||||
intro = 1 < number | ||||
return intro | ||||
|
r10734 | |||
|
r31187 | def _formatflags(ui, repo, rev, flags): | ||
"""build flag string optionally by template""" | ||||
tmpl = ui.config('patchbomb', 'flagtemplate') | ||||
if not tmpl: | ||||
return ' '.join(flags) | ||||
out = util.stringio() | ||||
opts = {'template': templater.unquotestring(tmpl)} | ||||
with formatter.templateformatter(ui, out, 'patchbombflag', opts) as fm: | ||||
fm.startitem() | ||||
fm.context(ctx=repo[rev]) | ||||
fm.write('flags', '%s', fm.formatlist(flags, name='flag')) | ||||
return out.getvalue() | ||||
|
r31186 | def _formatprefix(ui, repo, rev, flags, idx, total, numbered): | ||
|
r31183 | """build prefix to patch subject""" | ||
|
r31187 | flag = _formatflags(ui, repo, rev, flags) | ||
|
r31183 | if flag: | ||
flag = ' ' + flag | ||||
if not numbered: | ||||
return '[PATCH%s]' % flag | ||||
else: | ||||
tlen = len(str(total)) | ||||
return '[PATCH %0*d of %d%s]' % (tlen, idx, total, flag) | ||||
|
r31186 | def makepatch(ui, repo, rev, patchlines, opts, _charsets, idx, total, numbered, | ||
|
r12199 | patchname=None): | ||
|
r7354 | |||
desc = [] | ||||
node = None | ||||
body = '' | ||||
|
r12199 | for line in patchlines: | ||
|
r7354 | if line.startswith('#'): | ||
if line.startswith('# Node ID'): | ||||
node = line.split()[-1] | ||||
continue | ||||
if line.startswith('diff -r') or line.startswith('diff --git'): | ||||
break | ||||
desc.append(line) | ||||
if not patchname and not node: | ||||
raise ValueError | ||||
|
r16307 | if opts.get('attach') and not opts.get('body'): | ||
|
r7354 | body = ('\n'.join(desc[1:]).strip() or | ||
'Patch subject is complete summary.') | ||||
body += '\n\n\n' | ||||
if opts.get('plain'): | ||||
|
r12199 | while patchlines and patchlines[0].startswith('# '): | ||
patchlines.pop(0) | ||||
if patchlines: | ||||
patchlines.pop(0) | ||||
while patchlines and not patchlines[0].strip(): | ||||
patchlines.pop(0) | ||||
|
r7354 | |||
|
r30407 | ds = patch.diffstat(patchlines) | ||
|
r7354 | if opts.get('diffstat'): | ||
|
r12200 | body += ds + '\n\n' | ||
|
r7354 | |||
|
r16307 | addattachment = opts.get('attach') or opts.get('inline') | ||
if not addattachment or opts.get('body'): | ||||
body += '\n'.join(patchlines) | ||||
if addattachment: | ||||
|
r27150 | msg = emailmod.MIMEMultipart.MIMEMultipart() | ||
|
r7354 | if body: | ||
msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test'))) | ||||
|
r16683 | p = mail.mimetextpatch('\n'.join(patchlines), 'x-patch', | ||
opts.get('test')) | ||||
|
r28415 | binnode = nodemod.bin(node) | ||
|
r8761 | # if node is mq patch, it will have the patch file's name as a tag | ||
|
r7354 | if not patchname: | ||
patchtags = [t for t in repo.nodetags(binnode) | ||||
if t.endswith('.patch') or t.endswith('.diff')] | ||||
if patchtags: | ||||
patchname = patchtags[0] | ||||
elif total > 1: | ||||
|
r14290 | patchname = cmdutil.makefilename(repo, '%b-%n.patch', | ||
|
r16683 | binnode, seqno=idx, | ||
total=total) | ||||
|
r7354 | else: | ||
|
r14290 | patchname = cmdutil.makefilename(repo, '%b.patch', binnode) | ||
|
r7354 | disposition = 'inline' | ||
if opts.get('attach'): | ||||
disposition = 'attachment' | ||||
p['Content-Disposition'] = disposition + '; filename=' + patchname | ||||
msg.attach(p) | ||||
else: | ||||
msg = mail.mimetextpatch(body, display=opts.get('test')) | ||||
|
r31186 | prefix = _formatprefix(ui, repo, rev, opts.get('flag'), idx, total, | ||
numbered) | ||||
|
r7354 | subj = desc[0].strip().rstrip('. ') | ||
|
r15164 | if not numbered: | ||
|
r31183 | subj = ' '.join([prefix, opts.get('subject') or subj]) | ||
|
r7354 | else: | ||
|
r31183 | subj = ' '.join([prefix, subj]) | ||
|
r7354 | msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) | ||
msg['X-Mercurial-Node'] = node | ||||
|
r21282 | msg['X-Mercurial-Series-Index'] = '%i' % idx | ||
msg['X-Mercurial-Series-Total'] = '%i' % total | ||||
|
r12200 | return msg, subj, ds | ||
|
r7354 | |||
|
r23210 | def _getpatches(repo, revs, **opts): | ||
"""return a list of patches for a list of revisions | ||||
Each patch in the list is itself a list of lines. | ||||
""" | ||||
ui = repo.ui | ||||
prev = repo['.'].rev() | ||||
|
r24568 | for r in revs: | ||
|
r23210 | if r == prev and (repo[None].files() or repo[None].deleted()): | ||
ui.warn(_('warning: working directory has ' | ||||
'uncommitted changes\n')) | ||||
|
r28861 | output = stringio() | ||
|
r23210 | cmdutil.export(repo, [r], fp=output, | ||
|
r23450 | opts=patch.difffeatureopts(ui, opts, git=True)) | ||
|
r23210 | yield output.getvalue().split('\n') | ||
|
r23211 | def _getbundle(repo, dest, **opts): | ||
"""return a bundle containing changesets missing in "dest" | ||||
The `opts` keyword-arguments are the same as the one accepted by the | ||||
`bundle` command. | ||||
The bundle is a returned as a single in-memory binary blob. | ||||
""" | ||||
ui = repo.ui | ||||
tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-') | ||||
tmpfn = os.path.join(tmpdir, 'bundle') | ||||
|
r26563 | btype = ui.config('patchbomb', 'bundletype') | ||
if btype: | ||||
opts['type'] = btype | ||||
|
r23211 | try: | ||
commands.bundle(ui, repo, tmpfn, dest, **opts) | ||||
|
r27767 | return util.readfile(tmpfn) | ||
|
r23211 | finally: | ||
try: | ||||
os.unlink(tmpfn) | ||||
except OSError: | ||||
pass | ||||
os.rmdir(tmpdir) | ||||
|
r23212 | def _getdescription(repo, defaultbody, sender, **opts): | ||
"""obtain the body of the introduction message and return it | ||||
This is also used for the body of email with an attached bundle. | ||||
The body can be obtained either from the command line option or entered by | ||||
the user through the editor. | ||||
""" | ||||
ui = repo.ui | ||||
if opts.get('desc'): | ||||
body = open(opts.get('desc')).read() | ||||
else: | ||||
ui.write(_('\nWrite the introductory message for the ' | ||||
'patch series.\n\n')) | ||||
|
r30848 | body = ui.edit(defaultbody, sender, repopath=repo.path) | ||
|
r23212 | # Save series description in case sendmail fails | ||
|
r23877 | msgfile = repo.vfs('last-email.txt', 'wb') | ||
|
r23212 | msgfile.write(body) | ||
msgfile.close() | ||||
return body | ||||
|
r23211 | |||
|
r23213 | def _getbundlemsgs(repo, sender, bundle, **opts): | ||
"""Get the full email for sending a given bundle | ||||
This function returns a list of "email" tuples (subject, content, None). | ||||
The list is always one message long in that case. | ||||
""" | ||||
ui = repo.ui | ||||
_charsets = mail._charsets(ui) | ||||
subj = (opts.get('subject') | ||||
or prompt(ui, 'Subject:', 'A bundle for your repository')) | ||||
body = _getdescription(repo, '', sender, **opts) | ||||
|
r27150 | msg = emailmod.MIMEMultipart.MIMEMultipart() | ||
|
r23213 | if body: | ||
msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test'))) | ||||
|
r27150 | datapart = emailmod.MIMEBase.MIMEBase('application', 'x-mercurial-bundle') | ||
|
r23213 | datapart.set_payload(bundle) | ||
bundlename = '%s.hg' % opts.get('bundlename', 'bundle') | ||||
datapart.add_header('Content-Disposition', 'attachment', | ||||
filename=bundlename) | ||||
|
r27150 | emailmod.Encoders.encode_base64(datapart) | ||
|
r23213 | msg.attach(datapart) | ||
msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test')) | ||||
return [(msg, subj, None)] | ||||
|
r31186 | def _makeintro(repo, sender, revs, patches, **opts): | ||
|
r23214 | """make an introduction email, asking the user for content if needed | ||
email is returned as (subject, body, cumulative-diffstat)""" | ||||
ui = repo.ui | ||||
_charsets = mail._charsets(ui) | ||||
|
r31186 | # use the last revision which is likely to be a bookmarked head | ||
prefix = _formatprefix(ui, repo, revs.last(), opts.get('flag'), | ||||
0, len(patches), numbered=True) | ||||
|
r23214 | subj = (opts.get('subject') or | ||
prompt(ui, '(optional) Subject: ', rest=prefix, default='')) | ||||
if not subj: | ||||
return None # skip intro if the user doesn't bother | ||||
subj = prefix + ' ' + subj | ||||
body = '' | ||||
if opts.get('diffstat'): | ||||
# generate a cumulative diffstat of the whole patch series | ||||
diffstat = patch.diffstat(sum(patches, [])) | ||||
body = '\n' + diffstat | ||||
else: | ||||
diffstat = None | ||||
body = _getdescription(repo, body, sender, **opts) | ||||
msg = mail.mimeencode(ui, body, _charsets, opts.get('test')) | ||||
msg['Subject'] = mail.headencode(ui, subj, _charsets, | ||||
opts.get('test')) | ||||
return (msg, subj, diffstat) | ||||
|
r31185 | def _getpatchmsgs(repo, sender, revs, patchnames=None, **opts): | ||
|
r23215 | """return a list of emails from a list of patches | ||
This involves introduction message creation if necessary. | ||||
This function returns a list of "email" tuples (subject, content, None). | ||||
""" | ||||
ui = repo.ui | ||||
_charsets = mail._charsets(ui) | ||||
|
r31185 | patches = list(_getpatches(repo, revs, **opts)) | ||
|
r23215 | msgs = [] | ||
ui.write(_('this patch series consists of %d patches.\n\n') | ||||
% len(patches)) | ||||
# build the intro message, or skip it if the user declines | ||||
|
r23487 | if introwanted(ui, opts, len(patches)): | ||
|
r31186 | msg = _makeintro(repo, sender, revs, patches, **opts) | ||
|
r23215 | if msg: | ||
msgs.append(msg) | ||||
# are we going to send more than one message? | ||||
numbered = len(msgs) + len(patches) > 1 | ||||
# now generate the actual patch messages | ||||
name = None | ||||
|
r31186 | assert len(revs) == len(patches) | ||
for i, (r, p) in enumerate(zip(revs, patches)): | ||||
|
r23215 | if patchnames: | ||
name = patchnames[i] | ||||
|
r31186 | msg = makepatch(ui, repo, r, p, opts, _charsets, i + 1, | ||
|
r23215 | len(patches), numbered, name) | ||
msgs.append(msg) | ||||
return msgs | ||||
|
r23486 | def _getoutgoing(repo, dest, revs): | ||
'''Return the revisions present locally but not in dest''' | ||||
ui = repo.ui | ||||
url = ui.expandpath(dest or 'default-push', dest or 'default') | ||||
url = hg.parseurl(url)[0] | ||||
ui.status(_('comparing with %s\n') % util.hidepassword(url)) | ||||
|
r24568 | revs = [r for r in revs if r >= 0] | ||
|
r23486 | if not revs: | ||
revs = [len(repo) - 1] | ||||
revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs) | ||||
if not revs: | ||||
ui.status(_("no changes found\n")) | ||||
|
r24567 | return revs | ||
|
r23486 | |||
|
r14309 | emailopts = [ | ||
|
r16307 | ('', 'body', None, _('send patches as inline message text (default)')), | ||
|
r14309 | ('a', 'attach', None, _('send patches as attachments')), | ||
('i', 'inline', None, _('send patches as inline attachments')), | ||||
('', 'bcc', [], _('email addresses of blind carbon copy recipients')), | ||||
('c', 'cc', [], _('email addresses of copy recipients')), | ||||
('', 'confirm', None, _('ask for confirmation before sending')), | ||||
('d', 'diffstat', None, _('add diffstat output to messages')), | ||||
('', 'date', '', _('use the given date as the sending date')), | ||||
('', 'desc', '', _('use the given file as the series description')), | ||||
('f', 'from', '', _('email address of sender')), | ||||
('n', 'test', None, _('print messages that would be sent')), | ||||
('m', 'mbox', '', _('write messages to mbox file instead of sending them')), | ||||
('', 'reply-to', [], _('email addresses replies should be sent to')), | ||||
('s', 'subject', '', _('subject of first message (intro or single patch)')), | ||||
('', 'in-reply-to', '', _('message identifier to reply to')), | ||||
('', 'flag', [], _('flags to add in subject prefixes')), | ||||
('t', 'to', [], _('email addresses of recipients'))] | ||||
@command('email', | ||||
[('g', 'git', None, _('use git extended diff format')), | ||||
('', 'plain', None, _('omit hg patch header')), | ||||
('o', 'outgoing', None, | ||||
_('send changes not found in the target repository')), | ||||
('b', 'bundle', None, _('send changes not in target as a binary bundle')), | ||||
|
r32639 | ('B', 'bookmark', '', _('send changes only reachable by given bookmark')), | ||
|
r14309 | ('', 'bundlename', 'bundle', | ||
_('name of the bundle attachment file'), _('NAME')), | ||||
('r', 'rev', [], _('a revision to send'), _('REV')), | ||||
('', 'force', None, _('run even when remote repository is unrelated ' | ||||
'(with -b/--bundle)')), | ||||
('', 'base', [], _('a base changeset to specify instead of a destination ' | ||||
'(with -b/--bundle)'), _('REV')), | ||||
('', 'intro', None, _('send an introduction email for a single patch')), | ||||
|
r32375 | ] + emailopts + cmdutil.remoteopts, | ||
|
r14309 | _('hg email [OPTION]... [DEST]...')) | ||
|
r27150 | def email(ui, repo, *revs, **opts): | ||
|
r4283 | '''send changesets by email | ||
|
r1669 | |||
|
r11193 | By default, diffs are sent in the format generated by | ||
:hg:`export`, one per message. The series starts with a "[PATCH 0 | ||||
of N]" introduction, which describes the series as a whole. | ||||
|
r1672 | |||
|
r9269 | Each patch email has a Subject line of "[PATCH M of N] ...", using | ||
the first line of the changeset description as the subject text. | ||||
The message contains two or three parts. First, the changeset | ||||
|
r12749 | description. | ||
With the -d/--diffstat option, if the diffstat program is | ||||
installed, the result of running diffstat on the patch is inserted. | ||||
Finally, the patch itself, as generated by :hg:`export`. | ||||
|
r17880 | With the -d/--diffstat or --confirm options, you will be presented | ||
|
r12749 | with a final summary of all messages and asked for confirmation before | ||
the messages are sent. | ||||
|
r4262 | |||
|
r9269 | By default the patch is included as text in the email body for | ||
easy reviewing. Using the -a/--attach option will instead create | ||||
an attachment for the patch. With -i/--inline an inline attachment | ||||
|
r16307 | will be created. You can include a patch both as text in the email | ||
body and as a regular or an inline attachment by combining the | ||||
-a/--attach or -i/--inline with the --body option. | ||||
|
r8472 | |||
|
r32639 | With -B/--bookmark changesets reachable by the given bookmark are | ||
selected. | ||||
|
r9269 | With -o/--outgoing, emails will be generated for patches not found | ||
in the destination repository (or only those which are ancestors | ||||
of the specified revisions if any are provided) | ||||
|
r4280 | |||
|
r9269 | With -b/--bundle, changesets are selected as for --outgoing, but a | ||
single email containing a binary Mercurial bundle as an attachment | ||||
|
r26563 | will be sent. Use the ``patchbomb.bundletype`` config option to | ||
control the bundle type as with :hg:`bundle --type`. | ||||
|
r4280 | |||
|
r12749 | With -m/--mbox, instead of previewing each patchbomb message in a | ||
pager or sending the messages directly, it will create a UNIX | ||||
mailbox file with the patch emails. This mailbox file can be | ||||
previewed with any mail user agent which supports UNIX mbox | ||||
files. | ||||
With -n/--test, all steps will run, but mail will not be sent. | ||||
You will be prompted for an email recipient address, a subject and | ||||
an introductory message describing the patches of your patchbomb. | ||||
|
r31489 | Then when all is done, patchbomb messages are displayed. | ||
|
r12749 | |||
|
r13198 | In case email sending fails, you will find a backup of your series | ||
|
r23488 | introductory message in ``.hg/last-email.txt``. | ||
The default behavior of this command can be customized through | ||||
configuration. (See :hg:`help patchbomb` for details) | ||||
|
r13198 | |||
|
r9289 | Examples:: | ||
|
r4280 | |||
|
r9289 | hg email -r 3000 # send patch 3000 only | ||
hg email -r 3000 -r 3001 # send patches 3000 and 3001 | ||||
hg email -r 3000:3005 # send patches 3000 through 3005 | ||||
hg email 3000 # send patch 3000 (deprecated) | ||||
|
r4280 | |||
|
r9289 | hg email -o # send all patches not in default | ||
hg email -o DEST # send all patches not in DEST | ||||
hg email -o -r 3000 # send all ancestors of 3000 not in default | ||||
hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST | ||||
|
r4280 | |||
|
r32639 | hg email -B feature # send all ancestors of feature bookmark | ||
|
r9289 | hg email -b # send bundle of all patches not in default | ||
hg email -b DEST # send bundle of all patches not in DEST | ||||
hg email -b -r 3000 # bundle of all ancestors of 3000 not in default | ||||
hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST | ||||
|
r4280 | |||
|
r12749 | hg email -o -m mbox && # generate an mbox file... | ||
mutt -R -f mbox # ... and view it with mutt | ||||
hg email -o -m mbox && # generate an mbox file ... | ||||
|
r12839 | formail -s sendmail \\ # ... and use formail to send from the mbox | ||
|
r12749 | -bm -t < mbox # ... using sendmail | ||
|
r9269 | Before using this command, you will need to enable email in your | ||
hgrc. See the [email] section in hgrc(5) for details. | ||||
|
r4262 | ''' | ||
|
r7115 | _charsets = mail._charsets(ui) | ||
|
r11413 | bundle = opts.get('bundle') | ||
date = opts.get('date') | ||||
mbox = opts.get('mbox') | ||||
outgoing = opts.get('outgoing') | ||||
rev = opts.get('rev') | ||||
|
r32639 | bookmark = opts.get('bookmark') | ||
|
r11413 | |||
if not (opts.get('test') or mbox): | ||||
|
r5472 | # really sending | ||
|
r4489 | mail.validateconfig(ui) | ||
|
r32639 | if not (revs or rev or outgoing or bundle or bookmark): | ||
raise error.Abort(_('specify at least one changeset with -B, -r or -o')) | ||||
|
r4493 | |||
|
r11413 | if outgoing and bundle: | ||
|
r26587 | raise error.Abort(_("--outgoing mode always on with --bundle;" | ||
|
r5746 | " do not re-specify --outgoing")) | ||
|
r32695 | if rev and bookmark: | ||
raise error.Abort(_("-r and -B are mutually exclusive")) | ||||
|
r4278 | |||
|
r11413 | if outgoing or bundle: | ||
|
r4262 | if len(revs) > 1: | ||
|
r26587 | raise error.Abort(_("too many destinations")) | ||
|
r24306 | if revs: | ||
dest = revs[0] | ||||
else: | ||||
dest = None | ||||
|
r4262 | revs = [] | ||
|
r11413 | if rev: | ||
|
r4262 | if revs: | ||
|
r26587 | raise error.Abort(_('use only one form to specify the revision')) | ||
|
r11413 | revs = rev | ||
|
r32639 | elif bookmark: | ||
if bookmark not in repo._bookmarks: | ||||
raise error.Abort(_("bookmark '%s' not found") % bookmark) | ||||
revs = repair.stripbmrevset(repo, bookmark) | ||||
|
r4262 | |||
|
r24568 | revs = scmutil.revrange(repo, revs) | ||
|
r11413 | if outgoing: | ||
|
r24568 | revs = _getoutgoing(repo, dest, revs) | ||
|
r11413 | if bundle: | ||
|
r24567 | opts['revs'] = [str(r) for r in revs] | ||
|
r4262 | |||
|
r26626 | # check if revision exist on the public destination | ||
publicurl = repo.ui.config('patchbomb', 'publicurl') | ||||
|
r32825 | if publicurl: | ||
|
r26626 | repo.ui.debug('checking that revision exist in the public repo') | ||
try: | ||||
publicpeer = hg.peer(repo, {}, publicurl) | ||||
except error.RepoError: | ||||
repo.ui.write_err(_('unable to access public repo: %s\n') | ||||
% publicurl) | ||||
raise | ||||
if not publicpeer.capable('known'): | ||||
repo.ui.debug('skipping existence checks: public repo too old') | ||||
else: | ||||
out = [repo[r] for r in revs] | ||||
known = publicpeer.known(h.node() for h in out) | ||||
missing = [] | ||||
for idx, h in enumerate(out): | ||||
if not known[idx]: | ||||
missing.append(h) | ||||
if missing: | ||||
if 1 < len(missing): | ||||
msg = _('public "%s" is missing %s and %i others') | ||||
msg %= (publicurl, missing[0], len(missing) - 1) | ||||
else: | ||||
msg = _('public url %s is missing %s') | ||||
msg %= (publicurl, missing[0]) | ||||
|
r28907 | revhint = ' '.join('-r %s' % h | ||
|
r26626 | for h in repo.set('heads(%ld)', missing)) | ||
|
r28981 | hint = _("use 'hg push %s %s'") % (publicurl, revhint) | ||
|
r26626 | raise error.Abort(msg, hint=hint) | ||
|
r4262 | # start | ||
|
r11413 | if date: | ||
start_time = util.parsedate(date) | ||||
|
r4566 | else: | ||
start_time = util.makedate() | ||||
|
r1669 | |||
def genmsgid(id): | ||||
|
r4027 | return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn()) | ||
|
r1669 | |||
|
r25825 | # deprecated config: patchbomb.from | ||
|
r5818 | sender = (opts.get('from') or ui.config('email', 'from') or | ||
|
r2198 | ui.config('patchbomb', 'from') or | ||
|
r7354 | prompt(ui, 'From', ui.username())) | ||
|
r1669 | |||
|
r31184 | if bundle: | ||
|
r23213 | bundledata = _getbundle(repo, dest, **opts) | ||
bundleopts = opts.copy() | ||||
bundleopts.pop('bundle', None) # already processed | ||||
msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts) | ||||
|
r4278 | else: | ||
|
r31185 | msgs = _getpatchmsgs(repo, sender, revs, **opts) | ||
|
r1669 | |||
|
r12200 | showaddrs = [] | ||
|
r15162 | def getaddrs(header, ask=False, default=None): | ||
configkey = header.lower() | ||||
opt = header.replace('-', '_').lower() | ||||
addrs = opts.get(opt) | ||||
|
r11150 | if addrs: | ||
|
r15162 | showaddrs.append('%s: %s' % (header, ', '.join(addrs))) | ||
|
r12264 | return mail.addrlistencode(ui, addrs, _charsets, opts.get('test')) | ||
|
r9947 | |||
|
r15162 | # not on the command line: fallback to config and then maybe ask | ||
addr = (ui.config('email', configkey) or | ||||
|
r27697 | ui.config('patchbomb', configkey)) | ||
if not addr: | ||||
specified = (ui.hasconfig('email', configkey) or | ||||
ui.hasconfig('patchbomb', configkey)) | ||||
if not specified and ask: | ||||
addr = prompt(ui, header, default=default) | ||||
|
r15162 | if addr: | ||
showaddrs.append('%s: %s' % (header, addr)) | ||||
|
r15164 | return mail.addrlistencode(ui, [addr], _charsets, opts.get('test')) | ||
|
r32826 | elif default: | ||
return mail.addrlistencode( | ||||
ui, [default], _charsets, opts.get('test')) | ||||
return [] | ||||
|
r9947 | |||
|
r15162 | to = getaddrs('To', ask=True) | ||
|
r15164 | if not to: | ||
# we can get here in non-interactive mode | ||||
|
r26587 | raise error.Abort(_('no recipient addresses provided')) | ||
|
r32826 | cc = getaddrs('Cc', ask=True, default='') | ||
bcc = getaddrs('Bcc') | ||||
|
r15162 | replyto = getaddrs('Reply-To') | ||
|
r2679 | |||
|
r23488 | confirm = ui.configbool('patchbomb', 'confirm') | ||
confirm |= bool(opts.get('diffstat') or opts.get('confirm')) | ||||
if confirm: | ||||
|
r23173 | ui.write(_('\nFinal summary:\n\n'), label='patchbomb.finalsummary') | ||
ui.write(('From: %s\n' % sender), label='patchbomb.from') | ||||
|
r12200 | for addr in showaddrs: | ||
|
r23173 | ui.write('%s\n' % addr, label='patchbomb.to') | ||
|
r12200 | for m, subj, ds in msgs: | ||
|
r23173 | ui.write(('Subject: %s\n' % subj), label='patchbomb.subject') | ||
|
r12200 | if ds: | ||
|
r23173 | ui.write(ds, label='patchbomb.diffstats') | ||
|
r12200 | ui.write('\n') | ||
|
r19226 | if ui.promptchoice(_('are you sure you want to send (yn)?' | ||
'$$ &Yes $$ &No')): | ||||
|
r26587 | raise error.Abort(_('patchbomb canceled')) | ||
|
r12200 | |||
|
r1669 | ui.write('\n') | ||
|
r8025 | parent = opts.get('in_reply_to') or None | ||
|
r8826 | # angle brackets may be omitted, they're not semantically part of the msg-id | ||
if parent is not None: | ||||
if not parent.startswith('<'): | ||||
parent = '<' + parent | ||||
if not parent.endswith('>'): | ||||
parent += '>' | ||||
|
r27150 | sender_addr = emailmod.Utils.parseaddr(sender)[1] | ||
|
r7115 | sender = mail.addressencode(ui, sender, _charsets, opts.get('test')) | ||
|
r5973 | sendmail = None | ||
|
r21726 | firstpatch = None | ||
|
r12265 | for i, (m, subj, ds) in enumerate(msgs): | ||
|
r1669 | try: | ||
m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) | ||||
|
r21726 | if not firstpatch: | ||
firstpatch = m['Message-Id'] | ||||
|
r21727 | m['X-Mercurial-Series-Id'] = firstpatch | ||
|
r1669 | except TypeError: | ||
m['Message-Id'] = genmsgid('patchbomb') | ||||
if parent: | ||||
m['In-Reply-To'] = parent | ||||
|
r7413 | m['References'] = parent | ||
|
r17859 | if not parent or 'X-Mercurial-Node' not in m: | ||
|
r1669 | parent = m['Message-Id'] | ||
|
r8514 | |||
|
r8160 | m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version() | ||
|
r27150 | m['Date'] = emailmod.Utils.formatdate(start_time[0], localtime=True) | ||
|
r2443 | |||
|
r4027 | start_time = (start_time[0] + 1, start_time[1]) | ||
|
r1669 | m['From'] = sender | ||
m['To'] = ', '.join(to) | ||||
|
r5785 | if cc: | ||
m['Cc'] = ', '.join(cc) | ||||
if bcc: | ||||
m['Bcc'] = ', '.join(bcc) | ||||
|
r11150 | if replyto: | ||
m['Reply-To'] = ', '.join(replyto) | ||||
|
r5818 | if opts.get('test'): | ||
|
r16931 | ui.status(_('displaying '), subj, ' ...\n') | ||
|
r31489 | ui.pager('email') | ||
generator = emailmod.Generator.Generator(ui, mangle_from_=False) | ||||
|
r1871 | try: | ||
|
r6447 | generator.flatten(m, 0) | ||
|
r31489 | ui.write('\n') | ||
|
r25660 | except IOError as inst: | ||
|
r1871 | if inst.errno != errno.EPIPE: | ||
raise | ||||
|
r15560 | else: | ||
if not sendmail: | ||||
|
r29285 | sendmail = mail.connect(ui, mbox=mbox) | ||
|
r16931 | ui.status(_('sending '), subj, ' ...\n') | ||
r28474 | ui.progress(_('sending'), i, item=subj, total=len(msgs), | |||
unit=_('emails')) | ||||
|
r15560 | if not mbox: | ||
# Exim does not remove the Bcc field | ||||
del m['Bcc'] | ||||
|
r28861 | fp = stringio() | ||
|
r27150 | generator = emailmod.Generator.Generator(fp, mangle_from_=False) | ||
|
r6447 | generator.flatten(m, 0) | ||
|
r15559 | sendmail(sender_addr, to + bcc + cc, fp.getvalue()) | ||
|
r1669 | |||
|
r12265 | ui.progress(_('writing'), None) | ||
ui.progress(_('sending'), None) | ||||