Setting up Git pre-commit/pre-push hook for Ktlint check

In a previous article, I talked about how to set up and make use of Ktlint in your android project. I created and made use of a sample android project available on GitHub. If you haven’t checked it, feel free to check it out if need be, so you get a prior understanding of why and what is being done in this article.

This article describes how you can automate your Ktlint check and save yourself the stress of having to manually run checks by making use of Git pre-commit/pre-push hook. It can also help you ensure that your code goes through a Ktlint check before being committed or pushed, especially when working in a team.

Personally, I prefer to have my Ktlint check run before my code is committed, as it ensures I don’t end up committing code style violations. I would add an example pre-push hook at the end of the article, just to describe how it works.

What are 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 behaviour and trigger customizable actions at key points in the development life cycle.

In simple terms, Git hooks are scripts you can write that run automatically when some events like commit, push, rebase, etc occur within your repository.

With it, you can set up scripts that run, say some Gradle commands like assemble, test, ktlintCheck etc., when you commit or push updates in your repository.

From the word, pre-commit, you’d notice the keyword pre, which means before. So basically you are creating a script that runs before a commit is done. Same for pre-push and the likes. Mind you, you could also have hooks like post-commit and post-checkout which run after a commit and checkout respectively.

Setting up pre-commit hook

I would be using the sample project from the previous article. This time, to automate the Ktlint check using Git pre-commit hook.

Creating the script

I would like to have this script in a separate folder, so I would create a scripts folder in the project directory, then create two files, pre-commit-macos and pre-commit-windows. This is so we could handle for both MacOS (Linux) and Windows platforms. If you are sure, your project would only be run on a MacOS, then one script would be enough, same for Windows.

In the pre-commit-macos file, put in the following:

#!/bin/bash

echo "Running lint check..."

./gradlew app:ktlintCheck --daemon

status=$?

# return 1 exit code if running checks fails
[ $status -ne 0 ] && exit 1
exit 0

In the pre-commit-windows file, put in the following:

#!C:/Program\ Files/Git/usr/bin/sh.exe

echo "Running lint check..."

./gradlew app:ktlintCheck --daemon

status=$?

# return 1 exit code if running checks fails
[ $status -ne 0 ] && exit 1
exit 0

If you notice, the difference between the two scripts is just Line 1. This is us telling the operating system what shell to use for the command interpretation. Below that is the command we want to run, which is the Ktlint check. Then there is a status check to know if the check fails or succeeds, which then determines what code is being passed on exit. This success or failure (0 or 1) code is what Git uses to know if it should continue with the event (commit, push, rebase, etc) that launched the hook or not.

Writing Gradle task to install the Git hooks

Next step is to write a simple Gradle task that installs these Git hooks. By installation, I mean I want to have these script placed in the right location where it should be.

You might ask, where is it suppose to be and why didn’t we just create those files there? The default Git hooks directory can be found in .git/hooks. This directory is where Git checks for possible scripts that need to be run when an event occurs within your repository. Unfortunately, this folder remains on your local environment. So, even when you are able to set this up locally on your PC by just having your pre-commit script placed directly in this folder, it won’t be beneficial to your teammates when working in a team. So, it’s advisable you have these scripts in your repository (which is accessible to everyone) and then have a Gradle script that copies this script and place it in the expected folder which is .git/hooks.

In the project app build.gradle file, you can have the following task:

import org.apache.tools.ant.taskdefs.condition.Os

//...

task installGitHook(type: Copy) {
    def suffix = "macos"
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        suffix = "windows"
    }
    from new File(rootProject.rootDir, "scripts/pre-commit-$suffix")
    into { new File(rootProject.rootDir, '.git/hooks') }
    rename("pre-commit-$suffix", 'pre-commit')
    fileMode 0775
}

In the Gradle task above, we are basically checking for what operating system we are on to know which of the scripts to use. Then we do a copy to the git hooks directory. Then we rename the file to the filename Git expects and looks out for. Meanwhile, the check for OS in the code above requires you to add the import statement to the top of your build.gradle file (if not automatically added).

So to run this task, you simply type the command below in your terminal or use the Gradle sidebar in Android Studio as we did in the previous article:

./gradle installGitHook

Once you run this, you’d notice a new file, pre-commit in the .git/hooks directory of your repository.

Lastly, we want to be safe as much as possible. What if a teammate forgets or refuses ? to run the installGitHook task? So let’s make sure the task runs nevertheless. Add the following line to your build.gradle file, probably below the installGitHook task declaration.

tasks.getByPath(':app:preBuild').dependsOn installGitHook

This is basically us instructing Gradle to run installGitHook before building the project.

Testing the pre-commit hook

Now that I’m done with the pre-commit hook setup, it’s time to test.

screencast 2020 11 17 13 34 23

As you may notice above, as soon as I ran the commit command, Git triggered the pre-commit hook which then ran the Ktlint check. The project code is well-formatted, so no errors and the files were committed successfully. If there exist any violations, the commit process would be terminated and you’d see the list of affected files as you would for when you run Ktlint check. This works exactly the same way when you use the Android Studio Version Control System.

That’s it. Simple right? Enforcing Ktlint check before commit like this will ensure you don’t commit violations and make you write better code.

Using pre-push hook to run tests

Just like we did with pre-commit, you can also set up pre-push hook which runs just before your code is pushed. To demonstrate it, I will make use of a pre-push hook to run tests for the project every time a push is initiated.

Creating the script

I can simply duplicate the existing scripts I have written for pre-commit above and rename them as pre-push-macos and pre-push-windows. Then I can simply replace the Ktlint check command we had before with gradle test command.

#!/bin/bash

echo "Running test..."

./gradlew app:test --daemon

status=$?

# return 1 exit code if running checks fails
[ $status -ne 0 ] && exit 1
exit 0

And for windows…

#!C:/Program\ Files/Git/usr/bin/sh.exe

echo "Running test..."

./gradlew app:test --daemon

status=$?

# return 1 exit code if running checks fails
[ $status -ne 0 ] && exit 1
exit 0
Updating Gradle task

Next is to update the installGitHook Gradle task we wrote earlier to include copying of the pre-push hooks to the .git/hooks directory. You can also separate them into two different Gradle tasks.

task installGitHook(type: Copy) {
    def suffix = "macos"
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        suffix = "windows"
    }

    from new File(rootProject.rootDir, "scripts/pre-commit-$suffix")
    into { new File(rootProject.rootDir, '.git/hooks') }
    rename("pre-commit-$suffix", 'pre-commit')

    from new File(rootProject.rootDir, "scripts/pre-push-$suffix")
    into { new File(rootProject.rootDir, '.git/hooks') }
    rename("pre-push-$suffix", 'pre-push')

    fileMode 0775
}

Sync and build the Project. Now, you should have a pre-push file added to the .git/hooks directory as well as a pre-commit file.

Testing the pre-push hook

To test, I simply commit my changes and then try pushing to my origin branch.

screencast 2020 11 17 14 45 31

As you may notice above, as soon as I ran the push command, Git triggered the pre-push hook which then ran the Gradle test command. All tests passed successfully, so the push proceeded successfully. A failed test would terminate the push command.


Setting up pre-commit or pre-push hooks to run some tasks for you like this can improve your code workflow, save you some time and even help you get things done easily and collaboratively, especially when working in a team.

Thank you for reading. Till next time, cheers! ✌?