DevOpsGitHub

Git Submodules & External Libraries: Managing Dependencies

TT
TopicTrick Team
Git Submodules & External Libraries: Managing Dependencies

Git Submodules & External Libraries: Managing Dependencies

When your project depends on code from another repository, you have several options: copy-paste the code (terrible), use a package manager (great when available), or use git submodules or subtrees (powerful when a package manager is not an option).

Git submodules are common in C, C++, embedded systems, game development, and large monorepos. They let you embed a specific commit of an external repository inside your own, tracking exactly which version of the dependency you are using.

This guide covers the complete submodule workflow, the most common pitfalls, git subtrees as an alternative, and guidance on when to use each approach.


What Is a Git Submodule?

A git submodule is a pointer from your repository to a specific commit in another repository. It consists of two parts:

  1. A .gitmodules file that records the URL and path of each submodule
  2. A special "gitlink" entry in your tree that records the exact commit SHA the submodule should be at

When you clone a repository with submodules, git clones your repo but leaves the submodule directories empty. You must explicitly initialize and update them.

bash
# Clone a repo and initialize all submodules in one command
git clone --recurse-submodules https://github.com/user/my-project.git

# Or if you already cloned without submodules:
git submodule update --init --recursive

The --recursive flag handles nested submodules (submodules that themselves contain submodules).


Adding a Submodule

bash
# Add a submodule at vendor/imgui
git submodule add https://github.com/ocornut/imgui.git vendor/imgui

# Git creates:
# - vendor/imgui/ directory (cloned at HEAD of the default branch)
# - .gitmodules file (or appends to it)
# - A staged gitlink entry for the commit

After running this command, inspect what git created:

bash
cat .gitmodules
ini
[submodule "vendor/imgui"]
    path = vendor/imgui
    url = https://github.com/ocornut/imgui.git

Now commit both the .gitmodules file and the gitlink:

bash
git add .gitmodules vendor/imgui
git commit -m "add imgui as a submodule at v1.90.1"

Pinning to a Specific Version

By default git submodule add checks out the default branch HEAD. For production dependencies you want to pin to a specific tagged release or commit SHA, not a floating branch.

bash
# Enter the submodule directory
cd vendor/imgui

# Checkout the specific tag or commit you want
git checkout v1.90.1    # or a specific SHA: git checkout abc1234

# Go back to your project root
cd ../..

# Stage the updated gitlink
git add vendor/imgui
git commit -m "pin imgui to v1.90.1"

Your repository now records exactly commit abc1234 of imgui. This is the key safety feature of submodules — your project will never silently break because the upstream library released a breaking change.


The Detached HEAD Problem

When you enter a submodule directory, git checks out a specific commit — not a branch. This is called a "detached HEAD" state.

bash
cd vendor/imgui
git status
# HEAD detached at abc1234

The danger: If you make changes inside a submodule while in detached HEAD and you forget to create a branch first, those changes are not on any branch. Running git submodule update or checking out a different commit will silently discard your changes.

The fix before making changes:

bash
cd vendor/imgui

# Create a branch to work on
git checkout -b my-fixes

# Now make your changes safely
# ... edit files ...
git add -A
git commit -m "fix: patch rendering bug"

# Push your changes to the remote
git push origin my-fixes

Then, back in the parent repository, update the submodule pointer to your new commit:

bash
cd ../..
git add vendor/imgui
git commit -m "update imgui to include my-fixes branch"

Updating Submodules to Newer Upstream Versions

When the upstream library releases a new version:

bash
# Option 1: Update one specific submodule
cd vendor/imgui
git fetch origin
git checkout v1.91.0    # the new version
cd ../..
git add vendor/imgui
git commit -m "update imgui to v1.91.0"

# Option 2: Update all submodules to their remote tracking branch HEAD
git submodule update --remote --merge
git add -A
git commit -m "update all submodules to latest"

Caution with Option 2: --remote updates to the current HEAD of each submodule's tracking branch, which may include breaking changes. Prefer explicit versioned updates for production dependencies.


Cloning a Repository With Submodules

Anyone cloning your repository needs to initialize submodules:

bash
# Best: clone with submodules in one step
git clone --recurse-submodules https://github.com/your-org/project.git

# If submodules were added after cloning:
git submodule update --init --recursive

# Update submodule pointers to what the parent repo specifies
git submodule update --recursive

Add a note in your README so contributors know to use --recurse-submodules:

markdown
## Getting Started

```bash
git clone --recurse-submodules https://github.com/your-org/project.git
cd project
make build
text

---

## Removing a Submodule

Removing a submodule requires several steps because git does not have a single "remove submodule" command:

```bash
# 1. Deinitialize the submodule
git submodule deinit -f vendor/imgui

# 2. Remove from the git index
git rm -f vendor/imgui

