groveGitHub

OPEN SOURCE/GO/MIT/PRE-1.0

Every worktree, a real environment.

Grove gives each git worktree reserved ports, its own dependencies, its own database and a background runner. Eight branches running at once, and not one of them holding a terminal.

Read the source

Go 1.26+ · macOS and Linux · Windows via WSL2

lines of Go
5,964LINES OF GO
commands
15COMMANDS
real projects
2REAL PROJECTS
tests
0TESTS
3 OF 99 SLOTS

Every port stays inside its band, and the trailing digits are the slot. Reading down a column gives one worktree.

slot 3web:3103api:8203db:5503
grove ports
$ grove ports
 
acme ~/code/acme
 
band api web db
main 0 8200 3100 5500
payments 1 8201 3101 5501
auth-v2 2 8202 3102 5502
refactor 3 8203 3103 5503
 
3 of 99 slots in use. Bands: 3100, 5500, 8200

What Grove gives every worktree

PORTS

Every port is band + slot

Ninety-nine worktrees per project, and no two projects on one machine can claim the same band — reservations are global, not per repository.

ISOLATION

Its own dependencies

A generator with no output path writes into node_modules. Per-worktree installs by default, so one generate cannot rewrite every branch's client at once.

DATABASE

Proven, not assumed

A restore is not finished until it is counted. Grove runs ANALYZE and checks tables and rows against the thresholds your config sets, then fails with the exact reseed command if it came back short.

RUNNER

Nothing holds a terminal

Services start fully detached and reparent to PID 1. Eight branches run at once; your terminal stays free.

PORTS

One offset, applied to every band.

Each service owns a 100-wide band. A worktree owns one slot, and the main checkout is slot 0 — so a band is also its own port, and the trailing digits of any port are always the slot number. Ninety-nine worktrees fit before a band is full.

grove list
$ grove list
 
acme ~/code/acme
 
WORKTREE BRANCH SLOT MODE DIRTY DEPS RUNNING
acme (main) main 0 - 6 - db:5500
payments feat/payments 1 fullstack 8 real api:8201 web:3101 db:5501
auth-v2 feat/auth-v2 2 fullstack 6 real api:8202 web:3102
refactor feat/refactor 3 fullstack 6 real —

Bands are reserved globally

Reservations live in ~/.grove, not in the repository — so a second project is given a different band rather than the one already taken, and Grove will not reserve a colliding band unless you confirm it. Every allocation is a flock-guarded read-modify-write with an atomic rename.

Well-known ports are called out, not silently moved

A band containing 3000, 5173 or 5432 only bites at the one slot whose offset lands there. Grove warns and names that slot rather than relocating a band your project already uses. A collision with another project is graver: grove init -y refuses outright, and interactively you have to confirm it.

LIFECYCLE

Five commands, from empty repo to eight running branches.

Every block below is literal stdout, captured from the binary against a scratch pnpm monorepo.

Branch, worktree, env files, dependencies and a database — seeded and proven non-empty before the command returns.

grove create
$ grove create payments
creating worktree "payments" on branch feat/payments (from main)
Preparing worktree (new branch 'feat/payments')
HEAD is now at c787d86 real servers
env written (slot 1, fullstack mode)
installing pnpm in . ...
Scope: all 3 workspace projects
Already up to date
 
Done in 191ms using pnpm v11.9.0
dependencies installed — builds and code generation are safe here
starting database for "payments" on port 5501 ...
restoring the seed dump into acme_wt1 ...
applying pending migrations ...
verified: 12 tables, ~4800 rows
"payments" is serving seed data — acme_wt1 on localhost:5501
 
"payments" is ready at ~/code/acme/worktrees/payments
slot 1 — api:8201 web:3101 db:5501
 
Start it: grove up payments

CONFIGURATION

Two files, and one rule between them.

Nothing machine-specific may reach git, and nothing project-shaped may live only on your machine. A teammate clones the repo and grove works without editing anything.

.grove.jsonCOMMITTED

The project's shape — services, port bands, package manager, database, env files. Written by grove init after it inspects the repo, and edited by hand thereafter. It is the contract; detection is only a convenience.

~/.grove/registry.jsonNEVER COMMITTED

This machine's allocation — which project owns which band, which worktree holds which slot. Two people on the same project get different slots without coordinating.

Values are templated, never substituted

{{port.api}}, {{slot}}, {{db.name}}. An unknown variable is a hard error, so a typo fails at provisioning time instead of writing itself into a .env.

.grove.json
{
  "project": "acme",
  "maxSlots": 99,
  "services": [
    { "name": "api", "kind": "backend", "band": 8200,
      "cmd": "pnpm --filter ./apps/api run dev",
      "port": { "via": "env", "key": "PORT" } },
    { "name": "web", "kind": "frontend", "band": 3100,
      "cmd": "pnpm --filter ./apps/web run dev",
      "port": { "via": "flag", "key": "-p" } }
  ],
  "env": {
    "files": [{
      "path": "apps/api/.env",
      "fullstack": {
        "PORT": "{{port.api}}",
        "DATABASE_URL": "postgresql://acme:acme@localhost:{{port.db}}/{{db.name}}"
      }
    }]
  },
  "db": {
    "driver": "docker-postgres", "band": 5500, "image": "postgres:16",
    "name": "acme_wt{{slot}}", "container": "postgres-acme-wt{{slot}}",
    "verify": { "minTables": 10, "minRows": 1 }
  }
}

WHY IT LOOKS LIKE THIS

Grove is a rewrite. The bash version is the specification.

