git-forest manages one named workspace across multiple Git repositories using
linked worktrees. Git exposes the installed binary as both:
git-forest <command>
git forest <command>Running git forest without a subcommand opens an interactive workspace
launcher. The existing management commands remain non-interactive; open
explicitly starts the same launcher. Forest contacts remotes only for explicit
setup, fetch, and update commands. It does not reset or delete branches,
start runtime services, or maintain a separate worktree registry. Git worktree
metadata and the filesystem are authoritative. The launcher and the explicit
attach command can create or focus a workspace in a running
Herdr session.
The project currently tests Linux and macOS. Building from source requires Git
and Rust; the repository pins its Rust toolchain in
rust-toolchain.toml.
Clone the repository and install the binary with Cargo:
git clone https://github.com/hiradp/git-forest.git
cd git-forest
cargo install --locked --path .The just development recipes provide an equivalent
install command that forces replacement of an existing installation:
just installBoth commands install into Cargo's binary directory (normally
$CARGO_HOME/bin, which must be on PATH). Set CARGO_INSTALL_ROOT to choose
another installation prefix:
CARGO_INSTALL_ROOT="$HOME/.local" just installgit-forest reads a .forest.toml:
version = 1
[repositories]
root = "src"
remote = "git@github.com:example/{name}.git"
members = [
"api",
"operator",
]
[workspaces]
root = "src/.workspaces"
branch = "user/{workspace}"All paths are relative to the directory containing .forest.toml.
repositories.root contains the canonical clones. Each member is both its CLI
name and its directory beneath that root. repositories.remote is optional for
projects that provision repositories separately. It is required by setup
when a canonical clone is missing. Relative local remotes are resolved from the
directory containing .forest.toml.
The only supported placeholders are:
{name}inrepositories.remote;{workspace}inworkspaces.branch.
When present, the remote template must contain {name}; the branch template
must contain {workspace}. Unknown placeholders, duplicate members, absolute
roots, and unsupported configuration versions are rejected.
Configuration precedence is:
--config <path>;FOREST_CONFIG;.forest.tomlfound by walking from the current directory to the filesystem root.
Workspace names must match [A-Za-z0-9][A-Za-z0-9._-]*. . and .. are not
valid names.
git forest
git forest open
git forest setup [--json]
git forest repos [--json]
git forest fetch [<repository>...] [--jobs <N>] [--json]
git forest update [<repository>...] [--jobs <N>] [--json]
git forest create <workspace> <repository>... [--base <repository>=<ref>]... [--branch <repository>=<branch>]... [--json]
git forest add <workspace> <repository>... [--base <repository>=<ref>]... [--branch <repository>=<branch>]... [--json]
git forest list [--json]
git forest status [<workspace>] [--json]
git forest path <workspace> [--json]
git forest attach <workspace> [--json]
git forest remove <workspace> [<repository>...] [--json]
Global options:
--config <path>
--help
--version
Run git forest in a terminal to open the workspace launcher. git forest open
is the explicit equivalent. Start typing to fuzzy-search workspaces, then press
enter to attach the selected workspace in Herdr.
π² Forest
Pick a workspace. Weβll get it ready.
β Where do you want to work?
βΊ + Create a new workspace
logical-slots api Β· operator
review-123 api
[type to search Β· ββ to move Β· enter to open Β· esc to leave]
The first picker also offers Create a new workspace. Forest prompts for a
valid name and presents the configured repositories as a searchable
multi-select. After a successful preflight it creates the linked worktrees and
attaches the new workspace. The launcher uses the configured branch template
and each repository's local origin/HEAD; use the non-interactive create
command when branch or base overrides are needed.
Press escape at any prompt to leave without making changes. The launcher honors
NO_COLOR. Without an interactive terminal, invoking Forest without a
subcommand prints help instead of waiting for input.
Ensures every configured canonical repository exists. Existing Git worktrees
are reused without fetching or changing their remotes. Missing repositories are
cloned from the rendered repositories.remote template in configuration order.
Before cloning, Forest checks every configured destination. An existing path that is not a Git worktree or a missing repository without a configured remote prevents all cloning. If a clone fails after earlier repositories succeeded, the successful clones are preserved and later repositories are not run. Repeating the command safely reuses completed clones and resumes the rest. Clones are completed in adjacent staging directories before being moved into the configured canonical paths, so an interrupted clone is never reused as a completed repository. Forest never overwrites an existing path.
Lists configured repositories in configuration order. Missing canonical clones are reported rather than making the whole command fail. For present clones it reports the origin URL and the default ref when available.
The default base is discovered exclusively through the symbolic ref
refs/remotes/origin/HEAD. The command never guesses main or master and
never contacts a remote to repair a missing default.
Fetches origin for every configured canonical repository. Pass repository
names to fetch only a subset. Fetches run concurrently, with at most 16 in
flight by default; use --jobs <N> to change that bound. The report remains in
configuration order. The command attempts every selected repository and exits
unsuccessfully if any fetch fails. Forest does not fetch tags because its
workspace operations consume remote-tracking branch refs only.
fetch updates remote-tracking refs, including the origin/HEAD target used as
the default creation base, but does not merge, reset, or otherwise update local
branches or worktrees. To create a workspace from the latest fetched defaults:
git forest fetch
git forest create logical-slots api operatorFetches the remote default branch, then fast-forwards its local branch in every
configured canonical repository. Pass repository names to update only a subset.
Fetches use the same bounded concurrency and --jobs <N> option as fetch, but
transfer only the branch identified by refs/remotes/origin/HEAD rather than
negotiating every remote branch and tag.
Forest does not hard-code main or master. If the branch is checked out, Forest updates its
worktree with a fast-forward-only merge. If it is not checked out, Forest moves
the local ref only after verifying a fast-forward. Dirty checked-out branches,
ignored paths that overlap incoming changes, missing local default branches,
locally ahead branches, and diverged branches are reported as conflicts and are
never reset or forced. A fetch or Git failure
in one repository does not prevent the other selected repositories from being
processed, but conflicts and failures make the command exit unsuccessfully.
git forest updatecreate permits the workspace directory to be absent. add requires an
existing workspace. Both use the same idempotent creation engine.
Before mutation, every requested repository is checked for:
- a present canonical Git worktree;
- a valid rendered branch name;
- a non-conflicting destination path;
- existing worktree registration;
- branches checked out elsewhere;
- branch namespace conflicts;
- a resolvable base when a new branch is needed.
An existing worktree is reused only when its path, canonical repository, and
branch all match. An existing branch that is not checked out elsewhere is added
without being recreated. New branches use origin/HEAD unless --base is
provided.
Use --branch <repository>=<branch> to select a branch independently of the
workspace name. Forest first uses an existing local branch. If it is absent,
Forest creates a local branch from refs/remotes/origin/<branch> and configures
the remote branch as its upstream. The remote-tracking ref must already exist
locally; creation never fetches. For example, to review a branch after fetching:
git forest fetch api
git forest create review-123 api --branch api=contributor/fixRepositories without a branch override continue to use the configured branch template. A branch override and a base override cannot both target the same repository. After later fetches, Forest reports whether a tracking branch is behind but never merges, resets, or otherwise updates it implicitly.
Preflight conflicts prevent all mutation. If Git fails after earlier repositories have been created, successful worktrees are preserved and later repositories are marked as not run. Repeating the command resumes safely.
Human output keeps shared workspace details in one header and summarizes each repository on a compact result line:
Workspace logical-slots
Path /project/src/.workspaces/logical-slots
Branch user/logical-slots
β api created new branch
β operator reused
Colors are enabled only when stdout is a terminal and can be disabled with
NO_COLOR. Use --json when every report field is needed.
list reconciles workspace directories with every canonical repository's Git
worktree metadata. It reports unregistered paths, missing registered paths,
unexpected entries, and layout mismatches.
status additionally reports:
- current branch or detached state;
- HEAD commit;
- tracked and untracked dirty state;
- canonical worktree registration;
- upstream;
- ahead and behind counts.
Human output is exactly the absolute workspace path followed by a newline:
workspace=$(git forest path logical-slots)
tmux-sessionizer "$workspace" logical-slotsThe workspace must exist.
Opens an existing Forest workspace in Herdr. The herdr executable must be on
PATH, and a Herdr server for the current session must already be running.
Forest never starts or stops the server.
The layout is intentionally fixed. It contains one shell pane per tab and does not start commands:
- on initial attachment,
1-mainstarts in the Forest workspace root; - each present, registered repository gets a tab rooted in its worktree;
- repository tabs are initially created in configuration order;
- each managed tab's numeric prefix matches its current Herdr tab position,
such as
2-apiand3-operator.
Forest records the canonical workspace path in Herdr's runtime metadata. A later attachment with one matching Herdr workspace reuses it, creates missing managed tabs at the end, repairs managed tab names from their current Herdr tab positions, and focuses the managed main tab. Existing managed and unmanaged tabs retain their positions, and additional panes are preserved. Multiple matches are rejected rather than guessed.
Attachment does not create worktrees or otherwise change Git state. A workspace with inconsistent configured worktrees is rejected. Removing a Forest workspace does not close its Herdr workspace or processes.
Human output summarizes the Herdr workspace and each managed tab:
Workspace logical-slots
Path /project/src/.workspaces/logical-slots
Herdr w1
β 1-main created /project/src/.workspaces/logical-slots
β 2-api created /project/src/.workspaces/logical-slots/api
β 3-operator created /project/src/.workspaces/logical-slots/operator
Removal is deliberately conservative:
- modified, untracked, and ignored files prevent removal;
- every selected path must be registered with its configured canonical repository;
- removal always uses
git worktree remove; - branches are never deleted;
- there is no force option;
- the workspace directory is removed only when it is empty;
- unexpected files are reported and preserved.
Removing only named repositories leaves other members in place. Repeating a partially completed removal is safe.
Paths are absolute. Optional values are represented as null rather than
omitted.
{
"repositories": [
{
"name": "api",
"path": "/project/src/api",
"remote": "git@github.com:example/api.git",
"status": "cloned",
"message": null
}
]
}Setup status is cloned, reused, conflict, failed, or not_run.
{
"repositories": [
{
"name": "api",
"path": "/project/src/api",
"exists": true,
"is_git_worktree": true,
"origin_url": "git@github.com:example/api.git",
"default_ref": "refs/remotes/origin/main"
}
]
}{
"repositories": [
{
"name": "api",
"path": "/project/src/api",
"status": "fetched",
"message": null
}
]
}Fetch status is fetched or failed.
{
"repositories": [
{
"name": "api",
"path": "/project/src/api",
"branch": "main",
"status": "updated",
"message": null
}
]
}Update status is updated, up_to_date, conflict, or failed. branch is
null when origin/HEAD or the fetch result does not identify a default branch.
{
"workspace": "logical-slots",
"path": "/project/src/.workspaces/logical-slots",
"repositories": [
{
"name": "api",
"path": "/project/src/.workspaces/logical-slots/api",
"branch": "user/logical-slots",
"base_ref": "refs/remotes/origin/main",
"action": "create_branch",
"status": "created",
"message": null
}
]
}action is reuse, add_existing_branch, create_branch, or null for a
conflict discovered before an action could be selected. A branch created to
track an explicit --branch uses create_branch, with its remote-tracking ref
in base_ref. status is reused, created, conflict, failed, or
not_run.
{
"workspaces": [
{
"name": "logical-slots",
"path": "/project/src/.workspaces/logical-slots",
"exists": true,
"repositories": [
{
"name": "api",
"path": "/project/src/.workspaces/logical-slots/api",
"exists": true,
"registered": true,
"branch": "user/logical-slots",
"head": "0123456789abcdef",
"inconsistencies": []
}
],
"unexpected_entries": [],
"inconsistencies": []
}
]
}{
"workspaces": [
{
"name": "logical-slots",
"path": "/project/src/.workspaces/logical-slots",
"exists": true,
"repositories": [
{
"name": "api",
"path": "/project/src/.workspaces/logical-slots/api",
"exists": true,
"registered": true,
"branch": "user/logical-slots",
"detached": false,
"head": "0123456789abcdef",
"dirty": false,
"upstream": "origin/user/logical-slots",
"ahead": 1,
"behind": 0,
"inconsistencies": []
}
],
"unexpected_entries": [],
"inconsistencies": []
}
]
}{
"workspace": "logical-slots",
"path": "/project/src/.workspaces/logical-slots"
}{
"workspace": "logical-slots",
"path": "/project/src/.workspaces/logical-slots",
"herdr_workspace_id": "w1",
"status": "created",
"tabs": [
{
"label": "1-main",
"path": "/project/src/.workspaces/logical-slots",
"herdr_tab_id": "w1:t1",
"status": "created"
},
{
"label": "2-api",
"path": "/project/src/.workspaces/logical-slots/api",
"herdr_tab_id": "w1:t2",
"status": "created"
}
]
}Workspace and tab status is one of created, reused, or reconciled.
{
"workspace": "logical-slots",
"path": "/project/src/.workspaces/logical-slots",
"repositories": [
{
"name": "api",
"path": "/project/src/.workspaces/logical-slots/api",
"status": "removed",
"message": null
}
],
"workspace_removed": true,
"remaining_entries": []
}Removal status is removed, already_absent, conflict, failed, or
not_run.
Application errors in JSON mode are emitted as JSON to stderr:
{
"error": {
"message": "invalid input: unknown repository \"unknown\"",
"exit_code": 2
}
}Operational conflict reports remain on stdout because they contain the result for every requested repository.
0: successful, including fully idempotent operations;1: an operational conflict or Git, Herdr, or filesystem failure;2: usage, input, or configuration error.
The test suite creates temporary repositories and local bare origins. It does not require network access or the developer's Git identity.
just check # apply formatting and Clippy fixes, then validate the tree
just test # run all testsjust check is intentionally allowed to update tracked files. CI runs the
non-mutating formatting and Clippy checks plus tests on fixed Linux and macOS
runner images. All Cargo commands in automation use the committed lockfile.
The security workflow audits Cargo.lock with RustSec, reviews dependency
changes on pull requests, and rejects workflow actions that are not pinned to a
full commit SHA. Dependabot proposes weekly Cargo and GitHub Actions updates.
See SECURITY.md for private vulnerability reporting.
Copyright (c) 2026 Hirad Pourtahmasbi. Licensed under the MIT License.