rails-worktrees 0.1.1 → 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: 5c6da0f27411c99c20b5ab3496a4ac0672ba6a3833e429b33b53b5884d3a3c76
4
- data.tar.gz: cb144472bad91868165d8a37de3ba12f0675255aabc2fa3df0d9e8d6c3d736ff
3
+ metadata.gz: 5cf1debd660652a5429a2d5485e944006685268b423866a96e15af71f9cb3471
4
+ data.tar.gz: 14f6a3f5efaa6b829026b3d4d085c9b55d09236cb7b610bafa8f8834a534fb5b
5
5
  SHA512:
6
- metadata.gz: 7d959fde216a23fa191dca31294ff776b0091e3f230db9191a87890e8736217c3182b773e2950b18cbc151c20f833b238b7f3a07e618a5a4aa997bad5191760e
7
- data.tar.gz: 7abecada6524724a90bd993b3975ce8b20a05e5123c0a9e2f6a2e8bc3d5435d04590a2ba3df83561ec41004d03d83627d48659866290b78b5ce4ed3bebbabfb1
6
+ metadata.gz: e16d066bfb55c97b959e92b4c25d1b2ac08404a728cadf077852a937a1a4c00ea17ca0c08dcfecc571654f836ba0aeccc6f81b08b6a96f7a4443093681d3b25d
7
+ data.tar.gz: f7fdc3e954463fed68f62934d153fbe4b086fb86f4be97c8f8e172b54c0bcc4d402587b99c2b171cc024f7ef53be14e950da311399e49ef0a491d696ac15dc64
@@ -1 +1 @@
1
- {".":"0.1.1"}
1
+ {".":"0.2.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.1.1](https://github.com/asjer/rails-worktrees/compare/v0.1.0...v0.1.1) (2026-03-30)
4
11
 
5
12
 
data/README.md CHANGED
@@ -13,6 +13,8 @@
13
13
  ```bash
14
14
  bundle add rails-worktrees
15
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 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.
@@ -11,7 +11,8 @@ module Rails
11
11
  [
12
12
  '',
13
13
  ' Tip:',
14
- ' 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,',
15
16
  ' consider adding:',
16
17
  ' [env]',
17
18
  ' _.file = ".env"'
@@ -19,15 +20,24 @@ module Rails
19
20
  end
20
21
 
21
22
  def suggest_mise_env_file?
22
- File.file?(mise_toml_path) && !mise_env_file_configured?
23
+ mise_toml_path && !mise_env_file_configured?
23
24
  end
24
25
 
25
26
  def mise_env_file_configured?
26
- 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*$/)
27
30
  end
28
31
 
29
32
  def mise_toml_path
30
- 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
+ ]
31
41
  end
32
42
  end
33
43
  end
@@ -3,10 +3,13 @@ require 'rails/generators'
3
3
 
4
4
  require_relative '../../rails/worktrees/mise_follow_up'
5
5
  require_relative '../../../rails/worktrees/database_config_updater'
6
+ require_relative '../../../rails/worktrees/procfile_updater'
7
+ require_relative '../../../rails/worktrees/mise_toml_updater'
6
8
 
7
9
  module Worktrees
8
10
  module Generators
9
11
  # Installs the wt wrapper, configuration, and safe database.yml updates.
12
+ # rubocop:disable Metrics/ClassLength
10
13
  class InstallGenerator < ::Rails::Generators::Base
11
14
  include ::Rails::Worktrees::Generators::MiseFollowUp
12
15
 
@@ -15,6 +18,8 @@ module Worktrees
15
18
  source_root File.expand_path('../../rails/worktrees/templates', __dir__)
16
19
  class_option :conductor, type: :boolean, default: false,
17
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'
18
23
 
19
24
  FOLLOW_UP_TEMPLATE = <<~TEXT.freeze
20
25
  ============================================
@@ -46,6 +51,13 @@ module Worktrees
46
51
  template('Procfile.dev.worktree.example.tt', 'Procfile.dev.worktree.example')
47
52
  end
48
53
 
54
+ def apply_yolo_follow_ups
55
+ return unless options[:yolo]
56
+
57
+ update_procfile
58
+ update_mise_toml
59
+ end
60
+
49
61
  def update_database_configuration
50
62
  unless File.exist?(database_config_path)
51
63
  say_status(:skip, 'config/database.yml not found', :yellow)
@@ -76,6 +88,17 @@ module Worktrees
76
88
  File.join(destination_root, 'config/database.yml')
77
89
  end
78
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
+
79
102
  def database_update_result
80
103
  result = ::Rails::Worktrees::DatabaseConfigUpdater.new(
81
104
  content: File.read(database_config_path)
@@ -117,6 +140,51 @@ module Worktrees
117
140
  result.messages.each { |message| say(message) }
118
141
  end
119
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
+
120
188
  def git_repo?
121
189
  _stdout_str, _stderr_str, status = Open3.capture3(
122
190
  'git', 'rev-parse', '--is-inside-work-tree', chdir: destination_root
@@ -130,5 +198,6 @@ module Worktrees
130
198
  "File.expand_path('~/Sites/conductor/workspaces')"
131
199
  end
132
200
  end
201
+ # rubocop:enable Metrics/ClassLength
133
202
  end
134
203
  end
@@ -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,5 +1,5 @@
1
1
  module Rails
2
2
  module Worktrees
3
- VERSION = '0.1.1'.freeze
3
+ VERSION = '0.2.0'.freeze
4
4
  end
5
5
  end
@@ -6,6 +6,8 @@ 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.
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.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asjer Querido
@@ -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