NAME

Git::Hooks - Framework for implementing Git (and Gerrit) hooks

VERSION

version 3.6.0

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 v5.16.0;
        use warnings;
        use Git::Hooks;

        run_hook($0, @ARGV);

In fact, this module installs a script called githooks.pl exactly like that, so that all you have to do is to create symbolic links in your Git repository's .git/hook pointing to it.

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. (Git README)"

If you already know about Git and hooks and simply want to get on with business go straight to our wiki and read the relevant tutorials.

In order to really understand what this is all about you need to understand Git and its hooks. You can read everything about this in the documentation references on that site.

A Git 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 17 different hooks. They must be kept 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 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 next two can be used to format or check the commit message. The post-commit can be used to log or alert interested parties about the commit just performed.

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 functionality 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 start up cost because they are, well, scripts and not binaries. (For a dissent view on this, see this.) 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 hooks 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 functionality you need either directly or using some of the existing plugins, which are implemented by Perl scripts in the Git::Hooks:: name space. 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

Please, read the Git::Hooks::Tutorial if you want an easy guide to start using the framework. Most probably you can set it up in a few minutes with it. Continue on if you want to get deeper in the Documentation.

There are a few simple steps you should do in order to set up Git::Hooks so that you can configure it to use some predefined plugins or start coding your own hooks.

The first step is to create a generic script that will be invoked by Git for every hook. If you are implementing hooks in your local repository, go to its .git/hooks sub-directory. If you are implementing the hooks in a bare repository in your server, go to its hooks sub-directory.

You should see there a bunch of files with names ending in .sample which are hook examples. Create a three-line script called, e.g., git-hooks.pl, in this directory like this:

        $ 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

Now you should create symbolic links pointing to it for each hook you are interested in. For example, if you are interested in a commit-msg hook, create a symbolic link called commit-msg pointing to the git-hooks.pl file. This way, Git will invoke the generic script for all hooks you are interested in. (You may create symbolic links for all 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 commit-msg
        $ ln -s git-hooks.pl post-commit
        $ ln -s git-hooks.pl pre-receive

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

You may implement your own hooks using one of the hook directives described in the HOOK DIRECTIVES section below. Your hooks may be implemented in the generic script you have created. They must be defined after the use Git::Hooks line and before the run_hook() line.

A hook should return a Boolean value indicating if it was successful. run_hook dies after invoking all hooks if at least one of them returned false.

run_hook invokes the hooks inside an eval block to catch any exception, such as if a die is used inside them. When an exception is detected the hook is considered to have failed and the exception string ($@) is showed to the user.

The best way to produce an error message is to invoke the Git::Repository::Plugin::GitHooks::error method passing a prefix and a message for uniform formatting. Note that any hook invokes this method it counts as a failure, even if it ultimately returns true!

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->filter_files_in_index('AM');

        my $errors = 0;

        foreach ($git->run(qw/ls-files -s/, @changed)) {
            my ($mode, $sha, $n, $name) = split ' ';
            my $size = $git->file_size(":0:$name");
            if ($size > $LIMIT) {
                $git->fault("File '$name' has $size bytes, more than our limit of $LIMIT",
                            {prefix => 'CheckSize'});
                ++$errors;
            }
        }

        return $errors == 0;
    };

    # 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->filter_files_in_index('AM');

        foreach ($git->run(qw/ls-files -s/, @changed)) {
            my ($mode, $sha, $n, $name) = split ' ';
            require Perl::Critic;
            state $critic = Perl::Critic->new(-severity => 'stern', -top => 10);
            my $contents = $git->run('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;
            $git->fault('Violations', {
                 prefix  => 'Perl::Critic',
                 details => Data::Dumper::Dumper(\%violations),
            });
            return 0;
        }

        return 1;
    };

Note that you may define several hooks for the same operation. In the above example, we've defined two PRE_COMMIT hooks. Both are going to be executed when Git invokes the generic script during the pre-commit phase.

You may implement different kinds of hooks in the same generic script. The function run_hook() will activate just the ones for the current Git phase.

Using Plugins

