EthicsComputerCourses
Join training +91 90532 10052

Monorepos, Sparse Checkout & Worktrees

Git · Lesson 33 of 42 Advanced 9 min read Updated

As a company grows, it has to decide where its code lives. Some teams keep every app and service in one big repository, a monorepo. Others give each project its own repository. Both work, but a monorepo brings a practical problem: cloning and checking out everything becomes slow when you only need one folder.

In this lesson you will compare the main ways to organise many projects, then learn the Git features that make a large repository comfortable: partial clones, shallow clones, sparse checkout and worktrees. You will finish with CI and code ownership rules for a monorepo.

Monorepo, polyrepo or submodules

Imagine a food delivery start-up in Pune with a customer website, a mobile app, a payments service, an orders service and a shared UI library.

ApproachWhat it looks likeStrengthsPain points
MonorepoOne repository with apps/, services/ and packages/ foldersOne pull request can change the API and the app together; shared code is always in sync; one set of toolsRepository grows large; CI must avoid testing everything on every change
PolyrepoOne repository per app or serviceSmall, fast clones; each team controls its own repositoryA change across services needs several pull requests in the right order; shared code is versioned and published separately
SubmodulesA main repository that pins other repositories at exact commitsReuse a separate repository without copying itExtra commands, easy to forget to update; see Submodules

There is no single right answer. Monorepos suit teams that change many parts together; polyrepos suit independent teams with separate release cycles. The rest of this lesson is about making a monorepo fast.

Here is the layout of the start-up's shop-platform monorepo:

Text
shop-platform/
  apps/web/
  apps/mobile/
  services/payments/
  services/orders/
  packages/ui/
  docs/
  package.json

Partial clone: skip old file contents

A normal clone downloads every version of every file ever committed. A partial clone downloads all the commits and folder listings but leaves file contents (blobs) behind until you actually need them. The usual filter is blob:none:

Terminal
git clone --filter=blob:none --sparse https://github.com/meera-kulkarni/shop-platform.git web-dev

Git fetches the missing contents automatically, on demand, when a command needs them: checking out a branch, running git diff on an old commit or git blame. You keep the full history for git log, but the first download is much smaller. This works on GitHub and most hosting services; the server must allow filtering.

The --sparse option in that command starts the next feature: only the files in the root folder are checked out at first.

Sparse checkout: only the folders you work on

Sparse checkout keeps the whole project in the repository but puts only some folders on your disk. Meera works on the website, which uses the shared UI library. Right after the sparse clone above, her working tree holds just the root files:

Terminal
cd web-dev
ls -A
git sparse-checkout add apps/web packages/ui
ls -A
ls apps
git sparse-checkout list
Output
.git
package.json
.git
apps
package.json
packages
web
apps/web
packages/ui

apps contains only web; the mobile app and the services are not on her disk, yet commits, branches and pushes work normally. git sparse-checkout set replaces the whole list instead of adding to it, and git sparse-checkout disable brings every file back.

The folder list uses cone mode, which is the default. In cone mode you name directories, and Git includes everything inside them plus the files directly in their parent folders, such as the root package.json. Cone mode is fast even in huge repositories. Older guides use a non-cone mode with .gitignore-style patterns; the official documentation does not recommend it.

Shallow clones for CI

A shallow clone cuts history instead of file contents. --depth 1 downloads only the latest commit, which is ideal for a CI job or a deployment that just builds the current code:

Terminal
git clone --depth 1 https://github.com/meera-kulkarni/shop-platform.git ci-copy
git -C ci-copy log --oneline
Output
Cloning into 'ci-copy'...
930498c (grafted, HEAD -> main, origin/main, origin/HEAD) Update web and payments (3)

grafted means Git knows this commit has parents but did not download them. Commands that need older history, such as git log beyond that commit, git blame and git bisect, will not work fully. For daily development prefer a partial clone, which keeps the history. GitHub Actions' actions/checkout step makes a shallow clone with depth 1 by default.

Worktrees: two branches checked out at once

You are halfway through a feature when a production bug arrives. Instead of stashing or cloning the whole monorepo again, git worktree gives you a second working folder that shares the same repository. Kabir is on feature/festival-offers with uncommitted changes:

