61

Is it possible to run PowerShell scripts as git hooks?

I am running git in a PowerShell prompt, which shouldn't make any difference, but I can't seem to get them to work, as the hooks are named without extensions, and PowerShell needs (AFAIK) the .ps1 extension. I am not sure if that is the issue, or something else.

6
  • Isn't it possible to make the script invoke the powershell script (or any other script for that matter, regardless of their extension)? Commented Apr 12, 2011 at 1:32
  • 1
    Can you give a bit more information about git hooks. Commented Apr 12, 2011 at 4:45
  • @JPBlanc: The githooks manpage. I have no idea if there is different documentation provided for the Windows version(s). Commented Apr 12, 2011 at 6:09
  • holygeek - do you have an example of firing off a PowerShell script from a bash script? I can't find any examples, and I'm not sure how to go about it. Commented Apr 13, 2011 at 4:41
  • Erick: You should be able to call it via powershell -file someScript.ps1 args Commented Oct 4, 2011 at 10:39

12 Answers 12

53

Since powershell 7.2 the script must have .ps1 extension on Windows. So this answer will not work.


You can embed PowerShell script directly inside the hook file. Here is an example of a pre-commit hook I've used:

#!/usr/bin/env pwsh

# Verify user's Git config has appropriate email address
if ($env:GIT_AUTHOR_EMAIL -notmatch '@(non\.)?acme\.com$') {
    Write-Warning "Your Git email address '$env:GIT_AUTHOR_EMAIL' is not configured correctly."
    Write-Warning "It should end with '@acme.com' or '@non.acme.com'."
    Write-Warning "Use the command: 'git config --global user.email <[email protected]>' to set it correctly."
    exit 1
}

exit 0

This example requires PowerShell Core but as a result it will run cross-platform (assuming this file has been chmod +x on Linux/macOS).

Sign up to request clarification or add additional context in comments.

9 Comments

