Cách dùng git subtree split tách thư mục con thành repo độc lập giữ trọn commit history

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

2 giờ sáng, pipeline CI/CD của team đỏ rực. Module thanh toán cũ nằm sâu trong monolith monorepo 15GB kéo theo cả đống dependency thừa, khiến thời gian build vọt từ 4 phút lên hơn 35 phút. Cách nhanh nhất để cứu production lúc này: bóc tách ngay packages/payment-core sang repository riêng để hotfix và release độc lập. Thách thức lớn nhất? Không được làm mất dù chỉ một dòng git blame hay lịch sử commit của 3 năm qua.

3 cách tách thư mục con trong Git và cái giá phải trả

Đứng trước bài toán này, anh em dev thường cân nhắc 3 hướng đi quen thuộc:

  • Cách 1: Copy-paste thủ công sang repo mới
    Tạo repo trắng, kéo thả thư mục packages/payment-core qua rồi chạy git init && git commit -m "Initial commit". Nhanh nhưng xóa sạch toàn bộ lịch sử.
  • Cách 2: Dùng git filter-branch hoặc git-filter-repo
    Viết lại toàn bộ commit tree. Công cụ này quét từng commit trong quá khứ để loại bỏ mọi file nằm ngoài thư mục đích.
  • Cách 3: Dùng git subtree split
    Lệnh tích hợp sẵn trong Git từ bản 1.7.0. Cơ chế này trích xuất riêng các commit liên quan đến thư mục chỉ định thành một nhánh tổng hợp (synthetic branch) hoàn toàn cô lập.

So sánh thực tế: Chọn giải pháp nào khi production gặp sự cố?

Mỗi phương án đều có ưu và nhược điểm rõ ràng:

1. Copy-paste thủ công

  • Ưu điểm: Xong trong 30 giây. Không lo gõ sai lệnh Git.
  • Nhược điểm: Mất sạch commit log, tác giả và ngày sửa. Khi phát sinh bug trừ tiền sai lúc nửa đêm, bạn không còn cách nào git blame để biết ai đã đổi điều kiện logic từ 6 tháng trước.

2. Dùng git filter-branch hoặc git-filter-repo

  • Ưu điểm: Dọn dẹp triệt để dung lượng repo đích. Commit hash mới rất sạch sẽ.
  • Nhược điểm: Cực kỳ rủi ro. git filter-branch sửa đổi commit hash trực tiếp. Nếu lỡ tay push --force lên remote nhầm branch, toàn bộ nhánh main của team coi như toang. Chưa kể, git-filter-repo là tool ngoài, cần cài thêm Python package và không phải server nào cũng có sẵn.

3. Dùng git subtree split

  • Ưu điểm: An toàn tuyệt đối. Lệnh chỉ đọc lịch sử để tạo một branch phụ, không ghi đè hay biến đổi nhánh làm việc hiện tại. Tính năng có sẵn trong Git core, không cần cài thêm gì.
  • Nhược điểm: Tốc độ phụ thuộc vào số lượng commit của repo gốc. Với repo hơn 80.000 commit, quá trình quét có thể mất từ 2 đến 5 phút.

Vì sao git subtree split là lựa chọn an toàn nhất?

Khi xử lý sự cố gấp, hai yếu tố quan trọng nhất là an toàn dữ liệu và truy vết lỗi. git subtree split đáp ứng trọn vẹn cả hai: tạo repo mới với đầy đủ lịch sử commit, đồng thời giữ repo gốc nguyên vẹn 100%.

Thực tế khi áp dụng quy trình này cho team 10 người, chúng mình đã bóc tách 4 module dùng chung ra các micro-repo độc lập mà không gặp bất kỳ xung đột lịch sử nào.

Các bước thực hiện chi tiết

Giả sử chúng ta đang ở thư mục gốc của repo monorepo-core và cần tách packages/payment-core sang repo mới payment-service-repo.

Bước 1: Chuẩn bị working tree sạch

Hãy đảm bảo bạn đang ở branch mới nhất và không có uncommitted changes:

# Kiểm tra trạng thái
git status

# Kéo code mới nhất từ main
git checkout main
git pull origin main

Bước 2: Trích xuất lịch sử thành branch riêng

Chạy lệnh git subtree split với flag -P (prefix: đường dẫn thư mục) và -b (tên branch mới):

git subtree split -P packages/payment-core -b extract-payment-branch

Git sẽ quét toàn bộ cây commit. Những commit nào có tác động đến file trong packages/payment-core sẽ được trích xuất sang branch extract-payment-branch. Đồng thời, toàn bộ file trong thư mục này sẽ được đẩy lên làm thư mục gốc (root) của branch mới.

Bước 3: Đẩy code sang repository mới

Tạo một repository rỗng trên GitHub/GitLab (ví dụ: [email protected]:your-org/payment-service-repo.git). Bạn có 2 cách để push:

Cách 1: Push thẳng từ repo hiện tại (Nhanh nhất)

git push [email protected]:your-org/payment-service-repo.git extract-payment-branch:main

Lệnh này sẽ đẩy trực tiếp nhánh tạm lên làm branch main của repo mới.

Cách 2: Clone ra thư mục riêng để kiểm tra local trước

# Tạo thư mục mới ngoài monorepo
cd ..
mkdir payment-service-repo
cd payment-service-repo

# Khởi tạo repo và kéo branch đã tách về
git init
git pull /path/to/monorepo-core extract-payment-branch

# Gắn remote mới và push
git remote add origin [email protected]:your-org/payment-service-repo.git
git branch -M main
git push -u origin main

Bước 4: Xác thực lại lịch sử và git blame

Mở repo mới kiểm tra lại log xem các commit cũ có còn nguyên tác giả hay không:

# Xem 10 commit gần nhất
git log --oneline -n 10

# Soi blame trên một file quan trọng
git blame src/gateway.js

Kết quả: toàn bộ commit log, timestamp và author email của module thanh toán đều được giữ nguyên vẹn. Các file thừa ngoài monolith đã được loại bỏ hoàn toàn.

Bước 5: Dọn dẹp nhánh tạm ở repo gốc

Sau khi repo mới đã chạy ổn định, bạn quay lại repo gốc và xóa branch tạm để tránh rác workspace:

cd /path/to/monorepo-core
git branch -D extract-payment-branch

Giờ đây, bạn có thể xóa thư mục packages/payment-core trong repo cũ hoặc chuyển sang quản lý dưới dạng package riêng qua npm/composer mà không lo mất lịch sử phát triển.

Share: