Git

Git is a widely used version control system that is commonly used in modern software development. It is a software tool that helps developers to track changes in their code base over time, enabling them to collaborate with other developers on the same project more efficiently.

In Git, developers create a “repository” that stores all the source code files and related assets for their project. As developers work on their code, they can make changes to the repository, which Git tracks and records in a log of “commits.” Each commit includes a message that describes the changes made to the code.

Git hooks

Git hooks are scripts that run automatically every time a particular event occurs in a Git repository. They let you customize Git’s internal behavior and trigger customized actions at key points in the development life cycle. These events could include committing changes to the repository, pushing changes to a remote repository, or merging changes from a branch into another branch.

How git hooks work

Git determines which hooks to run based on their names and the events that trigger them. The names of the hooks correspond to specific Git events, such as committing changes or pushing changes to a remote repository. When a Git event occurs, Git will automatically run any hooks with the corresponding name in the .git/hooks directory.

Implementing git hooks

Git hooks are a built-in feature that come with every Git repository.  When you initialize a new repository with git init, Git populates the hooks folder with template files. Here are the steps to enable them:

Navigate to the hooks directory

we need to change our directory to the following:

$ cd /repository/.git/hooks

Install your hook

To enable the hook scripts, simply remove the .sample extension from the file name. Git will automatically execute the scripts based on the naming. To do so, we can use the following command:

mv hookname.sample hookname

After that, you have to provide the execute permission for the hook, using the following command:

chmod +x hookname

Select a language to write your hook scripts

The default files are written as shell scripts, but you can use any scripting language you are familiar with as long as it can be run as an executable. This includes Bash, Python, Ruby, Perl, Rust, Swift, and Go. In order to write a script first, you need to specify that in the first line of the script.

The first line of the script will be:

Bash : #!/bin/bash

Shell : #!/bin/sh

Python : #!/usr/bin python

Write your script

Each hook will contain a predefined script. You can modify this script as necessary, or you can write your own script from scratch and git will execute it before performing any actions or events.

Client-side hooks

Client-side or local hooks are scripts that are executed on the local machine when certain Git events occur, such as a commit or a push. These hooks can be used to perform checks or enforce policies on the code being committed or pushed.

Here are some of the most commonly used client-side hooks:

pre-commit

The pre-commit hook is a client-side Git hook that is executed before a commit is made. It can be used to enforce code formatting, check for syntax errors, or run tests.

Here is an example of a pre-commit hook that checks for console statements in the code:

#!/bin/sh

# Check for console statements in the code being committed
if grep -q -E 'console\.' $(git diff --cached --name-only | grep '.js$'); then
   echo "Console statements detected in code. Please remove any console statements before committing."
   exit 1
fi

exit 0

This script when run will print the message on the console in case the commit is aborted. In this case, git aborted the commit operation due to the exit code 1, which is a non-zero exit status.

Here is the output when any commit operation is done on the repository:

prepare-commit-msg

The prepare-commit-msg hook is executed after the commit message is created but before the commit is actually created. It can be used to modify the commit message or add additional information to it. It is useful for adding Ticket-ID, Branch name, Style Checklist, and Rules for commits.

Here is an example for prepare-commit-msg hook which adds branch name to the commit:

#!/bin/sh

# Get the name of the current branch
BRANCH_NAME=$(git symbolic-ref --short HEAD)

# Read the commit message from the file
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat $COMMIT_MSG_FILE)

# Prepend the branch name to the commit message
echo "[Branch: $BRANCH_NAME] $COMMIT_MSG" > $COMMIT_MSG_FILE

This script uses the git symbolic-ref command to get the name of the current branch and reads the commit message from the file specified as the first argument to the hook. This can be useful for providing additional context when looking at the commit history in complex projects with many branches.

Here is the output when any commit operation is done on the repository:

commit-msg

The commit-msg hook is executed after the user has entered a commit message but before the commit is finalized. The purpose of the commit-msg hook is to allow you to perform checks on the commit message and to ensure that it meets certain standards or guidelines.

Here is an example for commit-msg hook that checks for the presence of a valid ticket ID (like Jira ticket ID) in the commit message:

#!/bin/sh

COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat $COMMIT_MSG_FILE)

# Check for the presence of a valid ticket ID in the commit message
if ! echo "$COMMIT_MSG" | grep -qE '\b[A-Z]+-[0-9]+\b'; then
    echo "Commit message does not include a valid ticket ID." >&2
    exit 1
fi

This script when run will print the message on the console in case the commit is aborted. In this case, git aborted the commit operation due to the exit code 1, which is a non-zero exit status.

Here is the output when any commit operation is done on the repository:

post-commit

The post-commit hook is run after a commit is made. The purpose of the post-commit hook is to allow you to perform additional actions after a commit has been made, such as triggering a build process or sending notifications.

Here is an example for post-commit hook which sends notification to your slack channel when a commit is done:

#!/bin/sh

