Git submodules let you place one Git repository inside another while keeping the repositories separate. This Git submodule tutorial shows how to add a dependency, initialize one after cloning, and update it without losing the commit recorded by the parent repository.
The key command, git submodule init, is only part of the workflow. A submodule does not automatically follow a child branch: the parent repository records one specific commit from the child repository.
How Git submodules pin a child commit
A parent repository stores a submodule as a gitlink: a pointer to a particular child commit. It also stores the child repository’s URL and local path in the tracked .gitmodules file. For example, the parent might place a library at vendor/widget and record commit abc123.
When you check out the parent, Git can place the child repository at that exact commit. The parent does not record “the latest commit on main,” so changes in the child repository do not affect the parent until you deliberately select a new child commit and record the changed pointer.
Add a submodule with git submodule add and record its pointer
Run this from the parent repository:
git submodule add https://github.com/example/widget.git vendor/widget
This command clones the child repository into vendor/widget, adds its URL and path to .gitmodules, and stages both that file and the submodule’s gitlink in the parent index. It does not create the parent commit for you.
- Inspect the staged changes with git status.
- Commit the submodule pointer and configuration with git commit -m “Add widget submodule”.
- Share the parent commit so other developers receive the same child commit.
The child repository has its own Git history and working tree. If you enter vendor/widget and move to another child commit, the parent will see that its gitlink differs from the recorded value. You must commit that changed pointer in the parent before the update becomes part of the project.
Clone and initialize existing submodules with git clone –recurse-submodules or git submodule init
For a new checkout, the simplest option is:
git clone –recurse-submodules https://github.com/example/project.git
This clones the parent, reads each submodule entry from .gitmodules, initializes local submodule configuration in .git/config, and checks out each child at the commit recorded by the parent. The parent’s tracked files and gitlinks come from the parent commit; the child working trees are then populated at those pinned commits.
If you already cloned without recursion, run:
- git submodule init copies submodule URLs from the tracked .gitmodules file into your local .git/config. It registers the submodules but does not generally fetch and check out their recorded commits.
- git submodule update fetches the required child data and checks out the commit referenced by the current parent commit. It changes the submodule working tree, not the parent’s recorded pointer.
For nested submodules, use git submodule update –init –recursive. The –init option combines initialization with updating, while –recursive applies the operation to submodules inside other submodules.
Update a submodule and fix common pointer-state problems
git submodule update restores the commit already recorded by the parent. It does not follow the child repository’s latest branch commit. To intentionally move the dependency forward, update the child repository first:
- Enter the child and select its intended branch: git -C vendor/widget switch main.
- Fetch an approved newer commit: git -C vendor/widget pull –ff-only.
- Return to the parent and inspect the new gitlink with git status or git diff –submodule.
- Stage and commit the pointer: git add vendor/widget, then git commit -m “Update widget submodule”.
You can also use git submodule update –remote when the submodule is configured to follow a remote branch. Review the resulting commit before running git add; the parent still records a specific commit, not a floating branch.
- Detached HEAD: This is normal after git submodule update because Git checks out the pinned commit directly. Create or switch to a child branch before developing there.
- Modified submodule: Commit needed work inside the child first, then run git add vendor/widget in the parent to record its new commit. Discard unwanted work only after checking the child status.
- Missing submodule files: Run git submodule update –init –recursive.
- Changed repository URL: After editing .gitmodules, run git submodule sync –recursive, then initialize or update the submodule.
