It Works on My Machine — The Submodule Was Pinned to an Old Commit
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
- Create a library repository with 2 commits at
/root/gitx5/lib(version.txtgoing from1.0.0to1.1.0). - Create
/root/gitx5/appand pinlibin atvendor/libas a submodule. Leave innotes/protocol.txtthat the first attempt is blocked. - Leave what the parent remembers in
notes/pointer.txt. - On
lib, stack a1.2.0commit and move the submodule to that commit, then leave the detached HEAD and the first character ofsubmodule statusinnotes/detached.txt. - Commit that update to the parent.
- On
lib, stack1.3.0and, with only the submodule moved and the parent uncommitted, take a clone at/root/gitx5/clone, and write innotes/accident.txtwhich version the receiving side sees. - Without
--recurse-submodules, take/root/gitx5/clone2to produce the first character-, and summarize the four first characters innotes/status.txt. - Summarize the rules for using submodules in
notes/report.md.
Notes
- This image has no global git identity. Every time you create a repository, specify
git config user.emailanduser.name. - Fix the commit times with
GIT_AUTHOR_DATEandGIT_COMMITTER_DATE. - Common mistake: when the first attempt is blocked in step 2, thinking the lab is wrong and moving on. Being blocked is normal, and knowing the reason is the assignment of this step.
- Common mistake: committing the parent in step 6. You must take the clone with the parent left uncommitted for the accident to be reproduced.
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.