Great work!

XP to next level

BugEater
EN

Submodules: A Repo Inside a Repo

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what a submodule stores and what it does not
  • Clone a repository that contains submodules, with their content
  • Read .gitmodules and the submodule entry in git status
  • Say when a submodule is the right tool and when it is not

Why Testers Meet Them

Your automation project needs the shared fixture library. Three teams use it, it has its own repository, its own version history, its own reviewers.

You could copy it in — and then it is a fork that drifts. You could publish it as a package — often the right answer, but it needs a registry and a release process. Or you could pin it: reference the other repository from yours, at one exact commit.

That is a submodule, and it is common exactly where QA works: shared test frameworks, fixture data, internal tooling, protocol definitions.

What Is Actually Stored

This is the whole concept, and everything else follows from it.

A submodule is stored in the parent repository as a path and a commit ID. Not a copy of the files. Not a branch name. One commit ID, meaning: at this path lives that other repository, at exactly this commit.

git ls-tree HEAD tests/fixtures
160000 commit 8f3c2a1e94b7d0c1f5a2836b4c9d0e1f2a3b4c5d	tests/fixtures

Mode 160000 marks a submodule — it is neither a file nor a directory but "a commit in another repository".

Alongside it, a tracked file records where that repository lives:

cat .gitmodules
[submodule "tests/fixtures"]
	path = tests/fixtures
	url = https://github.com/example/qa-fixtures.git

.gitmodules gives the URL; the tree entry gives the exact commit. Together they are the complete definition.

The consequence worth internalising: a submodule is pinned, not tracked. The fixture repository can gain fifty new commits and your project still points at the one it was pinned to. Updating is a deliberate act, which is the main reason to use one.

Cloning a Repository With Submodules

The trap that catches everybody once:

git clone https://github.com/example/automation.git
cd automation
ls tests/fixtures
(empty)

A plain clone creates the submodule directory and leaves it empty. The tests fail with "cannot find module", and nothing obviously explains why.

Either clone correctly in the first place:

git clone --recurse-submodules https://github.com/example/automation.git

Or fix it afterwards:

git submodule update --init --recursive

--init registers the submodules from .gitmodules; --recursive handles submodules that themselves contain submodules.

This is the single most common submodule problem, and it looks exactly like a broken project rather than a missing step. If a repository has a .gitmodules file, this is your first check.

Reading the Status

git submodule status
 8f3c2a1e94b7d0c1f5a2836b4c9d0e1f2a3b4c5d tests/fixtures (v2.3.0)

The leading character is the important part:

Prefix Meaning
(space) Checked out at exactly the pinned commit — correct
- Not initialised — run submodule update --init
+ Checked out at a different commit than the parent pins
U Merge conflicts inside the submodule

A + means your submodule directory and your parent repository disagree. Usually you checked out a different commit inside it, or someone else changed the pin and you have not updated. Lesson 4 covers living with that day to day.

When To Use One — and When Not To

Reasonable:

  • Shared test fixtures or a framework several projects consume
  • A vendor library you must build from source at a known commit
  • Any dependency where "exactly this commit" genuinely matters and there is no package registry

Usually a mistake:

  • Splitting one project's own code across repositories for tidiness
  • Anything a normal package manager could handle — npm, pip and Maven all solve this better, with version ranges and dependency resolution
  • Code that changes frequently alongside the parent, where every change becomes two commits in two repositories

Be honest about the cost. Submodules add a step to every clone, a category of confusing status output, and a coordination burden on every update. Reach for a package first, and use a submodule when pinning to a commit is genuinely what you need.

Pro Tip: If you inherit a project with submodules, add git submodule update --init --recursive to its README setup steps and to the CI checkout. Most "the tests won't run for the new hire" incidents involving submodules are that one missing line.

Key Takeaways

  • A submodule stores a path plus one exact commit ID, not a copy of the files
  • .gitmodules records the URL; the tree entry records the pinned commit
  • A plain git clone leaves submodule directories empty
  • Use --recurse-submodules, or git submodule update --init --recursive afterwards
  • git submodule status prefixes: space is correct, - uninitialised, + different commit
  • Submodules pin deliberately — updating is always an explicit act
  • Prefer a package manager; use a submodule when pinning to a commit is the actual requirement

Quiz

What does the parent repository actually store for a submodule?

You cloned a project and its tests/fixtures directory is empty. What happened?

git submodule status shows a + before the commit ID. What does that mean?

When is a submodule usually the wrong choice?