Git Submodules in Practice | Add, Update, CI, and Monorepo Alternatives
이 글의 핵심
Submodules let a parent repo pin a specific commit SHA in a shared library pattern—clone, CI, and version pinning must be designed once or the whole team repeats the same mistakes.
Introduction
A Git submodule embeds another Git repository as a directory inside a repo. The parent (superproject) records only a specific commit SHA for the child, which matches “include dependency source but pin the version strictly.”
On the other hand, one clone does not fetch everything by default, and it is easy to skip the init step in CI or Docker builds, raising operational complexity. This post covers Git submodule practical workflows: add, update, delete, CI setup, common errors, and monorepo alternatives.
Table of contents
- Concept: superproject and gitlink
- Workflow: add, clone, update, remove
- Advanced: shallow, fork, URL substitution
- Compare: submodule vs subtree vs package vs monorepo
- Real-world cases
- Troubleshooting
- Wrap-up
Concept: superproject and gitlink
- The parent repo’s
.gitmodulesstores path · URL · branch tracking settings. - Git treats the subdirectory as a gitlink (commit pointer), not a normal file.
- So submodules do not “automatically follow latest main” by default—the SHA recorded in the parent is the source of truth.
Workflow: add, clone, update, remove
Add a submodule
git submodule add https://github.com/org/shared-lib.git vendor/shared-lib
git commit -m "chore: add shared-lib submodule"
Example .gitmodules:
[submodule "vendor/shared-lib"]
path = vendor/shared-lib
url = https://github.com/org/shared-lib.git
First clone
git clone --recurse-submodules https://github.com/org/main-app.git
If you already cloned:
git submodule update --init --recursive
Bump the child repo to latest
cd vendor/shared-lib
git fetch origin
git checkout main
git pull
cd ../..
git add vendor/shared-lib
git commit -m "chore: bump shared-lib"
The parent only records a new SHA. Teammates then:
git pull
git submodule update --init --recursive
Remove a submodule
- Remove the section from
.gitmodules - Remove the submodule section from
.git/configif needed git rm -f vendor/shared-lib- Commit
Avoid hand-editing .git/modules—document the official steps for your repo.
Advanced: shallow, fork, URL substitution
Faster fetch in CI
git submodule update --init --recursive --depth 1
Large histories shrink with shallow clones; if you need an old SHA, depth may be insufficient—validate per pipeline.
URL remapping (enterprise mirror)
git config submodule.vendor/shared-lib.url https://git.internal.corp/org/shared-lib.git
Combined with url.<base>.insteadOf, you can align HTTPS/SSH across local and CI.
Compare: submodule vs subtree vs package vs monorepo
| Approach | Pros | Cons |
|---|---|---|
| Submodule | Clear source pin and repo separation | Clone and CI steps are more complex |
| Subtree | Single-repo clone | Upstream sync workflow is heavier |
| npm/pip/cargo package | Standard versioning and registry | Needs a release pipeline |
| Monorepo | Atomic changes and shared CI | Repo size and permission design |
When evaluating monorepo alternatives, consider how often the team changes multiple packages together and whether permissions and release cadence can live in one repo.
Real-world cases
- Shared protobuf/schema repo: multiple apps pin the same SHA; schema breaks are explicit bumps.
- Docs or test-data repo: submodule docs into the main site repo; only doc owners bump often.
- CI: include
submodule updatein the cache key, or print which SHA was expected on failure to shorten debugging.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Empty directory | Submodule not initialized | submodule update --init --recursive |
| detached HEAD | Submodule follows pointer only | Create a branch when working; push explicitly |
| Permission errors | CI token cannot access child repo | Machine user, deploy key, org SSO |
| Merge conflicts | Parent and child changed together | Resolve in the child first, then update parent SHA |
| Version mismatch | Some people pulled only partially | Document a single always run command |
Prevent accidental pushes inside the submodule: use branch protection on the child; bump the parent only via PR.
Wrap-up
Git submodule expresses multi-repo dependencies in a Git-native way; success requires standard clone/update/CI commands in the repo README—even a one-liner. Pair with Git push/pull and remote collaboration and Git merge conflict case study for full workflow context.