Git Server Hooks: Cách mình “khóa” nhánh main và chặn đứng code rác cho team

Git tutorial - IT technology blog
Git tutorial - IT technology blog

Bài học đắt giá từ một cú “Enter” nhầm

Đêm thứ Sáu nửa năm trước, team mình suýt mất trắng công sức cả tuần chỉ vì một sai lầm sơ đẳng. Một bạn dev mới, do chưa quen xử lý xung đột, đã vô tình dùng git push --force thẳng vào nhánh main. Kết quả? 48 commit sạch đẹp biến mất trong 2 giây, thay vào đó là đống code dang dở của cá nhân bạn ấy.

Thực tế, chúng ta không thể quản lý dự án chỉ bằng niềm tin hay lời nhắc miệng. Trong một team 15-20 người, sai sót là điều tất yếu. Đó là lúc mình nhận ra: Phải có một chốt chặn cứng ngay trên server. Client-side hooks rất hay nhưng lại dễ bị bỏ qua bằng flag --no-verify. Chúng ta cần thứ gì đó quyền lực hơn.

Tại sao GitHub Actions hay CI/CD vẫn là chưa đủ?

Nhiều anh em sẽ thắc mắc: “Dùng GitHub Actions hay GitLab CI check lỗi là được rồi mà?”. Đúng, nhưng chúng vẫn có lỗ hổng lớn.

Thứ nhất, Client-side hooks nằm trong thư mục .git/hooks máy cá nhân. Chỉ cần thêm flag -n khi commit là mọi rào cản tan thành mây khói. Nó giống như việc bạn dặn bảo vệ khóa cửa, nhưng họ lại có quyền… lờ đi nếu thấy lười.

Thứ hai, CI/CD Pipelines thường chạy sau khi code đã lên server. Dù CI báo lỗi đỏ rực thì đống code “rác” đó đã nằm trong lịch sử Git rồi. Để dọn dẹp, bạn lại phải revert hoặc reset, tạo ra thêm những commit thừa thãi không đáng có.

Giải pháp triệt để là Server-side Hooks. Đây là những script chạy trực tiếp trên server chứa repo. Nếu script trả về lỗi (exit code khác 0), lệnh push bị từ chối ngay lập tức. Code lỗi thậm chí chưa kịp chạm vào ổ cứng server.

Tại sao quy tắc của team thường xuyên bị phá vỡ?

Sau khi quan sát cách anh em làm việc, mình rút ra 3 lý do khiến repo trở nên hỗn loạn:

  • Quyền hạn quá rộng: Ai cũng có thể đẩy code trực tiếp vào main hoặc develop.
  • Commit message tùy tiện: Người viết “fix bug”, người gõ “.”, khiến việc tra cứu sau này cực kỳ ức chế.
  • Quên chạy test local: Code lỗi logic vẫn được đẩy lên vì dev đang vội đi nhậu hoặc về sớm.

Cặp bài trùng: pre-receive và update hooks

Trên server Git, đây là hai “vệ binh” quan trọng nhất bạn cần nắm vững:

1. Pre-receive Hook

Script này chạy ngay khi server nhận được lệnh push. Nó chỉ kích hoạt một lần duy nhất cho mỗi đợt push, bất kể bạn đẩy lên bao nhiêu nhánh. Dữ liệu đầu vào từ stdin có dạng: <old-rev> <new-rev> <ref-name>.

Mình thường dùng nó để kiểm tra các quy tắc chung. Ví dụ: cấm push file nặng trên 5MB hoặc chặn các file nhạy cảm như .envnode_modules lọt vào repo.

2. Update Hook

Khác với cái trên, update hook chạy riêng cho từng nhánh. Nếu bạn push 3 nhánh cùng lúc, script sẽ chạy 3 lần. Đặc điểm này cực kỳ hữu ích để thiết lập luật riêng cho từng nhánh, chẳng hạn như cấm force push trên main nhưng thả cửa trên các nhánh feature/.

Triển khai thực tế trên Production

Giả sử bạn có một Bare Repository tại /home/git/project.git. Hãy vào thư mục hooks/ bên trong đó để bắt đầu.

Bước 1: Chặn Force Push nhánh quan trọng bằng update hook

Tạo file hooks/update và cấp quyền chmod +x cho nó:

#!/bin/bash

refname=$1
oldrev=$2
newrev=$3

# Chỉ bảo vệ nhánh main
if [ "$refname" == "refs/heads/main" ]; then
    # 1. Không cho phép xóa nhánh
    if [ "$newrev" == "0000000000000000000000000000000000000000" ]; then
        echo "[LỖI] Dừng lại! Nhánh main là bất khả xâm phạm."
        exit 1
    fi

    # 2. Chặn Force Push bằng cách check hậu duệ
    if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
        base=$(git merge-base $oldrev $newrev)
        if [ "$base" != "$oldrev" ]; then
            echo "[LỖI] Force push bị cấm trên main. Hãy pull và merge trước khi thử lại!"
            exit 1
        fi
    fi
fi

exit 0

Bước 2: Chuẩn hóa format commit bằng pre-receive hook

Để lịch sử Git trông chuyên nghiệp hơn, mình ép mọi người dùng prefix như FEAT:, FIX:, hoặc CHORE::

#!/bin/bash

while read oldrev newrev refname; do
    # Lấy danh sách commit mới (loại bỏ những gì đã có trên server)
    commits=$(git rev-list $oldrev..$newrev)

    for commit in $commits; do
        subject=$(git log -1 --format=%s $commit)
        if [[ ! $subject =~ ^(FEAT|FIX|CHORE|DOCS):\ .+ ]]; then
            echo "[LỖI] Commit message sai chuẩn: '$subject'"
            echo "Mẫu đúng: 'FEAT: Thêm tính năng thanh toán qua MoMo'"
            exit 1
        fi
    done
done

exit 0

Bí kíp kết hợp: Tốc độ là ưu tiên số một

Kinh nghiệm xương máu của mình: Tuyệt đối không chạy Unit Test trong Server Hooks. Tại sao ư? Vì hooks chạy đồng bộ. Nếu dev phải đợi 5 phút chỉ để biết lệnh push thành công hay không, họ sẽ rất ức chế và tìm cách lách luật ngay.

Chiến lược tối ưu:

  1. Server Hooks: Chỉ check những thứ cực nhanh (dưới 2 giây) như format commit, tên nhánh, dung lượng file.
  2. CI/CD (GitHub/GitLab): Dành cho việc nặng như chạy linter, test logic, build Docker image.

Thành quả sau 6 tháng áp dụng

Lúc mới triển khai, anh em cũng than phiền vì bị server “từ chối” liên tục. Nhưng chỉ sau một tháng, thói quen viết commit chỉn chu đã ngấm vào máu. Lịch sử Git giờ đây sạch sẽ như sách giáo khoa. Quan trọng nhất, mình không còn phải thức đêm để cứu dữ liệu do ai đó lỡ tay push force nữa.

Nếu bạn đang cầm trịch một dự án quan trọng, hãy dựng những “trạm kiểm soát” này ngay hôm nay. Nó không chỉ bảo vệ code, mà còn xây dựng tính kỷ luật cho team một cách tự nhiên nhất.

Share: