Chapter 15
Extending Git Functionality with Git Hooks

Now that I’ve covered the overall Git workflow and functionality in detail, let’s look at one way you can extend and customize how Git works: hooks. Hooks are scripts or programs that run before or after (and in some cases during) a subset of operations in Git. There are hooks for local operations, such as commits or merges, and hooks for remote operations, such as when changes are pushed to the remote.

In some other source management systems, hooks may be called by other names, such as triggers. However, the concept is the same—a program or script runs when a certain event or operation happens. Here are some common uses for hooks:

  • Sending e-mail or other notifications when a change is pushed to a repository
  • Validating that certain conditions have been met before a commit
  • Appending items to commit messages
  • Checking the format or existence of certain elements in a commit message
  • Updating content in the working directory after an operation
  • Enforcing coding standards

INSTALLING HOOKS

By default, hooks exist in the hooks directory underneath your Git directory. Your Git directory is directly under your working directory unless you’ve overridden that location by setting a different value in the $GIT_DIR environment variable, or by passing a different value to --git-dir on the Git command line. As a result, the default setting means that the hooks reside in .git/hooks.

As of Git 2.9, you can set a different relative or absolute path for hooks using the core.hooksPath configuration value. This allows you to set a centralized path to find the hooks for multiple projects or even multiple users if everyone has access to the path. For the rest of this chapter, I will refer to the hooks directory to mean wherever you have set your hooks to be.

After cloning or using init to create a project, if you look in the .git/hooks directory, you see a list of files like the following:

myproject/.git/hooks$ ls
applypatch-msg.sample*     pre-push.sample*
commit-msg.sample*         pre-rebase.sample*
post-update.sample*        prepare-commit-msg.sample*
pre-applypatch.sample*     update.sample*
pre-commit.sample*

Notice that all of these files end in .sample. The part of the filename before .sample is the actual hook name that Git expects. The .sample extension is there to allow the files to exist as sample hooks without actually being executed as hooks. In effect, the extension disables the hook. To enable any of these hooks, you need to remove the .sample extension. For example, to enable the commit-msg hook in this repository, you add whatever code or processing you want to use to the commit-msg.sample file and rename it as simply commit-msg. When Git verifies whether the commit-msg hook is enabled, it finds the file in the hooks directory with the expected name and executes the file. (Note that hook files also need to be executable, so it may be necessary to change the permissions for the file if it is not already executable.)

By default, the initial set of files in .git/hooks comes from the hooks directory of the template area where Git is installed (or where configuration points to the template). Running git init again on an existing directory picks up new content from the template area but doesn’t overwrite changed content. This provides another option for creating new hooks and distributing them to existing repositories by re-running the init command. See the help page for git init for more information on how to specify the location of the template area to populate from.

UPDATING HOOKS

Because hooks are not updated as part of a clone, pull, or fetch, it can be challenging to get updated hooks into all repositories that need them. There are a few methods that you can use to do this:

  • If everyone is using Git 2.9 or later, you can designate a commonly readable area for the hooks using the core.hooksPath configuration setting.
  • You can store the actual hooks scripts as part of the project and then copy or symlink them to the .git/hooks directory. You can also create a script file and include it in the project to automate the copying or symlinking from the working directory to the hooks directory when the script is run.
  • In a similar way, you can include a script with each project to copy updated hooks from a common area.
  • If the hook is a new file, you can update it in the templates area, and then users can run the git init command again to pick up the new file.

COMMON HOOK ATTRIBUTES

Next, let’s look at some of the attributes and behaviors that are shared across sets of hooks.

Hook Domain

Hooks can be targeted for execution on either the remote repository side (remote hooks) or the local repository side (local hooks). Local hooks are designed to run before or after local operations such as commit, checkout, rebase, and so on. Remote hooks are designed to run before or after operations on the remote side, such as push.

Here is the list of available local hooks:

Here is the list of available remote hooks:

  • pre-receive
  • update
  • post-receive
  • post-update
  • pre-auto-gc

