git Distributed version control system, created by Linus Torvalds

Submodules

A submodule embeds one Git repository inside another as a subdirectory, pinned to a specific commit — used to vendor a dependency while keeping its own independent history and remote.

Adding a submodule

git submodule add https://github.com/example/lib.git libs/lib
git commit -m "Add lib submodule"

This creates a .gitmodules file (tracked, describes the mapping) and a gitlink entry in the parent repo’s tree pointing at the submodule’s current commit — the parent repo never stores the submodule’s files, only which commit it’s pinned to.

Cloning a repo that has submodules

Plain git clone leaves submodule directories empty. Either clone with a flag, or initialize afterward:

git clone --recurse-submodules <url>
# already cloned without it?
git submodule update --init --recursive

Updating a submodule

cd libs/lib
git fetch
git checkout <new-sha-or-branch>
cd ../..
git add libs/lib
git commit -m "Bump lib submodule"

Or, to update every submodule to the latest commit on the branch each one tracks:

git submodule update --remote --merge

Other useful commands

git submodule status                 # current SHA of each submodule
git submodule foreach 'git status'   # run a command in every submodule
git submodule deinit libs/lib        # remove the working copy, keep the config
git rm libs/lib                      # fully remove the submodule

Common pitfalls

  • Forgetting --recurse-submodules on clone leaves empty directories that look broken but aren’t — run git submodule update --init --recursive.
  • Detached HEAD inside the submodule is the normal state after an update — the parent repo tracks a commit, not a branch. Commit new work inside a submodule on an actual branch, or it’s easy to lose.
  • Forgetting to commit the pointer bump in the parent repo after updating a submodule means everyone else still checks out the old commit.

Alternatives worth knowing about if submodules feel too heavyweight: git subtree (built in, copies history into the parent repo instead of pinning a pointer) and language-level package managers (npm, pub, cargo) for anything that’s actually published as a package rather than vendored source.