Hướng dẫn sử dụng git merge-base: Tìm commit tổ tiên chung để diff chính xác trong CI/CD

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

Nỗi ám ảnh mang tên “chạy test thừa” trong CI/CD

Một dự án monorepo hoặc backend cỡ vừa thường có 3.000 đến 5.000 file mã nguồn. Chạy linter và test toàn bộ repo mỗi lần mở Pull Request (PR) dễ ngốn từ 15 đến 20 phút. Con số này làm nghẽn hàng đợi CI và khiến cả team sốt ruột. Giải pháp tự nhiên nhất: chỉ quét đúng những file vừa thêm mới hoặc chỉnh sửa trong PR.

Vấn đề nằm ở khâu so sánh. Làm sao runner biết chính xác bạn đã sửa những file nào so với nhánh main? Chọn sai mốc commit, pipeline sẽ bỏ sót lỗi nghiêm trọng. Tệ hơn, bạn có thể bị phạt oan vì linter báo đỏ trên những file đồng nghiệp vừa merge cách đó 5 phút.

Ba cách lấy danh sách file thay đổi: Đâu là giải pháp chuẩn?

Các lập trình viên thường bắt đầu với 3 cách tiếp cận dưới đây khi viết script lọc file trên CI.

Approach 1: So sánh với commit liền trước (HEAD~1)

Cách trực quan nhất là chỉ kiểm tra commit cuối cùng vừa push lên nhánh:

git diff --name-only HEAD~1 HEAD

Approach 2: So sánh trực tiếp hai đầu nhánh (Double-dot syntax)

Nhiều bạn dùng ngay cú pháp hai dấu chấm để đối chiếu feature branch với main:

git diff --name-only origin/main..HEAD

Approach 3: Tìm commit tổ tiên chung bằng git merge-base

Lệnh git merge-base xác định commit tổ tiên chung gần nhất (best common ancestor) giữa hai nhánh. Từ điểm mốc lịch sử này, chúng ta so sánh tới đỉnh nhánh hiện tại:

BASE_COMMIT=$(git merge-base origin/main HEAD)
git diff --name-only $BASE_COMMIT HEAD

Git hỗ trợ viết tắt logic này bằng cú pháp 3 dấu chấm tiện lợi:

git diff --name-only origin/main...HEAD

Mổ xẻ ưu nhược điểm từng cách tiếp cận

Approach 1 (HEAD~1): Nhanh nhưng sai logic cơ bản

  • Ưu điểm: Tốc độ cực nhanh. Runner không cần tải lịch sử của nhánh nào khác.
  • Nhược điểm: Sai ngay khi nhánh tính năng có từ 2 commit trở lên. Đẩy 4 commit lên PR? CI chỉ quét file ở commit thứ tư và bỏ lọt hoàn toàn 3 commit trước đó.

Approach 2 (origin/main..HEAD): Chiếc bẫy phổ biến

  • Ưu điểm: Lệnh ngắn, dễ gõ.
  • Nhược điểm: Dễ gây lỗi dây chuyền. Bạn tách nhánh từ main sáng thứ Hai. Đến chiều, đồng nghiệp merge 8 PR khác vào main. Cú pháp origin/main..HEAD sẽ tính cả những thay đổi trên main mà nhánh bạn chưa có. Hậu quả: CI lôi hàng chục file của người khác ra bắt bạn sửa lỗi cú pháp.

Approach 3 (git merge-base): Chuẩn xác tuyệt đối

  • Ưu điểm: Lọc đúng 100% thay đổi do bạn tạo ra kể từ thời điểm phân nhánh. main có nhận thêm bao nhiêu commit cũng không ảnh hưởng. Đây chính là cơ chế GitHub và GitLab dùng để render tab “Files changed”.
  • Nhược điểm: Runner cần đủ lịch sử commit. Shallow clone mặc định có thể làm lệnh báo lỗi nếu thiếu cấu hình.

Hiệu quả thực tế khi áp dụng git merge-base

