There are detailed articles about GIT hooks already, so this post will be more about practical examples, than about theory :)
Table of contents
What are GIT hooks?
- GIT hooks are scripts, that are automatically run, if corresponding GIT action, takes place.
- If hook script ends with error, corresponding GIT action will be canceled.
- There are server and client side hooks and this article will be focusing on client hooks.
- client hooks are stored locally on the client (your computer) in .git\hooks, therefore they are more optional than mandatory (you can easily modify or delete them)
TL;DR
How to create GIT hook, that will run PowerShell script?
- In your GIT repository
<root_of_your_git_repository>\.git\hooks
choose one of the sample hook scripts.- Script name corresponds to associated GIT action i.e. for example script
pre-commit.sample
belongs to pre-commit action i.e. will be triggered before commit will be created
- Script name corresponds to associated GIT action i.e. for example script
- Remove
.sample
suffix from picked hook file name to make it active i.e. name has to match exactly GIT action name, so for examplepre-commit
- Set the script file content to
#!/bin/sh echo "# start some PS script" exec powershell.exe -NoProfile -ExecutionPolicy Bypass -file "<relative_path_to_ps1_script>" exit
- thats it, when corresponding GIT action takes place, this hook will be automatically started and run specified
<relative_path_to_ps1_script>
PowerShell script- If PowerShell script ends with error, GIT action will be canceled
- PS: to change default location of your GIT hooks scripts folder, run following command in GIT repository root:
git config core.hooksPath ".\<someNewPath>"
. As you probably know, content of.git
folder (where hooks are stored by default) is not part of your GIT repository i.e. cannot be tracked/committed etc. So by moving hooks folder for example to root of your repository via command above, makes them trackable/committable. So all your repository contributors can use same GIT hooks.
Real life use cases for use of GIT hooks
1. How to check syntax of PowerShell files before commit is created and abort if there are some errors (using pre-commit hook)
As title says, in this example I will show you, how to run PowerShell script, that will check files in commit for syntax errors. And if find some, ongoing commit will be cancelled. Pre-commit hook is perfect for this task, because it is automatically launched before commit creation process starts. So it is ideal place for commit content validation checks.
1. Create .git\hooks\pre-commit
hook script with following content
#!/bin/sh
echo "###### PRE-COMMIT git hook"
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -file ".\.git\hooks\pre-commit.ps1"
exit
2. Create .git\hooks\pre-commit.ps1
with following content
function _checkSyntax {
[CmdletBinding()]
param ($file)
$syntaxError = @()
[void][System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$null, [ref]$syntaxError)
return $syntaxError
}
function _ErrorAndExit {
param ($message)
if ( !([appdomain]::currentdomain.getassemblies().fullname -like "*System.Windows.Forms*")) {
Add-Type -AssemblyName System.Windows.Forms
}
# to GIT console output whole message
Write-Host $message
$null = [System.Windows.Forms.MessageBox]::Show($this, $message, 'ERROR', 'ok', 'Error')
exit 1
}
try {
$repoStatus = git.exe status -uno
# files to commit
$filesToCommit = @(git.exe diff --name-only --cached)
# files to commit (action type included)
$filesToCommitStatus = @(git.exe status --porcelain)
# modified but not staged files
$modifiedNonstagedFile = @(git.exe ls-files -m)
# get added/modified/renamed files from this commit (but not deleted)
$filesToCommitNoDEL = $filesToCommit | ForEach-Object {
$item = $_
if ($filesToCommitStatus -match ("^\s*(A|M|R)\s+[`"]?.+" + [Regex]::Escape($item) + "[`"]?\s*$")) {
# transform relative path to absolute + replace unix slashes for backslashes
Join-Path (Get-Location) $item
}
}
# deleted commited files
$commitedDeletedFile = @(git.exe diff --name-status --cached --diff-filter=D | ForEach-Object { $_ -replace "^D\s+" })
} catch {
$err = $_
if ($err -match "is not recognized as the name of a cmdlet") {
throw "Recency of repository can't be checked. Is GIT installed? Error was:`n$err"
} else {
throw $err
}
}
$psFilesToCommit = $filesToCommitNoDEL | Where-Object { $_ -match '\.ps1$|\.psm1$' }
$psFilesToCommit | ForEach-Object {
$script = $_
# check syntax of committed PowerShell files
$err = _checkSyntax $script
if ($err) {
_ErrorAndExit ($err.message -join "`n")
}
}
So the hooks folder will look like
What will happen when I try to commit ps1 with syntax error?
PS: for more advanced checks look at my CI/CD Powershell pre-commit.ps1
2. How to check commit message format (commit-msg)
In this example I will show you, how to enforce specific format (text: text
) of commit message.
1. Create .git\hooks\commit-msg
hook script with following content
#!/bin/sh
echo "#####################"
echo "###### COMMIT-MSG git hook"
echo "###### Checks that name of commit is in correct form: 'text: text"
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -file ".\.git\hooks\commit-msg.ps1" $1
exit
2. Create .git\hooks\commit-msg.ps1
with following content
# path to temporary file containing commit message
param ($commitPath)
$ErrorActionPreference = "stop"
# Write-Host is used to display output in GIT console
function _ErrorAndExit {
param ($message)
if ( !([appdomain]::currentdomain.getassemblies().fullname -like "*System.Windows.Forms*")) {
Add-Type -AssemblyName System.Windows.Forms
}
$message
$null = [System.Windows.Forms.MessageBox]::Show($this, $message, 'ERROR', 'ok', 'Error')
exit 1
}
try {
$commitMsg = Get-Content $commitPath -TotalCount 1
if ($commitMsg -notmatch "[^:]+: [^:]+" -and $commitMsg -notmatch "Merge branch ") {
_ErrorAndExit "Name of commit isn't in correct format: 'text: text'`n`nFor example:`n'Get-ComputerInfo: added force switch'"
}
} catch {
_ErrorAndExit "There was an error:`n$_"
}
So the hooks folder will look like What ill happen if I try to make commit with incorrect message format?
3. How to automatically push commit to remote repository (post-commit hook)
As title says, post-commit hook can help you to automate publishing of your changes, i.e. for example automatically push created commits to remote repository.
1. Create .git\hooks\post-commit
hook script with following content
#!/bin/sh
echo "#####################"
echo "###### POST-COMMIT git hook"
echo "###### Pushes commit to remote GIT repository"
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -file ".\.git\hooks\post-commit.ps1"
echo "#####################"
exit
2. Create .git\hooks\post-commit.ps1
with following content
<#
script
- is automatically run after new commit is successfully created (because of git post-commit hook)
- pushes commit to cloud repository if current branch is 'master'
#>
$ErrorActionPreference = "stop"
# Write-Host is used to display output in GIT console
function _ErrorAndExit {
param ($message)
if ( !([appdomain]::currentdomain.getassemblies().fullname -like "*System.Windows.Forms*")) {
Add-Type -AssemblyName System.Windows.Forms
}
# to GIT console output whole message
Write-Host $message
# in case message is too long, trim
$messagePerLine = $message -split "`n"
$lineLimit = 40
if ($messagePerLine.count -gt $lineLimit) {
$message = (($messagePerLine | select -First $lineLimit) -join "`n") + "`n..."
}
$null = [System.Windows.Forms.MessageBox]::Show($this, $message, 'ERROR', 'ok', 'Error')
exit 1
}
try {
# switch to repository root
Set-Location $PSScriptRoot
Set-Location ..
$root = Get-Location
function _startProcess {
[CmdletBinding()]
param (
[string] $filePath = '',
[string] $argumentList = '',
[string] $workingDirectory = (Get-Location)
)
$p = New-Object System.Diagnostics.Process
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.RedirectStandardError = $true
$p.StartInfo.WorkingDirectory = $workingDirectory
$p.StartInfo.FileName = $filePath
$p.StartInfo.Arguments = $argumentList
[void]$p.Start()
$p.WaitForExit()
$p.StandardOutput.ReadToEnd()
$p.StandardError.ReadToEnd()
}
#
# exit, if branch is not master
# it's probably just for testing purposes
$currentBranch = (_startProcess git "branch --show-current") -split "`n" | ? { $_ }
if ($currentBranch -ne "master") {
"- don't push commit to cloud repository, because current branch is not 'master'"
return
} else {
# it is 'master' branch
#
# push commit to cloud GIT repository
"- push commit to cloud repository"
$repoStatus = _startProcess git "push origin master"
# check that push was successful
if ($repoStatus -match "\[rejected\]") {
_ErrorAndExit "There was an error when trying to push commit to cloud repository:`n$repoStatus"
}
}
} catch {
_ErrorAndExit "There was an error:`n$_"
}
So the hooks folder will look like
What will happen if I create new commit?
- i.e. commit will be automatically pushed to remote repository
4. post-merge hook
- Post-merge hook is called when
git pull
is run. It can be used for example to warn users about auto-merged files. It is particularly useful, if you use VSC for managing GIT repository content instead of CMD. Ifgit pull
is called in command line, auto-merged files are automatically shown there. - It's little bit more complicated to show, so if you are interested, check this code.
Summary
I just show you some basic examples, but as you can see, GIT hooks can be very handy. I personally use all these mentioned GIT hooks in my CI/CD for PowerShell project. But there is a lot more GIT hooks and definitely more good examples of their use. So please, if you have any other tips, don't hesitate to write them down in comments :)