What are Git submodules versus Git subtree, and when would you choose one over the other for managing shared dependencies?
Quick Answer
Git submodules embed a separate repository as a pinned reference (gitlink) in the parent repo, maintaining independent histories. Git subtree merges another repository's content directly into a subdirectory of the parent, with no external references. Submodules preserve separation and are better for large shared libraries; subtree is simpler for consumers but harder to contribute upstream.
Detailed Answer
Think of submodules and subtree as two different ways to include a chapter from another author's book into your own. With submodules, your book contains a bookmark that says 'Insert Chapter 7 from Book X, 3rd Edition here.' The reader must go fetch that book separately. With subtree, you physically photocopy Chapter 7 and bind it into your book. Readers get a self-contained book, but if the original author updates the chapter, synchronizing the copy requires more effort than updating a bookmark.
Git submodules store a reference to another repository at a specific commit. The parent repository tracks a .gitmodules file (mapping submodule paths to URLs) and a gitlink entry (a tree entry with mode 160000 storing only the commit SHA, not file contents). Cloning the parent does not automatically fetch submodule contents; you must use git clone --recurse-submodules or run git submodule init followed by git submodule update. Updating a submodule to a newer version requires entering the submodule directory, pulling changes, then committing the updated gitlink in the parent. Git subtree, by contrast, merges the external repository's files directly into a subdirectory using git subtree add. The files become regular tracked files in the parent, requiring no special clone flags or initialization steps.
Internally, submodules maintain strict separation. The submodule's .git data lives in .git/modules/<name>/ in the parent, and the submodule working directory contains a .git file pointing there. This means the submodule has its own independent commit graph, branches, and remote configuration. Subtree works differently: git subtree add performs a merge that grafts the external repository's history into the parent's commit graph, then places the files under the specified prefix. Subsequent updates use git subtree pull to merge new changes. Contributing back uses git subtree push, which extracts commits touching the subtree prefix and pushes them to the external repository. The subtree split command can extract the subdirectory's history into a standalone branch.
In production, the choice depends on your team's workflow and the relationship between repositories. Submodules excel when: the shared code is a large, independently versioned library (like protocol buffer definitions shared across 20 microservices), consumers need to pin to specific versions for stability, and the shared library has its own CI/CD pipeline and release process. Subtree is better when: you want a self-contained repository with no special clone instructions, the shared code is small and infrequently updated, and you want contributors to modify the shared code without switching repositories. Many teams at mid-size companies start with subtree for simplicity and migrate to submodules or a package registry as the shared code grows in complexity and consumer count.
A critical gotcha with submodules: forgetting to run git submodule update after pulling the parent is the most common developer complaint, resulting in a detached HEAD at the wrong commit inside the submodule. Set git config submodule.recurse true to auto-update on pull and checkout. A gotcha with subtree: the merge history can become noisy, and git subtree push can be extremely slow on large repositories because it must scan the entire parent history to extract relevant commits. Another trap: if you use subtree and multiple team members independently subtree-pull at different times, merge conflicts in the subtree directory can be confusing because the conflict involves interleaved histories from two repositories.