How to enforce better technical debt practices

Technical debt is a curious issue and a plague to say the least. Here are some tips to automatically keep it out of your code.

The problem

Wanting to finish our code quickly, for whatever reason, sometimes leads us to take on some technical debt. This could mean badly designed code, bandage solutions or even causes for an unnecessary amount of manual intervention.

Once technical debt is in the code, we will rarely take the time to pay it back. We'll happily leave dirt hacks in the code as long as they make the difference between a working system and a non-working system.

I'm not going to go into the causes of technical debt. Both human and technical factors could be at the root of the problem, but I want to focus on some solutions. These are some ways to pick the low hanging fruit.

Git hooks

We can put some systems in place to automatically help us avoid technical debt. It's important that these systems enforce discipline automatically and don't require manual initiation.

Git hooks allow us to run arbitrary code at specific points in the version control process. In any git repository, we can find a .git repository with a directory of hooks:

.git
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── prepare-commit-msg.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   └── update.sample

Specifically I want to show what you can do with the pre-commit and pre-push hooks.

Style

As a warmup, you can implement automatic code formatting enforcement. You will need a command-line too that takes care of the formatting. Either a code formatter or a code style suggestion tool (like hlint) will do.

Let's assume the tool is called linter. It should have an exit code other than 0 if there are any warnings or style errors.

We put it in .git/pre-commit:

set -e # Exit on first error
linter

Now, any time we run git commit, Git will call linter and prevent you from committing if there are any errors. Of course you can also circumvent this by adding --no-verify to the git command.

Recommended checks

Now that we've added our first git hook, we're ready to start discussing what other checks should be enforced automatically. This discussion should ideally involve all the project's contributors.

In my opinion a number of code health checks are important. These include checking that files don't contain trailing whitespace, that code is indented correctly, that the linter doesn't produce any warnings. Feel free to add anything you can come up with. Use the tools that are at hand for the appropriate programming language.

I also recommend checking that all the code still compiles without problems, that all of the tests still pass and maybe even that benchmarks haven't gotten worse. Add checks for all predicates that you would like to be invariant under development.

Installing the hooks.

Even if you add the git hook scripts to your repository, you cannot force contributors to install them. This is a security feature of git. You can, however, make it easier for your contributors to install the hooks so that they don't have to copy scripts around. Just put your hook scripts in a scripts directory in your git repository and add this ħooks.sus file to the top level directory:

card hooks {
  into .git/hooks
  outof scripts
  pre_commit_test.sh -> pre-commit
  pre_push_test.sh -> pre-push
}

Then add a line to the README that tells contributors to run spark deploy hooks.sus before contributing.

Helpers

I have been using these git hooks in some of my personal projects and have made helper functions. Feel free to use any that you like:

Colors:

ESC_SEQ="\x1b["
COL_RESET=$ESC_SEQ"39;49;00m"
COL_RED=$ESC_SEQ"31;11m"
COL_GREEN=$ESC_SEQ"32;11m"
COL_YELLOW=$ESC_SEQ"33;11m"
COL_BLUE=$ESC_SEQ"34;11m"
COL_MAGENTA=$ESC_SEQ"35;11m"
COL_CYAN=$ESC_SEQ"36;11m"
print_colored_text () {
  local color="$1"
  local text="$2"
  local color_code="COL_$color"
  echo -n -e "${!color_code}${text}${COL_RESET}"
}

A 'higher order' checker function:

check () {
  name="$1"
  shift
  command="$*"
  echo -n "Check: ${name}... "
  OUT="/tmp/out.txt"
  ERR="/tmp/err.txt"
  ${command} > ${OUT} 2> ${ERR}
  if [[ "$?" == "0" ]]; then
    print_colored_text GREEN "Success\n"
  else
    print_colored_text RED "Failure\n"
    echo ${command}
    cat ${OUT}
    cat ${ERR}
    return -1
  fi
}

With it, you can rewrite the above linter example as follows and get some nice output.

set -e # Exit on first error
check "Code format" linter
$ git commit
Check: Code format... Success

If you liked this blog post, please consider becoming a supporter:

Become A Supporter