Branching in a Sapling Monorepo

Meta introduces directory branching in Sapling — a technique that gives monorepo teams proper branch semantics scoped to a directory, while keeping the repo's commit graph strictly linear.

Branching in a Sapling Monorepo

The reason teams adopt a monorepo is usually unified dependency management — everything is visible at HEAD, refactors cross package boundaries, CI tests the whole tree. Create a full-repo branch and you immediately undermine all three. The dependency graph forks. Cross-package refactors now have to land in two places. CI runs on two diverging commit graphs. The monorepo didn’t fail you; the branching model did.

Source

Branching in a Sapling Monorepo

By: Meta Engineering
Published: Oct 16 2025
Type: Engineering article

Saved because the directory branching model solves a problem I’ve seen teams paper over with copy-paste directories and manually tracked patch lists — and it does it with real source control semantics rather than a convention that relies on everyone following it correctly.

Why full-repo branches break at monorepo scale — and what replaces them

In a multi-repo setup, branching is cheap. You branch a repo, maintain it, cherry-pick back to main, and eventually delete it. The operation is scoped to one repository’s commit graph. Nobody else’s history gets complicated.

A monorepo inverts that. A branch touches the entire repository, so every project in the tree — unrelated features, dependencies, infrastructure code — gets frozen at the branch point. While the branch lives, those frozen paths don’t receive the updates landing on main. Merging the branch back eventually creates a merge commit with multiple parents, making the commit graph wide and non-linear. That’s expensive for the whole monorepo: log and blame slow down for every user, not only the team that made the branch. Meta’s experience with Sapling hardened this into a principle: a linear commit graph is not aesthetic preference, it’s a performance constraint, and full-repo branches make it impossible to maintain.

Full-repo branching does work in one case: when you never merge back. Release branches that track a shipped version and then die when that version goes end-of-life don’t need to return to main. Sapling supports this with the sl bookmark command family. The operative word is non-mergeable — the branch exists to track a frozen snapshot, not to carry forward active development. For everything else, Sapling’s answer is directory branching.

The mental model: treat a directory in the monorepo the way you’d treat a repository branch. You create it by copying the source directory to a new path with sl subtree copy. That command does more than copy files — it records metadata in the commit itself: source directory, source revision, and the copy relationship. Because that metadata is stored, Sapling can reconstruct the full history of every file in the new directory branch. It’s not a blind file copy you have to track by convention; the source control system knows the relationship.

From there, sl subtree graft cherry-picks individual changes between directory branches, and sl subtree merge runs a three-way merge. The common ancestor for the merge is derived from the stored copy and merge metadata, so Sapling knows exactly which revision to diff from. This is the same three-way merge algorithm used for regular repository merges, just scoped to directory content rather than the whole tree. Multiple directory branches can exist simultaneously, all visible at HEAD in the same working copy.

At the monorepo’s commit graph level, all of these operations appear as normal linear commits. No merge commits with multiple parents. log and blame stay fast for everyone. The branching semantics are real — they have history, cherry-pick, merge — but the commit graph stays linear. This is the core tradeoff the design is built around.

Because all directory branches coexist at HEAD, CI can test multiple branches in a single checkout. There are no hidden old branches quietly running in production and not covered by the test suite. When a component needs to depend on a specific directory branch, Meta’s build system Buck2 uses config modifiers (-m flag at build time) to select which copy to compile against.

Three patterns drove adoption across Meta’s engineering teams:

The first is production versus development separation. Some teams maintained a production copy of their code that received explicit, controlled cherry-picks while development continued freely on main. This is a staging/prod split without leaving the monorepo or forking the dependency graph.

The second is large experimental collaborations. When a significant feature requires months of parallel development by many engineers and carries real risk to the production path, a stack of individually submitted diffs stops being practical past a certain team size. A directory branch gives the feature its own coherent codebase with normal merge workflows while appearing linear to the rest of the monorepo.

The third is Git migration unblocking. Migrating a Git multi-repo setup to a monorepo usually means the Git side still has live branches mid-migration. Those branches can’t always be consolidated before the migration completes. Directory branching gives them somewhere to land in the monorepo without forcing full consolidation as a migration prerequisite.

There is one genuine friction point: code search. A function that exists in three directory branches produces three search hits. Search tools that can rank or filter results by “primary branch” handle this cleanly; tools that list all matches without context get noisy. It’s manageable but worth designing for before rollout — users who aren’t aware of the branching structure will be confused by duplicate results.

My take

This is a precise answer to a problem teams usually resolve with one of two wrong strategies: copy-paste directories tracked manually in a spreadsheet, or a policy of “no branching, everyone shares main.” Directory branching names the third option — it gives teams branch semantics without asking the commit graph to pay for them.

Where this is useful

  • Monorepo teams managing simultaneous product versions — stable release plus active development — that need mergeable cherry-pick workflows
  • Large experimental features that can’t be merged to main incrementally and are too big for a single stacked diff
  • Teams mid-way through a Git multi-repo to monorepo migration with live branches on the Git side
  • When NOT applicable: single-repo, standalone setups where a normal Git branch on its own repository is the correct and simpler tool

Apply this: If your team maintains “version directories” by manually copying files and tracking patches in comments or docs, look at sl subtree as a way to get actual cherry-pick and merge semantics on those copies without touching the monorepo’s commit graph structure.

What I want to remember

Remember: Directory branches stay linear at the repo commit-graph level by recording copy and merge metadata in commits, so three-way merges between directories work without ever creating multi-parent merge commits.