# Set the Slack webhook URL
SLACK_WEBHOOK_URL="<your_slack_webhook_url>"

# Get the name of the current branch
BRANCH_NAME=$(git symbolic-ref --short HEAD)

# Get the commit message
COMMIT_MSG=$(git log --format=%B -n 1 HEAD)

# Get the name of the committer
COMMITTER_NAME=$(git log --format="%an" -n 1 HEAD)

# Create the payload for the Slack message
PAYLOAD="payload={\"channel\": \"#general\", \"username\": \"Git Bot\", \"text\": \"New commit on branch *$BRANCH_NAME* by *$COMMITTER_NAME*:\n\`\`\`$COMMIT_MSG\`\`\`\"}"

# Send the message to Slack using curl
curl -X POST --data-urlencode "$PAYLOAD" "$SLACK_WEBHOOK_URL"

I have created a slack bot and added it to one of my slack channel, And provided the slack webhook url in the above code. When a commit is done, the notification will be sent to the slack channel.

Here is the output on slack channel when any commit operation is done on the repository:

pre-rebase

The pre-rebase hook is a script that runs before a git rebase command is executed. This hook is useful for enforcing certain rules or checks before allowing a branch to be rebased.

Here is an example of a pre-rebase hook that prevents rebasing a branch that is behind the master branch:

#!/bin/sh

# Get the name of the current branch
BRANCH=$(git symbolic-ref --short HEAD)

# Check if the current branch is behind master
if [ $(git rev-list --count $BRANCH ^master) -gt 0 ]; then
  echo "ERROR: Branch $BRANCH is behind the master branch. Please pull changes from master and try again."
  exit 1
fi

exit 0

This script uses git rev-list to check if the current branch is behind the master branch. If the current branch is behind master, the hook prints an error message and exits with a status of 1, which prevents the rebase from proceeding. If the current branch is not behind master, the hook exits with a status of 0, which allows the rebase to proceed.

Here is the output when trying to rebase a branch with master branch if the current branch is behind the master branch:

post-checkout

The post-checkout hook is executed after a branch is checked out. It can be used to trigger post-checkout actions, such as updating files, generating documentation, rebuilding the project.

Here is an example of a post-checkout hook that runs npm install when switching to a new branch:

#!/bin/sh

# Run npm install after checkout
if [ -f "package.json" ]; then
  echo "Running npm install..."
  npm install --quiet
fi

exit 0

This script checks if the package.json file exists in the root of the repository, and if it does, it runs npm install to install any dependencies defined in the file.

Here is the output when checkout operation is done on the repository:

Server-side hooks

Server-side or remote hooks are scripts that are executed on the server when certain Git events occur, such as a push or a receive. These hooks can be used to automate tasks or enforce policies for a repository.

(Note: To show server side hooks, I created two directories, Git-Hooks-Client and Git-Hooks-Server. Installed git in both directories, but initialized Git-Hooks-Server as a Bare Git Repository. On the Git-Hooks-Server repository, add the server-side hooks.)

Here are some of the most commonly used server-side hooks:

pre-receive

The pre-receive hook is a script that runs on the Git server before it accepts incoming changes from a client. It allows you to enforce rules or policies on the changes being pushed to the server.

Here is an example of a pre-receive hook that prevents a user from pushing changes to the master branch:

#!/bin/bash

while read oldrev newrev refname; do
  if [[ "$refname" == "refs/heads/master" ]]; then
    echo "Pushes to the master branch are not allowed."
    exit 1
  fi
done

exit 0

This script checks if the user is trying to push changes to the master branch. If so, it prints an error message and exits with a non-zero status code, which causes the push to be rejected.

Here is the output when any push operation is done on the client repository:

update

The update hook is called after the pre-receive hook, and is used to enforce more fine-grained control over which references are updated. It allows you to prevent certain branches from being updated or to perform additional checks on the changes being pushed.

Here is an example of an update hook that just echoes a message to the console to ensure that it works:

#!/bin/bash

echo "The update hook is running"

exit 0

Here is the output when any push operation is done on the client repository:

post-receive

The post-receive hook is executed after a push has been accepted by the server. It is typically used for post-processing tasks, such as sending email notifications, updating a database, or triggering build and deployment pipelines. This hook works just like the post-commit hook.

Here is an example of post-receive hook just to ensure that it works:

#!/bin/bash

while read oldrev newrev refname
do
  if [ "$refname" = "refs/heads/master" ]; then
    echo "Push to master branch detected"
    # do something here, such as deploy the changes to a server
  fi
done

Here is the output when any push operation is done on the client repository:

Precis:

Creating custom scripts that Git can run automatically at these key points in the development life-cycle allows you to enforce coding standards, run automated tests, and enforce code base quality. As a result, developers save time and effort while also ensuring that the code base remains consistent. Although setting up custom hooks can be challenging initially, the investment is usually worthwhile, as it saves the team manual work later on.