TT Lab
Get started
Learn Learning paths Courses

Git in Practice

It Works on My Machine — The Submodule Was Pinned to an Old Commit

Continue in TT Lab

Goal

You pin a library repository into a parent repository as a submodule, and create yourself the accident in which a colleague gets old code when the update is not committed to the parent.

Why it matters

Almost every accident with submodules comes from one misunderstanding — the misunderstanding that the parent points at a branch. What goes into the parent's tree is one entry of mode 160000, and its value is a commit hash. Since it is a commit and not a branch, a submodule is always checked out on a detached HEAD, and even if new commits pile up in the library, the parent does not follow by itself. Instead of hearing that fact as an explanation, this lab has you pull it out of the history yourself. And once you also confirm that git clone does not fetch submodules by default, the common symptom "the build breaks only in CI" is explained all at once.

Steps

  1. Create a library repository with 2 commits at /root/gitx5/lib (version.txt going from 1.0.0 to 1.1.0).
  2. Create /root/gitx5/app and pin lib in at vendor/lib as a submodule. Leave in notes/protocol.txt that the first attempt is blocked.
  3. Leave what the parent remembers in notes/pointer.txt.
  4. On lib, stack a 1.2.0 commit and move the submodule to that commit, then leave the detached HEAD and the first character of submodule status in notes/detached.txt.
  5. Commit that update to the parent.
  6. On lib, stack 1.3.0 and, with only the submodule moved and the parent uncommitted, take a clone at /root/gitx5/clone, and write in notes/accident.txt which version the receiving side sees.
  7. Without --recurse-submodules, take /root/gitx5/clone2 to produce the first character -, and summarize the four first characters in notes/status.txt.
  8. Summarize the rules for using submodules in notes/report.md.

Notes

Create a shared library repository

Create a library repository with 2 commits at /root/gitx5/lib (version.txt going from 1.0.0 to 1.1.0).

Create it with git init -b main and set user.email and user.name for each repository. Just change the one line of version.txt and stack two commits — it becomes the marker that lets you see with your own eyes, in later steps, which commit this value was pinned to.

A local-path submodule is blocked by default

Create /root/gitx5/app and pin lib in at vendor/lib as a submodule. Leave in notes/protocol.txt that the first attempt is blocked.

If you simply run git submodule add /root/gitx5/lib vendor/lib, it is blocked with transport 'file' not allowed. This is the default since CVE-2022-39253. Do it again with git -c protocol.file.allow=always submodule add ..., and after pinning it in, you must commit .gitmodules and vendor/lib.

The one line that went into the parent's tree

Leave what the parent remembers in notes/pointer.txt.

The single line from git ls-tree HEAD vendor/lib is all there is to a submodule. Write three things together: what the mode is, which commit of lib that value is, and what is written in .gitmodules. The point is that the branch name is nowhere to be found.

A submodule is always a detached HEAD

On lib, stack a 1.2.0 commit and move the submodule to that commit, then leave the detached HEAD and the first character of submodule status in notes/detached.txt.

In lib, after committing 1.2.0, inside app/vendor/lib run git fetch origin and then git checkout that commit. See what the first line of git status is inside it, and what the first character of git submodule status in the parent changes to. You do not commit the parent yet.

Commit the update to the parent

Commit that update to the parent.

If you run git add vendor/lib in the parent, a single gitlink value is staged. It is not the files in the directory but one line of commit hash that changes, so if you look at git diff --cached, only two Subproject commit lines appear.

An update you did not commit is yours alone

On lib, stack 1.3.0 and, with only the submodule moved and the parent uncommitted, take a clone at /root/gitx5/clone, and write in notes/accident.txt which version the receiving side sees.

In lib, commit 1.3.0 and move app/vendor/lib there. If you look at git status in the parent, vendor/lib shows up as modified, but do not commit it. In that state, run git -c protocol.file.allow=always clone --recurse-submodules /root/gitx5/app /root/gitx5/clone and compare the version.txt on both sides.

The four things the first character says

Without --recurse-submodules, take /root/gitx5/clone2 to produce the first character -, and summarize the four first characters in notes/status.txt.

git clone /root/gitx5/app /root/gitx5/clone2 does not fetch submodules. Run git submodule status inside it and see what the first character is. If you also write down the command that initializes it, you can use it right away the next time CI breaks.

Rules for using submodules

Summarize the rules for using submodules in notes/report.md.

Turn the three things you built yourself this time — what the parent remembers, the detached HEAD, and the accident of a missed update — into rules. Also write what you must do when you clone, and how to read a one-line Subproject commit diff in review.