There are several hooks already implemented as plugin modules, which you can use. Some are described succinctly below. Please, see their own documentation for more details.

  • Git::Hooks::CheckDiff

    Check if the differences introduced by new commits comply with specified policies.

  • Git::Hooks::CheckFile

    Check if the names and contents of added, modified, or deleted files comply with specified policies.

  • Git::Hooks::CheckJira

    Integrate Git with the Jira ticketing system by requiring that every commit message cites valid Jira issues.

  • Git::Hooks::CheckCommit

    Check various aspects of commits like author and committer names and emails, and signatures.

  • Git::Hooks::CheckLog

    Check commit log messages formatting.

  • Git::Hooks::CheckRewrite

    Check if a git rebase or a git commit --amend is safe, meaning that no rewritten commit is contained by any other branch besides the current one. This is useful, for instance, to prevent rebasing commits already pushed.

  • Git::Hooks::CheckReference

    Restrict who can do what (create, rewrite, update, or delete) to which references (branches and tags are just the most common Git references).

  • Git::Hooks::GerritChangeId

    Inserts a Change-Id line in the commit log message to allow integration with Gerrit's code review system.

  • Git::Hooks::Notify

    Sends email notifications to interested parties about pushed commits affecting specific files in the repository.

  • Git::Hooks::PrepareLog

    Prepare commit log messages before they are opened by the editor. It can be used to pre-format or to insert automatic information in the message before the user is given a chance to edit it.

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

These plugins are configured by Git's own configuration framework, using the git config command or by directly editing Git's configuration files. (See git help config to know more about Git's configuration infrastructure.)

To enable a plugin you must add it to the githooks.plugin configuration option.

The CONFIGURATION section below explains this in more detail.

Implementing Plugins

Plugins are simply Perl modules inside the Git::Hooks name space. Choose a descriptive name for it so that it can be installed by means of the githooks.plugin configuration option. The only requirement of a plugin is that it record one of more functions as hooks using the HOOK DIRECTIVES or the HOOK DRIVERS described below.

As an example of a bare-bones plugin we could transform the pre-commit hook checking for file sizes that we implemented above into a proper plugin simply by putting it inside a package and using the Git::Hooks module to import the PRE_COMMIT directive, like this:

    package Git::Hooks::CheckFileSize;
    # ABSTRACT: Git::Hooks plugin for checking file sizes

    use Git::Hooks;

    # 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->filter_files_in_index('AM');

        my $errors = 0;

        foreach ($git->run(qw/ls-files -s/, @changed)) {
            my ($mode, $sha, $n, $name) = split ' ';
            my $size = $git->file_size(":0:$name");
            if ($size > $LIMIT) {
                $git->fault("File '$name' has $size bytes, more than our limit of $LIMIT",
                            {prefix => 'CheckSize'});
                ++$errors;
            }
        }

        return $errors == 0;
    };

After having it installed where Perl can find it you can enable it by putting this into your global or local Git config file:

  [githooks]
        plugin = CheckFileSize

By using some of the Git::Repository::Plugin::GitHooks methods we can make this check work for other hooks as well:

    package Git::Hooks::CheckFileSize;
    # ABSTRACT: Git::Hooks plugin for checking file sizes

    use Git::Hooks;

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

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

    sub check_new_files {
        my ($git, $commit, @files) = @_;

        my $errors = 0;

        foreach ($git->run(qw/ls-files -s/, @files)) {
            my ($mode, $sha, $n, $name) = split ' ';
            my $size = $git->file_size(":0:$name");
            if ($size > $LIMIT) {
                $git->fault("File '$name' has $size bytes, more than our limit of $LIMIT",
                            {prefix => 'CheckSize', commit => $commit});
                ++$errors;
            }
        }

        return $errors == 0;
    }

    sub check_commit {
        my ($git) = @_;

        return check_new_files($git, ':0', $git->filter_files_in_index('AM'));
    }

    # This routine can act both as an update or a pre-receive hook.
    sub check_affected_refs {
        my ($git) = @_;

        return 1 if $git->im_admin();

        my $errors = 0;

        foreach my $ref ($git->get_affected_refs()) {
            my ($old_commit, $new_commit) = $git->get_affected_ref_range($ref);
            check_new_files($git, $new_commit, $git->filter_files_in_range('AM', $old_commit, $new_commit))
                or ++$errors;
        }

        return $errors == 0;
    }

    # Install hooks
    PRE_COMMIT       \&check_commit;
    UPDATE           \&check_affected_refs;
    PRE_RECEIVE      \&check_affected_refs;

