mirror of
https://github.com/facebook/sapling.git
synced 2024-10-08 07:49:11 +03:00
371257fd73
readconfig now reads only single files readconfig takes an optional list of sections readconfig trusts files we're looking for sections in
291 lines
10 KiB
Python
291 lines
10 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, incorporated herein by reference.
|
|
|
|
'''hook extension to email notifications on commits/pushes
|
|
|
|
Subscriptions can be managed through hgrc. Default mode is to print
|
|
messages to stdout, for testing and configuring.
|
|
|
|
To use, configure notify extension and enable in hgrc like this:
|
|
|
|
[extensions]
|
|
hgext.notify =
|
|
|
|
[hooks]
|
|
# one email for each incoming changeset
|
|
incoming.notify = python:hgext.notify.hook
|
|
# batch emails when many changesets incoming at one time
|
|
changegroup.notify = python:hgext.notify.hook
|
|
|
|
[notify]
|
|
# config items go in here
|
|
|
|
config items:
|
|
|
|
REQUIRED:
|
|
config = /path/to/file # file containing subscriptions
|
|
|
|
OPTIONAL:
|
|
test = True # print messages to stdout for testing
|
|
strip = 3 # number of slashes to strip for url paths
|
|
domain = example.com # domain to use if committer missing domain
|
|
style = ... # style file to use when formatting email
|
|
template = ... # template to use when formatting email
|
|
incoming = ... # template to use when run as incoming hook
|
|
changegroup = ... # template when run as changegroup hook
|
|
maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
|
|
maxsubject = 67 # truncate subject line longer than this
|
|
diffstat = True # add a diffstat before the diff content
|
|
sources = serve # notify if source of incoming changes in this list
|
|
# (serve == ssh or http, push, pull, bundle)
|
|
[email]
|
|
from = user@host.com # email address to send as if none given
|
|
[web]
|
|
baseurl = http://hgserver/... # root of hg web site for browsing commits
|
|
|
|
notify config file has same format as regular hgrc. it has two
|
|
sections so you can express subscriptions in whatever way is handier
|
|
for you.
|
|
|
|
[usersubs]
|
|
# key is subscriber email, value is ","-separated list of glob patterns
|
|
user@host = pattern
|
|
|
|
[reposubs]
|
|
# key is glob pattern, value is ","-separated list of subscriber emails
|
|
pattern = user@host
|
|
|
|
glob patterns are matched against path to repository root.
|
|
|
|
if you like, you can put notify config file in repository that users
|
|
can push changes to, they can manage their own subscriptions.'''
|
|
|
|
from mercurial.i18n import _
|
|
from mercurial import patch, cmdutil, templater, util, mail
|
|
import email.Parser, fnmatch, socket, time
|
|
|
|
# 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,
|
|
}
|
|
|
|
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', 0))
|
|
self.root = self.strip(self.repo.root)
|
|
self.domain = self.ui.config('notify', 'domain')
|
|
self.test = self.ui.configbool('notify', 'test', True)
|
|
self.charsets = mail._charsets(self.ui)
|
|
self.subs = self.subscribers()
|
|
|
|
mapfile = self.ui.config('notify', 'style')
|
|
template = (self.ui.config('notify', hooktype) or
|
|
self.ui.config('notify', 'template'))
|
|
self.t = cmdutil.changeset_templater(self.ui, self.repo,
|
|
False, None, mapfile, False)
|
|
if not mapfile and not template:
|
|
template = deftemplates.get(hooktype) or single_template
|
|
if template:
|
|
template = templater.parsestring(template, quoted=False)
|
|
self.t.use_template(template)
|
|
|
|
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 = {}
|
|
for user, pats in self.ui.configitems('usersubs'):
|
|
for pat in pats.split(','):
|
|
if fnmatch.fnmatch(self.repo.root, pat.strip()):
|
|
subs[self.fixmail(user)] = 1
|
|
for pat, users in self.ui.configitems('reposubs'):
|
|
if fnmatch.fnmatch(self.repo.root, pat):
|
|
for user in users.split(','):
|
|
subs[self.fixmail(user)] = 1
|
|
subs = util.sort(subs)
|
|
return [mail.addressencode(self.ui, s, self.charsets, self.test)
|
|
for s in subs]
|
|
|
|
def url(self, path=None):
|
|
return self.ui.config('web', 'baseurl') + (path or self.root)
|
|
|
|
def node(self, ctx):
|
|
'''format one changeset.'''
|
|
self.t.show(ctx, changes=ctx.changeset(),
|
|
baseurl=self.ui.config('web', 'baseurl'),
|
|
root=self.repo.root, webroot=self.root)
|
|
|
|
def skipsource(self, source):
|
|
'''true if incoming changes from this source should be skipped.'''
|
|
ok_sources = self.ui.config('notify', 'sources', 'serve').split()
|
|
return source not in ok_sources
|
|
|
|
def send(self, ctx, count, data):
|
|
'''send message.'''
|
|
|
|
p = email.Parser.Parser()
|
|
msg = p.parsestr(data)
|
|
|
|
# store sender and subject
|
|
sender, subject = msg['From'], msg['Subject']
|
|
del msg['From'], msg['Subject']
|
|
# store remaining headers
|
|
headers = msg.items()
|
|
# create fresh mime message from msg body
|
|
text = msg.get_payload()
|
|
# for notification prefer readability over data precision
|
|
msg = mail.mimeencode(self.ui, text, 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', 67))
|
|
if maxsubject and len(subject) > maxsubject:
|
|
subject = subject[:maxsubject-3] + '...'
|
|
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(self.subs)
|
|
|
|
msgtext = msg.as_string(0)
|
|
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(self.subs), count))
|
|
mail.sendmail(self.ui, util.email(msg['From']),
|
|
self.subs, msgtext)
|
|
|
|
def diff(self, ctx, ref=None):
|
|
|
|
maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
|
|
prev = ctx.parents()[0].node()
|
|
ref = ref and ref.node() or ctx.node()
|
|
chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
|
|
difflines = ''.join(chunks).splitlines()
|
|
|
|
if self.ui.configbool('notify', 'diffstat', True):
|
|
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()
|
|
if hooktype == 'changegroup':
|
|
start, end = ctx.rev(), len(repo)
|
|
count = end - start
|
|
for rev in xrange(start, end):
|
|
n.node(repo[rev])
|
|
n.diff(ctx, repo['tip'])
|
|
else:
|
|
count = 1
|
|
n.node(ctx)
|
|
n.diff(ctx)
|
|
|
|
data = ui.popbuffer()
|
|
n.send(ctx, count, data)
|