Subprojects

1. Overview

Projects often depend on code from other repositories — shared libraries, frameworks, or configuration templates. Copying the code manually means maintaining every copy independently. Git offers two built-in solutions to include external repositories: submodules and subtrees.

In this chapter you will learn:

2. Submodules

A submodule is a reference to a specific commit in another repository. Git stores only the URL and the commit hash — it does not copy the files into the parent repository until you explicitly initialize and update the submodule.

Submodules

Adding a submodule

$ git submodule add https://github.com/user/lib.git <submodule>
$ git commit -m "Add lib as submodule"

This creates two entries:

After adding, the project looks like this:

project/
├── .gitmodules                ← URL and path for each submodule
├── .git/modules/<submodule>/ ← submodule's Git database
├── <submodule>/              ← submodule files
│   └── ...
└── src/

Cloning a repository with submodules

$ git clone --recurse-submodules https://github.com/user/project.git

If you already cloned without --recurse-submodules:

$ git submodule update --init --recursive

Updating a submodule

$ cd <submodule>
$ git fetch origin
$ git switch main
$ git pull
$ cd ../..
$ git add <submodule>
$ git commit -m "Update lib submodule"

Or update all submodules at once:

$ git submodule update --remote

Removing a submodule

$ git submodule deinit <submodule>       # unregister the submodule
$ git rm <submodule>                     # remove from index and working tree
$ rm -rf .git/modules/<submodule>        # clean up cached module data
$ git commit -m "Remove lib submodule"

Trade-offs

AdvantageDrawback
Native to Git — no extra toolsRequires extra commands (submodule init, update)
Small footprint — commit reference onlyContributors must remember to initialize after cloning
Each submodule has independent historyNested submodules skipped by default (need --recursive)
Pin to a specific versionMerging changes back into the submodule is awkward

3. Subtrees

A subtree is a full copy of another repository — files and history — merged directly into a subdirectory of the parent project. Unlike submodules, the files are part of the parent repository and can be managed with standard Git commands.

Subtrees

Adding a subtree

$ git subtree add --prefix=<subtree> https://github.com/user/lib.git main --squash

The --squash flag collapses the subtree’s history into a single commit, keeping the parent history clean.

Pulling updates

$ git subtree pull --prefix=<subtree> https://github.com/user/lib.git main --squash

Pushing changes back

If you modify subtree files in the parent and want to push them back to the original repository:

$ git subtree push --prefix=<subtree> https://github.com/user/lib.git main

Removing a subtree

A subtree is just a directory — remove it like any other files:

$ git rm -r <subtree>
$ git commit -m "Remove lib subtree"

Unlike submodules, there is no metadata to clean up.

Trade-offs

AdvantageDrawback
No extra commands — files are in the repoIncreases repository size (full copy)
Works with standard clone, pull, pushMust not mix parent and subtree changes in commits
No .gitmodules or metadata filesRequires understanding of merge strategies

4. Which to use?

AspectSubmodulesSubtrees
StorageCommit reference onlyFull file copy
Contributor setupMust run submodule initNothing extra
Update methodManual (submodule update)Standard (subtree pull)
Pin to versionYes — by commit hashNo — always latest at pull time
Repo size impactMinimalLarger
Best forLibraries pinned to a versionFrequently modified dependencies

Use submodules for component-based development where you depend on a specific version of an external repository and rarely modify the dependency. Use subtrees for system-based development where you want a full copy of the code and expect to modify it alongside your project.

5. Other tools

Exercises

All exercises use the concepts-lab repository from previous chapters.

Exercise 1: Add and use a submodule

Task: Add an external repository as a submodule, update it, and verify the pinned commit.

Steps:

  1. In concepts-lab, add a public repository as a submodule: git submodule add https://github.com/braboj/tutorial-testing.git libs/testing
  2. Run git status — note the new .gitmodules file and the libs/testing entry
  3. Commit with the message Add testing as submodule
  4. Run cat .gitmodules to see the URL and path
  5. Run git submodule status to see the pinned commit hash
  6. Enter libs/testing and run git log --oneline -3 to see its history
  7. Back in the parent, run git diff --cached --submodule to confirm the reference

Verify:

.gitmodules lists the submodule. git submodule status shows the pinned commit hash. The submodule directory contains the external repository’s files.

Exercise 2: Clone a repository with submodules

Task: Simulate a fresh clone and verify submodules need explicit initialization.

Prerequisite: concepts-lab must be pushed to GitHub (see Remote Repositories, Exercise 1).

Steps:

  1. Clone concepts-lab into a new directory without --recurse-submodules: git clone <url> concepts-lab-fresh
  2. Enter concepts-lab-fresh/libs/testing — it should be empty
  3. Run git submodule update --init
  4. Check libs/testing again — files should now be present
  5. Run git submodule status to confirm the correct commit is checked out

Verify:

Before submodule update --init, the directory is empty. After, it contains the submodule’s files at the pinned commit.

Exercise 3: Add a subtree

Task: Add an external repository as a subtree and verify the files are part of the parent repository.

Steps:

  1. In concepts-lab, add a subtree: git subtree add --prefix=libs/docs https://github.com/braboj/tutorial-testing.git main --squash
  2. Run git log --oneline -3 — note the squash merge commit
  3. List libs/docs/ to confirm the files are present
  4. Run git status — the working tree should be clean (files are committed)
  5. Edit a file in libs/docs/, commit the change
  6. Run git log --oneline -5 to see both the subtree add and your edit

Verify:

The subtree files are committed directly in the parent repository. git log shows the squash merge and your edit as normal commits. No .gitmodules file was created.

Quiz

Q1. What does a submodule store in the parent repository?

Q2. What happens when you clone a repository with submodules without using --recurse-submodules?

Q3. What is the main advantage of subtrees over submodules?

Q4. When should you prefer submodules over subtrees?

Answers

  1. B — A URL and a pinned commit hash
  2. B — The submodule directories exist but are empty
  3. C — Contributors need no extra commands — files are already in the repo
  4. C — When you need to pin to a specific version and rarely change the dependency