Submodules
A submodule is a Git repository placed inside another repository, pinned to a specific commit. It's how a project can include another project's code — a shared library, a theme, a set of design assets — while keeping that code's history separate.
Adding a submodule
Sam's shop should use the company's shared theme, which lives in its own repository:
git submodule add https://github.com/sam-rivera/brand-theme.git theme
Cloning into '/home/sam/shop/theme'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done.
git status cat .gitmodules
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: theme [submodule "theme"] path = theme url = https://github.com/sam-rivera/brand-theme.git
Two things were staged: a .gitmodules file recording where the submodule comes from, and theme itself — stored not as files, but as a pointer to one commit of the theme repository:
git commit -qm "Add brand theme as a submodule"
git ls-tree HEAD
100644 blob 55fb9941f4e21165ebbe4e250e070c6a1bd034c7 .gitmodules 100644 blob 5361d0eb6f30d0cbc13bebf8dcd96fac8c0e5a9d index.html 160000 commit 9ba60f30bd6a9ee1735f4bc250287e1ac7b29f20 theme
The entry type commit (mode 160000) is a gitlink: the shop records which commit of the theme it uses, nothing more.
Cloning a project with submodules
A plain clone leaves submodule folders empty. Use --recurse-submodules:
cd ~ git clone --quiet --recurse-submodules https://github.com/sam-rivera/shop.git shop-copy ls shop-copy/theme
theme.css
If you already cloned without it, fetch the submodules afterwards:
git submodule update --init --recursive
Updating a submodule
The theme team has pushed a change. The shop stays pinned to the old commit until someone deliberately updates it:
git submodule update --remote theme git status -s git diff --submodule
remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 278 bytes | 278.00 KiB/s, done. From https://github.com/sam-rivera/brand-theme 9ba60f3..8e1b135 main -> origin/main Submodule path 'theme': checked out '8e1b13521678d9435955ca309041745c1bb5310e' M theme Submodule theme 9ba60f3..8e1b135: > Add heading colour
--remote fetched the theme's latest commit and checked it out in theme. The shop now sees the pointer has moved; commit it so everyone gets the new version:
git commit -qam "Update brand theme"
git submodule status
8e1b13521678d9435955ca309041745c1bb5310e theme (remotes/origin/HEAD)
Teammates then run git pull followed by git submodule update to move their theme folder to the new pinned commit. Setting git config --global submodule.recurse true makes pull, switch and friends do that automatically.
Things to watch for
- Detached HEAD inside the submodule. Submodules are checked out at a commit, not a branch. If you want to make changes in one,
cdinto it and switch to a branch first, then commit and push them there before committing the new pointer in the outer project. - Forgetting to push. If you commit a pointer to a submodule commit you haven't pushed, nobody else can check it out.
git push --recurse-submodules=checkcatches this. - Everyone must update. Stale submodules after a pull are a common source of confusion;
git statusshows them as modified.
Alternatives
| Option | Good when |
|---|---|
| A package manager (npm, pip, Maven, Composer…) | The shared code can be published as a versioned package — usually the best choice |
| Submodules | You need another repository's exact source at a pinned commit, and to update it deliberately |
git subtree | You want the other project's files copied into your repository (no extra steps for teammates), with a way to pull updates |
| A monorepo | Teams prefer to keep related projects in one repository |
Removing a submodule
git rm theme # removes the folder and its .gitmodules entry rm -rf .git/modules/theme # removes Git's stored copy of its history git commit -m "Remove brand theme submodule"
Try it yourself
Create two repositories on GitHub. Add one to the other as a submodule, push, and clone the outer one with --recurse-submodules into a new folder. Then commit to the inner repository and update the pointer in the outer one with git submodule update --remote.