Return Code to Control Workflow

In many cases, hooks are used not only to do additional processing, but also to serve as checks on whether or not operations should continue. In these cases, the hook checks some value or condition and exits with a return code to indicate whether or not the check passed or failed. A return code of zero indicates that all is good and the operation should continue. A non-zero return code indicates that something wasn’t correct or that a check failed, and tells Git to abort the operation. Different non-zero values can be returned to indicate specific error states.

Working Directory Access

One of the important considerations when a hook runs is what directory it is running in. Git defaults to one of two places to run hooks, depending on whether the hook is executing in a bare repository or a non-bare repository.

When Git runs a hook, if it is executing in a non-bare repository, it first changes to the root directory of the working directory. For example, if the local environment is in /usr/home/myrepo and an operation there causes a hook to run from /usr/home/myrepo/.git/hooks, then Git uses /usr/home/myrepo as the directory to run the hook in.

If the hook is running in a bare repository, then Git uses the directory of the bare repository. For example, if the bare repository is in /usr/share/git/repos/myrepo.git and you are executing a hook in /usr/share/git/repos/myrepo.git/hooks, then Git uses /usr/share/git/repos/myrepo.git as the ­directory to run the hook in.

Environment Variables

Each hook has access to some environment variables that are set through Git. Nearly all hooks have access to GIT_DIR. For the reasons outlined in the previous section, this variable usually has a value of “.git” for local hooks and “.” for remote hooks.

Hooks that work with the am or commit operations have values set for the date (GIT_AUTHOR_DATE), e-mail (GIT_AUTHOR_EMAIL), and username (GIT_AUTHOR_NAME), as set in the Git configuration. Some operations, such as those for rebasing or merging, also have a GIT_REFLOG_ACTION value corresponding to the operation being performed and whatever is written in the reflog.

HOOK DESCRIPTIONS

The rest of this chapter includes individual descriptions of the various hooks. The descriptions include what each hook does and what it is used for, information on bypassing (for those that can be bypassed), a description of the parameters the hook gets, and whether the hook can abort the operation.

For some of the hooks, I include example code that implements the basic hook functionality. These examples are written in several different languages, including shell, Ruby, Groovy, Python, and Perl, so you have a simple reference for each of them. The examples are contrived and not meant to include all functionality that a hook can provide or to be the best example of coding hooks. (These examples can also be downloaded from http://github.com/professional-git/hooks.)

Applypatch-msg

In Chapter 11, I talk about the git am command. The am command is designed to take a patch in an e-mail format and commit it into the local repository. The applypatch-msg hook is intended to operate on the proposed commit message for the apply part of the am operation.

Applypatch-msg takes a single parameter: the name of the temporary file that has the proposed commit message. A non-zero exit code from this hook aborts the apply part of the am operation. This hook can be used to verify the message file or even edit it in place.

Pre-applypatch

Pre-applypatch is another hook that works with the git am command. As I previously mentioned, the am command takes a patch, applies it, and commits the changes. The pre-applypatch hook is called after the patch is applied, but before any changes are committed. In this way, it allows Git to verify that the results of applying the patch are as expected. For example, this could mean verifying that the patch did no harm, by doing some kind of testing or building.

This hook does not take any parameters. If the hook exits with a non-zero return code, then the updated code (code with the patch applied) is not committed—at least not as part of the am operation.

Post-applypatch

Post-applypatch is the last of the hooks intended for use with the am command. This hook is called after the patch is applied and committed. It is primarily used for sending notifications. However, it can also be used for launching some kind of processing to verify that what was committed was viable. However, it would probably make more sense to do that kind of check with a pre-apply hook, as I described in the previous section.

The post-applypatch hook does not take any parameters. Because it is run after the apply and commit are completed, it cannot influence the outcome of the operation.

Pre-commit

As its name implies, the pre-commit hook is executed before a commit. In fact, it is executed before Git even asks the user to enter a commit message or puts together a commit object.

Pre-commit can be bypassed with the --no-verify option to git commit. It takes no parameters. A non-zero exit code from this hook aborts the commit.

You can use the pre-commit hook to verify that what’s about to be committed meets some condition or criteria and is okay to commit. An example script for this hook (written in Python) is included here. In this case, you don’t want to allow any files to be committed that have a reference to a company’s internal-use-only website (IUO_WEBSITE in the script). The script gets a list of staged files, and checks their contents for the disallowed string. If it finds the string, it prints an error message and exits with -1, thus failing the commit.

#!/usr/bin/env python

# Example pre-commit hook written in python
# (c) 2016 Brent Laster

import sys, os, subprocess

# define the string we want to check for
IUO_WEBSITE='http://internal.mycompany.com'
try:
             # Get the status from Git in the short form
             # Note: Could use -s instead of --porcelain
             #  but porcelain is guaranteed not to break
             #  backwards-compatibility between releases.
             status_output = subprocess.check_output(
                    ["git","status","-uno","--porcelain"],
                    stderr=subprocess.STDOUT).decode("utf-8")

             # create a list of status lines
             status_list = status_output.splitlines()

             for status_line in status_list:
                     # if the status line starts with A, then it is in the staging area
                    if status_line.startswith('A'):
                           status, _, status_file = status_line.partition(" ")
                           staged_file = status_file.lstrip()
                           # git the relative path from Git in case the file
                           #  is in a subdirectory of the working directory
                           rpath = subprocess.check_output(
                                 ["git","rev-parse","--show-prefix"],
                                stderr=subprocess.STDOUT).decode("ISO-8859-1")
                           relative_path = rpath.rstrip()

                           # construct the :<path> syntax needed for show
                           #  to dump the contents of the staged version
                           staged_fullpath = ":" + relative_path + staged_file

                           # Use the git show :<path> syntax to get the contents
                           #  of the actual staged version
                           staged_content = subprocess.check_output(
                                 ["git","show",staged_fullpath],
                                 stderr=subprocess.STDOUT).decode("ISO-8859-1")
                           # if we find the forbidden string, then abort the commit

                           if IUO_WEBSITE in staged_content:
                                 tpath = relative_path + staged_file
                                 print ("Error!",IUO_WEBSITE,"found in file",tpath)
                                 print ("Commit disallowed due to error.")
                                 sys.exit(-1)
               sys.exit(0)

except subprocess.CalledProcessError:
       print ("error!")
       sys.exit(-2)

Prepare-commit-msg

After the pre-commit hook checks and validates the content to be committed, the prepare-commit-msg hook is called. Its purpose is to do any additional editing or preparation of the commit message before it is brought up in an editor. (Note, however, that this hook is also called if you pass in a commit message with the -m option.)

The prepare-commit-msg hook takes a minimum of one and a maximum of three parameters, defined as follows:

  • Parameter 1—The name of the file that contains the proposed commit message.
  • Parameter 2—The type of operation that precipitates the message. The value can be one of the following:
    • message—If you passed a message to the commit using the -m option or the -F option, from a file
    • template—If you used the -t option or the commit.template configuration value was set
    • merge—If the commit is the result of a merge
    • squash—If the commit is the result of a squash
    • commit—If this is just a regular commit
  • Parameter 3—A SHA1 value if you used the --amend, -c, or -C option. (-C allows you to reuse a commit message from the specified SHA1 value; -c does the same, but also invokes the editor.)

The prepare-commit-msg hook is not suppressed by the --no-verify option. If this hook returns a non-zero return code, then the commit operation aborts.

An example script for this hook (written in Groovy) is included here. In this example, there is user-defined text that must be included in every commit message. The user-defined text is defined by configuring the new setting user.msg in Git. If that configuration setting is not found, the hook exits and aborts the commit. If the message already contains the text, the hook simply proceeds. Otherwise, if we are in the master branch, or a branch that starts with “prod” or “ship”, it appends the text to the commit message.

#!/usr/bin/env groovy

// Example prepare-commit-msg hook written in groovy
// (c) 2016 Brent Laster

import static java.lang.System.*

def argc = this.args.size()
def commitmsg_file_path, commit_type, commit_sha1

// Get explicit commit type if passed
commitmsg_file_path = this.args[0]
if (argc > 1)
       commit_type = this.args[1]
else
    commit_type = ''

if (argc > 2)
    commit_sha1 = this.args[2]
else
    commit_sha1 = ''

// See if we have user-defined message set
def iuo_msg = ["git", "config", "user.msg"].execute()
iuo_msg.waitFor()
def config_rc = iuo_msg.exitValue()
// If we don’t have the configuration value set, then abort
if (config_rc!=0)
{
       println "Configuration setting not found for user.msg."
       println "Aborting commit!"
       exit 1
}
def msg = iuo_msg.text

// Read in an existing commit message
def File commit_file = new File(commitmsg_file_path)

// If the commit message already contains the value, then just continue
if (commit_file.text.find(msg))
{
println("Commit message already contains $msg - proceeding…")
exit 0
}

// Determine the branch
def output_branch = ["git", "symbolic-ref", "--short", "HEAD"].execute()
output_branch.waitFor()
def current_branch = output_branch.text.trim()

// If it matches the desired branch names, then append the custom message
if  (current_branch.matches(/^master$|^(prod|ship).*$/))
{
       println "Current branch $current_branch is a production branch."
       commit_file.append(msg)
}
exit 0

Commit-message

The commit-msg hook is called after the user enters a commit message. Its main purpose is to validate the commit message that the user has entered. For example, it can check the message for certain required information or to make sure the contents fit an expected corporate format. It can also be used to edit the commit message file in place—for example, to append unique information to each commit message before the commit is processed. This append functionality is used in the Gerrit Code Review tool, where the commit-msg hook plays a key role in appending a unique Change-Id to each commit message. Note that, depending on need and timing, the functionality I discuss in the “Prepare-commit-message” section can also apply here, after the message is entered.

This hook can be bypassed with the --no-verify option. The hook takes one argument: the name of the temporary file with the commit message in it. If the hook returns a non-zero return code, then the commit operation aborts.

The example that I include for the commit-msg hook is written as a bash shell script. This hook implements several checks around the commit message. First, it checks to see if the commit message is a certain minimum length. If the message is too short, the hook aborts the commit. Second, it checks to see if the message is the same as the previous commit. If it is, the hook also aborts the commit. Finally, it checks to see if a user-defined message is included in the commit message. Like the prepare-commit-msg hook, the message is expected to be defined in the Git configuration value, user.msg.

#!/usr/bin/env bash

# Example commit-msg hook written in bash
# (c) 2016 Brent Laster

# define our error messages
error_msg1="Error: Commit message is too small."
error_msg2="Error: Commit message cannot be the same as the previous commit message."
error_msg3="Error: Expected text not in commit message:"
abort_msg="Commit will not be allowed due to error."
minimum_length=25

# just exit if we don't have any arguments
[ "$1" ] || exit 1

# read the contents of the temp commit message file and get the length of it
contents=$(<"$1")
size=${#contents}

# if the message is too short, error out
if [ $size -le $minimum_length ]; then
       echo "$error_msg1" >&2
       echo "$abort_msg" >&2
       exit 1
fi

# if we have a previous revision, make sure we're not using the same message as the last commit
if git rev-parse --verify HEAD >/dev/null 2>&1
then
    previous_log_msg=$(git show -s --format=%s)
       if [ "$previous_log_msg" == "$contents" ]; then
             echo "$error_msg2" >&2
             echo "$abort_msg" >&2
             exit 1
       fi
fi

# if our branch is master or starts with "prod" or "ship"
#  check to make sure we have the value defined in user.msg in the commit message
branch=$(git symbolic-ref --short HEAD)
if [[ $branch =~ ^master$|^(prod|ship).*$ ]]; then
       iuo_msg=$(git config user.msg)
       if ! grep -iqE "$iuo_msg" "$1"; then
             echo "$error_msg3" "$iuo_msg" >&2
             echo "$abort_msg" >&2
             exit 1
       fi
fi

# Redirect output to stderr.
exec 1>&2

exit 0

Post-commit

The post-commit hook is invoked after the commit-msg hook. It is primarily used for notification services, although you can also use it to launch a post-commit operation. For example, it could launch some kind of continuous integration builds or testing at the level of the local repository to ensure the code is good before you push it over to the remote repository.

Post-commit doesn’t take any parameters. Thus, you need to use some sort of git call in the hook to determine the latest SHA1 value to work against. There are a couple of calls you can use to do this, such as git log -1 HEAD or git rev-parse HEAD.

The post-commit hook’s exit code doesn’t affect the operation because the commit has already been done at this point. This hook, if it is active, can’t be bypassed by the --no-verify option.

Note that a corresponding hook is available on the remote side: post-receive. This is a better choice for launching continuous integration processes or testing if you want to run them against changes committed and then pushed from all users.

The following example is written in perl. It verifies whether the checkout is for a branch that starts with web. If so, it checks out a copy of the files into a separate directory. The user is expected to set the value for the desired directory using a Git configuration setting, hooks.webdir.

#!/usr/bin/env perl

# Example post-commit hook written in perl
# (c) 2016 Brent Laster

use strict;
use warnings;

use autodie;
use File::Temp qw( tempfile );
use IPC::Cmd qw( run );


# if we are doing a commit from a branch named web* then
# point git to the website worktree and do a checkout -f to mirror files out
my $web_dir = 'git config hooks.webdir';
chomp($web_dir);
my $new_head_ref = 'git rev-parse --abbrev-ref HEAD';

# remove since git index doesn’t exist here
delete $ENV{'GIT_INDEX_FILE'};
if (defined($web_dir) && ($new_head_ref =~ /^web.*$/)) {
 my $results = 'git --work-tree="$web_dir" --git-dir="$ENV{'GIT_DIR'}" checkout -f';
}

Pre-rebase

As its name implies, the pre-rebase hook is called prior to a git rebase and before Git actually does any operations related to the rebasing. As such, this hook provides an opportunity to validate that the rebase should go through, issue a warning message, and so on.

The sample hook that comes with Git for pre-rebase has an extensive example of how the hook can be used. It prevents topic branches that have already been merged from being rebased (and thus merged again).

The pre-rebase hook has one or two parameters passed to it. Parameter 1 is the upstream that the current series of commits came from. Parameter 2 is the branch being rebased (or it can be empty if that branch is the current branch).

Returning a non-zero return code aborts the operation.

Post-checkout

The post-checkout hook is called whenever you successfully do a checkout in Git. It is also run after a clone unless you specify the --no-checkout option. An example of using this hook is to remove automatically created files that you don’t need or want in the working directory, such as removing automatically generated backup files from an editor session.

The post-checkout hook gets three parameters. Parameter 1 is the reference of the previous HEAD (before the checkout). Parameter 2 is the reference of the current HEAD (after the checkout; this could be the same). Parameter 3 is a flag to indicate whether the checkout was for a branch or a file (1=branch and 0=file).

The exit code for this hook doesn’t have any effect on the checkout because that’s already been completed.

The following example script for post-checkout is written mainly as a one-line perl program wrapped in a shell script. In this example, the perl code gets a list of files, appends .bak to the filenames, and deletes those backup files if they exist.

#!/bin/sh
#
# Example post-checkout hook written
# Written as shell executing perl one-liner
# (c) 2016 Brent Laster

# Get a list of affected files and for each one, remove a backup file (.bak extension) if present
/usr/bin/perl -le '@files='git ls-tree --name-only -r '$2''; chomp(@files); @candidates=map {$_.".bak"} @files; unlink @candidates'

Post-merge

The post-merge hook is invoked after a successful git merge or git pull. You can use it to apply settings or data, such as permissions, after a merge is completed. (You can use a pre-commit hook to save these settings or data before the merge.) You can also use this hook to launch some process (test, install, and so on) after a merge if a particular file or files changed as a result of the merge—for example, to make sure something still builds or passes testing after being updated by a merge.

The post-merge hook gets one parameter: a flag indicating whether the merge was a squash or not. Because this hook is invoked after a merge (and only after a successful one), it cannot abort the merge or change its outcome.

Pre-push

The pre-push hook is called prior to a push to a remote. You can use it to prevent a push from happening.

Pre-push takes two parameters: the name of the destination remote (that is, origin), and the location of the destination remote (for example, http://gitsystem.site.com/myproject). If a named remote is not used, then parameter one also contains the location of the destination remote.

In addition to the two parameters, Git feeds this hook additional information about what is targeted for pushing through the hook’s standard input (stdin). Information about each item to be pushed is passed on a separate line, and formatted as follows:

<local reference> <local sha1> <remote reference> <remote sha1> LF

Each of the SHA1 values here is the full 40-character value.

As an example, if you execute the command

$ git push origin master:prod

the hook gets an input line similar to this one:

refs/heads/master A1B2C3<snip>FEDF refs/heads/prod  C2EA3F<snip>DE45

(Here, the SHA1 values do not show a full 40-character string for brevity. Instead, I use the <snip> nomenclature to indicate the missing characters.)

If the remote reference doesn’t exist yet, the remote SHA1 value is all zeros.

refs/heads/master A1B2C3<snip>FEDF refs/heads/prod 000000…0000

If a reference is intended to be deleted, the local reference is (delete) and the local SHA1 value is all zeros.

(delete) 000000<snip>0000 refs/heads/prod C2EA3F<snip>DE45

And if a non-branch reference is supplied for the local reference, it is passed as given.

HEAD~  A1B2C3<snip>FEDF refs/heads/prod C2EA3F<snip>DE45  

This provides a number of options for checking what is happening with the push. If the pre-push hook returns a non-zero value, the push is aborted.

Pre-receive

The pre-receive hook is invoked on the remote side by the git receive-pack command. As you can probably tell by the name, receive-pack is one of the Git plumbing commands. Users don’t normally invoke this command directly. Rather, it is invoked through a higher-level command that wraps it: git push.

Actually, git push invokes another plumbing command—git send-pack—which then invokes receive-pack. The syntax for the two plumbing commands is as follows:

git-receive-pack <directory>
git send-pack [--all] [--dry-run] [--force] [--receive-pack=<git-receive-pack>]
             [--verbose] [--thin] [--atomic]
             [--[no-]signed|--sign=(true|false|if-asked)]
             [<host>:]<directory> [<ref>…]

I won’t go into detail about these plumbing commands, but you should get the idea: push needs to send data to the remote, and the remote side needs to receive it.

The pre-receive hook doesn’t take any arguments. However, for each reference that is intended to be updated, the hook gets a line sent to it on stdin. Each line is of the form,

<old value> <new value> <reference name> LF

The old and new values are SHA1 values. Remember, you are updating something on the remote side (new value) from something on the local side (old value) using the push command.

You can generally think of this as one line per branch being pushed with the old and new SHA1 values for each branch in a line. A value of all zeros for one of the SHA1 values (old or new) is used to indicate a particular situation. If <old value> is equal to all zeros, that indicates a reference to be created. If <new value> is equal to all zeros, that indicates a reference to be deleted.

The pre-receive hook runs once—just before references actually start getting updated on the remote repository. If the hook exits with a non-zero return code, none of the references are updated. Even if the hook returns a zero return code, the updates of the references can still be denied by the update hook (described in the following section).

The hook sends stdout and stderr back to send-pack so the messages can be displayed to the user.

Update

The update hook is similar to the pre-receive hook. It is invoked in a similar manner through the receive-pack operation as part of a push. (See the “Pre-receive” section for more information.)

The difference between the update hook and the pre-receive hook is that the update hook is invoked once for each reference to be updated—as opposed to once for the push operation, which is the case with the pre-receive hook. As a result, you can use the update hook to allow or disallow the updating of each reference based on some checking or criteria.

The update hook takes three parameters: the name of the reference being updated, the old object’s identifier (SHA1), and the new object’s identifier (SHA1).

If the hook returns a non-zero return code, that reference will not be updated. Again, because this hook is called for each reference, failing one reference does not mean that all of them will be updated.

Post-receive

The post-receive hook is similar to the pre-receive hook. It is invoked in a similar manner through the receive-pack operation as part of a push. (See the “Pre-receive” section for more information.) The difference is that post-receive is invoked only after all the references have been updated. For example, you can use it to send notifications after the updates are complete or to do additional logging.

Post-receive doesn’t take any arguments. However, for each reference that is intended to be updated, the hook gets a line sent to it on stdin. Each line is of the form

<old value> <new value> <reference name> LF

You can generally think of this as one line per branch being pushed with the old and new SHA1 values for each branch in a line. The values here have the same meaning as with the pre-receive hook.

The post-receive hook runs once after all the references have been updated. It has no effect on the operation or updates because all of the updates have already been done at that point. The hook sends stdout and stderr back to send-pack so that the messages can be displayed to the user.

The following example script is written in Ruby. If there is a configuration value set (user.deploy-dir), and the branch that was updated was either master or starts with prod or ship, then the hook attempts to do a deployment (checkout -f) of the content out to the directory specified in user.deploy-dir.

#!/usr/bin/env ruby

# Example post-receive hook written in ruby
# (c) 2016 Brent Laster

puts "Running post-receive hook…"
deployment_dir='git config user.deploy-dir'
# rest of hook presumes deployment_dir exists

# if the configuration value isn't set for Git, don't deploy
if (deployment_dir == "")
       puts "user.deploy_dir value not configured"
       puts "Will not deploy"
else
       # multiple lines might be passed in - one for each branch being pushed
       STDIN.each do |input_line|
             (prev_rev, new_rev, refspec) = input_line.split
             refspec.gsub!('refs/heads/','')
             # only deploy if we're on master or branch that starts with prod or ship
             if refspec =~ /^master|^(prod|ship).*$/
                    # do the deploy
                    # git-dir is already set for the hook
                    deployment_dir.chomp()
                    puts "Deploying to #{deployment_dir}"
       result = 'git --work-tree=#{deployment_dir.chomp()} checkout -f #{refspec}'
                    puts "#{result}"
             end
    end
end

exit

Post-update

The post-update hook is similar to the post-receive hook. It is invoked in a similar manner through the receive-pack operation as part of a push. (See the “Pre-receive” section for more information.)

Post-update takes a variable number of parameters. Each parameter is the name of a reference that was updated.

The post-update hook knows what was updated, but unlike the post-receive hook, it doesn’t know the old and new values. However, you can use it for something like updating dumb (HTTP) servers when changes are made (such as through git update-server-info). The sample hook that comes with Git has an example of this. The hook sends stdout and stderr back to send-pack so the messages can be displayed to the user.

OTHER HOOKS

Git supports a few other hooks that are only useful in special cases. I won’t go into detail about these hooks, but I will briefly mention them for completeness. You can find more information on these hooks in the git hooks documentation (git hooks --help).

Push-to-checkout

In Git, it is possible to push to a non-bare repository. A non-bare repository is one that has a working directory and staging area and a checked-out copy of a branch. Repositories that you push to are usually bare. They don’t have a working directory or staging area because they aren’t meant to have content checked out directly from them.

If you try to push to a non-bare repository, as opposed to the usual commit operation, the working directory and staging area attached to that repository won’t reflect the current status, as they would if you checked things out from a local repository. So, there is a receive.denyCurrentBranch setting that can prevent this. The push-to-checkout hook can override that setting.

The push-to-checkout hook gets one parameter—the commit targeted for the update—and can return a non-zero code to block the push. Or, it can sync the working directory and the staging area up to make things consistent and return zero.

Pre-auto-gc

In the pre-auto-gc hook, gc refers to garbage collection—having Git clean up objects that aren’t being used anymore (don’t have any connections) in the repository. The auto part refers to an option that can be passed to that command to clean up if there are too many loose objects over a configured threshold. As a result, this hook runs first if git gc --auto is run. You can use it to do some sort of notification or verification.

The pre-auto-gc hook takes no parameters. If it returns a non-zero return code, the gc operation aborts.

Post-rewrite

The post-rewrite hook, if enabled, runs after either of two commands that rewrite history (git commit --amend or git rebase). Currently, its only parameter is the command that called it. On stdin, it receives information about what commits were rewritten in the form

<old sha1> <new sha1> [ <optional extra data> ]

In the future, additional data may be passed, but no extra data is currently passed.

HOOKS QUICK REFERENCE

Table 15.1 summarizes some of the basic information about the hooks that are available in Git. The first column contains a list of Git operations that have hooks available for them. (Note that some of these operations use only one option.) The remaining five columns identify the hooks by name and parameter descriptions for the operation, in the order of execution.

Table 15.1 List of Git Hooks by Operation

Git Operation Pre-operation Hook 1 Pre-operation Hook 2 During-Operation Hook Post-Operation Hook 1 Post-Operation Hook 2
am applypatch-msg
P1: Name of temporary file with proposed commit message
pre-applypatch post-applypatch
commit pre-commit prepare-commit-msg P1: Name of temporary file with proposed commit message P2: Type of operation P3: SHA1 value for certain operations commit-msg P1: Name of temporary file with proposed commit message post-commit
rebase pre-rebase P1: Upstream P2: Branch being rebased (if not the same as P1)
checkout post-checkout P1: Previous HEAD (before checkout) P2: Current HEAD (after checkout) P3: Checkout type flag: 1 = branch, 2 = file
merge, pull post-merge P1: Flag that indicates squash or merge
push pre-push (local) P1: Remote reference name P2: Remote URL Extra: STDIN lines of the form <local reference> <local sha1> <remote reference> <remote sha1> LF pre-receive (remote) No parameters Extra: STDIN lines of the form <old value> <new value> <reference name> LF update (remote) P1: Name of reference being updated P2: Old SHA1 value P3: New SHA1 value post-receive (remote) No parameters Extra: STDIN lines of the form <old value> <new value> <reference name> LF post-update (remote) P* (variable number): Name of references being updated
push (to non-bare repository) push-to-checkout P1: Target commit for updating
gc --auto pre-auto-gc
rebase, commit --amend post-rewrite P1: Command that invoked it. Extra: STDIN lines of the form <old sha1> <new sha1> [ <optional extra data> ]

The shading of the background for each cell in the table indicates whether or not the hook can abort the operation. The key is as follows:

Black background—This hook can abort the Git operation and indicate it failed.

White background—This hook does not affect whether the operation succeeds or fails.

Gray background —This hook can abort the operation, but the hook itself may be overridden. (For the two cases here, the --no-verify option can skip running the hook.) Also, in the table, the P# values indicate a parameter passed to the hook. The value of # indicates the order it is passed in a sequence.

SUMMARY

In this final chapter of the book, you’ve seen how to extend Git functionality through the various hooks that it provides. Each hook is a point where you can cause a script or program that you create to be executed before or after (or sometimes during) certain events related to a Git operation.

As you have seen, some of the hooks allow the user to abort the operation by returning a non-zero return code, while others are more suited for simple notifications.

Hooks can be written in any language that can be executed on the system. It is important to ensure that you understand the conditions under which the hook will be called, the arguments that it will be passed, and any environment settings that it needs to use.

Hooks can greatly enhance the usefulness users get out of Git and customize it to meet the needs of teams as well as enforce policies. For local hooks, however, it’s important to establish a consistent way to ensure that all users have the same set of hooks in place on all repositories.

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

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