Sometimes one project needs another project's code inside it. A website might use a shared design system that several other sites also use. A game might include an open-source physics library. A documentation site might pull in a theme maintained by someone else. You want that code in your folder, but you also want it to remain its own repository, with its own history and updates.
Submodules do exactly that. A submodule is a Git repository placed inside another repository, the parent. The parent doesn't copy the submodule's files into its own history; it records which commit of the submodule to use. Everyone who clones the parent gets that exact version.
Submodules have a reputation for being confusing, mostly because they don't update themselves and because cloning needs one extra step. In this lesson you'll add a submodule, clone a project that uses one, update it deliberately, remove it, and learn when a package manager is a simpler choice.
Adding a submodule
git submodule add https://github.com/acme/ui-kit.git vendor/ui-kit
git commit -m "Add ui-kit submodule"This clones the other repository into vendor/ui-kit and creates a .gitmodules file recording where it came from:
[submodule "vendor/ui-kit"]
path = vendor/ui-kit
url = https://github.com/acme/ui-kit.gitBoth .gitmodules and the submodule folder are staged, and you commit them like any other change. In the parent's history, the folder is stored as a single entry pointing at one commit hash, not as a copy of the files.
Pinned to a commit
The parent records an exact commit of the submodule, not "the latest version". That's deliberate: the project keeps working even when the other repository changes, because nothing updates until you choose to update it.
git submodule status shows the pinned commit for each submodule. Inside the submodule folder, you're in a normal repository: you can run git log, switch branches and even commit. Its HEAD is usually detached, pointing at that commit rather than at a branch, which surprises people who try to commit there.
Cloning and updating
A normal git clone of the parent creates empty submodule folders. Either clone with the extra flag, or fill them in afterwards:
git clone --recurse-submodules https://github.com/samlee/shop.git
git submodule update --init --recursive # for an existing cloneTo move a submodule to its newest commit:
git submodule update --remote vendor/ui-kit
git add vendor/ui-kit
git commit -m "Update ui-kit to latest"The new pointer is a change in the parent like any other: review it, test it and commit it. After pulling a teammate's update, run git submodule update --init --recursive so your submodule matches the new pointer.
Removing, and alternatives
To remove a submodule completely:
git submodule deinit -f vendor/ui-kit
git rm -f vendor/ui-kit
rm -rf .git/modules/vendor/ui-kit
git commit -m "Remove ui-kit submodule"Before choosing submodules, consider the alternatives. If the shared code is published as a package (npm, Composer, pip, Maven), a package manager handles versions and updates far more smoothly. git subtree copies another repository's files into yours while keeping the option to pull updates, so clones need no extra steps. Submodules suit code that isn't packaged, or projects that need exact, independently versioned repositories.
Git Submodules Syntax
Submodule commands
git submodule add <url> <path>
git clone --recurse-submodules <url>
git submodule update --init --recursive
git submodule status
git submodule update --remote [path]
git submodule deinit -f <path> && git rm -f <path>
git config --global submodule.recurse trueGit Submodules Summary
- A submodule is a repository inside another; the parent records one exact commit of it.
git submodule add <url> <path>creates the submodule and the .gitmodules file.- Clone with --recurse-submodules, or run git submodule update --init --recursive afterwards.
- Update with git submodule update --remote, then commit the new pointer in the parent.
- Consider package managers or git subtree, which are often simpler.
Git Submodules Examples
Each example shows the code, the output it produces and what each part does.
Adding a submodule
git submodule add https://github.com/acme/ui-kit.git vendor/ui-kit
cat .gitmodules
git status --short
git commit -m "Add ui-kit submodule"
git submodule statusCloning into '/home/sam/shop/vendor/ui-kit'...
done.
[submodule "vendor/ui-kit"]
path = vendor/ui-kit
url = https://github.com/acme/ui-kit.git
A .gitmodules
A vendor/ui-kit
[main 4f2a9c1] Add ui-kit submodule
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 vendor/ui-kit
697b804595975fa21e837430e6a6deea1f78e942 vendor/ui-kit (heads/main)Line-by-line explanation
- Line 1Clones the library into vendor/ui-kit and registers it.
- Line 3Two new entries are staged: the settings file and the submodule itself.
- Line 4Mode 160000 in the commit summary marks a submodule pointer rather than a normal file.
- Line 5The parent is pinned to commit 697b804 of the library.
Cloning a project with submodules
git clone https://github.com/samlee/shop.git
ls shop/vendor/ui-kit
cd shop
git submodule update --init --recursive
ls vendor/ui-kitCloning into 'shop'...
Submodule 'vendor/ui-kit' (https://github.com/acme/ui-kit.git) registered for path 'vendor/ui-kit'
Cloning into '/home/sam/shop/vendor/ui-kit'...
Submodule path 'vendor/ui-kit': checked out '697b804595975fa21e837430e6a6deea1f78e942'
ui.jsLine-by-line explanation
- Lines 1 to 2A plain clone leaves the submodule folder empty, so ls prints nothing.
- Line 4Register, clone and check out every submodule at its pinned commit.
- Line 5The library's files are now there. git clone --recurse-submodules would have done all of this in one step.
Updating to a newer version
git submodule update --remote vendor/ui-kit
git status --short
git diff --submodule
git add vendor/ui-kit
git commit -m "Update ui-kit to 1.1"From https://github.com/acme/ui-kit
697b804..b80faa0 main -> origin/main
Submodule path 'vendor/ui-kit': checked out 'b80faa05d92f4bf1e0d01a1dceb0d7f5f3b373dd'
M vendor/ui-kit
Submodule vendor/ui-kit 697b804..b80faa0:
> UI kit 1.1
[main 9d0e3b7] Update ui-kit to 1.1
1 file changed, 1 insertion(+), 1 deletion(-)Line-by-line explanation
- Line 1Fetch the library and check out the newest commit on its main branch.
- Line 2The parent sees the pointer has moved.
- Line 3--submodule lists the library commits included in the update.
- Lines 4 to 5Commit the new pointer so everyone gets the same version.
Common Git Submodules Mistakes
Cloning without the submodules
git clone https://github.com/samlee/shop.git
npm run build # fails: vendor/ui-kit is emptygit clone --recurse-submodules https://github.com/samlee/shop.gitUpdating but not committing the pointer
git submodule update --remote
git push # teammates still get the old versiongit submodule update --remote
git add vendor/ui-kit
git commit -m "Update ui-kit"
git pushCommitting inside a detached submodule
cd vendor/ui-kit
git commit -am "Fix button" # commit on a detached HEAD, easy to losecd vendor/ui-kit
git switch main
git commit -am "Fix button"
git push
cd ../.. && git add vendor/ui-kit && git commit -m "Use fixed ui-kit"Git Submodules in Real Projects
Submodules in practice
Game studios include engine forks and third-party libraries as submodules, embedded and firmware projects pull in hardware vendor code this way, and static site generators such as Hugo often install themes as submodules. Many web teams avoid them for anything available as a package, which is a sensible choice. Knowing how they work means you can clone, build and update projects that use them without frustration.
Git Submodules FAQ
How do I see the state of my submodules?
Run git submodule status. Each line shows the commit and path; a - in front means the submodule hasn't been initialised, and a + means the checked-out commit differs from the one recorded in the main repository.
How do I run a command in every submodule?
Use git submodule foreach, as in git submodule foreach 'git status' or git submodule foreach 'git pull'. Git enters each submodule in turn, prints its name and runs the command there. Add --recursive to include submodules inside submodules.
How do I change a submodule's URL?
Edit the url line for it in .gitmodules, then run git submodule sync to copy the new URL into your local config. Commit the changed .gitmodules so everyone else gets it; they also run git submodule sync after pulling.
Why does git diff show "Subproject commit" lines?
That's how Git shows a change to a submodule pointer: the old commit hash is shown as removed and the new one as added. It means the submodule is on a different commit from the one the main repository recorded. Commit it if the update was intended, or run git submodule update to go back.
How do I make a submodule follow a branch?
Set the branch with git config -f .gitmodules submodule.lib.branch main (or add it with git submodule add -b main). Then git submodule update --remote moves the submodule to the latest commit on that branch. You still commit the new pointer in the main repository.
Git Submodules Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the command that adds a submodule.
git add https://github.com/acme/ui-kit.git vendor/ui-kit
What does the parent repository store for a submodule?
You cloned a project and the submodule folder is empty. What do you run?
After git submodule update --remote, what else must you do so teammates get the new version?
When is a package manager usually a better choice than a submodule?
Git Submodules Quiz
5 questions to check what you learned.
Which file records a project's submodules?
- .gitignore
- .gitconfig
- submodules.txt
- .gitmodules
Which flag clones a project together with its submodules?
- --all
- --recurse-submodules
- --deep
- --with-modules
Why is a submodule's HEAD often detached?
- It's broken
- Submodules can't have branches
- It's checked out at the exact commit the parent records, not at a branch
- Because of .gitignore
What does
git config --global submodule.recurse truedo?- Makes pull, switch and checkout update submodules automatically
- Adds submodules automatically
- Deletes unused submodules
- Clones every repository on GitHub
What does mode 160000 in a commit summary mean?
- An executable file
- A symbolic link
- A deleted folder
- A submodule pointer (a commit reference)