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)