The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Git::Hooks - A framework for implementing Git hooks.

VERSION

version 0.015

SYNOPSIS

A single script can implement several Git hooks:

        #!/usr/bin/env perl

        use Git::Hooks;

        PRE_COMMIT {
            my ($git) = @_;
            # ...
        };

        COMMIT_MSG {
            my ($git, $msg_file) = @_;
            # ...
        };

        run_hook($0, @ARGV);

Or you can use Git::Hooks plugins or external hooks, driven by the single script below. These hooks are enabled by Git configuration options. (More on this later.)

        #!/usr/bin/env perl

        use Git::Hooks;

        run_hook($0, @ARGV);

INTRODUCTION

"Git is a fast, scalable, distributed revision control system with an unusually rich command set that provides both high-level operations and full access to internals. (https://github.com/gitster/git#readme)"

In order to really understand what this is all about you need to understand Git http://git-scm.org/ and its hooks. You can read everything about this in the documentation references on that site http://git-scm.com/documentation.

A hook is a specifically named program that is called by the git program during the execution of some operations. At the last count, there were exactly 16 different hooks which can be used (http://schacon.github.com/git/githooks.html). They must reside under the .git/hooks directory in the repository. When you create a new repository, you get some template files in this directory, all of them having the .sample suffix and helpful instructions inside explaining how to convert them into working hooks.

When Git is performing a commit operation, for example, it calls these four hooks in order: pre-commit, prepare-commit-msg, commit-msg, and post-commit. The first three can gather all sorts of information about the specific commit being performed and decide to reject it in case it doesn't comply to specified policies. The post-commit can be used to log or alert interested parties about the commit just done.

There are several useful hook scripts available elsewhere, e.g. https://github.com/gitster/git/tree/master/contrib/hooks and http://google.com/search?q=git+hooks. However, when you try to combine the functionality of two or more of those scripts in a single hook you normally end up facing two problems.

Complexity

In order to integrate the funcionality of more than one script you have to write a driver script that's called by Git and calls all the other scripts in order, passing to them the arguments they need. Moreover, some of those scripts may have configuration files to read and you may have to maintain several of them.

Inefficiency

This arrangement is inefficient in two ways. First because each script runs as a separate process, which usually have a high startup cost because they are, well, scripts and not binaries. (For a dissent view on this, see http://gnustavo.wordpress.com/2012/06/28/programming-languages-start-up-times/.) And second, because as each script is called in turn they have no memory of the scripts called before and have to gather the information about the transaction again and again, normally by calling the git command, which spawns yet another process.

Git::Hooks is a framework for implementing Git and driving existing external hooks in a way that tries to solve these problems.

Instead of having separate scripts implementing different functionality you may have a single script implementing all the funcionality you need either directly or using some of the existing plugins, which are implemented by Perl scripts in the Git::Hooks:: namespace. This single script can be used to implement all standard hooks, because each hook knows when to perform based on the context in which the script was called.

If you already have some handy hooks and want to keep using them, don't worry. Git::Hooks can drive external hooks very easily.

USAGE

Go to the .git/hooks directory under the root of your Git repository. You should see there a bunch of hook samples. Create a script there using the Git::Hooks module.

        $ cd /path/to/repo/.git/hooks

        $ cat >git-hooks.pl <<EOT
        #!/usr/bin/env perl
        use Git::Hooks;
        run_hook($0, @ARGV);
        EOT

        $ chmod +x git-hooks.pl

This script will serve for any hook. Create symbolic links pointing to it for each hook you are interested in. (You may create symbolic links for all 16 hooks, but this will make Git call the script for all hooked operations, even for those that you may not be interested in. Nothing wrong will happen, but the server will be doing extra work for nothing.)

        $ ln -s git-hooks.pl pre-receive
        $ ln -s git-hooks.pl commit-msg
        $ ln -s git-hooks.pl post-commit

As is, the script won't do anything. You have to implement some hooks in it, use some of the existing plugins, or set up some external plugins to be invoked properly. Either way, the script should end with a call to run_hook passing to it the name with which it was called ($0) and all the arguments it received (@ARGV).

Implementing Hooks

Implement hooks using one of the hook directives described in the HOOK DIRECTIVES section. For example:

    # Check if every added/updated file is smaller than a fixed limit.

    my $LIMIT = 10 * 1024 * 1024; # 10MB

    PRE_COMMIT {
        my ($git) = @_;

        my @changed = $git->command(qw/diff --cached --name-only --diff-filter=AM/);

        foreach ($git->command('ls-files' => '-s', @changed)) {
            chomp;
            my ($mode, $sha, $n, $name) = split / /;
            my $size = $git->command('cat-file' => '-s', $sha);
            $size <= $LIMIT
                or die "File '$name' has $size bytes, more than our limit of $LIMIT.\n";
        }
    };

    # Check if every added/changed Perl file respects Perl::Critic's code
    # standards.

    PRE_COMMIT {
        my ($git) = @_;
        my %violations;

        my @changed = grep {/\.p[lm]$/} $git->command(qw/diff --cached --name-only --diff-filter=AM/);

        foreach ($git->command('ls-files' => '-s', @changed)) {
            chomp;
            my ($mode, $sha, $n, $name) = split / /;
            require Perl::Critic;
            state $critic = Perl::Critic->new(-severity => 'stern', -top => 10);
            my $contents = $git->command('cat-file' => $sha);
            my @violations = $critic->critique(\$contents);
            $violations{$name} = \@violations if @violations;
        }

        if (%violations) {
            # FIXME: this is a lame way to format the output.
            require Data::Dumper;
            die "Perl::Critic Violations:\n", Data::Dumper::Dumper(\%violations), "\n";
        }
    };

Using Plugins

There are several hooks already implemented as plugin modules under the namespace Git::Hooks::, which you can use. The main ones are described succinctly below. Please, see their own documentation for more details.

Git::Hooks::check-acls.pl

Allow you to specify Access Control Lists to tell who can commit or push to the repository and affect which Git refs.

Git::Hooks::check-jira.pl

Integrate Git with the JIRA http://www.atlassian.com/software/jira/ ticketing system by requiring that every commit message cites valid JIRA issues.

Each plugin may be used in one or, sometimes, multiple hooks. Their documentation is explicit about this.

Invoking external hooks

Since the default Git hook scripts are taken by the Git::Hooks driver script, you must install your external hooks somewhere else. By default, the run_hook routine will look for external hook scripts in the directory .git/hooks.d (which you must create) under the repository. Below this directory you should have another level of directories, named after the default hook names, under which you can drop your external hooks.

For example, let's say you want to use some of the hooks in the standard Git package (https://github.com/gitster/git/blob/b12905140a8239ac687450ad43f18b5f0bcfb62e/contrib/hooks/update-paranoid). You should copy each of those scripts to a file under the appropriate hook directory, like this:

.git/hooks.d/pre-auto-gc/pre-auto-gc-battery
.git/hooks.d/pre-commit/setgitperms.perl
.git/hooks.d/post-receive/post-receive-email
.git/hooks.d/update/update-paranoid

Note that you may install more than one script under the same hook-named directory. The driver will execute all of them in a non-specified order. If any of them exits abnormally, the driver will exit with an appropriate error message.

CONFIGURATION

Git::Hooks is configured via Git's own configuration infrastructure. The framework defines a few options which are described below. Each plugin may define other specific options which are described in their own documentation.

You should get comfortable with git config command (read git help config) to know how to configure Git::Hooks.

When you invoke run_hook, the command git config --list is invoked to grok all configuration affecting the current repository. Note that this will fetch all --system, --global, and --local options, in this order. You may use this mechanism to define configuration global to a user or local to a repository.

githooks.applypatch-msg PLUGIN

githooks.pre-applypatch PLUGIN

githooks.post-applypatch PLUGIN

githooks.pre-commit PLUGIN

githooks.prepare-commit-msg PLUGIN

githooks.commit-msg PLUGIN

githooks.post-commit PLUGIN

githooks.pre-rebase PLUGIN

githooks.post-checkout PLUGIN

githooks.post-merge PLUGIN

githooks.pre-receive PLUGIN

githooks.update PLUGIN

githooks.post-receive PLUGIN

githooks.post-update PLUGIN

githooks.pre-auto-gc PLUGIN

githooks.post-rewrite PLUGIN

To enable a plugin you must register it with one of the above options. For instance, if you want to enable the check-jira.pl plugin in the update hook, you must do this:

    $ git config --add githooks.update check-jira.pl

Note that you may enable more than one plugin to the same hook. For instance:

    $ git config --add githooks.update check-acls.pl

And you may enable the same plugin in more than one hook, if it makes sense to do so. For instance:

    $ git config --add githooks.commit-msg check-jira.pl

githooks.plugins DIR

The plugins enabled for a hook are searched for in three places. First they're are searched for in the githooks directory under the repository path (usually in .git/githooks), so that you may have repository specific hooks (or repository specific versions of a hook).

Then, they are searched for in every directory specified with the githooks.plugins option. You may set it more than once if you have more than one directory holding your hooks.

Finally, they are searched for in Git::Hooks installation.

The first match is taken as the desired plugin, which is executed and the search stops. So, you may want to copy one of the standard plugins and change it to suit your needs better. (Don't shy away from sending your changes back to us, though.)

githooks.externals [01]

By default the driver script will look for external hooks after executing every enabled plugins. You may disable external hooks invokation by setting this option to 0.

githooks.hooks DIR

You can tell this plugin to look for external hooks in other directories by specifying them with this option. The directories specified here will be looked for after the default directory .git/hooks.d, so that you can use this option to have some global external hooks shared by all of your repositories.

Please, see the plugins documentation to know about their own configuration options.

MAIN METHOD

run_hook(NAME, ARGS...)

This is the main routine responsible to invoke the right hooks depending on the context in which it was called.

Its first argument must be the name of the hook that was called. Usually you just pass $0 to it, since it knows to extract the basename of the parameter.

The remaining arguments depend on the hook for which it's being called. Usually you just pass @ARGV to it. And that's it. Mostly.

        run_hook($0, @ARGV);

HOOK DIRECTIVES

Hook directives are routines you use to register routines as hooks. Each one of the hook directives gets a routine-ref or a single block (anonymous routine) as argument. The routine/block will be called by run_hook with proper arguments, as indicated below. These arguments are the ones gotten from @ARGV, with the exception of the ones identified by GIT. These are Git::More objects which can be used to grok detailed information about the repository and the current transaction. (Please, refer to the Git::More documentation to know how to use them.)

Note that the hook directives resemble function definitions but they aren't. They are function calls, and as such must end with a semi-colon.

Most of the hooks are used to check some condition. If the condition holds, they must simply end without returning anything. Otherwise, they must die with a suitable error message. On some hooks, this will prevent Git from finishing its operation.

Also note that each hook directive can be called more than once if you need to implement more than one specific hook.

APPLYPATCH_MSG(GIT, commit-msg-file)

PRE_APPLYPATCH(GIT)

POST_APPLYPATCH(GIT)

PRE_COMMIT(GIT)

PREPARE_COMMIT_MSG(GIT, commit-msg-file [, msg-src [, SHA1]])

COMMIT_MSG(GIT, commit-msg-file)

POST_COMMIT(GIT)

PRE_REBASE(GIT)

POST_CHECKOUT(GIT, prev-head-ref, new-head-ref, is-branch-checkout)

POST_MERGE(GIT, is-squash-merge)

PRE_RECEIVE(GIT)

UPDATE(GIT, updated-ref-name, old-object-name, new-object-name)

POST_RECEIVE(GIT)

POST_UPDATE(GIT, updated-ref-name, ...)

PRE_AUTO_GC(GIT)

POST_REWRITE(GIT, command)

METHODS FOR PLUGIN DEVELOPERS

Plugins should start by importing the utility routines from Git::Hooks:

    use Git::Hooks qw/:utils/;

Usually at the end, the plugin should use one or more of the hook directives defined above to install its hook routines in the apropriate hooks.

Every hook routine receives a Git::More object as its first argument. You should use it to infer all needed information from the Git repository.

Please, take a look at the code for the standard plugins under the Git::Hooks:: namespace in order to get a better understanding about this. Hopefully it's not that hard.

The utility routines implemented by Git::Hooks are the following:

hook_config(NAME)

This routine returns a hash-ref containing every configuration variable for the section NAME. It's usually called with the name of the plugin as argument, meaning that the plugin configuration is contained in a section by its name.

is_ref_enabled(SPECS, REF)

This routine returns a boolean indicating if REF matches one of the ref-specs in SPECS. REF is the complete name of a Git ref and SPECS is a reference to an array of strings, each one specifying a rule for matching ref names.

You may want to use it, for example, in an update or in a pre-receive hook which may be enabled depending on the particular refs being affected.

Each rule in SPECS may indicate the matching refs as the complete ref name (e.g. "refs/heads/master") or by a regular expression starting with a caret (^), which is kept as part of the regexp.

get_affected_refs()

This routine returns a list of all the Git refs affected by the current operation. During the update hook it returns the single ref passed via the command line. During the pre-receive hook it returns the list of refs passed via STDIN. During any other hook it returns the empty list.

get_affected_ref_range(REF)

This routine returns the two-element list of commit ids representing the OLDCOMMIT and the NEWCOMMIT of the affected REF.

get_affected_ref_commit_ids(REF)

This routine returns the list of commit ids leading from the affected REF's NEWCOMMIT to OLDCOMMIT.

get_affected_ref_commits(REF)

This routine returns the list of commits leading from the affected REF's NEWCOMMIT to OLDCOMMIT. The commits are represented by hashes, as returned by Git::More::get_commits.

SEE ALSO

Git::More.

AUTHOR

Gustavo L. de M. Chaves <gnustavo@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by CPqD <www.cpqd.com.br>.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.