Skip to content

Latest commit

 

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

yati

yāti (याति) — Sanskrit for "travels" or "goes forth." In the Rigveda, it describes the journeying of gods between realms. yāti lets you travel between worktrees seamlessly, each one a self-contained world.

What does it do?

It is a git worktree manager with tmux and docker-compose integration. Creates and manages isolated worktrees and environments for feature branches. For when you want to vibe code multiple branches of the same repository simultaneously but maintain proximity to the code.

Dev environment lifecycle management

yati manages the full lifecycle of a worktree: file copying, post_create/post_activate/pre_teardown hooks, tmux session layout, and per-worktree docker-compose isolation via automatic port offsetting ([ports]) and environment variable injection ([environment]).

Inspired by opencode-worktree

Installation

git clone git@github.com:jacobhm98/yati.git
cd yati
cargo install --path .

Usage

Create a worktree

yati create feature-branch

You can optionally specify a session index with --index. This controls port allocation and must not already be in use:

yati create --index 3 feature-branch

Pass --profile <name> to layer a named profile from [profiles.<name>] over the base config (see Profiles):

yati create --profile backend feature-branch

This will:

  1. Create a new git worktree at ~/.yati/<project>/feature-branch
  2. Create a new branch feature-branch (or use an existing one)
  3. Copy any configured files into the worktree
  4. Run any post_create hooks
  5. Open a new tmux session named <project>/feature-branch

Activate a worktree

Switch to an existing worktree:

yati activate project/feature-branch

or, if ran from project root

yati activate feature-branch

This will:

  1. Check that a worktree exists at ~/.yati/<project>/feature-branch
  2. If a tmux session already exists, switch to it
  3. If the session was lost (e.g., after a reboot), run post_create hooks and create a new tmux session
  4. Run post_activate hooks (runs on every activate, whether the session existed or was recreated)

Deactivate a worktree

From inside a yati-managed worktree:

yati deactivate

This leaves the current session without destroying it. If you switched from another tmux session, you'll be returned there. If you attached from a bare terminal, you'll be detached from tmux. The session stays alive for later reactivation with yati activate.

Tear down a worktree

From inside a yati-managed worktree:

yati teardown

This runs pre_teardown hooks, removes the worktree, deletes the branch, and kills the tmux session. If you came from another session you'll be switched back; otherwise you'll be returned to your original terminal.

Use --force to remove even with uncommitted changes:

yati teardown --force

List worktrees

yati list

Shows all yati-managed worktrees across all projects.

Initialize a config

yati init

Writes a starter yati.toml to the repository root, pre-filled with the example configuration (copy rules, hooks, tmux windows, ports, environment, and profiles) ready to edit. It won't overwrite an existing yati.toml.

Docker-Compose Isolation

When you run multiple worktrees of the same project, docker compose up in each one will clash — container names, networks, volumes, and host-port bindings all collide. yati solves this by automatically injecting environment variables into every tmux pane of a worktree session.

What yati does automatically

  1. COMPOSE_PROJECT_NAME is set to <project>-<branch> (sanitized for Docker). This isolates container names, networks, and volumes per worktree — no config needed.

  2. [ports] table — each worktree is assigned an auto-incrementing index (0, 1, 2, ...). For each entry in [ports], the env var is set to base + index * offset. This gives every worktree unique host ports.

  3. [environment] table — arbitrary env vars with {{project}} and {{branch}} template support, expanded per worktree.

yati also sets YATI_PORT_OFFSET to index * offset for use in custom scripts.

Index allocation

Each worktree gets the lowest available index starting at 0. The index is persisted in a .yati_index file inside the worktree directory and is freed when the worktree is torn down.

Using the variables in docker-compose.yml

Reference the env vars in your docker-compose.yml with fallback defaults so it still works outside yati:

services:
  db:
    ports:
      - "${DB_PORT:-5432}:5432"
  web:
    ports:
      - "${WEB_PORT:-3000}:3000"

