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 — submodule và subtree — 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
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".
--init tưởng bị mất filegit 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.
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 |
--squash với subtree--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.
Chế độ nhúng
Terminal giả lập (repo chính)
git submodule add lib.git libs/libgit commit -m "..."git submodule update --remotegit subtree add --prefix=libs/lib lib.git main --squashgit subtree pull --prefix=libs/lib lib.git main --squash
Nhật ký
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?