Source

hgflowdock / hgflowdock.py

Full commit
"""Mercurial commit hook for flowdock.com
======================================

Notifies one or more Flowdock flows on push.  May be used as a changegroup or
incoming hook::

  [plugins]
  hgflowdock = /path/to/hgflowdock.py
  
  [hooks]
  changegroup.hgflowdock = python:hgflowdock.hook
  # or
  # incoming.hgflowdock = python:hgflowdock.hook
  
  [flowdock]
  # required: API token- find this in your flow config.  may supply multiple,
  # separated by commas
  token = 
  
  # comma-separated list of tags to attach to all commits
  tags = 
  
  # boolean; tag commits with branch names if true
  tag_branches = true
  
  # comma-separated list of branches to skip when tagging
  uninteresting_branches = default
  
  # format for commit messages; see hg help templates
  template = {desc}
  
  # timeout for API connections (python 2.6 or higher)
  timeout = 15
  
  # style for changeset links.  may be 'hgweb' or 'fisheye'
  #link_style = hgweb
  
  # base url for links, e.g. http://selenic.com/hg
  #link_base =
  
  # override name of repository for link generation. default is guessed.
  #name = repo_name
  
  #api = https://api.flowdock.com/v1/mercurial/

---

Copyright (c) Jason Kirtland <jek@discorporate.us> and contributors

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
from datetime import datetime
from os.path import basename
import sys
from urllib import urlencode
from urllib2 import Request, URLError, urlopen

from mercurial import templater
from mercurial.cmdutil import changeset_templater
from mercurial.node import bin, short

try:
    from json import dumps
except ImportError:
    from simplejson import dumps


API = 'https://api.flowdock.com/v1/hg/'
python_26 = sys.version_info >= (2, 6)


link_styles = {
    'hgweb': ('{url}/shortlog/',
              '{url}/rev/{node|short}'),
    'fisheye': ('{url}/changelog/{reponame}',
                '{url}/changelog/{reponame}?cs={node}')
    }


class Reporter(object):
    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo

        self.repo_name = ui.config('flowdock', 'name')
        if not self.repo_name:
            self.repo_name = basename(repo.root)

        # formatting for inbox message
        template = ui.config('flowdock', 'template', '{desc}')
        self.message_templater = self._make_changeset_templater(template)

        # formatting for hyperlinks
        try:
            link_style = ui.config('flowdock', 'link_style')
            website_t, changeset_t = link_styles[link_style]
            self.website_template = website_t
            self.link_templater = self._make_changeset_templater(changeset_t)
        except KeyError:
            self.website_template = None
            self.link_templater = None

    def flowdock_payload(self, ctxs):
        info = self.envelope()
        branches = set()
        for ctx in ctxs:
            changeset_info = self.changeset_info(ctx)
            info['commits'].append(changeset_info)
            branches.add(changeset_info['branch'])
        payload = dumps(info, sort_keys=True, indent=4)
        if isinstance(payload, unicode):
            payload = payload.encode('utf8')
        return payload, branches

    def envelope(self):
        repo_name = self.repo_name
        info = {
            'repository': {
                'name': repo_name,
                'slug': repo_name,
                'website': '',
                'absolute_url': '',
                'owner': '',
                },
            'commits': [],
            'user': self.ui.username(),
            }

        if self.website_template:
            url = self.ui.config('flowdock', 'link_base', '')
            website = self.website_template.replace('{url}', url)
            website = website.replace('{reponame}', repo_name)
            repo = info['repository']
            repo['website'] = repo['absolute_url'] = website
        return info

    def changeset_info(self, ctx):
        node = ctx.node()
        url = self.ui.config('flowdock', 'link_base', '')
        link = self._render_ctx(self.link_templater, ctx,
                                url=url, reponame=self.repo_name)
        ts = datetime.utcfromtimestamp(ctx.date()[0]).isoformat()
        message = self._render_ctx(self.message_templater,
                                   ctx, changes=ctx.changeset())

        info = {
            'author': ctx.user(),
            'branch': ctx.branch(),
            'files': [],
            'link': link,
            'message': message,
            'node': short(node),
            'parents': [short(p.node()) for p in ctx.parents()],
            'raw_author': ctx.user(),
            'raw_node': node.encode('hex'),
            'revision': ctx.rev(),
            'size': 0,
            'timestamp': ts,
            }

        stat = self.repo.status(ctx.parents()[0].node(), ctx.node())
        for idx, action in enumerate(('modified', 'added', 'removed')):
            for path in stat[idx]:
                info['files'].append({'type': action, 'file': path})
        return info

    def _render_ctx(self, renderer, ctx, **kwargs):
        if not renderer:
            return ''
        self.ui.pushbuffer()
        renderer.show(ctx, **kwargs)
        return self.ui.popbuffer()

    def _make_changeset_templater(self, template):
        parsed = templater.parsestring(template, quoted=False)
        ct = changeset_templater(self.ui, self.repo, False, None, None, False)
        ct.use_template(parsed)
        return ct


def deliver(ui, payload, branches):
    url = ui.config('flowdock', 'api', API)
    tokens = ui.configlist('flowdock', 'token')
    tags = ui.configlist('flowdock', 'tags')

    if ui.configbool('flowdock', 'tag_branches', True):
        keep = set(branches)
        drop = ui.configlist('flowdock', 'uninteresting_branches', 'default')
        for skip in drop:
            keep.discard(skip)
        tags.extend(sorted(keep))

    try:
        timeout = int(ui.config('flowdock', 'timeout', 15))
    except ValueError:
        timeout = 15

    if ui.debugflag:
        ui.debug("Prepared Flowdock payload:")
        ui.debug(payload)

    ui.status("hgflowdock: sending notification to %d flows\n" % len(tokens))
    for token in tokens:
        endpoint = url + token
        if tags:
            endpoint += '+' + '+'.join(tags)

        data = urlencode({'payload': payload})
        req = Request(endpoint, data)
        try:
            if python_26:
                urlopen(req, timeout=timeout)
            else:
                urlopen(req)
        except URLError, exc:
            ui.warn(str(exc))
            break


def hook(ui, repo, hooktype, node=None, **kwargs):
    if node is None:
        return

    if not ui.config('flowdock', 'token'):
        ui.warn("No API token configured for Flowdock\n")
        return

    node = bin(node)
    log = repo.changelog
    if hooktype == 'changegroup':
        start, end = log.rev(node), len(log)
        ctxs = [repo.changectx(log.node(rev)) for rev in xrange(start, end)]
    else:
        ctxs = [repo.changectx(node)]

    reporter = Reporter(ui, repo)
    payload, branches = reporter.flowdock_payload(ctxs)
    deliver(ui, payload, branches)