Cấu hình Git Content Filters (Smudge và Clean): Tự động biến đổi file khi Checkout và Commit

1. Cơn đau đầu quen thuộc: File cấu hình dở dở ương ương

Dân dev hầu như ai cũng từng dính quả này: một file cấu hình như config.json, settings.php hay appsettings.json buộc phải nằm trên Git để chia sẻ cấu trúc cho cả team. Thế nhưng mỗi người lại cần chuỗi kết nối database hay port chạy local riêng.

Thế là đủ rắc rối nảy sinh:

  • Thành viên mới vô tình đẩy luôn DB_PASSWORD hoặc API token cá nhân lên GitLab repo chung.
  • Cứ mỗi lần git pull về là file cấu hình bị ghi đè, vỡ nát môi trường local và mất 15–30 phút ngồi resolve conflict vô nghĩa.
  • Script CI/CD cần đóng dấu Git hash vào code trước khi deploy, nhưng commit riêng chỉ để đổi version thì bẩn commit log.

2. Vì sao Git mặc định không giải quyết được?

Git theo dõi thay đổi dưới dạng snapshot. Khi bạn gõ git add, Git đọc nguyên xi nội dung trong working directory và tạo blob object mới trong index.

Vấn đề nằm ở hai nhu cầu chọi nhau chan chát:

  • Dưới máy local: Ứng dụng cần dữ liệu thật để chạy. Ví dụ: kết nối tới localhost:5432 hoặc Stripe test key.
  • Trên remote repo: Bạn chỉ muốn lưu khuôn mẫu (template) chứa placeholder, chẳng hạn __DB_HOST__ hoặc __API_KEY__.

Hai trạng thái này lệch nhau ở từng lần commit.

3. Ba cách đối phó tạm thời và điểm nghẽn

Trước khi biết đến Content Filters, đa số team thường dùng 3 mẹo sau:

Dùng .env.example rồi gitignore .env

Đây là best practice cho biến môi trường. Nhưng nếu dự án dùng file XML, JSON cấu trúc phức tạp hay file code bắt buộc phải tracked trong repo thì bó tay. .gitignore chỉ bỏ qua toàn bộ file chứ không lọc được từng dòng.

Dùng lệnh git update-index --skip-worktree

Lệnh này bảo Git ngó lơ thay đổi local của file đã track. Nhược điểm? Khi remote có đồng đội sửa file đó, lệnh git pull sẽ báo lỗi xung đột hoặc xóa luôn thay đổi local của bạn.

Dùng pre-commit hook

Pre-commit chặn không cho push credential nhạy cảm. Nhưng nó chỉ biết báo lỗi và dừng commit. Bạn vẫn phải tự sửa tay rồi commit lại.

4. Giải pháp gốc rễ: Git Content Filters (Smudge & Clean)

Ít người để ý Git có sẵn cơ chế Content Filters thông qua file .gitattributes. Dữ liệu sẽ tự động đi qua hai phễu biến đổi:

  • Clean Filter: Kích hoạt khi chạy git add (từ working tree vào index). Bộ lọc này biến secret thật thành placeholder trước khi ghi vào commit history.
  • Smudge Filter: Kích hoạt khi chạy git checkout hoặc git switch (từ repo ra working tree). Bộ lọc tự lấy placeholder và thế bằng secret thật của máy bạn.

Áp dụng quy trình này cho team 8 dev backend, team mình dứt điểm hẳn cảnh commit nhầm credential suốt 6 tháng qua.

Bước 1: Khai báo filter trong .gitattributes

Tạo file .gitattributes ở thư mục gốc repo và commit file này lên Git:

# Gán filter có tên "secret_filter" cho file config.json
config.json filter=secret_filter

Bước 2: Cấu hình lệnh Clean và Smudge dưới local

Cấu hình này nằm trong .git/config của từng máy cá nhân, không commit lên repo.

Giả sử file config.json trên repo có placeholder:

{
  "api_endpoint": "https://api.example.com",
  "api_key": "__LOCAL_SECRET_KEY__"
}

Chạy hai lệnh config sau trên terminal máy bạn:

# 1. Smudge: Khi checkout, thay placeholder bằng key local
git config filter.secret_filter.smudge 'sed "s/__LOCAL_SECRET_KEY__/local-dev-token-999/g"'

# 2. Clean: Khi stage (git add), đổi key local ngược về placeholder
git config filter.secret_filter.clean 'sed "s/local-dev-token-999/__LOCAL_SECRET_KEY__/g"'

Bước 3: Tự động hóa bằng shell script đọc biến môi trường

Viết cứng token vào git config chưa tối ưu. Hãy để script đọc trực tiếp từ biến môi trường của hệ thống.

Tạo script smudge (.git-filters/smudge.sh):

#!/usr/bin/env bash
# Đọc từ stdin, thế placeholder bằng biến môi trường MY_API_KEY
KEY="${MY_API_KEY:-sandbox_default_key}"
sed "s|__LOCAL_SECRET_KEY__|$KEY|g"

Tạo script clean (.git-filters/clean.sh):

#!/usr/bin/env bash
# Làm sạch dữ liệu trước khi commit
KEY="${MY_API_KEY:-sandbox_default_key}"
sed "s|$KEY|__LOCAL_SECRET_KEY__|g"

Phân quyền thực thi và đăng ký vào Git config:

chmod +x .git-filters/smudge.sh .git-filters/clean.sh
git config filter.secret_filter.smudge .git-filters/smudge.sh
git config filter.secret_filter.clean .git-filters/clean.sh

Kiểm tra hoạt động thực tế

Kiểm tra xem bộ lọc đã chạy chuẩn chưa:

  1. Xóa file local rồi checkout lại: rm config.json && git checkout -- config.json. Mở file, bạn sẽ thấy __LOCAL_SECRET_KEY__ đã đổi thành giá trị biến môi trường.
  2. Sửa vài dòng code khác trong config.json rồi chạy git add config.json.
  3. Gõ git diff --staged: Git chỉ hiển thị các thay đổi hợp lệ, còn chuỗi secret vẫn giữ nguyên dạng __LOCAL_SECRET_KEY__.

Code chạy local vẫn đủ config thật. Repo chung vẫn sạch bong dữ liệu nhạy cảm.

Share: