0.0
The project is in a healthy, maintained state
Rails::Worktrees is a Ruby gem intended to support working with git worktrees in Rails development workflows.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.1, < 8.2
 Project Readme

Rails::Worktrees

rails-worktrees adds Rails-friendly bin/wt and bin/ob commands for creating Git worktrees and opening the right local browser URL for each worktree.

Requirements

  • Ruby >= 3.2.0
  • Rails >= 7.1, < 8.2
  • Git

Installation

bundle add rails-worktrees --group development
bin/rails generate worktrees:install
# or, to also generate bin/ob without the yolo follow-ups:
bin/rails generate worktrees:install --browser
# or, to apply the common Procfile.dev + Puma + mise follow-ups automatically:
bin/rails generate worktrees:install --yolo
# optionally, to let the pi-worktree extension run bin/wt setup in new Pi worktrees:
bin/rails generate worktrees:install --pi

The generated initializer writes app config under Rails.application.config.x.rails_worktrees, so the file stays safe to load even in environments like test where the gem is only bundled for :development. When rails-worktrees is loaded, the gem applies those settings for both the CLI and in-process Rails usage.

The installer adds:

  • bin/wt — a mise-aware bootstrap wrapper that executes the gem-owned CLI
  • bin/ob — an optional browser helper generated by --browser or --yolo
  • config/initializers/rails_worktrees.rb — optional configuration
  • Procfile.dev.worktree.example — a copy-paste helper for ${DEV_PORT:-3000} in Procfile.dev on regular installs
  • a safe update to config/database.yml for common development/test database names
  • .pi/worktree.json — only with --pi; see Pi worktrees

With --yolo, the installer also:

  • generates bin/ob
  • skips Procfile.dev.worktree.example
  • replaces the existing web: entry in Procfile.dev with the DEV_PORT-aware command when Procfile.dev already exists
  • updates config/puma.rb to use port ENV['DEV_PORT'] || ENV.fetch('PORT', 3000) when it still uses a supported default PORT binding
  • updates mise.toml or .mise.toml to load .env from [env] when either file already exists

Usage

bin/wt                          # auto-pick a name from bundled *.txt lists
bin/wt my-feature               # use an explicit worktree name
bin/wt --skip-setup my-feature  # create now, run setup later
bin/wt --dry-run my-feature     # preview the full setup without changing anything
bin/wt --print-env my-feature   # preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
bin/wt setup                    # rerun setup for the current checkout/worktree
bin/wt setup my-feature         # rerun setup for a managed worktree by name
bin/wt setup ../my-project.worktrees/my-feature # setup a specific checkout without cd-ing into it
bin/wt doctor                   # audit install/config drift and basic worktree health
bin/wt update --dry-run         # preview safe maintenance fixes
bin/wt update                   # apply safe maintenance fixes for managed files
bin/wt update --pi              # also add the pi-worktree hook in .pi/worktree.json
bin/wt teardown --dry-run       # preview this worktree's Overmind and development/test databases
bin/wt teardown                 # stop Overmind and drop worktree databases; keep checkout/branch
bin/wt remove my-feature        # teardown a worktree, remove it and delete its local branch
bin/wt delete my-feature        # alias for `bin/wt remove`
bin/wt remove --force my-feature # also delete an unmerged local branch
bin/wt prune                    # remove merged worktrees created by wt
bin/wt help                     # same as --help; never creates a worktree
bin/wt version                  # same as --version

bin/ob                          # open http://localhost:$DEV_PORT/
bin/ob contact                  # open http://localhost:$DEV_PORT/contact
bin/ob '/contact?from=nav'      # open a local route with query params
bin/ob --print-url '?from=nav'  # print the resolved URL without opening a browser

Options

Flag Description
-h, --help, help Show the help message
-v, --version, version Show the script version
--dry-run [name] Preview worktree creation or cleanup without changing anything
--skip-setup Create a worktree without running setup steps
--force Force branch deletion for bin/wt remove / bin/wt delete
--env, --print-env <name> Preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
--pi With bin/wt update, create or merge .pi/worktree.json for the pi-worktree extension

Default behavior

By default bin/wt:

  • creates a sibling directory next to your app: workspace/my-project.worktrees/<name>
  • uses a branch named 🚂/<name>
  • creates new branches from origin's default branch
  • auto-picks names from bundled .txt files when no explicit name is given
  • retires bundled names so they are not picked twice
  • bootstraps a worktree-local .env with deterministic DEV_PORT and WORKTREE_DATABASE_SUFFIX values
  • runs setup automatically after creation: credential linking, bundle install, yarn install when applicable, the Playwright Chromium build for the pinned playwright package when it is missing, both db:prepare steps, test asset precompile, and a final bin/rails assets:clobber
workspace/
├── my-project/
└── my-project.worktrees/
    ├── feature-auth/
    ├── bugfix-123/
    └── experiment/

WT_WORKSPACES_ROOT or config.workspace_root overrides the destination root and uses the layout <root>/<project>/<name>.

Worktrees inside the repository

Set config.worktrees_dir to keep worktrees inside the main checkout instead, in <main checkout>/<worktrees_dir>/<name>. This is the layout the pi-worktree extension uses, so bin/wt and Pi share one place for worktrees:

Rails.application.config.x.rails_worktrees.tap do |config|
  config.worktrees_dir = '.worktrees'
  config.branch_prefix = 'worktree' # wt and pi-worktree both create worktree/<name>
end
my-project/
├── app/
├── config/
└── .worktrees/
    ├── feature-auth/
    └── bugfix-123/
  • worktrees_dir must be a relative path inside the repository, such as .worktrees or tmp/worktrees. It always resolves against the main checkout, also when you run bin/wt from inside a worktree.
  • If .gitignore does not already ignore the directory, bin/wt adds /.worktrees/ to .git/info/exclude when it creates a worktree, so git status in the main checkout stays clean. When it cannot write that file it prints a warning; add the directory to .gitignore yourself.
  • bin/wt remove, bin/wt prune and bin/wt doctor work in this layout too. bin/wt doctor warns when the directory is not ignored.
  • worktrees_dir cannot be combined with workspace_root. WT_WORKSPACES_ROOT still overrides both for a single run.
  • Tools that scan the main checkout's file tree (for example RuboCop, file watchers or Tailwind content globs) can pick up files from the worktrees. Exclude the directory there if needed.

Setup command

bin/wt setup reruns setup for the current checkout. Run it from inside a linked worktree created by bin/wt, a worktree created manually with git worktree, or a checkout prepared by another tool.

If the checkout was created by bin/wt, you can also point at it by managed worktree name from the main app checkout:

bin/wt setup my-feature

If you do not want to cd first, pass an explicit checkout path:

bin/wt setup ../my-project.worktrees/my-feature

This is the recovery path when you want to create first and bootstrap later:

bin/wt --skip-setup my-feature
cd ../my-project.worktrees/my-feature
bin/wt setup

bin/wt setup --dry-run previews the same steps without changing files.

Cleanup commands

bin/wt also supports cleanup commands for worktrees it manages:

  • bin/wt teardown — stop the current worktree's Overmind and drop its development/test databases, keeping the checkout and branch
  • bin/wt remove <name> — run the same teardown, remove the named worktree and delete its local branch
  • bin/wt delete <name> — alias for bin/wt remove <name>
  • bin/wt prune — remove merged worktrees created by bin/wt in bulk

bin/wt remove and bin/wt prune can be run from the main checkout or any sibling worktree in the same repository family.

bin/wt remove refuses to remove the worktree you're currently in, and by default only deletes a local branch after confirming it is already merged into origin's default branch. Use bin/wt remove --force <name> when you intentionally want to delete an unmerged local branch too.

bin/wt prune only targets linked worktrees created by bin/wt whose local branches are already merged into origin's default branch. It skips the main checkout, skips the checkout you're currently in, and asks for confirmation before deleting the batch.

Teardown command

Run bin/wt teardown inside a linked worktree (including manually created or Pi worktrees). It boots that app separately in test and development (so environment-specific files and database URLs are resolved correctly), checks all task-enabled development/test database configurations and Rails' protected-environment metadata, stops Overmind with overmind quit --socket .overmind.sock if that local socket exists, and drops only those validated databases through Rails' database tasks. Production databases and the checkout/branch are left alone. Multi-database configurations (primary, cache, queue, cable, etc.) are supported; replicas and configurations with database_tasks: false are not dropped.