Áp dụng cơ chế này cho team 8 kỹ sư, pipeline test của team mình giảm thời gian chờ từ 18 phút xuống còn chưa đầy 3 phút mỗi lượt commit. Tiết kiệm đáng kể chi phí runner trên AWS và GitHub Actions.

Hiện tượng dev kêu ca: “Em không hề sửa file này, sao CI bắt em fix format?” cũng biến mất hoàn toàn. Để pipeline phản ánh đúng phạm vi công việc, git merge-base là lựa chọn an toàn duy nhất.

Các bước triển khai chi tiết vào CI runner

Bước 1: Cơ chế tìm mốc qua đồ thị commit DAG

Quan sát cây commit trực quan dưới đây:

      B---C (feature - HEAD)
     /
A---M1---M2 (origin/main)

Trong đó:

  • A là commit gốc nơi bạn tách nhánh feature.
  • M1, M2 là các commit mới trên main do đồng nghiệp merge vào.
  • B, C là những commit bạn vừa code.

Khi thực thi lệnh sau:

git merge-base origin/main HEAD

Git duyệt ngược đồ thị định hướng DAG và trả về mã hash của commit A. Lệnh diff giữa A và C (HEAD) sẽ chỉ trả về những file được chỉnh sửa trong B và C.

Bước 2: Xử lý lỗi Shallow Clone trên CI Runner

Các CI engine như GitHub Actions hay GitLab CI mặc định shallow clone với depth: 1 nhằm tiết kiệm băng thông mạng. Runner lúc này không chứa commit A, khiến git merge-base văng lỗi fatal: Not a valid object name.

Khắc phục bằng cách tải thêm lịch sử nhánh target trước khi tính diff:

# Cách 1: Tải thêm commit của nhánh main
git fetch origin main --depth=100

# Cách 2: Chuyển sang full history nếu repo dung lượng vừa phải
git fetch --unshallow || true

Bước 3: Viết script lọc file thay đổi tự động

Tạo file shell script get_changed_files.sh với nội dung sau:

#!/usr/bin/env bash
set -euo pipefail

TARGET_BRANCH="origin/main"

# Xác định mốc commit chung gần nhất
BASE_COMMIT=$(git merge-base "$TARGET_BRANCH" HEAD)
echo "Base commit xác định được: $BASE_COMMIT"

# Lọc file sửa đổi hoặc thêm mới, bỏ qua file đã xóa (--diff-filter=d)
CHANGED_FILES=$(git diff --name-only --diff-filter=d "$BASE_COMMIT" HEAD)

if [ -z "$CHANGED_FILES" ]; then
  echo "Không phát hiện file thay đổi."
  exit 0
fi

echo "Danh sách file cần kiểm tra:"
echo "$CHANGED_FILES"

# Ví dụ: Chỉ chạy linter flake8 trên các file Python có thay đổi
PYTHON_FILES=$(echo "$CHANGED_FILES" | grep -E '\.py$' || true)
if [ -n "$PYTHON_FILES" ]; then
  echo "Bắt đầu lint file Python..."
  echo "$PYTHON_FILES" | xargs flake8
fi

Bước 4: Nhúng vào CI pipeline cho dự án Monorepo

Mẫu cấu hình chạy kiểm thử theo từng microservice bị ảnh hưởng:

# 1. Đồng bộ reference nhánh main
git fetch origin main:refs/remotes/origin/main

# 2. Lấy hash tổ tiên chung
MERGE_BASE=$(git merge-base origin/main HEAD)

# 3. Quét các folder module cấp 1 có chứa file thay đổi
CHANGED_DIRS=$(git diff --name-only "$MERGE_BASE" HEAD | cut -d/ -f1 | sort -u)
for dir in $CHANGED_DIRS; do
  if [ -f "$dir/package.json" ]; then
    echo "Kích hoạt test cho service: $dir"
    (cd "$dir" && npm test)
  fi
done

Hiểu rõ git merge-base giúp bạn làm chủ pipeline CI/CD, tiết kiệm tài nguyên máy chủ và tránh phụ thuộc vào các plugin ngoài thiếu ổn định.

Share: