Created
August 3, 2010 12:00
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!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