Git submodule: cách thêm, cập nhật và xóa submodule
Git submodule là một kho Git khác được lồng vào dự án của bạn và ghim ở một commit cụ thể. Thêm bằng git submodule add <url> <đường-dẫn>, lấy về khi clone bằng git clone --recurse-submodules, cập nhật bằng git submodule update --remote, còn xóa bằng git submodule deinit và git rm.
Git submodule (submodule, kho con) là một kho Git riêng được lồng vào thư mục dự án của bạn. Đặc điểm chính: kho cha không lưu các tệp của kho con, mà chỉ lưu một tham chiếu tới một commit cụ thể trong đó. Vì vậy mọi người clone dự án sẽ có kho con ở cùng một phiên bản — cho đến khi bạn chủ động đổi nó.
Submodule được dùng khi cần gắn một thư viện của người khác mà không qua trình quản lý gói, chia sẻ mã chung giữa nhiều dự án, hoặc tách chủ đề giao diện hay firmware ra một kho riêng với lịch sử riêng.
Submodule hoạt động thế nào
Một submodule gồm ba phần, và hiểu mối liên hệ giữa chúng giải quyết hầu hết vấn đề:
- Tệp
.gitmodulesở gốc dự án — được commit và mô tả đường dẫn nào lấy từ URL nào. - Mục gitlink trong cây commit — một phần tử đặc biệt có chế độ
160000, nơi thay vì nội dung là hash của commit trong kho con. - Chính kho của submodule — dữ liệu được lưu trong
.git/modules/<tên>của kho cha, còn trong thư mục submodule có tệp.gittrỏ tới đó.
cat .gitmodules
[submodule "libs/ui-kit"]
path = libs/ui-kit
url = https://github.com/example/ui-kit.git
git ls-tree HEAD libs/
160000 commit 8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit
Lưu ý: kho cha ghim đúng commit, không phải nhánh. Đây là điểm khác biệt cơ bản giữa submodule trong Git và một phụ thuộc kiểu «phiên bản mới nhất của main».
Cách thêm submodule
git submodule add https://github.com/example/ui-kit.git libs/ui-kit
Cloning into '/home/dev/shop/libs/ui-kit'...
done.
git status
new file: .gitmodules
new file: libs/ui-kit
git commit -m "Thêm ui-kit làm submodule"
Lệnh clone kho, tạo hoặc bổ sung .gitmodules, và thêm gitlink vào index. Việc còn lại là commit. Nếu không chỉ định đường dẫn, thư mục sẽ được đặt tên theo tên kho.
Để submodule theo dõi một nhánh cụ thể khi cập nhật, chỉ định ngay: git submodule add -b develop <url> <đường-dẫn>. Nhánh được ghi vào .gitmodules dưới dạng branch = develop.
URL tương đối
Nếu submodule nằm trên cùng máy chủ, có thể dùng đường dẫn tương đối: git submodule add ../ui-kit.git libs/ui-kit. URL được phân giải theo địa chỉ kho từ xa của cha, nên hoạt động cả với SSH lẫn HTTPS — tùy theo cách bạn đã clone dự án.
Cách clone dự án có submodule
Lỗi phổ biến nhất: «clone về rồi, thư mục submodule trống». Mặc định git clone không tải submodule. Đúng cách như sau:
git clone --recurse-submodules https://github.com/example/shop.git
Nếu dự án đã được clone mà không có submodule:
git submodule update --init --recursive
--init đăng ký các submodule từ .gitmodules vào .git/config cục bộ, update tải chúng và chuyển sang commit đã ghim, còn --recursive làm tương tự với submodule nằm trong submodule.
Cách cập nhật submodule
Ở đây cần phân biệt hai việc: «lấy đúng phiên bản mà đồng nghiệp đã commit» và «chuyển sang phiên bản mới của thư viện».
Lấy phiên bản đã được ghim trong dự án
Sau git pull ở kho cha, gitlink có thể đã thay đổi, nhưng tệp của submodule không tự cập nhật — git status sẽ hiển thị modified: libs/ui-kit (new commits). Đồng bộ bằng:
git pull
git submodule update --init --recursive
Để khỏi làm thủ công, bật tự động: git config submodule.recurse true. Khi đó pull, checkout và switch sẽ tự cập nhật submodule. Cách khác khi chỉ cần một lần là git pull --recurse-submodules.
Chuyển sang phiên bản mới của thư viện
# Cách 1: lấy commit mới nhất của nhánh đang theo dõi
git submodule update --remote libs/ui-kit
# Cách 2: chọn phiên bản bằng tay
cd libs/ui-kit
git fetch
git checkout v3.2.0
cd ../..
# Trong cả hai trường hợp: ghi phiên bản mới vào kho cha
git add libs/ui-kit
git commit -m "Cập nhật ui-kit lên v3.2.0"
--remote lấy nhánh từ tham số branch trong .gitmodules, còn nếu không có thì lấy nhánh mà HEAD của kho từ xa trỏ tới (thường là main). Nếu không commit ở kho cha, đồng nghiệp sẽ không nhận được phiên bản mới: cập nhật submodule luôn là một thay đổi gitlink cần được commit.
Để xem thay đổi trong các submodule:
git diff --submodule=log
git config diff.submodule log # đặt kiểu hiển thị này làm mặc định
git submodule status
8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit (v3.1.0)
+1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b libs/charts (v1.4.0-3-g1a2b3c4)
Ký tự đầu của git submodule status: - — submodule chưa được khởi tạo, + — commit hiện tại khác với commit đã ghim trong kho cha, U — xung đột khi gộp.
Cách sửa đổi code trong submodule
Sau git submodule update, submodule ở trạng thái detached HEAD — đứng trên một commit cụ thể chứ không phải nhánh. Nếu commit ngay trong trạng thái này, các commit dễ bị mất ở lần update sau. Quy trình đúng:
cd libs/ui-kit
git switch main # chuyển sang nhánh
# ... sửa ...
git commit -am "Sửa khoảng cách nút"
git push # push submodule trước!
cd ../..
git add libs/ui-kit
git commit -m "ui-kit: sửa khoảng cách nút"
git push
Thứ tự push rất quan trọng. Nếu đẩy kho cha trước submodule, trên máy chủ dự án sẽ tham chiếu tới một commit mà ngoài bạn ra không ai tải được, và đồng nghiệp chạy git submodule update sẽ gặp lỗi fatal: remote error: upload-pack: not our ref hoặc did not contain. Để tự bảo vệ, dùng git push --recurse-submodules=check (từ chối push nếu submodule chưa được đẩy) hoặc =on-demand (tự đẩy submodule trước).
Cách xóa submodule
Trước đây việc xóa đòi hỏi sửa tay nhiều tệp. Trong các phiên bản Git hiện nay chỉ cần ba bước:
git submodule deinit -f libs/ui-kit # gỡ khỏi .git/config và làm sạch thư mục
git rm -f libs/ui-kit # gỡ gitlink và mục trong .gitmodules
rm -rf .git/modules/libs/ui-kit # xóa dữ liệu đã lưu của submodule
git commit -m "Xóa submodule ui-kit"
Bước cuối không bắt buộc để kho đúng, nhưng nếu thiếu nó, khi thêm lại submodule vào cùng đường dẫn, Git có thể báo rằng thư mục cục bộ đã tồn tại (A git directory for 'libs/ui-kit' is found locally).
Khi URL của submodule thay đổi
Kho đã chuyển chỗ — sửa .gitmodules và đồng bộ cấu hình cục bộ:
git submodule set-url libs/ui-kit https://gitlab.com/example/ui-kit.git
git submodule sync --recursive
git commit -am "Địa chỉ mới của ui-kit"
Đồng đội sau khi pull cũng cần chạy git submodule sync: URL từ .gitmodules chỉ được sao chép vào .git/config của họ khi init hoặc sync.
Lệnh hữu ích khi có nhiều submodule
| Việc cần làm | Lệnh |
|---|---|
| Danh sách và trạng thái | git submodule status --recursive |
| Chạy lệnh trong từng submodule | git submodule foreach 'git switch main && git pull' |
| Tải nhanh hơn | git submodule update --init --jobs 4 |
| Tải không kèm lịch sử | git submodule update --init --depth 1 |
| Tóm tắt trong status | git config status.submoduleSummary true |
Các vấn đề thường gặp với submodule
- Thư mục trống sau khi clone — chưa chạy init. Cách sửa:
git submodule update --init --recursive. - «modified: (new commits)» ngay sau pull — kho cha trỏ tới commit khác, còn tệp vẫn là bản cũ. Cách sửa:
git submodule update. - Vô tình hoàn lại phiên bản submodule — chạy
git add .khi submodule vẫn ở commit cũ rồi commit, và phiên bản bị lùi lại. Luôn kiểm tragit diff --submoduletrước khi commit. - Xung đột gitlink khi gộp — trên hai nhánh, submodule được cập nhật tới các commit khác nhau. Vào submodule, chọn commit cần thiết (thường là commit mới hơn, bao gồm cả hai thay đổi), rồi
git add đường-dẫnvà hoàn tất merge. - Submodule riêng tư trong CI — build không tải được submodule vì không có quyền. Cần cấp cho CI khóa hoặc token truy cập kho submodule; URL tương đối giúp đơn giản hóa cấu hình.
Submodule trong nhóm: thỏa thuận
Phần lớn vấn đề với submodule không đến từ Git, mà từ việc mỗi người làm một kiểu. Vài quy tắc nên ghi trong README của dự án:
- Chỉ clone với
--recurse-submodulesvà bậtsubmodule.recurse truecho tất cả. - Cập nhật submodule trong một commit riêng với thông điệp rõ ràng: phiên bản nào và vì sao.
- Không sửa code submodule trong dự án cha mà không có commit riêng và push vào chính submodule.
- Trong CI, chạy rõ ràng
git submodule update --init --recursivevà kiểm tra build không phụ thuộc vào thay đổi cục bộ.
Để kiểm tra trong CI rằng mọi commit đã ghim của submodule thực sự truy cập được, chỉ cần một bản clone sạch: nếu ai đó quên đẩy submodule, build sẽ lỗi ở bước update, chứ không phải ở máy đồng nghiệp sau một tuần.
git clone --recurse-submodules --jobs 4 "$REPO_URL" build
cd build
git submodule status --recursive
Submodule hay các giải pháp khác
Submodule cho kiểm soát chính xác phiên bản và lịch sử tách biệt, nhưng mỗi thành viên phải thêm các bước thủ công. Nếu mã là một thư viện thông thường, trình quản lý gói (npm, pip, Composer, Go modules) đơn giản hơn. Nếu bạn cần mã của bên thứ ba chỉ nằm sẵn trong dự án và được clone mà không cần lệnh bổ sung, hãy xem git subtree: nó sao chép lịch sử vào kho chính, và đồng đội không cần biết gì thêm. Với nhiều dịch vụ liên quan, nhóm thường chọn monorepo. Submodule có ý nghĩa khi mã lồng vào có vòng đời riêng, bản phát hành riêng và đội ngũ riêng.
Tóm lại
Git submodule là tham chiếu tới một commit cụ thể của kho khác. Thêm bằng git submodule add, lấy về bằng --recurse-submodules hoặc update --init, nâng cấp bằng update --remote cộng một commit ở kho cha, còn xóa bằng deinit, git rm và dọn .git/modules. Hãy nhớ về detached HEAD bên trong submodule và thứ tự push. Các công cụ nâng cao của Git được thực hành trong khóa học Git.
Câu hỏi thường gặp
Vì sao submodule luôn ở trạng thái detached HEAD?
Kho cha lưu hash commit chứ không lưu nhánh, và git submodule update chuyển submodule đúng tới commit đó. Để chỉnh sửa, hãy tự chuyển sang nhánh hoặc dùng git submodule update --remote --merge.
Có thể ghim submodule vào thẻ (tag) không?
Không trực tiếp, nhưng có thể chuyển submodule sang thẻ bằng git checkout rồi commit ở kho cha commit thu được; trong git submodule status thẻ sẽ hiện trong ngoặc.
GitHub có tải submodule khi tải tệp ZIP không?
Không, các gói mã nguồn ZIP của GitHub không bao gồm nội dung submodule, các thư mục sẽ trống. Hãy dùng git clone --recurse-submodules.
Khác nhau giữa git submodule và git subtree là gì?
Submodule lưu tham chiếu tới commit của kho bên ngoài và cần lệnh riêng để lấy mã, còn subtree sao chép tệp và lịch sử ngay vào kho chính.
Có thể di chuyển submodule sang thư mục khác không?
Được, bằng git mv đường-cũ đường-mới: Git tự cập nhật .gitmodules và các tham chiếu bên trong, bạn chỉ cần commit.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.
Đến khóa học