Teardown refuses the main checkout, a missing/blank WORKTREE_DATABASE_SUFFIX, and any database whose name does not contain that suffix. For SQLite, the suffix must be in the filename, not merely a parent directory. All names are validated before Overmind or any database is touched. A working app bundle for both environments and bin/rails are required; install missing gems or fix unsafe database configuration before retrying. Both environments are booted even for a dry-run, so application initializers still execute. Rails' production/environment-mismatch protection cannot be bypassed with DISABLE_DATABASE_ENVIRONMENT_CHECK. The resolved test configuration is passed privately to the development process; all safety checks complete before Overmind is stopped or any database is dropped.

The worktree's .env may define a literal WORKTREE_DATABASE_SUFFIX (unquoted or single/double quoted, optionally with export). Teardown never generates a suffix. When run inside the worktree it can also use the current environment if .env does not define one. remove reads the target's .env and starts Rails with only the caller's OS/Ruby runtime variables, never its suffix, database URLs, application settings or bundle. Any other required application settings/credentials must therefore be loadable by the target app itself (for example with dotenv or Rails credentials). Ensure that target file contains the actual suffix used to create its databases.

bin/wt remove / delete run this same teardown before deleting the worktree or branch. If validation, Overmind shutdown, or a database drop fails, removal aborts and keeps the checkout and branch. Database drops are not transactional: databases already dropped before a later failure cannot be restored by this command. If only the branch remains (the worktree directory is already gone), removal only deletes the branch. prune retains its existing Git-only behavior; run teardown on its candidates first if they have databases.

Use bin/wt teardown --dry-run to list the local Overmind socket and exact development/test database names without stopping or dropping them. To preview both teardown and checkout/branch removal, use --dry-run with bin/wt remove:

bin/wt remove --dry-run feature-auth

If you want to see what bin/wt prune would clean up before saying yes, use --dry-run:

bin/wt prune --dry-run

Maintenance commands

bin/wt also includes a small maintenance surface for installer drift and checkout health:

  • bin/wt doctor — audit managed files plus basic Git/worktree health without changing anything
  • bin/wt update --dry-run — preview safe file-based fixes
  • bin/wt update — apply safe file-based fixes for supported managed files

bin/wt doctor checks the generated initializer, bin/wt, optional bin/ob, supported config files such as config/database.yml, Procfile.dev, config/puma.rb, and mise.toml/.mise.toml, plus basic repository/worktree conditions like resolving origin's default branch and spotting stale registered worktree paths.

bin/wt update is intentionally conservative: it only applies safe file-based fixes for managed templates and supported config shapes. It does not delete branches, remove worktrees, or rewrite ambiguous custom files automatically.

bin/wt doctor and bin/wt update only look at .pi/worktree.json once it already runs bin/wt setup, or when you pass bin/wt update --pi.

Pi worktrees (pi-worktree extension)

If you create worktrees from Pi with pi --worktree or /worktree, rails-worktrees can configure them to run bin/wt setup automatically. This is opt-in:

bin/rails generate worktrees:install --pi
# or, in an app that already has rails-worktrees installed:
bin/wt update --pi

Both commands create or update .pi/worktree.json:

{
  "linkEnvFiles": false,
  "postCreate": [
    "bin/wt setup"
  ]
}

.pi/worktree.json is read by the pi-worktree npm extension (pi install pi-worktree), not by Pi core. Without that extension, the file has no effect. pi-worktree places worktrees in .worktrees/ inside the repository by default. Set config.worktrees_dir = '.worktrees' and config.branch_prefix = 'worktree' to have bin/wt use the same directory and branch names; see Worktrees inside the repository.

linkEnvFiles: false matters. By default, pi-worktree symlinks the main checkout's gitignored .env* files into a new worktree before postCreate runs. bin/wt setup keeps existing .env values, so every Pi worktree would then reuse the main checkout's DEV_PORT and WORKTREE_DATABASE_SUFFIX, and setup would write through the symlink into the main checkout. With linking disabled, each worktree gets its own .env and database identity.

Existing configuration is merged, not replaced:

  • other keys (dir, branchPrefix, preRemove, …) and custom postCreate hooks are preserved; bin/wt setup is appended once and never duplicated
  • linkEnvFiles is only added when it is absent; an explicit "linkEnvFiles": true, invalid JSON, or a non-list postCreate is left untouched with a message explaining what to change by hand
  • no preRemove hook is added automatically, and the worktree directory and branch prefix stay under your control

