Làm chủ git for-each-ref: Lọc và định dạng Branch, Tag chuẩn xác cho Automation

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

Cơn ác mộng dọn dẹp Git repo trên hệ thống CI/CD

Khoảng nửa năm trước, Git server nội bộ của team mình bắt đầu kêu cứu. Repository chính phình to với hơn 800 remote branches và gần 2.500 tags tích tụ suốt 3 năm. Mình dựng ngay một cronjob bash script dọn dẹp. Mục tiêu khá đơn giản: tự động tìm và xóa feature branch không có commit nào trong 60 ngày.

Theo phản xạ, mình dùng git branch -r kết hợp grep, awk và cut. Tuần đầu êm đẹp. Sang tuần thứ hai, sự cố nổ ra.

Một dev đặt tên branch chứa dấu cách và ký tự #. Lệnh cut lấy lệch vị trí chuỗi. Tệ hơn, output từ terminal còn dính mã màu ANSI. Script hiểu sai tên branch, dẫn đến việc xóa nhầm một nhánh release candidate đang chuẩn bị deploy. Cú sập đó khiến cả team toát mồ hôi khôi phục reflog suốt 3 tiếng.

Parse text thủ công từ Git bằng bash pipe rất mong manh. Cấu trúc output đổi nhẹ giữa các OS là script gãy ngay lập tức.

Vì sao git branch và git tag không sinh ra để làm script?

Trong kiến trúc Git, các lệnh chia thành hai nhóm rõ rệt: Porcelain (lệnh giao diện cho người dùng gõ tay) và Plumbing (lệnh cấp thấp chuyên phục vụ công cụ tự động hóa).

Lệnh Porcelain sinh ra cho mắt người, không phải cho máy

Những lệnh quen thuộc như git branch hay git tag thuộc nhóm Porcelain. Chúng ưu tiên trải nghiệm nhìn:

  • Tự động gắn dấu hoa thị * và khoảng trắng trước branch đang checkout.
  • Chèn mã màu terminal gây nhiễu dữ liệu thô khi pipe qua công cụ khác.
  • Không có cơ chế truy vấn trực tiếp vào metadata như committer date, upstream tracking hay author email.

Cố parse output của Porcelain bằng regex chẳng khác nào dựng nhà trên cát.

Hai giải pháp chắp vá dev hay dùng (và lý do nên bỏ)

1. Kết hợp git branch với awk và sed

Đoạn script kinh điển mà nhiều anh em mới viết bash thường dùng:

git branch --sort=-committerdate | head -n 10 | awk '{print $1}'

Vấn đề nằm ngay dòng đầu tiên. Nếu bạn đang đứng ở branch đó, $1 sẽ trả về ký tự * thay vì tên nhánh. Lệnh chạy tiếp theo chắc chắn văng lỗi. Bạn lại phải nhồi thêm sed 's/^[* ]*//' để xử lý. Vừa cồng kềnh, vừa dễ sót case.

2. Vòng lặp duyệt git log từng ref

Một số dev chuyển sang đọc log từng branch trong vòng lặp:

for ref in $(git rev-parse --symbolic --branches); do
    last_date=$(git log -1 --format="%cd" --date=short "$ref")
    echo "$ref: $last_date"
done

Cách này đúng logic nhưng cực kỳ chậm. Với 800 branches, Git phải spawn 800 sub-process riêng biệt. Script mất hơn 45 giây chỉ để đọc ngày commit cuối cùng.

Giải pháp chuẩn chỉ: git for-each-ref

git for-each-ref là lệnh Plumbing sinh ra để giải quyết triệt để bài toán này. Nó truy xuất trực tiếp các ref object nhị phân dưới ổ cứng mà không qua lớp hiển thị UI. Cùng repo 800 branches đó, lệnh chạy chưa tới 0.05 giây.

Cấu trúc đường dẫn ref trong Git

Mọi con trỏ trong repository đều nằm gọn trong thư mục refs/:

  • Branch local: refs/heads/
  • Branch remote: refs/remotes/
  • Release tag: refs/tags/

1. Lấy danh sách tên branch sạch sẽ 100%

Để lấy danh sách tên local branch mà không lo dính dấu sao hay khoảng trắng:

git for-each-ref --format='%(refname:short)' refs/heads/

Modifier :short tự bóc tách tiền tố refs/heads/. Kết quả trả về là chuỗi thuần túy, an toàn tuyệt đối khi đưa vào bash array.

2. Sắp xếp branch theo thời gian commit mới nhất

Cần tìm branch bị bỏ quên? Hãy tận dụng tham số --sort:

git for-each-ref --sort=-committerdate \
  --format='%(committerdate:short) | %(refname:short) | %(authorname)' refs/heads/

Dấu trừ - trước committerdate giúp đẩy commit mới nhất lên đầu danh sách.

3. Bóc tách metadata từ Tag

Với annotated tag, bạn dễ dàng đọc cả ngày gắn tag lẫn commit message liên quan:

git for-each-ref --sort=-creatordate \
  --format='%(refname:short) | %(creatordate:iso8601) | %(contents:subject)' refs/tags/

4. Script thực chiến: Xuất JSON branch cũ để dọn dẹp CI/CD

Dưới đây là bash script mình đang chạy trên pipeline GitLab CI. Script xuất danh sách các remote branch không hoạt động trên 30 ngày thành file JSON chuẩn mà không cần cài thêm công cụ bên ngoài:

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

THIRTY_DAYS_AGO=$(date -d "30 days ago" +%s)
first_item=true

echo "["

# Dùng Tab (%09) làm delimiter an toàn
git for-each-ref --format='%(refname:short)%09%(committerdate:raw)' refs/remotes/origin/ | while IFS=$'\t' read -r branch_name date_raw; do
    [[ "$branch_name" == "origin/HEAD" ]] && continue

    # Trích xuất unix timestamp (date_raw có format: 1715000000 +0700)
    commit_timestamp="${date_raw%% *}"

    if [ "$commit_timestamp" -lt "$THIRTY_DAYS_AGO" ]; then
        [ "$first_item" = false ] && echo ","
        printf '  {"branch": "%s", "last_commit_epoch": %d}' "$branch_name" "$commit_timestamp"
        first_item=false
    fi
done

echo -e "\n]"

Ký tự %09 (Tab) đóng vai trò làm delimiter chuẩn. Dù tên branch hay tên author có chứa dấu cách, bash vẫn đọc chính xác từng biến. Chuyển sang git for-each-ref không chỉ cứu pipeline khỏi các lỗi vặt ngớ ngẩn mà còn tăng tốc độ thực thi lên gấp nhiều lần.

Share: