Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Git & GitHub: Submodules vs. Subtrees – The Zero-Fluff Guide to Handling Dependencies**
Source: https://www.fatskills.com/web-development/chapter/tech-git-github-submodules-vs-subtrees-the-zero-fluff-guide-to-handling-dependencies

TECH **Git & GitHub: Submodules vs. Subtrees – The Zero-Fluff Guide to Handling Dependencies**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~8 min read

Git & GitHub: Submodules vs. Subtrees – The Zero-Fluff Guide to Handling Dependencies


1. What This Is & Why It Matters

You’re working on a microservices project where service-A depends on a shared auth-library. The library is in its own repo, and you need to: - Keep it updated (bug fixes, security patches).
- Avoid copy-pasting code (DRY principle).
- Allow independent development (the library team works separately).
- Not break CI/CD when the library changes.

If you ignore this, you’ll end up with: ❌ Version drift – Different services use different versions of the same library.
Merge hell – Manually copying code leads to conflicts.
Broken builds – CI fails because dependencies aren’t where they should be.

Submodules and subtrees are Git’s two main tools for handling dependencies. They let you embed one repo inside another while keeping history, updates, and version control intact.

Real-world scenario:
You’re a DevOps engineer maintaining a monorepo with 10 microservices. Each service depends on a shared logging-library. Instead of duplicating the library in each service, you: - Use a submodule if the library is developed separately (e.g., by another team).
- Use a subtree if the library is tightly coupled to your project and you want to modify it directly.


2. Core Concepts & Components


? Git Submodule

  • Definition: A pointer to a specific commit in another repo. The parent repo tracks the submodule’s commit hash, not its files.
  • Production insight:
  • If the submodule repo is deleted or moved, your project breaks.
  • CI/CD pipelines must run git submodule update --init to fetch dependencies.
  • Security risk: If the submodule repo is compromised, your project pulls malicious code.

? Git Subtree

  • Definition: A full copy of another repo’s files, merged into your project as a subdirectory. History is preserved, but it’s part of your repo.
  • Production insight:
  • No external dependency – everything is self-contained.
  • Downside: Larger repo size (since you’re storing the entire history).
  • Best for: Internal libraries you modify frequently.

? .gitmodules (Submodules only)

  • Definition: A config file in the root of your repo that defines submodule paths and URLs.
  • Production insight:
  • If this file is missing or misconfigured, git submodule update fails.
  • Security: Never commit sensitive URLs (e.g., private repos with tokens in the URL).

? git submodule update

  • Definition: Fetches the latest commit of a submodule (based on the parent repo’s recorded hash).
  • Production insight:
  • Default behavior is dangerous: It checks out a detached HEAD (no branch). Always use --remote to track a branch.

? git subtree add

  • Definition: Pulls a repo into your project as a subdirectory, preserving its history.
  • Production insight:
  • Merge conflicts can happen if the subtree directory already exists.
  • Best practice: Use --prefix to specify the subdirectory name.

? git subtree pull

  • Definition: Updates a subtree with changes from the original repo.
  • Production insight:
  • Squashes history by default (use --squash to avoid bloating your repo).
  • Alternative: git subtree merge (more control over history).

? git subtree push

  • Definition: Pushes changes from your subtree back to the original repo.
  • Production insight:
  • Only works if you have write access to the original repo.
  • Use case: Contributing fixes to an upstream library.

? git submodule foreach

  • Definition: Runs a command in every submodule (e.g., git pull).
  • Production insight:
  • CI/CD lifesaver: Update all submodules in one command.
  • Example: git submodule foreach 'git checkout main && git pull'


3. Step-by-Step Hands-On


Task: Add a dependency to your project using submodules and subtrees.

Prerequisites:
- Git installed (git --version).
- Two repos: - my-project (your main repo).
- shared-library (a dependency, e.g., a utility repo).


Option 1: Using Submodules (Best for External Dependencies)

Step 1: Add the submodule

cd my-project
git submodule add https://github.com/your-org/shared-library.git libs/shared-library
  • libs/shared-library = Where the submodule will live in your project.
  • Git creates a .gitmodules file and a libs/shared-library directory.

Step 2: Commit the submodule

git commit -m "Add shared-library as a submodule"
  • What just happened?
  • Git recorded the exact commit hash of shared-library.
  • The submodule is now locked to that commit (won’t auto-update).

Step 3: Clone a repo with submodules

If someone else clones my-project, they must:


git clone https://github.com/your-org/my-project.git
cd my-project
git submodule update --init --recursive
  • --init = Initializes submodules.
  • --recursive = Fetches nested submodules (if any).

Step 4: Update the submodule to the latest commit

cd libs/shared-library
git checkout main
git pull origin main
cd ../..
git add libs/shared-library git commit -m "Update shared-library to latest main"
  • Why?
  • Submodules don’t auto-update. You must manually pull changes.
  • The parent repo now points to the new commit hash.

Step 5: Make the submodule track a branch (Recommended for CI/CD)

git config -f .gitmodules submodule.libs/shared-library.branch main
git submodule update --remote
  • --remote = Fetches the latest commit from the branch (instead of the recorded hash).
  • CI/CD tip: Add this to your build script to auto-update dependencies.


