Git Submodules in Practice | Add, Update, CI, and Monorepo Alternatives

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

  1. Concept: superproject and gitlink
  2. Workflow: add, clone, update, remove
  3. Advanced: shallow, fork, URL substitution
  4. Compare: submodule vs subtree vs package vs monorepo
  5. Real-world cases
  6. Troubleshooting
  7. Wrap-up

  • The parent repo’s .gitmodules stores 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

  1. Remove the section from .gitmodules
  2. Remove the submodule section from .git/config if needed
  3. git rm -f vendor/shared-lib
  4. Commit

Avoid hand-editing .git/modulesdocument 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

ApproachProsCons
SubmoduleClear source pin and repo separationClone and CI steps are more complex
SubtreeSingle-repo cloneUpstream sync workflow is heavier
npm/pip/cargo packageStandard versioning and registryNeeds a release pipeline
MonorepoAtomic changes and shared CIRepo 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 update in the cache key, or print which SHA was expected on failure to shorten debugging.

Troubleshooting

SymptomCauseFix
Empty directorySubmodule not initializedsubmodule update --init --recursive
detached HEADSubmodule follows pointer onlyCreate a branch when working; push explicitly
Permission errorsCI token cannot access child repoMachine user, deploy key, org SSO
Merge conflictsParent and child changed togetherResolve in the child first, then update parent SHA
Version mismatchSome people pulled only partiallyDocument 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.