Skip to content

Instantly share code, notes, and snippets.

@rszalski
Created August 3, 2010 12:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rszalski/506242 to your computer and use it in GitHub Desktop.
Save rszalski/506242 to your computer and use it in GitHub Desktop.
Post-Commit Hook for GIT - Used with Fixx Issue Tracker. Allows for automatic resolving, closing and commenting issues when commiting.
#!E:\Python31\python
# Created By: Radosław Szalski <radoslaw.szalski@gmail.com>
# Tested with Python 3.1, git v. 1.7.1 (using msysgit) and Fixx 1.9
# Rewriting to Python 2.X should be straightforward
# Read more - http://docs.python.org/py3k/whatsnew/3.1.html
# This hook script is called after a commit is made
# It's purpose is to update FIXX's (bug tracker) issues when they are fixed/resolved/commented
# Depending on HOW you format your commit message it does different things
# you can:
# post only comment to issue
# resolve issue & post comment
# resolve issue & post comment & close issue
# I use Fixx not only for bug tacking, but for general issues/task tracking
# thus I often supply 'Fixed' as a Resolution but also 'Done' etc.
# you probably won't resolve issues as 'Won't fix' or 'Not a bug' directly from git,
# nonetheless you are free to use whatever resolution names you have defined
# For the script to work your commit message needs to contain (at least) [#{num}] at the beginning
# This will only post a comment to an issue
# example: git commit -m '[#666] some message here'
# To resolve an issue you supply the resolution name before #{num}
# example: git commit -m '[Fixed #666] some message here'
# To resolve & close an issue you add 'c' right after #{num}
# example: git commit -m '[Fixed #666c] some msg here'
# Note that comments are always posted
# You can supply any valid resolution name that is set for your project
# they can be lowercase or upper case, you can put spaces before and after a resolution
# but note, that resolution itself has to be intact
# example: '[ Won't Fix #666] msg'
# Recommendation: use only 1 space to separate Resolution Name from issue ID
import re
import subprocess
# I find httplib2 easier to use than Python's urllib
# You can get it here - http://code.google.com/p/httplib2/
import httplib2
import json
# CONFIGURATION - change those
# {REMEMBER TO CHANGE THE SHEBANG TO REFLECT YOUR ENVIRONMENT}
# you can get project's ID by going to Projects->{Project} in Fixx and checking the URI
# example: http://localhost:9000/projects/2
FIXX_URI = 'http://localhost:9000' # (no trailing '/') Address of the Fixx installation, this one is an example
PROJECT_ID = 2
USERNAME = 'username'
PASSWORD = 'password'
git = subprocess.Popen(['git', 'log', '-1', '--pretty=format:"%h %s |author|%cn|date|%ci"'], stdout = subprocess.PIPE)
# The response is: "{short-hash} {commit_msg} |author|{author}|date|{date}"
resp = git.stdout.read()[1:-2] # Removing unnecessary characters
# TODO verbose
pID = re.compile(r'(?P<hash>\w+)\s\[(?P<resolution>.*)\s?#(?P<id>\d+)(?P<close>c|C)?.*\](?P<msg>.*)\|author\|(?P<author>\w+)\|date\|(?P<date>.*)')
matches = pID.search(resp.decode('utf-8'))
# Don't bother if it doesn't match
if matches is not None:
gitLog = {}
gitLog['issueResolution'] = matches.group('resolution').lower().strip()
gitLog['issueID'] = matches.group('id').strip()
gitLog['commitHash'] = matches.group('hash').strip()
gitLog['commitMsg'] = matches.group('msg').strip()
gitLog['commitAuthor'] = matches.group('author').strip()
gitLog['commitDate'] = matches.group('date').strip()
if matches.group('close') and matches.group('close').lower() == 'c':
gitLog['close'] = True
else:
gitLog['close'] = False
h = httplib2.Http()
h.follow_all_redirects = True
h.add_credentials(USERNAME, PASSWORD)
# For some weird reason, even though we supply credentials with add_credentials
# we still need to send an Authorization header, even if it's a dummy/fake one
# This probably is a bug in python 3.1 (because it happened when I used urllib as well)
# This is sent when we retrieve project resolutions
# because JSON is easier to parse than XML
headersJSON = {
'Accept': 'application/json',
'Content-type': 'application/json',
"Authorization": "Basic fake"
}
# Used for every other request
headersXML = {
'Accept': 'application/xml',
'Content-type': 'application/xml',
"Authorization": "Basic fake"
}
# The commentary that is attached to issues in Fixx
comment = """
<comment>
<text>
Resolution set to: {resolution}
Commit: {hash}
Commiter: {author}
Date: {date}
Message: {msg}
</text>
</comment>
""".format(resolution = gitLog['issueResolution'],
hash = gitLog['commitHash'],
author = gitLog['commitAuthor'],
date = gitLog['commitDate'],
msg = gitLog['commitMsg'])
# We're getting an issue with EVERY setting specified
# This is VERY important, since later we use PUT request to update the issue,
# And if we do not supply every possible Issue setting - those missing get reverted to defaults
# See API documentation for Fixx
resp, issue = h.request("{uri}/api/issues/{id}".format(uri = FIXX_URI, id = gitLog['issueID']), "GET", headers = headersXML)
issue = issue.decode('utf-8')
# We retrieve resolutions for current project, and parse them into a dictionary
def getResolutions(projectID):
resp, resolutions = h.request("{uri}/api/projects/{id}/resolutions".format(uri= FIXX_URI, id = projectID), "GET", headers = headersJSON)
decoder = json.JSONDecoder()
resolutionsJSON = decoder.decode(resolutions.decode("utf-8"))
resolutionsDict = {}
for res in resolutionsJSON["resolution"]:
resolutionsDict[res['name'].lower()] = res['id']
return resolutionsDict
# We're replacing only certain settings in Issue, others are not touched and preserved!
def resolveIssue(issue, issueID, resolutionID, close):
# This one probably might use some improvements
pattern = re.compile(r'</issue>')
pResolution = re.compile(r'<resolution>(\d+)</resolution>')
closePattern = re.compile(r'<closed>(\w+)</closed>')
if pResolution.match(issue):
issue = pResolution.sub('<resolution>{id}</resolution>'.format(id = resolutionID), issue)
else:
issue = pattern.sub('<resolution>{id}</resolution></issue>'.format(id = resolutionID), issue)
if close:
issue = closePattern.sub('<closed>true</closed>', issue)
# Updating Issue
resp, content = h.request("{uri}/api/issues/{id}".format(uri = FIXX_URI, id = issueID), "PUT", body = issue, headers = headersXML)
# Posting a commentary
def postComment(gitLog, comment):
resp, content = h.request("{uri}/api/issues/{id}/comments".format(uri = FIXX_URI, id = gitLog['issueID']), "POST", body = comment, headers = headersXML )
# We get resolutions in aa dictionary format
resolutions = getResolutions(PROJECT_ID)
# If there's a resolution specified, we update the issue and psot comment
# if not, we just post a comment (useful if you just want to make some updates)
if gitLog['issueResolution'] in resolutions:
resolveIssue(issue, gitLog['issueID'], resolutions[gitLog['issueResolution']], gitLog['close'])
postComment(gitLog, comment)
else:
postComment(gitLog, comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment