Donate profile for Rudi at Stack Overflow, Q&A for professional and enthusiast programmers

17/11/2019

70 views

Git Hooks and Java Projects


Picture of a close up section of an old fishing hook

I hope that reading this piece will convince you, not just to use Git hooks, but also how and why we should use them in our projects. I've tried to find best practices for using git hooks in our projects, but the only place this kind of information can be found is in dribs and drabs scattered across Stackoverflow questions. I hope that this can serve as a starting point, for a more comprehensive view on the subject.

Why Use Git Hooks?

One reason, is to automate something you do every time you perform a certain action in Git. For instance running a build when changes are received on a given branch. However, these kinds of tasks are usually solved by tools built for purpose. For example Jenkins, Travis CI, Github actions, etc.


My favorite reason for using hooks is as validation. Early validation provides us with quick and regular feedback (important for developing expertise in any field) and stops us from wasting time by straying down the wrong path.


Lets take a simple example, first without the hook. Your project has a standard commit messages format, and you've done an early commit on your branch wrong. Then you merge in another branch, add multiple new commits, and push. The remote repository rejects your changes. You now have to change that single commit message on your branch. You could do an interactive rebase, squash merge onto a new branch, or cherry-pick your commits onto a new branch. Either way, you have to do more work than normal.


What about with a validating hook in place to provide early feedback? Your project has a standard commit messages format, and you've done an early commit on your branch wrong. The commit-msg hook stops the commit, immediately flags your commit message as incorrect, and asks you to try again. Done. Early regular feedback stops you making work for yourself, and you learn not to make that mistake much quicker.

Managing Your Hooks

Let's say you're convinced and you want to add some hooks. How should we organise them? Makes sense to keep them in your project's Git repo so you can track changes to them. But what if you need to use them across multiple repositories? And how will you get the client side hooks installed for everyone working on the project?


Hooks should be managed the same way any good code should be. They should be a dependency of your project, pulled in and installed/updated when you build your application. Let's go through an example Java setup using Maven as our build tool. Full disclosure: I wrote one plugin and the client side Git hooks library used in the example that follows. So it might be worth checking to see if there is anything better suited for your needs.

A Java & Maven Example

First we're going to need a git repository for our own hooks, or a standard library of hooks. We're not going to go into writing the hooks themselves. There are plenty of resources out there for that. So for our example we will use my small configurable library of client side git hooks.


If you have your git hooks in a separate repository there is a good chance you will want your hooks to work in subtly different ways in each of your projects. A good way of handling this is to have your hooks take configuration from the git config of your local repository. You can set this config in your build differently for each project.

Next we need to store, and make available, releasable versions of our hooks. If your hooks are managed in a Maven project you could deploy them to a Maven repository, and get them into your project using the maven-dependency-plugin. If your hooks library is just a collection of shell files, as in our case, then we can just make each releasable version available for download by URL. GitHub lets us add arbitrary compressed files to each release, and gives us an end-point you can download from.


Now we need to get a particular release of our hooks into our project. We want it to do this during our build. As we just have a URL, we can use the maven-antrun-plugin to download and unpack our hooks. Here's our plugin definition from our pom.xml.


<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-antrun-plugin</artifactId>
  <version>1.8</version>
  <executions>
	<execution>
	  <id>download-hooks</id>
	  <phase>initialize</phase>
	  <goals>
		<goal>run</goal>
	  </goals>
	  <configuration>
		<failOnError>false</failOnError>
		<target>
		  <taskdef resource="net/sf/antcontrib/antcontrib.properties" />
		  <if>
			<available file="${basedir}/etc/git-hooks/v${git.hooks.version}" />
			<else>
			  <delete dir="${basedir}/etc/git-hooks/" />
			  <get src="https://github.com/rudikershaw/client-git-hooks/releases/download/${git.hooks.version}/git-hooks.zip"
		   dest="${project.build.directory}/git-hooks.zip" />
			  <unzip src="${project.build.directory}/git-hooks.zip" dest="${basedir}/etc" />
			  <chmod dir="${basedir}/etc/git-hooks/" includes="**/**" perm="700" />
			  <touch file="${basedir}/etc/git-hooks/v${git.hooks.version}" />
			</else>
		  </if>
		</target>
	  </configuration>
	</execution>
  </executions>
  <dependencies>
	<dependency>
	  <groupId>ant-contrib</groupId>
	  <artifactId>ant-contrib</artifactId>
	  <version>20020829</version>
	</dependency>
  </dependencies>
</plugin>

With the above, we download and unzip the archive into a directory and then give the files the appropriate permissions to allow Git to run them. We also create a version file in the directory we move the hooks to. That way we can check if we have the correct version already and then skip the process if it has already been done. By checking the version like this it will work as you would expect if you change the ${git.hooks.version} property declared at the top of our pom.xml (not shown).


I have chosen to put our hooks directory into the etc/ directory in our project. Initially it might seem target/ is a better place to put them, as they are effectively temporary files downloaded in the build. But we don't really want these files cleared away if someone runs mvn clean.


There are four standard ways of telling git to use your hooks; move them into the .git/hooks/ directory, symlink another directory to .git/hooks/, add core.hooksPath config, or use a git init template directory.

We have our hooks in our project, but we still need to tell git to use them. To do this, we are going to use a feature of git that has been available since version 2.9. We can tell git to use a different folder to read its hooks from by setting the git config core.hooksPath to our directory. But we also want to do this in our build. There shouldn't be any manual steps. Let's add another plugin to our pom.xml.


<plugin>
  <groupId>com.rudikershaw.gitbuildhook</groupId>
  <artifactId>git-build-hook-maven-plugin</artifactId>
  <version>3.0.0</version>
  <configuration>
	<gitConfig>
	  <core.hooksPath>etc/git-hooks/</core.hooksPath>
	  <hooks.commitmsgregex>(\w+\s){3,}(\w+\.)</hooks.commitmsgregex>
	</gitConfig>
  </configuration>
  <executions>
	<execution>
	  <goals>
		<goal>initialize</goal>
		<goal>configure</goal>
	  </goals>
	</execution>
  </executions>
</plugin>

This is the git-build-hook-maven-plugin (my precious). It is designed to manage your developer's local git from your build, and have changes applied when the build is run. In this case we are initializing a git repo if one does not already exist, setting the core.hooksPath config to point to our git hooks folder, and setting some custom git config (hooks.commitmsgregex) to set a regex to use to validate our commit messages.


Once we run the build with these plugins, our hooks should be ready to go. So let's try and commit the changes with a commit message that our regex wouldn't match.


rudi@XPS:~/projects/rudikershaw.com$ git add .
rudi@XPS:~/projects/rudikershaw.com$ git commit -m "wat"
Commit message does not adhere to '(\w+\s){3,}(\w+\.)', try again.
rudi@XPS:~/projects/rudikershaw.com$

To Summarise

It seems strange that where-ever we find explanations or tutorials online on git hooks, they never mention how we should manage the development and maintenance of these hooks. They rarely even mention how to get them out to everyone. The Atlassian documentation on git hooks even goes so far as to say that some of the things explained in this article can't be done. It's time we start thinking about using industry best practices when creating and maintaining our hooks.


Thank you for reading my thoughts on how we should be using git hooks (local git hooks in particular). If you have any feedback, questions, or Gods forbid praise, feel free to leave my a comment below.


Rudi Kershaw

Web & Software Developer, Science Geek, and Research Enthusiast