Terminal
git worktree add ../shop-hotfix -b hotfix/upi-timeout main
git worktree list
Output
Preparing worktree (new branch 'hotfix/upi-timeout')
HEAD is now at 930498c Update web and payments (3)
/home/kabir/shop-platform 930498c [feature/festival-offers]
/home/kabir/shop-hotfix   930498c [hotfix/upi-timeout]

He fixes the bug in ../shop-hotfix, commits there and pushes. His feature folder is untouched, uncommitted change and all. When the hotfix is done, he removes the extra folder:

Terminal
git worktree remove ../shop-hotfix
git branch
Output
* feature/festival-offers
  hotfix/upi-timeout
  main

The worktree is gone but the branch and its commit remain. One rule: a branch can be checked out in only one worktree at a time, so Git refuses to open main in two folders.

CI and ownership in a monorepo

Running every test for every change wastes time. GitHub Actions can start a workflow only when files in certain folders change, using paths filters:

YAML
name: Payments service
on:
  pull_request:
    paths:
      - "services/payments/**"
      - "packages/ui/**"
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci
      - run: npm test --workspace=services/payments

A website-only pull request now skips the payments tests. Be careful when such a workflow is a required check: GitHub documents that a workflow skipped by a path filter leaves its check pending, which can block the pull request. See GitHub Actions: First CI Workflow for the basics of workflow files.

Each team should also review changes to its own folder. A CODEOWNERS file maps folders to reviewers, for example /services/payments/ @shop-platform/payments-team. GitHub then requests that team's review automatically. Branch Protection, Rulesets & CODEOWNERS covers the syntax and how to make those reviews required.

Common mistakes

  • Using --depth 1 for daily work. History-based commands break. Use --filter=blob:none instead and keep shallow clones for CI.
  • Thinking sparse checkout limits access. Every file is still in the repository and visible to anyone who can clone it. It only saves disk space and time.
  • Deleting a worktree folder by hand. Git keeps a record of it. Use git worktree remove, or run git worktree prune after an accidental delete.
  • One giant CI job for the whole monorepo. Split workflows by folder with paths filters so a typo fix in docs/ does not run every test.
  • Choosing a monorepo only because big companies use one. They also have tooling teams. Pick the layout that fits how your teams change code together.

Key points

  • A monorepo keeps many projects in one repository; polyrepos and submodules are the alternatives.
  • git clone --filter=blob:none (partial clone) keeps full history but downloads file contents only when needed.
  • --depth 1 (shallow clone) fetches only recent commits and suits CI, not daily development.
  • git sparse-checkout set or add in cone mode puts only chosen folders on disk.
  • git worktree add checks out another branch in a second folder that shares the same repository.
  • Use paths filters in workflows and CODEOWNERS entries per folder to keep a monorepo's CI and reviews focused.

Exercise

Create a practice monorepo with the folders apps/web, apps/admin and services/api, each with a README.md, and commit it. Then:

  1. Clone it a second time with --sparse so only root files appear.
  2. Add only apps/admin to the sparse checkout and confirm with ls.
  3. In the original repository, open a worktree for a new branch fix/login-text in a sibling folder, make a commit there, list worktrees and remove it.
Show solution
Terminal
git clone --sparse monorepo-practice sparse-copy
cd sparse-copy
git sparse-checkout add apps/admin
ls apps
cd ../monorepo-practice
git worktree add ../login-fix -b fix/login-text
git -C ../login-fix commit --allow-empty -m "fix: correct login button text"
git worktree list
git worktree remove ../login-fix

ls apps shows only admin. git worktree list shows two folders until the remove, and the fix/login-text branch with its commit survives the removal. (A local clone ignores --filter, so it is left out here; use it when cloning from a server.)

Quick Quiz

1. Which clone keeps the full commit history but downloads file contents only when they are needed?

2. What does git sparse-checkout set apps/web change?

3. Why is a shallow clone a poor choice for everyday development?

4. You need to fix a bug on main while your feature branch has uncommitted work. Which command avoids stashing?

Learn Git with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call