Now it can check file sizes on the Git server, when the user pushes commits to it.

With a few changes we could make this plugin more general and consistent with the standard plugins. We just need to use the Hook Drivers like this:

    package Git::Hooks::CheckFileSize;
    # ABSTRACT: Git::Hooks plugin for checking file sizes

    use Git::Hooks;

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

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

    sub check_new_files {
        my ($git, $commit, @files) = @_;

        my $errors = 0;

        foreach ($git->run(qw/ls-files -s/, @files)) {
            my ($mode, $sha, $n, $name) = split ' ';
            my $size = $git->file_size(":0:$name");
            if ($size > $LIMIT) {
                $git->fault("File '$name' has $size bytes, more than our limit of $LIMIT",
                            {prefix => 'CheckSize', commit => $commit});
                ++$errors;
            }
        }

        return $errors;
    }

    sub check_commit {
        my ($git) = @_;

        return check_new_files($git, ':0', $git->filter_files_in_index('AM'));
    }

    sub check_ref {
        my ($git, $ref) = @_;

        my ($old_commit, $new_commit) = $git->get_affected_ref_range($ref);

        return check_new_files(
            $git,
            $new_commit,
            $git->filter_files_in_range('AM', $old_commit, $new_commit),
        );
    }

    # Install hooks
    GITHOOKS_CHECK_PRE_COMMIT        \&check_commit;
    GITHOOKS_CHECK_AFFECTED_REFS \&check_ref;

Plugins usually can be configured in their own configuration section. For instance, we could allow the user to configure the size limit by putting this on her configuration file:

    [githooks "checkfilesize"]
        limit = 10485760

We just have to change the check_new_files function:

    sub check_new_files {
        my ($git, $commit, @files) = @_;

        my $limit = $git->get_config_integer('githooks.checkfilesize', 'limit');

        return 1 unless defined $limit;   # By default there is no limit

        my $errors = 0;

        foreach ($git->run(qw/ls-files -s/, @files)) {
            my ($mode, $sha, $n, $name) = split ' ';
            my $size = $git->file_size(":0:$name");
            if ($size > $limit) {
                $git->fault("File '$name' has $size bytes, more than our limit of $limit",
                            {prefix => 'CheckSize', commit => $commit});
                ++$errors;
            }
        }

        return $errors == 0;
    }

Please, look at the implementation of the native Git::Hooks plugins for more examples.

Invoking external hooks

Since the default Git hook scripts are taken by the symbolic links to the Git::Hooks generic script, you must install any other 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). 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, run_hook dies with an appropriate error message.

Gerrit Hooks

Gerrit is a web based code review and project management for Git based projects. It's based on JGit, which is a pure Java implementation of Git.

Gerrit doesn't support Git standard hooks. However, it implements its own special hooks. Git::Hooks currently supports only a few of Gerrit hooks:

Synchronous hooks

These hooks are invoked synchronously so it is recommended that they not block.

Their purpose is the same as Git's update hook, i.e. to block commits from being integrated, and Git::Hooks's plugins usually support them all together.

  • ref-update

    This is called when a ref update request (direct push, non-fast-forward update, or ref deletion) is received by Gerrit. It allows a request to be rejected before it is committed to the Gerrit repository. If the hook fails the update will be rejected.

  • commit-received

    This is called when a commit is received for review by Gerrit. It allows a push to be rejected before the review is created. If the hook fails the push will be rejected.

  • submit

    This is called when a user attempts to submit a change. It allows the submit to be rejected. If the hook fails the submit will be rejected.

Asynchronous hooks

These hooks are invoked asynchronously on a background thread.

  • patchset-created

    The patchset-created hook is executed asynchronously when a user performs a push to one of Gerrit's virtual branches (refs/for/*) in order to record a new review request. This means that one cannot stop the request from happening just by dying inside the hook. Instead, what one needs to do is to use Gerrit's API to accept or reject the new patchset as a reviewer.

    Git::Hooks does this using a Gerrit::REST object. There are a few configuration options to set up this Gerrit interaction, which are described below.

    This hook's purpose is usually to verify the project's policy compliance. Plugins that implement pre-commit, commit-msg, update, or pre-receive hooks usually also implement this Gerrit hook.

    Since draft patchsets are visible only by their owners, the patchset-created hook is unusable because it uses a fixed user to authenticate. So, Git::Hooks exit prematurely when invoked as the patchset-created hook for a draft change.

  • draft-published

    The draft-published hook is executed when the user publishes a draft change, making it visible to other users. Since the patchset-created hook doesn't work for draft changes, the draft-published hook is a good time to work on them. All plugins that work on the patchset-created also work on the draft-published hook to cast a vote when drafts are published.

Logging

Git::Hooks logs using the Log::Any framework. You may tell where it should log using any available Log::Any::Adapter module.

For example, to log everything to a file you just have to add a line to your hook script, like this:

        #!/usr/bin/env perl
        use Log::Any::Adapter (File => '/var/log/githooks.log');
        use Git::Hooks;
        run_hook($0, @ARGV);

This will produce copious logs. If you are interested only in the informational messages, select the log_level info, like so:

        use Log::Any::Adapter (File => '/var/log/githooks.log', log_level => 'info');

Read the Log::Any documentation to know what other options you have.

Note that several log messages contain context data, which is a feature that was implemented on version 1.050 of Log::Any, released on 2017-08-04. If you're using an older version the context data will appear as a ref-scalar and won't make much sense.

MAIN FUNCTION

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' which are Git::Repository objects that can be used to grok detailed information about the repository and the current transaction. (Please, refer to Git::Repository specific 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 semicolon.

Some hooks are invoked before an action (e.g., pre-commit) so that one can check some condition. If the condition holds, they must simply end without returning anything. Otherwise, they should invoke the error method on the GIT object passing a suitable error message. On some hooks, this will prevent Git from finishing its operation.

Other hooks are invoked after the action (e.g., post-commit) so that its outcome cannot affect the action. Those are usually used to send notifications or to signal the completion of the action someway.

You may learn about every Git hook by invoking the command git help hooks. Gerrit hooks are documented in the project site.

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, upstream [, branch])

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

POST_MERGE(GIT, is-squash-merge)

PRE_PUSH(GIT, remote-name, remote-url)

The pre-push hook was introduced in Git 1.8.2. The default hook gets two arguments: the name and the URL of the remote which is being pushed to. It also gets a variable number of arguments via STDIN with lines of the form:

    <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF

The information from these lines is read and can be fetched by the hooks using the Git::Hooks::get_input_data method.

PRE_RECEIVE(GIT)

The pre-receive hook gets a variable number of arguments via STDIN with lines of the form:

    <old-value> SP <new-value> SP <ref-name> LF

The information from these lines is read and can be fetched by the hooks using the Git::Hooks::get_input_data method or, perhaps more easily, by using the Git::Repository::Plugin::GitHooks::get_affected_refs and the Git::Repository::Plugin::GitHooks::get_affected_ref_range methods.

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

POST_RECEIVE(GIT)

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

PUSH_TO_CHECKOUT(GIT, SHA1)

The push-to-checkout hook was introduced in Git 2.4.

PRE_AUTO_GC(GIT)

POST_REWRITE(GIT, command)

The post-rewrite hook gets a variable number of arguments via STDIN with lines of the form:

    <old sha1> SP <new sha1> SP <extra info> LF

The extra info and the preceding SP are optional.

The information from these lines is read and can be fetched by the hooks using the Git::Hooks::get_input_data method.

REF_UPDATE(GIT, OPTS)

COMMIT_RECEIVED(GIT, OPTS)

SUBMIT(GIT, OPTS)

PATCHSET_CREATED(GIT, OPTS)

DRAFT_PUBLISHED(GIT, OPTS)

These are Gerrit-specific hooks. Gerrit invokes them passing a list of option/value pairs which are converted into a hash, which is passed by reference as the OPTS argument. In addition to the option/value pairs, a Gerrit::REST object is created and inserted in the OPTS hash with the key 'gerrit'. This object can be used to interact with the Gerrit server. For more information, please, read the "Gerrit Hooks" section.

HOOK DRIVERS

