Monorepo management for opam overlays
OCaml 98.5%
Perl 1.2%
Dune 0.1%
Other 0.2%
156 1 0

Clone this repository

https://tangled.org/gazagnaire.org/monopam https://tangled.org/did:plc:jhift2vwcxhou52p3sewcrpx/monopam
git@git.recoil.org:gazagnaire.org/monopam git@git.recoil.org:did:plc:jhift2vwcxhou52p3sewcrpx/monopam

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

monopam#

Monorepo manager for OCaml.

Core Idea#

A monorepo is a directory of subtrees. Each subtree mirrors an external git repository. You edit code in the monorepo, build, test, commit — then sync with upstream.

Each subtree has:

  • source: where you pull from (any git repo, defaults to your monorepo)
  • origin: where you push to (always your monorepo repo)

Push always goes to your monorepo's git remote — a repo you own. You never accidentally push to someone else's repo. Per-subtree source overrides let you pull from upstream repos you don't own.

Installation#

opam install monopam

Quick Start#

# Initialize a workspace
monopam init --handle yourname.bsky.social

# Add packages (pulls from upstream, pushes to your monorepo)
monopam add https://github.com/mirage/eio.git
monopam add crowbar                              # resolve from opam

# Pull upstream changes
monopam pull

# Make changes, build, test, commit
dune build && dune test
git add -A && git commit -m "Add feature"

# Push to your monorepo remote
monopam push

Commands#

Command Description
monopam add <source> [name] Add a subtree
monopam remove <name> Remove a subtree
monopam pull [names...] Pull updates from source
monopam push [names...] Push changes to your monorepo remote
monopam status [names...] Show sync state
monopam diff [names...] Show changes
monopam publish Generate opam overlay
monopam init Initialize workspace

sources.toml#

Subtree metadata lives in one file. The top-level origin is your monorepo's git remote — the only place push ever writes to. Per-subtree entries override where pull reads from.

# Your monorepo remote — push always goes here
origin = "git@github.com:me/mono.git"

# Subtrees that pull from upstream (override source)
[eio]
source = "https://github.com/mirage/eio.git"

[cohttp]
source = "https://github.com/mirage/cohttp.git"
branch = "main"

# Subtrees with no entry pull from origin and push to origin.
# No need to list your own projects.
  • add <url> adds an entry with source override
  • Subtrees not listed use origin as both source and push target
  • push always goes to the top-level origin
  • pull uses per-subtree source if set, otherwise origin

A subtree that is itself a monorepo can be marked with mono = true:

[open-mono]
source = "git@github.com:me/mono.git"
mono = true

This tells push and pull to recurse into open-mono/ and process its own sources.toml first (depth-first). See Layers.

Source Resolution#

The <source> argument to add can be:

  • A URL: https://github.com/mirage/eio.git — used directly
  • A package name: crowbar — resolved via opam's dev-repo field
  • A URL#ref: https://github.com/mirage/eio.git#v0.15 — pins to a ref

Default branch is the remote's HEAD, not hardcoded.

Layers#

A monorepo is just a subtree. You can nest monorepos to create layers.

product/
  sources.toml
    origin = "git@private.com:co/product.git"
    [open-mono]
    source = "git@github.com:me/mono.git"
    mono = true
    [secret-lib]
    source = "git@private.com:co/secret-lib.git"

  open-mono/
    sources.toml
      origin = "git@github.com:me/mono.git"
      [eio]
      source = "https://github.com/mirage/eio.git"
      [cohttp]
      source = "https://github.com/mirage/cohttp.git"
    eio/
    cohttp/
    mylib/

  secret-lib/
  app/

Same tool, same commands, same sources.toml at every level. The only thing that changes is which directory you're in.

Changes flow one layer at a time:

  • Inward (pull): upstream eio → open-mono → product
  • Outward (push): product → open-mono → your fork → PR to upstream

monopam push is recursive. From the product directory, it detects that open-mono/ has its own sources.toml and pushes inner layers first (depth-first), then the outer layer. One command propagates changes all the way out.

This gives you open-source + closed-source separation naturally. Each layer is a separate git repo with its own access controls. Dune sees all directories and builds everything together.

Overlays#

Each layer can have its own opam-repo overlay for publishing package metadata:

  • open-mono/ → public opam-repo overlay
  • product/ → private opam-repo overlay (references public as base)

monopam publish generates opam entries for the current monorepo's packages.

Daily Workflow#

# Get latest from upstream
monopam pull

# Work
dune build && dune test
git add -A && git commit -m "Description"

# Send changes to your repos
monopam push

Diff#

monopam diff              # What you would push
monopam diff --incoming   # What pull would bring in
monopam diff eio          # Specific subtree

Collaboration (Verse)#

Verse lets you browse and pull from collaborators' monorepos.

# See what collaborators have
monopam verse diff

# Pull their changes
monopam verse pull alice.bsky.social

# Cherry-pick a specific commit
monopam verse cherrypick <sha>

# List members
monopam verse members

Collaboration is just "pull from a different source." A collaborator's monorepo is another subtree you can pull from.

Design Principles#

  1. Push is always safe. Push goes to your monorepo remote. Never to someone else's repo.
  2. Pull and push are the only sync verbs. No sync command. Pull first, build and test, then push. These steps should never be combined.
  3. A monorepo is a subtree. Layers emerge from nesting. Same tool at every level.
  4. One manifest, mostly overrides. sources.toml defines your remote and overrides per-subtree sources. Your own projects need no entry.
  5. Name resolution. add crowbar should work, not just URLs.

Licence#

ISC