Example

With this config:

[ports]
offset = 100
DB_PORT = 5432
WEB_PORT = 3000
  • Worktree index 0: DB_PORT=5432, WEB_PORT=3000
  • Worktree index 1: DB_PORT=5532, WEB_PORT=3100
  • Worktree index 2: DB_PORT=5632, WEB_PORT=3200

Shell Completions

yati supports dynamic shell completions for subcommands, flags, worktree targets, and branch names. Run the appropriate setup for your shell once:

Fish

echo 'source (COMPLETE=fish yati | psub)' >> ~/.config/fish/completions/yati.fish

Bash

echo 'source <(COMPLETE=bash yati)' >> ~/.bashrc

Zsh

echo 'source <(COMPLETE=zsh yati)' >> ~/.zshrc

After restarting your shell (or sourcing the file), yati <TAB> will complete subcommands, yati activate <TAB> will complete with existing worktree targets, and yati create <TAB> will complete with git branch names.

Man Page

yati can generate its own man page:

yati --generate man > ~/.local/share/man/man1/yati.1

Then man yati will work. If your system uses a different man path, adjust accordingly (e.g. /usr/local/share/man/man1/).

A tldr page is also included. To make it available to your tldr client:

cp tldr/yati.md ~/.local/share/tldr/pages/common/yati.md

Configuration

Create a yati.toml in your repository root (or run yati init to generate this starter file):

# Files or directories to copy from the main worktree into new worktrees
copy_files = [".env"]

# Patterns to exclude when copying
exclude = ["*.log"]

# Commands to run after creating a worktree.
# Plain strings run synchronously before the tmux session is created.
# Use { command = "...", async = true } to run in a background tmux window.
post_create = [
  "echo setting up",
  { command = "npm install", async = true },
]

# Commands to run every time a worktree is activated (including after creation)
post_activate = ["docker compose up -d"]

# Commands to run before tearing down a worktree
pre_teardown = ["docker compose down"]

# Tmux windows to create in the session.
# The first window replaces the default window; additional entries create new windows.
[tmux]
windows = [
  { name = "editor", command = "nvim" },
  { name = "server", command = "npm run dev" },
  { name = "claude" },
]

# Per-worktree port isolation for docker-compose.
# Each worktree gets an index (0, 1, 2...); port = base + index * offset.
[ports]
offset = 100
DB_PORT = 5432
WEB_PORT = 3000

# Arbitrary env vars injected into the tmux session.
# Supports {{project}} and {{branch}} templates.
[environment]
MY_SERVICE_ID = "{{project}}-{{branch}}"

# Named profiles. Each profile layers over the base config above, overriding
# only the fields it sets. Select one with `yati create --profile <name>`.
[profiles.backend]
windows = [
  { name = "editor", command = "nvim" },
  { name = "api", command = "go run ./cmd/api" },
]

[profiles.frontend]
# Inherits post_create/environment from the base; overrides only windows.
windows = [
  { name = "editor", command = "nvim" },
  { name = "dev", command = "npm run dev" },
]

Profiles

The top-level config ([tmux], hooks, [environment]) is the base, used when you don't pass --profile. Named [profiles.<name>] tables let you define alternative setups for the same repo (e.g. backend vs frontend work).

  • yati create <branch> uses the base config.
  • yati create <branch> --profile <name> starts from the base, then applies [profiles.<name>] on top. A profile only needs to specify what differs from the base.
  • A profile may override windows, post_create, post_activate, pre_teardown, and environment. List fields (windows, the hooks) replace the base; environment merges (profile keys win). [ports] is always global.
  • An unknown profile name fails before any worktree is created.

The selected profile is recorded in the worktree's .yati_index, so yati activate and yati teardown resolve the same profile later (e.g. recreating the right windows if a session was lost).

About

No description, website, or topics provided.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages