Server-side Hook

In addition to the commit-msg hook, you can use server-side hooks to apply policies for your project.  The server runs these scripts before and after the push.  The server-side hook, like commit-msg hook, requires Python to be installed.

In Linux and OSX, hook scripts must have executable permissions in the file system.

For information about the local hook that should be installed, see Customizing Git – Hooks ».  For further information about git hooks, see githooks.com ».

When the server handles the push from a client, the pre-receive script is run first.  When commits does not have proper Jira issue tagging, an error message from client to server is raised.

The pre-receive server-side hook requires git administrators to:

  1. Copy the pre-receive script file from the hooks/ folder in the git repository to the Git server repository hooks/ folder.
  2. Configure JIRA_HOST_NAME, JIRA_USER and JIRA_PASSWORD in the pre-receive file.


The PROJECT_KEYS setting defines an array of project keys which is compared to a ticket key from the commit message.  When Jira is not available, the hook simply checks the commit message if it contains the string pattern satisfying its declared conditions.
Pattern example:
(PROJECT_KEY1|PROJECT_KEY2|...)-\d+


The PROJECT_KEYS variable is only used when Jira is not available, otherwise, this setting in the pre-receive file is ignored. The hook will not perform any checks if the PROJECT_KEY array is empty and Jira is not available.


See the server-side hook script on the right panel or download the sample pre-receive file ↓ – make the necessary changes, and place it in the required folder.


The server-side hook script enforces users to include correct Jira tags.


Sample contents of the pre-receive file:
#!/usr/bin/python
#
# This script is intended to be run as a pre-receive hook script in a server GIT
# repository and check the presence of JIRA ticket numbers in the log messages.
#
#    - NO_JIRA_TICKET_MESSAGE (an error message returned to the user when the
#      git commit message doesn't contain a jira ticket);
#    - INVALID_JIRA_TICKET_MESSAGE (an error message returned to the user when
#      the git commit message contains an invalid jira ticket);
#    - NO_ACCESS_TO_JIRA_SERVER (an error message returned to the user when the
#      Jira server returns an 'Access denied' error);
#    - ERROR_ACCESSING_THE_JIRA_SERVER (an error message returned to the user
#      when an error occurred accessing the Jira server);
#    - JIRA_HOST_NAME (hostname of the JIRA server to look up issues);
#    - JIRA_USER (name of the JIRA user who has permission to look up issues in
#      the JIRA server);
#    - JIRA_PASSWORD (password of the JIRA user described above);

import sys
import os
import subprocess
import re
import httplib
import base64
import json

from errno import ENOENT

NO_JIRA_TICKET_MESSAGE = \
'No Jira ticket present in the commit message. \
Please include the JIRA ticket enclosed in brackets: [ABC-789].'
INVALID_JIRA_TICKET_MESSAGE = \
'Proper Jira ticket syntax was found, but none were valid tickets. \
Please check the tickets and try again.'
NO_ACCESS_TO_JIRA_SERVER = \
'Access denied to the Jira server. Please check your credentials.'
ERROR_ACCESSING_THE_JIRA_SERVER = 'Error accessing the Jira server: '

JIRA_HOST_NAME = 'jira.example.com'
JIRA_USER = 'user'
JIRA_PASSWORD = 'password'
PROJECT_KEYS = ['EXAMPLE', 'EXAMPLE']


jiraAvail = True
headers = {}
headers['Authorization'] = 'Basic %s' % base64.standard_b64encode('%s:%s' % (JIRA_USER, JIRA_PASSWORD)).replace('\n', '')
conn = httplib.HTTPSConnection(JIRA_HOST_NAME)


def git(args, **kwargs):
    environ = os.environ.copy()
    if 'repo' in kwargs:
        environ['GIT_DIR'] = kwargs['repo']
    if 'work' in kwargs:
        environ['GIT_WORK_TREE'] = kwargs['work']
    proc = subprocess.Popen(args, stdout=subprocess.PIPE, env=environ)
    return proc.communicate()


def get_log(a, b, **kw):
    entries = {}
    (results, code) = git(('git', 'log', '--pretty=oneline', "%s..%s" % (a, b)), **kw)
    lines = results.splitlines()
    for line in lines:
        (commit, msg)=line.split(' ', 1)
        entries[commit] = msg

    return entries


def check_message(message, ticket_re_pattern):
    tickets = ticket_re_pattern.findall(message)

    if not tickets:
        return NO_JIRA_TICKET_MESSAGE

    if not jiraAvail:
        return None

    for ticket in tickets:
        try:
            conn.request('GET', '/rest/api/2/issue/' + ticket, headers = headers)
            res = conn.getresponse()
            res.read()
            if res.status == 200:
                continue
            if res.status == 404:
                return INVALID_JIRA_TICKET_MESSAGE
            if res.status == 401 or res.status == 403:
                return NO_ACCESS_TO_JIRA_SERVER
            return ERROR_ACCESSING_THE_JIRA_SERVER + str(res.status)
        except IOError as e:
            errno, strerror = e.args
            return ERROR_ACCESSING_THE_JIRA_SERVER + str(errno) + ' ' + strerror

    return None


def build_ticket_re():
    global jiraAvail
    try:
        conn.request('GET', '/rest/api/2/project/', headers = headers)
        res = conn.getresponse()
        if res.status != 200:
            res.read()
            jiraAvail = False
            return build_ticket_re_for_project_list(PROJECT_KEYS)

        projects = json.loads(res.read())
        return build_ticket_re_for_project_list(map(lambda project: project['key'], projects));
    except:
        jiraAvail = False
        return build_ticket_re_for_project_list(PROJECT_KEYS)


def build_ticket_re_for_project_list(projects):
    pattern_string = '\\b('
    for idx, project in enumerate(projects):
        if (idx > 0):
            pattern_string += '|'
        pattern_string += project
        pattern_string += '-\d+?'

    pattern_string += ')\\b';

    return re.compile(pattern_string)


try:
    ticket_re_pattern = build_ticket_re()

    line = sys.stdin.read()
    (base, commit, ref) = line.strip().split()
    commits = get_log(base, commit)

    for c, msg in commits.iteritems():
        err_msg = check_message(msg, ticket_re_pattern)

        if err_msg:
            print >> sys.stderr, 'Error: %s\nCommit message:\n%s' % (err_msg, msg)
            print >> sys.stderr, 'Install pre-commit hook (https://bigbrassband.atlassian.net/wiki/spaces/GITSERVER/pages/92177150/Commit-msg+Hook) to run this check at the commit time'
            sys.exit(1)

except IOError as e:
    errno, strerror = e.args
    print("I/O error({0}): {1}".format(errno, strerror))
    sys.exit(1)

except:
    print("Unexpected error:", sys.exc_info()[0])
    sys.exit(1)

finally:
    conn.close()