Sending commit notifications using Git post-receive hooks

I make heavy use of Git, and have plugins that allow me to view my commits when viewing issues in JIRA. Unfortunately these plugins rely on Lucene indexes which has proven to be a bit of an issue when archiving projects (or maintaining a HTML fallback).

There are various post-receive hooks out there for sending mail notifications out whenever someone runs 'git push', however they're generally tailored towards notifying a group of developers.

I simply wanted the equivalent of 'git log' to appear within my JIRA activity flow on any issue which is mentioned in the commit message.

This documentation provides a python based post-receive hook intended to do just that, and also documents exactly how to go about applying that hook to all existing and future repos on your server.

 

The aim

There are three main objectives

  • Email commit details into JIRA whenever commits are pushed
  • Configure the server so I don't need to remember to add the hook whenever I create a repo
  • Apply the hook to all existing repos

Because the script will be server-wide, all config should either be within the script or set as a global config option in git.

 

Desired Output

Whilst we may add some formatting to aid display in JIRA, the output need be no more complex than the following

commit c8fa26084f88bda953237c1a1d01a658f11b6886
Author: B Tasker <git@example.com>
Date: Wed Oct 1 11:59:13 2014 +0100

Test commit. See TESTPROJ-4

test123 | 1 +
1 file changed, 1 insertion(+)

 

The Script

I've written post-receive hooks before, so it made sense to use this as a starting point, with a few tweaks and adjustments

#!/usr/bin/env python
#
# Copyright (C) 2014 B Tasker
#
# Based on earlier Pivotal Tracker hook
# https://github.com/bentasker/notify-webhook-pivotal-tracker
# Released under GNU GPL V2 - http://www.gnu.org/licenses/gpl-2.0.txt

import sys
import re
import os
import subprocess
import smtplib
from email.mime.text import MIMEText
from socket import gethostname
import json

RECIPIENT = 'foo@example.com' # Set this to the email you want to send to
FROM = 'bar@example.com' # Set this to the email you want to send from
COMMIT_URL="https://github.com/bentasker/%s/commit/%s" # String replacement will occur later
DIFF_URL='https://github.com/bentasker/%s/commit/%s'

# So for a repo named 'PHPCredlocker' and a commit of c8fa26084f88bda953237c1a1d01a658f11b6886
# We'd get
#
# https://github.com/bentasker/PHPCredlocker/commit/c8fa26084f88bda953237c1a1d01a658f11b6886

def git(args):
args = ['git'] + args
git = subprocess.Popen(args, stdout = subprocess.PIPE)
details = git.stdout.read()
details = details.strip()
return details

def get_config(key):
details = git(['config', '%s' % (key)])
if len(details) &gtl 0:
return details
else:
return None

def get_repo_name():
if git(['rev-parse','--is-bare-repository']) == 'true':
name = os.path.basename(os.getcwd())
if name.endswith('.git'):
name = name[:-4]
return name
else:
return os.path.basename(os.path.dirname(os.getcwd()))

def get_revisions(old, new):
if old == '0000000000000000000000000000000000000000':
git = subprocess.Popen(['git', 'log','--stat', '--pretty=medium', '--reverse', '%s' % (new)], stdout=subprocess.PIPE)
else:
git = subprocess.Popen(['git', 'log','--stat', '--pretty=medium', '--reverse', '%s..%s' % (old, new)], stdout=subprocess.PIPE)

output = git.stdout.read()
sections = output.split('\n\n')
revisions = []
s = 0
while s < len(sections):
lines = sections[s].split('\n')

# first line is 'commit HASH\n'
props = {'id': lines[0].strip().split(' ')[1]}

# read the header
for l in lines[1:]:
key, val = l.split(' ', 1)
props[key[:-1].lower()] = val.strip()

# read the commit message
props['message'] = sections[s+1]
props['full'] = "Repo: "+get_repo_name()+"\nHost:"+gethostname()+"\n\n"+sections[s]
props['full'] += "\n\nCommit Message: " + sections[s+1].strip() + "\n\n" +sections[s+2]
props['sections'] = json.dumps(sections) # Useful for debug!
props['commiturl'] = COMMIT_URL % (get_repo_name(),props['id'])
props['commitdiff'] = DIFF_URL % (get_repo_name(),props['id'])

