Git Hooks User Guide

Before We Begin
Suppose we run into this problem: in a Windows environment, we have a Git repository that contains images, and the image file extensions include both uppercase and lowercase forms, such as .PNG, .png.
We need to change uppercase image extensions to lowercase extensions, and we also need to make sure every future commit keeps image extensions lowercase. What should we do?
The first idea is to scan the existing image files, rename uppercase extensions to lowercase, commit the change, and push it to GitHub.
This immediately runs into a problem: Windows filenames are case-insensitive. Git does provide a solution for this: enable case sensitivity specifically in the Git repository.
But how can we ensure that image files in future commits always use lowercase extensions? We can try using Git hooks to automatically trigger our own script.
Git Hooks
Git Hooks are Git's script mechanism. They allow you to automatically run custom scripts before or after certain events occur in a Git repository. They can help automate workflows, run code quality checks, enforce commit conventions, and more.
Git Hooks are divided into two categories:
- Client-side Hooks: Run in the local repository in response to operations such as commits, merges, and pushes. They are commonly used for code formatting, code linting, commit message validation, and so on.
- Server-side Hooks: Run in the remote repository, or server, in response to operations such as receiving pushes and updating references. They are commonly used to enforce commit policies, trigger continuous integration, and so on.
Note
server-side hooks:
If you manage your own Git server, you can use server-side hooks to enforce stricter policies. For example, you can block commits that contain certain keywords, or automatically deploy code after receiving a push.
Note: If you use a hosted Git platform such as GitHub or GitLab, you usually cannot customize server-side hooks. However, these platforms provide Webhooks, CI/CD integrations, and other features that can achieve similar results.
The ./git/hooks Directory
Git Hooks are stored in the .git/hooks directory of each repository. By default, this directory contains some sample scripts ending in .sample.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
These sample scripts are templates for reference. If you want to enable a hook, simply remove the .sample extension, write your own script, and make sure the script has executable permissions.
Sharing Git Hooks in a Project
By default, Git Hooks are not added to version control. In other words, other users who clone the repository will not automatically get your hook scripts. To solve this, we can store the hook scripts in the repository and set hooksPath.
Create a directory in the repository to store hook scripts:
1 | |
Move your hook script into that directory:
1 | |
Tell Git to use the custom hooks directory:
1 | |
Add the hooks directory to version control:
1 2 | |
This way, other developers will also get the hook scripts after cloning the repository.
Check Uppercase Image Extensions
We use the pre-commit hook to automatically run a script before the git commit command. I use Python because both my Windows and Linux environments have Python installed.
Main function: before committing code, automatically convert the extensions of all image files in the staging area whose extensions contain uppercase letters to lowercase.
pre-commit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
Result

Code Explanation
File Header:
1 2 | |
The first line, #!/usr/bin/env python3, is a Shebang that specifies the script interpreter as python3. When the script is executed directly on a Unix/Linux system, the system uses the specified interpreter to run it.
Note
Pay special attention: the system must have the python3 environment variable, not only the python environment variable. In the following case, the script cannot run:
shell
➜ articles git:(main) ✗ python3 --version
➜ articles git:(main) ✗ python --version
Python 3.11.4
It will fail, and there will be no error message.
The second line, # -*- coding: utf-8 -*-, specifies that the script file uses UTF-8 encoding. This is very important for correctly handling strings that contain non-ASCII characters.
Import Required Modules:
1 2 3 | |
- The
osmodule provides functions for interacting with the operating system, such as file and directory operations. - The
sysmodule provides functions for interacting with the Python interpreter, such as exiting the program and getting command-line arguments. - The
subprocessmodule allows us to start new processes, connect to their input/output/error pipes, and get return values.
Get the List of Files in the Staging Area:
1 2 3 4 5 | |
- Use
subprocess.runto execute the Git commandgit diff --cached --name-only, which gets the list of changed files in the staging area. stdout=subprocess.PIPEmeans the child process's standard output is captured inresult.stdout.text=Truemeans the output data is handled as a string.- Split the output by line to get the file list
files.
Convert Uppercase Image Extensions to Lowercase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
- Define a list
image_extensionsthat contains uppercase image extensions. - Iterate through the list of files in the staging area:
- Use
os.path.isfile(file)to check whether the file exists in the working tree. - Use
os.path.splitext(file)to separate the filename and extension. - Conditions:
ext.upper() in image_extensions: whether the file extension, after being converted to uppercase, is in the image extension list.ext != ext.lower(): whether the extension contains uppercase letters.
- If the conditions are met:
- Use
ext.lower()to convert the extension to lowercase and generate the new filenamenew_file. - Use
os.rename(file, new_file)to rename the file. - Update the Git staging area:
git add new_file: add the new file to the staging area.git rm --cached file: remove the old file from the staging area, removing it only from the index without deleting it from the working tree.- Set
renamed = True, indicating that a file has been renamed. - Output the rename information.
- Use
Main Function Entry Point:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
- Call
get_staged_files()to get the list of files in the staging area. - If there are no staged files, the program exits normally, allowing the commit to continue.
- Call
rename_image_extensions(files)to process the files. - Decide what to do next based on the value of
renamed: - If any files were renamed, output a prompt message and stop the commit with
sys.exit(1), allowing the user to confirm the changes and commit again. - If no files were renamed, the program exits normally with
sys.exit(0), allowing the commit to continue.