To opt into environment cleanup when Pi's pi-worktree extension destroys a worktree, add bin/wt teardown to preRemove (preserve any existing hooks):

{
  "linkEnvFiles": false,
  "postCreate": ["bin/wt setup"],
  "preRemove": ["bin/wt teardown"]
}

The extension runs preRemove from inside the worktree. Teardown only cleans the environment; the calling tool remains responsible for removing the checkout and branch. Keep linkEnvFiles: false so a worktree does not share the main checkout's .env. Preview the cleanup inside the worktree with bin/wt teardown --dry-run first.

pi-worktree runs each postCreate step with a 120-second timeout. If a first bundle install or db:prepare takes longer, run bin/wt setup again inside the worktree.

Updating old bin/wt wrappers

Versions before 0.7.1 generated bin/wt as a Ruby shebang wrapper. On hosts where Ruby is provided by mise but not already on PATH, that legacy wrapper can fail with /usr/bin/env: ruby: No such file or directory before rails-worktrees can activate mise.

Use the current gem executable once to repair the app-owned wrapper. Scope mise discovery to the checkout and treat in-repository mise configs as trusted for this recovery shell, so mise exec loads Ruby from the same configuration the repaired wrapper will use:

# From the Rails app root
root=$(pwd -P)
trust_root=$(
  unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE \
    GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
  git -C "$root" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$root"
)
trust_root=$(cd "$trust_root" && pwd -P)
MISE_CEILING_PATHS=$(dirname "$trust_root")
MISE_TRUSTED_CONFIG_PATHS="$trust_root"
export MISE_CEILING_PATHS MISE_TRUSTED_CONFIG_PATHS

mise exec -C "$root" -- ruby -v
mise exec -C "$root" -- gem install rails-worktrees --no-document
mise exec -C "$root" -- ruby -e 'load Gem.bin_path("rails-worktrees", "wt")' -- update --dry-run "$root"
mise exec -C "$root" -- ruby -e 'load Gem.bin_path("rails-worktrees", "wt")' -- update "$root"
git status --short
git diff --stat

Review the diff before committing. wt update applies every safe managed maintenance fix it finds, so the resulting commit can include bin/wt plus other generated installer drift such as the initializer, database configuration, Procfile, Puma config, mise config, or bin/ob.

New installs and repaired apps use a bash bootstrap wrapper that activates mise from the app root before loading Ruby/Bundler, trusting mise configs only inside the repository root and bounding discovery above the checkout. That covers app-local, in-repository higher-scope, local override, and environment-specific mise configuration without trusting an unrelated caller directory or configs above the checkout. The wrapper also falls back to the globally installed wt executable for bootstrap-safe commands such as wt setup --dry-run when the app bundle is incomplete.

Interactive prompts

bin/wt handles several edge cases interactively:

  • Branch already exists locally — asks whether to attach a new worktree to it
  • Branch already exists on origin — asks whether to create a local tracking worktree
  • Target directory already exists with matching branch — asks whether to reuse it
  • Target directory already exists with a different branch — asks whether to remove and recreate it
  • Prune found merged worktrees created by wt — asks whether to delete the batch
  • Retired bundled name used explicitly — rejects it and suggests running wt with no argument

Name validation

Worktree names must not contain / or whitespace, must not be . or .., and must be a valid Git ref component.

These reserved words are commands, so bin/wt <word> never creates or reuses a worktree with that name and auto-picking skips them: help, version, doctor, setup, update, prune, remove and delete. bin/wt help and bin/wt version behave like --help and --version. An existing worktree with such a name (for example one created by bin/wt help in an older version) can still be cleaned up with bin/wt remove help or set up with bin/wt setup ../<project>.worktrees/help.

Configuration

The installer generates config/initializers/rails_worktrees.rb where you can override:

The initializer stays app-owned and gem-agnostic; rails-worktrees reads those settings whenever the gem is present in the current bundle groups.

Option Default Description
bootstrap_env true Write .env when creating a worktree
workspace_root nil Override the destination root (sibling layout when nil)
worktrees_dir nil Create worktrees inside the main checkout, e.g. '.worktrees'; see Worktrees inside the repository
dev_port_range 3000..3999 Port range for deterministic DEV_PORT allocation
branch_prefix 🚂 Prefix for worktree branch names
name_sources_path bundled names/ Directory containing .txt name lists
used_names_file ~/.local/state/rails-worktrees/used-names.tsv TSV tracking retired names
worktree_database_suffix_max_length 18 Max length for generated database suffixes

