Writing Githooks in Kotlin
You’re already using Kotlin on your codebase. Maybe, you’ve even migrated to the new Kotlin DSL for Gradle. Wouldn’t it be nice if you could use Kotlin for your git hooks too?
Well, turns out you can! Here’s how you do it…
What do I need?
Git will basically run whatever script you drop on the .git/hooks
directory. In their words:
To enable a hook script, put a file in the hooks subdirectory of your
.git
directory that is named appropriately (without any extension) and is executable
So all we need is to be able to execute Kotlin files as scripts. There is a Kotlin Scripting Support KEEP under definition. But for the time being we’ll stick with the awesome KScript library (by @holgerbrandl) that enables Kotlin scripting on *nix-based systems.
You can find the details for installing KScript here. On MacOS if you’re using Homebrew all you have to do is run: brew install holgerbrandl/tap/kscript
.
I’ll also be using Gradle to automatically install the githook and run the proper validation, but the same can be done with Maven.
The script
As an example I’m going to show how to do a pre-push client hook that aborts the push if grade check
task is not successful. For this I’ve created a file named Pre-Push.kts
:
The first line is all the magic incantation we need to execute the script. By setting the shebang to #!/usr/bin/env kscript
we get to use kscript
as interpreter for the script.
The code after the import is the actual script. Those are the lines that are going to be executed as soon as somebody calls the script. Just as you’d expect with any regular shell script.
In a nutshell this is what the script does:
- Stash uncommitted changes if any1
- Run code validation (in this case
gradle check
) - Unstash possible changes stashed on step 1
- Log outcome and set the proper exit value
The last step is important because if the script exits to anything other than 0 then git aborts the action (in this case the push).
How do I call things from a script?
To do anything useful on your script you’ll probably have to call some external tool at some point. In this particular case for example a mix of git commands and gradle tasks.
There are 2 ways you can go about this:
- Either use a Kotlin/Java library for the task you’re trying to accomplish (in this example we could use JGit and Gradle tooling API)
- Or call a shell command directly
While the first approach is more portable, it will introduce some dependencies to your script (which fortunately KScript has great support for). On the other hand the second option is probably easier to implement because it’s just using the same commands we use everyday on our workflow.
Since I can assume everybody in my team has git
and gradle
installed and in their path I went for option 2.
Running shell commands from Kotlin
We can run shell commands on Kotlin using ProcessBuilder
, just like we’d do from Java.
In this case I’ve created a runCommandWithRedirect
extension function that looks like this:
This function can be called on any String like this:
"gradle check".runCommandWithRedirect()
This function will:
- Redirect the standard and error output to the one for the current process, in our case that means the output of the command will be visible on the terminal when the githook is executed.
- Set the directory to the passed
dir
parameter, or use the current directory if no parameter is provided. - Execute the command, wait for it to finish and return the
ExitStatus
You can play around with the different ProcessBuilder
options. In my script above for example I’ve another version of this function called runCommand
that executes the command and returns it’s output as a Sequence<String>
.
Automatic installation
Githooks are great to enforce code quality practices (i.e. ”You can’t push if your coverage is less than 80% “ 👮). But for the client-side githook to be execute it needs to be in the .git/hooks
folder which is not versioned. That means that each developer on your team has to manually install the hook, which means that you are again, relying on the good memory of your teammates to enforce code quality.
Instead we could use this trick. We can create a gradle task called “copy” that copies the githook from the src
folder to the git/hooks
and removes the file extension in the process.
Then we can make the “build” task depend on this new ”copy” task. The next time the developer runs gradle build
the githook will be installed. And as a bonus: the githook script is now versioned too! 2
Here’s how this would look like (using Kotlin DSL for Gradle)
⚠️ Don’t forget to do chmod u+x Pre-Push.kts
to make the script runnable, otherwise it won’t work.
What about performance?
Kotlin is a compiled language, so at some point your script will have to be compiled. Fortunately thanks to KScript this only happens the first time you run the script and it’s only compiled again if the script changes.
Other than that there’s the JVM startup time which adds around 200ms of overhead. Maybe in the future we’ll be able to use Kotlin Native to compile to native binaries directly and avoid this overhead.
If you want to read more about performance comparison between Python and Kotlin scripts check the KScript documentation.
Bonus track: testing
Testing Kotlin scripts turned out not to be so straight forward.
This article suggests using a runCommand
method similar to the one described above to execute the script and check it’s outputs. Whereas KScript own tests are written using assert.sh.
Neither approach convinced me. I was just looking for a way of individually test the functions in my script using the same tools I use to test the other parts of my code.
So what I ended up doing was moving all the Pre-Push logic to a regular *.kt
file. And then simply creating a *kts
Kotlin script that calls my class using the //INCLUDE
KScript directive.
The downside is that I know have 2 files for my githook (a *.kt
and a *.kts
) but that seems a small price to pay for being able to easily test my code.
Conclusion
Writing githooks in Kotlin is possible and not that hard thanks to KScript. You’ll be glad you have tried it out the next time you have to refactor that pre-push hook.
You can find an example repository containing all the code for this blogpost here: https://github.com/jivimberg/kotlin-githook