# Git Hooks for Checking Code
## Language
[English](#English) | [中文](#中文)
## English
### Created Date
2025/03/07
### Environment
ubuntu-22.04.4-desktop-amd64.iso
> This is console output:
> ```console=
> $ cat /etc/os-release
> PRETTY_NAME="Ubuntu 22.04.4 LTS"
> NAME="Ubuntu"
> VERSION_ID="22.04"
> VERSION="22.04.4 LTS (Jammy Jellyfish)"
> VERSION_CODENAME=jammy
> ID=ubuntu
> ID_LIKE=debian
> HOME_URL="https://www.ubuntu.com/"
> SUPPORT_URL="https://help.ubuntu.com/"
> BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
> PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
> UBUNTU_CODENAME=jammy
>
> $ uname -a
> Linux wilkes-evt 6.8.0-52-generic #53~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Jan 15 19:18:46 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
> ```
### Symptom
None
### Cause
Perform static code analysis using Git Hooks when executing the `git commit` command.
### Solution
After running the `git init` command to add the project to Git version control, a .git directory will be created at the root of the project. This directory contains Git configurations and files necessary for version control, and it cannot be added to the Git repository using `git add`. To enable static code analysis, modify the `.git/hooks/pre-commit` script inside the project directory accordingly.
#### Tools Installing
```shell
sudo apt install git vim cppcheck
```
#### Example Workflow
1. Create a new project and add it to version control, then check whether the .git directory has been created.
```shell
mkdir GIT_TEST
cd GIT_TEST
git init
ls -a
```
> This is console output:
> ```console=
> $ mkdir GIT_TEST
> $ cd GIT_TEST
> $ git init
> hint: Using 'master' as the name for the initial branch. This default branch name
> hint: is subject to change. To configure the initial branch name to use in all
> hint: of your new repositories, which will suppress this warning, call:
> hint:
> hint: git config --global init.defaultBranch <name>
> hint:
> hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
> hint: 'development'. The just-created branch can be renamed via this command:
> hint:
> hint: git branch -m <name>
> Initialized empty Git repository in /home/chrisdeng/GIT_TEST/.git/
> $ ls -a
> . .. .git
> ```
2. Add a new file at `.git/hooks/pre-commit`. You can use [git-pre-commit-cppcheck](<https://github.com/danmar/cppcheck/blob/main/tools/git-pre-commit-cppcheck>) as a reference template. \
**Important!** The difference from the template is that the cppcheck line includes the additional `--enable=all` option.
```shell
vim .git/hooks/pre-commit
```
```bash
#!/bin/sh
# Usage: add this file to your project's .git/hooks directory. Rename it to
# just 'pre-commit'.
# Now, when you change some files in repository and try to commit these
# changes, git will run this script right before commit. Cppcheck will scan
# changed/new files in repository. If it finds some issues, script returns with
# exit code 1, rejecting commit. Otherwise, script returns 0, and you can
# actually commit your changes.
#
# Example:
# $ cat hello.c
# int main() {
# int *s = malloc(10);
# }
# $ git add hello.c
# $ git commit
# Checking hello.c...
# [hello.c:3]: (error) Memory leak: s
# [hello.c:2]: (error) The allocated size 10 is not a multiple of the underlying type's size.
#
# $ vim hello.c
# $ cat hello.c
# int main() {
# }
# $ git add hello.c
# $ git commit
# Checking hello.c...
# $
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# We should pass only added or modified C/C++ source files to cppcheck.
changed_files=$(git diff-index --cached $against | \
grep -E '[MA] .*\.(c|cpp|cc|cxx)$' | cut -f 2)
if [ -n "$changed_files" ]; then
cppcheck --enable=all --error-exitcode=1 $changed_files
exit $?
fi
exit 0
```
3. Change the file to be executable.
```shell
chmod 775 .git/hooks/pre-commit
```
4. Create a new file for error detection.
```shell
vim err.c
```
```c
#include <stdio.h>
int main()
{
int i;
return 0;
}
```
5. Failed to commit the file.
```shell
git add err.c
git commit -m "First commit"
```
> This is console output:
> ```console=
> $ git add err.c
> $ git commit -m "First commit"
> Checking err.c ...
> err.c:5:9: style: Unused variable: i [unusedVariable]
> int i;
> ^
> nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingIncludeSystem]
> ```
6. Correct the errors as instructed by the prompts.
```shell
vim err.c
```
```c
#include <stdio.h>
int main()
{
return 0;
}
```
7. The file was successfully re-committed.
```shell
git add err.c
git commit -m "First commit"
```
> This is console output:
> ```console=
> $ git add err.c
> $ git commit -m "First commit"
> Checking err.c ...
> nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingIncludeSystem]
>
> [master (root-commit) ed2dc78] First commit
> 1 file changed, 6 insertions(+)
> create mode 100644 err.c
> ```
8. Confirm that the commit was completed successfully.
```shell
git log -p
```
> This is console output:
> ```console=
> $ git log -p
> commit ed2dc78e4f0bbe6ee2e93572f5cb82b88f91bd66 (HEAD -> master)
> Author: User <User@mail.com>
> Date: Fri Mar 7 16:47:24 2025 +0800
>
> First commit
>
> diff --git a/err.c b/err.c
> new file mode 100644
> index 0000000..c5d6809
> --- /dev/null
> +++ b/err.c
> @@ -0,0 +1,6 @@
> +#include <stdio.h>
> +
> +int main()
> +{
> + return 0;
> +}
> ```
#### Extension
You can configure a `global hooks` directory in Git, so there's no need to set it up individually for each project. Use the following Git command:
```shell
git config --global core.hooksPath /path/to/global/hooks
```
> Replace /path/to/global/hooks with the directory path where you want to store your `global hooks`.
> > Git will automatically use the pre-commit script located in this path for all projects. Whenever `git commit` is executed in any Git project, that pre-commit hook should be triggered.
> > If a specific project requires customization, you can configure a custom pre-commit hook in that project's `.git/hooks` directory. This will override the `global hooks` pre-commit script.
### Reference
- [什麼是 Git Hooks?為什麼它這麼萬能?](<https://yhtechnote.com/git-hooks/>)
- [如何在執行git commit前自動進行檢查?Git Hooks的基本用法](<https://magiclen.org/git-hooks/>)
- [git-pre-commit-cppcheck](<https://github.com/danmar/cppcheck/blob/main/tools/git-pre-commit-cppcheck>)
## 中文
### 建立日期
2025/03/07
### 環境
ubuntu-22.04.4-desktop-amd64.iso
> 這是控制台輸出:
> ```console=
> $ cat /etc/os-release
> PRETTY_NAME="Ubuntu 22.04.4 LTS"
> NAME="Ubuntu"
> VERSION_ID="22.04"
> VERSION="22.04.4 LTS (Jammy Jellyfish)"
> VERSION_CODENAME=jammy
> ID=ubuntu
> ID_LIKE=debian
> HOME_URL="https://www.ubuntu.com/"
> SUPPORT_URL="https://help.ubuntu.com/"
> BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
> PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
> UBUNTU_CODENAME=jammy
>
> $ uname -a
> Linux wilkes-evt 6.8.0-52-generic #53~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Jan 15 19:18:46 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
> ```
### 症狀
無
### 原因
執行 `git commit` 指令時,利用 Git 的 Hooks 功能,進行程式碼靜態分析。
### 解決方案
當 `git init` 指令替專案加入 Git 版本控制後,專案目錄底下會多出一個 .git 目錄,這個目錄用來存放 Git 設定與版本控制所需的檔案,它並不能被 `git add` 進 Git 倉庫中。為了程式碼靜態分析之目的,修改專案目錄裡的 `.git/hooks/pre-commit` 來達成。
#### 工具安裝
```shell
sudo apt install git vim cppcheck
```
#### 範例工作流程
1. 建立新專案加入版本控制,然後檢查 .git 目錄是否生成。
```shell
mkdir GIT_TEST
cd GIT_TEST
git init
ls -a
```
> 這是控制台輸出:
> ```console=
> $ mkdir GIT_TEST
> $ cd GIT_TEST
> $ git init
> hint: Using 'master' as the name for the initial branch. This default branch name
> hint: is subject to change. To configure the initial branch name to use in all
> hint: of your new repositories, which will suppress this warning, call:
> hint:
> hint: git config --global init.defaultBranch <name>
> hint:
> hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
> hint: 'development'. The just-created branch can be renamed via this command:
> hint:
> hint: git branch -m <name>
> Initialized empty Git repository in /home/chrisdeng/GIT_TEST/.git/
> $ ls -a
> . .. .git
> ```
2. 新增檔案 `.git/hooks/pre-commit`。範本參考: [git-pre-commit-cppcheck](<https://github.com/danmar/cppcheck/blob/main/tools/git-pre-commit-cppcheck>)。\
**注意!** 下面內容與範本差異在 cppcheck 那行有新增 `--enable=all`。
```shell
vim .git/hooks/pre-commit
```
```bash
#!/bin/sh
# Usage: add this file to your project's .git/hooks directory. Rename it to
# just 'pre-commit'.
# Now, when you change some files in repository and try to commit these
# changes, git will run this script right before commit. Cppcheck will scan
# changed/new files in repository. If it finds some issues, script returns with
# exit code 1, rejecting commit. Otherwise, script returns 0, and you can
# actually commit your changes.
#
# Example:
# $ cat hello.c
# int main() {
# int *s = malloc(10);
# }
# $ git add hello.c
# $ git commit
# Checking hello.c...
# [hello.c:3]: (error) Memory leak: s
# [hello.c:2]: (error) The allocated size 10 is not a multiple of the underlying type's size.
#
# $ vim hello.c
# $ cat hello.c
# int main() {
# }
# $ git add hello.c
# $ git commit
# Checking hello.c...
# $
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# We should pass only added or modified C/C++ source files to cppcheck.
changed_files=$(git diff-index --cached $against | \
grep -E '[MA] .*\.(c|cpp|cc|cxx)$' | cut -f 2)
if [ -n "$changed_files" ]; then
cppcheck --enable=all --error-exitcode=1 $changed_files
exit $?
fi
exit 0
```
3. 改成可執行權限。
```shell
chmod 775 .git/hooks/pre-commit
```
4. 新增檢查錯誤的檔案。
```shell
vim err.c
```
```c
#include <stdio.h>
int main()
{
int i;
return 0;
}
```
5. 提交檔案失敗。
```shell
git add err.c
git commit -m "First commit"
```
> 這是控制台輸出:
> ```console=
> $ git add err.c
> $ git commit -m "First commit"
> Checking err.c ...
> err.c:5:9: style: Unused variable: i [unusedVariable]
> int i;
> ^
> nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingIncludeSystem]
> ```
6. 按照提示把錯誤修正。
```shell
vim err.c
```
```c
#include <stdio.h>
int main()
{
return 0;
}
```
7. 再次提交檔案成功。
```shell
git add err.c
git commit -m "First commit"
```
> 這是控制台輸出:
> ```console=
> $ git add err.c
> $ git commit -m "First commit"
> Checking err.c ...
> nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingIncludeSystem]
>
> [master (root-commit) ed2dc78] First commit
> 1 file changed, 6 insertions(+)
> create mode 100644 err.c
> ```
8. 確認提交是否成功。
```shell
git log -p
```
> 這是控制台輸出:
> ```console=
> $ git log -p
> commit ed2dc78e4f0bbe6ee2e93572f5cb82b88f91bd66 (HEAD -> master)
> Author: User <User@mail.com>
> Date: Fri Mar 7 16:47:24 2025 +0800
>
> First commit
>
> diff --git a/err.c b/err.c
> new file mode 100644
> index 0000000..c5d6809
> --- /dev/null
> +++ b/err.c
> @@ -0,0 +1,6 @@
> +#include <stdio.h>
> +
> +int main()
> +{
> + return 0;
> +}
> ```
#### 延伸
可以在 Git 中設定 `global hooks` 目錄,而不需要在每個專案中單獨配置。利用 Git 指令:
```shell
git config --global core.hooksPath /path/to/global/hooks
```
> 將 /path/to/global/hooks 替換為你希望存放 `global hooks` 的目錄路徑。
> > Git 會自動在所有專案中使用這個路徑下的 pre-commit。在任何 Git 專案中執行 `git commit`,該 pre-commit 都應該會被執行。
> > 如果個別專案需要自定義,可以在該專案的 `.git/hooks` 目錄中配置自己的 pre-commit,這樣會覆蓋 `global hooks` 的 pre-commit。
### 參考
- [什麼是 Git Hooks?為什麼它這麼萬能?](<https://yhtechnote.com/git-hooks/>)
- [如何在執行git commit前自動進行檢查?Git Hooks的基本用法](<https://magiclen.org/git-hooks/>)
- [git-pre-commit-cppcheck](<https://github.com/danmar/cppcheck/blob/main/tools/git-pre-commit-cppcheck>)