Database naming

The installer adds WORKTREE_DATABASE_SUFFIX to common development and test database names in config/database.yml. For a multi-database app, the target shape is:

development:
  primary:
    database: my_app_development<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>_primary

test:
  primary:
    database: my_app_test<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>_primary

If your database.yml is too custom to patch safely, the installer leaves it alone and tells you what to update manually.

Environment bootstrap

When bin/wt creates a worktree it writes a worktree-local .env with:

  • DEV_PORT — deterministic port derived from the worktree name via CRC32, rotated through dev_port_range, skipping ports already claimed by peer worktrees
  • WORKTREE_DATABASE_SUFFIX — derived from the best available worktree identity (managed name when known, otherwise the current branch or checkout basename). When a readable suffix is already claimed by a peer checkout, bin/wt appends a short DEV_PORT-based token to keep the databases isolated.

Existing .env values are never overwritten.

Browser helper

If you install with --browser or --yolo, the installer generates bin/ob.

bin/ob reads DEV_PORT from the current app or worktree's .env, falls back to ENV['DEV_PORT'], then falls back to 3000, and opens http://localhost:$DEV_PORT/... in your default browser.

Route examples:

bin/ob
bin/ob contact
bin/ob admin/users
bin/ob '/contact?from=footer'
bin/ob --print-url '?from=footer'

bin/ob accepts a single optional local route argument. Query-only routes such as ?from=footer resolve to the app root, and full URLs such as https://example.com are intentionally rejected.

If DEV_PORT is missing from .env, bin/ob tells you when it falls back to ENV['DEV_PORT'] or 3000.

Browser opening currently uses:

  • open on macOS
  • xdg-open on Linux and other Unix-like environments that provide it

For scripts and debugging, bin/ob --print-url [route] prints the resolved localhost URL without opening a browser.

By default, the installer does not edit your Procfile.dev, config/puma.rb, or mise config. It generates Procfile.dev.worktree.example with a ready-to-copy line:

web: env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0 -p ${DEV_PORT:-3000}

If you run bin/rails generate worktrees:install --yolo, the installer applies the three common follow-ups for you when the files already exist:

  • skip generating Procfile.dev.worktree.example
  • replace the existing web: entry in Procfile.dev
  • update config/puma.rb to port ENV['DEV_PORT'] || ENV.fetch('PORT', 3000) when it still uses a supported default PORT binding
  • add _.file = ".env" to the [env] section of mise.toml or .mise.toml

On a regular install, the follow-up message also suggests the same config/puma.rb edit when Puma still uses the default PORT binding.

Use a project-local env loader like mise with _.file = ".env" to keep values scoped per-worktree.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Smoke testing

RSpec remains the main automated test suite. For installer and integration changes, you can also run a disposable Rails app smoke test:

bundle exec rake smoke_test

This smoke test:

  • creates a temporary Rails app from a compatible Rails version
  • installs rails-worktrees from the current checkout path
  • runs bin/rails generate worktrees:install --yolo
  • verifies bin/wt, bin/ob, the generated initializer, that --yolo skips the Procfile example, yolo updates to Procfile.dev, config/puma.rb, and mise.toml, config/database.yml patching, and worktree .env bootstrapping
  • creates a temporary bare origin, confirms bin/wt smoke-branch creates a real worktree, and confirms bin/wt remove smoke-branch can remove that merged worktree from a sibling worktree checkout

By default, the script cleans up all temp directories after the run. Set KEEP_SMOKE_TEST_ARTIFACTS=1 to keep them around for debugging, or set RAILS_WORKTREES_SMOKE_RAILS_VERSION to try a different compatible Rails version.

There is also a manually triggered GitHub Actions workflow named Smoke Test for running the same disposable-app verification in CI without slowing down the default pull request checks.

The workflow accepts optional ruby_version, rails_version, keep_artifacts, and verbose inputs. Enable keep_artifacts when you want the disposable app, bare origin, and worktree directories uploaded from CI for debugging, and enable verbose when you want shell tracing in the smoke-test log.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/asjer/rails-worktrees.

License

The gem is available as open source under the terms of the MIT License.