Git Hooks the Hard Way

This post is adapted from an advanced chapter of Learn Git the Hard Way.

Each section is self-contained, and should be typed out by hand to ensure the concepts are embedded in your mind and to force you to think. This is the Hard Way.


Git hooks allow you to control what the git repository does when certain actions are performed. They’re called ‘hooks’ because they allow you to ‘hook’ a script at a specific point in the git workflow.

In this post you will cover:

  • What git hooks are
  • Pre-commit hooks
  • Pre-receive hooks
  • The `git cat-file` command

By the end, you should be comfortable with what git hooks are, and able to use them in your own projects.

Create Repositories

To understand git hooks properly, you’re going to create a ‘bare’ repository with nothing in it, and then clone from that ‘bare’ repo.

1  $ mkdir lgthw_hooks 
2 $ cd lgthw_hooks
3 $ mkdir git_origin
4 $ cd git_origin
5 $ git init --bare
6 $ cd ..
7 $ git clone git_origin git_clone

Now you have two repositories: git_origin, which is the bare repository you will push to, and git_clone, which is the repository you will work in. You can think of them as part of a client-server git workflow where users treat the git_origin folder as the server, and clones as the client.

Next, add some content to the repository, and push it:

8  $ echo 'first commit' > file1
9 $ git add file1
10 $ git commit -m 'adding file1'
11 $ git push

Nothing surprising should have happened there. The content was added, committed and pushed to the origin repo.

Adding a ‘pre-commit’ Hook

Now imagine that you’ve set a rule for yourself that you shouldn’t work at weekends. To try and enforce this you can use a git hook in your clone.

Add a second change, and take a look at the .git/hooks folder:

12 $ echo 'second change in clone' >> file1
13 $ ls .git/hooks

In the .git/hooks folder are various examples of scripts that can be run at various points in the git content lifecycle. If you want to, you can take a look at them now to see what they might do, but this can be a bit bewildering.

What you’re going to do now is create a script that is run before any commit is accepted into your local git repository:

14 $ cat > .git/hooks/pre-commit << EOF
15 > echo NO WORKING AT WEEKENDS!
16 > exit 1
17 > EOF
18 $ chmod +x .git/hooks/pre-commit

What you have done is create a pre-commit script in the hooks folder of the repository’s local .git folder, and made it executable. All the script does is print the message about not working at weekends, and exits with a code of 1, which is a generic error code in a shell script (exit 0 would mean ‘OK’).

Now see what happens when you try to commit:

19 $ git commit -am 'Second change'

You should have seen that the commit did not work. If you’re still not sure whether it got in, run a log command and check that the diff is still there:

20 $ git log
21 $ git diff

This should confirm that no commit has taken place.

To show a reverse example that lets the commit through, replace the script with this content:

22 $ cat > .git/hooks/pre-commit << EOF
23 > echo OK
24 > exit 0
25 > EOF

This time you’ve added an ‘OK’ message, and exited with a 0 (success) code rather than a 1 for error.

Now your commit should work, and you should see an ‘OK’ message as you commit.

26 $ git commit -am 'Second change'

A More Sophisticated Example

The above pre-commit scripts were fairly limited in their usefulness, but just to give a flavour of what’s possible, we’re going to give an example that is able to choose whether to allow or reject a commit based on its content.

Imagine you’ve decided not to allow any mention of politics in your code. The following hook will reject any mention of ‘politics’ (or any word beginning with ‘politic’).

27 $ echo 'a political comment' >> file1
28 $ cat > .git/hooks/pre-commit << EOF
29 $ if grep -rni politic *
30 > then
31 > echo 'no politics allowed!'
32 > exit 1
33 > fi
34 > echo OK
35 > exit 0
36 > EOF
37 $ git commit -am 'Political comment'

Again, the commit should have been rejected. If you change the content to something else that doesn’t mention politics, it will commit and push just fine.

