DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

A Practical Guide to Git Submodules: Managing Shared Dependencies Across Repositories

A Practical Guide to Git Submodules: Managing Shared Dependencies Across Repositories

A Practical Guide to Git Submodules: Managing Shared Dependencies Across Repositories

Working with multiple repositories is common in modern software projects. When you have a shared codebase-like a design system, common utilities, or platform SDK-you want to reuse it cleanly without duplicating code or breaking isolation. Git submodules let you include one repository inside another as a specific commit, giving you a stable, trackable pointer to the dependency while retaining its history. This guide walks you through a practical, production-ready workflow for using submodules effectively, with concrete commands, pitfalls, and tips.

Why use submodules

  • Single source of truth for shared code
  • Precise control over which commit of the dependency you build against
  • History preserved in the submodule repository
  • Clear separation of concerns between apps and libraries

Common alternatives:

  • Subtree merges: easier to push updates but can be messier to keep in sync.
  • Package managers (e.g., npm, Maven, PyPI): better for versioned releases but may lose exact commit-level traceability.
  • Monorepos: tighter integration, but larger repos and more complex workflows.

Submodules are best when you need stable pinning to a commit in a separate repo, and when the consumer projects want to opt into updates on their schedule.

Setting up a submodule

1) Identify the shared repository and where it should live

  • Example dependency: a shared UI components library at git@github.com:org/ui-components.git
  • Destination path in your project: libs/ui-components

2) Add the submodule

  • Ensure you’re at the root of your main project
  • Run:
    • git submodule add git@github.com:org/ui-components.git libs/ui-components
    • This creates the .gitmodules entry and initializes the submodule at the current HEAD of the external repo

3) Initialize and fetch submodules (for fresh clones)

  • git clone
  • git submodule init
  • git submodule update
  • You can shorthand with: git clone recurse-submodules

4) Pin to a specific commit (optional but recommended for stability)

  • cd libs/ui-components
  • git fetch
  • git checkout
  • cd ..
  • Commit the submodule reference change in the main repo:
    • git commit -am "Pin ui-components submodule to "
    • git push

Note: Submodules are tracked by a special entry in the parent repo that records the exact commit of the submodule.

Working with submodules day to day

  • Inspect the current submodule status:
    • git submodule status
  • Update a submodule to the latest commit on its default branch (often not recommended for production pins):
    • cd libs/ui-components
    • git fetch
    • git checkout main
    • git pull
    • cd ..
    • git add libs/ui-components
    • git commit -m "Update submodule to latest main in ui-components"
  • Stash or commit local changes inside the submodule just like any other repo
  • If the submodule’s default branch diverges from the commit you want, specify a commit or tag explicitly:
    • cd libs/ui-components
    • git fetch
    • git checkout
    • cd ..
    • git add libs/ui-components
    • git commit -m "Pin submodule to tag v1.2.3"

Common gotchas:

  • Submodule updates are not automatic. Every change to submodule pointers must be committed in the parent repo.
  • Cloning with recurse-submodules ensures you get the submodule content, but you still need to run git submodule update to checkout the pinned commit.
  • Submodules do not automatically get pushed when you push the parent repo. You must push both the submodule changes (if any) and the parent repo. ### Practical workflow: feature development with a shared library

Suppose you’re developing a feature in app-repo (the main project) that uses components from the shared library.

1) Create a feature branch in the main repo

  • git checkout -b feature/new-dashboard

2) Update submodule to a new feature branch or commit if needed

  • If you want to test a change in the submodule, navigate to it and check out a feature branch there:
    • cd libs/ui-components
    • git fetch
    • git checkout feature/improve-accordion
    • cd ..
    • git add libs/ui-components
    • git commit -m "Test new UI accordion in submodule"
  • If you prefer to rely on a commit from the submodule, skip the above and pin to a commit:
    • cd libs/ui-components
    • git fetch
    • git checkout
    • cd ..
    • git add libs/ui-components
    • git commit -m "Pin to commit for feature testing"