Option 2: Using Subtrees (Best for Internal Dependencies)

Step 1: Add the subtree

cd my-project
git remote add shared-library https://github.com/your-org/shared-library.git
git subtree add --prefix=libs/shared-library shared-library main --squash
  • --prefix = Where the subtree will live.
  • --squash = Combines all commits into one (keeps history clean).
  • Alternative: Omit --squash to preserve full history (larger repo).

Step 2: Update the subtree with upstream changes

git subtree pull --prefix=libs/shared-library shared-library main --squash
  • What just happened?
  • Git merges the latest shared-library changes into your subtree.
  • --squash prevents history bloat.

Step 3: Push changes back to the original repo

git subtree push --prefix=libs/shared-library shared-library my-feature-branch
  • Use case: You fixed a bug in the subtree and want to contribute back.
  • Requires: Write access to shared-library.


4. ? Production-Ready Best Practices


Security

Use SSH for submodules (not HTTPS) to avoid token leaks:


git submodule add [email protected]:your-org/shared-library.git libs/shared-library

Pin submodules to a commit hash (not a branch) in production to avoid unexpected updates.
Audit .gitmodules – Ensure no private repos are exposed.

Cost Optimization

Subtrees increase repo size – Use --squash to minimize bloat.
Submodules require extra CI steps – Cache them to speed up builds.

Reliability & Maintainability

Document dependencies in a README.md:


## Dependencies
- `libs/shared-library` (submodule, pinned to commit `abc123`)

Use git submodule foreach in CI to update all submodules:


git submodule foreach 'git checkout main && git pull'

Avoid nested submodules – They complicate CI/CD and cloning.

Observability

Monitor submodule updates – Set up GitHub Actions to alert on changes:


name: Check Submodule Updates
on: [push]
jobs:
  check-submodules:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: git submodule update --init --recursive
- run: git submodule status

Log subtree merges – Use git log --grep="subtree" to track changes.


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Not initializing submodules git submodule update fails with fatal: not a git repository Always run git submodule update --init --recursive after cloning.
Committing submodule changes without updating the parent repo Submodule shows as "modified" but git status is clean Run git add <submodule-path> and commit the new hash.
Using --remote without a branch config Submodule updates to the wrong commit Set the branch in .gitmodules: submodule.<name>.branch = main
Forgetting --squash in subtrees Repo size explodes due to full history Always use --squash unless you need full history.
Pushing subtree changes without write access git subtree push fails with Permission denied Fork the repo and push to your fork first.


6. ? Exam/Certification Focus


Typical Question Patterns

  1. "You need to include a third-party library in your project. Which Git feature should you use?"
  2. Answer: Submodules (if the library is external and you don’t modify it).
  3. Trap: Subtrees are better for internal libraries you modify.

  4. "A teammate clones your repo but the submodule is empty. What’s missing?"

  5. Answer: They forgot git submodule update --init --recursive.
  6. Trap: git clone --recurse-submodules is the one-liner alternative.

  7. "How do you update a submodule to the latest commit?"

  8. Answer:
    bash
    cd <submodule-path>
    git pull origin main
    cd ..
    git add <submodule-path>
    git commit -m "Update submodule"
  9. Trap: --remote updates to the branch, not the latest commit.

  10. "You want to contribute changes back to a subtree’s original repo. Which command do you use?"

  11. Answer: git subtree push --prefix=<path> <remote> <branch>
  12. Trap: You need write access to the original repo.

7. ? Hands-On Challenge


Challenge:

You have a repo my-app that depends on utils-lib. The utils-lib team just released a critical bug fix. Update your submodule to the latest commit and verify it works.

Solution:

cd my-app/libs/utils-lib
git checkout main
git pull origin main
cd ../..
git add libs/utils-lib git commit -m "Update utils-lib to latest main" git push

Why it works:
- git pull origin main fetches the latest changes.
- git add updates the parent repo’s pointer to the new commit.


8. ? Rapid-Reference Crib Sheet

Command Purpose Key Flags
git submodule add <url> <path> Add a submodule --branch <name> (track a branch)
git submodule update --init --recursive Initialize submodules --remote (fetch latest from branch)
git submodule foreach '<command>' Run a command in all submodules N/A
git subtree add --prefix=<path> <remote> <branch> Add a subtree --squash (clean history)
git subtree pull --prefix=<path> <remote> <branch> Update a subtree --squash
git subtree push --prefix=<path> <remote> <branch> Push subtree changes upstream N/A
git config -f .gitmodules submodule.<name>.branch <branch> Set submodule branch N/A
⚠️ Submodules track commits, not branches Always pin to a commit in production N/A
⚠️ Subtrees merge history Use --squash to avoid repo bloat N/A


9. ? Where to Go Next

  1. Git Submodules Docs – Official Git book.
  2. Git Subtrees Explained – Atlassian’s guide.
  3. GitHub Submodule Best Practices – GitHub’s recommendations.
  4. Git Submodule vs. Subtree – Deep dive comparison.

Final Pro Tip:

  • Use submodules when you don’t control the dependency (e.g., third-party libraries).
  • Use subtrees when you do control the dependency and want to modify it directly.

Now go update those dependencies—without breaking CI/CD! ?



ADVERTISEMENT