rails-worktrees 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fc7ddb67fbc5fde18c04ab164229207c6f54284dc4bb5c1fb955f40544706cbb
4
+ data.tar.gz: a4005286b6c1dbad990d94ca3999b1490f2532c981fa8e5a7fb9b5768e4f30d6
5
+ SHA512:
6
+ metadata.gz: 8eef6c43ad39e5fc38c3dbdc10fc2618fc07f0ccd65c5c25bef94ae3991aeff143687ee45a37983846cc7ec8fc4c031d39b2b4cd6abcdd37d31243bb705a24a0
7
+ data.tar.gz: dfdeeb6c73a81e8c3c8147883b5ca752b79ff4a63e318db409792c6723c4fd84ae3f9effc93edc1d456f74fa22f326e954ac7239d044a27709436d1e462810b6
@@ -0,0 +1 @@
1
+ {".":"0.1.0"}
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-03-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * add wt command and rails installer ([5d798d5](https://github.com/asjer/rails-worktrees/commit/5d798d5129585331780f0259b39061194feb66e3))
9
+
10
+ ## [Unreleased]
11
+
12
+ - Add a gem-managed `wt` CLI for creating Rails worktrees.
13
+ - Add a Rails installer generator that creates `bin/wt` and `config/initializers/rails_worktrees.rb`.
14
+ - Add conservative `config/database.yml` patching for common development/test database names.
15
+ - Add a manual-dispatch GitHub Actions workflow for the disposable Rails smoke test.
16
+ - Add smoke-test workflow debug controls for retained artifacts and verbose output.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Asjer Querido
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # Rails::Worktrees
2
+
3
+ `rails-worktrees` adds a Rails-friendly `bin/wt` command for creating Git worktrees with isolated development and test databases.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby >= 3.2.0
8
+ - Rails >= 7.1, < 8.2
9
+ - Git
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ bundle add rails-worktrees
15
+ bin/rails generate rails:worktrees:install
16
+ ```
17
+
18
+ The installer adds:
19
+
20
+ - `bin/wt` — a thin wrapper that executes the gem-owned CLI
21
+ - `config/initializers/rails_worktrees.rb` — optional configuration
22
+ - `Procfile.dev.worktree.example` — a copy-paste helper for `${DEV_PORT:-3000}` in `Procfile.dev`
23
+ - a safe update to `config/database.yml` for common development/test database names
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ bin/wt # auto-pick a name from bundled *.txt lists
29
+ bin/wt my-feature # use an explicit worktree name
30
+ bin/wt --dry-run my-feature # preview the full setup without changing anything
31
+ bin/wt --print-env my-feature # preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
32
+ ```
33
+
34
+ ### Options
35
+
36
+ | Flag | Description |
37
+ |------|-------------|
38
+ | `-h`, `--help` | Show the help message |
39
+ | `-v`, `--version` | Show the script version |
40
+ | `--dry-run [name]` | Preview the full worktree setup without changing anything |
41
+ | `--env`, `--print-env <name>` | Preview `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX` |
42
+
43
+ ### Default behavior
44
+
45
+ By default `bin/wt`:
46
+
47
+ - creates a sibling directory next to your app: `workspace/my-project.worktrees/<name>`
48
+ - uses a branch named `🚂/<name>`
49
+ - creates new branches from `origin`'s default branch
50
+ - auto-picks names from bundled `.txt` files when no explicit name is given
51
+ - retires bundled names so they are not picked twice
52
+ - bootstraps a worktree-local `.env` with deterministic `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX` values
53
+
54
+ ```text
55
+ workspace/
56
+ ├── my-project/
57
+ └── my-project.worktrees/
58
+ ├── feature-auth/
59
+ ├── bugfix-123/
60
+ └── experiment/
61
+ ```
62
+
63
+ `WT_WORKSPACES_ROOT` or `config.workspace_root` overrides the destination root and uses the layout `<root>/<project>/<name>`.
64
+
65
+ ### Interactive prompts
66
+
67
+ `bin/wt` handles several edge cases interactively:
68
+
69
+ - **Branch already exists locally** — asks whether to attach a new worktree to it
70
+ - **Branch already exists on origin** — asks whether to create a local tracking worktree
71
+ - **Target directory already exists with matching branch** — asks whether to reuse it
72
+ - **Target directory already exists with a different branch** — asks whether to remove and recreate it
73
+ - **Retired bundled name used explicitly** — rejects it and suggests running `wt` with no argument
74
+
75
+ ### Name validation
76
+
77
+ Worktree names must not contain `/` or whitespace, must not be `.` or `..`, and must be a valid Git ref component.
78
+
79
+ ### Configuration
80
+
81
+ The installer generates `config/initializers/rails_worktrees.rb` where you can override:
82
+
83
+ | Option | Default | Description |
84
+ |--------|---------|-------------|
85
+ | `bootstrap_env` | `true` | Write `.env` when creating a worktree |
86
+ | `workspace_root` | `nil` | Override the destination root (sibling layout when `nil`) |
87
+ | `dev_port_range` | `3000..3999` | Port range for deterministic `DEV_PORT` allocation |
88
+ | `branch_prefix` | `🚂` | Prefix for worktree branch names |
89
+ | `name_sources_path` | bundled `names/` | Directory containing `.txt` name lists |
90
+ | `used_names_file` | `~/.local/state/rails-worktrees/used-names.tsv` | TSV tracking retired names |
91
+ | `worktree_database_suffix_max_length` | `18` | Max length for generated database suffixes |
92
+
93
+ ### Database naming
94
+
95
+ 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:
96
+
97
+ ```yaml
98
+ development:
99
+ primary:
100
+ database: my_app_development<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>_primary
101
+
102
+ test:
103
+ primary:
104
+ database: my_app_test<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>_primary
105
+ ```
106
+
107
+ If your `database.yml` is too custom to patch safely, the installer leaves it alone and tells you what to update manually.
108
+
109
+ ### Environment bootstrap
110
+
111
+ When `bin/wt` creates a worktree it writes a worktree-local `.env` with:
112
+
113
+ - `DEV_PORT` — deterministic port derived from the worktree name via CRC32, rotated through `dev_port_range`, skipping ports already claimed by peer worktrees
114
+ - `WORKTREE_DATABASE_SUFFIX` — derived from the worktree name so the `database.yml` ERB works immediately
115
+
116
+ Existing `.env` values are never overwritten.
117
+
118
+ The gem does **not** edit your `Procfile.dev` or add dotenv. The installer generates `Procfile.dev.worktree.example` with a ready-to-copy line:
119
+
120
+ ```text
121
+ web: env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0 -p ${DEV_PORT:-3000}
122
+ ```
123
+
124
+ Use a project-local env loader like `mise` with `_.file = ".env"` to keep values scoped per-worktree.
125
+
126
+ ## Development
127
+
128
+ 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.
129
+
130
+ 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](https://rubygems.org).
131
+
132
+ ### Smoke testing
133
+
134
+ RSpec remains the main automated test suite. For installer and integration changes, you can also run a disposable Rails app smoke test:
135
+
136
+ ```bash
137
+ bundle exec rake smoke_test
138
+ ```
139
+
140
+ This smoke test:
141
+
142
+ - creates a temporary Rails app from a compatible Rails version
143
+ - installs `rails-worktrees` from the current checkout path
144
+ - runs `bin/rails generate rails:worktrees:install`
145
+ - verifies `bin/wt`, the generated initializer, the Procfile example, `config/database.yml` patching, and worktree `.env` bootstrapping
146
+ - creates a temporary bare `origin` and confirms `bin/wt smoke-branch` creates a real worktree
147
+
148
+ 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.
149
+
150
+ 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.
151
+
152
+ 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.
153
+
154
+ ## Contributing
155
+
156
+ Bug reports and pull requests are welcome on GitHub at https://github.com/asjer/rails-worktrees.
157
+
158
+ ## License
159
+
160
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ namespace :smoke do
13
+ desc 'Run the disposable Rails app smoke test'
14
+ task :test do
15
+ script = File.expand_path('spec/integration/smoke_test.sh', __dir__)
16
+
17
+ Bundler.with_unbundled_env do
18
+ success = system(script)
19
+ abort('Smoke test failed') unless success
20
+ end
21
+ end
22
+ end
23
+
24
+ desc 'Run the disposable Rails app smoke test'
25
+ task smoke_test: 'smoke:test'
26
+
27
+ task default: %i[spec rubocop]
data/exe/wt ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path('../lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require 'rails/worktrees'
8
+
9
+ exit(Rails::Worktrees::CLI.new.start)
data/lefthook.yml ADDED
@@ -0,0 +1,5 @@
1
+ remotes:
2
+ - git_url: https://github.com/asjer/lefthook-configs
3
+ refetch_frequency: 24h
4
+ configs:
5
+ lefthook-gem.yml
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'rails/generators'
5
+ require_relative 'mise_follow_up'
6
+ require_relative '../../../rails/worktrees/database_config_updater'
7
+
8
+ module Rails
9
+ module Worktrees
10
+ module Generators
11
+ # Installs the wt wrapper, configuration, and safe database.yml updates.
12
+ class InstallGenerator < ::Rails::Generators::Base
13
+ include MiseFollowUp
14
+
15
+ namespace 'rails:worktrees:install'
16
+ desc 'Installs bin/wt, a Rails::Worktrees initializer, and updates config/database.yml when safe.'
17
+ source_root File.expand_path('templates', __dir__)
18
+ class_option :conductor, type: :boolean, default: false,
19
+ desc: 'Configure the installer for ~/Sites/conductor/workspaces'
20
+
21
+ FOLLOW_UP_TEMPLATE = <<~TEXT
22
+ ============================================
23
+ rails-worktrees installed successfully! 🚂
24
+
25
+ Installed:
26
+ %<installed>s
27
+
28
+ Get started:
29
+ $ bin/wt
30
+ $ bin/wt my-feature
31
+
32
+ Configure:
33
+ config/initializers/rails_worktrees.rb
34
+ %<notes>s
35
+ ============================================
36
+ TEXT
37
+
38
+ def create_bin_wrapper
39
+ template('bin/wt', 'bin/wt')
40
+ chmod('bin/wt', 0o755)
41
+ end
42
+
43
+ def create_initializer
44
+ template('rails_worktrees.rb.tt', 'config/initializers/rails_worktrees.rb')
45
+ end
46
+
47
+ def create_procfile_worktree_example
48
+ template('Procfile.dev.worktree.example.tt', 'Procfile.dev.worktree.example')
49
+ end
50
+
51
+ def update_database_configuration
52
+ unless File.exist?(database_config_path)
53
+ say_status(:skip, 'config/database.yml not found', :yellow)
54
+ @database_outcome = :not_found
55
+ return
56
+ end
57
+
58
+ result = database_update_result
59
+ @database_outcome = result.changed? ? :updated : :identical
60
+ announce_database_update(result)
61
+ end
62
+
63
+ def verify_installation
64
+ if git_repo?
65
+ say_status(:ok, 'git repository detected', :green)
66
+ else
67
+ say_status(:warning, 'run inside a git repository for bin/wt to work', :yellow)
68
+ end
69
+ end
70
+
71
+ def show_follow_up
72
+ say(follow_up_message)
73
+ end
74
+
75
+ private
76
+
77
+ def database_config_path
78
+ File.join(destination_root, 'config/database.yml')
79
+ end
80
+
81
+ def database_update_result
82
+ result = ::Rails::Worktrees::DatabaseConfigUpdater.new(
83
+ content: File.read(database_config_path)
84
+ ).call
85
+
86
+ File.write(database_config_path, result.content) if result.changed?
87
+
88
+ result
89
+ end
90
+
91
+ def follow_up_message
92
+ "\n#{format(FOLLOW_UP_TEMPLATE, installed: installed_items_text, notes: follow_up_notes_text)}"
93
+ end
94
+
95
+ def installed_items_text
96
+ items = [
97
+ ' • bin/wt',
98
+ ' • config/initializers/rails_worktrees.rb',
99
+ ' • Procfile.dev.worktree.example'
100
+ ]
101
+ items << database_follow_up_line if database_follow_up_line
102
+ items.join("\n")
103
+ end
104
+
105
+ def database_follow_up_line
106
+ case @database_outcome
107
+ when :updated
108
+ ' • config/database.yml (updated with WORKTREE_DATABASE_SUFFIX)'
109
+ when :not_found
110
+ ' • config/database.yml was not found — see README for manual setup'
111
+ end
112
+ end
113
+
114
+ def announce_database_update(result)
115
+ status = result.changed? ? :update : :identical
116
+ color = result.changed? ? :green : :blue
117
+
118
+ say_status(status, 'config/database.yml', color)
119
+ result.messages.each { |message| say(message) }
120
+ end
121
+
122
+ def git_repo?
123
+ _stdout_str, _stderr_str, status = Open3.capture3(
124
+ 'git', 'rev-parse', '--is-inside-work-tree', chdir: destination_root
125
+ )
126
+ status.success?
127
+ rescue Errno::ENOENT
128
+ false
129
+ end
130
+
131
+ def conductor_workspace_root
132
+ "File.expand_path('~/Sites/conductor/workspaces')"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Worktrees
5
+ module Generators
6
+ # Detects common mise.toml setups and suggests loading the worktree-local .env.
7
+ module MiseFollowUp
8
+ private
9
+
10
+ def follow_up_notes_text
11
+ return '' unless suggest_mise_env_file?
12
+
13
+ [
14
+ '',
15
+ ' Tip:',
16
+ ' Detected mise.toml. To auto-load the worktree-local .env when you enter a worktree,',
17
+ ' consider adding:',
18
+ ' [env]',
19
+ ' _.file = ".env"'
20
+ ].join("\n")
21
+ end
22
+
23
+ def suggest_mise_env_file?
24
+ File.file?(mise_toml_path) && !mise_env_file_configured?
25
+ end
26
+
27
+ def mise_env_file_configured?
28
+ File.read(mise_toml_path).match?(/^\s*_.file\s*=\s*["']\.env["']\s*$/)
29
+ end
30
+
31
+ def mise_toml_path
32
+ File.join(destination_root, 'mise.toml')
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ # Example Procfile.dev entry for worktree-aware DEV_PORT usage.
2
+ # Copy the web line into your Procfile.dev if you want bin/dev, Foreman, or Overmind
3
+ # to respect the worktree-local DEV_PORT that bin/wt writes into .env.
4
+ web: env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0 -p ${DEV_PORT:-3000}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
5
+
6
+ require 'bundler/setup'
7
+ load Gem.bin_path('rails-worktrees', 'wt')
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails::Worktrees.configure do |config|
4
+ <% if options['conductor'] -%>
5
+ config.workspace_root = <%= conductor_workspace_root %>
6
+ <% else -%>
7
+ # By default, worktrees go in a sibling "<project>.worktrees" directory.
8
+ # Uncomment to override with a custom parent directory that uses <root>/<project>/<name>.
9
+ # config.workspace_root = File.expand_path('~/worktrees')
10
+ <% end -%>
11
+ # config.bootstrap_env = false
12
+ # config.dev_port_range = 3000..3999
13
+ # config.worktree_database_suffix_max_length = 18
14
+ # config.branch_prefix = '🚂'
15
+ # config.name_sources_path = Rails.root.join('config/worktree_names').to_s
16
+ # config.used_names_file = File.join(
17
+ # ENV.fetch('XDG_STATE_HOME', File.expand_path('~/.local/state')),
18
+ # 'rails-worktrees',
19
+ # 'used-names.tsv'
20
+ # )
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Worktrees
5
+ # Shell entrypoint for the wt executable.
6
+ class CLI
7
+ def initialize(
8
+ argv: ARGV,
9
+ io: { stdin: $stdin, stdout: $stdout, stderr: $stderr },
10
+ env: ENV,
11
+ cwd: Dir.pwd
12
+ )
13
+ @argv = argv
14
+ @io = io
15
+ @env = env
16
+ @cwd = cwd
17
+ end
18
+
19
+ def start
20
+ Command.new(
21
+ argv: @argv,
22
+ io: @io,
23
+ env: @env,
24
+ cwd: @cwd,
25
+ configuration: ::Rails::Worktrees.configuration
26
+ ).run
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Worktrees
5
+ class Command
6
+ # Env bootstrap helpers and preview output.
7
+ module EnvironmentSupport
8
+ private
9
+
10
+ def preview_worktree_environment_command
11
+ raise Error, 'Usage: wt --print-env <worktree-name>' unless @argv.length == 2
12
+
13
+ require_git_repo
14
+ context = resolve_worktree_context(explicit_worktree_name: @argv[1])
15
+ result = env_bootstrapper_for(context).preview
16
+
17
+ print_env_preview(result.values)
18
+ 0
19
+ end
20
+
21
+ def env_bootstrapper_for(context)
22
+ EnvBootstrapper.new(
23
+ target_dir: context[:target_dir],
24
+ worktree_name: context[:worktree_name],
25
+ configuration: @configuration
26
+ )
27
+ end
28
+
29
+ def bootstrap_worktree_environment(context)
30
+ result = env_bootstrapper_for(context).call(dry_run: dry_run?)
31
+
32
+ result.messages.each { |message| info(message) }
33
+ result
34
+ rescue StandardError => e
35
+ warning("Could not bootstrap #{context[:target_dir]}/.env: #{e.message}")
36
+ end
37
+
38
+ def preview_worktree_environment(context)
39
+ env_bootstrapper_for(context).preview
40
+ rescue StandardError => e
41
+ warning("Could not inspect #{context[:target_dir]}/.env: #{e.message}")
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end