Bài worktree là "nhiều working tree cho một kho". Submodule là bài toán ngược: một kho Git nằm trong một kho Git khác. Bạn dùng nó khi muốn nhúng một thư viện, một component dùng chung, hay một repo khác vào dự án mà vẫn giữ nó là repo độc lập có lịch sử riêng. Nghe đơn giản, nhưng submodule là thứ gây rối bậc nhất trong Git — vì nhiều người không hiểu điểm cốt lõi: repo cha không chép file của submodule, nó chỉ ghi một con trỏ tới đúng một commit. Đo thật bằng git 2.39 để thấy rõ.
Submodule là một con trỏ commit
Khi thêm submodule, repo cha không lưu nội dung file của kho con. Thay vào đó nó lưu:
- Một file
.gitmodules(khai báo path + url của submodule) — cái này commit vào repo cha. - Một gitlink: một entry đặc biệt trong cây (mode
160000) ghi đúng một commit SHA của kho con. Nghĩa là repo cha nói: "ở thư mục này, dùng submodule tại đúng commit này".
git submodule add <url> vendor/lib # thêm; sinh .gitmodules + gitlink
git clone --recursive <url> # clone kèm lấy nội dung submodule
git submodule update --init # nếu clone quên --recursive
# cập nhật: cd vào submodule, pull, rồi commit CON TRỎ mới ở repo cha

Hình 1: Submodule nhúng repo con nhưng repo cha chỉ ghi một con trỏ commit (gitlink mode 160000) + file .gitmodules. Ghim phiên bản là tính năng (build tái lập) nhưng cũng là nguồn của ba bẫy kinh điển.
Đo thật: gitlink và việc ghim phiên bản

Hình 2: submodule add sinh .gitmodules. Trong repo cha, vendor/lib là gitlink 160000 commit ab22844. Lib phát hành v2 (2c57ec2) nhưng submodule trong app vẫn trỏ ab22844 — không tự cập nhật. Muốn lên v2: pull trong submodule rồi commit con trỏ mới.
Kết quả làm rõ cơ chế:
.gitmodules:git submodule add ../lib.git vendor/libsinh file khai báopath/url. File này được commit để đồng đội biết submodule ở đâu.- Gitlink, không phải file:
git ls-tree HEAD vendor/libcho160000 commit ab22844...— không phảiblob(file) haytree(thư mục) thông thường, mà là gitlink trỏ tới commitab22844của kho lib. Repo cha không chứa nội dunglib.js; nó chỉ ghi "dùng lib tại commit này". - Ghim phiên bản: lib phát hành v2 (
2c57ec2), nhưng submodule trong app vẫn trỏab22844(v1). Đây là tính năng cốt lõi: submodule ghim đúng phiên bản, nên build của app luôn tái lập được — không bị thư viện đổi ngầm dưới chân. Nó không tự nhảy theo lib mới. - Cập nhật có chủ đích: muốn lên v2, phải
cd vendor/lib && git pull(đưa submodule tới commit mới) rồigit add vendor/lib && git commitở repo cha để ghi con trỏ mới. Sau đó gitlink thành2c57ec2.
Ba bẫy kinh điển và cách tránh
Bẫy 1: clone quên --recursive → thư mục submodule rỗng. Vì repo cha chỉ lưu con trỏ, git clone thường không tự lấy nội dung submodule — bạn được một thư mục vendor/lib trống. Cách sửa: git clone --recursive, hoặc sau khi clone chạy git submodule update --init --recursive. Đây là câu hỏi "vì sao thư mục lib trống" mà ai mới dùng submodule cũng hỏi.
Bẫy 2: cập nhật submodule mà quên commit con trỏ ở cha. Bạn pull trong submodule lên v2, code chạy ngon trên máy bạn — nhưng nếu quên git add vendor/lib && commit ở repo cha, con trỏ vẫn là v1 với mọi người khác. Đồng đội clone/pull vẫn nhận lib cũ. Luôn commit gitlink sau khi cập nhật submodule.
Bẫy 3: submodule ở detached HEAD. Khi checkout, submodule thường ở trạng thái detached HEAD (trỏ thẳng vào commit, bài git-11). Sửa code trong đó rồi commit mà không tạo nhánh là dễ mất commit khi con trỏ nhảy. Nếu cần sửa submodule, vào nó, git switch sang một nhánh thật trước khi commit.
Đánh đổi và lưu ý
Submodule tốt cho: repo độc lập, cần ghim phiên bản, ít thay đổi cùng lúc. Ví dụ một thư viện nội bộ dùng chung nhiều dự án, hoặc theme/plugin của bên thứ ba. Nó dở khi hai repo phải đổi song song liên tục (commit chéo qua lại rất phiền).
Cân nhắc thay thế: monorepo, hoặc package manager. Nhiều đội bỏ submodule vì phiền, chuyển sang: gói thư viện qua package manager (npm/pip/go modules — quản version tốt hơn), monorepo (mọi thứ một repo), hoặc git subtree (nhúng cả lịch sử vào cha, không cần con trỏ ngoài). Chọn submodule khi thật sự cần giữ repo con tách biệt mà vẫn ghim version.
Ba ý mang về
- Submodule ghim một con trỏ commit, không chép file: đo thật,
vendor/liblà gitlink160000 commit ab22844, repo cha chỉ ghi "dùng lib ở commit này" + file.gitmodules— nhờ vậy build tái lập được. - Submodule không tự cập nhật: lib lên v2 (
2c57ec2) mà app vẫn ghim v1 (ab22844); muốn lên phải pull trong submodule rồi commit con trỏ mới ở repo cha (đây là tính năng ghim version, không phải lỗi). - Ba bẫy phải nhớ: clone quên
--recursive→ thư mục rỗng (dùngsubmodule update --init); quên commit con trỏ → đồng đội ở version cũ; submodule detached HEAD → tạo nhánh trước khi sửa. Cân nhắc package manager/monorepo nếu submodule quá phiền.
Nguồn
- Pro Git — Git Tools: Submodules: https://git-scm.com/book/en/v2/Git-Tools-Submodules
- Git Documentation — git-submodule: https://git-scm.com/docs/git-submodule
Phần sau ta tự động hóa quy trình Git bằng git hooks — chạy kiểm tra trước commit/push, khác biệt hook phía client và phía server, và vì sao hook không tự chia sẻ theo repo.