Skip to content

Navigation Menu

Sign in
Sign up

Latest commit

History

406 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

🌳 Grove

GitHub release License

A fast, intuitive CLI that makes Git worktrees as simple as switching branches.

Without Grove:

git clone --bare git@github.com:org/repo.git .bare
echo "gitdir: ./.bare" > .git
git worktree add main
cd main
cp ../other-worktree/.env . # Don't forget this
npm install # Or this
# Later: cd ../feat-auth to switch

With Grove:

grove clone git@github.com:org/repo
grove add feat/auth --switch # .env copied, hooks run automatically
grove switch main # Just like git checkout, but each branch keeps its own directory
demo.mp4

Note

Grove is under active development. Core functionality is stable, but APIs may change between major versions.


New to worktrees?

Git worktrees let you work on multiple branches simultaneously in separate directories. No stashing. No "wrong branch" mistakes. Your work stays exactly where you left it.

The catch: git worktree is clunky. Grove makes it feel like git checkout β€” but each branch gets its own persistent directory.

πŸ“‹ Requirements

  • Git 2.48+ β€” Grove uses --relative-paths for portable worktrees

πŸ“¦ Installation

Quick install (Linux/macOS)

curl -fsSL https://raw.githubusercontent.com/sQVe/grove/main/install.sh | sh

Go install

go install github.com/sqve/grove/cmd/grove@latest

From source

git clone https://github.com/sQVe/grove && cd grove
go build -o grove ./cmd/grove
sudo mv grove /usr/local/bin/

Package managers

Download .deb or .rpm packages from GitHub Releases.

Optional: GitHub CLI

Grove works without additional dependencies, but installing the GitHub CLI (gh) enables enhanced features:

  • PR worktrees: Create worktrees from pull requests with grove add --pr 123 or grove clone https://github.com/owner/repo/pull/123
  • Squash-merge detection: grove prune accurately detects branches merged via GitHub's squash-and-merge, even with multiple commits. Without gh, only single-commit squash merges are detected via git.

See GitHub CLI installation for setup instructions.

πŸ”§ Setup

Shell Integration (Required)

Without this, grove switch only prints a path β€” you'd have to cd manually. Add to your shell config:

# bash (~/.bashrc) or zsh (~/.zshrc)
eval "$(grove switch shell-init)"
# fish (~/.config/fish/config.fish)
grove switch shell-init | source
# powershell ($PROFILE)
grove switch shell-init | Invoke-Expression

Shell Completion

Tab completion for commands, flags, and worktree names.

# bash
eval "$(grove completion bash)"
# zsh
eval "$(grove completion zsh)"
# fish
grove completion fish | source
# powershell
grove completion powershell | Invoke-Expression

πŸš€ Quick Start

# Clone a repository
grove clone https://github.com/owner/repo
cd repo
# Start a feature
grove add feat/auth --switch
# Check your worktrees
grove list
# Switch back to main
grove switch main
# Clean up when done
grove remove feat/auth --branch

πŸ“— Commands

grove clone <url> [directory]

Clone a repository into a Grove workspace.

Flags:

  • --branches <list> β€” Comma-separated branches to create worktrees for
  • --shallow β€” Shallow clone (depth=1)
  • -v, --verbose β€” Show git output

Examples:

grove clone https://github.com/owner/repo
grove clone https://github.com/owner/repo my-project
grove clone https://github.com/owner/repo --branches main,develop
grove clone https://github.com/owner/repo/pull/123 # Clone and checkout PR
grove init new [directory] / grove init convert

Initialize a Grove workspace.

Subcommands:

  • new [directory] β€” Create new workspace
  • convert β€” Convert existing repository

Flags (convert):

  • --branches <list> β€” Additional branches to create worktrees for
  • -v, --verbose β€” Show git output

Examples:

grove init new my-project
grove init convert
grove init convert --branches develop,staging
grove add [branch|PR-URL|ref]

Add a worktree for a branch, pull request, or ref.

Flags:

  • -s, --switch β€” Switch to the worktree; prints the path of an existing one instead of erroring
  • --base <branch> β€” Create new branch from this base instead of the default branch
  • --no-fetch β€” Skip fetching the base branch or the existing branch's upstream
  • --name <name> β€” Custom directory name
  • -d, --detach β€” Detached HEAD state
  • --pr <number> β€” Create worktree for a pull request
  • --from <worktree> β€” Source worktree for file preservation (name or branch)
  • --reset β€” Reset diverged PR branch to match remote (use with --pr)

Examples:

grove add feat/auth
grove add feat/auth --switch
grove add --base main feat/auth
grove add --pr 123 # PR by number
grove add --pr 123 --reset # PR, discarding local commits
grove add --detach v1.0.0 # Tag in detached HEAD
grove add --from dev feat/auth # Copy .env from dev worktree

New branches start from the default branch on origin, fetched with a five-second timeout. If fetching fails, Grove warns with the base ref and commit age, then uses the existing origin ref, local default branch, or bare HEAD. Existing worktrees stay unchanged. An explicit --base <branch> fetches the branch from origin before resolving it, even when its remote-tracking ref is missing. It prefers origin/<branch>, falls back to the local branch, and errors if neither exists. --base origin/<branch> requires the origin ref. --base HEAD selects the bare repository's HEAD without fetching. Use --no-fetch to skip fetching once, or grove config set --global grove.fetchBase false to disable it by default. Both leave the refs on disk untouched and pick from them in the same order.

