Sau Bài 9 đồng bộ giữa 1 local và 1 remote, bài này giải quyết bài toán nhúng nhiều repo lại với nhau: dự án chính cần dùng chung 1 thư viện nội bộ (hoặc bên thứ 3) mà không muốn copy-paste code thủ công. Git có 2 cách tiếp cận hoàn toàn khác nhau — submodulesubtree — và chọn sai có thể khiến cả team bối rối vì sao "clone xong mà thư mục thư viện trống trơn".

1. Bài Toán: Dùng Chung 1 Thư Viện Giữa Nhiều Repo

Giả sử repo chính của bạn cần 1 thư viện nội bộ sống trong repo riêng, được nhiều dự án khác dùng chung. Copy-paste code thư viện vào repo chính là tồi tệ nhất — mất khả năng cập nhật, mất lịch sử gốc. Git cho 2 lựa chọn để "nhúng" repo thư viện vào bên trong repo chính mà vẫn giữ được liên kết tới nguồn gốc của nó.

2. Submodule: Con Trỏ Tới 1 Commit Cụ Thể

git submodule add <url> libs/lib không copy file thư viện vào repo chính — nó tạo 1 entry đặc biệt gọi là gitlink (mode 160000) trong tree, chỉ lưu đúng 40 byte SHA của 1 commit cụ thể trong repo thư viện, cộng với 1 file .gitmodules ghi URL tương ứng. Repo chính hoàn toàn không biết gì về nội dung file bên trong thư viện ở commit đó — object database của nó không hề chứa các blob này.

Vì vậy, sau khi clone 1 repo có submodule, thư mục libs/lib sẽ trống rỗng cho tới khi bạn chạy git submodule update --init để Git tự clone riêng repo thư viện vào đúng vị trí đó, checkout đúng commit được ghi trong gitlink.

ℹ️ git clone --recurse-submodules làm mọi thứ trong 1 bước
Thay vì clone xong rồi mới nhớ ra phải submodule update --init, thêm cờ --recurse-submodules ngay lúc clone để Git tự động khởi tạo và checkout đúng commit cho mọi submodule luôn — tránh hoàn toàn tình huống "tưởng thiếu file, hoá ra chỉ chưa init".
🕳️ Cạm bẫy thường gặp: quên --init tưởng bị mất file
git clone thường (không có --recurse-submodules) để lại thư mục submodule trống, không lỗi, không cảnh báo rõ ràng. Rất nhiều người mới tưởng nhầm là repo bị hỏng hoặc file bị xoá. Luôn nhớ chạy git submodule update --init --recursive ngay sau clone nếu quên cờ trên.

3. Subtree: Nhúng Thẳng Nội Dung/Lịch Sử Vào Cây Thư Mục

git subtree add --prefix=libs/lib <url> main --squash làm điều ngược lại hoàn toàn: nó merge toàn bộ nội dung file của thư viện (và tuỳ chọn cả lịch sử, hoặc gộp thành 1 commit duy nhất nếu dùng --squash) thẳng vào thư mục libs/lib dưới dạng commit bình thường của chính repo chính. Không có gitlink, không có .gitmodules, không có bước "init" nào cả — clone xong là có đủ file để build ngay lập tức.

Cập nhật thư viện dùng git subtree pull --prefix=libs/lib <url> main --squash, tạo thêm 1 commit merge mới chứa đúng phần thay đổi. Đóng góp code ngược lại thư viện gốc dùng git subtree push — phức tạp hơn 1 chút vì phải tách đúng phần thay đổi trong libs/lib ra khỏi lịch sử chung của repo chính.

🔬 Đào sâu: gitlink là 1 loại entry hoàn toàn khác trong tree
Object tree (xem lại Bài 1) thường chỉ chứa entry trỏ tới blob (file) hoặc tree khác (thư mục con). Gitlink là loại entry thứ 3 — mode 160000 — trỏ thẳng tới 1 commit nằm trong object database hoàn toàn khác (repo thư viện). Đây chính xác là lý do file bên trong không nằm trong repo chính: gitlink không phải tham chiếu nội bộ, nó là 1 con trỏ "liên repo".

4. So Sánh Trực Quan 2 Cách Nhúng

Submodule Subtree
Lưu gì trong repo chính? 1 con trỏ 40-byte (gitlink) + .gitmodules Toàn bộ nội dung file (và tuỳ chọn lịch sử) dạng commit thường
Sau khi clone repo chính Thư mục thư viện trống tới khi update --init Đầy đủ file ngay lập tức, build được luôn
Kích thước repo chính Nhỏ gọn — không kéo theo lịch sử thư viện Phình to hơn — nội dung thư viện nằm ngay bên trong
Đóng góp ngược lại thư viện gốc Đơn giản — vào thẳng thư mục, dùng Git bình thường, push ở đó Phức tạp hơn — cần git subtree push tách đúng phần thay đổi
💡 Mẹo: luôn dùng --squash với subtree
Không dùng --squash, mọi commit trong lịch sử thư viện sẽ bị nhúng nguyên vẹn vào repo chính — làm phình to git log tổng thể rất nhanh. --squash gộp toàn bộ thay đổi từ lần merge trước thành đúng 1 commit duy nhất mỗi lần add/pull, giữ lịch sử repo chính gọn gàng, dễ đọc hơn nhiều.

Sân chơi tương tác: Demo Submodule vs Subtree

Bên dưới có 1 "thư viện" riêng và 1 "repo chính". Chọn chế độ (Submodule hoặc Subtree), thêm thư viện vào repo chính, giả lập thư viện có bản cập nhật mới, rồi thử đồng bộ lại — quan sát khác biệt: 1 bên chỉ là con trỏ mảnh, 1 bên nhúng thẳng nội dung.

📦 Sân chơi tương tác: Subtree/Submodule Simulator

Chế độ nhúng

Terminal giả lập (repo chính)

  • git submodule add lib.git libs/lib
  • git commit -m "..."
  • git submodule update --remote
  • git subtree add --prefix=libs/lib lib.git main --squash
  • git subtree pull --prefix=libs/lib lib.git main --squash

Nhật ký

git-subtree-submodule-live.js

Trắc nghiệm ôn tập

Câu 1: Sau khi git clone (không kèm --recurse-submodules) 1 repo có submodule, thư mục submodule sẽ ra sao?

Trắc nghiệm ôn tập

Câu 2: Khác biệt CỐT LÕI giữa submodule và subtree về mặt lưu trữ trong repo chính là gì?

Trắc nghiệm ôn tập

Câu 3: Vì sao nhiều team vẫn chọn subtree dù biết repo chính sẽ phình to?

📖 Tài liệu tham khảo / References

Bài viết liên quan trong series

Bài 11: Hooks & Worktree Bài 9: Remote & Collaboration Quay lại Lộ trình Series Git