Git hooks

This article contains a few collected notes about working with git hooks in Windows environments.

Client-side git hooks are locally-installed scripts that run at certain points in your git workflow. For example:

  • A pre-commit hook runs automatically when you run git commit, even before a commit message is required.
  • A post-merge runs automatically after a successful git merge.

Hook scripts are stored in the .git/hooks folder in the root of your repository – to use a hook script, you just need to make sure it's present in the folder in an appropriately named file name. If .git/hooks contains a file named pre-commit, git will automatically attempt to execute it as a script file when you run git commit.

A default repo installation includes sample git hook scripts, suffixed .sample – to use the sample pre-commit hook, you would just rename file pre-commit.sample to pre-commit (no extension).

Hook scripts, like other *nix shell scripts start with a shebang. This initial line tells the system which script interpreter to use to execute the file. In a *nix system this might be e.g.:

  • #! /bin/sh (Bourne shell)
  • #! /bin/bash (Bash shell)

but obviously /bin/sh and /bin/bash aren't valid Windows paths.

Instead,1) you need to include the path to the relevant script interpreter on your local Windows system, e.g.

#! C:/Program\ Files/Git/usr/bin/sh.exe

specifies the location of the sh interpreter bundled with my Git for Windows install. Notice the forward slash path separator and the backward slash escape character (used to prevent git from interpreting the path as stopping at C:/Program).

The contents of .git/hooks are excluded from version control and aren't cloned from a source repo. For this reason I prefer to:

  • include a HookScripts folder somewhere inside the “normal” repo contents (e.g. shared/HookScripts) and store hook scripts there
  • define hook behaviour separate script files, also in the HookScripts folder, and have the actual hook scripts call the separate scripts.

The HookScripts folder is then version controlled like any other folder in the repo. To install a hook script in your local repo (e.g. pre-commit in the screenshot), copy it into (or soft link it in) your local .git/hooks folder. If you copy it, you'll need to modify the shebang for your local git install.

The “official” way to manage non-standard hook script location is to use the core.hooksPath git config option, but many users seem to report difficulties with this approach, particularly in Windows systems.

In a Windows system, writing hook scripts in PowerShell is convenient. In the screenshot above, the two .ps1 PowerShell scripts files provide actions available for use in a hook script – the pre-commit hook in the same folder then need only contain calls to the necessary scripts, e.g.:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\shared\HookScripts\Sort-SemanticModels.ps1"

The path to the script file is relative to the root of the repository2).

A common use case for pre-commit hooks is to make additional standard changes to files before committing them, e.g. applying format conventions, linting code etc.

Bear in mind that changes are made in a git repo by being first staged (git add), and then committed (git commit). A pre-commit hook runs on git commit, so any additional changes made to files by the hook script must also be staged if they are to be included in the commit.

In PowerShell, I:

  • define an Invoke-Utility cmdlet to invoke Windows commands3)
  • call it with git add for any file modified in the hook script.
Function Invoke-Utility {
    $exe, $argsForExe = $Args
    $ErrorActionPreference = 'Continue' # to prevent 2> redirections from triggering a terminating error.
    try { & $exe $argsForExe } catch { Throw } # catch triggered ONLY if $exe not found, not for errors reported by $exe itself
    if ($LASTEXITCODE) { Throw "$exe indicated failure (exit code $LASTEXITCODE; full command: $Args)." }
} 
 
# modify some file $filePath
# ...
 
# re-stage file after modifying
Invoke-Utility git add $filePath

3)
I can't remember where I got this tip 🙁, but I'd love to credit it – let me know if you find it!