Automation of your GIT repository via GIT hooks and PowerShell scripts

Automation of your GIT repository via GIT hooks and PowerShell scripts

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
  • Remove .sample suffix from picked hook file name to make it active i.e. name has to match exactly GIT action name, so for example pre-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 image.png

What will happen when I try to commit ps1 with syntax error? pre-commit.gif

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 image.png What ill happen if I try to make commit with incorrect message format? pre-commit.gif


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 image.png

What will happen if I create new commit? post-commit.gif

  • 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. If git 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 :)

Did you find this article valuable?

Support Ondrej Sebela by becoming a sponsor. Any amount is appreciated!