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)