As shown in the example above in the "Implementing Plugins" section, the methods in Git::Repository::Plugin::GitHooks make it easy to implement checks that can be associated with several hooks at once. For example, the standard plugins Git::Hooks::CheckCommit, Git::Hooks::CheckDiff, Git::Hooks::CheckFile, Git::Hooks::CheckJira, Git::Hooks::CheckLog, Git::Hooks::CheckReference, and Git::Hooks::CheckWhitespace all implement their checks for the following hooks: commit-received, pre-receive, ref-update, submit, and update. In order to make it easier for similar plugins to associate themselves to the same hooks, and to make their behaviour more consistent, Git::Hooks implements some HOOK DRIVERS, which are kind of "meta-directives" used to register routines as several related hooks at once.

Each driver gets a routine-ref or a single block (anonymous routine) as a required first argument. Optionally, they can get a hash-ref as its second argument.

The first argument must be a routine to check the changes being performed by a Git command. Each driver associates this check routine with a set of hooks, invoking them passing a Git::Repository object and a few other arguments. The routines passed to the drivers must all return zero if all checks pass or a positive number if any check fails.

The second argument may contain one or more of the following extra options:

  • config

    This must map to another routine-ref which will be invoked once before the routine passed as the first argument is invoked in order to setup the plugin configuration options. It will be passed the Git::Repository as the sole argument.

  • destroy

    This must map to another routine-ref which will be invoked once after the routine passed as the first argument is invoked in order to tear down any resources acquired by the plugin. It will be passed the Git::Repository as the sole argument.

All drivers check the githooks.admin configuration option and do not do anything if the user performing the action is an admin.

GITHOOKS_CHECK_AFFECTED_REFS(SUB, [, OPTIONS])

This driver associates the routine SUB to the following hooks: commit-received, pre-receive, ref-update, submit, and update.

They will invoke SUB once for each affected reference, as long as it is enabled as specified by the githooks.ref and the githooks.noref options.

The SUB routine will receive two arguments: a Git::Repository object and the reference name.

GITHOOKS_CHECK_PRE_COMMIT(SUB [, OPTIONS])

This driver associates the routine SUB to the following hooks: pre-applypatch and pre-commit.

They will invoke SUB once for the current branch, as long as it is enabled as specified by the githooks.ref and the githooks.noref options.

The SUB routine will receive two arguments: a Git::Repository object and the current branch name.

GITHOOKS_CHECK_PATCHSET(SUB [, OPTIONS])

This driver associates the routine SUB to the following hooks: draft-published and patchset-created.

They will invoke SUB once for the current branch, as long as it is enabled as specified by the githooks.ref and the githooks.noref options.

The SUB routine will receive two arguments: a Git::Repository object and a hash containing the options the hook obtained from Gerrit.

GITHOOKS_CHECK_MESSAGE_FILE(SUB [, OPTIONS])

This driver associates the routine SUB to the following hooks: applypatch-msg and commit-msg.

They will invoke SUB once for the current branch, as long as it is enabled as specified by the githooks.ref and the githooks.noref options.

The SUB routine will receive three arguments: a Git::Repository object, the commit message, and the current branch name.

CONFIGURATION

Git::Hooks is configured via Git's own configuration infrastructure. There are a few global options which are described below. Each plugin may define other specific options which are described in their own documentation. The options specific to a plugin usually are contained in a configuration subsection of section githooks, named after the plugin base name. For example, the Git::Hooks::CheckFile plugin has its options contained in the configuration subsection githooks.checkfile. Note that the subsection name must be all in lowercase.

You should get comfortable with git config command and the config file syntax (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.

Gerrit keeps its repositories in a hierarchy and its specific configuration mechanism takes advantage of that to allow a configuration definition in a parent repository to trickle down to its children repositories. Git::Hooks uses Git's native configuration mechanisms and doesn't support Gerrit's mechanism, which is based on configuration files kept in a detached refs/meta/config branch. But you can implement a hierarchy of configuration files by using Git's inclusion mechanism. Please, read the "Includes" section of git help config to know how.

The sections below describe the options of the githooks configuration section.

plugin PLUGIN...

To enable or disable one or more plugins you must add them to this configuration option, like this:

    [githooks]
      plugin CheckFile CheckJira !CheckLog

You can add another list to the same variable to enable or disable more plugins, like this:

    [githooks]
      plugin CheckFile CheckJira
      plugin !CheckLog

This is useful, for example, to enable or disable some plugins globally and others locally, per repository.

A plugin may hook itself to one or more hooks. CheckJira, for example, hooks itself to three: commit-msg, pre-receive, and update. It's important that the corresponding symbolic links be created pointing from the hook names to the generic script so that the hooks are effectively invoked.

In the previous examples, the plugins were referred to by their short names. In this case they are looked for in three places, in this order:

  1. 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).

  2. 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.

  3. In the Git::Hooks installation.

The first match is taken as the desired plugin, which, if enabled, is executed (via do) 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 the author, please.)

However, if you use the fully qualified module name of the plugin in the configuration, then it will be simply required as a normal module. For example:

    [githooks]
      plugin = My::Hook::CheckSomething

If a plugin name is prefixed by an exclamation point (!) it is disabled. This is useful if you want to enable a plugin globally and only disable it for some repositories. You can even re-enable it later, if you want, by mentioning it again without the prefix.

[DEPRECATED after v3.3.0] disable PLUGIN...

This option is deprecated. Please, use the githooks.plugin option with an exclamation point before the plugin name to disable it. This option has higher priority than the githooks.plugin option and don't allow for re-enabling a disabled plugin later.

This option disables plugins enabled by the githooks.plugin option. It's useful if you want to enable a plugin globally and only disable it for some repositories. For example:

    # In ~/.gitconfig:
    [githooks]
      plugin = CheckJira

    # In .git/config:
    [githooks]
      disable = CheckJira

You may also temporarily disable a plugin by assigning to "0" an environment variable with its name. This is useful sometimes, when you are denied some perfectly fine commit by one of the check plugins. For example, suppose you got an error from the CheckLog plugin because you used an uncommon word that is not in the system's dictionary yet. If you don't intend to use the word again you can bypass all CheckLog checks this way:

    $ CheckLog=0 git commit

This works for every hook. For plugins specified by fully qualified module names, the environment variable name has to match the last part of it. For example, to disable the My::Hook::CheckSomething plugin you must define an environment variable called CheckSomething.

Note, however, that this works for local hooks only. Remote hooks (like update or pre-receive) are run on the server. You can set up the server so that it defines the appropriate variable, but this isn't so useful as for the local hooks, as it's intended for once-in-a-while events.

plugins DIR

This option specify a list of directories where plugins are looked for besides the default locations, as explained in the githooks.plugin option above.

externals BOOL

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

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.

groups GROUPSPEC

You can define user groups in order to make it easier to configure access control plugins. A group is specified by a GROUPSPEC, which is a multi-line string containing a sequence of group definitions, one per line. Each line defines a group like this, where spaces are significant only between users and group references:

    [githooks]
      groups = \
        groupA = userX \
        groupB = userA userB @groupA userC

Note that a group can reference other groups by name. To make a group reference, simply prefix its name with an at sign (@). Group references must reference groups previously defined.