# 3. Remove the metadata directory
rm -rf .git/modules/vendor/imgui

# 4. Commit the removal
git commit -m "remove imgui submodule"

If you skip step 3, re-adding the submodule later will fail with a confusing error about the module already existing.


Git Submodule vs. Git Subtree

Both tools embed external repositories, but they work differently:

FeatureSubmodulesSubtrees
Clone behaviorRequires --recurse-submodulesWorks with normal git clone
HistoryExternal repo history is separateExternal repo history merged in
Disk usageOnly downloads what's neededCopies full history
Updatinggit submodule update --remotegit subtree pull --prefix=vendor/lib remote main
Pushing changes upstreamComplex (enter submodule, push separately)git subtree push --prefix=vendor/lib remote main
Detached HEAD riskYesNo
Best forLarge external dependencies, game engines, C/C++Shared utilities, internal libraries, simpler workflows

Git Subtree Example

bash
# Add an external repo as a subtree
git subtree add --prefix=vendor/common https://github.com/your-org/common-utils.git main --squash

# Update it later
git subtree pull --prefix=vendor/common https://github.com/your-org/common-utils.git main --squash

# Push changes back upstream
git subtree push --prefix=vendor/common https://github.com/your-org/common-utils.git my-feature-branch

The --squash flag merges the external repo's history into a single commit, keeping your git log clean.


When to Use Submodules vs. a Package Manager

Before reaching for submodules, ask whether a package manager solves the problem:

EcosystemPackage managerUse submodule when
JavaScript/TypeScriptnpm / yarn / pnpmForking a library with custom patches
Pythonpip / poetryVendoring a private repo
Gogo modulesEmbedding non-Go assets
RustCargoPatching a crate with local changes
C/C++Conan / vcpkgBuilding from source is required
Embedded/Bare-metalNone availableAlways a good fit

Rule of thumb: If the dependency is published on a public package registry, use the package manager. Use submodules when you need to track an unpublished fork, embed a build system dependency, or vendor code in C/C++ where there is no standard package manager.


Submodules in CI/CD Pipelines

GitHub Actions and other CI systems need explicit submodule initialization:

yaml
# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive    # Initialize all submodules
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Build
        run: make build

For private submodules, you need a personal access token or deploy key with access to the submodule repositories. Set the token in repository secrets and pass it to the checkout action.


Common Submodule Errors and Fixes

Error: "fatal: No url found for submodule path"

bash
# The .gitmodules file is missing or corrupt
git submodule init   # Re-read .gitmodules and register submodule URLs
git submodule update

Error: "repository already exists"

bash
# Left over .git/modules directory from a previous removal
rm -rf .git/modules/vendor/old-lib
git submodule update --init vendor/old-lib

Submodule shows as "modified" even when unchanged

bash
# Usually a line-ending issue on Windows
git config core.autocrlf false
git submodule foreach git config core.autocrlf false
git submodule update --force

Submodule stuck at wrong commit after git pull

bash
# git pull updates the parent but does not automatically update submodules
git pull
git submodule update --recursive    # Always run this after git pull

To automate this, set the submodule.recurse config option:

bash
git config --global submodule.recurse true
# Now git pull automatically updates submodule pointers

Frequently Asked Questions

Q: Are submodules still popular in 2026?

Yes, especially in C/C++, game development, embedded systems, and large monorepos. For ecosystems with mature package managers (npm, Cargo, pip, Go modules), submodules are less common but remain useful for private dependencies or when you need to vendor code at a specific commit without a registry.

Q: Can you have a submodule inside a submodule?

Yes, nested submodules work. Use --recursive everywhere: git clone --recurse-submodules, git submodule update --init --recursive. Without --recursive, nested submodules stay empty.

Q: What is the difference between git submodule update and git submodule update --remote?

git submodule update (without --remote) checks out the exact commit that the parent repository records — the commit that was last committed to the parent. git submodule update --remote fetches the latest commit from the submodule's remote tracking branch (ignoring what the parent recorded). Use the first for reproducible builds, use the second only when deliberately pulling in upstream updates.

Q: Why does my submodule show as a modified file after cloning?

This usually happens on Windows because of line-ending conversion (CRLF vs LF). Set core.autocrlf = false in both the parent repository and the submodule. It can also happen if the submodule's tracked commit does not match what the parent recorded — run git submodule update to resync.


Key Takeaway

Git submodules are the right tool when you need to embed an external repository at a specific, pinned commit with a clear separation between your codebase and the dependency. The main complexity is the detached HEAD state and the need to run git submodule update after every git pull. Mitigate both by setting submodule.recurse = true globally and always creating a branch before editing submodule contents. For simpler use cases where you do not need a separate history, git subtree is easier to work with.

Read next: Git Internals: Objects, Blobs, and the .git Folder →


Part of the GitHub Mastery Course — masters of dependencies.