mirror of
https://github.com/facebook/sapling.git
synced 2024-10-12 01:39:21 +03:00
408782f48a
Summary: Better handling of parentheses. ignore-unit-failures Reviewed By: carljm Differential Revision: D8294476 fbshipit-source-id: d0bdfd14d1b39cf19ac8a8a3c6d74c9f4fea8b13
466 lines
16 KiB
Python
466 lines
16 KiB
Python
# notify.py - email notifications for mercurial
|
|
#
|
|
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
"""hooks for sending email push notifications
|
|
|
|
This extension implements hooks to send email notifications when
|
|
changesets are sent from or received by the local repository.
|
|
|
|
First, enable the extension as explained in :hg:`help extensions`, and
|
|
register the hook you want to run. ``incoming`` and ``changegroup`` hooks
|
|
are run when changesets are received, while ``outgoing`` hooks are for
|
|
changesets sent to another repository::
|
|
|
|
[hooks]
|
|
# one email for each incoming changeset
|
|
incoming.notify = python:hgext.notify.hook
|
|
# one email for all incoming changesets
|
|
changegroup.notify = python:hgext.notify.hook
|
|
|
|
# one email for all outgoing changesets
|
|
outgoing.notify = python:hgext.notify.hook
|
|
|
|
This registers the hooks. To enable notification, subscribers must
|
|
be assigned to repositories. The ``[usersubs]`` section maps multiple
|
|
repositories to a given recipient. The ``[reposubs]`` section maps
|
|
multiple recipients to a single repository::
|
|
|
|
[usersubs]
|
|
# key is subscriber email, value is a comma-separated list of repo patterns
|
|
user@host = pattern
|
|
|
|
[reposubs]
|
|
# key is repo pattern, value is a comma-separated list of subscriber emails
|
|
pattern = user@host
|
|
|
|
A ``pattern`` is a ``glob`` matching the absolute path to a repository,
|
|
optionally combined with a revset expression. A revset expression, if
|
|
present, is separated from the glob by a hash. Example::
|
|
|
|
[reposubs]
|
|
*/widgets#branch(release) = qa-team@example.com
|
|
|
|
This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
|
|
branch triggers a notification in any repository ending in ``widgets``.
|
|
|
|
In order to place them under direct user management, ``[usersubs]`` and
|
|
``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
|
|
incorporated by reference::
|
|
|
|
[notify]
|
|
config = /path/to/subscriptionsfile
|
|
|
|
Notifications will not be sent until the ``notify.test`` value is set
|
|
to ``False``; see below.
|
|
|
|
Notifications content can be tweaked with the following configuration entries:
|
|
|
|
notify.test
|
|
If ``True``, print messages to stdout instead of sending them. Default: True.
|
|
|
|
notify.sources
|
|
Space-separated list of change sources. Notifications are activated only
|
|
when a changeset's source is in this list. Sources may be:
|
|
|
|
:``serve``: changesets received via http or ssh
|
|
:``pull``: changesets received via ``hg pull``
|
|
:``unbundle``: changesets received via ``hg unbundle``
|
|
:``push``: changesets sent or received via ``hg push``
|
|
:``bundle``: changesets sent via ``hg unbundle``
|
|
|
|
Default: serve.
|
|
|
|
notify.strip
|
|
Number of leading slashes to strip from url paths. By default, notifications
|
|
reference repositories with their absolute path. ``notify.strip`` lets you
|
|
turn them into relative paths. For example, ``notify.strip=3`` will change
|
|
``/long/path/repository`` into ``repository``. Default: 0.
|
|
|
|
notify.domain
|
|
Default email domain for sender or recipients with no explicit domain.
|
|
|
|
notify.style
|
|
Style file to use when formatting emails.
|
|
|
|
notify.template
|
|
Template to use when formatting emails.
|
|
|
|
notify.incoming
|
|
Template to use when run as an incoming hook, overriding ``notify.template``.
|
|
|
|
notify.outgoing
|
|
Template to use when run as an outgoing hook, overriding ``notify.template``.
|
|
|
|
notify.changegroup
|
|
Template to use when running as a changegroup hook, overriding
|
|
``notify.template``.
|
|
|
|
notify.maxdiff
|
|
Maximum number of diff lines to include in notification email. Set to 0
|
|
to disable the diff, or -1 to include all of it. Default: 300.
|
|
|
|
notify.maxsubject
|
|
Maximum number of characters in email's subject line. Default: 67.
|
|
|
|
notify.diffstat
|
|
Set to True to include a diffstat before diff content. Default: True.
|
|
|
|
notify.merge
|
|
If True, send notifications for merge changesets. Default: True.
|
|
|
|
notify.mbox
|
|
If set, append mails to this mbox file instead of sending. Default: None.
|
|
|
|
notify.fromauthor
|
|
If set, use the committer of the first changeset in a changegroup for
|
|
the "From" field of the notification mail. If not set, take the user
|
|
from the pushing repo. Default: False.
|
|
|
|
If set, the following entries will also be used to customize the
|
|
notifications:
|
|
|
|
email.from
|
|
Email ``From`` address to use if none can be found in the generated
|
|
email content.
|
|
|
|
web.baseurl
|
|
Root repository URL to combine with repository paths when making
|
|
references. See also ``notify.strip``.
|
|
|
|
"""
|
|
from __future__ import absolute_import
|
|
|
|
import email
|
|
import fnmatch
|
|
import socket
|
|
import time
|
|
|
|
from mercurial import cmdutil, error, mail, patch, registrar, util
|
|
from mercurial.i18n import _
|
|
|
|
|
|
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
|
|
# 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.
|
|
testedwith = "ships-with-hg-core"
|
|
|
|
configtable = {}
|
|
configitem = registrar.configitem(configtable)
|
|
|
|
configitem("notify", "changegroup", default=None)
|
|
configitem("notify", "config", default=None)
|
|
configitem("notify", "diffstat", default=True)
|
|
configitem("notify", "domain", default=None)
|
|
configitem("notify", "fromauthor", default=None)
|
|
configitem("notify", "incoming", default=None)
|
|
configitem("notify", "maxdiff", default=300)
|
|
configitem("notify", "maxsubject", default=67)
|
|
configitem("notify", "mbox", default=None)
|
|
configitem("notify", "merge", default=True)
|
|
configitem("notify", "outgoing", default=None)
|
|
configitem("notify", "sources", default="serve")
|
|
configitem("notify", "strip", default=0)
|
|
configitem("notify", "style", default=None)
|
|
configitem("notify", "template", default=None)
|
|
configitem("notify", "test", default=True)
|
|
|
|
# template for single changeset can include email headers.
|
|
single_template = """
|
|
Subject: changeset in {webroot}: {desc|firstline|strip}
|
|
From: {author}
|
|
|
|
changeset {node|short} in {root}
|
|
details: {baseurl}{webroot}?cmd=changeset;node={node|short}
|
|
description:
|
|
\t{desc|tabindent|strip}
|
|
""".lstrip()
|
|
|
|
# template for multiple changesets should not contain email headers,
|
|
# because only first set of headers will be used and result will look
|
|
# strange.
|
|
multiple_template = """
|
|
changeset {node|short} in {root}
|
|
details: {baseurl}{webroot}?cmd=changeset;node={node|short}
|
|
summary: {desc|firstline}
|
|
"""
|
|
|
|
deftemplates = {"changegroup": multiple_template}
|
|
|
|
try:
|
|
xrange(0)
|
|
except NameError:
|
|
xrange = range
|
|
|
|
|
|
class notifier(object):
|
|
"""email notification class."""
|
|
|
|
def __init__(self, ui, repo, hooktype):
|
|
self.ui = ui
|
|
cfg = self.ui.config("notify", "config")
|
|
if cfg:
|
|
self.ui.readconfig(cfg, sections=["usersubs", "reposubs"])
|
|
self.repo = repo
|
|
self.stripcount = int(self.ui.config("notify", "strip"))
|
|
self.root = self.strip(self.repo.root)
|
|
self.domain = self.ui.config("notify", "domain")
|
|
self.mbox = self.ui.config("notify", "mbox")
|
|
self.test = self.ui.configbool("notify", "test")
|
|
self.charsets = mail._charsets(self.ui)
|
|
self.subs = self.subscribers()
|
|
self.merge = self.ui.configbool("notify", "merge")
|
|
|
|
mapfile = None
|
|
template = self.ui.config("notify", hooktype) or self.ui.config(
|
|
"notify", "template"
|
|
)
|
|
if not template:
|
|
mapfile = self.ui.config("notify", "style")
|
|
if not mapfile and not template:
|
|
template = deftemplates.get(hooktype) or single_template
|
|
spec = cmdutil.logtemplatespec(template, mapfile)
|
|
self.t = cmdutil.changeset_templater(
|
|
self.ui, self.repo, spec, False, None, False
|
|
)
|
|
|
|
def strip(self, path):
|
|
"""strip leading slashes from local path, turn into web-safe path."""
|
|
|
|
path = util.pconvert(path)
|
|
count = self.stripcount
|
|
while count > 0:
|
|
c = path.find("/")
|
|
if c == -1:
|
|
break
|
|
path = path[c + 1 :]
|
|
count -= 1
|
|
return path
|
|
|
|
def fixmail(self, addr):
|
|
"""try to clean up email addresses."""
|
|
|
|
addr = util.email(addr.strip())
|
|
if self.domain:
|
|
a = addr.find("@localhost")
|
|
if a != -1:
|
|
addr = addr[:a]
|
|
if "@" not in addr:
|
|
return addr + "@" + self.domain
|
|
return addr
|
|
|
|
def subscribers(self):
|
|
"""return list of email addresses of subscribers to this repo."""
|
|
subs = set()
|
|
for user, pats in self.ui.configitems("usersubs"):
|
|
for pat in pats.split(","):
|
|
if "#" in pat:
|
|
pat, revs = pat.split("#", 1)
|
|
else:
|
|
revs = None
|
|
if fnmatch.fnmatch(self.repo.root, pat.strip()):
|
|
subs.add((self.fixmail(user), revs))
|
|
for pat, users in self.ui.configitems("reposubs"):
|
|
if "#" in pat:
|
|
pat, revs = pat.split("#", 1)
|
|
else:
|
|
revs = None
|
|
if fnmatch.fnmatch(self.repo.root, pat):
|
|
for user in users.split(","):
|
|
subs.add((self.fixmail(user), revs))
|
|
return [
|
|
(mail.addressencode(self.ui, s, self.charsets, self.test), r)
|
|
for s, r in sorted(subs)
|
|
]
|
|
|
|
def node(self, ctx, **props):
|
|
"""format one changeset, unless it is a suppressed merge."""
|
|
if not self.merge and len(ctx.parents()) > 1:
|
|
return False
|
|
self.t.show(
|
|
ctx,
|
|
changes=ctx.changeset(),
|
|
baseurl=self.ui.config("web", "baseurl"),
|
|
root=self.repo.root,
|
|
webroot=self.root,
|
|
**props
|
|
)
|
|
return True
|
|
|
|
def skipsource(self, source):
|
|
"""true if incoming changes from this source should be skipped."""
|
|
ok_sources = self.ui.config("notify", "sources").split()
|
|
return source not in ok_sources
|
|
|
|
def send(self, ctx, count, data):
|
|
"""send message."""
|
|
|
|
# Select subscribers by revset
|
|
subs = set()
|
|
for sub, spec in self.subs:
|
|
if spec is None:
|
|
subs.add(sub)
|
|
continue
|
|
revs = self.repo.revs("%r and %d:", spec, ctx.rev())
|
|
if len(revs):
|
|
subs.add(sub)
|
|
continue
|
|
if len(subs) == 0:
|
|
self.ui.debug("notify: no subscribers to selected repo " "and revset\n")
|
|
return
|
|
|
|
p = email.Parser.Parser()
|
|
try:
|
|
msg = p.parsestr(data)
|
|
except email.Errors.MessageParseError as inst:
|
|
raise error.Abort(inst)
|
|
|
|
# store sender and subject
|
|
sender, subject = msg["From"], msg["Subject"]
|
|
del msg["From"], msg["Subject"]
|
|
|
|
if not msg.is_multipart():
|
|
# create fresh mime message from scratch
|
|
# (multipart templates must take care of this themselves)
|
|
headers = msg.items()
|
|
payload = msg.get_payload()
|
|
# for notification prefer readability over data precision
|
|
msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
|
|
# reinstate custom headers
|
|
for k, v in headers:
|
|
msg[k] = v
|
|
|
|
msg["Date"] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
|
|
|
|
# try to make subject line exist and be useful
|
|
if not subject:
|
|
if count > 1:
|
|
subject = _("%s: %d new changesets") % (self.root, count)
|
|
else:
|
|
s = ctx.description().lstrip().split("\n", 1)[0].rstrip()
|
|
subject = "%s: %s" % (self.root, s)
|
|
maxsubject = int(self.ui.config("notify", "maxsubject"))
|
|
if maxsubject:
|
|
subject = util.ellipsis(subject, maxsubject)
|
|
msg["Subject"] = mail.headencode(self.ui, subject, self.charsets, self.test)
|
|
|
|
# try to make message have proper sender
|
|
if not sender:
|
|
sender = self.ui.config("email", "from") or self.ui.username()
|
|
if "@" not in sender or "@localhost" in sender:
|
|
sender = self.fixmail(sender)
|
|
msg["From"] = mail.addressencode(self.ui, sender, self.charsets, self.test)
|
|
|
|
msg["X-Hg-Notification"] = "changeset %s" % ctx
|
|
if not msg["Message-Id"]:
|
|
msg["Message-Id"] = "<hg.%s.%s.%s@%s>" % (
|
|
ctx,
|
|
int(time.time()),
|
|
hash(self.repo.root),
|
|
socket.getfqdn(),
|
|
)
|
|
msg["To"] = ", ".join(sorted(subs))
|
|
|
|
msgtext = msg.as_string()
|
|
if self.test:
|
|
self.ui.write(msgtext)
|
|
if not msgtext.endswith("\n"):
|
|
self.ui.write("\n")
|
|
else:
|
|
self.ui.status(
|
|
_("notify: sending %d subscribers %d changes\n") % (len(subs), count)
|
|
)
|
|
mail.sendmail(
|
|
self.ui, util.email(msg["From"]), subs, msgtext, mbox=self.mbox
|
|
)
|
|
|
|
def diff(self, ctx, ref=None):
|
|
|
|
maxdiff = int(self.ui.config("notify", "maxdiff"))
|
|
prev = ctx.p1().node()
|
|
if ref:
|
|
ref = ref.node()
|
|
else:
|
|
ref = ctx.node()
|
|
chunks = patch.diff(self.repo, prev, ref, opts=patch.diffallopts(self.ui))
|
|
difflines = "".join(chunks).splitlines()
|
|
|
|
if self.ui.configbool("notify", "diffstat"):
|
|
s = patch.diffstat(difflines)
|
|
# s may be nil, don't include the header if it is
|
|
if s:
|
|
self.ui.write(_("\ndiffstat:\n\n%s") % s)
|
|
|
|
if maxdiff == 0:
|
|
return
|
|
elif maxdiff > 0 and len(difflines) > maxdiff:
|
|
msg = _("\ndiffs (truncated from %d to %d lines):\n\n")
|
|
self.ui.write(msg % (len(difflines), maxdiff))
|
|
difflines = difflines[:maxdiff]
|
|
elif difflines:
|
|
self.ui.write(_("\ndiffs (%d lines):\n\n") % len(difflines))
|
|
|
|
self.ui.write("\n".join(difflines))
|
|
|
|
|
|
def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
|
|
"""send email notifications to interested subscribers.
|
|
|
|
if used as changegroup hook, send one email for all changesets in
|
|
changegroup. else send one email per changeset."""
|
|
|
|
n = notifier(ui, repo, hooktype)
|
|
ctx = repo[node]
|
|
|
|
if not n.subs:
|
|
ui.debug("notify: no subscribers to repository %s\n" % n.root)
|
|
return
|
|
if n.skipsource(source):
|
|
ui.debug('notify: changes have source "%s" - skipping\n' % source)
|
|
return
|
|
|
|
ui.pushbuffer()
|
|
data = ""
|
|
count = 0
|
|
author = ""
|
|
if hooktype == "changegroup" or hooktype == "outgoing":
|
|
start, end = ctx.rev(), len(repo)
|
|
for rev in xrange(start, end):
|
|
if n.node(repo[rev]):
|
|
count += 1
|
|
if not author:
|
|
author = repo[rev].user()
|
|
else:
|
|
data += ui.popbuffer()
|
|
ui.note(
|
|
_("notify: suppressing notification for merge %d:%s\n")
|
|
% (rev, repo[rev].hex()[:12])
|
|
)
|
|
ui.pushbuffer()
|
|
if count:
|
|
n.diff(ctx, repo["tip"])
|
|
else:
|
|
if not n.node(ctx):
|
|
ui.popbuffer()
|
|
ui.note(
|
|
_("notify: suppressing notification for merge %d:%s\n")
|
|
% (ctx.rev(), ctx.hex()[:12])
|
|
)
|
|
return
|
|
count += 1
|
|
n.diff(ctx)
|
|
if not author:
|
|
author = ctx.user()
|
|
|
|
data += ui.popbuffer()
|
|
fromauthor = ui.config("notify", "fromauthor")
|
|
if author and fromauthor:
|
|
data = "\n".join(["From: %s" % author, data])
|
|
|
|
if count:
|
|
n.send(ctx, count, data)
|