Table of Contents
- 2.1 Introduction
- 2.2 Module overview
- 2.3 Basic concepts
- 2.3.1 Defining Git Hooks
- 2.3.2 The three types of hooks
- 2.3.3 The commit workflow
- 2.3.4 Nature of Git Hooks — what Git expects
- 2.3.5 Hooks client-side vs server-side
- 2.4 Demo: First look at Git Hooks
- 2.5 Demo: The pre-commit hook
- 2.6 Demo: The prepare-commit-msg hook
- 2.7 Demo: The commit-msg hook
- 2.8 Demo: The post-checkout hook
- 2.9 Demo: The pre-rebase hook
- 2.10 Module Summary
- 3.1 Module overview
- 3.2 Demo: First look at server-side hooks
- 3.3 Demo: The pre-receive hook
- 3.4 Demo: The update hook
- 3.5 Demo: The post-receive hook
- 3.6 Course summary
1. Course Overview
Hello everyone! My name is Timothy and welcome to my Git Hooks course. I am a Software Architect and I have designed and architected software for over 10 years.
If you’ve ever wondered how to customize your Git experience — for example, adding commits and checks before a Git command is executed, or even implementing post-command notifications — then Git Hooks are the answer to your questions.
Course objectives
In this course, we’ll cover a quick introduction to customizing Git commands using Git Hooks. The topics covered are:
- Prerequisites: Knowledge of basic Git commands is required to follow the examples in this course.
- Definition: We are going to explain what Git Hooks are.
- Practical examples: We will see examples of writing hooks, both on the client side and on the server side of the Git repository.
- Automation: The examples will demonstrate how to automate everyday use cases when interacting with Git.
Expected result
By the end of this course, you will know the basics of Git Hooks and be ready to implement your own hooks for your Git repositories.
2. Client side Hooks
2.1 Introduction
Welcome to the first of two modules on Git Hooks. In this module, we’ll talk about client-side hooks — those that install on the client side of the Git repository.
2.2 Module overview
Before we go any further, let’s take a moment to review the topics we’ll cover in this module:
- Basic concepts about Git Hooks.
- Hooks related to the commit process: There are three, and we will cover them in detail.
- Two additional hooks on the client side:
- A hook related to the rebase operation.
- A hook related to checkout.
2.3 Basic concepts
2.3.1 Defining Git Hooks
Git Hooks are extensibility points that are triggered when specific Git actions occur or when specific Git workflows are activated, such as the commit workflow that we’ll look at later in this module.
In summary:
- These are scripts or executables placed at a specific location in the Git repository.
- Git automatically calls them at key moments of its execution.
- They allow you to automate, validate or notify during Git operations.
2.3.2 The three types of hooks
Git defines several types of hooks, which can be grouped according to their name prefix:
| Prefix | Execution time | Impact on Git command |
|---|---|---|
pre- | Before starting the Git command | Can cancel the command if the exit code is non-0 |
post- | After the Git | The exit code does not affect the command |
| Miscellaneous | While running the Git | Can cancel the command if the exit code is non-0 |
Group 1 — pre- hooks:
This group includes all hooks called by Git before Git commands are actually started. These hooks have the ability to cancel the Git command if they exit with a non-0 exit code.
Group 2 — post- hooks:
As the name suggests, these hooks are called after the Git command has finished executing. Therefore, their exit code will not affect the Git command.
Group 3 — Miscellaneous hooks: This third group is a miscellaneous set of hooks called by Git during the execution of Git commands. Git typically uses these hooks to get specific data it needs during command execution. These hooks also have the ability to cancel the Git command if they exit with a non-0 exit code.
2.3.3 The commit workflow
One of the main Git workflows where all three types of hooks exist is the commit workflow — the process of applying changes to the Git repository.
Here is the complete flow of the commit workflow with its hooks:
git commit
│
▼
[1] pre-commit hook
│ → Type : pre- (peut annuler le commit)
│ → Usage : validations, vérifications de code, application de styles
│
▼ (si exit code = 0)
[2] prepare-commit-msg hook
│ → Type : divers (peut annuler le commit)
│ → Usage : populer le fichier de message de commit par défaut
│
▼ (si exit code = 0)
[Éditeur présenté au committer pour modifier le message]
│
▼
[3] commit-msg hook
│ → Type : divers (peut annuler le commit)
│ → Usage : valider le message de commit final
│
▼ (si exit code = 0)
[Les changements sont écrits dans le dépôt Git]
│
▼
[4] post-commit hook
│ → Type : post- (ne peut PAS annuler le commit)
│ → Usage : notifications, déclenchement d'actions post-commit
Detail of each step:
-
pre-commit: Git invokes this hook first. It receives a snapshot of the changes that will be committed. It is typically used to perform various code checks, validations, or even to apply code styles. If this hook exits with a non-0 exit code (failure), the entire commit workflow will be rolled back. This hook belongs to the first type (pre-hooks). -
prepare-commit-msg: After thepre-commithook succeeds, the actual commit process starts. Git will invoke this hook to get the default commit message. If this hook completes successfully, Git will present the committer with an editor to modify the default commit message. If this hook fails, the entire commit workflow will be rolled back. This hook belongs to the third type (miscellaneous hooks). -
commit-msg: Before the changes are completely written to the Git repository, Git invokes this hook to commit the commit message. If this hook completes successfully, the changes are written to the Git repository. If it fails, the entire commit workflow will be rolled back. This hook also belongs to the third type (miscellaneous hooks). -
post-commit: After the commit workflow completes, Git invokes this last hook. The exit code from this hook has no effect on the commit process. This hook belongs to the second type (post-hooks).
2.3.4 Nature of Git Hooks — what Git expects
It’s very important to know that Git Hooks are basically just executables, and that’s all Git cares about: an executable that it can run.
Implications:
- Any programming language can be used to write a Git Hook: Perl, Python, Bash, C#, Java, or any other language — as long as you produce an executable.
- Primitive communication: When Git executes the executable, it uses primitive means of communication with this process:
- command line arguments (command line arguments)
- The input stream (input stream / stdin)
- The output and error streams (output and error streams / stdout, stderr)
- environment variables (environment variables)
- Exit code: Git checks the exit code of this executable to determine whether it succeeded or failed.
- IDE neutrality: Since hooks run at the Git level, they are IDE neutral used by the developer. Regardless of the IDE, as long as it integrates Git, Git will decide when to invoke the Git Hook, and it will be executed.
- Working directory: Git will define the working directory of this executable as the root of the Git repository on which the hook is installed.
# Exemple de hook écrit en Bash
#!/bin/bash
echo "Hook exécuté depuis : $PWD"
exit 0
# Exemple de hook écrit en Python
#!/usr/bin/env python3
import sys
print("Hook Python exécuté")
sys.exit(0)
2.3.5 Client-side vs server-side hooks
An important aspect of Git Hooks is the fact that some are intended to be executed client-side, while others are intended to be executed server-side.
Main difference:
| Characteristic | Client-side Hooks | Server-side Hooks |
|---|---|---|
| Control | Under the full control of the developer | Controlled by server administrators |
| Bypass | Can be easily bypassed or disabled | Cannot be bypassed by client-side switches |
| Deactivation | May be forgotten by the developer | Can be disabled by deleting them from the filesystem (but not by the client) |
| Main use | Developer productivity and compliance with project standards | Policy enforcement of the server-side repository |
-
Client-side hooks: They are under the full control of the developer and can be easily bypassed or disabled, or even forgotten. Their main goal is developer productivity and perhaps a defensive style to maintain project standards.
-
Server-side hooks: Although they can be disabled by removing them from their location on the filesystem, they cannot be bypassed by client-side switches or arguments. Their main use case is to enforce server-side policies on the Git repository.
2.4 Demo: First look at Git Hooks
In this demo, we’ll take a first look at Git Hooks.
Background: The instructor is starting work on a calculator project and has decided to use Git as his source control system.
Location of hooks in a Git repository:
.git/
└── hooks/
├── applypatch-msg.sample
├── commit-msg.sample
├── fsmonitor-watchman.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-merge-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── pre-receive.sample
├── prepare-commit-msg.sample
├── push-to-checkout.sample
└── update.sample
Inside the .git folder there is a subfolder called hooks that Git has pre-populated with a set of files ending with the .sample extension.
Origin of these files:
For each new Git repository, Git will take a copy of these files from a shared folder:
usr/share/git-core/templates/hooks/
Git also provides configurations to change these two locations (the one at the repository level and the shared folder).
Nature of these files:
All of these files are shell scripts. However, since all Git cares about is having an executable in this folder, the instructor will create a .NET console application to demonstrate the concept.
What the trace application does:
For this discovery demo, the .NET application is designed to:
- Read ProcessName and standardInput
- Log executable invocation time in a file called
hooks-trace.log - Create file with process name (i.e. hook name)
- Write the environment variables, the command line arguments, and the standard input that were passed by Git
// Exemple de logique de l'application console .NET
var processName = Process.GetCurrentProcess().ProcessName;
var standardInput = Console.In.ReadToEnd();
var invocationTime = DateTime.Now;
// Enregistrement dans le fichier de trace
File.AppendAllText("hooks-trace.log", $"{invocationTime} - {processName}{Environment.NewLine}");
// Création du fichier de log du hook
var logFileName = $"{processName}.log";
var envVars = Environment.GetEnvironmentVariables();
var args = Environment.GetCommandLineArgs();
var content = new StringBuilder();
content.AppendLine("=== Environment Variables ===");
foreach (DictionaryEntry env in envVars)
content.AppendLine($"{env.Key}={env.Value}");
content.AppendLine("=== Command Line Arguments ===");
foreach (var arg in args)
content.AppendLine(arg);
content.AppendLine("=== Standard Input ===");
content.AppendLine(standardInput);
File.WriteAllText(logFileName, content.ToString());
Observed results:
After making a simple commit to the calculator project:
- The
hooks-trace.logfile was created. - Three log files were created by three different hooks invoked by Git.
Contents of the hooks-trace.log file:
The three hooks invoked by Git during commit, in order:
pre-commitprepare-commit-msgcommit-msg
This confirms the order of execution of hooks during the commit workflow described in Basic Concepts.
2.5 Demo: The pre-commit hook
Hook description:
The pre-commit hook is the first hook invoked by Git during the commit operation. It receives a snapshot of the changes that will be committed to Git. It is typically used to perform various code checks and validations, or even to apply code styles.
Use case in the demo:
The trainer will use this hook to execute the test cases and ensure that all pass before the commit is written to the Git repository.
Implementation:
When this hook is executed by Git, it will in turn execute the dotnet test command and configure this command to display the results in a folder called TestResults:
#!/bin/bash
# Hook pre-commit : exécution des tests avant le commit
dotnet test --results-directory TestResults
# Si dotnet test se termine avec un code de sortie non-zéro
# (certains cas de test ont échoué), on sort avec le même code
# ce qui annulera le processus de commit
exit $?
// Implémentation en C# (dans l'application .NET de hooks)
private static int HandlePreCommit()
{
var result = RunProcess("dotnet", "test --results-directory TestResults");
return result.ExitCode; // Si non-0, annule le commit
}
Expected behavior:
- If the
dotnet testcommand exits with a non-zero exit code (some test cases failed), the hook will also exit with the same exit code, which will in effect abort the commit process. - If all tests pass (exit code 0), the commit continues normally.
Demo scenario:
- The trainer added a test case for a non-existent feature — so this test is expected to fail.
- When attempting to make a commit, the
pre-commithook will execute the test cases. - Test case fails → commit command also fails → changes remain staged.
The --no-verify switch:
In some cases, we want to separate commits that add test cases from commits that implement them. To bypass the pre-commit hook in these situations:
git commit -m "Add failing test case" --no-verify
The --no-verify switch tells Git to bypass the pre-commit hook.
Important note: The
--no-verifyswitch also bypasses thecommit-msghook. However, theprepare-commit-msghook is unaffected by this switch.
Summary of behaviors with --no-verify:
| Hook | Affected by --no-verify? |
|---|---|
pre-commit | ✅ Yes, bypassed |
prepare-commit-msg | ❌ No, executed anyway |
commit-msg | ✅ Yes, bypassed |
post-commit | ❌ No, executed anyway |
2.6 Demo: The prepare-commit-msg hook
Hook description:
The prepare-commit-msg hook is the second hook invoked by Git during the commit process. It is used to populate the commit message file with a default message that will be presented to the committer.
Arguments received by the hook:
By examining the log file generated by this hook, we see that Git passes two command line arguments to it:
- First argument: A file path to a file containing the commit message (the
COMMIT_EDITMSGfile). - Second argument: A string value that indicates the source of the commit message inside the file.
- In case the message is specified as a parameter of the
git commitcommand, the value will bemessage.
When is it useful?
In cases where the git commit command is called without specifying a commit message, the prepare-commit-msg hook can be used to populate the commit message file with a default message that will be presented to the committer.
Implementation in the demo:
The trainer defines a simple mapping between users’ emails and their active task descriptions:
// Mapping email → tâche active
private static readonly Dictionary<string, string> UserTaskMapping = new()
{
{ "timothy@example.com", "[TASK-123] Implement subtraction feature" },
{ "alice@example.com", "[TASK-456] Fix division by zero bug" },
{ "bob@example.com", "[TASK-789] Add unit tests for multiplication" }
};
private static int HandlePrepareCommitMsg(string commitEditMsgFile, string source)
{
// Récupérer l'email de l'auteur depuis les variables d'environnement
var authorEmail = Environment.GetEnvironmentVariable("GIT_AUTHOR_EMAIL")
?? Environment.GetEnvironmentVariable("GIT_COMMITTER_EMAIL")
?? string.Empty;
// Chercher la description de la tâche active
if (UserTaskMapping.TryGetValue(authorEmail, out var taskDescription))
{
// Populer le fichier commitEditMsg avec la description de la tâche
File.WriteAllText(commitEditMsgFile, taskDescription);
}
return 0; // Succès - ne pas annuler le commit
}
Environment variables available for this hook:
GIT_AUTHOR_NAME - Nom de l'auteur du commit
GIT_AUTHOR_EMAIL - Email de l'auteur du commit
GIT_COMMITTER_NAME - Nom du committer
GIT_COMMITTER_EMAIL - Email du committer
GIT_AUTHOR_DATE - Date de l'auteur
GIT_COMMITTER_DATE - Date du committer
Demo progress:
- The functionality to pass the test case has been implemented.
- When running
git commit(without a message), Git prompts for the commit message. - The default commit message is now populated by the value provided by the
prepare-commit-msghook. - The instructor accepts this message by default because it correctly describes the changes made.
- After saving and closing the editor, the commit completes successfully.
# Appel sans message → le hook prepare-commit-msg entre en jeu
git commit
# L'éditeur s'ouvre avec le message pré-rempli :
# [TASK-123] Implement subtraction feature
# ~
# # Please enter the commit message for your changes. Lines starting
# # with '#' will be ignored, and an empty message aborts the commit.
2.7 Demo: The commit-msg hook
Hook description:
The commit-msg hook is the last hook invoked by Git before the Git command completes successfully. It is used to validate the final commit message.
Arguments received by the hook:
By examining the log file generated by this hook, we see that Git passes it a single command line argument: the path to the same COMMIT_EDITMSG file that was passed to the prepare-commit-msg hook.
At this point, this file contains the final version of the commit message that will be added to the commit before it is written to the Git repository.
Use case in the demo:
The formatter will use the commit-msg hook to ensure that the commit message contains a valid task number, because one of the project standards is that all commit messages must have valid task IDs.
Implementation:
private static int HandleCommitMsg(string commitEditMsgFile)
{
var commitMessage = File.ReadAllText(commitEditMsgFile);
// Vérifier que le message contient un numéro de tâche valide
// Format attendu : [TASK-XXXX] ou #XXXX
var taskNumberPattern = new Regex(@"\[TASK-\d+\]|#\d+");
if (!taskNumberPattern.IsMatch(commitMessage))
{
Console.Error.WriteLine("ERROR: Le message de commit doit contenir un numéro de tâche valide.");
Console.Error.WriteLine("Format attendu : [TASK-123] ou #123");
return 1; // Code non-0 → annuler le commit
}
return 0; // Validation réussie
}
# Exemple en Bash
#!/bin/bash
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Vérifier la présence d'un numéro de tâche
if ! echo "$COMMIT_MSG" | grep -qE '\[TASK-[0-9]+\]|#[0-9]+'; then
echo "ERROR: Le message de commit doit contenir un numéro de tâche valide."
echo "Format attendu : [TASK-123] ou #123"
exit 1
fi
exit 0
Demo progress:
- The formatter has a new commit to make.
- It executes
git commitand accepts the default commit message provided by theprepare-commit-msghook. - However, it does not include any task ID in this commit message.
- Result: The
commit-msghook rejects the commit → the commit is rolled back. - When checking the changes, we see that they are still staged (the commit has not taken place).
$ git commit
# L'éditeur s'ouvre avec le message par défaut...
# (le formateur sauvegarde sans ajouter d'ID de tâche)
error: Le message de commit doit contenir un numéro de tâche valide.
Format attendu : [TASK-123] ou #123
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Calculator.cs
# Les changements sont toujours stagés - le commit a été annulé
Full flow of three commit hooks:
git commit
↓
[pre-commit]
→ Exécute dotnet test
→ Si tests échouent : exit(1) → COMMIT ANNULÉ
→ Si tests passent : exit(0) → continue
↓
[prepare-commit-msg] (commitEditMsgFile, source)
→ Lit GIT_AUTHOR_EMAIL
→ Recherche la tâche active correspondante
→ Écrit le message par défaut dans commitEditMsgFile
→ exit(0) → continue
↓
[Éditeur présenté au committer]
↓
[commit-msg] (commitEditMsgFile)
→ Lit le contenu final du commitEditMsgFile
→ Vérifie la présence d'un numéro de tâche
→ Si absent : exit(1) → COMMIT ANNULÉ
→ Si présent : exit(0) → COMMIT RÉUSSI
2.8 Demo: The post-checkout hook
Hook description:
The post-checkout hook is a hook invoked by Git in several situations:
- When repositories are cloned (
git clone) - When files are checked out (
git checkout <file>) - When we switch from one branch to another (
git checkout <branch>orgit switch <branch>)
Problem identified in the project:
As the formatter makes commits to its Git repository, more and more files accumulate:
- Log files generated by hooks.
- Test results files generated by the
dotnet testcommand.
This disorder is particularly problematic when changing branches, because we want:
- Distinguish which test result logs were generated on which branch.
- Distinguish which hook log files were generated on which branch.
Use case in the demo:
The trainer will use the post-checkout hook to automatically clean these temporary files at each branch change.
Implementation:
private static int HandlePostCheckout(string previousHead, string newHead, bool isBranchCheckout)
{
if (isBranchCheckout)
{
// Supprimer les fichiers de log générés par les hooks liés au commit
foreach (var logFile in Directory.GetFiles(".", "*.log"))
{
File.Delete(logFile);
}
// Supprimer le dossier TestResults
if (Directory.Exists("TestResults"))
{
Directory.Delete("TestResults", recursive: true);
}
// Supprimer le fichier hooks-trace.log
if (File.Exists("hooks-trace.log"))
{
File.Delete("hooks-trace.log");
}
}
return 0;
}
#!/bin/bash
# Hook post-checkout
PREVIOUS_HEAD=$1
NEW_HEAD=$2
IS_BRANCH_CHECKOUT=$3 # 1 = changement de branche, 0 = checkout de fichier
if [ "$IS_BRANCH_CHECKOUT" = "1" ]; then
# Supprimer les fichiers de log des hooks
rm -f *.log
# Supprimer le dossier TestResults
rm -rf TestResults/
# Supprimer le fichier de trace des hooks
rm -f hooks-trace.log
echo "Nettoyage effectué lors du changement de branche."
fi
exit 0
Arguments received by the post-checkout hook:
| Position | Description |
|---|---|
$1 | The SHA of the previous HEAD (before checkout) |
$2 | The SHA of the new HEAD (after checkout) |
$3 | 1 if it’s a branch change, 0 if it’s a file checkout |
Demo result:
By moving to another branch, we see that:
- The
TestResultsfolder has been deleted. - The other log files there were also deleted.
$ git checkout feature-branch
Switched to branch 'feature-branch'
Nettoyage effectué lors du changement de branche.
$ ls
Calculator.cs
Calculator.Tests/
# Plus de fichiers de log ni de dossier TestResults !
2.9 Demo: The pre-rebase hook
Hook description:
The pre-rebase hook is invoked by Git each time branches are rebased (git rebase).
Context and motivation:
Rebase operations rewrite source control history. In general, they are considered dangerous operations, particularly in a team context where several developers are working on the same repository.
Use case in the demo:
The trainer will use this hook to prevent any rebase operation on the branches of his Git repository.
Implementation:
private static int HandlePreRebase(string upstreamBranch, string rebasingBranch)
{
Console.Error.WriteLine($"ERROR: Les opérations de rebase sont interdites sur ce dépôt.");
Console.Error.WriteLine($"Tentative de rebase sur la branche : {upstreamBranch}");
Console.Error.WriteLine("Utilisez git merge à la place.");
return 1; // Code non-0 → annuler le rebase
}
#!/bin/bash
# Hook pre-rebase - Interdire toutes les opérations de rebase
UPSTREAM_BRANCH=$1
REBASING_BRANCH=$2
echo "ERROR: Les opérations de rebase sont interdites sur ce dépôt."
echo "Branche upstream : $UPSTREAM_BRANCH"
echo "Utilisez 'git merge' à la place du rebase."
exit 1
Arguments received by the pre-rebase hook:
| Position | Description |
|---|---|
$1 | The name of the upstream branch on which we are trying to rebase |
$2 | The name of the branch being rebased (can be empty) |
Demo progress:
- The formatter attempts to rebase the
masterbranch on one of its other branches. - The
pre-rebasehook immediately rejects the operation and rolls back. - By examining the log file generated by the
pre-rebasehook, we can see that Git passed as a command line argument the name of the branch on which we were trying to rebase.
$ git rebase feature-branch
error: Les opérations de rebase sont interdites sur ce dépôt.
Branche upstream : feature-branch
Utilisez 'git merge' à la place du rebase.
$ cat pre-rebase.log
=== Command Line Arguments ===
feature-branch
# Git a passé le nom de la branche comme argument
Note: Unlike the
pre-commithook which can be bypassed with--no-verify, thepre-rebasehook cannot be bypassed as easily by a standard Git switch. This feature makes it more robust for enforcing client-side policies.
2.10 Module Summary
This module covered the following topics:
- Basic concepts of Git Hooks: Definition, types, and characteristics.
- Client-side hooks related to commit:
pre-commit: Execution of tests before commit.prepare-commit-msg: Automatic population of the commit message.commit-msg: Validation of the commit message format.
- Additional client-side hooks:
post-checkout: Cleaning temporary files when changing branches.pre-rebase: Prohibition of rebase operations.
Fundamental takeaway of module:
client-side hooks are under the full control of the developer, and therefore cannot be used to enforce repository policies. This is why we need server-side hooks, which we will talk about in the next module.
3. Server side Hooks
3.1 Module overview
Welcome to the second module on Git Hooks. In this module, we will talk about server-side hooks. We’ll look at some of the server-side hooks that will be invoked by Git during a git push operation, and then we’ll look at how to use these hooks to enforce server-side policies on our Git repository.
Hooks covered in this module:
| Hook | Summoning Time | Typical usage |
|---|---|---|
pre-receive | Before accepting pushed commits | Global commits on all commits/branches |
update | For each updated branch | Branch-specific validations |
post-receive | After accepting all commits | Notifications, CI/CD, deployments |
3.2 Demo: First look at server-side hooks
In this demo, we’ll take a first look at server-side hooks, doing something similar to what we did with client-side hooks.
Initial configuration:
After installing the hooks on the server repository, the formatter pushes all its branches. By examining the contents of the server repository folder:
server-repo/
├── hooks-trace.log ← fichier de trace généré
├── pre-receive.log ← log du hook pre-receive
├── update-branch1.log ← log du hook update (branche 1)
├── update-branch2.log ← log du hook update (branche 2)
└── post-receive.log ← log du hook post-receive
Execution order of server-side hooks:
By examining the contents of the hooks-trace.log file, the execution order is confirmed:
1. pre-receive → Invoqué une seule fois pour toutes les branches
2. update → Invoqué pour chaque branche mise à jour (2 fois dans cet exemple)
3. post-receive → Invoqué une seule fois après toutes les mises à jour
What each hook receives:
Hook pre-receive — via stdin:
The log file for the pre-receive hook shows that it received two lines in the Standard Input — one line per branch pushed from the client side:
<ancien-SHA> <nouveau-SHA> refs/heads/master
<ancien-SHA> <nouveau-SHA> refs/heads/feature-branch
Each line contains:
- The old SHA (state before push)
- The new SHA (state after push)
- The reference name (branch name)
Hook update — via command line arguments:
The first log file of the update hook shows that it received via command line arguments the same data as in the pre-receive log file, but for the first branch only. The second log file of the update hook contains the second line of the pre-receive file.
# Arguments reçus par le hook update pour chaque branche :
# $1 = nom de la référence (ex: refs/heads/master)
# $2 = ancien SHA
# $3 = nouveau SHA
Hook post-receive — via stdin:
The post-receive hook log file contains the same data as the pre-receive hook (all branches, via stdin).
3.3 Demo: The pre-receive hook
Hook description:
As seen in the previous demo, the pre-receive hook receives in its standard input a line for each branch updated by the client. For each branch, it also receives the range of commits that are pushed by the client.
This is therefore a very suitable place to perform global validations and checks on all branches updated by clients.
Use case in the demo:
The trainer will use this hook to enforce a global policy: any commit pushed to any branch must have a task number in its message.
Implementation:
private static int HandlePreReceive()
{
var stdin = Console.In;
string? line;
while ((line = stdin.ReadLine()) != null)
{
// Chaque ligne correspond à une branche mise à jour
var parts = line.Split(' ');
var oldSha = parts[0];
var newSha = parts[1];
var refName = parts[2]; // ex: refs/heads/master
// Scanner la plage de commits poussés pour cette branche
var commitRange = $"{oldSha}..{newSha}";
var commits = GetCommitsInRange(commitRange);
foreach (var commit in commits)
{
var commitMessage = GetCommitMessage(commit);
// Vérifier la présence d'un numéro de tâche
if (!HasTaskNumber(commitMessage))
{
Console.Error.WriteLine($"ERROR: Le commit {commit} n'a pas de numéro de tâche.");
Console.Error.WriteLine($"Message : {commitMessage}");
return 1; // Annuler le push
}
}
}
return 0; // Tous les commits sont valides
}
private static bool HasTaskNumber(string message)
{
return Regex.IsMatch(message, @"\[TASK-\d+\]|#\d+");
}
private static IEnumerable<string> GetCommitsInRange(string range)
{
var result = RunProcess("git", $"log {range} --format=%H");
return result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
}
private static string GetCommitMessage(string sha)
{
var result = RunProcess("git", $"log -1 --format=%s {sha}");
return result.Output.Trim();
}
#!/bin/bash
# Hook pre-receive - Validation globale des messages de commit
while read oldSHA newSHA refName; do
# Pour chaque branche mise à jour
branchName=${refName#refs/heads/}
# Scanner tous les commits dans la plage poussée
git log "$oldSHA..$newSHA" --format="%H %s" | while read sha message; do
# Vérifier la présence d'un numéro de tâche
if ! echo "$message" | grep -qE '\[TASK-[0-9]+\]|#[0-9]+'; then
echo "ERROR: Le commit $sha n'a pas de numéro de tâche valide."
echo "Message : $message"
echo "Tous les commits doivent contenir un numéro de tâche [TASK-XXX]."
exit 1
fi
done
# Vérifier le code de sortie du sous-shell
if [ $? -ne 0 ]; then
exit 1
fi
done
exit 0
Demo progress:
- In the calculator project, the trainer merged the
subtractionbranch into themasterbranch. - As part of this merge, a few commits were added to the
masterbranch without task number. - When attempting to push the
masterbranch, thepre-receivehook rejects the push operation because one of the commits does not have a task number in its message.
$ git push origin master
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 320 bytes | 320.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: ERROR: Le commit abc1234 n'a pas de numéro de tâche valide.
remote: Message : Add subtraction functionality
remote: Tous les commits doivent contenir un numéro de tâche [TASK-XXX].
To server:/path/to/repo.git
! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'server:/path/to/repo.git'
Special case — New deposit:
During a first push to an empty repository, the old SHA will consist of all zeros (000000000000000000000000000000000000000000). This particular case must be handled in the implementation:
# Si oldSHA est tout en zéros, c'est un nouveau dépôt
if [ "$oldSHA" = "0000000000000000000000000000000000000000" ]; then
# Scanner depuis le début de l'historique
commitRange="$newSHA"
git log "$commitRange" --format="%H %s" | while read sha message; do
# ...validation...
done
else
commitRange="$oldSHA..$newSHA"
# ...scanning normal...
fi
3.4 Demo: The update hook
Hook description:
The update hook is the second hook invoked by Git during the push operation. As seen in the previous demo, this hook is invoked for each branch updated by the push operation.
Arguments received:
On each invocation, the update hook receives in its command line arguments:
- The updated branch name
- The starting SHA of the pushed commit range (old SHA)
- The trailing SHA of the pushed commit range (new SHA)
So this is the best place to do branch-specific commits and checks before accepting pushed commits.
Use case in the demo:
The trainer will use this hook to implement branch access controls on the server — allowing for each branch only a subset of users to push commits.
Implementation — Access Control List (ACL):
// Définition de l'ACL (Access Control List)
private static readonly Dictionary<string, List<string>> BranchAccessControl = new()
{
// Seul Timothy peut pousser des commits sur master
{ "refs/heads/master", new List<string> { "timothy@example.com" } },
// Alice et Bob peuvent pousser sur la branche develop
{ "refs/heads/develop", new List<string> { "alice@example.com", "bob@example.com" } },
// N'importe qui peut pousser sur les branches feature
// (non spécifié = pas de restriction)
};
private static int HandleUpdate(string refName, string oldSha, string newSha)
{
// Vérifier si la branche est dans l'ACL
if (!BranchAccessControl.TryGetValue(refName, out var allowedUsers))
{
// Branche non listée dans l'ACL → pas de restriction
return 0;
}
// Scanner les commits dans la plage poussée
var commitRange = $"{oldSha}..{newSha}";
var commits = GetCommitsInRange(commitRange);
foreach (var commit in commits)
{
var authorEmail = GetCommitAuthorEmail(commit);
// Vérifier que l'email de l'auteur est dans la liste des utilisateurs autorisés
if (!allowedUsers.Contains(authorEmail))
{
Console.Error.WriteLine($"ERROR: L'utilisateur {authorEmail} n'est pas autorisé");
Console.Error.WriteLine($" à pousser des commits sur la branche {refName}.");
Console.Error.WriteLine($"Utilisateurs autorisés : {string.Join(", ", allowedUsers)}");
return 1; // Rejeter le push
}
}
return 0; // Push autorisé
}
private static string GetCommitAuthorEmail(string sha)
{
var result = RunProcess("git", $"log -1 --format=%ae {sha}");
return result.Output.Trim();
}
#!/bin/bash
# Hook update - Contrôle d'accès par branche
REF_NAME=$1
OLD_SHA=$2
NEW_SHA=$3
# ACL - Définir les branches et leurs utilisateurs autorisés
declare -A BRANCH_ACL
BRANCH_ACL["refs/heads/master"]="timothy@example.com"
BRANCH_ACL["refs/heads/develop"]="alice@example.com bob@example.com"
# Extraire le nom court de la branche
BRANCH_SHORT=${REF_NAME#refs/heads/}
# Vérifier si la branche est dans l'ACL
if [ -z "${BRANCH_ACL[$REF_NAME]}" ]; then
# Branche non restreinte
exit 0
fi
ALLOWED_USERS="${BRANCH_ACL[$REF_NAME]}"
# Scanner les commits dans la plage poussée
git log "$OLD_SHA..$NEW_SHA" --format="%H" | while read COMMIT_SHA; do
AUTHOR_EMAIL=$(git log -1 --format="%ae" "$COMMIT_SHA")
# Vérifier si l'auteur est autorisé
if ! echo "$ALLOWED_USERS" | grep -qw "$AUTHOR_EMAIL"; then
echo "ERROR: $AUTHOR_EMAIL n'est pas autorisé à pousser sur $BRANCH_SHORT."
echo "Utilisateurs autorisés : $ALLOWED_USERS"
exit 1
fi
done
exit 0
Demo progress:
- Formatter has a new commit to push.
- Looking at this commit, the author email of this commit is not the trainer email.
- When attempting to push this commit to the
masterbranch on the server, theupdatehook rejects the push because the email in the commit is not allowed to push commits to themasterbranch.
$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 285 bytes | 285.00 KiB/s, done.
remote: ERROR: other.user@example.com n'est pas autorisé à pousser sur master.
remote: Utilisateurs autorisés : timothy@example.com
To server:/path/to/repo.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'server:/path/to/repo.git'
Difference between pre-receive and update:
| Characteristic | pre-receive | update |
|---|---|---|
| Summon Frequency | Only once for all refs | Once per ref (per branch) |
| Data received | stdin (all branches) | Command line arguments (one branch) |
| Best Use | global policies | branch-specific policies |
| Power | Can reject everything in one output | Can selectively reject by branch |
3.5 Demo: The post-receive hook
Hook description:
The post-receive hook is the last hook executed by Git during the push operation. As seen in the previous demo, this hook receives the same data as the pre-receive hook (via stdin).
Fundamental difference with pre- hooks:
Unlike the pre-receive and update hooks, the formatter will not use this information to perform validations or checks. It will simply take advantage of the fact that this hook is invoked at the end of the push operation to implement a simple continuous integration (CI) functionality in its server-side Git repository.
Use case in the demo:
When this hook is executed by Git, the formatter will:
- Clone the server-side repository into a workspace folder.
- Run a
dotnet buildon the server side.
private static int HandlePostReceive()
{
var workspacePath = Path.Combine(Path.GetTempPath(), "calculator-build");
// Nettoyer le workspace précédent si existant
if (Directory.Exists(workspacePath))
{
Directory.Delete(workspacePath, recursive: true);
}
// Cloner le dépôt serveur dans le workspace
var serverRepoPath = Environment.GetEnvironmentVariable("GIT_DIR") ?? ".";
var cloneResult = RunProcess("git", $"clone {serverRepoPath} {workspacePath}");
if (cloneResult.ExitCode != 0)
{
Console.Error.WriteLine("ERROR: Le clonage du dépôt a échoué.");
return 0; // post-receive ne peut pas annuler le push
}
Console.WriteLine($"Dépôt cloné dans : {workspacePath}");
// Exécuter dotnet build
var buildResult = RunProcess("dotnet", "build", workingDirectory: workspacePath);
if (buildResult.ExitCode != 0)
{
Console.Error.WriteLine("WARNING: Le build a échoué !");
}
else
{
Console.WriteLine("Build réussi ! L'application est prête à être déployée.");
}
return 0; // Le code de sortie n'affecte pas le push
}
#!/bin/bash
# Hook post-receive - Intégration continue simple
WORKSPACE_PATH="/tmp/calculator-build"
SERVER_REPO_PATH="$GIT_DIR"
# Nettoyer le workspace précédent
rm -rf "$WORKSPACE_PATH"
# Cloner le dépôt serveur
echo "--- CI: Clonage du dépôt ---"
git clone "$SERVER_REPO_PATH" "$WORKSPACE_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: Le clonage a échoué."
exit 0 # Ne pas annuler le push (post-receive ne le peut pas)
fi
echo "--- CI: Build de l'application ---"
cd "$WORKSPACE_PATH"
dotnet build
if [ $? -ne 0 ]; then
echo "WARNING: Le build a échoué !"
else
echo "--- CI: Build réussi ! ---"
echo "L'application est disponible dans : $WORKSPACE_PATH/bin/"
fi
exit 0
Important: Even if the build fails, the
post-receivehook cannot cancel the push — the commits have already been accepted. This is why important validations should always be done in thepre-receiveorupdatehooks.
Demo progress:
- The trainer has one last commit to make to make his calculator usable from the command line.
- By pushing this commit to the server, the
post-receivehook:
- Complete repository cloning.
- Run build on server.
- Going to the build location and checking the output, the calculator was built successfully.
- When I tried it, it works correctly.
$ git push origin master
Counting objects: 4, done.
Writing objects: 100% (4/4), 450 bytes | 450.00 KiB/s, done.
remote: --- CI: Clonage du dépôt ---
remote: Clonage dans '/tmp/calculator-build'...
remote: --- CI: Build de l'application ---
remote: Build succeeded.
remote: 0 Warning(s)
remote: 0 Error(s)
remote: --- CI: Build réussi ! ---
remote: L'application est disponible dans : /tmp/calculator-build/bin/
To server:/path/to/repo.git
abc1234..def5678 master -> master
$ /tmp/calculator-build/bin/Calculator 5 + 3
= 8
$ /tmp/calculator-build/bin/Calculator 10 - 4
= 6
Overview of the complete workflow during a git push:
git push origin master
│
▼
[pre-receive]
│ → Reçoit via stdin : toutes les branches poussées
│ → Validation globale : numéros de tâche dans les commits
│ → Si échec : exit(1) → PUSH REJETÉ pour toutes les branches
│
▼ (si exit code = 0)
[update] × N (une fois par branche)
│ → Reçoit via args : nom de branche, ancien SHA, nouveau SHA
│ → Validation par branche : ACL (contrôle d'accès)
│ → Si échec : exit(1) → PUSH REJETÉ pour cette branche
│
▼ (si exit code = 0)
[Les commits sont acceptés dans le dépôt serveur]
│
▼
[post-receive]
│ → Reçoit via stdin : toutes les branches (comme pre-receive)
│ → Pas de validation - seulement des actions post-push
│ → Ex: clone + build (CI), notifications, déploiements
│ → Exit code : N'AFFECTE PAS le push (déjà accepté)
3.6 Course Summary
In this course we talked about Git Hooks. Here are the key points covered:
1. Basic concepts:
- Setting Git Hooks as extensibility points
- Their types (pre-, post-, miscellaneous)
- Their characteristics (executables, primitive communication, neutrality towards IDEs)
2. Client-side hooks:
- These hooks can only be used for developer productivity.
- They cannot be used to enforce team-wide repository policies.
3. Server-side hooks:
- These hooks allow enforcing repository policies on the server side.
- They can also be used to implement other server-side aspects of the Git repository, such as continuous integration.
Hooks covered in this course:
| Hook | Type | Category | Demonstrated use |
|---|---|---|---|
pre-commit | pre- | Customer | Running Tests Before Commit |
prepare-commit-msg | miscellaneous | Customer | Automatic commit message population |
commit-msg | miscellaneous | Customer | Validating commit message format |
post-checkout | post | Customer | Cleaning temporary files |
pre-rebase | pre- | Customer | Prohibition of rebase operations |
pre-receive | pre- | Server | Global validation of commits (task numbers) |
update | miscellaneous | Server | Branch Access Control (ACL) |
post-receive | post | Server | Continuous integration (clone + build) |
To obtain a copy of the Git Hook project developed throughout this course, visit the project site on GitHub.
4. Hooks Quick Reference
Client-side hooks
Commit related hooks
| Hook | Arguments | stdin | Can cancel? | Bypassable by --no-verify |
|---|---|---|---|---|
pre-commit | None | None | ✅ Yes (exit ≠ 0) | ✅ Yes |
prepare-commit-msg | <msg-file> [<source> [<sha1>]] | None | ✅ Yes (exit ≠ 0) | ❌ No |
commit-msg | <msg-file> | None | ✅ Yes (exit ≠ 0) | ✅ Yes |
post-commit | None | None | ❌ No | ❌ No (already done) |
Other client-side hooks
| Hook | Arguments | stdin | Can cancel? |
|---|---|---|---|
pre-checkout | None | None | N/A |
post-checkout | <old-HEAD> <new-HEAD> <branch-flag> | None | ❌ No |
pre-rebase | <upstream> [<branch>] | None | ✅ Yes (exit ≠ 0) |
pre-push | <remote-name> <remote-url> | Push Refs | ✅ Yes (exit ≠ 0) |
pre-merge-commit | None | None | ✅ Yes (exit ≠ 0) |
post-merge | <flag-squash> | None | ❌ No |
Server-side hooks
| Hook | Arguments | stdin | Can cancel the push? | Frequency |
|---|---|---|---|---|
pre-receive | None | <old-SHA> <new-SHA> <ref> (one line/branch) | ✅ Yes (exit ≠ 0) | Once per push |
update | <ref> <old-SHA> <new-SHA> | None | ✅ Yes (exit ≠ 0) | Once per ref |
post-receive | None | <old-SHA> <new-SHA> <ref> (one line/branch) | ❌ No | Once per push |
post-update | <ref1> [<ref2>...] | None | ❌ No | Once per push |
Hook location
# Dans un dépôt Git local (client-side)
<racine-du-dépôt>/.git/hooks/
# Dans un dépôt Git bare (server-side)
<racine-du-dépôt>/hooks/
# Templates Git (source par défaut des hooks)
/usr/share/git-core/templates/hooks/ # Linux/macOS
C:\Program Files\Git\share\git-core\templates\hooks\ # Windows
Enable a .sample hook
# Renommer le fichier en supprimant l'extension .sample
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
# Rendre le script exécutable (Linux/macOS)
chmod +x .git/hooks/pre-commit
# Vérifier que le hook est exécutable
ls -la .git/hooks/pre-commit
Minimal structure of a shell hook
#!/bin/bash
# Hook : pre-commit (exemple minimal)
# Votre logique ici...
echo "Hook pre-commit exécuté"
# Sortir avec 0 pour le succès, non-0 pour l'échec/annulation
exit 0
Debug hooks
# Ajouter des logs dans le hook
#!/bin/bash
exec 1>/tmp/hook-debug.log 2>&1
set -x # Afficher chaque commande exécutée
echo "Arguments : $@"
echo "Variables d'environnement liées à Git :"
env | grep GIT_
# Votre logique...
exit 0
5. Best practices
For client-side hooks
-
Never block legitimate workflows without a workaround option: The
--no-verifyswitch is intentionally available for cases where the developer knows what he is doing (eg: committing a failed test to work on it). -
Keep hooks fast: A slow
pre-commithook discourages developers from making frequent commits. If the tests take too long, only run tests related to modified files. -
Provide clear error messages: When a hook rejects an action, clearly explain why and how to correct the problem.
-
Document project hooks: Hooks are not versioned by default in
.git/hooks/. It is recommended to place them in a versioned folder (eg:scripts/hooks/) and to provide an installation script.# scripts/install-hooks.sh #!/bin/bash HOOKS_DIR=".git/hooks" SOURCE_DIR="scripts/hooks" for hook in "$SOURCE_DIR"/*; do hook_name=$(basename "$hook") ln -sf "../../$SOURCE_DIR/$hook_name" "$HOOKS_DIR/$hook_name" chmod +x "$HOOKS_DIR/$hook_name" echo "Hook installé : $hook_name" done -
Share hooks via dedicated tools: Tools like
husky(for Node.js projects) orpre-commit(for Python) simplify the distribution and installation of hooks within the team.
For server-side hooks
-
Commit early with
pre-receive: Reject invalid pushes as early as possible, before accepting commits. -
Use
updatefor per-branch policies: Theupdatehook is perfect for branch-specific validations (ACLs, branch name formats, etc.). -
Reserve
post-receivefor non-critical actions: Since this hook cannot cancel the push, use it only for actions that can fail without impacting the integrity of the repository (notifications, CI, deployments). -
Handle the case of a new deposit: When the previous SHA is
00000000000000000000000000000000000000000, this is a first push. Ensure that the validation logic handles this case correctly. -
Test hooks in isolation: Before deploying a hook on the server, test it locally by simulating the arguments and stdin that Git would pass to it.
# Simuler l'appel au hook pre-receive echo "0000000 abc1234 refs/heads/master" | .git/hooks/pre-receive # Simuler l'appel au hook update .git/hooks/update refs/heads/master 0000000 abc1234
Recommended structure for team hooks
projet/
├── .git/
│ └── hooks/ ← Hooks installés (non versionnés)
├── scripts/
│ ├── hooks/ ← Sources des hooks (versionnés)
│ │ ├── pre-commit
│ │ ├── commit-msg
│ │ ├── prepare-commit-msg
│ │ └── pre-push
│ └── install-hooks.sh ← Script d'installation
├── README.md
│ └── # Section : Configuration du développement
│ # 1. Installer les hooks Git : bash scripts/install-hooks.sh
└── ...
Search Terms
git · hooks · ci/cd · devops · hook · client-side · server-side · commit · side