Boosting Codebase Hygiene: Unleashing the Power of Pre-commit Hook

Abdullah Al Sayeed
9 min readDec 29, 2023

--

Preface

Imagine you are working on a project, and you are just done implementing the feature or fixing the bug, or may be just some code refactoring. Now is the time to commit the changes and push it to others. But are you forgetting anything? Maybe you need to do some checks before committing. Maybe you want to run the tests to check if everything is working like before, or maybe you just want to check that there is no code smell in the code you are about to push.

Have you thought of running this check automatically for you? Me too! I always wondered if there’s anything like that which will enforce me to being nice to the code I write.

And guess what? We don’t need to re-invent the wheel. Because git itself is so powerful and charming, it would be so unlikely if there is not anything already.

To read this article in Bangla, click here

Git hooks

Let’s take a look at this nice definition from Atlassian.

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 customizable actions at key points in the development life cycle.

Nicely described! Hooks are some scripts that may trigger at different points of your development — before or after commit, before rebase, after merge, before push and many more. It can trigger both client or server side. You can learn more from the documentation.

In this article, we are interested in a client side hook, specifically the pre-commit hook in a flutter project.

Pre-commit hook

A pre-commit hook is a script that triggers every time we are about to make a commit. So we can do whatever we want to check before committing the changes. If the check fails, the commit will be halted.

Have you ever been to the hooks/ folder inside the .git folder of your project?

If you do, you will see there are already some hooks there. If you look closely, all the files have .sample extension. That means they are sample files, kinda still deactivated.

Rename the pre-commit.sample file by removing .sample extension and open it in a text editor

You can try to read the script or may be just delete everything in it. Because we are going to write our own script!

we can use our preferred language e.g. python, ruby or bash to write the script. bash is widely used, and we will also use bash.

So we put #!/bin/bash at the very beginning of the file, indicating it’s a bash file.

Now, what are we going to do?

Before every commit, we want to check and do the following —

  1. format the code
  2. Run the dart analysis to check for any warning or issues
  3. Run the test cases

But first, we have to understand one thing. Are we going to check all files in our project? That will not be wise because there could be many directories like assets or some others where we don’t want these checks. Ideally, our development related files are inside one directory, in flutter, which is the lib/ directory. Again, do we want to check all files inside lib/ ?

Nah! There could be a hundred files, but we changed may be 10 files, and we want to commit only 5 of them which are relevant to our commit. That’s why we have to add files in the staging area those needs to be committed with git add command.

The summary is, we want to check only the staged files that are going to be committed. We are settled here.

Now we want to run the format, analyze and test commands on all the staged files. To remove duplication, let’s create a function for that.

# Function to check staged files
check_staged_files() {
local command="$1"

# Get a list of staged files within the lib/ or test/ directory
local staged_files
staged_files=$(git diff --name-only --cached | grep -E '^(lib/|test/)')

# Run the specified command on each staged file
for file in $staged_files; do
if ! $command "$file"; then
exit 1 # Exit the script if there's an issue
fi
done
}

Wow! Lots of who-knows-what thrown at the face!

Hold on, let’s quickly go through what is happening there.

First, we declare a function check_staged_files(), with a command that will be passed with this function. It is generic, since we will run 3 different commands.

Second, we are storing all staged files in a variable from lib/ and test/ directory.

Third, we are iterating over all the staged files, and running the command on each of them. If anything goes wrong in any file, that means our check failed, so we are exiting with exit 1.

Not so hard, right? Let’s use this function to run our commands.

# Run code formatting using dart format on staged files
check_staged_files "dart format --set-exit-if-changed"

# Run static code analysis using flutter analyze on staged files
check_staged_files "flutter analyze"

# Run unit tests using flutter test
if ! flutter test; then
exit 1
fi
  1. We are passing dart formatcommand to the check_staged_files function, which eventually format all staged files properly.
  2. Similarly, we are running flutter analyze command, that will check all the staged files for any warning or issue raised by flutter linting.
  3. At last, we are running the flutter test command. We are not using check_staged_file here, because running test is not related to any specific files, instead we want to run all tests in our repository.

If everything is fine, the commit will be succeeded. Whether the checks pass or any issue occurs, we will go back to the state before running the script.

So putting all the pieces together, the pre-commit file will look like this

#!/bin/bash

