hgext/notify.py
changeset 2219 ec82cff7d2c4
parent 2203 9569eea1707c
child 2221 05b6c13f43c6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/notify.py	Mon May 08 08:04:46 2006 -0700
@@ -0,0 +1,258 @@
+# 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 to people when changesets are
+# committed to a repo they subscribe to.
+#
+# 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
+#   [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 repo root.
+#
+# if you like, you can put notify config file in repo that users can
+# push changes to, they can manage their own subscriptions.
+
+from mercurial.demandload import *
+from mercurial.i18n import gettext as _
+from mercurial.node import *
+demandload(globals(), 'email.Parser mercurial:commands,templater,util')
+demandload(globals(), '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
+        self.ui.readconfig(self.ui.config('notify', 'config'))
+        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.sio = templater.stringio()
+        self.subs = self.subscribers()
+
+        mapfile = self.ui.config('notify', 'style')
+        template = (self.ui.config('notify', hooktype) or
+                    self.ui.config('notify', 'template'))
+        self.t = templater.changeset_templater(self.ui, self.repo, mapfile,
+                                               self.sio)
+        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 path and 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 = templater.email(addr.strip())
+        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 = subs.keys()
+        subs.sort()
+        return subs
+
+    def url(self, path=None):
+        return self.ui.config('web', 'baseurl') + (path or self.root)
+
+    def node(self, node):
+        '''format one changeset.'''
+
+        self.t.show(changenode=node, changes=self.repo.changelog.read(node),
+                    baseurl=self.ui.config('web', 'baseurl'),
+                    root=self.repo.root,
+                    webroot=self.root)
+
+    def send(self, node, count):
+        '''send message.'''
+
+        p = email.Parser.Parser()
+        self.sio.seek(0)
+        msg = p.parse(self.sio)
+
+        def fix_subject():
+            '''try to make subject line exist and be useful.'''
+
+            subject = msg['Subject']
+            if not subject:
+                if count > 1:
+                    subject = _('%s: %d new changesets') % (self.root, count)
+                else:
+                    changes = self.repo.changelog.read(node)
+                    s = changes[4].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] + '...'
+            del msg['Subject']
+            msg['Subject'] = subject
+
+        def fix_sender():
+            '''try to make message have proper sender.'''
+
+            sender = msg['From']
+            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)
+            del msg['From']
+            msg['From'] = sender
+
+        fix_subject()
+        fix_sender()
+
+        msg['X-Hg-Notification'] = 'changeset ' + short(node)
+        if not msg['Message-Id']:
+            msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
+                                 (short(node), int(time.time()),
+                                  hash(self.repo.root), socket.getfqdn()))
+
+        msgtext = msg.as_string(0)
+        if self.ui.configbool('notify', 'test', True):
+            self.ui.write(msgtext)
+            if not msgtext.endswith('\n'):
+                self.ui.write('\n')
+        else:
+            mail = self.ui.sendmail()
+            mail.sendmail(templater.email(msg['From']), self.subs, msgtext)
+
+    def diff(self, node):
+        maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
+        if maxdiff == 0:
+            return
+        fp = templater.stringio()
+        commands.dodiff(fp, self.ui, self.repo, node,
+                        self.repo.changelog.tip())
+        difflines = fp.getvalue().splitlines(1)
+        if maxdiff > 0 and len(difflines) > maxdiff:
+            self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
+                           (len(difflines), maxdiff))
+            difflines = difflines[:maxdiff]
+        elif difflines:
+            self.sio.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
+        self.sio.write(*difflines)
+
+def hook(ui, repo, hooktype, node=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)
+    if not n.subs: return True
+    node = bin(node)
+    if hooktype == 'changegroup':
+        start = repo.changelog.rev(node)
+        end = repo.changelog.count()
+        count = end - start
+        for rev in xrange(start, end):
+            n.node(repo.changelog.node(rev))
+    else:
+        count = 1
+        n.node(node)
+    n.diff(node)
+    n.send(node, count)
+    return True