public
Last active

Post-Commit Hook for GIT - Used with Fixx Issue Tracker. Allows for automatic resolving, closing and commenting issues when commiting.

  • Download Gist
gistfile1.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
#!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)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.