Adding an existing local branch fetches its upstream (origin/<branch> when none is configured) and fast-forwards the branch first when it is strictly behind; a branch that is ahead or diverged is checked out as is. --no-fetch skips that fetch too. Re-running grove add --pr N on a same-repo PR that already has a worktree refreshes it: a clean worktree is fast-forwarded, a branch with local commits errors unless --reset discards them, and a worktree with uncommitted tracked changes refuses to refresh. Fork PRs still error. With -s, grove add prints the path of an existing worktree for a branch instead of erroring.

grove switch <worktree>

Switch to a worktree by directory or branch name.

Requires shell integration (see Setup section).

Examples:

grove switch main
grove switch feat-auth
grove switch feat/auth
grove list

List all worktrees with status.

Flags:

  • --fast β€” Skip dirty and sync status checks
  • --filter <status> β€” Filter by dirty, ahead, behind, gone, locked, or pr. Comma-separated filters use OR. locked and pr work with --fast.
  • --json β€” JSON output. Includes pr when a PR number is recorded and last_commit when commit times are known (omitted with --fast).
  • --sort name|recent β€” Sort by name or latest commit (recent needs commit times, so it cannot be combined with --fast)
  • -v, --verbose β€” Show paths, upstreams, and a PR column (#42) when any worktree has a recorded PR number, blank on the rows without one. PR numbers come from local config; list stays offline.

Examples:

grove list
grove list --fast
grove list --filter dirty
grove list --filter ahead,behind
grove list --filter pr
grove list --json
grove list --sort recent
grove fetch

Fetch all remotes and show branch changes.

Flags:

  • --json β€” JSON output
  • -v, --verbose β€” Show commit hash details

Examples:

grove fetch
grove fetch --verbose
grove fetch --json
grove status

Show current worktree status.

Flags:

  • -v, --verbose β€” Show all diagnostic sections
  • --json β€” JSON output

Examples:

grove status
grove status --verbose
grove remove <worktree>...

Remove one or more worktrees. With --branch, unmerged branches are rejected before removal unless --force is set.

Flags:

  • -f, --force β€” Remove even if dirty or locked; with --branch, delete unmerged and unpushed commits
  • --branch β€” Also delete the branch
  • --ignore-missing β€” Skip unknown targets with a warning

Examples:

grove remove feat-auth
grove remove feat-auth --branch
grove remove --force wip
grove remove feat-auth bugfix-123 # Remove multiple
grove remove --ignore-missing feat-auth bugfix-123 # Skip missing worktrees
grove move <worktree> <new-branch>

Rename a branch and its worktree.

Examples:

grove move feat/old feat/new
grove lock <worktree>...

Lock one or more worktrees to prevent removal.

Flags:

  • --reason <text> β€” Reason for locking

Examples:

grove lock main
grove lock release --reason "Production release"
grove lock feat-auth bugfix-123 # Lock multiple
grove unlock <worktree>...

Unlock one or more worktrees.

Examples:

grove unlock feat-auth
grove unlock feat-auth bugfix-123 # Unlock multiple
grove prune

Remove worktrees with deleted upstream branches. Dry-run by default.

When removing worktrees whose upstream was deleted on remote, local branches are also deleted.

Flags:

  • --commit β€” Actually remove (default is dry-run)
  • -f, --force β€” Remove even if dirty, locked, or unpushed
  • --stale <duration> β€” Include inactive worktrees (e.g., 30d, 2w)
  • --merged β€” Include branches merged into default branch
  • --detached β€” Include detached worktrees

Examples:

grove prune # Dry-run
grove prune --commit # Actually remove
grove prune --stale 30d --commit
grove prune --merged --commit
grove prune --detached --commit
grove exec [worktrees...] -- <command>

Execute a command in worktrees.

Flags:

  • -a, --all β€” Execute in all worktrees
  • -j, --parallel N β€” Run at most N commands at once (default: 1)
  • --fail-fast β€” Stop starting commands after the first failure; running commands finish
  • --json β€” Write results as JSON to stdout and child output to stderr

With N > 1, each worktree's header, stdout, and stderr appear together on stderr as each command finishes. Output is buffered in memory without a size cap. Parallel commands receive no stdin. --json always lists worktrees in target order, though with --fail-fast it can list more of them than a sequential run, since commands already running when the first failure lands still finish and still report. With N = 1 or no flag, output streams as before and commands inherit stdin.

Examples:

grove exec --all -- npm install
grove exec --all -j 4 -- npm test
grove exec main feat-auth -- git pull
grove exec --all --fail-fast -- go build
grove exec --all -- bash -c "npm install && npm test"
grove config <subcommand>

Manage configuration.

Subcommands:

  • list β€” Show all settings
  • get <key> β€” Get value
  • set <key> <value> β€” Set value (requires --shared or --global)
  • unset <key> β€” Remove setting (requires --shared or --global)
  • init β€” Create .grove.toml template

Flags:

  • --shared β€” Target .grove.toml
  • --global β€” Target git config

Examples:

grove config list
grove config get preserve.patterns
grove config set --global grove.plain true
grove config init

Edit .grove.toml to set array values:

[preserve]
patterns = [".env", ".env.local"]
[link]
patterns = [".next", ".turbo"]
[autolock]
patterns = ["main", "release/*"]
grove doctor

Diagnose workspace issues.

Flags:

  • --fix β€” Auto-fix safe issues
  • --json β€” JSON output
  • --perf β€” Disk space analysis

Detects:

  • Dependency versions (Git 2.48+, optional gh CLI)
  • Broken .git pointers
  • Stale worktree entries
  • Unreachable remotes
  • Invalid .grove.toml syntax
  • Hook commands not found in PATH
  • Stale lock files

Examples:

grove doctor
grove doctor --fix
grove doctor --perf

βš™ Configuration

Grove uses a two-layer configuration system:

  • .grove.toml β€” Team settings (checked into repository)
  • Git config (~/.gitconfig) β€” Personal settings

Run grove config init to create a .grove.toml template.

Default configuration
# Grove - Git worktree management
# https://github.com/sqve/grove
# Use Nerd Font icons in output (when not in plain mode).
nerd_fonts = true
# Threshold for marking worktrees as stale (no commits within this period).
# Format: number followed by d (days), w (weeks), or m (months).
stale_threshold = "30d"
# Disable colors and symbols in output.
plain = false
# Enable debug logging.
debug = false
# Fetch the base branch from origin before creating a new branch.
fetch_base = true
[preserve]
# Files to copy from the current worktree when creating a new one.
# Useful for environment files and local configuration that shouldn't be in git.
# Supports glob patterns. These patterns override git config grove.preserve.
patterns = [
 ".env",
 ".env.keys",
 ".env.local",
 ".env.*.local",
 ".envrc",
 ".grove.toml",
 "docker-compose.override.yml",
]
# Path segments to exclude from preservation.
# Files containing any of these path segments will be skipped.
exclude = [
 # Caches
 ".cache",
 "__pycache__",
 # Build output
 "build",
 "coverage",
 "dist",
 "out",
 "target",
 # Dependencies
 ".venv",
 "node_modules",
 "vendor",
 "venv",
]
# Directories to recursively copy when creating a new worktree.
# Unlike preserve.patterns, this copies the full directory tree.
directories = []
[link]
# Directories to symlink from the source worktree when creating a new one.
# Useful for sharing tool state (e.g., .claude) across worktrees.
# Patterns without / match directory names in the worktree root.
# Patterns with / match paths relative to the worktree root.
# Creates relative symlinks so the workspace remains portable.
patterns = []
[hooks]
# Shell commands to run after creating a worktree.
# Runs sequentially, stops on first failure.
# Examples: ["npm install"], ["go mod download", "make setup"]
add = []
[autolock]
# Branch patterns to automatically lock when creating worktrees.
# Locked worktrees are protected from accidental deletion.
# Supports exact matches and trailing /* wildcards (e.g., "release/*").
patterns = ["develop", "main", "master"]

Recipes

Git hooks managers

Husky and lefthook set core.hooksPath to a relative path (.husky or .lefthook). In a bare worktree setup, this config is shared across all worktrees via .bare/config. The relative path resolves correctly from any worktree because the directory exists in every checkout. Re-run the installer after worktree creation to ensure the path is set:

Husky
[hooks]
add = ["npm install", "npx husky"]
lefthook
[hooks]
add = ["lefthook install"]

pre-commit writes hook scripts directly into the git hooks directory (.bare/hooks/), which is shared by all worktrees. Run pre-commit install once from any worktree β€” no Grove hook needed.

Next.js shared build caches

Next.js build output (.next) and Turborepo cache (.turbo) can be shared across worktrees to avoid redundant rebuilds. Avoid symlinking node_modules/ unless its contents are portable and dependency versions match across worktrees. It can contain absolute paths and branch-specific dependency versions.

[link]
patterns = [".next", ".turbo"]

For a monorepo that meets those conditions, use a path pattern to link each app's dependencies:

[link]
patterns = ["apps/*/node_modules"]

Rust shared target directory

Rust target/ directories consume significant disk space. Sharing via symlink avoids duplicate compilation across worktrees.

[link]
patterns = ["target"]

Parallel cargo build across worktrees sharing the same target directory will serialize on file locks.

AI coding tools

AI coding tools store local configuration in git-ignored files that are lost in new worktrees. Preserve them to carry project context and settings across worktrees.

[preserve]
patterns = [
 "CLAUDE.local.md",
 ".claude/settings.local.json",
 ".aider.conf.yml",
 ".copilot/config.json",
]

These patterns replace the default preserve patterns.

🀝 Contributing

Contributions welcome! See CONTRIBUTING.md for development setup and guidelines.

About

A fast, intuitive CLI that makes Git worktrees as simple as switching branches.

Resources

Contributing

Security policy

Stars

51 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /