Commits

Joongi Kim  committed a81a4b6

initial version

  • Participants

Comments (0)

Files changed (4)

File changeset/__init__.py

+from changeset import *

File changeset/notifier.py

+# -*- coding: utf-8 -*-
+#
+from StringIO import StringIO
+
+from trac.core import *
+from trac.config import BoolOption
+from trac.util.text import CRLF
+from trac.mimeview import Mimeview
+from trac.versioncontrol import IRepositoryChangeListener
+from trac.versioncontrol.api import Changeset, Node
+from trac.versioncontrol.diff import unified_diff
+from trac.notifications import NotifyEmail
+
+class ChangesetNotifyEmail(NotifyEmail):
+    template_name = 'changeset_notify_email.txt'
+
+    def __init__(self, env):
+        NotifyEmail.__init__(self, env)
+
+    def notify(self, resid, subject, diff):
+        self.data.update({
+            'diff_body': diff,
+        })
+        NotifyEmail.notify(self, resid, self._format_subject(subject))
+
+class ChangesetNotifier(Component):
+    implements(IRepositoryChangeListener)
+
+    notify = BoolOption('changesets', 'notify_changesets', 'true',
+                        """Send notifications for changeset updates.""")
+
+    _last_cset_id = None
+    
+    # IRepositoryChangeListener methods
+
+    def changeset_added(self, repos, changeset):
+        if self._is_duplicate(changeset):
+            return
+        diff = self._make_diff(repos, changeset)
+        cn = ChangesetNotifyEmail(self.env)
+        subject = self._format_subject('Changeset %s' % repos.display_rev(changeset.rev))
+        cn.notify(changeset.rev, subject, diff)
+
+    def changeset_modified(self, repos, changeset, old_changeset):
+        if self._is_duplicate(changeset):
+            return
+        # currently not used.
+
+    # Custom methods
+
+    def _is_duplicate(self, changeset):
+        # Avoid duplicate changes with multiple scoped repositories
+        cset_id = (changeset.rev, changeset.message, changeset.author,
+                   changeset.date)
+        if cset_id != self._last_cset_id:
+            self._last_cset_id = cset_id
+            return False
+        return True
+
+    def _format_subject(self, subject):
+        prefix = self.config.get('notification', 'smtp_subject_prefix')
+        if prefix == '__default__':
+            prefix = '[%s]' % self.env.project_name
+        return '%s %s' % (prefix, subject)
+
+    def _make_diff(self, repos, changeset):
+        """Generate a unified diff for the given changeset."""
+        buf = StringIO()
+        mimeview = Mimeview(self.env)
+
+        # We always compare the whole changeset from the root.
+        new_path = '/'
+        new_rev = changeset.rev
+
+        prev = repos.get_node(new_path, new_rev).get_previous()
+        if prev:
+            prev_path, prev_rev = prev[:2]
+        else:
+            prev_path, prev_rev = new_path, repos.previous_rev(new_rev)
+        data = {
+            'new_path': new_path,
+            'new_rev': new_rev,
+            'old_path': old_path,
+            'old_rev': old_rev,
+            'diff': {
+                'options': {}, # use default options
+            }
+        }
+
+        for old_node, new_node, kind, change in repos.get_changes(
+                new_path=data['new_path'], new_rev=data['new_rev'],
+                old_path=data['old_path'], old_rev=data['old_rev']):
+            # TODO: Property changes
+            # Content changes
+            if kind == Node.DIRECTORY:
+                continue
+            new_content = old_content = ''
+            new_node_info = old_node_info = ('','')
+            if old_node:
+                #if not old_node.can_view(req.perm):
+                #    continue
+                if mimeview.is_binary(old_node.content_type, old_node.path):
+                    continue
+                old_content = old_node.get_content().read()
+                if mimeview.is_binary(content=old_content):
+                    continue
+                old_node_info = (old_node.path, old_node.rev)
+                old_content = mimeview.to_unicode(old_content,
+                                                  old_node.content_type)
+            if new_node:
+                #if not new_node.can_view(req.perm):
+                #    continue
+                if mimeview.is_binary(new_node.content_type, new_node.path):
+                    continue
+                new_content = new_node.get_content().read()
+                if mimeview.is_binary(content=new_content):
+                    continue
+                new_node_info = (new_node.path, new_node.rev)
+                new_path = new_node.path
+                new_content = mimeview.to_unicode(new_content,
+                                                  new_node.content_type)
+            else:
+                old_node_path = repos.normalize_path(old_node.path)
+                diff_old_path = repos.normalize_path(data['old_path'])
+                new_path = pathjoin(data['new_path'],
+                                    old_node_path[len(diff_old_path) + 1:])
+            if old_content != new_content:
+                options = data['diff']['options']
+                context = options.get('contextlines', 3)
+                if context < 0 or options.get('contextall'):
+                    context = 3 # FIXME: unified_diff bugs with context=None
+                ignore_blank_lines = options.get('ignoreblanklines')
+                ignore_case = options.get('ignorecase')
+                ignore_space = options.get('ignorewhitespace')
+                if not old_node_info[0]:
+                    old_node_info = new_node_info # support for 'A'dd changes
+                buf.write('Index: ' + new_path + CRLF)
+                buf.write('=' * 67 + CRLF)
+                buf.write('--- %s\t(revision %s)' % old_node_info + CRLF)
+                buf.write('+++ %s\t(revision %s)' % new_node_info + CRLF)
+                for line in unified_diff(old_content.splitlines(),
+                                         new_content.splitlines(), context,
+                                         ignore_blank_lines=ignore_blank_lines,
+                                         ignore_case=ignore_case,
+                                         ignore_space_changes=ignore_space):
+                    buf.write(line + CRLF)
+
+        diff_str = buf.getvalue().encode('utf-8')
+        return diff_str

File changeset/templates/changeset_notify_email.txt

+The following changeset has been added:
+
+$diff_body
+
+-- 
+$project.name <${project.url or abs_href()}>
+$project.descr
+from setuptools import find_packages, setup
+
+PACKAGE = 'TracChangesetNotifier'
+VERSION = '0.1'
+
+setup(
+    name='TracChangesetNotifier', version=VERSION,
+    packages=find_packages(exclude=['*.tests*']),
+    entry_points = {
+        'trac.plugins': [
+            '%s = myplugs.helloworld' % PACKAGE,
+        ],
+    },
+)