3) Build and test against the pinned submodule

  • Ensure your CI or local environment uses the pinned commit
  • Run your tests:
    • npm test (or your project’s test command)
    • Ensure the submodule-based code compiles and runs correctly

4) Merge flow

  • Once the feature is ready:
    • In the submodule: merge any tests or changes into the target branch (e.g., main)
    • Push submodule changes:
    • cd libs/ui-components
    • git push origin feature/improve-accordion
    • In the parent repo: update the submodule pointer to the new commit
    • cd ..
    • git add libs/ui-components
    • git commit -m "Update ui-components submodule to include feature/improve-accordion"
    • git push origin feature/new-dashboard

5) Rollback plan

  • If a submodule update breaks the main project, revert the submodule pointer in the parent repo to the previous commit:

    • git reflog submodule libs/ui-components
    • git checkout libs/ui-components
    • git commit -m "Revert ui-components submodule to previous commit due to issue"
    • git push origin feature/new-dashboard ### Best practices for reliable submodule workflows
  • Keep submodule pins explicit

    • Never rely on the submodule pointing to a moving branch in production code. Pin to a commit or tag.
  • Document the submodule strategy in your repo

    • Explain when to update, who approves submodule changes, and how to handle CI.
  • Use tags for stable releases

    • The submodule can point to a tag like v1.2.3, providing a clean, repeatable baseline.
  • Align CI/CD to submodule pins

    • Your pipeline should checkout submodules and verify builds against the pinned commit.
  • Automate updates with caution

    • Consider a controlled process: create a separate branch or PR to update the submodule, require reviews, run tests, and then merge.
  • Minimize submodule churn

    • Prefer updating submodules in small, incremental steps rather than sweeping changes across multiple dependencies. ### Example: a minimal JavaScript project with a shared library submodule

Project layout:

  • apps/ - main application
  • libs/ui-components/ - submodule containing shared UI components

Key commands:

  • Initialize
    • git submodule add git@github.com:org/ui-components.git libs/ui-components
    • git commit -m "Add ui-components submodule"
  • Pin to a tag
    • cd libs/ui-components
    • git fetch tags
    • git checkout tags/v1.0.0 -b stable-v1.0.0
    • cd ..
    • git add libs/ui-components
    • git commit -m "Pin ui-components to v1.0.0"
  • Update in feature branch
    • cd libs/ui-components
    • git fetch
    • git checkout main
    • git pull ff-only
    • cd ..
    • git add libs/ui-components
    • git commit -m "Update submodule to latest main in ui-components"
  • Push
    • git push origin feature/new-dashboard

Usage in code (example with a React project)

  • Import path in your code remains unchanged, since the submodule is a separate repository. You only need to ensure your build system includes the submodule directory:
    • import { Button } from '../../libs/ui-components/dist/button';
    • Or configure your bundler alias to map to the submodule’s built artifacts.

Note: If your submodule exposes a package that you publish (e.g., via npm), you can also consume that published version in your apps, but submodules give you a precise, offline-available history that’s tied to a specific commit.

Troubleshooting quick fixes

  • Submodule not initialized after clone
    • git submodule update init recursive
  • Submodule changes not appearing in parent commit
    • Ensure you added the submodule directory: git add libs/ui-components
    • Then commit: git commit -m "Update submodule pointer"
  • Conflicts inside submodule during an update

    • Treat the submodule as a separate repo: cd into the submodule, resolve conflicts, commit inside the submodule, then update the parent repo ### When submodules aren’t the right fit
  • If you need seamless cross-repo updates without manual pinning, consider package managers with strict version ranges and a dedicated release cycle.

  • If you want to publish stable, versioned bundles of the shared code, a library package (npm, PyPI, Maven) with semantic versioning can be simpler to reason about for teams that want straightforward dependency management.

    If you’d like, I can tailor this guide to your tech stack (e.g., Node.js monorepo with Yarn workspaces, Python services, or a Rust/Go polyrepo) and provide a concrete example repository structure and CI pipeline configuration. Would you like that, and which language/framework are you using?

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)