There is a certain confidence boost when you see that message “All tests pass”, unless you are like me and get lazy, skimping on writing good tests for your logic. There are a lot of tools integrated with github, gitlab and probably other git interfaces that run a set of pre-defined tasks before potentially allowing you to raise a PR or mark it safe for merge. For the uninitiated, it’s know as CI or continuous integration tools. For example,

What if you could run those tests, lint checks and other stuff before you push your commit and realize it’s going to fail in your CI pipeline and make you look like a n00b. Wouldn’t that be great !

Unfortunately, there aren’t a lot of tools that do the same for plain old git, I mean locally in your development environment. There is one for nodejs, called the pre-commit, but let’s ignore that since we are gophers here.

There is a project pre-commit/pre-commit that can be used to manage a wide variety of pre-commit stuff and in a pretty way. It looks really good ! The only gripe that I have with it (highly personal) is it needs a runtime. I am not saying that runtimes are bad, they just feel a bit bloated and then you run into their dependencies and versions and give up. The most painful transition for me has been python2.x to python3.x. So, I try to avoid python until python3 is an adopted standard and the only version present in all of my machines. If that’s not a concern for you, then feel free to use the pre-commit project. It will probably suit your needs much better.

If you share similar thoughts as me or don’t have anything better to do than reading this, let’s see how these things are implemented.

Hooks all over the place

I can feel the incoming disappointment. Git, the vanilla thing has something called githooks at different stages that you can use to hook in and run your stuff.

Let’s try something !

$ git clone https://github.com/cloudmarker/cloudmarker.git
$ exa --tree cloudmarker/.git/hooks
.git/hooks
├── applypatch-msg.sample
├── commit-msg.sample
├── fsmonitor-watchman.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── pre-receive.sample
├── prepare-commit-msg.sample
└── update.sample

Notice the pre-commit.sample file. It has a sample defintion that could be run before the commit, as the name suggests. What if we create a file pre-commit with the scripts that we need to run, tests, lints, checks, print xkcd, whatever be it.

That script would get called before you try and commit and the commit would fail if the script exited with a non-zero error code, thus preventing you from your impulsive force pushes.

make it more manageable

This is a file present in the .git folder that is not under version control, so how do you manage it better. If the commands or workflow changes in between, you would have to keep updating the script manually which is not what we want.

One way to do it is, softlinks. Create a softlink to your version controlled script file and make the pre-commit hook to point your script. A sample project structure that you make want to follow.

$ exa --tree --level 1 .
.
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── Makefile
└── .pre-commit

The project contains a version controlled file called .pre-commit (a dotfile to keep the folder view clean) which will contain your commands that you may need to run.

The first time setup can be automated using your favourite tools. Let’s try to do something using gnu make (which is not my favourite).

Contents of the .pre-commit.

#/bin/bash
./.git/hooks/pre-commit.sample
make pre-commit

The make target.

pre-commit:
    @go test ./...
    @go fmt ./...
    @goimports -w .
    @golint ./...

    @go vet ./...
    @gocyclo -over 10 .

deps:
    @echo "Installing tools: goimports, golint, gocyclo"
    @go get -u golang.org/x/lint/golint
    @go get -u github.com/fzipp/gocyclo
    @go get golang.org/x/tools/cmd/goimports

    @echo "Setting up pre-commit hook"
    @ln -snf ../../.pre-commit .git/hooks/pre-commit
    @chmod +x .pre-commit

Notice the deps target that downloads all the tools that I am using in my pre-commit flow and then sets a softlink to the git hooks pointing to the relative path of the .pre-commit file.

This may seem a bit annoying (to have an indirection from the pre-commit file to the makefile) but it gives you a minimal pre-commit file and all of dependencies and complexities are captured and controlled from a single place which is your build mechanism, make in this case,

Using this workflow

To setup this workflow, you could just do

$ make deps

And you are all set with the dependencies for your project and the git hook as well. The next time you try a commit, it will run the whole pre-commit target and break your commit if have messed something up automatically.

I like this because it’s minimal (has it’s own downsides) and does not require installing any extra tools or libraries other than what you anyways need (make is usually present on most unix systems). If your project is large and this is not enough, I would recommend going with a more verbose and configurable tool, but for small project, this should be familiar enough. I have seen people customize their makefiles to extremes, so maybe that’s all they need.


Discussion thread: here