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.
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.
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! ✌?
The task, is it supposed to be in project-level build Gradle or app-level build Gradle file or it doesn’t matter?
It doesn’t matter actually. I keep mine in app-level.
Do you have the build.gradle.kts code for the task version?
@femi The task below should do:
val installGitHook by tasks.creating(Copy::class) {
var suffix = “macos”
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
suffix = “windows”
}
val sourceDir = File(rootProject.rootDir, “automation/scripts/pre-commit-$suffix”)
val targetDir = File(rootProject.rootDir, “.git/hooks”)
from(sourceDir)
into(targetDir)
rename(“pre-commit-$suffix”, “pre-commit”)
fileMode = 775
}
tasks.getByPath(“:app:preBuild”).dependsOn(installGitHook)
Thanks emma.
The version seems not to work.
what i have tried:
1. changed the #!/bin/bash to #!/bin/zsh as i am using zsh
2. place this task code in build.gradle.kts of app, top and buildSrc levels
when i commit i don’t see the installGitHook running
Something to note: Did you run the task that ensures the git hook is installed, before committing?
The 2 files just differ in the first line which are just comments, so why does this matter?