|$ curl https://forge-ai.dev/api/markdown?path=docs/git/submodules
$cat docs/git-submodules.md
updated Recently·25 min read·published

Git Submodules

GitIntermediate🎯Free Tools
Introduction

Git submodules allow you to embed one Git repository inside another. The parent repo tracks a specific commit of the submodule, not a branch — ensuring reproducible builds across environments. Submodules are useful for shared libraries, vendor code, and monorepo-like structures without the overhead.

Adding & Cloning Submodules
submodules.sh
Bash
1# Add a submodule
2git submodule add https://github.com/org/shared-lib.git libs/shared-lib
3
4# This creates:
5# .gitmodules — submodule configuration
6# libs/shared-lib/ — the submodule directory
7
8# Add with a specific branch
9git submodule add -b main https://github.com/org/shared-lib.git libs/shared-lib
10
11# Commit the submodule reference
12git add .gitmodules libs/shared-lib
13git commit -m "Add shared-lib submodule"
14
15# Clone a repo with submodules
16git clone https://github.com/org/main-project.git
17cd main-project
18git submodule init
19git submodule update
20
21# One-liner: clone with submodules
22git clone --recurse-submodules https://github.com/org/main-project.git
23
24# Update to latest commits
25cd libs/shared-lib
26git pull origin main
27cd ../..
28git add libs/shared-lib
29git commit -m "Update shared-lib to latest"

info

Always use --recurse-submodules when cloning. Without it, the submodule directories will be empty — one of the most common surprises for new Git users.
Managing Submodules
managing-submodules.sh
Bash
1# List all submodules
2git submodule status
3# abc1234 libs/shared-lib (heads/main)
4
5# Update all submodules to their tracked commits
6git submodule update --init --recursive
7
8# Update to the latest commit on the tracked branch
9git submodule update --remote --merge
10
11# Update a specific submodule
12git submodule update --remote libs/shared-lib
13
14# See what changed in a submodule
15cd libs/shared-lib
16git log --oneline HEAD..origin/main
17cd ../..
18
19# Remove a submodule
20git submodule deinit -f libs/shared-lib
21git rm -f libs/shared-lib
22rm -rf .git/modules/libs/shared-lib
23git add .gitmodules
.gitmodules
Bash
1# .gitmodules file (auto-generated)
2[submodule "libs/shared-lib"]
3 path = libs/shared-lib
4 url = https://github.com/org/shared-lib.git
5 branch = main
6
7# CI/CD: always initialize submodules
8# GitHub Actions:
9# - uses: actions/checkout@v4
10# with:
11# submodules: recursive
12
13# Docker: copy .gitmodules for multi-stage builds
14# COPY .gitmodules .
15# RUN git submodule update --init --recursive
Submodules vs Subtrees
AspectSubmodulesSubtrees
SetupSimpleModerate
TrackingPinned to a commitFull history merge
CloningRequires init/updateAutomatic (inlined)
Contributing backSeparate workflowPush to subtree remote
Repo sizeStays smallGrows with history
subtree.sh
Bash
1# Add a subtree
2git subtree add --prefix=libs/shared-lib https://github.com/org/shared-lib.git main --squash
3
4# Update subtree
5git subtree pull --prefix=libs/shared-lib https://github.com/org/shared-lib.git main --squash
6
7# Push changes back to the subtree repo
8git subtree push --prefix=libs/shared-lib https://github.com/org/shared-lib.git feature/improvement

best practice

Use submodules when you need precise version pinning and the submodule has its own development lifecycle. Use subtreeswhen you want simpler cloning and don't mind the larger repo size.
Advanced Patterns

Extra depth for production teams — conflict strategies, automation, and recovery.

Automation-friendly flags

automation.sh
Bash
1git status --porcelain=v1
2git diff --name-only --diff-filter=ACMR
3git log -1 --pretty=format:%H
4git merge-base HEAD origin/main
5git rev-list --count origin/main..HEAD

Recovery drill

recovery.sh
Bash
1git reflog | head -20
2git fsck --lost-found | head
3git branch rescue HEAD@{1}
4git log --oneline rescue -5
🔥

pro tip

Practice recovery in /tmp labs before you need it on a deadline.
Production Checklist
  • No secrets in history for this change set
  • CI green on the PR
  • Rebased or merged with latest main
  • Rollback plan: revert SHA known
  • Tags/releases updated if needed
prod-check.sh
Bash
1git status -sb
2git log --oneline origin/main..HEAD
3git diff --check
4git rev-parse HEAD
Additional Examples
more-a.sh
Bash
1# Cherry-pick a range onto a release branch
2git switch release/1.2
3git cherry-pick abc123^..def456
4# conflict?
5git status
6# fix, then:
7git add -A && git cherry-pick --continue
8# or abort:
9# git cherry-pick --abort
more-b.sh
Bash
1# Bisect with a script
2git bisect start
3git bisect bad HEAD
4git bisect good v1.0.0
5git bisect run ./scripts/test-bug.sh
6git bisect reset
more-c.sh
Bash
1# Submodule bump
2git submodule update --remote --merge libs/shared
3git add libs/shared
4git commit -m "chore(deps): bump shared submodule"
5git submodule status
more-d.sh
Bash
1# Workflow: trunk-based short PR
2git fetch origin
3git switch -c fix/timeout origin/main
4# change + test
5git commit -am "fix: request timeout"
6git push -u origin HEAD
7gh pr create --fill
8gh pr checks
9gh pr merge --squash --delete-branch
Submodule Lifecycle Deep Dive

Submodules pin another repository at an exact commit. The parent stores the gitlink mode 160000 and the SHA — not the submodule files themselves.

Add & clone

sm-add.sh
Bash
1git submodule add git@github.com:org/lib.git libs/lib
2git commit -m "chore: add lib submodule"
3# Fresh clone:
4git clone --recurse-submodules git@github.com:org/app.git
5# Or after clone:
6git submodule update --init --recursive

Update & bump

sm-bump.sh
Bash
1cd libs/lib
2git fetch
3git switch main
4git pull --ff-only
5cd ../..
6git add libs/lib
7git commit -m "chore(deps): bump lib to $(git -C libs/lib rev-parse --short HEAD)"
8# Or:
9git submodule update --remote --merge libs/lib
10git add libs/lib && git commit -m "chore(deps): bump lib"

Remove

sm-remove.sh
Bash
1git submodule deinit -f libs/lib
2git rm -f libs/lib
3rm -rf .git/modules/libs/lib
4git commit -m "chore: remove lib submodule"

Pitfalls

  • Forgetting --recurse-submodules on clone leaves empty dirs
  • Detached HEAD inside submodule is normal — checkout a branch before committing there
  • Prefer packages (npm/pip) when you do not need exact git history coupling

warning

Submodules increase onboarding cost. Prefer them for true multi-repo version pins (firmware, docs themes), not for every shared library.
sm-lab.sh
Bash
1rm -rf /tmp/sm && mkdir -p /tmp/sm/{lib,app}
2cd /tmp/sm/lib && git init -b main && echo lib > f && git add f && git commit -m lib
3cd /tmp/sm/app && git init -b main && echo app > f && git add f && git commit -m app
4git submodule add /tmp/sm/lib libs/lib
5git commit -m "add submodule"
6git submodule status
Submodules vs Subtree vs Packages
  • Submodule: pin external repo SHA; separate history
  • Subtree: vendor history into monorepo (heavier)
  • Package registry: usually best for libraries
$Blueprint — Engineering Documentation·Section ID: GIT-SM-01·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.