Git Submodules: When and How

 |  Niceties

Git submodules are a built-in Git mechanism for composing repositories at fixed commits. Unlike a monorepo, separate repositories, or worktrees, they let a root repository record exact revisions of independently owned submodules.

Contents


The basic idea is simple: the root repository records a nested repository at an exact commit. The root repository stores the path and remote URL in .gitmodules, and records the submodule commit as a gitlink. A clone can later reconstruct the same combination of repositories.

Use Cases

Briefly: use a submodule when a root repository must record an approved revision of code maintained elsewhere. Here are some useful applications of submodules:

  • Keep private documentation and deployment configuration in the root repository, with public source code in a submodule.
  • Pin the exact revision of a shared library maintained and released separately.
  • Record the approved revisions of independently released services or tools.
  • Pin a reviewed commit from a maintained fork of an external project.

In product development, the root repository represents the product release context: its documentation, configuration, integration decisions, and approved component revisions. Each submodule represents a component with its own codebase, maintainers, and delivery cycle.

Submodules, Subtrees, Mono- or Multirepo?

Here are some key distinctions, briefly.

A submodule keeps source independent while the root repository records the exact revision used by the product. It fits when public source and private product material must remain separate but form one reproducible product revision.

A subtree copies external source into the root repository. Use it when the root repository should own that source.

A monorepo works well when code, access rules, and release cadence belong together. Nx and similar monorepo tools allow for task execution, caching, project boundaries, and releases conveniently from the single root repository.

A multirepo gives each component its own repository, ownership, and history, but does not by itself record which revisions belong together.

A Practical Setup Example

Consider a product with private documentation and deployment configuration, and public source code maintained in a separate repository.

  • Root repository: private; contains private material and one or more submodule references.
  • Submodule: public; contains source code and is its own source of truth.
root/
├── .gitmodules
├── docs/                    # private specifications and operational material
├── deployment/              # private deployment configuration
└── submodule/               # separately owned source repository
    ├── README.md
    └── src/

The root repository tracks .gitmodules and the exact commit recorded at submodule/. The files inside submodule/ belong to the submodule repository.

fork.dev

I use here the Fork Git client. The setup via terminal is in the next section.

  1. Create the local folder structure:

    mkdir root
    cd root
    mkdir docs deployment submodule
    
  2. In Fork, create a Git repository in root/.

  3. Open submodule/ in Fork and create a separate Git repository there. Add an initial file such as README.md, commit it, and keep this repository independent from root/.

  4. Create two empty repositories in your Git hosting service, for example: private: https://github.com/example/root.git, public: https://github.com/example/submodule.git.

  5. In Fork, add https://github.com/example/root.git as the origin remote of the root/ repository.

  6. Add https://github.com/example/submodule.git as the origin remote of the submodule/ repository, then push its initial commit.

  7. Return to the root/ repository. In the sidebar, right-click Submodules → Add New Submodule. Enter repository URL: https://github.com/example/submodule.git, local path: submodule. Fork registers the existing local repository as the submodule.

  8. Commit .gitmodules and the new submodule gitlink in the root repository, then push the root repository to its private origin.

Terminal

The same setup from a terminal, creating both repositories locally first and then attaching their remotes:

# Create the local folder structure.
mkdir root
cd root
mkdir docs deployment submodule

# Initialize the private root repository.
git init -b main
git remote add origin https://github.com/example/root.git

# Initialize the public submodule repository.
cd submodule
git init -b main
echo "# Submodule" > README.md
git add README.md
git commit -m "Initial commit"
git remote add origin https://github.com/example/submodule.git
git push -u origin main

# Return to the root repository and register the existing local repository
# as a submodule.
cd ..
git submodule add --force https://github.com/example/submodule.git submodule

# Commit the root repository state.
git add docs deployment .gitmodules submodule
git commit -m "Add submodule"
git push -u origin main

Daily Workflow

Treat the submodule commit as a dependency lock. Change code in the submodule repository, then deliberately update the root repository's pointer.

With that workflow, the repositories keep their separate ownership, history, and visibility, while the root repository still records the exact combination that belongs to the system.

Example:

  1. Doble-click on Submodules->submodule repository to open its tab. Add its source files, stage them, commit them, and push them to the public origin.

  2. Return to the root repository. Add private files under docs/ and deployment/. Stage docs/, deployment/, commit them, and push them to the private origin.

For private documentation or configuration changes, work in the root repository directly. For source changes, commit and push them in the submodule first, then commit the changed submodule reference in the root repository.

When cloning later, use recursive initialization:

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

For an existing clone, use:

git submodule update --init --recursive

References: Git submodule documentation and Git submodules guide.