Well, my friends, it's been a while since my last blog post
but it's good to be back.
Last one was a bit bird's eye and hand-wavy, so on this occasion let's get practical.
Note
This one goes out to Or Bendel from my team, with whom we discussed this useful feature.
Tip
As with every one of my blog posts, if you want the tl;dr
feel free to go over the Key Takeaways sections.
Today, I wanted to share a quick workflow that I think can help developers (especially ones who aren't prone to scrutinizing their changes before committing)
to help make sure no undesired changes are pushed.
I will be addressing a common pain point for developers -
how do I avoid accidentally committing or checking in so-called DNC (Do Not Commit) comments/phrases
in source code?
Leaving unintended snippets of code in code reviews can not only require redundant code review rounds and add "noise" to the code review process,
but also cause unintentional changes to be checked in at times (I remember the awkwardness of slipping in a naive "Test!" console.log earlier in my career).
So, let's start with the question - what are DNC messages anyway? These may appear as comments
that you add while developing to remember to go back and change something.
For example:
1functiondoStuff(){2console.count("doStuff was called");// DNC3// ...4}5
I often use "TODO(PR)" or "TODO(DNC)" as a reminder to myself to go back and resolve that thing
before publishing a pull request (or, God forbid, before checking in with TFS), but in general this
may be any arbitrary VCS that you use.
Personally, I prefer to use uncommon expressions so that I can quickly search the code to make sure
I don't actually introduce them in my changes.
In general, when I asked various developers about this issue this is how the conversation went:
Me: Do you ever add temporary comments for yourself before committing for changes you don't want to introduce to the pull request?
Them: Yeah, I use <X> or <Y>.
Me: And how do you make sure you don't accidentally commit them?
Them: I diff my changes.
Me: So you do it manually?
Them: Yep.
Me: And nothing ever slipped past your scrutiny?
Them: Well...
So, we have a problem which seems to require manual solutions using an error prone process that
trips up even the most thorough and diligent developers.
Well, what do productive (see: bored) humans do for problems that require a routine manual step?
They automate it, of course!
...Oh, how to automate it, you ask? Well, git hooks might be a good option!
Note
This is a good thing to enforce in your CI (Azure Pipelines, GitHub Actions, TravisCI, etc.).
In fact, Dell have a GitHub Action that forbids certain words (in this specific case, non-inclusive language),
which is a great use-case for a CI gate!
However, in this case these DNC messages may be "personal", by design, as as you don't want to mix up your DNCs with general TODOs in the codebase,
since you would want to easily search for them to spot your own changes.
For example, I could use "TODO(PR)" for my DNC phrases and you could use "DO NOT COMMIT").
Takeaways
Developers sometimes add temporary comments to code changes which may be accidentally introduced in the code review
This is often an error-prone process which can be easily automated with git hooks
##git hooks
###What are git hooks?
git hooks are a way to "hook" into stages of the git version control lifecycle,
and give the ability to run scripts at certain events, for example before a commit, after a commit or before a push.
Some examples of common hooks are:
pre-commit (before committing): often used for linting/formatting
commit-msg (after submitting the commit message): often used for enforcing styles on commit messages
post-commit (after committing): often used to tag commits
There are server-side hooks which can run on the VCS server, but we'll be focusing on client-side scripts,
which are triggered by local events in your local git repository.
Note
Atlassian have a great tutorial about git hooks, which goes much more in-depth than this humble blog post.
###Where do the hooks come from?
In general, there are two ways I know of to run hooks:
Local hooks which reside in the .git/hooks directory in the local git repository.
These are scoped to your local repository.
Note
These can be added automatically if you define a so-called git Template Directory.
Whenever you define a Template Directory, it will be used as the "boilerplate" when you run git init.
By default, when you git init, your local repo should contain sample commit hooks in the .git/hooks directory,
with hooks containing a .sample file extension.
The .sample extension actually prevents them from being actually registered/installed (once removed, they should run normally).
by git as real hooks.
Global hooks which you can configure in one place which git will search for, regardless of the local repo you're in (useful for a central location of all your git hooks)
Note
From my experiments, these seem to override the local hooks.
i.e., instead of looking at the local hooks, git will only use the global ones.
core.hooksPath was introduced in version 2.9
so check whether your git version supports it (run git --version)
There is also the option of using local hooks with symbolic links (symlinks) to the centralized hooks location.
As mentioned, using Template Directories you can define the skeleton for new git repos, and you could theoretically define
symlinks to your global hooks.
This is a more "explicit" approach, which may be preferable to some.
In essence, we can boil the options down to:
I chose option C (global hooks) for my workflows, because I have a dotfiles folder containing all the shell stuff I like to configure
for my different machines (I have a MacBook, a Lenovo laptop, occasionally a VM/desktop) and GitHub Codespaces
(did I mention Microsoft is the greatest company in the world?).
Within my dotfiles repo which I take "with me" across machines and use symbolic links to reference, I have a directory for
git hooks. I assume that there are no project-specific hooks, since they would otherwise override those.
In general, I would advise that things you wish to enforce for changes happen in your CI,
for example enforcement of linting, style guides etc., as git hooks are a "client" mechanism and in general
any developer can override those (for example with git commit --no-verify).
###Setting up global git hooks
The git configuration core.hooksPath (link) controls
the directory git uses for finding the hook to run.
For configuring the global hooks directory (I chose /etc/git/hooks for this example) you can run:
On UNIX systems you'll need to make the script executable (e.g., chmod +x /etc/git/hooks/pre-commit)
Takeaways
Git hooks are a mechanism for triggering a script to run at certain points of your git workflows (before a commit, after a push, etc.)
There are client-side hooks (which run on your machine) and server-side hooks (which run on the machine hosting your repository)
There are local hooks which are scoped to your local repository and global hooks which you could leverage for hooks you want to run from any repo
I chose global hooks for this blog post since they were the ones I found most suitable for my workflows
You can configure the global git hooks using the core.hooksPath configuration
##Pre-commit hook
###Pre-commit hooks to forbid DNC messages
So, going back to our original problem - how do we use git hooks to forbid "Do Not Commit" TODOs?
What I did was a add a pre-commit shell script to my global hooks folder (which in the example above was /etc/git/hooks,
but in my case was my dotfiles repo).
I have prepared a BASH version (which I'll go over in more detail) and a PowerShell version.
Both are available in a GitHub Gist I prepared for your convenience: link to Gist
Note that you could even use Python if you wanted to.
e.g., if you were so inclined, your pre-commit could look like this (in UNIX systems):
1#!/usr/bin/env python23print("Hello!")4
Also note that the scripts aren't perfect - they may leave much to be desired, and I welcome contributions (tweet at me!).
###Pre-commit hook (BASH)
The following is the content of my pre-commit script.
1#!/usr/bin/env bash23# Forbidden phrases.4FORBIDDEN_PHRASES=("DNC""DO NOT COMMIT""TODO(PR)")56# ANSI color codes.7CLEAR="\033[0m"8RED="\033[00;31m"9BLUE="\033[00;34m"1011violation_output=""1213# Go over the staged changes, and if any of the forbidden phrases is used,14# add an appropriate message to the output.15forforbidden_wordin"${FORBIDDEN_PHRASES[@]}";do16changed_file_names=$(gitdiff --cached --name-only)17forchanged_file_namein$changed_file_names;do18changed_file_content=$(gitdiff HEAD --no-ext-diff -U0 --exit-code -a --no-prefix $changed_file_name |egrep"^\+")19ifecho$changed_file_content|grep -q "$forbidden_word";then20violation_output+="${CLEAR} • ${BLUE}${changed_file_name}${CLEAR} contains ${RED}\"$forbidden_word\"${CLEAR}\n"21fi22done23done2425# If there are any violations, print the output and exit with an error code.26if[[! -z $violation_output]];then27printf"COMMIT REJECTED (DNC violation): see below for details\n"28printf"$violation_output"29exit130fi3132# If there are no violations, exit without an error code.33exit034
Let's go over it:
1#!/usr/bin/env bash23FORBIDDEN_PHRASES=('DNC''DO NOT COMMIT''TODO(PR)')45# ...(OMITTED)6
👆 Here we define the list of forbidden phrases - customize these to your heart's desire.
You could extract these to a separate file if you were so inclined.
1# ...(OMITTED)23# ANSI color codes.4CLEAR="\033[0m"5RED="\033[00;31m"6BLUE="\033[00;34m"78# ...(OMITTED)9
👆 Here we define the ANSI color codes, to get a beautiful colored prompt.
1# ...(OMITTED)23violation_output=""45# Go over the staged changes, and if any of the forbidden phrases is used,6# add an appropriate message to the output.7forforbidden_wordin"${FORBIDDEN_PHRASES[@]}";do8changed_file_names=$(gitdiff --cached --name-only)9forchanged_file_namein$changed_file_names;do10changed_file_content=$(gitdiff HEAD --no-ext-diff -U0 --exit-code -a --no-prefix $changed_file_name |egrep"^\+")11ifecho$changed_file_content|grep -q "$forbidden_word";then12violation_output+="${CLEAR} • ${BLUE}${changed_file_name}${CLEAR} contains ${RED}\"$forbidden_word\"${CLEAR}\n"13fi14done15done1617# ...(OMITTED)18
👆 Here we go over the new code changes and check if they contain any of the aforementioned DNC phrases.
If so, we collect the violations into violation_output.
1# ...(OMITTED)23# If there are any violations, print the output and exit with an error code.4if[[! -z $violation_output]];then5printf"COMMIT REJECTED (DNC violation): see below for details\n"6printf"$violation_output"7exit18fi910# If there are no violations, exit without an error code.11exit01213# ...(OMITTED)14
👆 Here we check output to determine if the commit can be approved or not (and if not, we print out the violations).
Note that the exit code is used by git to determine whether the commit can be applied or not.
###Pre-commit hook (PowerShell)
Below is a PowerShell (pwsh, AKA PowerShell 7+) version.
It is similar in structure to the BASH version, so I will avoid going into
detail with this one.
Note
On Windows, you'll need to name this file pre-commit.ps1 (with the .ps1 extension),
though I admit I haven't tried on a Windows system
On Linux/macOS (yes, PowerShell Core is cross-platform, how awesome is that?), you'll need to add a shebang
(e.g., #!/usr/bin/env pwsh)
1# Forbidden phrases.2$FORBIDDEN_PHRASES = "DNC","DO NOT COMMIT","TODO(PR)"34$violations = ""56# Go over the staged changes, and if any of the forbidden phrases is used,7# add an appropriate message to the output.8foreach($forbiddenWord in $FORBIDDEN_PHRASES){9$changedFileNames = "$(git diff--cached --name-only)"10foreach($changedFileName in $changedFileNames){11$changedFileContent = $(git diff HEAD --no-ext-diff-U0 --exit-code-a --no-prefix $changedFileName|Select-String"^\+")12foreach($line in $changedFileContent){13if($line-Match$forbiddenWord){14$violations+="$($PSStyle.Reset) • "15$violations+="$($PSStyle.Foreground.Blue)${changedFileName} contains: $($PSStyle.Reset)"16$violations+="$($PSStyle.Foreground.Red)${forbiddenWord}$($PSStyle.Reset)"17$violations+="`n"# PowerShell newline. Yes... I know ¯\_(ツ)_/¯18}19}20}21}2223# If there are any violations, print the output and exit with an error code.24if($violations){25Write-Host"COMMIT REJECTED DNC violation): see below for details"26Write-Host$violations27exit 1
28}2930# If there are no violations, exit without an error code.31exit 0
32
Takeaways
You can add your own pre-commit scripts which go over your changes and check for DNC violations
On UNIX systems, you'll need to chmod +x
The scripts can be BASH, PowerShell (which requires a .ps1 extension on Windows), Python or what have you.
On UNIX systems, specify your interpreter with a shebang (#!)
I've supplied a BASH script and a PowerShell script - you can see both above or on this GitHub Gist
##Conclusion
So, my friends, we have seen how we can leverage git hooks to spare our code reviewers, and add a simple
automation by leveraging a nice (and not that well-known, surprisingly) feature in our daily VCS tool.
See you at the next one!
P.S. Recently added an RSS feed to this blog, if it's useful to anyone.