38 $ echo 'a boring comment' >> file1
39 $ git commit -am 'Boring comment'
40 $ git push

Even more sophisticated scripts are possible, but require a deeper knowledge of bash (or other scripting languages), which is out of scope. We will, however, look at one much more realistic example in last section of this chapter.

Are Hooks Part of Git Content?

A question you may be asking yourself at this point is whether the hooks are part of the code or not. You won’t have seen any mention of the hooks in your commits, so does it move with the repository as you commit and push?

An easy way to check is to look at the remote bare repository directly.

41 $ cd ../git_origin
42 $ ls hooks

Examining the output of the above will show that the `pre-commit` script is not present on the bare origin remote.

This presents us with a problem if we are working in a team. If the whole team decides that they want no mention of politics in their commits, then they will have to remember to add the hook to their local clone. This isn’t very practical.

But if we (by convention) have a single origin repository, then we can prevent commits being pushed to it by implementing a `pre-receive` hook. These are a little more complex to implement, but arguably more useful as they can enforce rules per team on a canonical repository.

The `pre-commit` hook we saw before is an example of a ‘client-side hook’, that sits on the local repository. Next we’ll look at an example of a ‘server-side hook’ that is called when changes are ‘received’ from another git repository.

Pre-Receive Hooks

First type this out, and then I’ll explain what it’s doing. As best you can, try and work out what it’s doing as you go, but don’t worry if you can’t figure it out.

43 $ cat > hooks/pre-receive << 'EOF'
44 > #!/bin/bash
45 > read _oldrev newrev _branch
46 > git cat-file -p $newrev | grep '[A-Z][A-Z]*-[0-9][0-9]*'
47 > EOF

This time you created a pre-receive script, which will be run when anything is pushed to this repository. These pre-receive scripts work in a different way to the pre-commit hook scripts. Whereas the pre-commit script allowed you to grep the content that was being committed, pre-receive scripts do not. This is because the commit has been ‘packaged up’ by git, and the contents of the commit are delivered up as that package.

The read command in the above code is the key one to understand. It reads three variables: _oldrev, newrev, and _branch from standard input. The contents of these variables will match, respectively: the previous git revision reference this commit refers to; the new git revision reference this commit refers to; and the branch the commit is on. Git arranges that these references are given to the pre-receive script on standard input so that action can be taken accordingly.

Then you use the (previously unseen)git cat-file command to output details of the latest commit value stored in the newrev variable. The output of this latest commit is run through a grep command that looks for a specific string format in the commit message. If the grep finds a match, then it returns no error and all is ok. If it doesn’t find a match, then grep returns an error, as does the script.

Make the script executable:

48 $ chmod +x hooks/pre-receive

Then make a new commit and try to push it:

49 $ cd ../git_clone
50 $ echo 'another change' >> file1
51 $ git commit -am 'no mention of ticket id'
52 $ git push

That should have failed, which is what you wanted. The reason you wanted it to fail is buried in the grep you typed in:

grep '[A-Z][A-Z]*-[0-9][0-9]*'

This grep only returns successfully if it matches a string that matches the format of a JIRA ticket ID (eg PROJ-123). The end effect is to enforce that the last commit being pushed must have a reference to such a ticket ID for it to be accepted. You might want such a policy to ensure that every set of commits can be traced back to a ticket ID.

Cleanup

To clean up what you just did:

53 $ cd ../..
54 $ rm -rf lgthw_hooks

What You Learned

We’ve only scratched the surface of what commit hooks can do, and their subtleties and complexities. But you should now be able to:

  • Know what a git hook is
  • Understand the difference between a client-side and server-side hook
  • Implement your own git hooks
  • Understand how GitHub/BitBucket’s hook mechanisms work

Learn Bash the Hard Way

Learn Git the Hard Way

Learn Terraform the Hard Way


Get 39% off Docker in Practice with the code: 39miell

2 thoughts on “Git Hooks the Hard Way

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.