Starting in October 2018, this should be the Accepted Answer. Thanks Keith.
Great answer! This #!/usr/bin/env pwsh "trick" works also for git rebase --exec and git bisect run. Unfortunately I noticed that pwsh doesn't exit with error code when script fails so be sure to change your script to use exit keyword when somethings fails.
This solution doesn't seem to work for me - /usr/bin/env: 'pwsh': No such file or directory, and I have powershell core installed
Unfortunaly, since powershell 7.2, the script must have .ps1 extension on Windows (github.com/PowerShell/PowerShell/releases/tag/v7.2.0-preview.9). A wrapper script like some answsers below will be needed.
Yeah, that was disappointing. :-(
|
40

Rename pre-commit.sample to pre-commit in hooks folder. Then make pre-commit.ps1 powershell script file in same folder.

#!/bin/sh
c:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy RemoteSigned -File '.git\hooks\pre-commit.ps1'

3 Comments

I think the syntax in this command line is wrong after -Command it's expecting an inline powershell command but you are specifying a file as well. It will throw an error about -File not being recognised as the name of a cmdlet, function or script file.
See my answer below if you get errors like "#! is not recognized..." or "The term -File is not recognized...".
to make it work across platforms, you could enclose above in an if statement if [[ -f path_to_powershell.exe ]]; then etc etc ; fi... And yes above works, for anyone in doubt. Thanks, this was useful Kim.
12

For the sake of completeness:

If you only have Windows PowerShell and not PowerShell Core installed then Keith Hill's neat answer doesn't work. The various answers that use a bash script to run PowerShell, passing in the path to the PowerShell script to run, are straight-forward and the way I chose to go in the end. However, I discovered there is another way:

Create two files for the git hook, say pre-commit and pre-commit.ps1. The pre-commit.ps1 file is the file that PowerShell will run. The other pre-commit file (without a file extension) is empty apart from a PowerShell interpreter directive on the first line:

#!/usr/bin/env powershell

Git will run the pre-commit file, parse the PowerShell interpreter directive and run up PowerShell, passing in the path to the pre-commit file. PowerShell will assume the file passed in should have a ".ps1" extension. It will search for pre-commit.ps1 and, since you created a file with that name and extension, PowerShell will find it and run it.

This approach is nice and simple but, in the end, I decided against it because it seemed a little "magical" and might have maintainers scratching their heads about how it works.

5 Comments

Powershell Core is the way ahead. MSFT has said all new stuff will be there so I think you're safe move to it. The only downside I can see as of this writing is that Windows 10 didn't make it a choice in the Windows+x shortcut ... yet.
This is brilliant. It was the only solution that worked for me right away.
@NoRefundsNoReturns: The problem with using PowerShell Core is portability. If would be okay if I were just writing scripts to run on my own machine. However, I write scripts that other developers can use, or that can be used on servers. So I have to stick to PowerShell 5.1 until PowerShell Core is deployed by default.
I also found this phenomenon. It feels really wonderful!
This what worked for me, too. However, before it would work I had to open git bash AS AN ADMINISTRATOR and then run powershell Set-RemoteExecutionPolicy RemoteSigned
9

From what I gather the only option due to Git's design here would be a bash script calling PowerShell. Unfortunate, but then again, Git didn't place any thought on non-Linux compatibility.

5 Comments

This seems to be the answer. Is it a pity - we're not all bash lovers, and bash on windows will always be second place. Thanks.
If git supported scripting for arbitrary platforms, how different would the config files for those hooks really look from the bash script bootstraps?
As of October 2018 you can directly use Powershell Core in the pre-commit hook as detailed by Keith Hill. Life is good. Please consider changing the accepted answer.
If you're looking for a different, possible better answer that lets you use Powershell Core directly, see my response to this question. The world has changed since this question was asked / answered and now it's almost too easy to do this.
So how do you do this? What should the bash script be named? What should it contain? What has to be installed and configured for it to run and work? In what context would this all actually work while using git (would you have to use git from bash?)?
9

Kim Ki Won's answer above didn't work for me, but it has upvotes so I'll assume it works for some people.

What worked for me was dropping the bin/sh and instead of executing using -File, executing the command directly:

c:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy RemoteSigned -Command .\.git\hooks\pre-commit.ps1

1 Comment

I was able to invoke a powershell from my precommit hook like this. Thanks for the comment. I don't know if it's the correct area to ask this question but is there a way i can get the return value of the powershell here and check it
9

Here's a starting PWSH script that I've been using for my PowerShell Git Hooks since reading Keith Hill's answer. Very nice.

#!/usr/bin/env pwsh

Process {
    Write-Information -MessageData "I Ran" -InformationAction Continue
}
Begin {
    Write-Information -MessageData "Beginning" -InformationAction Continue
}
End {
    Write-Information -MessageData "Ending" -InformationAction Continue

    Exit 0
}

I should also mention I share a single copy of hooks across all my repos. My repos all live in R:\Git and I created R:\Git\Hooks and used https://git-scm.com/docs/githooks to git config core.hooksPath=R:\Git\Hooks globally. Life is good.

4 Comments

Unfortunately, as indicated in an update to Keith's answer on Windows, shebang line #!/usr/bin/env pwsh doesn't work anymore in still-supported versions of PowerShell (Core) 7 (stopped working in v7.2), because the hook file names lack the required .ps1 extension (for the same reason #!/usr/bin/env -S powershell -File never worked in Windows PowerShell).
If there's enough community interest, perhaps support will be reinstated in a future PowerShell 7 version (but not in Windows PowerShell, which will only see security-critical fixes going forward); you can signal your interest by giving GitHub issue #26584 a 👍
Signaled Interest. Please don't make me buy new wheels and tires when my existing wheels and tires are just fine. I haven't used powershell since PWSH became available.
Making a rare appearance in SO again. Since moderators removed a highly popular and wildly upvoted answer of mine, I no longer believe SO serves the community like it did when I joined. The removal of a very useful hook feature is in line with my opinion of how the SO moderators believe they are kings. Moderators are not meant to be kings. I'll wait another 2-3 years for one of my answers to generate an email to me. b'bye agin
6

I have been looking for this myself, and i found the following:

Git Powershell pre-commit hook (Source)

## Editor's note: Link is dead as of 2014-5-2.  If you have a copy, please add it.

PHP Syntax check for git pre-commit in PowerShell (Soure)

##############################################################################
#
# PHP Syntax Check for Git pre-commit hook for Windows PowerShell
#
# Author: Vojtech Kusy <[email protected]>
#
###############################################################################

### INSTRUCTIONS ###

# Place the code to file "pre-commit" (no extension) and add it to the one of 
# the following locations:
# 1) Repository hooks folder - C:\Path\To\Repository\.git\hooks
# 2) User profile template   - C:\Users\<USER>\.git\templates\hooks 
# 3) Global shared templates - C:\Program Files (x86)\Git\share\git-core\templates\hooks
# 
# The hooks from user profile or from shared templates are copied from there
# each time you create or clone new repository.

### SETTINGS ###

# Path to the php.exe
$php_exe = "C:\Program Files (x86)\Zend\ZendServer\bin\php.exe";
# Extensions of the PHP files 
$php_ext = "php|engine|theme|install|inc|module|test"
# Flag, if set to 1 git will unstage all files with errors, se to 0 to disable
$unstage_on_error = 0;

### FUNCTIONS ###

function php_syntax_check {
    param([string]$php_bin, [string]$extensions, [int]$reset) 

    $err_counter = 0;

    write-host "Pre-commit PHP syntax check:" -foregroundcolor "white"

    git diff-index --name-only --cached HEAD -- | foreach {             
        if ($_ -match ".*\.($extensions)$") {
            $file = $matches[0];
            $errors = & $php_bin -l $file           
            if ($errors -match "No syntax errors detected in $file") {
                write-host $file ": OK" -foregroundcolor "green"
            }
            else {              
                write-host $file ":" $errors -foregroundcolor "red"
                if ($reset) {
                    git reset -q HEAD $file
                    write-host "Unstaging" $file "..." -foregroundcolor "magenta"
                }
                $err_counter++
            }
        }
    }

    if ($err_counter -gt 0) {
       exit 1
    }    
}

### MAIN ###

php_syntax_check $php_exe $php_ext $unstage_on_error

The code is for a pre-commit hook, but you could modify it to do pretty much anything. Should help what you need to do!

1 Comment

The first one does not work for me. The relative path to the powershell script does not resolve correctly.
4

This is my git hook on Windows located in .\git\hooks.

post-update

#!/bin/sh
c:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy Bypass -Command '.\post-update.ps1'

Powershell script located in the project root folder (where you initially run git init). Powershell goes to another repository and calls pull, updating that repository.

post-update.ps1

Set-Location "E:\Websites\my_site_test"
$env:GIT_DIR = 'E:\Websites\my_site_test\.git';
$env:GIT_EXEC_PATH= 'C:\Program Files (x86)\Git/libexec/git-core';
git pull

Comments

1

Better solutions for pwsh in the new era

Many of the answers above are many years old, and there are now simpler and better options.

In the days of Windows PowerShell, it was not possible to use #! /usr/bin/env powershell, because it did not accept files without extensions.

The workaround was to create script files in the directory with the same name but with the extension .ps1, which was mentioned in someone else's answer. But this takes advantage of a possible undocumented internal implementation, and unexpected problems may occur.

However, in the pwsh era, running script files without extensions has been supported for cross-platform compatibility. Even on windows platforms, it is only necessary to add #! /usr/bin/env pwsh, you can write scripts directly in this file without any other additional actions.

2 Comments

Thanks for the info Andy. This was already explained in existing answers (see the one by Keith Hill, for example), so it may be worth rather upvoting the existing answer(s) than adding another one.
To reiterate the update to Keith's answer: Sadly, on Windows shebang line #!/usr/bin/env pwsh doesn't work anymore in still-supported versions of PowerShell (Core) 7 (stopped working in v7.2), because the hook file names lack the required .ps1 extension (for the same reason #!/usr/bin/env -S powershell -File never worked in Windows PowerShell). The workarounds relying on companion .ps1 files - while cumbersome and potentially obscure - do use official features.
0

I didn't get Simon's answer to work, potentially because of other things in my path not being parsed properly by the windows git environment and/or using bonobo git server.

My objective was writing a pre-receive hook for a repository hosted in bonobo.

I ended up with the following shebang:

#!/c/Windows/System32/WindowsPowerShell/v1.0/powershell

Otherwise works identically:

  • Create pre-receive file with only shebang
  • Create pre-receive.ps1 in hooks directory. Powershell loads this instead.

In my case, for some cleanliness, i also used

mklink <path-to-repo>\hooks\pre-receive.ps1 c:\scripts\hooks\someLibraryScript.ps1

This allows me to keep my scripts in a central repository, of course.

EDIT: It's worth noting i did not manage to get Powershell to accept the stdin stream for the pre-receive hook. As a result, i'm still using a shell script to bootstrap powershell and pipe, rather than redirect, stdin to powershell.

In this case, i used the following shell script for my pre-receive hook:

#!/bin/sh
IFS=
echo `cat -` | powershell.exe -NoProfile -ExecutionPolicy Bypass -File "c:\scripts\hooks\pre-receive.ps1"

Powershell seems satisfied with that.

1 Comment

If you don't want to mess with links, you can also set the core.hooksPath config (eg. git config core.hooksPath) see docs
0

assume you have:

  • pwsh installed
  • your branch is feature/jira-1234-impl-something
  • you make a commit git commit -m "add user service"
  • you expect to have jira branch as prefix [jira-1234] add user service

Follow steps below to setup:

  1. create a ps1 script, this script will modify your commit message:
# create a file prepare-commit-msg.ps1 in the git hook directory
# not that you can name it whatever you want
ni .git/hooks/prepare-commit-msg.ps1
  1. use below script for the file prepare-commit-msg.ps1 above:
# Requires -Version 5.1 # Or higher for cross platform compatibility

# Get the branch name
$BRANCH_NAME = git rev-parse --abbrev-ref HEAD 2>$null

# with git commit, args[0] is the commit message file
# $args # print the args

# Check if the branch name is valid and not in a detached HEAD state.
if (-not [string]::IsNullOrEmpty($BRANCH_NAME) -and $BRANCH_NAME -ne "HEAD" -and $env:SKIP_PREPARE_COMMIT_MSG -ne 1) {

    # Define the prefix pattern
    $PREFIX_PATTERN = '[A-Z]{2,5}-[0-9]{1,6}'

    # Match the prefix pattern against the branch name
    if ($BRANCH_NAME -match $PREFIX_PATTERN) {

        # Extract the prefix
        $PREFIX = $Matches[0]

        # Count the occurrences of the prefix in the commit message
        $PREFIX_IN_COMMIT = $args.Count -gt 0 ? (Get-Content $args[0] | Select-String "${PREFIX}:" | Measure-Object).Count : 0

        # Check if the prefix exists and is not already in the commit message
        if (-not [string]::IsNullOrEmpty($PREFIX) -and $PREFIX_IN_COMMIT -eq 0) {

            # Modify the commit message file
            $CommitMessage = Get-Content $args[0] # get the commit message
            $CommitMessage = "[${PREFIX}]: " + $CommitMessage # add the prefix to the commit message
            $CommitMessage | Set-Content $args[0] # write the commit message to the file
        }
    }
}

feel free to adjust the pwsh script to your needs.

  1. finally, modify the actual file that git will trigger
# create a file EXACT name .git/hooks/prepare-commit-msg
ni .git/hooks/prepare-commit-msg

use following script:

#!/bin/sh
pwsh -ExecutionPolicy RemoteSigned -File "$(dirname "$0")/prepare-commit-msg.ps1" "$1" "$2" "$3"

what actually happen:

In your .git/hook/ folder, there are 2 files:

prepare-commit-msg
prepare-commit-msg.ps1

whatever your terminal/application you use, git always execute the file prepare-commit-msg

but depends on the shell (for example on Windows):

  • if you use git bash, it can execute commands available for "bash shell", which is direct script in prepare-commit-msg
  • if you use pwsh, of course bash script cannot be executed. That's why you have to "redirect" it to execute pwsh script.

Comments

0

Many years later, a summary as of:

  • Windows PowerShell v5.1 - called WinPS below - the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and final version is 5.1, whose CLI is powershell.exe

  • PowerShell (Core) 7 v7.5.x - called PS7 below - the modern, cross-platform, install-on-demand edition, whose CLI is pwsh (pwsh.exe on Windows)


  • The CLIs of both editions require that a script file passed to it on Windows have a .ps1 filename extension,[1] which Git hook scripts (e.g., pre-commmit) by design do not have.

    • This requirement was originally lifted for now-obsolete PS7 versions (up to v7.1.x) on Windows too, but it was reinstated in v7.2, with a stated rationale of, "[...] Windows doesn't support shebang, so we should be consistent with Windows PowerShell and only allow for scripts with .ps1 extension."
  • Direct use of PowerShell code in Git hook scripts using a shebang line such as #!/usr/bin/env pwsh is therefore NOT an option on Windows[2] as of PS7 v7.5.x, but that is unlikely to change.

    • That said, if there's enough community interest, perhaps the .ps1 requirement will be lifted again in a future PS7 version (but not for WinPS, which will only see security-critical fixes going forward); you can signal your interest by giving GitHub issue #26584 a 👍.

Workaround:

  • The following workaround merely requires placing a single, static line of Bash (sh) code after the default shebang line, #!/bin/sh.

    • The rest of the file can then directly contain PowerShell code.

    • The Bash code creates a copy of the hook script at hand in a temporary file with extension .ps1, invokes PowerShell with that temporary file, then cleans up the latter and passes the PowerShell process' exit code through.

  • The approach works in cross-platform manner if you target pwsh, i.e. the PS7 CLI, i.e. equally on Windows and Unix-like platforms.

    • However, if you only need Windows support and want to use WinPS so you don't have to install PS7, all you need to do is replace pwsh with powershell below.

    • Either way, the Bash line assumes that the targeted PowerShell executable is discoverable via a directory listed in the PATH environment variable; this is true by default for WinPS and typically true for PS7, depending on the installation method.

The following proof-of-concept shows the workaround with a trivial piece of PowerShell code placed directly in a hook script, e.g., ./git/hooks/pre-commit:

#!/bin/sh
tmpPs1Script="$(mktemp -u).ps1"; tail +3 "$0" > "$tmpPs1Script"; pwsh -NoProfile -File "$tmpPs1Script"; ec=$?; rm "$tmpPs1Script"; exit $ec

# The above two lines would be the same for all hook scripts that you want to
# run with PowerShell.
# Place your PowerShell code below. 

"Hi from a hook script on $(Get-Date)"

Note:

  • For cross-platform use, on Unix-like platforms, don't forget to make your hook scripts executable first, e.g. chmod +x ./git/hooks/pre-commit

  • The command used to create the temporary .ps1 script,
    tmpPs1Script="$(mktemp -u).ps1", has been chosen for compatibility with macOS too, but comes with a small, largely hypothetical caveat: another, concurrently running process hypothetically could try to use the same file path.

    • Therefore, a (marginally) more robust option on Linux platforms / platforms with GNU utilities (which includes Git Bash on Windows) is to use
      tmpPs1Script="$(mktemp --suffix=.ps1)" instead.
  • A concrete application of this technique can be found in the bottom section of this answer.


[1] Note that a shebang line that targets PowerShell e.g. #!/usr/bin/env powershell (WinPS) or #!/usr/bin/env pwsh (PS7) would in effect pass the invoked script's file path via the default CLI parameter, which differs by edition: In WinPS, it is -Command (-c), in PS7 it is -File (-f). While technically only the -File parameter enforces the presence of a .ps1 extension in its argument, passing an extension-less file (such as in the case of Git hook scripts) to -Command wouldn't work as expected: due to lacking an executable extension, such a file is treated as a document and opening it is delegated to the Windows shell, which pops up a What-application-do-you-want-to-open-this GUI dialog, asynchronously.

[2] Simon Elms' helpful answer demonstrates a clever workaround, which, however, requires creating a separate helper script with extension .ps1 for each hook script of interest, with the hook script itself containing only a WinPS shebang line, #!/usr/bin/env powershell; to make the approach work in PS7, you'd have to use #!/usr/bin/env -S pwsh -Command.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.