Submodules & Subtrees

Course 3 · Ch 4
Submodules and Subtrees
Proper setup for embedding one repository inside another — and a clean alternative that avoids the gitlink trap entirely

This chapter closes the loop on something teased across the entire series — Course 1, Chapter 9's broken-gitlink incident, and Course 2, Chapter 10's full diagnosis. Both showed what happens when a folder ends up tracked as a gitlink by accident. This chapter covers the two real, supported ways to do this deliberately: submodules, set up correctly this time, and subtrees, an entirely different approach that sidesteps the gitlink mechanism altogether.

Submodules, Done Properly

Chapter 1 explained the mechanism: a submodule is a gitlink (tree entry, mode 160000) pointing at a commit hash in a separate repository, plus a .gitmodules file recording the URL and path. Adding one correctly:

$ git submodule add https://github.com/someone/shared-ui-components.git libs/ui
Cloning into 'libs/ui'...
$ git status
new file: .gitmodules
new file: libs/ui
$ git commit -m "Add shared-ui-components as a submodule"
.gitmodules ← records URL + path, the thing the broken incident was missing entirely
[submodule "libs/ui"]
  path = libs/ui
  url = https://github.com/someone/shared-ui-components.git

The part everyone gets wrong: cloning a repo WITH submodules

A plain git clone creates the gitlink-tracked folder but leaves it empty — the submodule's actual content needs a separate, explicit step.

$ # Either clone with this flag from the start...
$ git clone --recurse-submodules https://github.com/you/project.git

$ # ...or, after a plain clone, populate them after the fact
$ git submodule update --init --recursive
An empty submodule folder after a plain clone is normal, not broken
Unlike the genuinely broken gitlink from earlier courses (pointing at a commit hash from a vanished repository), an empty-but-correctly-configured submodule folder is expected behaviour — the fix is simply git submodule update --init, not a diagnostic investigation. Checking for a valid .gitmodules entry (Course 2, Chapter 10's key distinguishing signal) tells you immediately which situation you're in.

Updating a submodule to a newer commit

$ cd libs/ui
$ git pull origin main
$ cd ../..
$ # The parent repo sees this as a change — the gitlink now points at a newer commit
$ git add libs/ui
$ git commit -m "Update ui submodule to latest"

Subtrees — A Different Approach Entirely

git subtree takes the opposite philosophy: instead of a pointer to another repository, it actually copies the other project's files directly into your repo, as ordinary tracked content — no gitlink, no .gitmodules, no separate clone step for anyone using the repo.

$ git subtree add --prefix=libs/ui https://github.com/someone/shared-ui-components.git main --squash

After this, libs/ui contains ordinary tracked files — anyone cloning your repo gets them automatically, with zero extra steps, exactly like every other file in the project.

Submodules vs Subtrees

🔗
Submodule
Stored as: a pointer (gitlink) to a separate repository.

Cloning: requires an extra step (--recurse-submodules or submodule update).

Best for: when you want to track an exact upstream version and occasionally pull in updates from the original project cleanly.
📋
Subtree
Stored as: a real, ordinary copy of the files, fully merged into your repo's own history.

Cloning: works exactly like any other clone — nothing extra required.

Best for: simpler day-to-day use, especially when contributors shouldn't need to learn submodule-specific commands at all.

Choosing Between Them

SituationUse
Need to track an exact upstream version, pull updates occasionallySubmodule
Want zero extra steps for anyone cloning the repoSubtree
Team unfamiliar with submodule workflow, wants simplicitySubtree
Need to occasionally contribute changes back upstreamSubmodule — keeps a clean separate history to push from
Just need a folder with normal tracked content, no real "other project"Neither — this is the broken-gitlink trap; just track it normally
The honest summary: most projects need neither
Both tools solve a specific, relatively narrow problem — genuinely sharing code between separate, independently-versioned repositories. The far more common situation (a folder of files that's just part of your one project) needs neither — it should simply be tracked normally, exactly the fix applied in Course 2, Chapter 10 to undo the accidental gitlink. Reach for these tools only when there's a real second repository genuinely worth keeping separate.

Command Reference

CommandWhat it does
git submodule add <url> <path>Adds a properly configured submodule, creating/updating .gitmodules
git clone --recurse-submodules <url>Clones a repo and populates its submodules in one step
git submodule update --init --recursivePopulates submodules after a plain clone left them empty
git subtree add --prefix=<path> <url> <branch> --squashCopies another repo's files directly into your repo as ordinary content

Chapter 4 Quick Reference

  • A proper submodule always has a matching .gitmodules entry — the exact thing the broken incident lacked
  • Plain git clone leaves submodules empty — use --recurse-submodules or submodule update --init afterward; this is normal, not broken
  • git subtree copies another project's files directly into your repo — no gitlink, no extra clone steps for anyone
  • Submodule — best when tracking an exact upstream version and occasionally contributing back
  • Subtree — best for simplicity, zero extra steps for contributors
  • Most projects need neither — a folder of files that's just part of your project should be tracked normally
  • Next chapter: advanced GitHub Actions — matrix builds, secrets, caching, and deployment pipelines