Great work!

XP to next level

BugEater
EN

Living With a Submodule Day to Day

Learning Objectives

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

  • Update a submodule to a newer commit and record the change
  • Interpret "modified: tests/fixtures (new commits)" correctly
  • Make a change inside a submodule and publish it in the right order
  • Avoid the two failure modes that break a colleague's build

Updating a Submodule

The fixture library released v2.4.0 and you need it.

cd tests/fixtures
git fetch
git checkout v2.4.0
cd ..

You are now standing on the new version — but the parent repository still pins the old one. Check:

git status
Changes not staged for commit:
        modified:   tests/fixtures (new commits)

Read that message carefully. It does not mean files inside the submodule changed. It means the commit this submodule is checked out at is different from the commit the parent pins. The "change" is the pin itself.

Record it:

git add tests/fixtures
git commit -m "chore: update test fixtures to v2.4.0"
git push

Now the parent points at v2.4.0, and everyone who pulls gets that version — after running git submodule update.

The shorter route, when you just want the submodule's current branch tip:

git submodule update --remote tests/fixtures
git add tests/fixtures
git commit -m "chore: update test fixtures"

Receiving Someone Else's Update

A colleague changed the pin and pushed. You pull:

git pull
git status
        modified:   tests/fixtures (new commits)

The same message, opposite cause: the parent now pins a commit your submodule directory is not on. Your fixtures are still the old ones, and tests may fail confusingly.

git submodule update --init --recursive

That checks out whatever the parent pins. Clean again.

This is not automatic. git pull updates the pin but does not move the submodule. Make it automatic if you work with submodules regularly:

git config --global submodule.recurse true

With that set, git pull, git checkout and git switch all update submodules for you, and an entire category of confusion disappears.

Changing Code Inside a Submodule

Two rules, and the second is the one that breaks builds.

Rule 1: commit inside the submodule first. It is a full repository — branch, commit and push there as normal:

cd tests/fixtures
git switch -c fix/user-fixture-email
# edit, then
git commit -am "fix: correct the email domain in the user fixture"
git push -u origin fix/user-fixture-email
cd ..

Rule 2: push the submodule before you push the parent. The parent will pin your new commit, and if that commit only exists on your machine, everyone else gets:

fatal: remote error: upload-pack: not our ref 4c1f8ab...
Failed to recurse into submodule path 'tests/fixtures'

Their clone is now broken through no fault of theirs. Git can check for you:

git push --recurse-submodules=check

That refuses the parent push while any pinned submodule commit is unpushed. Or have it push them automatically:

git push --recurse-submodules=on-demand

Set check as your default and it becomes impossible to make this mistake.

Detached HEAD Inside a Submodule

cd tests/fixtures
git status
HEAD detached at 8f3c2a1

This is normal, not a problem. The parent pins a commit, not a branch, so submodule update checks out that commit directly.

It matters only when you want to make a change. Committing in detached HEAD there has exactly the risk Module 5 described — the commit belongs to no branch. So before editing anything inside a submodule, switch to a branch first:

git switch -c fix/user-fixture-email

The Two Failure Modes, Summarised

Almost every submodule incident is one of these:

"The tests won't run after cloning." Submodule content was never fetched. git submodule update --init --recursive.

"It works for you but not for me." Someone pushed a parent commit pinning a submodule commit that was never pushed. The fix is on their side — push the submodule — and the prevention is --recurse-submodules=check.

Knowing these two by shape means you diagnose in seconds what otherwise costs an afternoon.

Pro Tip: Turn on submodule.recurse globally and set push.recurseSubmodules to check. Two lines of config, and the two failure modes above stop being possible. It is the highest-value five minutes you can spend on a project that uses submodules.

Key Takeaways

  • Updating a submodule is two commits: one inside it, one in the parent recording the new pin
  • "modified: (new commits)" means the pin and the checkout disagree, not that files changed
  • After pulling a changed pin, run git submodule update --init --recursive
  • git config --global submodule.recurse true makes pull, checkout and switch do it for you
  • Always push the submodule before the parent, or colleagues get a broken clone
  • git push --recurse-submodules=check refuses the push that would cause it
  • Detached HEAD inside a submodule is normal; create a branch before committing there

Quiz

git status in the parent shows "modified: tests/fixtures (new commits)". What changed?

You pulled a colleague's change to the submodule pin. Why do your tests still use the old fixtures?

Why must you push the submodule before pushing the parent?

git status inside a submodule says "HEAD detached at 8f3c2a1". What should you conclude?