Bối cảnh: Tại sao team mình phải dùng Pre-commit
Hồi team mình còn 4–5 người, code review khá thoải mái — ai cũng quen nhau, biết nhau code kiểu gì. Nhưng khi lên 8 người với 3 senior, 2 mid-level và 3 junior mới vào, mọi thứ bắt đầu lộn xộn. PR nào cũng có comment kiểu “thiếu newline cuối file”, “indent tab thay vì space”, “import chưa sắp xếp”. Reviewer mất thời gian, người được review thì frustrated vì bị comment những thứ lẽ ra tool phải bắt tự động.
Mình quyết định áp dụng pre-commit framework sau khi thấy một repo open source dùng nó. Kết quả: số lượng comment style/format trong PR giảm xuống gần như về 0, reviewer tập trung được vào logic thay vì dấu phẩy thừa. Trong team 8 người, mình áp dụng pre-commit song song với Git flow đã dùng — và số merge conflict cũng giảm hẳn vì code nhất quán hơn, ít xung đột format hơn.
Pre-commit là framework quản lý Git hooks — cụ thể là hook pre-commit chạy ngay trước khi lệnh git commit tạo commit. Nếu có hook nào fail, commit bị chặn lại. Điểm khác biệt so với viết hook thủ công là pre-commit quản lý dependencies của từng hook riêng biệt, không đụng vào môi trường Python hay Node của project — mỗi hook chạy trong virtualenv cách ly của nó.
Cài đặt Pre-commit
Yêu cầu
Pre-commit yêu cầu Python 3.8+ và pip. Kiểm tra trước:
python --version # Python 3.8.x trở lên
pip --version
Cài đặt global hoặc trong virtualenv
# Cài global
pip install pre-commit
# Hoặc với pipx (isolated, clean hơn — mình dùng cái này)
pipx install pre-commit
# Kiểm tra version
pre-commit --version
# pre-commit 3.7.x
Kích hoạt hooks cho repo
Sau khi cài xong, vào thư mục repo và chạy một lệnh duy nhất:
cd /path/to/your/repo
pre-commit install
Lệnh này tạo file .git/hooks/pre-commit — từ giờ mỗi lần git commit, pre-commit tự chạy. Nếu muốn chặn cả git push, thêm:
pre-commit install --hook-type pre-push
Cấu hình chi tiết với .pre-commit-config.yaml
Toàn bộ cấu hình nằm trong file .pre-commit-config.yaml ở root repo. File này commit vào git, nên mọi người trong team tự động dùng chung config — không cần mỗi người tự setup.
Cấu hình cơ bản cho project Python
# .pre-commit-config.yaml
repos:
# Các hook cơ bản từ pre-commit-hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace # Xóa khoảng trắng thừa cuối dòng
- id: end-of-file-fixer # Đảm bảo file kết thúc bằng newline
- id: check-yaml # Kiểm tra cú pháp YAML
- id: check-json # Kiểm tra cú pháp JSON
- id: check-merge-conflict # Phát hiện conflict markers còn sót
- id: check-added-large-files # Chặn file quá lớn (tránh commit nhầm binary)
args: ['--maxkb=500']
- id: debug-statements # Phát hiện print(), breakpoint() còn sót
# Black — code formatter cho Python
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
language_version: python3
# isort — sắp xếp import statements
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black"] # Compatible với Black
# Flake8 — linter phát hiện lỗi logic và style
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
args: ['--max-line-length=88'] # Match với Black default
Thêm hooks cho JavaScript/TypeScript
Project mix Python + JS (API backend + frontend) thì thêm vào phần repos:
# Prettier — format JS/TS/CSS/JSON
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, typescript, css, json]
# ESLint
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.4.0
hooks:
- id: eslint
files: \.(js|ts|jsx|tsx)$
types: [file]
Hook local — script tự viết
Đây là phần mình thấy mạnh nhất: viết hook riêng không cần publish lên đâu, chỉ cần để trong repo:
# Hook local — chạy script trong repo
- repo: local
hooks:
- id: no-leftover-todos
name: Check for leftover TODO/FIXME in staged files
entry: grep -n "TODO\|FIXME\|HACK"
language: system
types: [python]
pass_filenames: true
- id: run-unit-tests
name: Run unit tests
entry: python -m pytest tests/unit -q --tb=short
language: system
pass_filenames: false
stages: [pre-push] # Chỉ chạy khi push, không phải mỗi commit
Lưu ý stages: [pre-push] — test nặng để ở pre-push, lint nhẹ để ở pre-commit để không làm chậm workflow commit hằng ngày.
Tự động cập nhật version hooks
Sau khi viết config xong, chạy lệnh này để update rev của tất cả hooks lên bản mới nhất:
pre-commit autoupdate
Mình đặt lệnh này vào lịch chạy mỗi tháng một lần — tránh dùng hooks cũ có security issue.
Kiểm tra và Monitoring
Chạy thủ công trước khi commit
Muốn kiểm tra toàn bộ codebase mà không cần tạo commit giả:
# Chạy tất cả hooks trên tất cả file
pre-commit run --all-files
# Chạy chỉ một hook cụ thể
pre-commit run black --all-files
pre-commit run flake8 --all-files
# Chạy trên file cụ thể
pre-commit run --files src/main.py src/utils.py
Lần đầu chạy, pre-commit tải về và cài môi trường riêng cho từng hook — mất khoảng 1–2 phút. Từ lần sau, cache lại nên chạy ngay.
Đọc output khi hook fail
Đây là output thực tế khi commit bị chặn:
$ git commit -m "Add user authentication"
[INFO] Stashing unstaged changes to tracked files.
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check for merge conflicts................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook
reformatted src/auth.py
isort....................................................................Failed
- hook id: isort
- files were modified by this hook
Fixing src/auth.py
Black và isort tự động sửa file — chỉ cần git add src/auth.py rồi commit lại. Với flake8 thì khác: nó báo lỗi nhưng không tự sửa, phải sửa tay theo hướng dẫn.
Bỏ qua hooks khi thực sự cần
# Bỏ qua tất cả hooks (hotfix khẩn cấp)
git commit --no-verify -m "hotfix: production down"
# Bỏ qua hook cụ thể
SKIP=flake8 git commit -m "WIP: will fix lint later"
Trong team mình có quy định: ai dùng --no-verify phải ghi rõ lý do trong commit message và tạo ticket để fix trong vòng 1 ngày. Không phải rule nghiêm ngặt, nhưng đủ để mọi người có trách nhiệm.
Tích hợp với CI/CD để đảm bảo không ai bỏ qua
Local hooks chỉ chạy trên máy dev — người khác vẫn có thể push mà không cài pre-commit. Thêm vào GitHub Actions để CI chặn luôn:
# .github/workflows/lint.yml
name: Lint Check
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/[email protected] # Action chính thức từ pre-commit
Action này tái sử dụng cache của pre-commit, thường chỉ mất 30–60 giây trong CI — nhẹ hơn nhiều so với chạy full test suite.
Onboard member mới
Sau khi member mới clone repo, họ chỉ cần chạy 2 lệnh:
pip install pre-commit
pre-commit install
Mình có thêm 2 lệnh này vào Makefile target setup và README để không ai bị quên. Một tip nữa: chạy pre-commit run --all-files ngay khi onboard — để member mới thấy trạng thái hiện tại của codebase và fix hết legacy lint ngay từ đầu, tránh tích lũy thêm.
Sau tháng đầu áp dụng cho cả team 8 người, CI fail do style giảm từ ~40% xuống dưới 5%. Quan trọng hơn, code review bắt đầu tập trung vào architecture và logic — những thứ thực sự cần con người đọc, thay vì dấu phẩy thừa mà tool có thể xử lý trong vài giây.

