Git Submodules — Setup & Nightmares
Submodules ek aisi cheez hai jo dikhne mein simple lagti hai, lekin nightmare ban jaati hai. Submodules seekho, ya toh sahi se seekho ya subtree use karo.
A submodule is a Git repository embedded INSIDE another Git repository. The parent repo does NOT store the submodule's code — it stores a pointer (a specific commit hash) to the submodule.
Use cases for submodules:
- Shared libraries used across multiple projects
- Third-party code you want to track closely
- Design systems used by multiple applications
When you clone the parent repo, the submodule directory is EMPTY by default — you must explicitly initialize and update it.
Submodules are tracked by two things:
- The commit hash stored in the parent repo (which exact version to use)
- The
.gitmodulesfile which stores the URL and path configuration
They create a tight coupling between two repositories that must be carefully managed.
# Add a submodule to your project
git submodule add https://github.com/company/shared-lib.git libs/shared
# What Git creates:
# 1. .gitmodules file (stores URL and path)
cat .gitmodules
# [submodule "libs/shared"]
# path = libs/shared
# url = https://github.com/company/shared-lib.git
# 2. An entry in the index (the commit hash pointer)
git ls-files -s libs/shared
# 160000 commit abc1234... libs/shared
# 160000 mode = Git special mode for submodules
# 3. Clones the submodule repo into libs/shared/
ls libs/shared/
# (files from shared-lib repo)
# Commit the submodule addition
git add .gitmodules libs/shared
git commit -m "add shared-lib submodule"The most common submodule problem: cloning the parent repo gives you empty submodule directories.
git clone does NOT automatically initialize submodules by default. The submodule directory exists but contains no files.
The fix: git submodule init + git submodule update, OR clone with --recurse-submodules.
This happens to EVERY new developer on the team until they learn the pattern. CI/CD pipelines also need --recurse-submodules or they will build with missing dependencies.
# NIGHTMARE: Clone without submodules
git clone https://github.com/company/main-app.git
cd main-app
ls libs/shared/
# (EMPTY! No files!)
# The app will not build — missing shared library
npm run build
# Error: Cannot find module shared-lib
# FIX 1: Initialize and update after clone
git submodule init
git submodule update
# Now libs/shared/ has files!
# FIX 2: Clone with --recurse-submodules (BEST)
git clone --recurse-submodules https://github.com/company/main-app.git
# Submodule libs/shared registered
# Cloning into libs/shared...
# Everything works immediately!
# FIX 3: After forgetting --recurse-submodules
git submodule update --init --recursive
# --init = initialize if not yet
# --recursive = handle nested submodules
# This is the "oh I forgot" fix
# CI/CD: Always use --recurse-submodules
# GitHub Actions
- uses: actions/checkout@v4
with:
submodules: recursive
git submodule update --init --recursive. New developers should not have to discover this by broken builds. Document it in your README!The parent repo stores a SPECIFIC commit hash for the submodule. When the submodule repo is updated elsewhere, the parent still points to the OLD commit.
You must MANUALLY update the submodule pointer in the parent repo. This is not automatic.
If you pull the parent and someone updated the submodule, you must run git submodule update to get the new version locally.
If you forget, your build uses outdated code, causing bugs that are hard to diagnose. "It works on my machine" — because one developer updated and another did not.
# Update a submodule to latest commit
cd libs/shared
git fetch origin
git checkout origin/main # or specific branch/tag
# Go back to parent repo
cd ../..
git add libs/shared # stage the new pointer
git commit -m "update shared-lib to latest"
# When your teammate pulls the parent:
git pull origin main
# They get the new pointer, BUT...
ls libs/shared/ -la
# Still shows the OLD version!
# They MUST run:
git submodule update
# Now the submodule matches the pointer
# Or update automatically on pull:
git config --global submodule.recurse true
# Now git pull automatically updates submodules
# Or pull with flag:
git pull --recurse-submodulesSubmodules are checked out in a DETACHED HEAD state by default. This means the submodule is at a specific commit, not on any branch.
If you make changes in the submodule while in detached HEAD, those changes are not on any branch. If you checkout a different commit or run submodule update, your changes can be lost.
To work on a submodule: first checkout a branch (git checkout main), then make changes.
After committing in the submodule, you must also update the parent repo's pointer. This two-step commit process is a major source of confusion.
# Check submodule status
cd libs/shared
git status
# HEAD detached at abc1234 ← DANGER!
# Any commits here could be lost!
# Step 1: Checkout a branch before making changes
git checkout main
git pull origin main # get latest
# Step 2: Make your changes
echo "new feature" >> lib.js
git add . && git commit -m "add new feature"
git push origin main
# Step 3: Go back to parent and update the pointer
cd ../..
git add libs/shared
git commit -m "update shared-lib: add new feature"
git push origin main
# If you forgot to checkout a branch:
cd libs/shared
# (in detached HEAD)
echo "important work" >> lib.js
git add . && git commit -m "important work"
# This commit is on NO branch!
# SAVE IT:
git checkout -b my-fix
git push origin my-fix
git status inside a submodule before making changes. If it says "HEAD detached", run git checkout main first. If you accidentally commit in detached HEAD, DO NOT leave the directory — create a branch immediately with git checkout -b save-my-work to preserve your commit.Removing a submodule requires multiple steps — there is no single git submodule remove command (until Git 2.31+).
Steps: deinit the submodule, delete the .git/modules entry, remove the directory, commit.
Even with Git 2.31+, git rm <path> handles most of it but you still need to clean up .git/modules.
The complexity of removal makes teams hesitant to use submodules. Consider: if adding and removing are both complex, is this the right tool?
# Removing a submodule (before Git 2.31)
# Step 1: Deinit the submodule
git submodule deinit -f libs/shared
# Step 2: Remove the .git/modules entry
rm -rf .git/modules/libs/shared
# Step 3: Remove the directory and index entry
git rm -f libs/shared
# Step 4: Remove .gitmodules (if no more submodules)
git rm .gitmodules
# Step 5: Commit
git commit -m "remove shared-lib submodule"
# Git 2.31+: Simpler but still multi-step
git rm libs/shared
# Removes the directory and deinit
# Still need to clean .git/modules manually sometimes
git commit -m "remove shared-lib submodule"
# Alternative: Use git subtree instead (next chapter)
# Subtrees are easier to add, update, and remove
--recurse-submodules, always run git submodule update after pulling, and always checkout a branch before editing. Better yet, consider Git Subtree or package registries (npm, Maven, PyPI) for shared code.Lo kar liya — Key Points:
- ✅ A submodule is a Git repository embedded inside another, tracked by a commit hash pointer — not the code itself
- ✅ The #1 nightmare: cloning without --recurse-submodules gives empty submodule directories
- ✅ The #2 nightmare: submodule pointers become stale when the submodule is updated elsewhere
- ✅ The #3 nightmare: submodules are checked out in detached HEAD state by default
- ✅ Updating a submodule requires two commits: one in the submodule repo, one in the parent repo
- ✅ Always clone with --recurse-submodules and run git submodule update after pulling
- ✅ Removing submodules is complex (deinit, rm .git/modules, git rm) — consider Git Subtree instead
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login