NAME
Git::Hooks Tutorial - Gentle introduction to Git::Hooks
VERSION
version 3.6.0
NAME
Tutorial - Gentle introduction to Git::Hooks users and Git administrators
INTRODUCTION
This document is intended to make it easy to start using the Git::Hooks framework as fast as possible, with a minimum of set up. There are major sections for Git users, administrators, and hook developers. After setting it up with these instructions you're ready to go.
TUTORIAL FOR GIT USERS
As a Git user you may be interested in enabling some hooks for your local Git repositories. In particular, you may be interested in guaranteeing that the same policies that are being enforced by the remote repositories you push to are enforced earlier when you commit locally, so that you can avoid an onerous round trip to the common repository.
User Driver Script
Git::Hooks only need a single script to drive all hooks implemented by yourself or by the plugins you enable. If you do not need to create your own hooks, but want to use just the ones that come with Git::Hooks plugins, you can use a shared script like this for all your local repositories:
#!/usr/bin/env perl
use Git::Hooks;
run_hook($0, @ARGV);
As a user, I save this script as $HOME/bin/githooks.pl and make it executable.
If you invoke the driver script directly from the inside of a Git repository it should do nothing but exit normally:
$ cd /my/git/repo
$ $HOME/bin/githooks.pl
$ echo $?
0
If you invoke it from the outside though, it should die:
$ cd ..
$ $HOME/bin/githooks.pl
fatal: Not a git repository: . at /usr/share/perl5/Git.pm line 210.
User Hook Links
Now you must create symbolic links under the .git/hooks directory of your repositories pointing to the common script. So, for example, if you want to enable some pre-commit
and some commit-msg
hooks, you would do this:
$ cd /my/git/repo/.git/hooks
$ ln -s $HOME/bin/githooks.pl prepare-commit-msg
$ ln -s $HOME/bin/githooks.pl commit-msg
$ ln -s $HOME/bin/githooks.pl pre-commit
$ ln -s $HOME/bin/githooks.pl pre-rebase
Automating the creation of links
However, doing it manually for every repository is cumbersome and prone to mistakes and neglect. Fortunately, there is a better way. In order to make it easy to setup your hooks, it's useful to create a repository template for Git to use when you perform a git init
or a git clone
.
In Ubuntu Linux, Git's standard repository template resides in /usr/share/git-core/templates. If you can't find it there, read the TEMPLATE DIRECTORY
section of the git help init
manual to see where is your Git's default template directory.
You may customize one for you like this:
$ cp -a /usr/share/git-core/templates $HOME/.git-templates
$ cd $HOME/.git-templates/hooks
$ rm *
$ for i in prepare-commit-msg commit-msg post-commit pre-commit pre-rebase
> do ln -s $HOME/bin/githooks.pl $i
> done
These commands copy the default template directory to $HOME/.git-template (you may choose another directory), removes all sample hooks and creates symbolic links to the Git::Hooks driver script which we created above for four hooks: commit-msg
, post-commit
, pre-commit
, and pre-rebase
. These are all the hooks I'm interested in locally. If you're setting this up for a Git server you'll want to create links for other hooks, such as pre-receive
or update
.
You must tell Git to use your repository template instead of its default. The best way to do it is to configure it globally like this:
$ git config --global init.templatedir $HOME/.git-templates
Now, whenever you git init
or git clone
a new repository, it will automatically be configured to use Git::Hooks.
User Configuration
By default Git::Hooks does nothing. At the very least, it must be configured to enable some plugins and configure them to your taste. You should read the plugins's documentation to understand them and decide which ones you would like to enable globally and which ones you would like to enable locally for particular repositories.
Here I show my personal preferences. You are encouraged to make your own variations.
This is what I have in my global Git configuration ($HOME/.gitconfig):
[githooks]
plugin = CheckLog
plugin = CheckRewrite
abort-commit = 0
[githooks "checklog"]
title-max-width = 62
[githooks "checkjira"]
jiraurl = https://jira.cpqd.com.br
jirauser = gustavo
jirapass = a-very-large-and-difficult-to-crack-password
matchlog = (?s)^\\[([^]]+)\\]
The only plugins I want enabled for every repository are CheckLog
and CheckRewrite
. The latter is simple, as it doesn't require any configuration whatsoever. With it I feel more confident to perform git commit --amend
and git rebase
commands knowing that I'm going to be notified in case I'm doing anything dangerous.
The CheckLog
is also useful to guarantee that I'm not deviating from the common Git policies regarding the commit messages. The only thing I change from the defaults is the title-max-width
, because I think 50 characters is very constraining.
I disable the githooks.abort-commit
option so that pre-commit
and commit-msg
hooks don't abort the commit in case of errors. That's because I find it easier to amend the commit than to remember to recover my carefully crafted commit message from the .git/COMMIT_EDITMSG file afterwards.
The section githooks "checkjira"
contains some global configuration for the CheckJira
plugin, which I enable only for some repositories. Since the CheckJira
plugin has to connect to our Jira server, it needs the server URL and some credentials to authenticate. The matchlog
regex makes Jira issue keys be looked for only inside a pair of brackets at the beginning of the commit messages's title line.
I enable other plugins for specific repositories, since they depend on the context in which they are developed.
At CPQD we use Jira and Gerrit. So, for my work-related repositories I have this in their .git/config:
[githooks]
plugin = CheckJira
plugin = GerritChangeId
[githooks "checkjira"]
jql = project = CDS
GerritChangeId
doesn't require any configuration. It simply inserts a Change-Id
line in the messages of all commits. These are required by Gerrit.
I use CheckJira
to remind me to cite a Jira issue in every commit message. The jql
filter makes it accept only issues of the CDS Jira project for this particular repository.
Disabling plugins locally
The Git configuration follows a hierarchy, reading first the system configuration (/etc/gitconfig), then the global configuration ($HOME/.gitconfig), and then the local configuration ($GIT_DIR/config). If you have some plugins enabled globally you may disable then locally by putting the following in the .git/config of a particular repository:
[githooks]
disable = CheckJira
Disabling plugins temporarily
If you prefer the default behavior of having your pre-commit
and commit-msg
abort on errors, it's sometimes useful to disable a plugin temporarily in order to do a commit that otherwise would be rejected. For instance, if you enable CheckLog
's spelling checks and it rejects a commit because you used a cute-but-not-quite-right word in its message you can disable it for the duration of the commit by defining the environment variable CheckLog
as 0
like this:
CheckLog=0 git commit
You can disable any plugin in the same manner. Just define as zero (0) an environment variable homonymous to the plugin (you can use the plugin module full name or just its last component, as in the example above) for the duration of the commit and the plugin will be disabled.
TUTORIAL FOR GIT ADMINISTRATORS
As the administrator of a Git server you may be interested in enabling some hooks for your Git repositories to enforce project policies through source code verification or access rights.
Server Driver Script
For server hooks, it's advisable to enable logging. So, we increment the driver script described above for user repositories with a line enabling log to a file:
#!/usr/bin/env perl
use Log::Any::Adapter (File => '/var/log/githooks.log', log_level => 'info');
use Git::Hooks;
run_hook($0, @ARGV);
As a Git administrator, I save it as /usr/local/bin/githooks.pl in my Git server. You may save it elsewhere in the machine your hooks will run. Just do not forget to make it executable!
Server Hook Links
As a Git administrator, you would be interested in the back-end hooks. So, you should create some symbolic links under the .git/hooks directories of your repositories pointing to the drive script:
$ cd .../.git/hooks
$ ln -s /usr/local/bin/githooks.pl pre-receive
$ ln -s /usr/local/bin/githooks.pl update
$ ln -s /usr/local/bin/githooks.pl ref-update
Also, read the section about "Automating the creation of links" to know to have such links automatically created for you when you initialize or clone a repository.
Server Configuration
In your Git server you should insert global configuration in the $HOME/.gitconfig file at the HOME of the user running Git. This is an example using some of the available plugins:
[githooks]
plugin = CheckCommit
plugin = CheckJira
plugin = CheckLog
admin = gustavo
[githooks "checkcommit"]
email-valid = 1
[githooks "checkjira"]
jiraurl = https://jira.cpqd.com.br
jirauser = gustavo
jirapass = a-very-large-and-difficult-to-crack-password
matchlog = (?s)^\\[([^]]+)\\]
[githooks "checklog"]
title-max-width = 62
In the server the CheckCommit
, CheckJira
, and CheckLog
plugins are enabled for every repository. The <githooks.checkjira> section specifies the URL and credentials of the Jira server as well as where in the commit message the Jira references are to be looked for.
The githooks.checkcommit
enables the email-valid
check to guarantee that authors and committers use sane email addresses in their commits.
The githooks.checklog
section specifies a nonstandard value for the title-max-width
option.
As the administrator, I've configured myself (githooks.admin = gustavo
) to be exempt from any checks so that I can brag about my superpowers to my fellow users. Seriously, though, sometimes it's necessary to be able to bypass some checks and this is a way to allow some user to do it.
In particular repositories you can make local configurations to complement or supersede the global configuration. This is an example .git/config file:
[githooks]
disable = CheckJira
plugin = CheckReference
groups = integrators = tiago juliana
[githooks "checkreference"]
acl = deny CRUD ^refs/
acl = allow U ^refs/heads/
acl = allow CRUD ^refs/heads/user/{USER}/
acl = allow CRUD ^refs/ by @integrators
In this repository the CheckJira
plugin is disabled, even though it is enabled globally.
The CheckReference
plugin is enabled and configured in the githooks.checkreference
section with three ACLs.
The githooks.checkreference.acl options are used to restrict who can do what to which reference in the remote repository during a git-push. The rules are processed in reverse order and are (almost) self explanatory. The four acls above mean that:
The users in the
integrators
group can (C) create, (R) rewrite, (U) update, and (D) delete any branch or tag.Any user can create, rewrite, update, and delete any branch prefixed with user/{USER}, where
{USER}
is replaced by the username she used to authenticate during the git-push.Any user can update any branch.
No other change is allowed. (The first acl is important because any action not matching any acl is allowed by default.)
Distributed configuration
By default you only get a single global and one local configuration file for each repository in the server. Sometimes it's useful to factor out some configuration in specific files. If you have, say, three development teams holding their repositories in a single server but each one of them wants different CheckReference
configuration you may separate these configurations in three files and include one of them in each repository using Git's include
section. For example, team A's repositories could have this in their .git/config files:
[include]
path = /usr/local/etc/githooks/teamA.acls
Using include files you can manage complex configurations more easily.
TUTORIAL FOR GERRIT ADMINISTRATORS
Gerrit is a Git server but since it uses JGit instead of standard Git, it doesn't support the standard Git hooks. It supports its own hooks instead.
Git::Hooks supports just three of the many Gerrit hooks so far: ref-update
, patchset-created
, and draft-published
. The first one is much like the standard hooks pre-receive
and update
in that it can reject pushes when the commits being pushed don't comply. However, since Gerrit's revision process takes place before the commits are integrated, it's more useful to enable just the other two.
First, you have to create the same driver script as described for the server.
Then we must create the symlinks from the hook names to the driver script. However, in Gerrit there's a single hooks directory per server, instead of one per repository. Normally, when you install Gerrit, the hooks directory isn't created. It should be created below the Gerrit's site directory. Create it and the two symlinks like so:
$ cd .../gerrit-site
$ mkdir hooks
$ cd hooks
$ ln -s /usr/local/bin/githooks.pl patchset-created
$ ln -s /usr/local/bin/githooks.pl draft-published
The patchset-created
hook is invoked when you push a patchset to Gerrit for revision, but Git::Hooks only enable it for non-draft patchsets, because draft patchsets can only be reviewed by their owners and invited reviewers. The draft-published
hook is invoked when you publish a draft-patchset. Both hooks run asynchronously so that they can't reject the push. Instead, they review the patchset as a normal reviewer would, casting a positive or negative vote, depending on the result of the checks made by the enabled plugins.
All the (standard) Git::Hooks plugins that attach to the pre-receive
and update
hooks also attach themselves to both the patchset-created
and the draft-published
hooks, so that you can use the same configuration we did above.
You have to do a little extra configuration in the githooks.gerrit
section:
[githooks "gerrit"]
url = https://gerrit.cpqd.com.br
username = gerrit
password = a-very-large-and-difficult-to-crack-password
votes-to-approve = Verification+1
votes-to-reject = Verification-1
The three options url
, username
, and password
tell where to connect to Gerrit and with which user's credentials. This is the user that will appear to be making comments and reviewing the patchsets.
Then you have to tell Git::Hooks how it should vote to approve and to reject a change using the options votes-to-approve
and votes-to-reject
. In the example above you tell Git::Hooks to cast a +1 in the Verification
label to approve the change and to cast a -1 in the same label to reject it. You may cast multiple votes in multiple labels by separating the vote specifications with commas.
Gerrit has a notion of a hierarchy of repositories (called 'projects' in Gerrit). Gerrit's own configuration uses this hierarchy so that child repositories inherit their ancestor's configuration. Git's own configuration mechanism has no such notion, but you can fake it using the same include mechanism discussed above. But you have to do it manually, though.
TUTORIAL FOR BITBUCKET SERVER ADMINISTRATORS
Bitbucket Server is a proprietary Git server made by Atlassian. It doesn't support the standard Git hooks natively. Instead, it supports its own hooks which are implemented in Java as plugins.
The commercial plugin External Hooks allows you to use standard pre-receive
and post-receive
Git hooks with Bitbucket.
Follow the plugin installation and configuration instructions to enable it for particular repositories. The plugin must be configured with an executable, which can be our usual githooks.pl
script.
Bitbucket git repositories are kept in the server but are not easy to locate, because they're named after a numeric ID. In order to locate the repository directory in the server, go to its repository details page in the repository settings section in Bitbucket's web interface. The page shows the repository's location on disk where you find the config file which you must edit to configure Git::Hooks plugins.
TUTORIAL FOR GITLAB ADMINISTRATORS
GitLab is another very well known Git server, with proprietary and free software versions. It supports the standard Git hooks natively. Follow these instructions to know where you have to install hooks for the repositories.
TUTORIAL FOR USING GIT::HOOKS WITH DOCKER
You may want to use Git::Hooks in a Docker container to avoid the need to install it and any other package needed to implement specific checks in your hooks.
Besides being easier to install and upgrade it, using containers is more secure because the hooks don't have to have access to all of your environment. This is particularly important in a Git server context.
The following sections explain how to set up your hooks to use containers, assuming you're logged as the user who owns the Git repositories and that you already have Docker installed. If not, follow the instructions for your system.
Install and configure the githooks-docker.sh script
If you install Git::Hooks it comes with a script called githooks-docker.sh which makes it easy to use it in a Docker container. You don't have to install the module though, as the script can be downloaded directly from GitHub like this:
mkdir -p $HOME/bin
cd $HOME/bin
curl -s -O https://raw.githubusercontent.com/gnustavo/Git-Hooks/next/scripts/githooks-docker.sh
chmod +x githooks-docker.sh
Build your own git-hooks Docker image
The first step is to build a custom Docker image based on the official Perl image and installing Git::Hooks and a few other Perl modules in it.
If you can't invoke docker without sudo edit the githooks-docker.sh script and set its SUDO
configuration variable like this:
SUDO=sudo
Then, build the image with the following command:
$HOME/bin/githooks-docker.sh build
It should take a few minutes to build the image.
If you need to install other tools or modules you can edit the Dockerfile embedded in the script.
The build process re-creates in the image the same group and user with which the command was invoked. This allows it to access the repositories and the global git configuration which will be bind-mounted on the containers as volumes.
If you want to recreate your image to update Perl, Git::Hooks or to make any other change in the Dockerfile you simply have to edit the script and run the build
sub-command again.
Configure hooks to create new containers
The simplest way to use the new image is to create a new container for each hook invocation. First, create a script called $HOME/bin/githooks.sh with the following contents:
#!/bin/sh
REPO_ROOT=$(realpath ../..)
$HOME/bin/githooks-docker.sh run $REPO_ROOT "$0" "$@"
Make sure it's executable.
Now you must create symbolic links under the .git/hooks directory of your repositories pointing to the script. So, for example, if you want to enable some pre-receive
hook, you would do this:
$ cd /my/git/repo/.git/hooks
$ ln -s $HOME/bin/githooks.sh pre-receive
The script will invoke the githooks-docker.sh script using the run
sub-command. The first argument (REPO_ROOT) is important. It must be a directory containing all of your Git repositories. It doesn't have to be the direct parent of the repositories, but must be a prefix of the absolute path of all of repositories.
In the example above, the REPO_ROOT is defined dynamically by the command realpath ../..
, which resolves to the grandparent of the repository which invoked the hook. This is good for Git servers in which all repositories are siblings, such as Bitbucket and GitLab. Gerrit, on the other hand, keeps its repositories in a hierarchy and you should define the REPO_ROOT
variable with a fixed string.
This is all there is to it!
Configure hooks to reuse an existing container
Creating a new container isn't free. It takes about half a second on my machine to create a container that do nothing and finishes. In a heavy loaded server, hooks may be executed very frequently, and the overhead of creating and removing containers can have a significant impact.
You can optimize it by keeping a never-ending container in which you can execute the hooks using docker-exec
instead of docker-run
. The container functions as a "git-hooks service".
First, you need to start the git-hooks service with the following command:
$HOME/bin/githooks-docker.sh start $REPO_ROOT
The variable REPO_ROOT must be a directory containing all of your Git repositories.
If all goes well you should be able to see the container with the following command:
$HOME/bin/githooks-docker.sh status
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
319d57c5e34d git-hooks:3.3.1 "sleep infinity" 5 minutes ago Up 5 minutes git-hooks
Usually you'll keep the container running forever but if you need to restart it in order to, for example, update the image, you can stop it with the following command:
$HOME/bin/githooks-docker.sh stop
Now you just need to change the $HOME/bin/githooks.sh script to be like this:
#!/bin/sh
REPO_ROOT=$(realpath ../..)
$HOME/bin/githooks-docker.sh exec "$0" "$@"
That's it. Your hooks should be faster and leaner now.
TUTORIAL FOR HOOK DEVELOPERS
I'm sorry but there is no gentle introduction to this. ;-)
If you want to develop your own hooks or plugins, please read the detailed documentation for "Implementing Hooks" in Git::Hooks and "Implementing Plugins" in Git::Hooks. Then, go ahead and read the code of the plugins which come with the distribution. Most probably you can start by copying the overall structure of one of them as a starting point for your own plugin.
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.