What does a Git submodule actually store inside the parent repository's commit?
answer
- The parent stores a reference, not the files
- A single object id per submodule path
- A special tree entry mode
- 160000, plus a tracked mapping file
basics
~20 sOnly a commit SHA. The parent's tree holds a special gitlink entry, mode 160000, recording which commit of the other repository belongs at that path. The URL to fetch it from lives separately, in the tracked .gitmodules file.
solid answer
~40 sA submodule is not a copy of another project's files — the superproject's tree contains a single **gitlink** entry at that path, with mode `160000`, whose value is one commit SHA from the other repository. None of the submodule's blobs or trees are stored in the parent. That is why a submodule pins an exact commit: checking out the superproject tells you precisely which commit of the dependency belongs with it. The SHA alone is not enough to fetch anything, so the URL lives in `.gitmodules`, an ordinary tracked file at the top of the superproject mapping each submodule's name to its path and URL. On `git submodule init` that URL is copied into the local `.git/config`, which is why editing `.gitmodules` later requires `git submodule sync` to take effect.
code
console · 8 lines$ git ls-tree HEAD
100644 blob 8d4f2a1... .gitmodules
040000 tree c19ab77... src
160000 commit 9fceb02... vendor/liblog
$ git config -f .gitmodules --list
submodule.vendor/liblog.path=vendor/liblog
submodule.vendor/liblog.url=https://example.invalid/liblog.gitgo deeper
Be able to say that the parent repository records which commit of the other repository to use, and that the address it is fetched from is in a tracked file. Do not claim the files themselves are stored.
Explain the mechanics: a gitlink tree entry of mode 160000 holding one commit id, plus .gitmodules mapping name to path and URL, and the copy of the URL into local config that init performs.
Draw the operational consequences: an unpushed submodule commit makes the superproject commit unusable for everyone else, URL changes need an explicit sync, and gitlink diffs are opaque in review unless configured otherwise.
Own the tradeoff this data model creates: exact, auditable pinning at the cost of a second repository per dependency, opaque review diffs, and a coordination burden on every bump. Decide when that precision is worth the coordination.
## The gitlink A Git tree object lists entries, each with a mode, a type, an object id and a name. Regular files are mode `100644`, executables `100755`, directories are `040000` trees, symlinks `120000`. A submodule adds one more: mode `160000`, an entry whose object id is a **commit** rather than a blob or tree. This is called a gitlink. That single line is the entire representation of the submodule in the parent's history. Run `git ls-tree HEAD` in a superproject and you will see something like: `160000 commit 9fceb02... vendor/liblog` No file content from the other project is stored in the superproject's object database. The commit id points into a *different* repository, and the superproject has no way to resolve it on its own. ## .gitmodules Because a SHA is not a location, the superproject also tracks a plain file at its root called `.gitmodules`, in Git's config format. Each submodule gets a section keyed by its name, holding at minimum `path` and `url`, optionally `branch` (used by remote-tracking updates), `update`, `shallow`, or `ignore`. Crucially `.gitmodules` is *content* — it is committed, reviewed and merged like any other file. The gitlink and `.gitmodules` answer different questions: the gitlink says *which commit*, `.gitmodules` says *from where*. ## Where the URL ends up locally `git submodule init` reads `.gitmodules` and copies the URL into the superproject's local `.git/config` as `submodule.<name>.url`. This indirection exists so an individual can point a submodule at a fork or a mirror without changing the shared file. The consequence trips people up constantly: after someone changes a URL in `.gitmodules`, everyone else's local config still holds the old value until they run `git submodule sync`, which re-copies the URL from `.gitmodules` into `.git/config`. ## Where the submodule's own repository lives In modern Git, the submodule's object database is not kept in a nested `.git` directory inside the working tree. It lives under the superproject's `.git/modules/<name>/`, and the submodule's working directory contains a `.git` **file** — a one-line text file containing `gitdir: ../.git/modules/<name>`. This makes the submodule's checkout disposable: removing the directory does not destroy its history. `git submodule absorbgitdirs` migrates an older nested layout into this scheme. ## What a change looks like in review Because the gitlink is one object id, changing the submodule to a different commit shows up as a one-line diff: ``` -Subproject commit 9fceb02d... +Subproject commit 1a7fdc3b... ``` That is both the strength and the weakness. It is exact and tiny; it is also opaque, because the reviewer cannot see what changed between those two commits without going into the submodule. Setting `diff.submodule` to `log` makes Git summarise the commits between the two SHAs instead of printing bare ids, and `status.submoduleSummary` does the same for `git status`. ## Consequences that follow from the model - **Pinning is exact.** The superproject records a commit, not a branch or a version range, so a checkout of any historical superproject commit names exactly the dependency commit it was built against. - **Nothing is vendored.** The submodule's content is never in the parent's objects, so a clone of the superproject alone gives you an empty directory at that path until the submodule is fetched. - **Two repositories, two histories.** A commit inside the submodule is invisible to the superproject until you stage the new gitlink; conversely, updating the gitlink does nothing to the submodule's own branches. - **The recorded commit must be reachable in the submodule's remote,** otherwise nobody else can materialise it — a gitlink pointing at a commit that only exists on one laptop is a broken superproject commit for everyone. ## Inspecting it `git submodule status` prints, per submodule, the recorded SHA and the path, with a leading marker: `-` means not initialised, `+` means the checked-out commit differs from the recorded one, `U` means merge conflicts. `git ls-tree HEAD <path>` shows the raw gitlink. `git config -f .gitmodules --list` dumps the tracked mapping without touching local config.
- Why does changing a URL in .gitmodules not take effect for your teammates automatically?Because the URL is copied into each clone's local `.git/config` when the submodule is initialised, and that local copy is what Git actually fetches from. The tracked file is only the source for that copy. Running `git submodule sync` re-copies the current `.gitmodules` value over the local one; without it, everyone keeps fetching from the old location.
- Where does a submodule's object database actually live in a modern clone?Under the superproject's `.git/modules/<name>/`. The submodule's working directory holds a `.git` file — not a directory — containing a `gitdir:` line pointing there. That separation means you can delete or re-checkout the submodule's working directory without losing its history, and it is what makes `git submodule deinit` a cheap, reversible operation.
- A reviewer sees only a changed Subproject commit line. How do you make that diff more informative?Set `diff.submodule` to `log`, which makes Git list the submodule commits between the old and new gitlink instead of printing two bare object ids, and `status.submoduleSummary` for the same effect in `git status`. Beyond configuration, the practical answer is to keep the commit message on the superproject side describing why the pin moved.
saying these in an interview costs you the question
- Says the submodule's files are copied into the parent repository
- Thinks the parent tracks a branch rather than a commit
- Believes .gitmodules alone is enough to reproduce a checkout
- Confuses the URL in .gitmodules with the URL Git actually fetches from
- Assumes committing inside the submodule updates the parent automatically