Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

16 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

git-forest

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.

Installation

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 install

Both 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 install

Configuration

git-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} in repositories.remote;
  • {workspace} in workspaces.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:

  1. --config <path>;
  2. FOREST_CONFIG;
  3. .forest.toml found 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.

Commands

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

Interactive launcher

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.

setup

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.

repos

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.

fetch

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 operator

update

Fetches 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 update

create and add

create 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/fix

Repositories 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 and status

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.

path

Human output is exactly the absolute workspace path followed by a newline:

workspace=$(git forest path logical-slots)
tmux-sessionizer "$workspace" logical-slots

The workspace must exist.

attach

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-main starts 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-api and 3-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

remove

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.

JSON contract

Paths are absolute. Optional values are represented as null rather than omitted.

Setup

{
  "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

{
  "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"
    }
  ]
}

Fetch

{
  "repositories": [
    {
      "name": "api",
      "path": "/project/src/api",
      "status": "fetched",
      "message": null
    }
  ]
}

Fetch status is fetched or failed.

Update

{
  "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.

Create and add

{
  "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.

List

{
  "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": []
    }
  ]
}

Status

{
  "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": []
    }
  ]
}

Path

{
  "workspace": "logical-slots",
  "path": "/project/src/.workspaces/logical-slots"
}

Attach

{
  "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.

Remove

{
  "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.

Exit status

  • 0: successful, including fully idempotent operations;
  • 1: an operational conflict or Git, Herdr, or filesystem failure;
  • 2: usage, input, or configuration error.

Development

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 tests

just 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.

Security

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.

License

Copyright (c) 2026 Hirad Pourtahmasbi. Licensed under the MIT License.

About

🌲 Manage git worktrees

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages