How to Use git subtree Instead of Submodules the Safe Way
Use git subtree to include another repository inside yours as normal files: add, pull, push back, squash history, and compare it with submodules.

Submodules point at another repository, and subtree copies it in. The difference is that nobody on your team has to learn a new workflow, since the files are normal and a plain clone gets everything. That makes subtree the lower-friction way to vendor code, with the trade-off of a larger history. The comparison to the pointer-based approach is in git submodule. The commands assume Git 2.47, where git subtree ships in the contrib directory of most distributions.
Add a subtree
git remote add shared-lib https://github.com/example/shared-lib.git
git subtree add --prefix=vendor/shared-lib shared-lib main --squash
git log --oneline -3e1f2a3b Merge commit 'c4d5e6f' as 'vendor/shared-lib'
c4d5e6f Squashed 'vendor/shared-lib/' content from commit 9a8b7c6
The remote is a convenience name, and the prefix is the directory that will hold the files. --squash compresses the upstream history into a single commit, which keeps your log readable and your repository small. Pick squash or no squash at the start and keep to it: mixing them in one subtree confuses later merges.
Pull updates
git subtree pull --prefix=vendor/shared-lib shared-lib main --squashThis fetches upstream and merges the differences into the prefix directory. Conflicts are resolved like any merge, as in resolving merge conflicts. The merge commit records which upstream commit you now track, via a trailer in the squash commit, so run the same command with the same arguments every time.
Push changes back
git subtree push --prefix=vendor/shared-lib shared-lib fix/null-checkgit push using: shared-lib fix/null-check
* [new branch] 4a3b2c1 -> fix/null-check
Git walks your history, extracts commits that touched the prefix and pushes the result to a branch upstream, where you open a pull request. On a long history this extraction is slow. Keep changes to the vendored directory in their own commits, so the extraction produces a clean series. If upstream accepts the change, the next subtree pull brings it back and the two sides agree.
Compare with a submodule
Concern subtree submodule
Plain clone gets the code yes no (needs init)
Repository size grows stays small
Pin to exact upstream sha via squash msg yes, in the tree
Edit and push upstream possible, slow natural
CI setup none recurse flag
Pick subtree when the code is read mostly and edited occasionally, and when you want git clone and git archive to work without ceremony. Pick a submodule when you need an exact, auditable pointer and a small parent repository.
What a subtree is
A subtree copies another repository's content into a subdirectory of yours and records the relationship in the commit history. Unlike a submodule, which stores a pointer to a commit in another repository, the files are ordinary files in your repository: a clone gets them with no extra step, and tools that walk the tree see them. The cost is that the history of the foreign project is mixed into yours when you add it without squashing, which grows the repository, and the link between the two is kept in commit messages, not in a metadata file. For consumers of the code, this makes a subtree invisible, which is its main attraction.
It suits vendored dependencies that change rarely, shared libraries in a small set of projects and moving a directory into its own repository while keeping history. It suits less well a component edited daily by two teams, where the friction of splitting and merging adds up.
Squash or keep history
When adding or pulling, the --squash option collapses the foreign history into one commit. That keeps your history small and your log readable, at the price of losing the detailed history in your repository. Whatever you choose, be consistent: mixing squashed and unsquashed operations on the same subtree confuses later merges, since Git relies on the recorded commit ids to find the common ancestor. Write the commands in the project documentation and use a script to run them, so every contributor uses the same options.
Add the remote under a name first, for example git remote add libfoo URL, so that later pulls and pushes use the name instead of retyping the URL. The remote is purely a convenience and is not recorded in the repository.
Contributing changes upstream
To send changes back, git subtree push --prefix=vendor/libfoo libfoo branch extracts the commits that touched the prefix and pushes them to the other repository as a branch. The extraction walks your history and can be slow on large repositories; a git subtree split run once, caching its results, speeds up later pushes. Keep changes to the subtree in separate commits that touch only that directory, so the extracted history is clean and the upstream maintainers see sensible commits. Mixed commits that change the subtree and the main project together produce awkward results.
If you modify vendored code locally with no plan to send it upstream, document the divergence in a file next to the code, so future updates do not erase the patches without notice.
Weighing it against alternatives
A submodule keeps the repositories cleanly separate and records an exact version, but it needs extra commands to clone and update, and it trips up newcomers. A package manager is the better choice when the dependency is published with versions, since it handles resolution and integrity. A monorepo avoids the question by keeping everything in one repository with shared tooling, at the cost of repository size and access control. Subtrees sit between: simple for consumers and awkward for frequent two-way work. Pick based on how often the shared code changes and who edits it. Whatever you pick, write down the update procedure and who owns it, and schedule a review of vendored code, because copied code receives no automatic security updates and ages quietly until someone notices a vulnerability.
Keep the subtree tidy over time. Record the upstream commit you last merged in the commit message, which the tool does for you when squashing, and avoid editing vendored files for cosmetic reasons, since each local change makes future merges harder. If the upstream project is abandoned or you fork it permanently, say so in the documentation and treat the directory as your own code, with its own tests and reviews. The clearest failure with vendored code is the one nobody owns, so name an owner when you add it.
Where this does not apply
If the embedded code changes weekly and several people send changes upstream, the subtree push is painful and a package manager or monorepo is better. A subtree cannot be removed from history once merged without rewriting it. Deleting the directory with git rm -r removes the files going forward only. The subtree command is also missing in some minimal Git packages; install the contrib package if git subtree says it is not a git command. See the git-subtree documentation and Advanced Merging in Pro Git. For the daily commands behind it, review merging versus rebasing.


