Write a Pre-Commit Hook for Managing Comments
The codebase I’ve been working in has slowly accumulated comments over time. Some comments explain nearby code, some suggest future improvements, and some point out current or potential issues. Some comments are useful and remain relevant, but unsurprisingly, most remain unaddressed and forgotten. Comments like these are written for future developers, but comments can also be used by the current developer in the middle of their workflow. They can help a developer pseudocode, keep a piece of to-be-deleted code for temporary reference, or remember things to come back to. However comments are used, it’s easy for them to quickly become irrelevant, confusing to other developers, and make it into commits unintentionally.
To help address these issues with managing comments in addition to helping myself learn more about zsh scripting, I wrote a pre-commit hook which checks for labels at the beginning of comments, outputs search results, and prevents a commit if there are any unintentionally leftover comments.
These steps are for building this hook in a JavaScript project, but can be easily translated to other languages.
Step 1: Add Labels
Prepend the content of your comments with a consistent label in a distinguishing format like an all caps “FIXME” or “TODO” or “NOTE”
Step 2: Categorize Labels
Categorize these labels by whether they should allow a commit to be accepted or not.
comment_labels=("FIXME" "TODO" "DELETEME" "NOTE")
comment_labels_no_commit=("NOCOMMIT")
Step 3: Set up Git Hook Library
Add a git hook library like Husky for JS projects and follow its set-up instructions.
yarn add husky
Create a file for the hook ex: check-comments.sh
#!/bin/sh
comment_labels=("FIXME" "TODO" "DELETEME" "NOTE")
comment_labels_no_commit=("NOCOMMIT")
Add command to package.json
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"check-comments": "./check-comments.sh"
}
}
Call command from husky script
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn check-comments
Step 4: Customize Grep
At the heart of this script is grep
Define your grep flags and arguments and wrap it in a function so it can be re-used in the script.
grep_wrapper() {
grep "$@" -r -F -n --include=*.{jsx,js} --exclude-dir=node_modules --color
}
"$@”
is a parameter which expands all arguments passed to the grep_wrapper
function call into a string literal.
-r
or — recursive
means the search will recursively search through all directories and sub-directories where grep
is called (we want it to search through an entire project)
-F
or — Fixed-Strings
means we will be able to search for the labels themselves
-n
or --line-number
will output the line number and file path of where the matching label was found
— include=*.{jsx,js}
and — exclude-dir=node_modules
specify file types to include and directories to exclude
--color
will highlight the matching labels in the output
You can test the output of this function by calling it in your script with a sample argument like: grep_wrapper "FIXME"
Step 5: Create a function which changes behavior depending on the label type
Create a function to loop through the labels passed in, call grep
on each label and change behavior depending on the label type (this will take a few steps to build).
check_comments() {
label_type="$1"
labels=("${@:2}")
}
check_comments "COMMIT" "${comment_labels[@]}"
check_comments "NOCOMMIT" "${comment_labels_no_commit[@]}"
The first argument ex. "COMMIT”
is the label type and the second argument passes in the array as a single string by expanding the array with spaces as separators between elements. Then inside the function the arguments are captured as string literals assigned to variables with clear names.
The second argument "${comment_labels[@]}"
expands the comment_labels
variable into a series of separate string elements
label_type="$1"
captures the first argument passed to check_comments
labels=("${@:2}")
creates an array of strings starting with the 2nd argument until the last argument
Step 6: Call grep on each label
Loop through the labels and call the grep_wrapper
function on each label.
for label in "${labels[@]}"; do
results=$(grep_wrapper "$label")
done
This is a for in
loop which calls grep_wrapper
on each iteration and stores the output in an array.
Step 7: Connect the script output to git hook library
Prevent a commit from going through by checking the label type and checking if any matches were found after calling grep
.
should_not_commit=false
for label in "${labels[@]}"; do
results=$(grep_wrapper "$label")
grep_status="$?"
if [[ "$grep_status" = 0 && "$label_type" = "NOCOMMIT" ]]; then
should_not_commit=true
fi
if [[ "$grep_status" = 0 ]]; then
echo "$results"
fi
done
if [[ "$should_not_commit" = true ]]; then
echo "There were NOCOMMIT comment labels matched\n\n"
exit 1
fi
grep_status="$?"
captures the exit code of the most recent operation. If matches were found the output will be 0
otherwise it will be 1
.
So the subsequent conditional checks if a match was found for a NOCOMMIT
label and will change should_not_commit
to true
. This is checked further down outside of the loop and will cause the script to exit with code 1
so that Husky knows to prevent the commit.
If matches are found then the next conditional will output the grep
results.
Step 8: Add descriptive messages
Improve output readability by using echo
to print descriptive messages around the grep
output.
Also, provide info about the number of matches found per label by passing the grep
results into wc -l
using the here string operator <<<
. wc -l
calculates the number of separate lines in its input.
check_comments() {
label_type="$1"
labels=("${@:2}")
echo "Comment label type: ${label_type}"
echo "Comment label(s): ${labels[*]}\n\n"
should_not_commit=false
for label in "${labels[@]}"; do
results=$(grep_wrapper "$label")
grep_status="$?"
if [[ "$grep_status" = 0 && "$label_type" = "NOCOMMIT" ]]; then
should_not_commit=true
fi
if [[ "$grep_status" = 0 ]]; then
count=$(wc -l <<< "$results")
echo "\nFound ${label} comment label ${count} time(s)"
echo "$results"
else
echo "\nNo matches found for: ${label}\n"
fi
done
if [[ "$should_not_commit" = true ]]; then
echo "There were NOCOMMIT comment labels matched\n\n"
exit 1
fi
}
Step 9: More output readability improvement
Add a line of characters to distinguish between each time check_comments
is called.
columns=$(tput cols)
check_comments() {
label_type="$1"
labels=("${@:2}")
echo "\n"
printf "%0${columns}d\n" 0 | tr 0 -
"%0${columns}d\n"
is the format string argument passed to printf
. It tells the utility to pad its input with a number of 0
characters equal to the width(number of columns) in the user’s terminal. tput cols
is used to access this number.
The results of printf
are piped to tr
which replaces all of the 0
characters with -
characters.
Step 10: Add color
Add more color to the output to highlight important information.
CYAN=$'\e[36m'
NC=$'\e[m'
check_comments() {
label_type="$1"
labels=("${@:2}")
echo "\n"
printf "%0${columns}d\n" 0 | tr 0 -
echo "Comment label type: ${CYAN}${label_type}${NC}"
echo "Comment label(s): ${CYAN}${labels[*]}${NC}\n\n"
should_not_commit=false
for label in "${labels[@]}"; do
results=$(grep_wrapper "$label")
grep_status="$?"
if [[ "$grep_status" = 0 && "$label_type" = "NOCOMMIT" ]]; then
should_not_commit=true
fi
if [[ "$grep_status" = 0 ]]; then
count=$(wc -l <<< "$results")
echo "\nFound ${CYAN}${label}${NC} comment label ${CYAN}${count}${NC} time(s)"
echo "$results"
else
echo "\nNo matches found for: ${CYAN}${label}${NC}\n"
fi
done
if [[ "$should_not_commit" = true ]]; then
echo "There were NOCOMMIT comment labels matched\n\n"
exit 1
fi
}
The variables CYAN
and NC
contain strings with escape sequences which tells the terminal to color the text and to stop coloring the text. NC
stands for “no color”.
Step 11: Test the script
Add some sample comments in your code, test the script’s output by running yarn check-comments
, and confirm that commits do not go through when NOCOMMIT
labels are found.
Conclusion
Here’s the full script:
#!/bin/sh
comment_labels=("FIXME" "TODO" "DELETEME" "NOTE")
comment_labels_no_commit=("NOCOMMIT")
columns=$(tput cols)
CYAN=$'\e[36m'
NC=$'\e[m'
grep_wrapper() {
grep "$@" -r -F -n --include=*.{jsx,js} --exclude-dir=node_modules --color=always
}
check_comments() {
label_type="$1"
labels=("${@:2}")
echo "\n"
printf "%0${columns}d\n" 0 | tr 0 -
echo "Comment label type: ${CYAN}${label_type}${NC}"
echo "Comment label(s): ${CYAN}${labels[*]}${NC}\n\n"
should_not_commit=false
for label in "${labels[@]}"; do
results=$(grep_wrapper "$label")
grep_status="$?"
if [[ "$grep_status" = 0 && "$label_type" = "NOCOMMIT" ]]; then
should_not_commit=true
fi
if [[ "$grep_status" = 0 ]]; then
count=$(wc -l <<< "$results")
echo "\nFound ${CYAN}${label}${NC} comment label ${CYAN}${count}${NC} time(s)"
echo "$results"
else
echo "\nNo matches found for: ${CYAN}${label}${NC}\n"
fi
done
if [[ "$should_not_commit" = true ]]; then
echo "There were NOCOMMIT comment labels matched\n\n"
exit 1
fi
}
check_comments "COMMIT" "${comment_labels[@]}"
check_comments "NOCOMMIT" "${comment_labels_no_commit[@]}"
I hope that by following this tutorial you have learned a little more about zsh and scripting as well as have a new tool to better manage comments in your code.