TT Lab
Get started
Learn Learning paths Courses

Git in Practice

The Parent Remembers a Commit, Not a Branch

Continue in TT Lab

One-line summary

What the parent repository records for a submodule is a single commit hash. Not a branch. So a submodule is always checked out on a detached HEAD, and if you do not commit to the parent the fact that you pulled the latest, that change remains yours alone.

Why this is needed

It is common for several services to share the same code. Things like an internal common library, protocol definitions and configuration templates. If you copy it, the copies quickly diverge, and if you package it and publish it, you get one more deployment procedure. A submodule is the answer in between — it pins another repository into one directory of your own repository.

The problem is that people start using it without knowing exactly what "pin" means. Many people think "the parent points at the main branch of that library", but it does not. In the parent's tree, at that place goes one entry with mode 160000, and its value is a commit hash. The branch name is not written in .gitmodules by default either.

$ git ls-tree HEAD vendor/lib
160000 commit 9f3c1a4... 	vendor/lib

This single line is all there is to a submodule. If you know this, every behavior that follows is explained.

How it works

.gitmodules is a configuration file that records only where to fetch from (url) and where to put it (path), and it is an ordinary tracked file. Which version it actually is is decided by the 160000 entry in the tree. The two have different roles, and so they are committed separately.

If you go inside the submodule directory and run git status, you get this.

HEAD detached at 9f3c1a4

Because the parent remembers a commit rather than a branch, the checkout is also done by commit. If you simply commit here, it becomes a commit attached to no branch and is hard to find later. If you have work to do in the submodule, you must first get onto a branch.

The first character of git submodule status tells the state.

First character Meaning
(blank) The commit the parent recorded and the actual checkout are the same
+ The submodule is at a different commit from the one the parent recorded
- Not yet initialized (the directory is empty)
U A merge conflict is unresolved

You meet - especially often. Because git clone does not fetch submodules by default, if you build right after cloning, it fails because of the empty directory. Fetch it with git clone --recurse-submodules, or after cloning, run git submodule update --init --recursive. If you keep git config submodule.recurse true turned on, checkout and pull follow along into the submodule.

+ is the seed of an accident. You pulled the latest in the submodule and confirmed that it works well, but if you do not commit that change to the parent with git add vendor/lib, the new code is used only on your machine. When a colleague clones, they get the old commit the parent recorded. The symptom is the classic "it works on my computer", and it takes a long time to notice that the cause is the submodule.

One more thing. Since git 2.38.1, fetching submodules from a local path or file:// is blocked by default (CVE-2022-39253). This is because a malicious repository could use .gitmodules to make another person's machine take its files. When practicing with local paths or using a cache repository in CI, you must state -c protocol.file.allow=always explicitly.

What it looks like in the field

What you will do in the next lab

You create a library repository and a parent repository yourself and pin the library in as a submodule. You first meet the fact that local paths are blocked by default, and you confirm with your own eyes that a 160000 entry goes into the parent's tree. Then you stack a new commit on the library and, with only the submodule moved and the parent uncommitted, take a clone, reproducing exactly the accident in which a colleague gets the old code.

The official documentation is git-submodule and Pro Git 7.11 Submodules.