Even when using feature/staging-branches in the development, one might want to link bug tickets to the commits that fix the bug in master. When the fix is in the features repo, or in the staging area, it is very likely that the commit id of the bug fix changes before it enters master.

To still have the correct link in the bug ticket, we had the idea to automatically add a link from the bug ticket in trac to a certain commit if the commit has the text "Fixes: #8320" or some variant given that it matches to a certain regex. To accomodate this, I have written a git hook that adds a comment to the bug in trac when such a line in detected: "Fixed in master at [deadbeef/lyxgit]". The exact regex is customizable, also we can think about adding keywords automatically, doing the same for branch, and to notify the committer by e-mail.

Attached is the first version of the script.

Can I proceed adding this to the git hook on the server ?

Anyone willing to review the python code, and to follow the steps I have to take in the server configuration ?

Vincent
#!/usr/bin/env python

import sys, os, re
from trac.env import open_environment
from trac.ticket import Ticket
from trac.db import with_transaction, DatabaseManager
from trac.util.datefmt import utc
from datetime import datetime
from trac.versioncontrol import RepositoryManager
import subprocess
from StringIO import StringIO
from collections import defaultdict

# A temporary file that receives the information from git. Should
# typically be in /var/tmp.
POST_RECEIVE_LOGFILE = 'hooks.post-receive-logfile'

# Path to the trac directory which contains the VERSION file.
TRAC_ENVIRONMENT_PATH = 'hooks.trac-environment'

# The name here should match with the repository name as configured 
# in Trac: Admin->Repositories->Name.
TRAC_REPOSITORY_NAME = 'hooks.trac-repository-name'

# Defines the regex that is used to find the command (e.g., Fixes)
# and the ticket indication (e.g., #1). The first group should match
# with the ticket number.
TRAC_TICKET_REGEX = 'hooks.trac-ticket-regex'

def get_commits(old_rev, new_rev):
        p = subprocess.Popen(['git', 'log', '--pretty=format:%H', '--reverse', 
                                '%s..%s' % (old_rev, new_rev)],
                                stdout=subprocess.PIPE)
        return p.stdout.read().split('\n')

def parse_post_receive_line(l):
        return l.split()

def post_receive(trac_updater):
        lines = sys.stdin.readlines()
        commits = {}
        for line in lines:
                old_rev, new_rev, ref_name = parse_post_receive_line(line)
                commits[ref_name] = get_commits(old_rev, new_rev)
        return process_commits(trac_updater, commits)

def get_commit_info(hash):
    p = subprocess.Popen(['git', 'show', '--pretty=format:%s%n%h%n%ae%n%an', 
'-s', hash],
                         stdout = subprocess.PIPE)
    s = StringIO(p.stdout.read())
    def undefined():
        return 'undefined'
    info = defaultdict(undefined)
    for k in ['message', 'hash', 'email', 'name']:
        info[k] = s.readline().strip()
    return info

def get_commit_log(hash):
    p = subprocess.Popen(['git', 'show', '--pretty=format:%B', '-s', hash],
                        stdout = subprocess.PIPE)
    s = StringIO(p.stdout.read())
    regex = git_config_get(TRAC_TICKET_REGEX)
    for line in s:
        match = re.match(regex, line);
        if match:
                return match.group(1)
    return None

def process_commits(trac_updater, commits):
        for ref_name in commits.keys():
                for i, commit in enumerate(commits[ref_name]):
                        info = get_commit_info(commit)
                        rname = ref_name
                        t = "refs/heads/master"
                        if not rname.startswith(t):
                                continue
                        ticket_id = get_commit_log(commit);
                        if ticket_id:
                                comment = compose_comment(ref_name, commit[0:8])
                                author = re.match('(.*)@.*', 
info['email']).group(1)
                                trac_updater.pushComment(ticket_id, author, 
comment)

def git_config_get(name):
        p = subprocess.Popen(['git', 'config', '--get', name], 
                                stdout=subprocess.PIPE)
        return p.stdout.read()[:-1]

def compose_comment(ref_name, hash):
        reponame = git_config_get(TRAC_REPOSITORY_NAME)
        commit_link = '[%s/%s]' % (hash, reponame)
        return 'Fixed in %s at %s.' % (ref_name[len('refs/heads/'):], 
commit_link)

class TracUpdater :
        """ A class that add comments to trac tickets"""
        env = None

        def __init__(self):
                trac_environment = git_config_get(TRAC_ENVIRONMENT_PATH)
                self.env = open_environment(trac_environment)

        def pushComment(self, ticket_id, author, comment):
                @with_transaction(self.env)
                def do_something(db):
                        ticket = None
                        try:
                                ticket = Ticket(self.env, ticket_id, db)
                        except: 
                                print 'Warning: Ticket does not exist'
                                return
                        ticket.save_changes(author, comment, datetime.now(utc))
                        
trac_updater = TracUpdater()
log_file_path = git_config_get(POST_RECEIVE_LOGFILE)
with open(log_file_path, 'a') as log_file:
        post_receive(trac_updater)


Reply via email to