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

Ảnh chụp đoạn mã nền tối giải thích git submodule repo lồng trong repo ghim ở một commit, submodule là gì nhúng một kho Git thư viện thành phần chung vào kho khác giữ nó là repo độc lập có lịch sử riêng repo cha không chép file của submodule nó chỉ ghi một con trỏ dùng submodule ở đúng commit này gitlink mode 160000, các lệnh submodule git submodule add url vendor lib thêm sinh .gitmodules git clone --recursive url clone kèm submodule git submodule update --init nếu clone quên --recursive cập nhật submodule cd vào nó pull rồi commit con trỏ mới ở cha, ghim phiên bản là tính năng cũng là bẫy submodule ghim đúng một commit build tái lập không bị lib đổi ngầm bẫy 1 clone quên --recursive thư mục submodule rỗng bẫy 2 quên commit con trỏ mới đồng đội vẫn ở lib cũ bẫy 3 submodule ở detached HEAD sửa trong đó dễ mất commit

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.

Ảnh chụp bảng kết quả đo thật nền tối chạy git 2.39, phần một submodule add sinh .gitmodules git submodule add lib.git vendor lib submodule vendor lib path vendor lib url lib.git, phần hai trong repo cha submodule là con trỏ commit git ls-tree HEAD vendor lib 160000 commit ab22844 vendor lib gitlink không phải blob tree app ghi dùng lib ở commit ab22844, phần ba lib lên v2 2c57ec2 nhưng app vẫn ghim v1 lib remote 2c57ec2 submodule trong app vẫn trỏ ab22844 không tự nhảy theo, phần bốn cập nhật pull trong submodule cộng commit con trỏ cd vendor lib git pull cd git add vendor lib git commit con trỏ mới 2c57ec2 giờ app dùng lib v2

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/lib sinh file khai báo path/url. File này được commit để đồng đội biết submodule ở đâu.
  • Gitlink, không phải file: git ls-tree HEAD vendor/lib cho 160000 commit ab22844... — không phải blob (file) hay tree (thư mục) thông thường, mà là gitlink trỏ tới commit ab22844 của kho lib. Repo cha không chứa nội dung lib.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ồi git add vendor/lib && git commit ở repo cha để ghi con trỏ mới. Sau đó gitlink thành 2c57ec2.

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ề

  1. Submodule ghim một con trỏ commit, không chép file: đo thật, vendor/lib là gitlink 160000 commit ab22844, repo cha chỉ ghi "dùng lib ở commit này" + file .gitmodules — nhờ vậy build tái lập được.
  2. 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).
  3. Ba bẫy phải nhớ: clone quên --recursive → thư mục rỗng (dùng submodule 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

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.