A GROUPSPEC may be in the format file:PATH/TO/FILE, which means that the external text file PATH/TO/FILE contains the group definitions. The path may be absolute or relative to the hooks current directory, which is usually the repository's root in the server. It's syntax is very simple. Blank lines are skipped. The hash (#) character starts a comment that goes to the end of the current line. The remaining lines must define groups in the same format exemplified above.

The may be multiple definitions of this variable, each one defining different groups. You can't redefine a group.

userenv STRING

When Git is performing its chores in the server to serve a push request it's usually invoked via the SSH or a web service, which take care of the authentication procedure. These services normally make the authenticated user name available in an environment variable. You may tell this hook which environment variable it is by setting this option to the variable's name. If not set, the hook will try to get the user's name from the GERRIT_USER_EMAIL or the USER environment variable, in this order, and let it undefined if it can't figure it out.

If the user name is not directly available in an environment variable you may set this option to a code snippet by prefixing it with eval:. The code will be evaluated and its value will be used as the user name.

For example, if the Gerrit user email is not what you want to use as the user id, you can set the githooks.userenv configuration option to grok the user id from one of these environment variables. If the user id is always identical to the part of the email before the at sign, you can configure it like this:

    git config githooks.userenv \
      'eval:(exists $ENV{GERRIT_USER_EMAIL} && $ENV{GERRIT_USER_EMAIL} =~ /([^@]+)/) ? $1 : undef'

Git::Hooks defines the environment variable GITHOOKS_AUTHENTICATED_USER to the authenticated user, making it available for hooks and plugins.

The Gerrit hooks unfortunately do not have access to the user's id. But they get the user's full name and email instead. Git:Hooks takes care so that two environment variables are defined in the hooks, as follows:

  • GERRIT_USER_NAME

    This contains the user's full name, such as "User Name".

  • GERRIT_USER_EMAIL

    This contains the user's email, such as "user@example.net".

This variable is useful for any hook that need to authenticate the user performing the git action.

admin USERSPEC

There are several hooks that perform access control checks before allowing a git action, such as the ones installed by the CheckFile and the CheckJira plugins. It's useful to allow some people (the "administrators") to bypass those checks. These hooks usually allow the users specified by this variable to do whatever they want to the repository. You may want to set it to a group of "super users" in your team so that they can "fix" things more easily.

The value of each option is interpreted in one of these ways:

  • username

    A username specifying a single user. The username specification must match "/^\w+$/i" and will be compared to the authenticated user's name case sensitively.

  • @groupname

    A groupname specifying a single group.

  • ^regex

    A regex which will be matched against the authenticated user's name case-insensitively. The caret is part of the regex, meaning that it's anchored at the start of the username.

ref REFSPEC

noref REFSPEC

These multi-valued options are meant to selectively enable/disable hook processing for commits in particular references (usually branches). Hook developers should use the is_reference_enabled method Git::Repository::Plugin method to check it.

Local hooks should pass the current branch to the method and server hooks should pass the names of the references affected by the push command.

The REFSPECs can be specified as complete ref names (e.g. "refs/heads/master") or by regular expressions starting with a caret (^), which is kept as part of the regexp (e.g. "^refs/heads/(master|fix)").

abort-commit BOOL

This option is true by default, meaning that the pre-commit and the commit-msg hooks will abort the commit if they detect anything wrong in it. This may not be the best way to handle errors, because you must remember to retrieve your carefully worded commit message from the .git/COMMIT_EDITMSG to try it again, and it is easy to forget about it and lose it.

Setting this to false makes these hooks simply warn the user via STDERR but let the commit succeed. This way, the user can correct any mistake with a simple git commit --amend and doesn't run the risk of losing the commit message.

gerrit.url URL

gerrit.username USERNAME

gerrit.password PASSWORD

These three options are required if you enable Gerrit hooks. They are used to construct the Gerrit::REST object that is used to interact with Gerrit.

gerrit.votes-to-approve VOTES

This option defines which votes should be cast in which labels to approve a review in the Gerrit change when all verification hooks pass.

VOTES is a comma-separated list of LABEL and VOTE mappings, such as:

  Code-Review+2,Verification+1

Which means that the Code-Review label should receive a +2 and the label Verification should receive a +1.

If not specified, the default VOTES is:

  Code-Review+1

gerrit.votes-to-reject VOTES

This option defines which votes should be cast in which labels to reject a review in the Gerrit change when some verification hooks fail.

VOTES has the same syntax as described for the githooks.gerrit.votes-to-approve option above.

If not specified, the default VOTES is:

  Code-Review-1

gerrit.comment-ok COMMENT

By default, when approving a review Git::Hooks simply casts a positive vote but does not add any comment to the change. If you set this option, it adds a comment like this in addition to casting the vote:

  [Git::Hooks] COMMENT

You may want to use a simple comment like 'OK'.

gerrit.auto-submit BOOL

If this option is enabled, Git::Hooks will try to automatically submit a change if all verification hooks pass.

Note that for the submission to succeed you must vote with githooks.gerrit.votes-to-approve so that the change has the necessary votes to be submitted. Moreover, the username and password you configured above must have the necessary rights to submit the change in Gerrit.

This may be useful to provide a gentle introduction to Gerrit for people who don't want to start doing code reviews but want to use Gerrit simply as a Git server.

gerrit.notify WHO

Notify handling that defines to whom email notifications should be sent after the review is stored.

Allowed values are NONE, OWNER, OWNER_REVIEWERS, and ALL.

If not set, the default is ALL.

error-prefix STRING

This option specifies a fixed string that will be inserted as a prefix to all the lines in the error messages produced by the Git::Repository::Plugin::GitHooks::fault method.

It's useful, for instance, to produce error messages for <Gitlab|https://docs.gitlab.com/ee/administration/server_hooks.html#custom-error-messages> as in:

  [githooks]
    error-prefix = "GL-HOOK-ERR: "

error-header CMD

This option specifies a command that should produce a multi-line string which will be used as a header prefixing the error messages, if there are any. The command is invoked using Perl's qx{CMD} operator, with no error detection. Since the string will most probably appear at the user's terminal their lines should have no more than 70 characters or so.

The following commands may give you an idea as to which commands to use:

  • fortune

  • FIGlet

  • cowsay

  • fortune -s | cowsay

  • GET 'http://api.icndb.com/jokes/random?limitTo=nerdy' | jq -r '.value.joke' | cowsay

This option is similar to the githooks.error-header above, but produces a footer to the error messages generated by Git::Hooks, if any.

error-length-limit INT

This option specifies a limit to the length of the error messages in characters. If the errors are bigger than the limit the message is truncated and a special mark is appended to the end like this:

  [MESSAGE TRUNCATED AT githooks.error-length-limit]

This is useful to avoid clogging the git server logs. Gerrit, for instance, has a configuration named change.commentSizeLimit which specified a limit to the size of change comments, and Git::Hook's error messages are usually inserted as comments. So, it's wise to set this option when using Gerrit.

help-on-error MESSAGE

This option allows you to specify a helpful message that will be shown if any hook fails. This may be useful, for instance, to provide information to users about how to get help from your site's Git gurus.

<PLUGIN>.help-on-error MESSAGE

You can also provide helpful messages specific to each enabled PLUGIN in its own subsection.

color [never|auto|always]

This option tells if Git::Hooks's output should be colorized. It accepts the same values as Git's own color.ui option. If it's not set, the color.ui value is used by default. The meaning of each value is the following:

timeout SECONDS

This option sets a limit in seconds to how long the hook script can run. It's used by the Git::Repository::Plugin::GitHooks::check_timeout method.

never (or false)

Do not use colors.

auto (or true)

Use colors only if the messages go to a terminal. (This is the default value of color.ui since Git 1.8.4.)

always

Do use colors.

color.<slot> COLOR

Use customized colors for the Git::Hooks output colorization. <slot> specifies which part of the output to use the specified color, as shown below.

The COLOR value must comply with Git's color config type, which is explained in the git(1) manpage, under the CONFIGURATION FILE/Values/color section.

The available slots are the following:

The text output for the githooks.error-header option. (Default value is "green".)

The text output for the githooks.error-footer option. (Default value is "green".)

context

The line containing the prefix and the context of error messages. (Default value is "red bold".)

message

The error message proper. (Default value is "yellow".)

details

The indented lines providing details for error messages. (Default value is empty.)

GIT AND PERL VERSION COMPATIBILITY POLICY

Currently Git::Hooks require Perl 5.16 and Git 1.8.3.

We try to be compatible with the Git and Perl native packages of the oldest Ubuntu LTS and CentOS Linux distributions still getting maintenance updates.

  +-------------+-----------------------+------+--------+
  | End of Life | Distro                | Perl |   Git  |
  +-------------+-----------------------+------+--------+
  |   2023-04   | Ubuntu 18.04 (bionic) | 5.26 | 2.15.1 |
  |   2024-07   | CentOS 7              | 5.16 |  1.8.3 |
  |   2025-04   | Ubuntu 20.04 (focal ) | 5.30 | 2.25.1 |
  |   2027-04   | Ubuntu 22.04 (jammy)  | 5.34 | 2.34.1 |
  |   2029-05   | CentOS 8              | 5.26 | 2.18.4 |
  +-------------+-----------------------+------+--------+

As you can see, we're kept behind mostly by the slow pace of CentOS (actually, RHEL) releases.

There are a few features of Git::Hooks which require newer Gits. If they're used with older Gits an appropriate error message tells the user to upgrade Git or to disable the feature.

SEE ALSO

REPOSITORY

https://github.com/gnustavo/Git-Hooks

AUTHOR

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

COPYRIGHT AND LICENSE

This software is copyright (c) 2023 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.