Chapter 14. Hooks

You can use a Git hook to run one or more arbitrary scripts whenever a particular event, such as a commit or a patch, occurs in your repository. Typically, an event is broken into several prescribed steps, and you can tie a custom script to each step. When the Git event occurs, the appropriate script is called at the outset of each step.

Hooks belong to and affect a specific repository and are not copied during a git clone. In other words, hooks you set up in your private repository are not propagated to and do not alter the behavior of the new clone. If for some reason your development process mandates hooks in each coder’s personal development repository, arrange to copy the directory .git/hooks through some other (nonclone) method.

A hook runs either in the context of your current, local repository or in the context of the remote repository. For example, fetching data into your repository from a remote repository and making a local commit can cause local hooks to run; pushing changes to a remote repository may cause hooks in the remote repository to run.

Most Git hooks fall into one of two categories:

  • A pre hook runs before an action completes. You can use this kind of hook to approve, reject, or adjust a change before it’s applied.

  • A post hook runs after an action completes and can be used to trigger notifications (such as email) or launch additional processing, such as running a build or closing a bug.

As a general rule, if a pre-action hook exits with a nonzero status (the convention to indicate failure), the Git action is aborted. In contrast, the exit status of a post-action hook is generally ignored because the hook can no longer affect the outcome or completion of the action.

In general, the Git developers advocate using hooks with caution. A hook, they say, should be a method of last resort, to be used only when you can’t accomplish the same result in some other way. For example, if you want to specify a particular option each time you make a commit, check out a file, or create a branch, a hook is unnecessary. You can accomplish the same task with a Git alias (see Configuring an Alias) or with shell scripts to augment git commit, git checkout, and git branch, respectively.[27]

At first blush, a hook may seem an appealing and straightforward solution. However, there are several implications of its use:

  • A hook changes the behavior of Git. If a hook performs an unusual operation, other developers familiar with Git may run into surprises when using your repository.

  • A hook can slow operations that are otherwise fast. For example, developers are often enticed to hook Git to run unit tests before anyone makes a commit, but this makes committing slow. In Git, a commit is supposed to be a fast operation, thus encouraging frequent commits to prevent the loss of data. Making a commit run slowly makes Git less enjoyable.

  • A hook script that is buggy can interfere with your work and productivity. The only way to work around a hook is to disable it. In contrast, if you use an alias or shell script instead of a hook, you can always fall back on the normal Git command wherever that makes sense.

  • A repository’s collection of hooks is not automatically replicated. Hence, if you install a commit hook in your repository, it won’t reliably affect another developer’s commits. This is partly for security reasons—a malicious script could easily be smuggled into an otherwise innocuous-looking repository—and partly because Git simply has no mechanism to replicate anything other than blobs, trees, and commits.

With those warnings behind us, hooks exist for very good reasons, and their use can be incredibly advantageous.

Installing Hooks

Each hook is a script, and the collection of hooks for a particular repository can be found in the .git/hooks directory. As mentioned earlier, Git doesn’t replicate hooks between repositories; if you git clone or git fetch from another repository, you won’t inherit that repository’s hooks. You have to copy the hook scripts by hand.

Each hook script is named after the event with which it is associated. For example, the hook that runs immediately before a git commit operation is named .git/hooks/pre-commit.

