File irker-svnpoller of Package irker

#!/usr/bin/env python
"""
irker-svnpoller - Polls a specified SVN commit and sends it to
                  an IRC channel using irkerd

Copyright (C) 2012 - 2013 by Ignacio Riquelme Morelle <shadowm2006@gmail.com>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice is present in all copies.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""

import argparse, json, os, pysvn, re, socket, sys

# Whether to send full commit messages, or just the subject
# lines.
COMMIT_SUBJECTS_ONLY = False

# Maximum number of non-empty commit message body lines to
# send to irkerd when using full commit messages. This value
# is only used when COMMIT_SUBJECTS_ONLY is set to False.
# If there are more non-empty lines than this value, "(Log
# message trimmed)" will be added in a new line at the end.
# When set to 0, it behaves as if COMMIT_SUBJECTS_ONLY were
# set to True.
MAX_COMMIT_BODY_LINES = 5

# Hostname to use when connecting to an irkerd instance.
IRKER_HOST = 'localhost'

# Port number to use when connecting to an irkerd instance.
IRKER_PORT = 6659

# The commit notification template. You probably do not
# need to change this.
TEMPLATE = "%(bold)s%(project)s:%(bold)s %(green)s%(author)s%(reset)s * r%(bold)s%(revision)s%(bold)s /%(files)s%(bold)s:%(bold)s %(log)s"

# The template used for continuation lines for full commit
# messages.
TEMPLATE_BODY = "%(bold)s%(project)s:%(bold)s %(line)s"

# The template used for the "(Log message trimmed)" line.
TEMPLATE_TRIMMED = "%(bold)s%(project)s:%(bold)s %(gray)s(Log message trimmed.)"

# No user-serviceable parts below this line.

debug = False
simulate = False
rangemode = False

IRC_FORMAT_CONSTANTS = {
    'bold': '\x02',
    'green': '\x033',
    'blue': '\x032',
    'yellow': '\x037',
    'gray': '\x0314',
    'reset': '\x0F',
}

class IrkerSVNSource:
    "Represents a SVN repository from which irker notifications are generated."
    def __init__(self, project, repository):
        self.project = project
        self.repository = repository
        self._client = pysvn.Client()
        self._template = TEMPLATE
        self._body_template = TEMPLATE_BODY
        self._trim_template = TEMPLATE_TRIMMED

    def list_revisions_since(self, revision):
        "Return a list of all revisions on the remote repository newer than "
        "the one specified, or an empty list if there are no such."
        if type(revision) != type(int()):
            revision = int(revision)
        log = self._client.log(
                self.repository,
                pysvn.Revision(pysvn.opt_revision_kind.head),
                pysvn.Revision(pysvn.opt_revision_kind.head),
                True)[0]
        if not log:
            sys.stderr.write("Could not fetch HEAD revision info for %s\n" %
                    self.repository)
            return []
        head = log['revision'].number
        if head <= revision:
            sys.stderr.write("Revision %d is greater than HEAD (%d)\n" %
                    revision, head)
            return []
        return range(revision + 1, head + 1)

    def generate_notification(self, revision):
        "Generate a message for irkerd deliver from the given commit."
        if type(revision) != type(int()) and revision.upper() == 'HEAD':
            revobj = pysvn.Revision(pysvn.opt_revision_kind.head)
        else:
            revobj = pysvn.Revision(pysvn.opt_revision_kind.number, revision)
        log = self._client.log(self.repository, revobj, revobj, True)[0]
        if 'author' in log:
            author = log['author']
        else:
            author = '<Unknown>'
        if 'message' in log:
            if COMMIT_SUBJECTS_ONLY:
                message = log['message'].splitlines()[0]
            else:
                message_lines = []
                format_vars = IRC_FORMAT_CONSTANTS
                format_vars['project'] = self.project
                line_count = 0
                for line in log['message'].splitlines():
                    # Skip empty lines.
                    if line.strip() == '':
                        continue
                    line_count = line_count + 1
                    # Always let the subject line through.
                    if line_count > MAX_COMMIT_BODY_LINES + 1:
                        message_lines.append(self._trim_template % format_vars)
                        break
                    format_vars['line'] = line
                    message_lines.append(self._body_template % format_vars)
                # Force a newline at the start when showing full commits,
                # so the stat summary and the subject are on different lines.
                message = "\n" + "\n".join(message_lines)
        else:
            message = '<Missing commit message>'
        format_vars = IRC_FORMAT_CONSTANTS
        format_vars.update({
            'project': self.project,
            'branch': '', # unused for SVN
            'module': '', # unused for SVN
            'revision': log['revision'].number,
            'author': author,
            'log': message,
            'files': self._format_paths(log['changed_paths'])
        })
        msg = self._template % format_vars
        return msg

    def _format_paths(self, paths_dict):
        "Format a commit's changed paths list for display in a similar "
        "fashion to that of CIA bots."
        prefix, relative_paths = self._consolidate_paths(paths_dict)
        filelist_str = ' '.join(relative_paths)
        if len(filelist_str) > 60:
            filelist_str = self._format_path_summary(relative_paths)
        if prefix.startswith('/'):
            prefix = prefix[1:]
        if filelist_str:
            return "%s (%s)" % (prefix, filelist_str)
        return prefix

    def _consolidate_paths(self, paths_dict):
        "Return the common prefix for a commit's changed paths and the "
        "relative paths for each affected file."
        paths = []
        for entry in paths_dict:
            paths.append(entry['path'])
        # Optimization: if we only have one file, don't waste CPU on any of the other
        # stuff we do to pretend to be CIA.
        if len(paths) == 1:
            return paths[0], []
        prefix = re.sub("[^/]*$", "", os.path.commonprefix(paths))
        relative_paths = []
        for path in paths:
            relative = path[len(prefix):].strip()
            if relative == '':
                relative = '.'
            relative_paths.append(relative)
        return prefix, relative_paths

    def _format_path_summary(self, relative_paths):
        "Return a changed paths count summary for display."
        dirs = {}
        for path in relative_paths:
            dirs[os.path.split(path)[0]] = True
        if len(dirs) <= 1:
            return "%d files" % len(relative_paths)
        return "%d files in %d dirs" % (len(relative_paths), len(dirs))


def deliver_to_irker(uris, message):
    "Delivers the given notification for one or more channels to irkerd."
    envelope = json.dumps({ "to": uris, "privmsg": message })
    if debug:
        print envelope
    if not simulate:
        (host, port) = (IRKER_HOST, IRKER_PORT)
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.sendto(envelope + "\n", (host, port))
        finally:
            sock.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', action='store_true', dest='debug',
                        help="show extra debugging information")
    parser.add_argument('-n', action='store_true', dest='simulate',
                        help="simulate, don't send anything to irker")
    parser.add_argument('-r', action='store_true', dest='rangemode',
                        help="deliver notifications for all revisions newer than the specified revision up to HEAD")
    parser.add_argument('project_id')
    parser.add_argument('repository_URI')
    parser.add_argument('revision_or_HEAD')
    parser.add_argument('channel_URI', nargs='*')
    args = parser.parse_args()
    debug = args.debug
    simulate = args.simulate
    rangemode = args.rangemode
    source = IrkerSVNSource(args.project_id, args.repository_URI)
    try:
        rev_range = []
        if rangemode:
            rev_range = source.list_revisions_since(args.revision_or_HEAD)
        else:
            rev_range = [ args.revision_or_HEAD ]
        for rev in rev_range:
            message = source.generate_notification(rev)
            deliver_to_irker(args.channel_URI, message)
    except pysvn.ClientError, e:
        sys.stderr.write("pysvn.ClientError: %s\n" % e.message)

# kate: indent-mode normal; encoding utf-8; space-indent on;
openSUSE Build Service is sponsored by