快轉到主要內容
  1. 教學文章/

Pre-commit Hooks:讓壞 Code 連 Commit 的機會都沒有

·4 分鐘· loading · loading · ·
Python Pre-Commit Git Linter Code-Quality Ruff Mypy
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 21: 本文

一. 前言
#

你有沒有過這種經驗:信心滿滿地 git push,結果 CI 噴了一堆 linting 錯誤,只好再推一個 “fix lint” 的 commit?或是 code review 時被同事指出格式不統一,尷尬到想鑽地洞? 🫠

如果有個工具能在你 git commit 的那一瞬間,自動幫你檢查格式、跑 linter、甚至檢查有沒有不小心把密碼提交上去——那豈不是太完美了?

這就是 pre-commit 框架的用途!它利用 Git 的 hook 機制,在 commit 前自動執行你指定的檢查工具。今天拍拍君就來帶大家從零開始設定 pre-commit,打造一個銅牆鐵壁的開發工作流 💪

二. 什麼是 Git Hooks?
#

在介紹 pre-commit 框架之前,先來了解一下 Git hooks。

Git 在特定動作發生時,可以觸發自訂腳本,這些腳本就叫做 hooks。常見的有:

Hook 名稱 觸發時機 用途
pre-commit git commit 檢查程式碼品質
commit-msg 寫完 commit message 後 檢查 commit message 格式
pre-push git push 跑測試

手動管理這些 hooks 很麻煩——每個人要自己設定、不同專案格式不同、更新也是噩夢。所以 pre-commit 框架誕生了,它幫你統一管理所有 hooks!

三. 安裝
#

用 pip 或 uv 安裝都行:

# pip
pip install pre-commit

# 或者用 uv(推薦!)
uv tool install pre-commit

確認安裝成功:

pre-commit --version
# pre-commit 4.1.0

四. 基本設定
#

在你的專案根目錄建立 .pre-commit-config.yaml

# .pre-commit-config.yaml
repos:
  # 一些通用的基本檢查
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace       # 移除行尾空白
      - id: end-of-file-fixer         # 確保檔案結尾有換行
      - id: check-yaml                # 檢查 YAML 語法
      - id: check-added-large-files   # 防止提交超大檔案
      - id: check-merge-conflict      # 檢查是否有未解決的 merge conflict

然後安裝 hooks:

# 在專案目錄下執行
pre-commit install

這個指令會在 .git/hooks/pre-commit 建立一個腳本。之後每次 git commit,都會自動觸發檢查 ✅

來測試一下:

# 故意加一些行尾空白
echo "hello   " > test.txt
git add test.txt
git commit -m "test"

你會看到類似這樣的輸出:

Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
Check for merge conflicts................................................Passed

如果有問題,pre-commit 會自動幫你修復(像是移除行尾空白),然後告訴你 Failed。你只需要重新 git add 修復後的檔案,再 commit 一次就好。

五. 加入 Python 專用的 Hooks
#

光是基本檢查還不夠,讓我們加入 Python 開發者最愛的工具!

5.1 Ruff — 超快的 Linter + Formatter
#

如果你還沒用過 Ruff,可以參考拍拍君之前的文章 👉 Ruff:Python 最速 Linter

  # Ruff - 超快速 Python linter & formatter
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.9.6
    hooks:
      - id: ruff          # linting
        args: [--fix]     # 自動修復可修復的問題
      - id: ruff-format   # formatting(取代 black)

5.2 MyPy — 靜態型別檢查
#

  # MyPy - 型別檢查
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.15.0
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]  # 需要的 type stubs
        args: [--ignore-missing-imports]

💡 如果你的專案用了很多第三方套件,MyPy 可能會比較慢。可以考慮只在 CI 跑,不放在 pre-commit。

5.3 防止密碼洩漏
#

  # 偵測不小心提交的密碼、API key
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

第一次使用需要先建立 baseline:

detect-secrets scan > .secrets.baseline