A hook script must follow the normal rules for Unix scripts: it must be executable (chmod a+x .git/hooks/pre-commit) and must start with a line indicating the language in which the script is written (for example, #!/bin/bash or #!/usr/bin/perl).

If a particular hook script exists and has the correct name and file permissions, Git uses it automatically.

Example Hooks

Depending on your exact version of Git, you may find some hooks in your repository at the time it’s created. Hooks are copied automatically from your Git template directory at the time you create a new repository. On Debian and Ubuntu, for example, the hooks are copied from /usr/share/git-core/templates/hooks. Most Git versions include some example hooks that you can use, and these are preinstalled for you in the templates directory.

Here’s what you need to know about the example hooks:

  • The template hooks probably don’t do exactly what you want. You can read them, edit them, and learn from them, but you rarely want to use them as is.

  • Even though the hooks are created by default, all the hooks are initially disabled. Depending on your version of Git and your operating system, the hooks are disabled either by removing the execute bit or by appending .sample to the hook filename. Modern versions of Git have executable hooks named with a .sample suffix.

  • To enable an example hook, you must remove the .sample suffix from its filename (mv .git/hooks/pre-commit.sample .git/hooks/pre-commit) and set its execute bit, as is apropos (chmod a+x .git/hooks/pre-commit).

Originally, each example hook was simply copied into the .git/hooks/ directory from the template directory with its execute permission removed. You could then enable the hook by setting its execute bit.

That worked fine on systems like Unix and Linux, but it didn’t work well on Windows. In Windows, file permissions work differently and, unfortunately, files are executable by default. This means the example hooks were executable by default, causing great confusion among new Git users because all the hooks ran when none should have.

Because of this problem with Windows, newer versions of Git suffix each hook filename with .sample so it won’t run even if it’s executable. To enable the example hooks, you’ll have to rename the appropriate scripts yourself.

If you aren’t interested in the example hooks, it is perfectly safe to remove them from your repository: rm .git/hooks/*. You can always get them back by copying them from their home in the templates directory.

Tip

In addition to the template examples, there are more example hooks in Git’s contrib directory, a portion of the Git source code. The supplemental files may also be installed along with Git on your system. On Debian and Ubuntu, for example, the contributed hooks are installed in /usr/share/doc/git-core/contrib/hooks.

Creating Your First Hook

To explore how a hook works, let’s create a new repository and install a simple hook. First, we create the repository and populate it with a few files:

$ mkdir hooktest

$ cd hooktest

$ git init
Initialized empty Git repository in .git/

$ touch a b c

$ git add a b c

$ git commit -m 'added a, b, and c'
Created initial commit 97e9cf8: added a, b, and c
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a
 create mode 100644 b
 create mode 100644 c

Next, let’s create a pre-commit hook to prevent checking in changes that contain the word broken. Using your favorite text editor, put the following in a file called .git/hooks/pre-commit:

#!/bin/bash
echo "Hello, I'm a pre-commit script!" >&2
if git diff --cached | grep '^+' | grep -q 'broken'; then
        echo "ERROR: Can't commit the word 'broken'" >&2
        exit 1  # reject
fi
exit 0  # accept

The script generates a list of all differences about to be checked in, extracts the lines to be added (that is, those lines that begin with a + character), and scans those lines for the word broken.

There are many ways to test for the word broken, but most of the obvious ones result in subtle problems. I’m not talking about how to test for the word ‘broken’ but how to find the text to be scanned for the word broken.

For example, you might have tried the test

if git ls-files | xargs grep -q 'broken'; then

or, in other words, searched for the word broken in all files in the repository. But this approach has two problems. If someone else had already committed a file containing the word broken, this script would prevent all future commits (until you fix it), even if those commits are totally unrelated. Moreover, the Unix grep command has no way of knowing which files will actually be committed; if you add broken to file b, make an unrelated change to a, and then run git commit a, there’s nothing wrong with your commit, because you’re not trying to commit b. However, a script with this test would reject it anyway.

Tip

If you write a pre-commit script that restricts what you’re allowed to check in, it’s almost certain that you’ll need to bypass it someday. You can bypass the pre-commit hook either by using the --no-verify option to git commit or by temporarily disabling your hook.

Now that you’ve created the pre-commit hook, make sure it’s executable:

$ chmod a+x .git/hooks/pre-commit

And now you can test that it works as expected:

$ echo "perfectly fine" >a

$ echo "broken" >b

$ git commit -m "test commit -a" -a
Hello, I'm a pre-commit script!
ERROR: Can't commit the word 'broken'

$ git commit -m "test only file a" a
Hello, I'm a pre-commit script!
Created commit 4542056: test
1 files changed, 1 insertions(+), 0 deletions(-)

$ git commit -m "test only file b" b
Hello, I'm a pre-commit script!
ERROR: Can't commit the word 'broken'

Observe that even when a commit works, the pre-commit script still emits Hello. This would be annoying in a real script, so you should use such messages only while debugging the script. Notice also that when the commit is rejected, git commit doesn’t print an error message; the only message is the one produced by the script. To avoid confusing the user, be careful always to print an error message from a pre script if it’s going to return a nonzero (reject) exit code.

Given those basics, let’s talk about the different hooks you can create.



[27] As it happens, running a hook at commit time is such a common requirement that a pre-commit hook exists for that, even though it isn’t strictly necessary.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.56.28