Exercises in Restful Integration and Continuous Delivery

Musing on Software Development and Technologies.

Tue 01 January 2019

Git Repo in Shared Hosting #3 - Git::Hooks for a Secure and Clean Repo

Posted by Mikko Koivunalho in How-To   

Git Logo

In this series of four articles we show how to share a Git repository via a Linux shell account and a shared hosting Apache HTTP server without root access.

In the first article we learned how we can share our Git repository with others securily and yet allow them to commit (push) to the repo.

In the second article we set up the web-based collaborative code review tool Review Board to work with our repository and make it accessible for users via our homepage.

In the third article we will now make our repository even more secure and usable with the help of a Git hooks framework, the Perl based Git::Hooks

In the fourth article we will use SSH connection and SSH public keys to give access and also limit access to our repositories.

Prerequisites

  • Shell account on a Linux server. All shell commands are executed in bash.
  • Git.
  • Perl with access to CPAN.
  • The need to work collaboratively! Need is the greatest innovator.
  • The repositories we set up in the first article.

Git Hooks

Git hooks are programs you can place in a special directory to trigger actions at certain points in git’s execution.

It is common, for example, to trigger a hook during commit operation to ensure the commit message follows a company standard. Another very common use case is to send an email after a push to central repository telling other users about the update.

Git hooks are normal executables or scripts the operating system can run. By default the hooks directory is $GIT_DIR/hooks, but that can be changed via the core.hooksPath configuration variable (see git-config[1]).

Git::Hooks - A Framework to Make it Easy to Use And Implement Git Hooks

Besides being a framework to build your own git hooks, Git::Hooks is also a collection of usable hooks with an easy and centralized way to configure them.

In this article we will not implement a new hook but only configure two existing hooks to give an example. We will use hooks both on client side and server side.

We will also do this in a way that saves the configuration in the same repository. This allows:

  1. Different configuration for different branches.
  2. Configuration is always accessible.
  3. User can use same configuration files client side.

Local Configuration

Preparations

N.B. All Unix shell commands in this tutorial are prefixed with '\' to ensure they are running pure, not a shell alias.

For local configuration create an environmental variables to use in the rest of this tutorial. Variable GIT_REPO contains the full path to the cloned repository shared-repo.git we created in the first article. If you haven't cloned it, do it now. GIT_REPO points to the working dir of the repo, not the .git directory!

export GIT_REPO="<REPO shared-repo CLONE>"

Install Git::Hooks in the system. If you don't have root access, as is assumed, you can use local::lib, plenv or perlbrew to get Git::Hooks to your Perl module path. I can recommend plenv. Managing the Perl module path is, however, beyond the scope of this tutorial!

With plenv I would create a virtual environment dedicated for the hooks.

\plenv install 5.22.2 --noman --as=5.22.2-githooks
\plenv shell 5.22.2-githooks
\plenv install-cpanm
cpanm Git::Hooks

Next create the general script to invoke hooks and put it somewhere reachable, e.g. ${HOME}/bin. If you are using plenv you can do as below:

\mkdir -p ${HOME}/bin
\cat > ${HOME}/bin/githooks.pl <<EOF
#!/usr/bin/env $(plenv which perl)
use Git::Hooks;
run_hook(\$0, @ARGV);

EOF

\chmod 755 ${HOME}/bin/githooks.pl

For more information about the general configuration, please refer to Tutorial for Git users.

Now create the links for all the hooks. We do it here manually only for this repository but Tutorial for Git users shows also how to make Git do it automatically whenever you perform git clone.

for hook in $(\ls ${GIT_REPO}/.git/hooks); do \ln --symbolic ${HOME}/bin/githooks.pl ${GIT_REPO}/.git/hooks/${hook%.*}; done

Configuration Within the Repository

Create a dot directory to contain our repo configuration.

\mkdir -p ${GIT_REPO}/.git-repo-admin/

Create a file containing Git::Hooks specific configuration. N.B. The backslash '\' is written four times because Git config itself needs it twice. Otherwise it would escape the following character instead of passing it forward.

\cat > ${GIT_REPO}/.git-repo-admin/config_hooks <<EOF
[githooks]
        plugin = CheckReference
        plugin = CheckLog
        plugin = PrepareLog
        plugin = CheckWhitespace
        userenv = REMOTE_USER
        groups = GroupAdmins = ${USER}
        #admin = @GroupAdmins
        help-on-error = "Git::Hooks failed. Please consult error messages."
[githooks "checkreference"]
        acl = deny CRUD ^refs/
        acl = allow CRUD ^refs/heads/{REMOTE_USER}/[A-Z]+-\\\\d+
        acl = allow CRUD ^refs/heads/[[A-Z]+-\\\\d+
[githooks "preparelog"]
        issue-branch-regex = "[A-Z]+-\\\\d+$"
        issue-place = "title [%I] %T"
[githooks "checklog"]
        title-required = true
        title-max-width = 50
        title-period = deny
        title-match = "^\\\\[[A-Z]+-\\\\d+\\\\]\\\\s{1}.*{1,}$"
        body-max-width = 72
        match = !\\\\t
        spelling = false
        spelling-lang = EN
        signed-off-by = false
        deny-merge-revert = false
        help-on-error = "Some general information on commit messages and possible policies for them: http://www.kernel.org/pub/software/scm/git/docs/git-commit.html"

EOF

In order to use this config file we need to link it to the normal git config file ${GIT_DIR}/config. We create a symbolic link which points to the file in the repository because we want both the local and the central repo configuration to be as equal as possible, for easier maintenance in the long run. On client side the symbolic link is an extra step.

cd ${GIT_REPO}/.git
ln --symbolic ../.git-repo-admin/config_hooks ./config_hooks
EOF
\git -C ${GIT_REPO} config --local --add include.path "./config_hooks"

Now test the local configuration: create a local feature branch named e.g. ISSUE-1, and try to commit (\git commit) the newly created .git-repo-admin/config_hooks. Git should complain about whitespace error (hook CheckWhitespace). Fix the error and commit again. Now hook PrepareLog has prepared the prefix of the commit message. Try making the commit message title longer than 50 characters and commit will fail because of hook CheckLog.

N.B. (1) Hook CheckWhitespace has no additional configuration; we merely activate it.

N.B. (2) Config item githooks.userenv is very important because it tells to Git::Hooks how to get the current user name. Items githooks.groups and githooks.admin define the administrators of the repo. The admins can commit and push anything without the other hooks doing their job of validating. This can be especially useful if the admin needs to, for example, create new branches, reorganize the directory structure or force a push, i.e. rewrite branch history. For the sake of the tutorial, item githooks.admin is commented out.

N.B. (3) The hook CheckReference does not run on the client side but only in the central repo in connection to git push. Hook PrepareLog on the other hand only runs on client side.

Central Repository Configuration

Now we will install and configure Git::Hooks on the server side. Server side hooks are more important than client side. If a bad commit passes into the repository it harms the work of all users. Client side bad commits are only a single developer's headache.

Let's start from installing the Perl interpreter if required. Use the same instructions as with client installation. However, be careful with the full path to Perl executable and to repository. The path must be accessible from Apache HTTP server! Please read the first article Git Repo in Shared Hosting #1 - Secure HTTP Access to a Repo if you need help with this.

export REPOS_PATH="<FULL PATH HERE>"
export GIT_REPO="${REPOS_PATH}/shared/shared-repo.git"

Here we cannot use plenv to install the Perl executable because plenv only installs centrally and under its own installation. Use Perl::Build instead.

perl-build 5.20.2 --noman ${REPOS_PATH}/perl-5.20.2
${REPOS_PATH}/perl-5.20.2/bin/cpan Git::Hooks

When creating the general script to invoke hooks, if the home directory is not accessible, you can place it in the repos directory.

\mkdir -p ${REPOS_PATH}/bin
\cat > ${REPOS_PATH}/bin/githooks.pl <<EOF
#!/usr/bin/env ${REPOS_PATH}/perl-5.20.2/bin/perl
use Git::Hooks;
run_hook(\$0, @ARGV);

EOF

\chmod 755 ${REPOS_PATH}/bin/githooks.pl

for hook in pre-receive update post-receive; do \ln --symbolic ${REPOS_PATH}/bin/githooks.pl ${GIT_REPO}/hooks/${hook}; done

Configuration for Hooks and Automatic Updates

We already have the hooks configuration in :.git-repo-admin directory. We want to use that same file and our system should update the central repo hooks configuration when a new version is pushed on master branch and only on master.

How do we do this:

  1. After every push we read the file :.git-repo-admin/config_hooks from the master branch and we write it to (${GIT_DIR}/config_hooks). Reading is done in post-receive hook.
  2. We refer to that file in the Git config file ${GIT_DIR}/config.

Let's start by creating the link.

\touch ${GIT_REPO}/config_hooks
\git --git-dir ${GIT_REPO} config --local --add include.path "./config_hooks"

Git::Hooks allows us to run external hook programs if we place them to directory ${GIT_REPO}/hooks.d/. The hook post-receive is invoked by git-receive-pack when it reacts to git push and updates reference(s) in its repository. It executes on the remote repository once after all the refs have been updated. Therefore, at that point it is okay to read a file.

\mkdir -p ${GIT_REPO}/hooks.d/post-receive
\cat > ${GIT_REPO}/hooks.d/post-receive/update-config_hooks << EOF
#!/usr/bin/env bash
echo "Copying file '.git-repo-admin/config_hooks' from *master* branch to '\${GIT_DIR}/config_hooks'."
git --git-dir \${GIT_DIR} cat-file -p \$(git ls-tree master .git-repo-admin/config_hooks | cut -d " " -f 3 | cut -f 1) >\${GIT_DIR}/config_hooks

EOF

\chmod 755 ${GIT_REPO}/hooks.d/post-receive/update-config_hooks

That's it! You are ready to make a push to the repository. Push in the master branch! Log in with a different user, try to push in a new branch with a disallowed name.

There are many useful hooks that are easy to take into use in Git::Hooks package. Besides the main package there are third party hooks available, such as Git::Hooks::RubyNoDebugger or my Git::MoreHooks package.


    
 
 

Comments