mercurial-trac-hook / trachook.py

#!/usr/bin/env python

# trac-post-commit-hook
# ----------------------------------------------------------------------------
# Copyright (c) 2004 Stephen Hansen, Mads Sulau Joergensen
#
# 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 mercurial import demandimport
demandimport.disable()

import re
import os
import sys
import time

from trac.env import open_environment
from trac.ticket.notification import TicketNotifyEmail
from trac.ticket import Ticket
from trac.ticket.web_ui import TicketModule
# TODO: move grouped_changelog_entries to model.py
from trac.util.text import to_unicode
# convert from float to datetime
from trac.util.datefmt import to_datetime
from trac.versioncontrol.api import NoSuchChangeset
from trac.web.href import Href

from mercurial.i18n import _
from mercurial.node import short
from mercurial import cmdutil, templater, util

_supported_cmds = {'close':      '_cmdClose',
                   'closed':     '_cmdClose',
                   'closes':     '_cmdClose',
                   'fix':        '_cmdClose',
                   'fixed':      '_cmdClose',
                   'fixes':      '_cmdClose',
                   'addresses':  '_cmdRefs',
                   're':         '_cmdRefs',
                   'references': '_cmdRefs',
                   'refs':       '_cmdRefs',
                   'see':        '_cmdRefs'}

ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
time_pattern = r'[ ]?(?:\((?:(?:spent|sp)[ ]?)?(-?[0-9]*(?:\.[0-9]+)?)\))?'
ticket_reference = ticket_prefix + '[0-9]+'+time_pattern
support_cmds_pattern = '|'.join(_supported_cmds.keys())
ticket_command =  (r'(?P<action>(?:%s))[ ]*'
                   '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
                   (support_cmds_pattern,ticket_reference, ticket_reference))

# command_re = re.compile(ticket_command)
# ticket_re = re.compile(ticket_prefix + '([0-9]+)')

command_re = re.compile(ticket_command)
ticket_re = re.compile(ticket_prefix + '([0-9]+)'+time_pattern)

class CommitHook:
    def init_env(self, project):
        self.env = open_environment(project)
    
    def __init__(self, project):
        self.init_env(project)
        
        self.repos = self.env.get_repository()
        self.repos.sync()
    
    def update(self, ctx, rev, url=None):
        # href and abs_href properties have no fset defined (see property()
        # documentation and site-packages/trac/env.py). Overwriting
        # self.env.href caused str is not callable error
        self.env._href = Href(url)
        self.env._abs_href = Href(url)

        # convert time.time() to datetime, because to_timestamp needs it
        self.now = to_datetime(time.time())
        
        # Instead of bothering with the encoding, we'll use unicode data
        # as provided by the Trac versioncontrol API (#1310).
        try:
            chgset = self.repos.get_changeset(rev)
            self.msg = "(In [%s]) %s" % (rev, chgset.message)
            self.author = chgset.author
            self.date = chgset.date
        except NoSuchChangeset:
            self.msg = "(In [%s]) %s" % (rev,  ctx.description())
            self.author = ctx.user()
            self.date = self.now
            
        self.rev = rev

        cmd_groups = command_re.findall(self.msg.lower())
        tickets = {}

        #print 'Command groups: %s' % cmd_groups

        for cmd, tkts, xxx1, xxx2 in cmd_groups:
            funcname = _supported_cmds.get(cmd.lower(), '')
            if funcname:
                for tkt_id, spent in ticket_re.findall(tkts):
                    func = getattr(self, funcname)
                    lst = tickets.setdefault(tkt_id, [])
                    lst.append([func, spent])


        #print "Tickets: %s" % tickets
        
        for tkt_id, vals in tickets.iteritems():
            tkt_id = int(tkt_id)
            spent_total = 0.0
            try:
                db = self.env.get_db_cnx()
                
                ticket = Ticket(self.env, tkt_id, db)
                for (cmd, spent) in vals:
                    cmd(ticket)
                    if spent:
                        spent_total += float(spent)

                hours_txt = ''
                if spent_total != 0.0:
                    hours_txt = 'adding %.2f hour(s) spent' % spent_total
                
                print '  %s ticket %s %s' % (self.action, tkt_id, hours_txt)

                # determine sequence number... 
                cnum = 0
                tm = TicketModule(self.env)
                for change in tm.grouped_changelog_entries(ticket, db):
                    if change['permanent']:
                        cnum += 1
                if spent_total:
                    self._setTimeTrackerFields(ticket, spent_total)
                ticket.save_changes(self.author, self.msg, self.date, db, cnum+1)
                db.commit()
                
                tn = TicketNotifyEmail(self.env)
                tn.notify(ticket, newticket=0, modtime=self.now)
            except Exception, e:
                # import traceback
                # traceback.print_exc(file=sys.stderr)
                print>>sys.stderr, 'Unexpected error while processing ticket ' \
                                   'ID %s: %s' % (tkt_id, e)
                continue
    
    def _cmdClose(self, ticket):
        ticket['status'] = 'closed'
        ticket['resolution'] = 'fixed'
        self.action = 'closing'
    
    def _cmdRefs(self, ticket):
        self.action = 'commenting'
    
    def _setTimeTrackerFields(self, ticket, spent):
        if (spent != ''):
            spentTime = float(spent)
            if (ticket.values.has_key('hours')):
                ticket['hours'] = str(spentTime)

def hook(ui, repo, hooktype, node=None, **kwargs):
    """
    Mercurial trac commit hook.
    """
    if node is None:
        raise util.Abort(_('hook type %s does not pass a changeset id') % hooktype)
    
    project = ui.config('trac-hook', 'root', None)
    url = ui.config('trac-hook', 'url', None)
    
    if project is None:
        raise util.Abort(_('you need to configure the trac-hook in your hgrc - root missing'))
    
    ctx = repo.changectx(node)
    rev = ctx.rev()
    
    try:
        # try mercurial 1.1+ API
        until = len(repo.changelog)
    except TypeError:
        # fallback to previous
        until = repo.changelog.count()         
    
    trac_hook = CommitHook(project)
    
    for r in set(range(rev, until)):
        r = short(repo.lookup(r))
        c = repo.changectx(r)
        # print 'running trac-hook for change %s:%s' % (str(c.rev()), r)
        trac_hook.update(c, r, url)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.