rails-worktrees 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc7ddb67fbc5fde18c04ab164229207c6f54284dc4bb5c1fb955f40544706cbb
4
- data.tar.gz: a4005286b6c1dbad990d94ca3999b1490f2532c981fa8e5a7fb9b5768e4f30d6
3
+ metadata.gz: 5cf1debd660652a5429a2d5485e944006685268b423866a96e15af71f9cb3471
4
+ data.tar.gz: 14f6a3f5efaa6b829026b3d4d085c9b55d09236cb7b610bafa8f8834a534fb5b
5
5
  SHA512:
6
- metadata.gz: 8eef6c43ad39e5fc38c3dbdc10fc2618fc07f0ccd65c5c25bef94ae3991aeff143687ee45a37983846cc7ec8fc4c031d39b2b4cd6abcdd37d31243bb705a24a0
7
- data.tar.gz: dfdeeb6c73a81e8c3c8147883b5ca752b79ff4a63e318db409792c6723c4fd84ae3f9effc93edc1d456f74fa22f326e954ac7239d044a27709436d1e462810b6
6
+ metadata.gz: e16d066bfb55c97b959e92b4c25d1b2ac08404a728cadf077852a937a1a4c00ea17ca0c08dcfecc571654f836ba0aeccc6f81b08b6a96f7a4443093681d3b25d
7
+ data.tar.gz: f7fdc3e954463fed68f62934d153fbe4b086fb86f4be97c8f8e172b54c0bcc4d402587b99c2b171cc024f7ef53be14e950da311399e49ef0a491d696ac15dc64
@@ -1 +1 @@
1
- {".":"0.1.0"}
1
+ {".":"0.2.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0](https://github.com/asjer/rails-worktrees/compare/v0.1.1...v0.2.0) (2026-03-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * add `--yolo` mode to install common follow-ups without manual edits ([1ec82ec](https://github.com/asjer/rails-worktrees/commit/1ec82ec83726b5ca9cbaee39697c607fdb825f26))
9
+
10
+ ## [0.1.1](https://github.com/asjer/rails-worktrees/compare/v0.1.0...v0.1.1) (2026-03-30)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **bin/wt:** remove unnecessary frozen_string_literal comment ([e3752c8](https://github.com/asjer/rails-worktrees/commit/e3752c86e4003b80e401253e3c07bd0107ba1514))
16
+ * **ci:** update gem installation steps to prevent `Gemfile.lock` freeze ([e3c75e8](https://github.com/asjer/rails-worktrees/commit/e3c75e85528e575185adb417b6bd9efa995087f1))
17
+ * **frozen-string-literal:** remove unnecessary frozen_string_literal comments ([a8583d2](https://github.com/asjer/rails-worktrees/commit/a8583d2bab1097a6c6a1906a3d6f6092d7b8d867))
18
+ * **generator:** update generator command to use shorter namespace ([cef728e](https://github.com/asjer/rails-worktrees/commit/cef728e3642c36da054f0d53ab5d8a932b2511c4))
19
+ * **installation:** show setup instructions on boot when generator hasn't run ([af31a3f](https://github.com/asjer/rails-worktrees/commit/af31a3f3849408cf7098e2a4e90a9246c89b0bbe))
20
+
3
21
  ## 0.1.0 (2026-03-30)
4
22
 
5
23
 
data/README.md CHANGED
@@ -12,7 +12,9 @@
12
12
 
13
13
  ```bash
14
14
  bundle add rails-worktrees
15
- bin/rails generate rails:worktrees:install
15
+ bin/rails generate worktrees:install
16
+ # or, to apply the common Procfile.dev + mise follow-ups automatically:
17
+ bin/rails generate worktrees:install --yolo
16
18
  ```
17
19
 
18
20
  The installer adds:
@@ -22,6 +24,11 @@ The installer adds:
22
24
  - `Procfile.dev.worktree.example` — a copy-paste helper for `${DEV_PORT:-3000}` in `Procfile.dev`
23
25
  - a safe update to `config/database.yml` for common development/test database names
24
26
 
27
+ With `--yolo`, the installer also:
28
+
29
+ - replaces the existing `web:` entry in `Procfile.dev` with the DEV_PORT-aware command when `Procfile.dev` already exists
30
+ - updates `mise.toml` or `.mise.toml` to load `.env` from `[env]` when either file already exists
31
+
25
32
  ## Usage
26
33
 
27
34
  ```bash
@@ -115,12 +122,17 @@ When `bin/wt` creates a worktree it writes a worktree-local `.env` with:
115
122
 
116
123
  Existing `.env` values are never overwritten.
117
124
 
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:
125
+ By default, the installer does **not** edit your `Procfile.dev` or `mise` config. It generates `Procfile.dev.worktree.example` with a ready-to-copy line:
119
126
 
120
127
  ```text
121
128
  web: env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0 -p ${DEV_PORT:-3000}
122
129
  ```
123
130
 
131
+ If you run `bin/rails generate worktrees:install --yolo`, the installer applies the two common follow-ups for you when the files already exist:
132
+
133
+ - replace the existing `web:` entry in `Procfile.dev`
134
+ - add `_.file = ".env"` to the `[env]` section of `mise.toml` or `.mise.toml`
135
+
124
136
  Use a project-local env loader like `mise` with `_.file = ".env"` to keep values scoped per-worktree.
125
137
 
126
138
  ## Development
@@ -141,8 +153,8 @@ This smoke test:
141
153
 
142
154
  - creates a temporary Rails app from a compatible Rails version
143
155
  - 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
156
+ - runs `bin/rails generate worktrees:install --yolo`
157
+ - verifies `bin/wt`, the generated initializer, the Procfile example, yolo updates to `Procfile.dev` and `mise.toml`, `config/database.yml` patching, and worktree `.env` bootstrapping
146
158
  - creates a temporary bare `origin` and confirms `bin/wt smoke-branch` creates a real worktree
147
159
 
148
160
  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.
data/Rakefile CHANGED
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'bundler/gem_tasks'
4
2
  require 'rspec/core/rake_task'
5
3
 
data/exe/wt CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
2
  lib = File.expand_path('../lib', __dir__)
5
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
4
 
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  module Generators
@@ -13,7 +11,8 @@ module Rails
13
11
  [
14
12
  '',
15
13
  ' Tip:',
16
- ' Detected mise.toml. To auto-load the worktree-local .env when you enter a worktree,',
14
+ " Detected #{File.basename(mise_toml_path)}. To auto-load the worktree-local .env when",
15
+ ' you enter a worktree,',
17
16
  ' consider adding:',
18
17
  ' [env]',
19
18
  ' _.file = ".env"'
@@ -21,15 +20,24 @@ module Rails
21
20
  end
22
21
 
23
22
  def suggest_mise_env_file?
24
- File.file?(mise_toml_path) && !mise_env_file_configured?
23
+ mise_toml_path && !mise_env_file_configured?
25
24
  end
26
25
 
27
26
  def mise_env_file_configured?
28
- File.read(mise_toml_path).match?(/^\s*_.file\s*=\s*["']\.env["']\s*$/)
27
+ return false unless mise_toml_path
28
+
29
+ File.read(mise_toml_path).match?(/^\s*_.file\s*=\s*["']\.env["']\s*(?:#.*)?\s*$/)
29
30
  end
30
31
 
31
32
  def mise_toml_path
32
- File.join(destination_root, 'mise.toml')
33
+ @mise_toml_path ||= mise_toml_paths.find { |path| File.file?(path) }
34
+ end
35
+
36
+ def mise_toml_paths
37
+ [
38
+ File.join(destination_root, 'mise.toml'),
39
+ File.join(destination_root, '.mise.toml')
40
+ ]
33
41
  end
34
42
  end
35
43
  end
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
2
  ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
5
3
 
6
4
  require 'bundler/setup'
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  Rails::Worktrees.configure do |config|
4
2
  <% if options['conductor'] -%>
5
3
  config.workspace_root = <%= conductor_workspace_root %>
@@ -0,0 +1,203 @@
1
+ require 'open3'
2
+ require 'rails/generators'
3
+
4
+ require_relative '../../rails/worktrees/mise_follow_up'
5
+ require_relative '../../../rails/worktrees/database_config_updater'
6
+ require_relative '../../../rails/worktrees/procfile_updater'
7
+ require_relative '../../../rails/worktrees/mise_toml_updater'
8
+
9
+ module Worktrees
10
+ module Generators
11
+ # Installs the wt wrapper, configuration, and safe database.yml updates.
12
+ # rubocop:disable Metrics/ClassLength
13
+ class InstallGenerator < ::Rails::Generators::Base
14
+ include ::Rails::Worktrees::Generators::MiseFollowUp
15
+
16
+ namespace 'worktrees:install'
17
+ desc 'Installs bin/wt, a Rails::Worktrees initializer, and updates config/database.yml when safe.'
18
+ source_root File.expand_path('../../rails/worktrees/templates', __dir__)
19
+ class_option :conductor, type: :boolean, default: false,
20
+ desc: 'Configure the installer for ~/Sites/conductor/workspaces'
21
+ class_option :yolo, type: :boolean, default: false,
22
+ desc: 'Apply common Procfile.dev and mise .env follow-up edits when safe'
23
+
24
+ FOLLOW_UP_TEMPLATE = <<~TEXT.freeze
25
+ ============================================
26
+ rails-worktrees installed successfully! 🚂
27
+
28
+ Installed:
29
+ %<installed>s
30
+
31
+ Get started:
32
+ $ bin/wt
33
+ $ bin/wt my-feature
34
+
35
+ Configure:
36
+ config/initializers/rails_worktrees.rb
37
+ %<notes>s
38
+ ============================================
39
+ TEXT
40
+
41
+ def create_bin_wrapper
42
+ template('bin/wt', 'bin/wt')
43
+ chmod('bin/wt', 0o755)
44
+ end
45
+
46
+ def create_initializer
47
+ template('rails_worktrees.rb.tt', 'config/initializers/rails_worktrees.rb')
48
+ end
49
+
50
+ def create_procfile_worktree_example
51
+ template('Procfile.dev.worktree.example.tt', 'Procfile.dev.worktree.example')
52
+ end
53
+
54
+ def apply_yolo_follow_ups
55
+ return unless options[:yolo]
56
+
57
+ update_procfile
58
+ update_mise_toml
59
+ end
60
+
61
+ def update_database_configuration
62
+ unless File.exist?(database_config_path)
63
+ say_status(:skip, 'config/database.yml not found', :yellow)
64
+ @database_outcome = :not_found
65
+ return
66
+ end
67
+
68
+ result = database_update_result
69
+ @database_outcome = result.changed? ? :updated : :identical
70
+ announce_database_update(result)
71
+ end
72
+
73
+ def verify_installation
74
+ if git_repo?
75
+ say_status(:ok, 'git repository detected', :green)
76
+ else
77
+ say_status(:warning, 'run inside a git repository for bin/wt to work', :yellow)
78
+ end
79
+ end
80
+
81
+ def show_follow_up
82
+ say(follow_up_message)
83
+ end
84
+
85
+ private
86
+
87
+ def database_config_path
88
+ File.join(destination_root, 'config/database.yml')
89
+ end
90
+
91
+ def procfile_path
92
+ File.join(destination_root, 'Procfile.dev')
93
+ end
94
+
95
+ def mise_toml_paths
96
+ [
97
+ File.join(destination_root, 'mise.toml'),
98
+ File.join(destination_root, '.mise.toml')
99
+ ]
100
+ end
101
+
102
+ def database_update_result
103
+ result = ::Rails::Worktrees::DatabaseConfigUpdater.new(
104
+ content: File.read(database_config_path)
105
+ ).call
106
+
107
+ File.write(database_config_path, result.content) if result.changed?
108
+
109
+ result
110
+ end
111
+
112
+ def follow_up_message
113
+ "\n#{format(FOLLOW_UP_TEMPLATE, installed: installed_items_text, notes: follow_up_notes_text)}"
114
+ end
115
+
116
+ def installed_items_text
117
+ items = [
118
+ ' • bin/wt',
119
+ ' • config/initializers/rails_worktrees.rb',
120
+ ' • Procfile.dev.worktree.example'
121
+ ]
122
+ items << database_follow_up_line if database_follow_up_line
123
+ items.join("\n")
124
+ end
125
+
126
+ def database_follow_up_line
127
+ case @database_outcome
128
+ when :updated
129
+ ' • config/database.yml (updated with WORKTREE_DATABASE_SUFFIX)'
130
+ when :not_found
131
+ ' • config/database.yml was not found — see README for manual setup'
132
+ end
133
+ end
134
+
135
+ def announce_database_update(result)
136
+ status = result.changed? ? :update : :identical
137
+ color = result.changed? ? :green : :blue
138
+
139
+ say_status(status, 'config/database.yml', color)
140
+ result.messages.each { |message| say(message) }
141
+ end
142
+
143
+ def update_procfile
144
+ unless File.exist?(procfile_path)
145
+ say_status(:skip, 'Procfile.dev not found', :yellow)
146
+ say('Skipped Procfile.dev yolo update because the file does not exist yet.')
147
+ return
148
+ end
149
+
150
+ result = ::Rails::Worktrees::ProcfileUpdater.new(content: File.read(procfile_path)).call
151
+ File.write(procfile_path, result.content) if result.changed?
152
+ announce_updater_result('Procfile.dev', result)
153
+ end
154
+
155
+ def update_mise_toml
156
+ path = first_mise_toml_path
157
+ return announce_missing_mise_toml unless path
158
+
159
+ result = ::Rails::Worktrees::MiseTomlUpdater.new(
160
+ content: File.read(path),
161
+ file_name: File.basename(path)
162
+ ).call
163
+
164
+ File.write(path, result.content) if result.changed?
165
+ announce_updater_result(File.basename(path), result)
166
+ end
167
+
168
+ def announce_updater_result(path, result)
169
+ status, color = case result.status
170
+ when :updated then %i[update green]
171
+ when :identical then %i[identical blue]
172
+ else %i[skip yellow]
173
+ end
174
+
175
+ say_status(status, path, color)
176
+ result.messages.each { |message| say(message) }
177
+ end
178
+
179
+ def first_mise_toml_path
180
+ mise_toml_paths.find { |candidate| File.file?(candidate) }
181
+ end
182
+
183
+ def announce_missing_mise_toml
184
+ say_status(:skip, 'mise.toml/.mise.toml not found', :yellow)
185
+ say('Skipped mise yolo update because no supported mise config file was found.')
186
+ end
187
+
188
+ def git_repo?
189
+ _stdout_str, _stderr_str, status = Open3.capture3(
190
+ 'git', 'rev-parse', '--is-inside-work-tree', chdir: destination_root
191
+ )
192
+ status.success?
193
+ rescue Errno::ENOENT
194
+ false
195
+ end
196
+
197
+ def conductor_workspace_root
198
+ "File.expand_path('~/Sites/conductor/workspaces')"
199
+ end
200
+ end
201
+ # rubocop:enable Metrics/ClassLength
202
+ end
203
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  # Shell entrypoint for the wt executable.
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  class Command
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'open3'
4
2
 
5
3
  module Rails
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  class Command
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  class Command
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  class Command
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'fileutils'
4
2
  require_relative 'command/environment_support'
5
3
  require_relative 'command/git_operations'
@@ -1,11 +1,9 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  # Stores application-level settings for the wt command.
6
4
  class Configuration
7
5
  DEFAULT_BOOTSTRAP_ENV = true
8
- DEFAULT_BRANCH_PREFIX = '🚂'
6
+ DEFAULT_BRANCH_PREFIX = '🚂'.freeze
9
7
  DEFAULT_DEV_PORT_RANGE = (3000..3999)
10
8
  DEFAULT_USED_NAMES_DIRECTORY = File.join(
11
9
  ENV.fetch('XDG_STATE_HOME', File.join(Dir.home, '.local/state')),
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
3
  # Safely patches common database.yml layouts for worktree suffixes.
@@ -10,7 +8,7 @@ module Rails
10
8
  end
11
9
  end
12
10
 
13
- SUFFIX_TEMPLATE = "<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>"
11
+ SUFFIX_TEMPLATE = "<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>".freeze
14
12
  SUPPORTED_ENVIRONMENTS = %w[development test].freeze
15
13
  DATABASE_LINE_PATTERN = /\A(\s*database:\s*)(.+?)(\s*(?:#.*)?\n?)\z/
16
14
  SECTION_PATTERN = /\A([A-Za-z0-9_]+):(?:\s|$)/
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'pathname'
4
2
  require 'zlib'
5
3
 
@@ -14,7 +12,7 @@ module Rails
14
12
  attr_accessor :values
15
13
  end
16
14
 
17
- ENV_FILE_NAME = '.env'
15
+ ENV_FILE_NAME = '.env'.freeze
18
16
 
19
17
  def initialize(target_dir:, worktree_name:, configuration:)
20
18
  @target_dir = target_dir
@@ -0,0 +1,82 @@
1
+ module Rails
2
+ module Worktrees
3
+ # Safely updates mise config to load the worktree-local .env.
4
+ class MiseTomlUpdater
5
+ Result = Struct.new(:content, :changed, :status, :messages) do
6
+ def changed?
7
+ changed
8
+ end
9
+ end
10
+
11
+ ENV_SECTION_PATTERN = /\A\s*\[env\]\s*(?:#.*)?\z/
12
+ ENV_FILE_PATTERN = /\A\s*_.file\s*=\s*["']\.env["']\s*(?:#.*)?\z/
13
+ SECTION_PATTERN = /\A\s*\[[^\]]+\]\s*(?:#.*)?\z/
14
+ ENV_FILE_ENTRY = '_.file = ".env"'.freeze
15
+
16
+ def initialize(content:, file_name:)
17
+ @content = content
18
+ @file_name = file_name
19
+ end
20
+
21
+ def call
22
+ lines = @content.lines(chomp: true)
23
+ return update_existing_env_section(lines) if env_section_present?(lines)
24
+
25
+ append_env_section(lines)
26
+ end
27
+
28
+ private
29
+
30
+ def update_existing_env_section(lines)
31
+ env_section_start, env_section_end = env_section_bounds(lines)
32
+ return identical_result if env_file_configured?(lines, env_section_start, env_section_end)
33
+
34
+ updated_lines = lines.dup
35
+ updated_lines.insert(env_section_end, ENV_FILE_ENTRY)
36
+ updated_result(rebuild_content(updated_lines, trailing_newline: @content.end_with?("\n")))
37
+ end
38
+
39
+ def append_env_section(lines)
40
+ updated_lines = lines.dup
41
+ updated_lines << '' if updated_lines.any? && !updated_lines.last.empty?
42
+ updated_lines << '[env]'
43
+ updated_lines << ENV_FILE_ENTRY
44
+
45
+ updated_result(rebuild_content(updated_lines, trailing_newline: true))
46
+ end
47
+
48
+ def updated_result(content)
49
+ Result.new(content, true, :updated, ["Configured #{@file_name} to load .env from [env]."])
50
+ end
51
+
52
+ def identical_result
53
+ Result.new(@content, false, :identical, ["#{@file_name} already loads .env from [env]."])
54
+ end
55
+
56
+ def env_section_bounds(lines)
57
+ start_index = lines.find_index { |line| line.match?(ENV_SECTION_PATTERN) }
58
+ return [nil, nil] unless start_index
59
+
60
+ end_index = ((start_index + 1)...lines.length).find do |index|
61
+ lines[index].match?(SECTION_PATTERN)
62
+ end || lines.length
63
+ [start_index, end_index]
64
+ end
65
+
66
+ def env_section_present?(lines)
67
+ env_section_bounds(lines).first
68
+ end
69
+
70
+ def env_file_configured?(lines, start_index, end_index)
71
+ lines[(start_index + 1)...end_index].any? { |line| line.match?(ENV_FILE_PATTERN) }
72
+ end
73
+
74
+ def rebuild_content(lines, trailing_newline:)
75
+ content = lines.join("\n")
76
+ return content if content.empty? || !trailing_newline
77
+
78
+ "#{content}\n"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,69 @@
1
+ module Rails
2
+ module Worktrees
3
+ # Safely updates Procfile.dev to use the worktree-local DEV_PORT.
4
+ class ProcfileUpdater
5
+ Result = Struct.new(:content, :changed, :status, :messages) do
6
+ def changed?
7
+ changed
8
+ end
9
+ end
10
+
11
+ STANDARD_WEB_ENTRY = 'web: env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0 -p ${DEV_PORT:-3000}'.freeze
12
+ WEB_ENTRY_PATTERN = /\Aweb:\s*.*\z/
13
+
14
+ def initialize(content:)
15
+ @content = content
16
+ end
17
+
18
+ def call
19
+ lines = @content.lines(chomp: true)
20
+ web_entry_indexes = web_entry_indexes(lines)
21
+ return skip_result if web_entry_indexes.empty?
22
+
23
+ updated_content = rebuild_content(
24
+ replace_web_entries(lines, web_entry_indexes),
25
+ trailing_newline: @content.end_with?("\n")
26
+ )
27
+
28
+ build_result(updated_content)
29
+ end
30
+
31
+ private
32
+
33
+ def updated_result(content)
34
+ Result.new(content, true, :updated, ['Updated Procfile.dev web entry to use DEV_PORT.'])
35
+ end
36
+
37
+ def identical_result(content)
38
+ Result.new(content, false, :identical, ['Procfile.dev already uses the DEV_PORT-aware web entry.'])
39
+ end
40
+
41
+ def skip_result
42
+ Result.new(@content, false, :skip, ['No web: entry found in Procfile.dev; update it manually if needed.'])
43
+ end
44
+
45
+ def web_entry_indexes(lines)
46
+ lines.each_index.select { |index| lines[index].match?(WEB_ENTRY_PATTERN) }
47
+ end
48
+
49
+ def replace_web_entries(lines, web_entry_indexes)
50
+ lines.dup.tap do |updated_lines|
51
+ web_entry_indexes.each { |index| updated_lines[index] = STANDARD_WEB_ENTRY }
52
+ end
53
+ end
54
+
55
+ def build_result(content)
56
+ return identical_result(content) if content == @content
57
+
58
+ updated_result(content)
59
+ end
60
+
61
+ def rebuild_content(lines, trailing_newline:)
62
+ content = lines.join("\n")
63
+ return content if content.empty? || !trailing_newline
64
+
65
+ "#{content}\n"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,8 +1,10 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
3
+ # Hooks install guidance into Rails boot when the generator has not run yet.
5
4
  class Railtie < ::Rails::Railtie
5
+ initializer 'rails_worktrees.installation_hint' do
6
+ Rails::Worktrees.warn_about_missing_installation
7
+ end
6
8
  end
7
9
  end
8
10
  end
@@ -1,7 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Rails
4
2
  module Worktrees
5
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'.freeze
6
4
  end
7
5
  end
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: true
1
+ require 'pathname'
2
2
 
3
3
  require_relative 'worktrees/version'
4
4
  require_relative 'worktrees/configuration'
@@ -6,12 +6,21 @@ require_relative 'worktrees/env_bootstrapper'
6
6
  require_relative 'worktrees/command'
7
7
  require_relative 'worktrees/cli'
8
8
  require_relative 'worktrees/database_config_updater'
9
+ require_relative 'worktrees/procfile_updater'
10
+ require_relative 'worktrees/mise_toml_updater'
9
11
 
10
12
  module Rails
11
13
  # Rails-specific git worktree helpers and installer support.
12
14
  module Worktrees
13
15
  class Error < StandardError; end
14
16
 
17
+ INSTALL_GENERATOR_COMMAND = 'bin/rails generate worktrees:install'.freeze
18
+ INSTALL_GENERATOR_NAMES = %w[worktrees:install].freeze
19
+ REQUIRED_INSTALLATION_PATHS = [
20
+ 'bin/wt',
21
+ 'config/initializers/rails_worktrees.rb'
22
+ ].freeze
23
+
15
24
  class << self
16
25
  def configuration
17
26
  @configuration ||= Configuration.new
@@ -25,6 +34,95 @@ module Rails
25
34
  def reset_configuration!
26
35
  @configuration = Configuration.new
27
36
  end
37
+
38
+ def installation_complete?(root = resolve_root)
39
+ return false unless root
40
+
41
+ required_installation_paths(root).all?(&:exist?)
42
+ end
43
+
44
+ def missing_installation_message(root: resolve_root)
45
+ return generic_missing_installation_message unless root
46
+
47
+ detailed_missing_installation_message(normalize_root(root))
48
+ end
49
+
50
+ def warn_about_missing_installation(root: resolve_root, stderr: $stderr, argv: ARGV)
51
+ return unless missing_installation_warning_needed?(root: root, argv: argv)
52
+
53
+ stderr.puts(missing_installation_message(root: root))
54
+ end
55
+
56
+ private
57
+
58
+ def missing_installation_warning_needed?(root:, argv:)
59
+ return false unless root
60
+ return false if installation_complete?(root)
61
+ return false if install_generator_invocation?(argv)
62
+
63
+ true
64
+ end
65
+
66
+ def install_generator_invocation?(argv)
67
+ normalized_args = Array(argv).map(&:to_s)
68
+ generator_commands = %w[generate g]
69
+
70
+ INSTALL_GENERATOR_NAMES.any? do |generator_name|
71
+ normalized_args.include?(generator_name) ||
72
+ normalized_args.each_cons(2).any? do |left, right|
73
+ generator_commands.include?(left) && right == generator_name
74
+ end
75
+ end
76
+ end
77
+
78
+ def required_installation_paths(root)
79
+ root_path = normalize_root(root)
80
+
81
+ REQUIRED_INSTALLATION_PATHS.map { |relative_path| root_path.join(relative_path) }
82
+ end
83
+
84
+ def generic_missing_installation_message
85
+ <<~MSG
86
+
87
+ rails-worktrees is in your bundle, but the app installer has not run yet.
88
+
89
+ Run:
90
+ $ #{INSTALL_GENERATOR_COMMAND}
91
+
92
+ Docs: https://github.com/asjer/rails-worktrees
93
+ MSG
94
+ end
95
+
96
+ def detailed_missing_installation_message(root_path)
97
+ <<~MSG
98
+
99
+ rails-worktrees is in your bundle, but the app installer has not run yet.
100
+
101
+ Run:
102
+ $ #{INSTALL_GENERATOR_COMMAND}
103
+
104
+ Missing expected files under #{root_path}:
105
+ #{missing_installation_items_text(root_path)}
106
+
107
+ Docs: https://github.com/asjer/rails-worktrees
108
+ MSG
109
+ end
110
+
111
+ def missing_installation_items_text(root)
112
+ required_installation_paths(root).reject(&:exist?).map do |path|
113
+ " • #{path.relative_path_from(root)}"
114
+ end.join("\n")
115
+ end
116
+
117
+ def resolve_root
118
+ return unless defined?(Rails) && Rails.respond_to?(:root)
119
+
120
+ Rails.root
121
+ end
122
+
123
+ def normalize_root(root)
124
+ root.is_a?(Pathname) ? root : Pathname(root)
125
+ end
28
126
  end
29
127
  end
30
128
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-worktrees
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asjer Querido
@@ -45,11 +45,11 @@ files:
45
45
  - Rakefile
46
46
  - exe/wt
47
47
  - lefthook.yml
48
- - lib/generators/rails/worktrees/install_generator.rb
49
48
  - lib/generators/rails/worktrees/mise_follow_up.rb
50
49
  - lib/generators/rails/worktrees/templates/Procfile.dev.worktree.example.tt
51
50
  - lib/generators/rails/worktrees/templates/bin/wt
52
51
  - lib/generators/rails/worktrees/templates/rails_worktrees.rb.tt
52
+ - lib/generators/worktrees/install/install_generator.rb
53
53
  - lib/rails/worktrees.rb
54
54
  - lib/rails/worktrees/cli.rb
55
55
  - lib/rails/worktrees/command.rb
@@ -61,7 +61,9 @@ files:
61
61
  - lib/rails/worktrees/configuration.rb
62
62
  - lib/rails/worktrees/database_config_updater.rb
63
63
  - lib/rails/worktrees/env_bootstrapper.rb
64
+ - lib/rails/worktrees/mise_toml_updater.rb
64
65
  - lib/rails/worktrees/names/cities.txt
66
+ - lib/rails/worktrees/procfile_updater.rb
65
67
  - lib/rails/worktrees/railtie.rb
66
68
  - lib/rails/worktrees/version.rb
67
69
  - mise.toml
@@ -77,7 +79,7 @@ metadata:
77
79
  rubygems_mfa_required: 'true'
78
80
  post_install_message: "\n============================================\n Thank you
79
81
  for installing rails-worktrees! \U0001F389\n\n Run the installer:\n $ bin/rails
80
- generate rails:worktrees:install\n\n Docs: https://github.com/asjer/rails-worktrees\n============================================\n\n"
82
+ generate worktrees:install\n\n Docs: https://github.com/asjer/rails-worktrees\n============================================\n\n"
81
83
  rdoc_options: []
82
84
  require_paths:
83
85
  - lib
@@ -1,137 +0,0 @@
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