Git Subtree — Alternative
Submodules giving you a headache? Subtree puts code directly in your repo. No empty folders, no detached HEAD — just working code.
Git Subtree merges another repository's code DIRECTLY into your repository. Unlike submodules that store a pointer to an external repo, subtree copies the actual files into your project.
Why this matters: When you clone a repo with submodules, you get empty folders. When you clone a repo with subtrees, you get the actual code. No initialization needed, no detached HEAD, no .gitmodules file to manage.
Advantages over submodules:
- No empty folders — code exists in your repo after clone
- No detached HEAD — you work on a regular branch, not a detached state
- No .gitmodules — one less config file to manage
- Simpler clone — just
git clone, no--recurse-submodulesneeded - Easy removal — just
git rm -r libs/shared, no multi-step deinit
Disadvantages:
- Larger repo size — the subtree code is stored in your repo
- Slower push — pushing changes back upstream requires history splitting
- Complex merges — pulling updates can create merge conflicts
The --squash option is your best friend: it combines the subtree's entire history into one commit before merging, keeping your repo's history clean and size manageable.
# Submodule vs Subtree comparison
# SUBMODULE: Stores a POINTER to external repo
git submodule add https://github.com/company/shared.git libs/shared
# Result: .gitmodules file + pointer (libs/shared is empty until initialized)
# SUBTREE: Stores the ACTUAL CODE in your repo
git subtree add --prefix=libs/shared https://github.com/company/shared.git main --squash
# Result: libs/shared/ has all the files IMMEDIATELY
# No .gitmodules, no empty folders, no detached HEAD
# Key differences:
# | Feature | Submodule | Subtree |
# |----------------|--------------|--------------|
# | Code storage | Pointer | Actual code |
# | Empty folders | YES (bad!) | NO |
# | Detached HEAD | YES (bad!) | NO |
# | Clone simple | Need --recurse| Works normally|
# | Update process | Two-step | One command |
# | Repo size | Small | Larger |
# | Remove easy | NO | YES |
The git subtree add command brings another repository's code into your project. The basic syntax is:
git subtree add --prefix=<path> <repo-url> <branch> --squash
Let's break down each part:
--prefix=libs/shared— the directory where the subtree code will live inside your repo<repo-url>— the URL of the upstream repository you want to include<branch>— which branch of the upstream repo to pull (usuallymain)--squash— combines the subtree's entire history into one commit (recommended)
With --squash (recommended): Git fetches the upstream repo, squashes all its commits into one, then merges that single commit into your repo. Two commits are created: a squashed content commit and a merge commit. Your repo stays clean.
Without --squash: Git imports the ENTIRE commit history of the upstream repo into your repository. This makes your repo larger but allows you to use git log --follow across subtree boundaries.
After adding, the files exist in your repo just like any other files. No special treatment needed. Edit them, commit them, push them — they're YOUR files now.
# Add a subtree (with squash — recommended)
git subtree add --prefix=libs/shared \
https://github.com/company/shared.git main --squash
# What happened:
# 1. Git fetched the shared repo
# 2. Squashed its history into one commit
# 3. Merged the files into libs/shared/
# 4. Created two commits: fetch commit + merge commit
# Verify
ls libs/shared/
# (all files from shared repo — IMMEDIATELY available!)
cat libs/shared/utils.js
# (code is right there, no initialization needed)
# Add WITHOUT squash (imports full history)
git subtree add --prefix=libs/shared \
https://github.com/company/shared.git main
# Imports ALL commit history from shared repo
# Useful if you need git log --follow across subtree boundaries
# Commit message
git log --oneline -2
# abc1234 Merge commit 'sq-shared' into libs/shared
# def5678 Squash 'shared/' content from commit xyz...When the upstream (shared) repository gets updated, you need to pull those changes into your subtree. The command is similar to the add command:
git subtree pull --prefix=libs/shared <repo-url> main --squash
Important: Always use the same --squash option you used when adding the subtree. Mixing squash and non-squash operations creates conflicting histories.
The pull process:
- Git fetches new commits from the upstream repository
- Squashes them (if
--squashis used) into one commit - Merges that commit into your subtree directory
Merge conflicts can occur if you modified the subtree files in your repo AND the upstream also modified the same files. Resolve them like any other merge conflict — edit the files, mark as resolved, and commit.
Speed tip: Add the upstream repo as a named remote to avoid typing the full URL every time and to let Git cache the fetch data.
# Pull updates from the upstream shared repo
git subtree pull --prefix=libs/shared \
https://github.com/company/shared.git main --squash
# If there are no conflicts, this creates a merge commit
# libs/shared/ now has the latest upstream code
# If there ARE conflicts:
# CONFLICT (content): Merge conflict in libs/shared/utils.js
# Resolve like any merge conflict
# Then commit the resolution
# Speed up: save the remote URL
git remote add shared https://github.com/company/shared.git
git subtree pull --prefix=libs/shared shared main --squash
# Shorter command, and Git caches the fetch
# Pull WITHOUT squash (if you added without squash)
git subtree pull --prefix=libs/shared shared main
# Preserves upstream commit historyIf you have push access to the upstream repository, you can push your subtree changes back. This is useful when you fix a bug or add a feature in the shared code and want to share it with other projects.
git subtree push --prefix=libs/shared <repo-url> main
WARNING: git subtree push can be VERY SLOW. Git needs to analyze your entire repository history, extract only the commits that affected the subtree directory, create a separate branch with just those commits, and push that branch upstream. For large repositories, this can take 10+ minutes.
This is the main disadvantage of subtrees. While adding and pulling are fast, pushing back is slow because of the history-splitting process.
Workaround: Use git subtree split to create a dedicated branch with just the subtree history, then push that branch. This separates the slow split operation from the push.
Alternative workflow: Make changes directly in the shared repository and pull them into your main project. This avoids git subtree push entirely.
# Make changes in the subtree directory
cd libs/shared
echo "new feature" >> utils.js
cd ../..
# Commit in your main repo
git add libs/shared
git commit -m "add new feature to shared lib"
#// Push subtree changes back to upstream
git subtree push --prefix=libs/shared shared main
# This is SLOW — Git must split the subtree commits
# For a large repo, this can take 10+ minutes
# Speed up: use a dedicated branch for splitting
git subtree split --prefix=libs/shared -b shared-split
# Creates a branch with just the subtree history
git push shared shared-split:main
# Much faster than subtree push
# Alternative workflow: make changes directly in shared repo
# and pull them into main repo (avoids subtree push entirely)Not every shared code problem needs subtrees or submodules. Choose the right tool for the job:
Subtree is best for: internal shared code that changes frequently, when you need to edit the shared code in-place, when you want a simple clone experience for your team.
Submodule is best for: third-party code you track but rarely modify, when you need to pin exact versions, when repo size matters more than workflow simplicity.
Package registry (npm, Maven, PyPI) is best for: versioned libraries with stable releases, when you don't need to edit the dependency's source, when you want semver versioning.
Monorepo is best for: tightly coupled projects that change together, when you need atomic commits across projects, when the same team works on all projects.
# Decision matrix:
# Use PACKAGE REGISTRY when:
# - Library has stable releases
# - You don't edit the dependency source
# - You want semver versioning
npm install shared-lib@2.3.1
# Use SUBTREE when:
# - You need to edit shared code in your project
# - You want simple clone/build workflow
# - Shared code changes frequently
git subtree add --prefix=libs/shared <url> main --squash
# Use SUBMODULE when:
# - You rarely update the dependency
# - You need to pin exact versions
# - Repo size is critical
# - You're tracking third-party code
git submodule add <url> libs/shared
# Use MONOREPO when:
# - Projects are tightly coupled
# - You need atomic commits
# - Same team works on both projects
# (No submodules, no subtrees, just one repo)
Lo kar liya — Key Points:
- ✅ Git Subtree merges another repository's code directly into your repo, avoiding the submodule pointer model
- ✅ Unlike submodules, subtree code exists in your repository — no empty folders, no detached HEAD, no .gitmodules
- ✅ Add a subtree with git subtree add --prefix=<path> <url> <branch> --squash
- ✅ Pull upstream updates with git subtree pull --prefix=<path> <url> <branch> --squash
- ✅ Push changes back to upstream with git subtree push — but this is SLOW due to history splitting
- ✅ Use --squash to keep the subtree's history out of your main repo, reducing repo size
- ✅ Subtree is better for frequently-updated internal code; submodules are better for rarely-updated third-party code
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