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.
Go 1.26+ · macOS and Linux · Windows via WSL2
- lines of Go
- 5,964LINES OF GO
- commands
- 15COMMANDS
- real projects
- 2REAL PROJECTS
- tests
- 0TESTS
✓ Every port stays inside its band, and the trailing digits are the slot. Reading down a column gives one worktree.
$ grove portsacme ~/code/acmeband api web dbmain 0 8200 3100 5500payments 1 8201 3101 5501auth-v2 2 8202 3102 5502refactor 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 listacme ~/code/acmeWORKTREE BRANCH SLOT MODE DIRTY DEPS RUNNINGacme (main) main 0 - 6 - db:5500payments feat/payments 1 fullstack 8 real api:8201 web:3101 db:5501auth-v2 feat/auth-v2 2 fullstack 6 real api:8202 web:3102refactor 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.
Inspects package manager, workspaces, dev scripts, how each service takes its port, Prisma and docker-compose. Proposes a config; you confirm it.
$ grove initDetectedroot ~/code/acmepackage manager pnpmworkspaces 2env files apps/api/.envdatabase docker-postgres (postgres:16, host port 5500)servicesapi backend unknown PORT currently :8200web frontend next -p (flag) currently :3100✓ wrote ~/code/acme/.grove.json✓ reserved bands 3100, 5500, 8200 in ~/.grove→ Next: `grove create <name>` to provision a worktree, `grove ports` to see the table.
Branch, worktree, env files, dependencies and a database — seeded and proven non-empty before the command returns.
$ 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 projectsAlready up to dateDone 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
Starts detached: new session, no controlling terminal, output to a per-service log, reparented to PID 1. Close the terminal; it keeps running.
$ grove up payments→ starting "payments" (fullstack) — [web]✓ db up on 5501✓ api up on 8201✓ web up on 3101→ Detached — close this terminal freely. Status: grove ps Logs: grove logs <name> <service>
What is running, on which port, under which pid — across every worktree at once.
$ grove psacme ~/code/acmeWORKTREE SERVICE PORT PID STATEauth-v2 api 8202 50008 upauth-v2 web 3102 50159 uppayments api 8201 49049 uppayments web 3101 49151 up→ 4 service(s) running
Three passes: recorded process trees, whatever holds the slot's ports, then anything whose working directory is inside the worktree.
$ grove down auth-v2→ stopping "auth-v2" ...✓ "auth-v2" stopped
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.jsonCOMMITTEDThe 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 COMMITTEDThis 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.
{
"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.
- 01internal/registry/registry.go
The 24-worktree ceiling
- 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.
- 02internal/provision/env.go
The sed that silently stopped matching
- 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.
- 03internal/runner/runner.go
The vite that survived its own kill
- 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.
- 04internal/dbx/dbx.go
Two generators, one container name
- 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.
- 05internal/config/config.go
The flag that beats the environment
- 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.
- 06internal/detect/detect.go
Six services reading one PORT
- 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.
- 07internal/provision/provision.go
Copy-on-write is a volume property
- 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.
- 08cmd/delete.go
--force by default
- 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.
$ grove delete payments✗ "payments" has 8 uncommitted change(s) — commit them, or re-run with --force
$ 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