revisions.append(props)
s += 3
return revisions

# Cycle through each revision in the push
def process_revisions(old, new, ref):
revisions = get_revisions(old, new)
for r in revisions:
mail(r)

# Mail out the commit
def mail(r):
msg = MIMEText("{quote}"+ r['full'] + "{quote}\n\n[View Commit|"+
r['commiturl']+"] | [View Changes|" + r['commitdiff']+"]")
msg['Subject'] = r['message']
msg['From'] = FROM
msg['To'] = RECIPIENT
s = smtplib.SMTP('localhost')
s.sendmail(FROM,RECIPIENT, msg.as_string())
s.quit()

# Grab some information about the repo
REPO_NAME = get_repo_name()
REPO_DESC = ""
try:
REPO_DESC = get_config('meta.description') or open('description', 'r').read()
except Exception:
pass

REPO_OWNER_NAME = get_config('meta.ownername')
REPO_OWNER_EMAIL = get_config('meta.owneremail')

if REPO_OWNER_NAME is None:
REPO_OWNER_NAME = git(['log','--reverse','--format=%an']).split("\n")[0]
if REPO_OWNER_EMAIL is None:
REPO_OWNER_EMAIL = git(['log','--reverse','--format=%ae']).split("\n")[0]

EMAIL_RE = re.compile("^(.*) <(.*)>$")

if __name__ == '__main__':
for line in sys.stdin.xreadlines():
old, new, ref = line.strip().split(' ')
data = process_revisions(old, new, ref)

 

Configuration is performed in script - we simply need to set where to send the mail and who it should appear to come from (so that JIRA can attribute the resulting comment to a dedicated user). As a convenience, links are included so that the commit can be viewed, so the URL for this needs configuring too

Assuming you're using GitPHP, your config block may look something like this

RECIPIENT = 'foo@example.com'
FROM = 'git@example.com'
COMMIT_URL="http://repos.example.com/projects/%s.git/commit/%s"
DIFF_URL='http://repos.example.com/projects/%s.git/commitdiff/%s'

You can test the script by dropping it into the hooks directory of a git repo and then pushing to it. Any exceptions should be displayed to you, and your commit message will be used as a subject line in the email - so if you&pos;re emailing into JIRA and mention an issue reference, a new comment should be added with the commit details

 

Making the post-receive script global

All my repos are owned by a single user, so making sure the script is applied to new repos is as simple as defining a template directory and dropping the script into there

mkdir -p ~/.git-template/hooks
git config --global init.templatedir '~/.git-template'

Anything created within that template directory will also be populated to new repositories, so if we save our post-receive script (and make it executable!) all new repos will have a copy

cp ~/post-receive ~/.git-template/hooks/
chmod +x ~/.git-template/hooks/post-receive

 

Changing existing Repos

An important thing to note about the change we made above - if an existing repo is re-init'd then any existing config won't be overwritten - so any repo with an existing post-receive will need to be manually update (we'll cover that in a moment).

Assuming all your repositories are stored within one directory (~/Repos), and are all bare (if not, you'll need to amend!), you can run the following (it shouldn't break or overwrite anything, but always backup first!)

cd ~/Repos
for i in *.git; do cd "$i"; git init --bare; cd ..; done

 

So what do we do if we want to update repos that already have a post-receive? It's possible in the future we'll want to update the template and then move the changes in, but re-init won't overwrite the existing script

Again, assuming your repos are stored within ~/Repos

find ./ -name post-receive -exec cp -f /home/$USER/.git-template/hooks/post-receive {} \;

 

Conclusion

Setting up a post-receive hook is pretty straight-forward, and git's support for global templates (introduced in version 1.7.1) means that it's incredibly easy to ensure that all future repositories are configured the way that you want them to be.

JIRA's a valuable tool, but it'd be wrong to assume that it will always be the best tool for the job - having Git email notifications into the comment flow helps to ensure that I'll still be able to access and use data created in JIRA, even if I later decide to use a different project management system - records from a software development project aren't always much use without a record of which commits are considered related!