notify.py
360 lines
| 12.6 KiB
| text/x-python
|
PythonLexer
/ hgext / notify.py
Vadim Gelfer
|
r2203 | # notify.py - email notifications for mercurial | ||
# | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Dirkjan Ochtman
|
r7127 | |||
Patrick Mezard
|
r14940 | '''hooks for sending email push notifications | ||
Dirkjan Ochtman
|
r7127 | |||
Patrick Mezard
|
r14940 | This extension let you run hooks sending email notifications when | ||
changesets are being pushed, from the sending or receiving side. | ||||
Dirkjan Ochtman
|
r7127 | |||
Patrick Mezard
|
r14940 | First, enable the extension as explained in :hg:`help extensions`, and | ||
register the hook you want to run. ``incoming`` and ``outgoing`` hooks | ||||
are run by the changesets receiver while the ``outgoing`` one is for | ||||
the sender:: | ||||
Dirkjan Ochtman
|
r7127 | |||
Martin Geisler
|
r9105 | [hooks] | ||
# one email for each incoming changeset | ||||
incoming.notify = python:hgext.notify.hook | ||||
Patrick Mezard
|
r14940 | # one email for all incoming changesets | ||
Martin Geisler
|
r9105 | changegroup.notify = python:hgext.notify.hook | ||
Patrick Mezard
|
r14940 | |||
# one email for all outgoing changesets | ||||
Ingo Bressler
|
r14617 | outgoing.notify = python:hgext.notify.hook | ||
Dirkjan Ochtman
|
r7127 | |||
Patrick Mezard
|
r14940 | Now the hooks are running, subscribers must be assigned to | ||
repositories. Use the ``[usersubs]`` section to map repositories to a | ||||
given email or the ``[reposubs]`` section to map emails to a single | ||||
repository:: | ||||
Martin Geisler
|
r9157 | |||
Martin Geisler
|
r9105 | [usersubs] | ||
Patrick Mezard
|
r14940 | # key is subscriber email, value is a comma-separated list of glob | ||
# patterns | ||||
Martin Geisler
|
r9105 | user@host = pattern | ||
Dirkjan Ochtman
|
r7127 | |||
Martin Geisler
|
r9105 | [reposubs] | ||
Patrick Mezard
|
r14940 | # key is glob pattern, value is a comma-separated list of subscriber | ||
# emails | ||||
Martin Geisler
|
r9105 | pattern = user@host | ||
Dirkjan Ochtman
|
r7127 | |||
Patrick Mezard
|
r14940 | Glob patterns are matched against absolute path to repository | ||
root. The subscriptions can be defined in their own file and | ||||
referenced with:: | ||||
[notify] | ||||
config = /path/to/subscriptionsfile | ||||
Alternatively, they can be added to Mercurial configuration files by | ||||
setting the previous entry to an empty value. | ||||
At this point, notifications should be generated but will not be sent until you | ||||
set the ``notify.test`` entry to ``False``. | ||||
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 sent only | ||||
if it includes the incoming or outgoing changes source. Incoming | ||||
sources can be ``serve`` for changes coming from http or ssh, | ||||
``pull`` for pulled changes, ``unbundle`` for changes added by | ||||
:hg:`unbundle` or ``push`` for changes being pushed | ||||
locally. Outgoing sources are the same except for ``unbundle`` which | ||||
is replaced by ``bundle``. Default: serve. | ||||
notify.strip | ||||
Number of leading slashes to strip from url paths. By default, notifications | ||||
references repositories with their absolute path. ``notify.strip`` let you | ||||
turn them into relative paths. For example, ``notify.strip=3`` will change | ||||
``/long/path/repository`` into ``repository``. Default: 0. | ||||
notify.domain | ||||
If subscribers emails or the from email have no domain set, complete them | ||||
with this value. | ||||
Dirkjan Ochtman
|
r7127 | |||
Patrick Mezard
|
r14940 | 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 incoming hook, override ``notify.template``. | ||||
notify.outgoing | ||||
Template to use when run as outgoing hook, override ``notify.template``. | ||||
notify.changegroup | ||||
Template to use when running as changegroup hook, override | ||||
``notify.template``. | ||||
notify.maxdiff | ||||
Maximum number of diff lines to include in notification email. Set to 0 | ||||
to disable the diff, -1 to include all of it. Default: 300. | ||||
notify.maxsubject | ||||
Maximum number of characters in emails 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. | ||||
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 generated email content. | ||||
web.baseurl | ||||
Root repository browsing URL to combine with repository paths when making | ||||
references. See also ``notify.strip``. | ||||
Martin Geisler
|
r9068 | ''' | ||
Vadim Gelfer
|
r2203 | |||
Matt Mackall
|
r3891 | from mercurial.i18n import _ | ||
Matt Mackall
|
r3877 | from mercurial import patch, cmdutil, templater, util, mail | ||
Christian Ebert
|
r9313 | import email.Parser, email.Errors, fnmatch, socket, time | ||
Vadim Gelfer
|
r2203 | |||
# 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, | ||||
Thomas Arendsen Hein
|
r4498 | } | ||
Bryan O'Sullivan
|
r2201 | |||
class notifier(object): | ||||
Vadim Gelfer
|
r2203 | '''email notification class.''' | ||
def __init__(self, ui, repo, hooktype): | ||||
Bryan O'Sullivan
|
r2201 | self.ui = ui | ||
Vadim Gelfer
|
r2329 | cfg = self.ui.config('notify', 'config') | ||
if cfg: | ||||
Matt Mackall
|
r8142 | self.ui.readconfig(cfg, sections=['usersubs', 'reposubs']) | ||
Bryan O'Sullivan
|
r2201 | self.repo = repo | ||
Vadim Gelfer
|
r2203 | self.stripcount = int(self.ui.config('notify', 'strip', 0)) | ||
Bryan O'Sullivan
|
r2201 | self.root = self.strip(self.repo.root) | ||
Vadim Gelfer
|
r2203 | self.domain = self.ui.config('notify', 'domain') | ||
Christian Ebert
|
r7498 | self.test = self.ui.configbool('notify', 'test', True) | ||
Christian Ebert
|
r7116 | self.charsets = mail._charsets(self.ui) | ||
Vadim Gelfer
|
r2203 | self.subs = self.subscribers() | ||
David Champion
|
r9516 | self.merge = self.ui.configbool('notify', 'merge', True) | ||
Vadim Gelfer
|
r2203 | |||
mapfile = self.ui.config('notify', 'style') | ||||
template = (self.ui.config('notify', hooktype) or | ||||
self.ui.config('notify', 'template')) | ||||
Matt Mackall
|
r3739 | self.t = cmdutil.changeset_templater(self.ui, self.repo, | ||
Jim Correia
|
r7762 | False, None, mapfile, False) | ||
Vadim Gelfer
|
r2203 | 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) | ||||
Bryan O'Sullivan
|
r2201 | |||
def strip(self, path): | ||||
Vadim Gelfer
|
r2203 | '''strip leading slashes from local path, turn into web-safe path.''' | ||
Bryan O'Sullivan
|
r2201 | path = util.pconvert(path) | ||
count = self.stripcount | ||||
Vadim Gelfer
|
r2326 | while count > 0: | ||
Bryan O'Sullivan
|
r2201 | c = path.find('/') | ||
if c == -1: | ||||
break | ||||
Matt Mackall
|
r10282 | path = path[c + 1:] | ||
Bryan O'Sullivan
|
r2201 | count -= 1 | ||
return path | ||||
Vadim Gelfer
|
r2203 | def fixmail(self, addr): | ||
'''try to clean up email addresses.''' | ||||
Matt Mackall
|
r5975 | addr = util.email(addr.strip()) | ||
Alexis S. L. Carvalho
|
r4094 | if self.domain: | ||
a = addr.find('@localhost') | ||||
if a != -1: | ||||
addr = addr[:a] | ||||
if '@' not in addr: | ||||
return addr + '@' + self.domain | ||||
Vadim Gelfer
|
r2203 | return addr | ||
Bryan O'Sullivan
|
r2201 | def subscribers(self): | ||
Vadim Gelfer
|
r2203 | '''return list of email addresses of subscribers to this repo.''' | ||
Martin Geisler
|
r8154 | subs = set() | ||
Vadim Gelfer
|
r2203 | for user, pats in self.ui.configitems('usersubs'): | ||
for pat in pats.split(','): | ||||
if fnmatch.fnmatch(self.repo.root, pat.strip()): | ||||
Martin Geisler
|
r8154 | subs.add(self.fixmail(user)) | ||
Bryan O'Sullivan
|
r2201 | for pat, users in self.ui.configitems('reposubs'): | ||
Vadim Gelfer
|
r2203 | if fnmatch.fnmatch(self.repo.root, pat): | ||
for user in users.split(','): | ||||
Martin Geisler
|
r8154 | subs.add(self.fixmail(user)) | ||
Christian Ebert
|
r7498 | return [mail.addressencode(self.ui, s, self.charsets, self.test) | ||
Martin Geisler
|
r8154 | for s in sorted(subs)] | ||
Bryan O'Sullivan
|
r2201 | |||
Bryan O'Sullivan
|
r9486 | def node(self, ctx, **props): | ||
David Champion
|
r9516 | '''format one changeset, unless it is a suppressed merge.''' | ||
if not self.merge and len(ctx.parents()) > 1: | ||||
return False | ||||
Dirkjan Ochtman
|
r7726 | self.t.show(ctx, changes=ctx.changeset(), | ||
Vadim Gelfer
|
r2203 | baseurl=self.ui.config('web', 'baseurl'), | ||
Bryan O'Sullivan
|
r9486 | root=self.repo.root, webroot=self.root, **props) | ||
David Champion
|
r9516 | return True | ||
Vadim Gelfer
|
r2203 | |||
Vadim Gelfer
|
r2230 | 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 | ||||
Dirkjan Ochtman
|
r7726 | def send(self, ctx, count, data): | ||
Vadim Gelfer
|
r2203 | '''send message.''' | ||
p = email.Parser.Parser() | ||||
Christian Ebert
|
r9313 | try: | ||
msg = p.parsestr(data) | ||||
except email.Errors.MessageParseError, inst: | ||||
raise util.Abort(inst) | ||||
Vadim Gelfer
|
r2203 | |||
Christian Ebert
|
r7116 | # store sender and subject | ||
sender, subject = msg['From'], msg['Subject'] | ||||
Christian Ebert
|
r7658 | del msg['From'], msg['Subject'] | ||
Christian Ebert
|
r9313 | |||
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 | ||||
Christian Ebert
|
r7116 | |||
Christian Ebert
|
r7705 | msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2") | ||
Vadim Gelfer
|
r2203 | |||
Christian Ebert
|
r7705 | # try to make subject line exist and be useful | ||
if not subject: | ||||
if count > 1: | ||||
subject = _('%s: %d new changesets') % (self.root, count) | ||||
else: | ||||
Dirkjan Ochtman
|
r7726 | s = ctx.description().lstrip().split('\n', 1)[0].rstrip() | ||
Christian Ebert
|
r7705 | subject = '%s: %s' % (self.root, s) | ||
maxsubject = int(self.ui.config('notify', 'maxsubject', 67)) | ||||
Yuya Nishihara
|
r13202 | if maxsubject: | ||
subject = util.ellipsis(subject, maxsubject) | ||||
Christian Ebert
|
r7705 | msg['Subject'] = mail.headencode(self.ui, subject, | ||
self.charsets, self.test) | ||||
Vadim Gelfer
|
r2203 | |||
Christian Ebert
|
r7705 | # 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) | ||||
Vadim Gelfer
|
r2203 | |||
Dirkjan Ochtman
|
r7726 | msg['X-Hg-Notification'] = 'changeset %s' % ctx | ||
Vadim Gelfer
|
r2203 | if not msg['Message-Id']: | ||
msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' % | ||||
Dirkjan Ochtman
|
r7726 | (ctx, int(time.time()), | ||
Vadim Gelfer
|
r2203 | hash(self.repo.root), socket.getfqdn())) | ||
Vadim Gelfer
|
r2230 | msg['To'] = ', '.join(self.subs) | ||
Vadim Gelfer
|
r2203 | |||
Nicolas Dumazet
|
r9136 | msgtext = msg.as_string() | ||
Christian Ebert
|
r7498 | if self.test: | ||
Vadim Gelfer
|
r2203 | self.ui.write(msgtext) | ||
if not msgtext.endswith('\n'): | ||||
self.ui.write('\n') | ||||
Bryan O'Sullivan
|
r2201 | else: | ||
Vadim Gelfer
|
r2329 | self.ui.status(_('notify: sending %d subscribers %d changes\n') % | ||
Thomas Arendsen Hein
|
r4498 | (len(self.subs), count)) | ||
Matt Mackall
|
r5975 | mail.sendmail(self.ui, util.email(msg['From']), | ||
Matt Mackall
|
r2889 | self.subs, msgtext) | ||
Bryan O'Sullivan
|
r2201 | |||
Dirkjan Ochtman
|
r7726 | def diff(self, ctx, ref=None): | ||
Vadim Gelfer
|
r2203 | maxdiff = int(self.ui.config('notify', 'maxdiff', 300)) | ||
Matt Mackall
|
r13878 | prev = ctx.p1().node() | ||
Dirkjan Ochtman
|
r7726 | ref = ref and ref.node() or ctx.node() | ||
Dirkjan Ochtman
|
r7308 | chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui)) | ||
difflines = ''.join(chunks).splitlines() | ||||
divy@chelsio.com
|
r6979 | |||
Matt Doar
|
r3096 | if self.ui.configbool('notify', 'diffstat', True): | ||
s = patch.diffstat(difflines) | ||||
Sean Dague
|
r4077 | # s may be nil, don't include the header if it is | ||
if s: | ||||
self.ui.write('\ndiffstat:\n\n%s' % s) | ||||
Dirkjan Ochtman
|
r7726 | |||
Benoît Allard
|
r6305 | if maxdiff == 0: | ||
return | ||||
Dirkjan Ochtman
|
r7726 | elif maxdiff > 0 and len(difflines) > maxdiff: | ||
msg = _('\ndiffs (truncated from %d to %d lines):\n\n') | ||||
self.ui.write(msg % (len(difflines), maxdiff)) | ||||
Vadim Gelfer
|
r2203 | difflines = difflines[:maxdiff] | ||
elif difflines: | ||||
Matt Mackall
|
r3739 | self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines)) | ||
Dirkjan Ochtman
|
r7726 | |||
divy@chelsio.com
|
r6979 | self.ui.write("\n".join(difflines)) | ||
Bryan O'Sullivan
|
r2201 | |||
Vadim Gelfer
|
r2230 | def hook(ui, repo, hooktype, node=None, source=None, **kwargs): | ||
Vadim Gelfer
|
r2203 | '''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.''' | ||||
Dirkjan Ochtman
|
r7726 | |||
Vadim Gelfer
|
r2203 | n = notifier(ui, repo, hooktype) | ||
Dirkjan Ochtman
|
r7726 | ctx = repo[node] | ||
Vadim Gelfer
|
r2329 | if not n.subs: | ||
Martin Geisler
|
r9467 | ui.debug('notify: no subscribers to repository %s\n' % n.root) | ||
Vadim Gelfer
|
r2329 | return | ||
if n.skipsource(source): | ||||
Martin Geisler
|
r9467 | ui.debug('notify: changes have source "%s" - skipping\n' % source) | ||
Vadim Gelfer
|
r2230 | return | ||
Dirkjan Ochtman
|
r7726 | |||
Matt Mackall
|
r3739 | ui.pushbuffer() | ||
David Champion
|
r9516 | data = '' | ||
count = 0 | ||||
Ingo Bressler
|
r14617 | if hooktype == 'changegroup' or hooktype == 'outgoing': | ||
Dirkjan Ochtman
|
r7726 | start, end = ctx.rev(), len(repo) | ||
Vadim Gelfer
|
r2203 | for rev in xrange(start, end): | ||
David Champion
|
r9516 | if n.node(repo[rev]): | ||
count += 1 | ||||
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']) | ||||
Vadim Gelfer
|
r2203 | else: | ||
David Champion
|
r9516 | 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 | ||||
Dirkjan Ochtman
|
r7726 | n.diff(ctx) | ||
David Champion
|
r9516 | data += ui.popbuffer() | ||
if count: | ||||
n.send(ctx, count, data) | ||||