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
Output
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
Output
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
Output
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
Output
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
Output
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
Output
 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, cd into 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=check catches this.
  • Everyone must update. Stale submodules after a pull are a common source of confusion; git status shows them as modified.

Alternatives

OptionGood when
A package manager (npm, pip, Maven, Composer…)The shared code can be published as a versioned package — usually the best choice
SubmodulesYou need another repository's exact source at a pinned commit, and to update it deliberately
git subtreeYou want the other project's files copied into your repository (no extra steps for teammates), with a way to pull updates
A monorepoTeams 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.