sapling/hgext/notify.py

290 lines
10 KiB
Python
Raw Normal View History

# 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, incorporated herein by reference.
'''hooks for sending email notifications at commit/push time
2009-07-10 00:42:43 +04:00
Subscriptions can be managed through a hgrc file. Default mode is to print
messages to stdout, for testing and configuring.
2009-07-10 00:42:43 +04:00
To use, configure the notify extension and enable it 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]
2009-07-10 00:42:43 +04:00
# config items go here
2009-07-10 00:42:43 +04:00
Required configuration items:
config = /path/to/file # file containing subscriptions
2009-07-10 00:42:43 +04:00
Optional configuration items:
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
2009-07-10 00:42:43 +04:00
The notify config file has same format as a regular hgrc file. 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
2009-07-10 00:42:43 +04:00
Glob patterns are matched against path to repository root.
2009-07-10 00:42:43 +04:00
If you like, you can put notify config file in repository that users can push
changes to, they can manage their own subscriptions.
'''
2006-12-15 05:25:19 +03:00
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,
2007-06-05 00:02:17 +04:00
}
2006-05-04 23:25:10 +04:00
class notifier(object):
'''email notification class.'''
def __init__(self, ui, repo, hooktype):
2006-05-04 23:25:10 +04:00
self.ui = ui
cfg = self.ui.config('notify', 'config')
if cfg:
self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
2006-05-04 23:25:10 +04:00
self.repo = repo
self.stripcount = int(self.ui.config('notify', 'strip', 0))
2006-05-04 23:25:10 +04:00
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'))
2006-12-01 10:28:20 +03:00
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)
2006-05-04 23:25:10 +04:00
def strip(self, path):
'''strip leading slashes from local path, turn into web-safe path.'''
2006-05-04 23:25:10 +04:00
path = util.pconvert(path)
count = self.stripcount
2006-05-20 01:57:45 +04:00
while count > 0:
2006-05-04 23:25:10 +04:00
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.'''
2008-01-31 23:44:19 +03:00
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
2006-05-04 23:25:10 +04:00
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 fnmatch.fnmatch(self.repo.root, pat.strip()):
subs.add(self.fixmail(user))
2006-05-04 23:25:10 +04:00
for pat, users in self.ui.configitems('reposubs'):
if fnmatch.fnmatch(self.repo.root, pat):
for user in users.split(','):
subs.add(self.fixmail(user))
return [mail.addressencode(self.ui, s, self.charsets, self.test)
for s in sorted(subs)]
2006-05-04 23:25:10 +04:00
def url(self, path=None):
return self.ui.config('web', 'baseurl') + (path or self.root)
2009-02-05 20:21:22 +03:00
def node(self, ctx):
'''format one changeset.'''
2009-02-05 20:21:22 +03:00
self.t.show(ctx, changes=ctx.changeset(),
baseurl=self.ui.config('web', 'baseurl'),
2009-02-05 20:21:22 +03:00
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
2009-02-05 20:21:22 +03:00
def send(self, ctx, count, data):
'''send message.'''
p = email.Parser.Parser()
2006-12-01 10:28:20 +03:00
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:
2009-02-05 20:21:22 +03:00
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)
2009-02-05 20:21:22 +03:00
msg['X-Hg-Notification'] = 'changeset %s' % ctx
if not msg['Message-Id']:
msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
2009-02-05 20:21:22 +03:00
(ctx, int(time.time()),
hash(self.repo.root), socket.getfqdn()))
msg['To'] = ', '.join(self.subs)
2006-05-04 23:25:10 +04:00
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') %
2007-06-05 00:02:17 +04:00
(len(self.subs), count))
2008-01-31 23:44:19 +03:00
mail.sendmail(self.ui, util.email(msg['From']),
self.subs, msgtext)
2006-05-04 23:25:10 +04:00
2009-02-05 20:21:22 +03:00
def diff(self, ctx, ref=None):
2009-02-05 20:21:22 +03:00
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)
2009-02-05 20:21:22 +03:00
if maxdiff == 0:
return
2009-02-05 20:21:22 +03:00
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:
2006-12-01 10:28:20 +03:00
self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
2009-02-05 20:21:22 +03:00
self.ui.write("\n".join(difflines))
2006-05-04 23:25:10 +04:00
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.'''
2009-02-05 20:21:22 +03:00
n = notifier(ui, repo, hooktype)
2009-02-05 20:21:22 +03:00
ctx = repo[node]
if not n.subs:
ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
return
if n.skipsource(source):
2009-02-05 20:21:22 +03:00
ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
return
2009-02-05 20:21:22 +03:00
2006-12-01 10:28:20 +03:00
ui.pushbuffer()
if hooktype == 'changegroup':
2009-02-05 20:21:22 +03:00
start, end = ctx.rev(), len(repo)
count = end - start
for rev in xrange(start, end):
2009-02-05 20:21:22 +03:00
n.node(repo[rev])
n.diff(ctx, repo['tip'])
else:
count = 1
2009-02-05 20:21:22 +03:00
n.node(ctx)
n.diff(ctx)
2006-12-01 10:28:20 +03:00
data = ui.popbuffer()
2009-02-05 20:21:22 +03:00
n.send(ctx, count, data)