Submodules
Sometimes a project needs code that lives in a different repository: a shared design kit used by several websites, a library you maintain separately, or course material reused across projects. A submodule puts another Git repository inside yours as a folder, pinned to one exact commit.
Submodules are powerful but have sharp edges, and many teams prefer a package manager instead. This lesson covers the essentials so you can work with projects that use them, plus advice on when not to.
How a submodule works
Your repository does not store the submodule's files. It stores two things:
- A
.gitmodulesfile that records the folder path and the submodule's URL. - A special entry in the tree that records which commit of the submodule to use.
Anyone who clones your project gets those two pieces of information and can then download the submodule at exactly that commit. When the submodule's own repository gets new commits, your project does not change until you choose to update it.
Adding a submodule
Priya's family runs two shops, and both websites use the same buttons and colours from a ui-kit repository. She adds it to the sweet-shop project under vendor/ui-kit:
git submodule add https://github.com/priya-sharma/ui-kit.git vendor/ui-kit
git statusCloning into 'C:/Users/priya/projects/sweet-shop/vendor/ui-kit'... done. On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: .gitmodules new file: vendor/ui-kit
Two new entries are staged. .gitmodules is a small text file:
[submodule "vendor/ui-kit"]
path = vendor/ui-kit
url = https://github.com/priya-sharma/ui-kit.gitAnd the diff for vendor/ui-kit shows that Git stores only a commit, with the special mode 160000:
git diff --cached vendor/ui-kitdiff --git a/vendor/ui-kit b/vendor/ui-kit
new file mode 160000
index 0000000..01a92e5
--- /dev/null
+++ b/vendor/ui-kit
@@ -0,0 +1 @@
+Subproject commit 01a92e58052ab3d3464b0f6e6526225b25cb63f0She commits and pushes as usual. git submodule status lists each submodule with its commit and the nearest tag:
git commit -m "Add ui-kit as a submodule"
git submodule status01a92e58052ab3d3464b0f6e6526225b25cb63f0 vendor/ui-kit (v1.0)
Cloning a project with submodules
Here is the classic surprise. Meera clones the sweet-shop project normally, and the vendor/ui-kit folder is empty. A - before the commit in git submodule status means "not downloaded yet":
git clone https://github.com/priya-sharma/sweet-shop.git
cd sweet-shop
git submodule status-01a92e58052ab3d3464b0f6e6526225b25cb63f0 vendor/ui-kit
One command fetches every submodule at the recorded commit:
git submodule update --initSubmodule 'vendor/ui-kit' (https://github.com/priya-sharma/ui-kit.git) registered for path 'vendor/ui-kit' Cloning into 'C:/Users/meera/projects/sweet-shop/vendor/ui-kit'... done. Submodule path 'vendor/ui-kit': checked out '01a92e58052ab3d3464b0f6e6526225b25cb63f0'
Next time, she can do both steps at once with git clone --recurse-submodules <url>. Add --recursive to git submodule update if submodules contain their own submodules.
Updating a submodule
The ui-kit gets a new "primary button" style. The sweet-shop still uses the old commit, by design. To move to the newest commit of the submodule's default branch:
git submodule update --remote
git status -s
git diff --submodule=logSubmodule path 'vendor/ui-kit': checked out '38b38bab27d6b0498a17f0f352c6eb3f6c9996dc' M vendor/ui-kit Submodule vendor/ui-kit 01a92e5..38b38ba: > Add primary button
Git shows the submodule as modified, and --submodule=log lists the new commits it brings. Like any change, this must be committed (git commit -am "Update ui-kit to add primary button") and pushed, so teammates get the same version. When a teammate pulls that commit, they run git submodule update to check out the new pinned commit in their copy.
Working inside a submodule
The submodule folder is a complete repository of its own. You can cd vendor/ui-kit, switch to a branch, commit and push to the ui-kit repository. Two rules prevent trouble: after git submodule update, the submodule is in detached HEAD, so git switch main before committing inside it; and push the submodule's commits before pushing the parent project, or teammates will be pinned to a commit that does not exist on GitHub yet.
When to avoid submodules
Submodules are best when you need an exact, separately developed repository and have few of them. For ordinary libraries, a package manager is usually simpler: npm for JavaScript, pip or uv for Python, Composer for PHP, Maven or Gradle for Java. Packages have version numbers, install with one command, and do not need extra Git steps from every teammate. Another alternative, git subtree, copies the other project's files and history directly into yours, so clones need no extra command.
Common mistakes
- Cloning without submodules and getting empty folders or build errors. Run
git submodule update --init, or clone with--recurse-submodules. - Forgetting to commit the new submodule pointer after updating. Teammates stay on the old version.
- Committing inside a detached submodule. Switch to a branch inside the submodule first.
- Pushing the parent before the submodule. Others cannot fetch a commit that exists only on your laptop.
Key points
- A submodule is another repository inside yours, pinned to one exact commit.
git submodule add <url> <path>creates.gitmodulesand the pinned entry; commit both.- After a normal clone, run
git submodule update --init, or clone with--recurse-submodules. git submodule update --remotemoves to the submodule's newest commit; commit the change so others get it.- For ordinary libraries, a package manager is usually the simpler choice.
Exercise
Create two local repositories: library with one commit containing helpers.js, and app with one commit. Add library to app as a submodule in a lib folder, commit it, and clone app into a new folder with its submodule. Then make a new commit in library and update the clone to use it.
Show solution
git -C app -c protocol.file.allow=always submodule add ../library lib
git -C app commit -m "Add library as a submodule"
git -c protocol.file.allow=always clone --recurse-submodules app app-copy
git -C library commit --allow-empty -m "Improve helpers"
git -C app-copy -c protocol.file.allow=always submodule update --remote
git -C app-copy status -s-C <folder> runs Git in that folder without cd. For security, Git refuses submodules from local folder paths unless you allow the file protocol, which -c protocol.file.allow=always does for one command. With real GitHub URLs you do not need it. The last command shows M lib, ready to be committed.
Quick Quiz
1. What does the parent repository store for a submodule?
The pinned commit means everyone gets the same submodule version.
2. You cloned a project and its submodule folder is empty. What do you run?
A normal clone does not download submodules; update --init does.
3. After git submodule update --remote, what else must you do so teammates get the new version?
The new pinned commit is a change in the parent project like any other.
4. When is a package manager usually better than a submodule?
Packages are simpler for teammates and use clear version numbers.
Learn Git with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
