Git in a Monorepo
A monorepo is a single Git repository that contains multiple distinct projects — frontend app, backend API, shared component library, mobile app, infrastructure code — all in one place. Companies like Google, Meta, and Microsoft have used monorepos at massive scale. Many teams of 5–50 developers also benefit from monorepos for entirely different reasons: atomic cross-package changes, unified CI, and no dependency version drift.
Git was not designed with monorepos in mind. With hundreds of packages and thousands of files, certain Git operations become slow, CI builds become bloated, and history becomes hard to navigate. This guide covers the specific Git techniques, tools, and patterns that make monorepos manageable.
What a Monorepo Is and Git-Specific Challenges
Challenge | Root cause | Solution |
|---|---|---|
Slow git status / git fetch | Thousands of files to stat | git maintenance, fsmonitor, partial clone |
Bloated CI builds | Every PR triggers full test suite | Path-based CI triggering |
Hard to find package history | git log shows all packages mixed | git log -- packages/ui/ |
Large repository size | Binary assets, generated files | Git LFS, .gitignore build outputs |
Long clone times for new developers | Full history of all packages | Shallow clone + sparse checkout |
Version coordination | Which package version goes with which? | Changesets, Nx release, or repo-wide tags |
Shallow Clone and Sparse Checkout
When a developer only needs to work on packages/ui, cloning all 20 years of history and all 50 packages is wasteful. Combine shallow clone (limited history depth) with sparse checkout (limited file tree) to make the working copy fast and small.
Shallow clone with sparse checkout for a specific package
# 1. Clone with no files checked out and minimal history git clone --filter=blob:none --no-checkout --depth=1 \ git@github.com:org/monorepo.git cd monorepo # 2. Enable sparse checkout git sparse-checkout init --cone # 3. Specify which directories you want git sparse-checkout set packages/ui packages/design-tokens # 4. Now check out main git checkout main # Only packages/ui and packages/design-tokens are on disk # Everything else is available on-demand if needed
Add more paths to sparse checkout
# Later, if you need to work on another package: git sparse-checkout add packages/api-client # Or reset to get everything: git sparse-checkout disable
Path-Based CI Triggering
The biggest productivity win in a monorepo CI setup is only building and testing packages that were actually changed by a PR. Running the full test suite for all 20 packages when you only changed a CSS file in packages/ui is wasteful and slows down every developer.
.github/workflows/ci.yml — path-based triggers
name: CI
on:
pull_request:
branches: [main]
jobs:
ui:
name: Test packages/ui
runs-on: ubuntu-latest
if: |
contains(github.event.pull_request.changed_files, 'packages/ui')
# Simpler approach: use paths filter
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test --workspace=packages/ui
# Better approach: use the paths key
api:
name: Test packages/api
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test --workspace=packages/api.github/workflows/ci-ui.yml — dedicated workflow per package
name: UI Package CI
on:
push:
paths:
- 'packages/ui/**'
- 'packages/design-tokens/**' # UI depends on this
- '.github/workflows/ci-ui.yml'
pull_request:
paths:
- 'packages/ui/**'
- 'packages/design-tokens/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint --workspace=packages/ui
- run: npm test --workspace=packages/ui
- run: npm run build --workspace=packages/uiCommit Messages in a Monorepo
Conventional Commits shine in monorepos because the scope field maps naturally to package names. This enables automatic changelog generation per package.
Commit message with package scope
# Scope = package name feat(ui): add Button component with size variants fix(api-client): handle 429 rate limit with exponential backoff chore(design-tokens): update spacing scale to 8pt grid docs(web-app): add deployment guide to README # Multi-package change (rare — prefer separate commits) feat(ui, web-app): integrate new Button into web app # Infrastructure / repo-wide change (no scope) chore: upgrade Node.js to 20 LTS across all packages ci: add dependency caching to all workflows
Package-Specific History with git log
The -- path filter restricts git log to commits that touched a specific directory. This is invaluable in a monorepo for tracing the history of a single package.
Inspect history for a specific package
# All commits that touched packages/ui git log --oneline -- packages/ui/ # a3f9c21 feat(ui): add Button component # b1d8e34 fix(ui): correct focus ring on dark backgrounds # c2e7f45 chore(ui): update peer dependencies # Full diff of every commit that touched packages/ui git log -p -- packages/ui/ # Who last changed each file in packages/ui git log --oneline --follow -- packages/ui/src/Button/index.tsx # Changes to a package between two releases git log v1.0.0..v2.0.0 --oneline -- packages/ui/ # Stats: how many lines changed per commit in a package git log --stat -- packages/ui/ | head -40
Tagging Strategies in a Monorepo
Strategy | Example tag | Pros | Cons |
|---|---|---|---|
Repo-wide semver | v3.4.5 | Simple, one version to communicate | All packages must release together |
Per-package tags | @org/ui@1.2.0 | Independent release cadence per package | More tags, complex CI release logic |
Package + global | packages/ui@1.2.0 + repo v2026.5 | Best of both worlds | Most complex setup |
Per-package tagging
# Tag a specific package release git tag -a "@org/ui@1.2.0" -m "Release @org/ui v1.2.0 - feat: add Button component - fix: correct focus ring - chore: update peer deps" git push origin "@org/ui@1.2.0" # List all tags for the ui package git tag -l "@org/ui*" # @org/ui@1.0.0 # @org/ui@1.1.0 # @org/ui@1.2.0
Monorepo Tooling
Tool | Purpose | Key feature |
|---|---|---|
Nx | Build system & task orchestration | Affected detection, caching, distributed builds |
Turborepo | Build orchestration | Fast incremental builds, remote caching |
Changesets | Version management & changelogs | Tracks which packages changed, automates CHANGELOG + publish |
Lerna | Package management | Classic monorepo tool, now delegates to Nx/Turborepo |
Rush | Enterprise monorepo tool | Strict reproducibility, used at Microsoft scale |
Workspaces (npm/yarn/pnpm) | Dependency hoisting | Links local packages to each other |
Changesets workflow for versioning
# Install changesets npm install --save-dev @changesets/cli npx changeset init # When making a change, add a changeset describing it npx changeset # ? Which packages would you like to include? packages/ui # ? Is this a patch, minor, or major change? minor # ? Summary: Add Button component with size variants # This creates .changeset/happy-fox-sings.md # On release: bump versions and generate changelogs npx changeset version # Bumps packages/ui from 1.1.0 to 1.2.0 # Updates CHANGELOG.md in packages/ui # Publish all changed packages npx changeset publish # npm publish on each changed package
Repository Performance Optimisation
As a monorepo grows, Git operations slow down. Here are the most impactful performance improvements:
Enable background maintenance
# Run git maintenance on the repo (schedules background optimisations) git maintenance start # Enables prefetch, commit-graph, incremental-repack on a schedule # Check what maintenance tasks are configured git config --list | grep maintenance
Enable file system monitor (faster git status)
# Enable fsmonitor for faster git status git config core.fsmonitor true git config core.untrackedCache true # Verify git config --list | grep fsmonitor # core.fsmonitor=true
Build commit graph for faster log/merge operations
# Build a commit-graph file (cached graph structure for faster traversal) git commit-graph write --reachable # Enable automatic commit-graph updates git config fetch.writeCommitGraph true
Configure pack file optimisations
# Use multi-pack-index for better pack management git multi-pack-index write # Repack with delta compression git repack -adf --window=250