Managing multi-repository dependencies: embedding an external library into the main repository with git submodule add, cloning nested repositories, and comparing submodule and subtree to choose the most suitable approach.

In episode 17 we navigated and rescued history within a single repository. In episode 18 the question widens: what if a project needs code from another repository — for example a shared library used by three different teams? We discuss Git's two official approaches: Submodules and Subtrees.
This is a real problem in companies: internal libraries like lib-auth, lib-design-system, or lib-utils are used across teams, but each team has its own main codebase. Copy-pasting library code into each project is naive — a bug fix in one copy never reaches the other copies. Submodule and subtree are the official answers.
Before choosing a tool, make sure the problem is real. Multi-repository is needed when:
If the library is only used by one project, consider moving it directly into that project. Language dependency managers (npm, pip, Bun) remain the primary choice; submodules and subtrees are for cases those tools do not handle.
A submodule is an embedded external commit in the main repository. The main repository only stores a reference — a pointer to a specific commit in another repository.
git submodule add https://github.com/devnull/lib-auth.git libs/authThe command above clones lib-auth into the libs/auth folder and records its reference in the .gitmodules file:
[submodule "libs/auth"]
path = libs/auth
url = https://github.com/devnull/lib-auth.git.gitmodules is committed to the main repository — and this is where its main weakness lies, as we will see shortly.
A plain git clone does not fill in the submodule contents — the submodule folder arrives empty. The solution:
git clone --recurse-submodules https://github.com/devnull/my-app.gitFor a repository that is already cloned:
git submodule update --init --recursiveThe --recursive flag matters when a submodule has its own submodule (nested repositories). When the library version goes up, update the submodule then commit the pointer change:
git submodule update --remote
git add libs/auth && git commit -m "chore: update lib-auth"To check the status of all submodules, including whether any commits have not been synced with the version recorded in .gitmodules:
git submodule statusA line with a minus prefix means the submodule has not been initialized, while a line with an extra suffix means the submodule working tree deviates from the locked commit.
Subtree is the opposite approach: another repository's contents are copied directly into the main repository's tree, with no pointer file and no .gitmodules:
git subtree add --prefix=libs/auth https://github.com/devnull/lib-auth.git mainThe result is a libs/auth folder containing the entire library code, committed normally to the main repository. To pull in upstream updates:
git subtree pull --prefix=libs/auth https://github.com/devnull/lib-auth.git mainBecause the code is integrated, the workflow is like normal files: edit, commit, review via PR — no extra initialization rituals.
| Aspect | Submodule | Subtree |
|---|---|---|
| Storage | Pointer to an external commit | Full copy inside the tree |
.gitmodules file | Required | None |
| Code directly readable | Needs update and init first | Directly present in the working tree |
| Version updates | git submodule update | git subtree pull |
| Ease of collaboration | More complex, easy to forget init | Normal git workflow |
| Library code history trail | Not in the main repository | Fully preserved |
Tip
Rule of thumb: if you have many team members and the most important value is ease of collaboration, subtree is usually friendlier — no .gitmodules, no init step, and PRs work as usual. Choose submodule if you need to lock the library version explicitly per environment and accept the initialization cost.
git submodule update. The team downloads the repository, the submodule is empty, and the app errors. Know the symptom: the submodule folder appears to exist but its contents are missing..gitmodules is committed and the URL contains no credentials.main.The points to take with you:
.gitmodules file.git clone --recurse-submodules and git submodule update --init --recursive fill in submodules after cloning..gitmodules — the workflow is like normal files.Automation at the repository level is complete, but there is one last layer on the local side: making sure every commit is high quality from the laptop onward. In the next episode 19 we cover Git Hooks & Local Automation to run linters, commit message validation, and unit tests automatically. See you in episode 19!