Bối cảnh: Khi nào Git Workflow thực sự cần thiết?
Hồi code một mình, mình cũng chỉ dùng mỗi nhánh main, commit kiểu “fix”, “update”, “thử lại” — chẳng ai phàn nàn cả. Nhưng khi vào team 3 người, mọi thứ bắt đầu lộn xộn: code đè lên nhau, không biết ai sửa cái gì, deploy xong hỏng feature người khác vừa hoàn thành sáng đó.
Vấn đề không phải là team làm việc kém — chỉ là thiếu quy ước chung. Không có workflow, mỗi người dùng Git theo cách của mình, và “tai nạn” là điều sớm muộn xảy ra.
Mình từng mất code quan trọng vì force push nhầm branch — push lên main thay vì feature branch, xóa sạch 2 ngày làm việc của đồng nghiệp. Từ đó mình rất cẩn thận với git push --force và đặt ra một số quy tắc cứng cho team. Bài này chia sẻ cách mình đang áp dụng — đủ gọn để không tốn công maintain, đủ chặt để không gặp rắc rối khi làm nhóm.
Cài đặt: Chuẩn bị môi trường Git trước khi bắt đầu
Config Git user và editor
Bước đầu tiên — và hay bị bỏ qua nhất — là cấu hình đúng thông tin user. Thiếu cái này, commit của bạn sẽ hiển thị tên lạ hoặc email sai, rất khó trace sau này:
git config --global user.name "Tên Của Bạn"
git config --global user.email "[email protected]"
git config --global core.editor "nano" # hoặc vim, code --wait
git config --global init.defaultBranch main
Nếu làm việc với nhiều repo và nhiều tài khoản khác nhau (cá nhân + công ty), dùng config local thay vì global để tránh nhầm:
# Chạy trong thư mục repo công ty — chỉ áp dụng cho repo đó
git config user.email "[email protected]"
Alias tiết kiệm thao tác hàng ngày
Mấy cái alias này mình gõ hàng ngày, tiết kiệm kha khá thao tác:
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.undo "reset HEAD~1 --mixed"
Sau khi set, git lg sẽ hiển thị lịch sử commit theo dạng cây — rất tiện để xem nhánh hiện tại đang ở đâu so với main.
Khởi tạo repo với .gitignore đúng ngay từ đầu
git init
cat > .gitignore << EOF
node_modules/
.env
*.log
dist/
__pycache__/
.DS_Store
EOF
git add .gitignore
git commit -m "chore: init project structure"
Cấu hình chi tiết: Workflow thực tế cho cá nhân và team nhỏ
Branching strategy: Đơn giản nhưng đủ dùng
Với team dưới 5 người, mình thấy mô hình này hoạt động tốt nhất — không quá phức tạp như GitFlow đầy đủ, nhưng cũng không quá lỏng lẻo:
- main — nhánh production, chỉ nhận code qua Pull Request, không commit trực tiếp
- develop — nhánh tích hợp, test trước khi merge vào main
- feature/tên-tính-năng — nhánh phát triển từng tính năng mới
- hotfix/tên-lỗi — nhánh vá lỗi khẩn cấp tách từ main
Còn nếu bạn làm side project một mình? Chỉ cần main + feature/* là đủ. Không cần develop khi chỉ có một người.
# Tạo feature branch từ develop
git checkout develop
git pull origin develop
git checkout -b feature/them-tinh-nang-login
# Làm việc, commit từng bước nhỏ...
git add src/login.py
git commit -m "feat: add login form with email validation"
git add tests/test_login.py
git commit -m "test: add unit tests for login validation"
# Khi xong, push lên remote để tạo PR
git push origin feature/them-tinh-nang-login
Commit convention: Quy ước bắt buộc phải đồng nhất trong team
Mình dùng Conventional Commits — format đơn giản, đọc lịch sử dễ hiểu, tự động generate changelog được:
# Format: <type>(<scope>): <description>
git commit -m "feat: add user authentication via JWT"
git commit -m "fix(api): handle null response from payment gateway"
git commit -m "docs: update README with docker setup guide"
git commit -m "refactor: extract email validator to separate module"
git commit -m "chore: upgrade dependencies to latest stable"
Các type hay dùng nhất:
feat— tính năng mớifix— sửa bugdocs— chỉ thay đổi tài liệuchore— task phụ: update deps, config CI…refactor— refactor không thêm tính năng, không sửa bugtest— thêm hoặc sửa test
3 tháng sau nhìn lại lịch sử, bạn sẽ biết ngay commit nào làm gì — không phải ngồi mở từng file ra đoán. Đây là thứ mình thấy rõ giá trị nhất của commit convention.
Pull Request và Code Review trong team nhỏ
Ngay cả team 2 người cũng nên dùng PR — không phải vì nghi ngờ nhau. Chỉ đơn giản là: khi review code người khác, mình hay phát hiện bug mà chính người viết không nhìn ra, vì họ đã nhìn vào đoạn đó quá lâu rồi.
Quy tắc PR mình thống nhất với team:
- PR phải có description ngắn: làm gì, tại sao, cách test thủ công
- Mỗi PR nên nhỏ — dưới 300 dòng thay đổi là lý tưởng, dễ review hơn nhiều
- Ít nhất 1 người approve trước khi merge vào
main - Tuyệt đối không force push lên
mainhaydevelop
Về điểm cuối — mình đã học bài học này theo cách khó nhất. Force push vào main có thể xóa sạch lịch sử commit của cả team. Giờ mình luôn bật Branch Protection trên GitHub cho các nhánh quan trọng:
# Trên GitHub: Settings → Branches → Add branch protection rule
# Branch name pattern: main
# ✅ Require a pull request before merging
# ✅ Require approvals: 1
# ✅ Do not allow bypassing the above settings
Rebase hay Merge? Dùng đúng chỗ
Rebase vs merge — tranh luận quen thuộc trong mọi team. Cách mình phân chia đơn giản:
- Dùng rebase khi cập nhật feature branch với code mới từ develop — lịch sử tuyến tính, dễ đọc hơn
- Dùng merge khi merge feature vào develop/main — giữ nguyên context, dễ rollback cả feature
# Cập nhật feature branch với develop mới nhất (rebase)
git checkout feature/them-tinh-nang-login
git fetch origin
git rebase origin/develop
# Nếu có conflict, giải quyết rồi tiếp tục
git rebase --continue
# Merge feature vào develop sau khi PR được approve
git checkout develop
git merge --no-ff feature/them-tinh-nang-login -m "merge: add login feature (#42)"
Option --no-ff (no fast-forward) tạo merge commit riêng, giúp dễ revert cả feature nếu phát hiện vấn đề sau khi merge.
Kiểm tra & Monitoring: Theo dõi workflow hoạt động đúng không
Xem lịch sử dạng graph
git lg
# Output mẫu:
# * a3f2c1b (HEAD -> feature/login) feat: add form validation
# * 8d9e4f0 feat: add login form UI
# | * c4b1a2e (origin/develop) fix: resolve null pointer in API
# |/
# * 7f3d2c1 (develop) chore: update dependencies
Nhìn vào đây biết ngay feature branch đang sau develop 1 commit — cần rebase trước khi tạo PR.
Thống kê và audit định kỳ
Trước mỗi release, mình thường chạy mấy lệnh này để kiểm tra lại:
# Ai commit gì trong tuần vừa rồi
git shortlog -sn --since="1 week ago"
# Xem diff giữa 2 phiên bản
git diff v1.0.0..v1.1.0 --stat
# Tìm commit nào thêm/xóa một đoạn code cụ thể
git log -S "function handleLogin" --oneline
# Xem file nào hay được thay đổi nhất
git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head -10
Đặt tag cho mỗi release
Bước này nhiều team bỏ qua — cho đến khi cần rollback khẩn cấp lúc 11 giờ đêm và không biết trỏ về commit nào:
# Tạo annotated tag với message mô tả
git tag -a v1.2.0 -m "Release v1.2.0 - add login feature, fix payment bug"
git push origin v1.2.0
# Xem danh sách tag
git tag -l
# Rollback về tag cũ nếu cần khẩn cấp
git checkout v1.1.0
Bảo vệ branch khỏi force push trên self-hosted Git
Nếu tự host Gitea hoặc GitLab, có thể dùng server-side hook để block force push:
# File: /path/to/repo.git/hooks/pre-receive
#!/bin/bash
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/main" ]; then
# Kiểm tra có phải force push không
if git rev-list $newrev..$oldrev | grep -q "."; then
echo "ERROR: Force push to main is not allowed!"
exit 1
fi
fi
done
chmod +x /path/to/repo.git/hooks/pre-receive
Tóm lại
Workflow tốt không đồng nghĩa với phức tạp. Với cá nhân hoặc team nhỏ dưới 5 người, nhất quán trong 4 điểm sau là đủ:
- Cấu trúc branch rõ ràng (main / develop / feature)
- Commit message có nghĩa, theo convention
- Review code qua PR — dù chỉ 2 người
- Bảo vệ nhánh quan trọng khỏi force push
Mình mất code một lần mới thấm được giá trị của những quy tắc tưởng như phiền phức này. Bắt đầu từ project nhỏ tiếp theo — setup workflow đúng từ đầu sẽ tiết kiệm rất nhiều đau đầu về sau.

