berth / Documentation / berth vs raw git worktree

berth vs raw git worktree

A linked Git worktree is the substrate berth is built on, not a competitor to it. This page separates what git worktree already solves from the four things it deliberately leaves to you, then says plainly where the raw command is the better answer.

Git worktrees solve the part of parallel work that is hardest to solve any other way: two working directories, two branches, one object store, one command, no daemon, nothing resident afterwards. berth does not replace that — berth new runs it. The difference is everything a checkout needs in order to run: a port set, private data and a process graph. Git leaves those to you on purpose, and berth is the layer that stops leaving them to you.

The axes

Design-axis comparison. Each row describes what the two approaches do at that axis; the sections below expand the ones that decide the outcome.
Design axis berth raw git worktree
Isolation boundary The workspace: a linked worktree, a private data directory, a reserved port set and one supervised process graph Files and branches. Ports, data and processes stay on the host, shared with every other checkout
Startup cost One git worktree add plus a short registry transaction behind a file lock — no daemon, and nothing left running One git worktree add. Nothing else is set up, because nothing else is managed
Port allocation Named ports reserved in one cross-process transaction with the workspace record, stable for its lifetime: BERTH_PORT_<NAME> in-context, BERTH_HOST_PORT_<NAME> on host 127.0.0.1 Manual. The number lands in a local config file or a shell override, which is exactly where it gets committed by mistake
Data isolation $BERTH_DATA_DIR (<workspace>/.berth/data) for databases, caches and sockets; copy_dirs and .worktreeinclude seed dependency trees and local files during setup None. Gitignored state — .env, a local database directory, caches — is shared with every checkout or copied by hand
Process supervision A declared process graph supervised by process-compose natively, with readiness probes and up/down/status/logs per workspace None. Background shells and remembered PIDs; a server that outlives its terminal still holds its port
Per-workspace configuration Each workspace reads berth.yaml from its own branch, so processes, hooks and env are branch-local; the runtime and port contract is immutable once created Branch-local for tracked files only. Ignored files and environment are whatever the host and the primary checkout happen to have
Host filesystem speed Native mode runs host processes on the host filesystem; copy_dirs uses copy-on-write where the filesystem supports it and an independent copy elsewhere Host speed by definition: the checkout is an ordinary directory
Safe removal berth done requires the commit preserved and the worktree clean, then removes the checkout and its registration; branch deletion uses Git's atomic old-value check, and berth gc never forces git worktree remove plus git branch -d: two commands, and the judgement about whether the work is preserved is yours

What a linked worktree genuinely solves

Git already provides the hard part. git worktree add ../my-repo.berths/feature-a -b berth/feature-a gives you a second working directory with its own index and HEAD, backed by the same object store as the primary checkout. No clone, no second fetch, no duplicated history, no service to keep running, and nothing to install. git worktree list is a truthful inventory of what is checked out on the machine, and creating one is cheap enough to do per task rather than per week.

That is precisely why berth uses it instead of inventing something else. berth new creates a linked worktree on branch berth/<slug>, beside the repository at <repo>.berths/<slug> (or under worktree_root when you configure one). berth adopt exists for the mirror case: a linked worktree created by something else is already most of a workspace, and adopting it registers the checkout without recreating it. If your entire problem is "two branches, one repository, two editors", Git solved it and does not need help.

The four things a worktree leaves to you

A port for every checkout

Worktrees isolate files; they do not isolate a TCP port. The primary checkout says PORT=3000 in a gitignored .env, the new worktree needs another number, and the next task needs a third. The usual workaround is a per-checkout environment file or a command-line override — and an override written into a tracked file is a commit waiting to happen. That is the specific trap: dynamic local state turns into a repository diff, and then into a merge conflict about a port number. berth answers it with a named port set declared in berth.yaml, reserved atomically with the workspace record in one cross-process transaction and stable for the lifetime of the workspace. Programs read BERTH_PORT_<NAME> for the port inside their execution context and BERTH_HOST_PORT_<NAME> for the host publication on 127.0.0.1.

Ignored state: .env, local databases, caches

Git protects what it tracks, and most projects deliberately do not track .env, the local database directory, caches or generated fixtures. Every worktree that skips them therefore reads and writes the same bytes as every other checkout. Two migration runs against one data directory is not a merge conflict you can resolve; it is corrupted local state, and the symptom usually appears somewhere far away from the cause. berth gives each workspace $BERTH_DATA_DIR for mutable state, copies writable dependency trees through copy_dirs, and copies individual repository-relative files such as .env through a .worktreeinclude path list.

Process lifecycle

A checkout contains no scheduler. The dev server you started in a background shell keeps holding its port after the terminal that started it is gone, and identifying it later means reading netstat output and guessing which of the results is yours. berth keeps a declared process graph per workspace, supervised by process-compose in native mode or by the container engine in container mode. Readiness probes make berth up wait for ready instead of a sleep loop, berth status --json reports what is actually running, berth logs <proc> shows why something is not, and berth down stops the graph without touching data.

Ownership, and removal that is safe to run

Six worktrees in, the question is no longer only "which branch is this" but "may I delete it". With raw Git that is two decisions made from memory: is this checkout still needed, and is its branch safe to delete? berth stores a workspace record, so the answer is a lookup rather than a recollection. berth done requires the branch commit to be preserved — merged or present upstream — and the worktree to be clean, then tears down the runtime and removes the checkout and its registration. Branch deletion uses Git's atomic old-value check against the recorded commit, --force cannot bypass ownership, identity, primary-worktree or shutdown checks, and an adopted checkout is never removed. berth gc selects candidates and never forces.

What berth adds on top of Git

Read together, the four sections above are additive, not a rewrite of Git. berth keeps the worktree and wraps it in a workspace record: the record carries the ports, the data directory, the runtime choice and the process graph, and it is what makes the rest of the lifecycle answerable without memory. The raw Git path and the berth path differ less in what gets checked out than in what happens next.

git worktree add ../my-repo.berths/feature-a -b berth/feature-a
cd ../my-repo.berths/feature-a
# now: pick a free port, write a local .env, start the services, remember the PIDs

The same task through berth keeps the first line and replaces the other three with managed ones:

berth new feature-a --up        # linked worktree, reserved ports, supervised processes
cd ../my-repo.berths/feature-a
berth run -- npm test
berth status --json             # readiness instead of sleep loops
berth done feature-a            # refuses until the commit is preserved and the tree is clean

Commit berth.yaml before creating the workspace. Each workspace loads its configuration from its own branch, so uncommitted edits never reach a workspace created from them — and a workspace created from an old commit will not see the ports and processes you just added.

Do not clean up a workspace by hand. rm -rf, git worktree remove and guessed PID or port cleanup corrupt the registry instead of releasing a workspace. Use berth down to stop it, berth done to release it, and berth gc to reclaim what was abandoned.

When raw git worktree is the right answer

Often. A worktree is the correct tool when you want to read another branch while keeping your working tree untouched, when the task is short and single-purpose and you will merge it in one sitting, and when the project genuinely has no services, no database and no listening ports — a documentation repository, a library, a small CLI. It is also the right answer when you cannot or will not install another binary on the machine, when adding a tool to the repository requires review you do not want to spend, or when you want the smallest possible mental model: worktrees have no registry to understand and no state to reconcile.

berth costs you a committed berth.yaml, a workspace record and a lifecycle to learn. It earns that back only if at least one of the four sections above is a real problem for you. For a repository whose main resource contention is a shared port and a shared database directory, that is usually the case; for one where two checkouts rarely run at the same time, it usually is not, and using worktrees directly is the honest recommendation.