← Back to Blog

Git Submodule vs. Git Subtree

Git Tutorial

This guide covers everything you need to know about managing nested repositories using Git Submodules and Git Subtrees, complete with practical scenarios and a detailed comparison.

Part 1: Git Submodule

What is a Git Submodule?

A Git Submodule allows you to keep another Git repository in a subdirectory of your own repository. It essentially acts as a pointer to a specific commit in another repository.

How it helps:

  • Strict Version Control: It guarantees that your project uses an exact, specific version of a dependency.
  • Clean Separation: The submodule's history remains entirely separate from your main repository's history.

What it is NOT for:

  • It is not a replacement for package managers (like npm, pip, or Maven).
  • It is not ideal for dependencies that you plan to heavily and frequently modify alongside your main project, as the branching and committing workflow across two separate repositories can become cumbersome.

Cloning a Repository with Submodules

If you clone a project that contains submodules, you won't get the submodule files automatically. You must use the --recurse-submodules flag:

git clone --recurse-submodules <repository-url>

If you already cloned the repository normally, you can initialize and fetch the submodule data using:

git submodule update --init --recursive

Practical Scenario: Development Lifecycle

Imagine you are building a web application and want to include a shared UI library as a submodule.

1. Adding the Submodule

git submodule add https://github.com/example/ui-lib.git libs/ui-lib

Important Tip: This command clones the repo into libs/ui-lib and creates/updates a special hidden file called .gitmodules. This file maps the submodule's path to its URL and must be committed to your repository.

2. Committing the Submodule

git commit -m "Add ui-lib as a submodule"

Git records the exact commit hash of ui-lib.

3. Updating the Submodule (Fetching upstream changes)

When the upstream ui-lib has new updates that you want to pull in:

git submodule update --remote libs/ui-lib

Then, commit the new pointer in your main repo.

4. Removing a Submodule

Removing a submodule can be tricky. Here are the steps:

  • If you want to stop tracking it but keep the files on your disk:
git rm --cached libs/ui-lib
  • If you want to remove it entirely from Git and your filesystem:
git rm -rf libs/ui-lib

Don't forget: You may also need to manually clean up references in .git/modules/libs/ui-lib and .git/config for a completely clean removal.

Directory Structure

my-project/
├── .git/                      <-- Main repository Git data
├── .gitmodules                <-- Tracks the path and URL of submodules
├── src/
│   └── index.js
└── libs/                      <-- Directory containing submodules
    └── ui-lib/
        ├── .git               <-- Submodule's own Git data pointer
        └── component.js

Part 2: Git Subtree

What is a Git Subtree?

Git Subtree is an alternative to submodules that allows you to nest one repository inside another by physically merging its history into your main repository's history.

How it helps:

Unlike submodules, a subtree doesn't require special commands for anyone cloning your repository. The nested code acts exactly like normal files in your repository.

Best Practice for Directory Structure:

The --prefix flag in subtree commands dictates where the external code lives. It is highly recommended to group all vendored projects or external repositories under a single directory (like libs/ or repos/). This keeps your project organized and makes it easy to reference external code in configurations or AI agent instructions.

Practical Scenario: Development Lifecycle

Let's use the same scenario: adding a ui-lib to our project, but this time using Subtree.

1. Adding the Subtree

git subtree add --prefix=libs/ui-lib https://github.com/example/ui-lib.git main --squash

Crucial Tip: Always use the --squash flag! Without it, Git will pull the entire commit history of the external repository into your project, which could mean thousands of unwanted commits. Using --squash cleanly condenses all that external history into a single commit.

2. Pulling Updates (Fetching upstream changes)

To pull the latest updates from the original ui-lib repository:

git subtree pull --prefix=libs/ui-lib https://github.com/example/ui-lib.git main --squash

3. Pushing Changes Back

If you modify libs/ui-lib inside your project and want to contribute those changes back to the original repository:

git subtree push --prefix=libs/ui-lib https://github.com/example/ui-lib.git main

Directory Structure

Notice there are no .gitmodules or nested .git folders.

my-project/
├── .git/                      <-- Only ONE Git folder for the whole project
├── src/
│   └── index.js
└── libs/
    └── ui-lib/                <-- Treated as normal files within my-project
        └── component.js

Configuring Your Editor (VSCode)

If you keep external subtrees in a dedicated folder (like libs/), you probably don't want your code editor to index them, search through them, or suggest auto-imports from them.

You can exclude this directory in VSCode by adding these rules to your .vscode/settings.json:

{
  "files.exclude": {
    "libs/**": true
  },
  "files.watcherExclude": {
    "libs/**": true
  },
  "search.exclude": {
    "libs/**": true
  },
  "typescript.preferences.autoImportFileExcludePatterns": [
    "libs/**"
  ],
  "javascript.preferences.autoImportFileExcludePatterns": [
    "libs/**"
  ]
}

Part 3: Submodule vs. Subtree Comparison

Feature Git Submodule Git Subtree
Mechanism Acts as a pointer to a specific commit. Merges the external repo's history into the main repo.
Consumer Experience Requires special clone commands (--recurse-submodules) or post-clone init commands. Seamless. Anyone cloning the main repo gets the files immediately.
Configuration Relies on the .gitmodules file. No extra configuration files needed.
Pros
  • Keeps repository sizes smaller.
  • Strict commit-level versioning.
  • Clean history separation.
  • Easy for teammates to clone/use.
  • Code can be modified and pushed back upstream relatively easily.
Cons
  • Easy to end up in a "detached HEAD" state.
  • Complex commands to update and remove.
  • git clone requires extra flags.
  • Can clutter the main repo's history if not squashed.
  • Main repository size increases.
When to use Use for large external dependencies, huge assets, or strict third-party libraries where you rarely modify the code yourself. Use for smaller utilities, shared libraries between microservices, or code you want to frequently modify and push back upstream.