Eighteen hundred lines of shell ran this daily, on a machine with eight worktrees, until each of these broke it. Every non-obvious decision in the Go version is here because the obvious one was tried first.

  1. 01

    The 24-worktree ceiling

    internal/registry/registry.go
    naive
    Compute a worktree's ports by multiplying its slot: band + slot × 100.
    broke
    One worktree fanned out across 3000–6900, and at slot 25 it collided with the database band. A hard 24-worktree ceiling, on a machine already running eight.
    grove
    Each service owns a 100-wide band and the slot is a +1 offset. Bands are reserved globally in ~/.grove — the only thing stopping two repositories both claiming 3100.
  2. 02

    The sed that silently stopped matching

    internal/provision/env.go
    naive
    sed the main checkout's literal port numbers into each worktree's env files.
    broke
    When the main checkout's ports changed, the patterns quietly stopped matching. No error — every worktree kept pointing at the main backend and the main database.
    grove
    Env files are computed key=value assignments, never textual substitution. Keys are rewritten in place, preserving order, comments and every key Grove was not asked to touch.
  3. 03

    The vite that survived its own kill

    internal/runner/runner.go
    naive
    Stop a worktree by killing the pids you recorded when you started it.
    broke
    Dev servers fork children that outlive the pid file. And pids plus current ports only cover what the slot owns now — a vite left on a pre-migration port outlived a full teardown of its own worktree.
    grove
    down kills in three passes, and the third — anything whose working directory is inside the worktree — is the one everyone forgets. It was proven explicitly: a hand-started server on a port the slot does not own died; a server in the main checkout survived.
  4. 04

    Two generators, one container name

    internal/dbx/dbx.go
    naive
    Two compose generators, both emitting the same container name on the same port — one seeded, one empty.
    broke
    Whichever ran last won. The convenient command was the one that produced the empty database.
    grove
    One compose file, and the database is proven non-empty before the command returns. AssertPopulated is a hard error, not a warning.
  5. 05

    The flag that beats the environment

    internal/config/config.go
    naive
    Configure every service's port through environment variables.
    broke
    Next.js -p overrides PORT. A project wired only through the environment binds whatever its dev script hardcodes — in every worktree, forever.
    grove
    Ports declare how they bind. via: env or via: flag, per service.
  6. 06

    Six services reading one PORT

    internal/detect/detect.go
    naive
    Treat each workspace's .env as that service's own env file.
    broke
    A monorepo commonly symlinks every workspace's .env back to one root file. Treated as distinct, six services read the same PORT from that single file, collided, and all moved off their correct bands.
    grove
    Detection resolves symlinks before treating a file as a service's own env file.
  7. 07

    Copy-on-write is a volume property

    internal/provision/provision.go
    naive
    Assume cloning from the package store is free because the filesystem supports it.
    broke
    pnpm clones from its store only within one filesystem; across volumes the same install silently becomes full copies. And du cannot see shared blocks — it reports many times the real cost, so the problem is invisible from the obvious tool.
    grove
    Grove compares the store's device id against the worktree's and downgrades rather than eating the disk. Measure with df, not du.
  8. 08

    --force by default

    cmd/delete.go
    naive
    Force worktree removal unconditionally, and delete the branch too.
    broke
    Worktree removal discards uncommitted changes; deleting the branch discards unpushed commits. Both, with no prompt.
    grove
    delete checks for uncommitted changes and unpushed commits first, refuses, and names the flag that overrides it.
the guard that fires
$ grove delete payments
"payments" has 8 uncommitted change(s) — commit them, or re-run with --force
grove doctor
$ grove doctor
 
repository root: ~/code/acme
.grove.json is valid — 2 services, maxSlots 99
registered in ~/.grove — 3 slot(s) allocated
port bands 3100, 5500, 8200 are clear of other projects and well-known ports
pnpm store shares a volume with worktrees — installs stay copy-on-write
 
no problems found

REFERENCE

Fifteen commands.

Nothing hidden behind a plugin system.

grove init
detect this repo, write .grove.json, reserve bands
grove create <name> [base]
new branch + worktree, provisioned and seeded
grove track <branch> [name]
check out an existing branch, local or remote
grove setup <name>
re-provision: repair ports and links, convert mode or deps
grove delete <name>
remove worktree, database, slot and branch
grove db <name> [profile]
provision its database, verified non-empty
grove up <name> [service...]
start detached
grove down <name>
stop — pids, ports, and working directory
grove ps
what is running
grove logs <name> <service>
captured output
grove dev [service...]
run in the foreground, on this worktree's ports
grove list
worktrees, slots, modes, what is listening
grove ports [-g]
port table; -g for every project on this machine
grove doctor
config, registry and machine checks
grove prune
drop stale worktrees and free their slots

WHERE IT IS NOT DONE

Pre-1.0, and specific about it.

In daily use on two real projects — a pnpm/Turbo monorepo with six services and Postgres, and a polyglot repo tying a Flutter app to a NestJS API. Here is what it still is not.

No tests yet

Zero _test.go files against ~6,000 lines. config and detect are pure functions with obvious table tests — that is the highest-value next work, and the most useful thing to contribute.

Postgres via Docker only

docker-postgres is the one implemented driver. MySQL, SQLite and Mongo are not supported. Migrations are best-effort: a failure warns and continues.

Windows is stubbed on purpose

Reading another process's working directory has no public Win32 API — it needs NtQueryInformationProcess. WSL2 is the answer today, and the stub documents what a native port would take.

Detection proposes, it does not decide

It is strongest on JavaScript workspace monorepos. The polyglot path covers Node and Flutter sub-projects tied together by a justfile; anything else means writing .grove.json by hand. It is a contract, not a guess.

Stop stopping servers to change branches.

One command to install, one to detect your repo, one per branch after that.

github.com/PrerakGada/grove