# Function to check staged files
check_staged_files() {
local command="$1"

# Get a list of staged files within the lib/ and test/ directory
local staged_files
staged_files=$(git diff --name-only --cached | grep -E '^(lib/|test/)')

# Run the specified command on each staged file
for file in $staged_files; do
if ! $command "$file"; then
exit 1 # Exit the script if there's an issue
fi
done
}


# Run code formatting
check_staged_files "dart format --set-exit-if-changed"

# Run static code analysis
check_staged_files "flutter analyze"

# Run unit tests
if ! flutter test; then
exit 1
fi

# Exit with success status
exit 0

We are done (almost) with the script writing. It’s time for testing it in action.

Drum roll please! 🥁🥁

You can use it in any of your project, but I have used this Flutter animation project where I intentionally have some warnings and a failing test.

Let’s take a look inside the test file

and just a dummy unit test that absolutely does nothing but simulating test fail and pass scenario.

Now for the demonstration, I have added some changes in this test file and another file, but we will try to commit only the main and the test file,

so we have added these 2 files to the staging area

Now try to commit. Our expectation is that, the checks will be running on main and test files since they are in staging area (the green ones), then start running the commands.

We can see it formatted in 2 files. Then it started the flutter analyze command.

This program is executed on top to bottom manner. If at any point it encounters any issue, it immediately exits (remember the exit 1?) without executing anything else. In our case it starts with main.dart file, found a issue and exits. We can see in the console what are the issues. Now that we are reminded there are some issues in our staged files, let’s fix both of the file, add to staging area and commit again

Now we can see that no issues found in analyzing those files. Then it starts running the tests, and failed. Let’s fix the test case to be passed, by, add the changes and commit again

Hurrah! All of our checks passed, and the commit is successful. But the console looks a bit boring, right? Let’s add a some color to it.

#!/bin/bash

# Color formatting sequences
error="\e[31;1m%s\e[0m\n"
success="\e[32;1m%s\e[0m\n"
info="\e[33;1m%s\e[0m\n"
reset="\e[0m"

# Symbolic characters
checkmark="✅"
crossmark="❌"

# Function to check staged files within the lib/ directory
check_staged_files() {
local command="$1"

# Get a list of staged files within the lib/ directory
local staged_files
staged_files=$(git diff --name-only --cached | grep -E '^(lib/|test/)')

# Run the specified command on each staged file
for file in $staged_files; do
if ! $command "$file"; then
printf "${error}" "${crossmark} $command error ${crossmark} "
exit 1 # Exit the script if there's an issue
fi
done
}

# Inform the user that the pre-commit checks are starting
printf "${success}" " ${checkmark} Starting pre-commit checks... ${checkmark} "



# Run code formatting using dart format on staged files
printf "${info}" 'Running Flutter Formatter...'
check_staged_files "dart format --set-exit-if-changed"
printf "${success}" 'Done!'

# Run static code analysis using flutter analyze on staged files
printf "${info}" 'Running Flutter analyzer...'
check_staged_files "flutter analyze"
printf "${success}" 'Done!'

# Run unit tests using flutter test
printf "${info}" 'Running Unit Tests...'
if ! flutter test; then
printf "${error}" "${crossmark} Unit tests error ${crossmark} "
exit 1
fi
printf "${success}" 'Done!'


# Inform the user that the pre-commit checks passed
printf "${success}" "${checkmark} Pre-commit checks passed. Committing ${checkmark} "


exit 0

The output will look like this

It looks pretty nice. I use this version a lot. You can make your own hook and decorate the way you want.

But what if you reeaally need to bypass some checks for now and commit anyway?

You can always check later, since these are not biting right at this moment. But ideally, you should never slip these off your hands for later. Because later often means never, and in coding it is very much true.

But still there can be a scenario where you just want to commit. Just append — no-verifyat the end of your commit message. It will ignore the hook.

 git commit -m 'Update: everythign is fine' --no-verify

At first, it may seem a lot of things going, but it only takes some seconds to run and check the tests. If you are used to committing specific small changes (which you should), you will find it very useful, like a mate who reminds you, hey! Be mindful of those issues and the test cases before pushing changes to others!

In the long run it will make your codebase nice and clean, and make you a careful programmer, who believes the great beauty lies in minding small things.

That’s a lot for today. If you find it useful, then my efforts are not in vain. See you later in some other articles.

You can check my medium and LinkedIn profile to get updates about new articles.

--

--