六. 完整範例設定檔
#

這是拍拍君推薦的 Python 專案完整設定:

# .pre-commit-config.yaml
default_language_version:
  python: python3.12

repos:
  # 通用檢查
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: check-merge-conflict
      - id: debug-statements  # 檢查是否有遺留的 pdb/breakpoint

  # Ruff
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.9.6
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  # Commit message 格式檢查
  - repo: https://github.com/commitizen-tools/commitizen
    rev: v4.4.1
    hooks:
      - id: commitizen

七. 實用指令大全
#

手動跑所有檔案
#

# 對所有檔案執行(不只是 staged 的)
pre-commit run --all-files

只跑特定 hook
#

pre-commit run ruff --all-files
pre-commit run trailing-whitespace --all-files

更新所有 hooks 到最新版
#

pre-commit autoupdate

這會自動更新 .pre-commit-config.yaml 中的 rev 版本號 🔄

暫時跳過檢查
#

偶爾你可能真的需要跳過檢查(例如緊急 hotfix):

# 跳過所有 hooks
git commit --no-verify -m "emergency fix"

# 跳過特定 hook
SKIP=mypy git commit -m "skip type check this time"

⚠️ 不要養成跳過的習慣!這是緊急逃生口,不是常態。

清除快取
#

pre-commit clean   # 清除已下載的 hook 環境
pre-commit gc      # 垃圾回收

八. 搭配 CI 雙重保險
#

光靠本機 pre-commit 還是有漏洞——有人可能用 --no-verify 跳過。所以建議在 CI 也跑一次:

# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - uses: pre-commit/action@v3.0.1

這樣就算有人跳過本機的 pre-commit,CI 也會抓到問題 🛡️

九. 進階技巧
#

9.1 自訂 Local Hook
#

除了用別人的 repo,你也可以寫自己的 hook:

  - repo: local
    hooks:
      - id: check-todo
        name: Check for TODO comments
        entry: grep -rn "TODO" --include="*.py"
        language: system
        pass_filenames: false
        always_run: true

9.2 只檢查特定檔案
#

filesexclude 控制範圍:

      - id: mypy
        files: ^src/   # 只檢查 src/ 目錄下的檔案
        exclude: ^src/legacy/  # 排除 legacy 目錄

9.3 設定 stages
#

不同 hook 可以在不同階段執行:

      - id: pytest
        stages: [pre-push]  # 只在 push 前跑測試

記得也要安裝 pre-push hook:

pre-commit install --hook-type pre-push

結語
#

pre-commit 就像是你程式碼的守門員 🧤 ——在壞 code 進入 git 歷史之前,就把它擋下來。

拍拍君覺得,一個好的開發工作流應該是:

  1. 寫 code → 開心寫
  2. git add → 選好要提交的
  3. git commit → pre-commit 自動檢查 ✅
  4. git push → CI 再跑一次確認 ✅✅

設定一次,受益終生。趕快在你的專案裡加上 pre-commit 吧!🚀

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 21: 本文

相關文章

Ruff:用 Rust 寫的 Python Linter,快到你會懷疑人生
·4 分鐘· loading · loading
Python Ruff Linter Formatter Code-Quality
Polars:比 Pandas 快 10 倍的 DataFrame 新選擇
·6 分鐘· loading · loading
Python Polars Dataframe 資料分析 Rust
PyTorch 神經網路入門:從零開始建立你的第一個模型
·5 分鐘· loading · loading
Python Pytorch Neural-Network Deep-Learning Machine-Learning
少寫一半程式碼:dataclasses 讓你的 Python 類別煥然一新
·6 分鐘· loading · loading
Python Dataclasses Oop 標準庫
超快速 Python 套件管理:uv 完全教學
·6 分鐘· loading · loading
Python Uv Package Manager Rust
Python asyncio 非同步程式設計入門:讓你的程式不再傻等
·8 分鐘· loading · loading
Python Asyncio 非同步 並行