In this article, we will learn how easily we can automate our Kotlin project with git hooks on the example of Ktlint pre-commit check.
At the end of this tutorial, you will know precisely:
- what are Git client-side Hooks,
- what types of client-side hooks Git offers,
- how to implement an automated code check before we commit our changes,
- and how to combine that together with Kotlin and Gradle.
And although this guide may seem a bit specific, you can trust me that after reading it you will be able to adjust this knowledge to many other cases in your real-life scenarios.
Video Tutorial
If you prefer video content, then check out my video:
If you find this content useful, please leave a subscription 😉
Git Hooks And Client-Side Types
Before we see how to do a Ktlint check as a pre-commit hook, let’s learn a bit about Git Hooks.
Git Hooks, in simple words, are a way to trigger custom scripts, whenever a particular action happens in our repository. An action can be a rebase, a checkout, a merge, etc.
And in order to run our script, the only thing we must do is to put it inside the hooks directory, which we can find in the .git directory.
Moreover, when we navigate there, we will see that it already contains a bunch of useful examples:
If we would like to try any of the predefined samples, then we must simply remove the .sample
suffix from the filename.
And what client-side Git Hooks can we work with? Well:
- pre-commit – run before creating a commit,
- prepare-commit-msg – run before the commit message editor is opened,
- commit-msg – triggered after the commit message is created but before the commit is finalized,
- post-commit – run after a commit is made,
- applypatch-msg – invoked during the git apply operation to edit patch messages,
- pre-applypatch / post-applypatch – run before/after applying changes from a patch,
- pre-rebase – run before starting a rebase operation,
- post-rewrite – triggered after commands that rewrite commit history,
- post-checkout – invoked after a successful git checkout operation,
- post-merge – run after a successful git merge operation,
- pre-push – run before a push to a remote repository.
The pre-receive
, update
, and
post-receive
are server-side hooks, which we won’t cover here.
Import Ktlint
Excellent!
At this point, we know what we’re dealing with today and we can start the practice part of the Ktlint pre-commit hook implementation.
As the first thing, let’s navigate to the build.gradle.kts
and import the library:
plugins { kotlin("jvm") version "1.9.10" application id("org.jlleitschuh.gradle.ktlint") version "11.6.1" }
At the moment of writing, the most recent version is 11.6.1
and you can always figure out what’s the current one here.
Following, let’s sync the project.
When the synchronization process finishes, we should see plenty of new tasks added, among which, these we will use the most:
- ktlintFormat– to format according to the code style all
SourceSets
Kotlin files and project Kotlin script files, - ktlintCheck– to check all
SourceSets
and project Kotlin script files
Add Script
With that done, let’s create the scripts
directory and put the pre-commit
file there:
#!/bin/bash echo "Running git pre-commit hook" ./gradlew ktlintCheck ktlintCheckStatus=$? # return 1 if check fails if [[ $ktlintCheckStatus -ne 0 ]]; then exit 1 else exit 0 fi
But why don’t we put that directly to the .git/hooks
directory?
Well, the problem is that by default, the .git
directory is not tracked. It contains all the information about the repository, including the repository’s configuration, commit history, branches, and other metadata. And if the .git directory were tracked, it would create a kind of recursive loop, leading to potential issues and conflicts.
So, when working with Gradle and Kotlin, we will simply put our script in the scripts
destination, and later configure Gradle to copy it to the desired folder.
And what is our script?
Well, it is a simple script, which will run the ktlint check command using the gradle wrapper. If it is successful, the command returns 0 and we return 0, too. In other case, we return 1 and the commit will simply fail.
Update build.gradle.kts
Following, let’s navigate to the build.gradle.kts
file and add a new task- the copyPreCommitHook
:
tasks.register<Copy>("copyPreCommitHook") { description = "Copy pre-commit git hook from the scripts to the .git/hooks folder." group = "git hooks" outputs.upToDateWhen { false } from("$rootDir/scripts/pre-commit") into("$rootDir/.git/hooks/") }
As we can see, this task is responsible for copying the pre-commit file from the scripts/pre-commit
to the .git/hooks/
.
Moreover, we make a small workaround- outputs.upToDateWhen { false }
– so that our task will never be cached.
When we sync our Gradle project, we should see that our task is available from now on inside the git hooks
group:
At this point, we can run the task and verify that the script was moved successfully.
But is that all?
Well, we could stop right here, but if we automate things, it would be good to avoid manual run of the copyPreCommitHook
task, right? And we can easily achieve that this way:
tasks.build { dependsOn("copyPreCommitHook") }
With this setting, we simply instruct Gradle to run copyPreCommitHook
before the build
task.
Verification
With all of that done, we can finally verify if our ktlint check will be run as a pre-commit hook.
To do so, let’s make some changes that will fail ktlintCheck
and try to commit. In IntelliJ, we should see the following:
And when we check the details, we will see a meaningful message:
Running git pre-commit hook Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details > Task :loadKtlintReporters UP-TO-DATE > Task :runKtlintCheckOverKotlinScripts UP-TO-DATE > Task :runKtlintCheckOverTestSourceSet NO-SOURCE > Task :ktlintTestSourceSetCheck SKIPPED > Task :ktlintKotlinScriptCheck > Task :runKtlintCheckOverMainSourceSet > Task :ktlintMainSourceSetCheck FAILED C:\Users\Piotr\src\main\kotlin\Main.kt:2:1 Unexpected indentation (10) (should be 4) FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':ktlintMainSourceSetCheck'. > A failure occurred while executing org.jlleitschuh.gradle.ktlint.worker.ConsoleReportWorkAction > KtLint found code style violations. Please see the following reports: - C:\Users\Piotr\build\reports\ktlint\ktlintMainSourceSetCheck\ktlintMainSourceSetCheck.txt * Try: > Run with --stacktrace option to get the stack trace. > Run with --info or --debug option to get more log output. > Run with --scan to get full insights. * Get more help at https://help.gradle.org BUILD FAILED in 7s 5 actionable tasks: 3 executed, 2 up-to-date
And this snippet proves that our pre-commit
hook was triggered and works as expected.
Moreover, when we correct our code, we will see that we can commit without any message!
Summary
And that’s all for this article about implementing ktlint check as a pre-commit Git Hook.
I hope you enjoyed this tutorial and that this will be a great start for adding more automation to your project. If you would like to get a ready-to-go project, then please navigate to my GitHub repo here.
Lastly, I would like to invite you to my free newsletter, so that you will never miss any important updates from both my blog and Kotlin world!