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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +18 -0
- data/README.md +16 -4
- data/Rakefile +0 -2
- data/exe/wt +0 -2
- data/lib/generators/rails/worktrees/mise_follow_up.rb +14 -6
- data/lib/generators/rails/worktrees/templates/bin/wt +0 -2
- data/lib/generators/rails/worktrees/templates/rails_worktrees.rb.tt +0 -2
- data/lib/generators/worktrees/install/install_generator.rb +203 -0
- data/lib/rails/worktrees/cli.rb +0 -2
- data/lib/rails/worktrees/command/environment_support.rb +0 -2
- data/lib/rails/worktrees/command/git_operations.rb +0 -2
- data/lib/rails/worktrees/command/name_picking.rb +0 -2
- data/lib/rails/worktrees/command/output.rb +0 -2
- data/lib/rails/worktrees/command/workspace_paths.rb +0 -2
- data/lib/rails/worktrees/command.rb +0 -2
- data/lib/rails/worktrees/configuration.rb +1 -3
- data/lib/rails/worktrees/database_config_updater.rb +1 -3
- data/lib/rails/worktrees/env_bootstrapper.rb +1 -3
- data/lib/rails/worktrees/mise_toml_updater.rb +82 -0
- data/lib/rails/worktrees/procfile_updater.rb +69 -0
- data/lib/rails/worktrees/railtie.rb +4 -2
- data/lib/rails/worktrees/version.rb +1 -3
- data/lib/rails/worktrees.rb +99 -1
- metadata +5 -3
- data/lib/generators/rails/worktrees/install_generator.rb +0 -137
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cf1debd660652a5429a2d5485e944006685268b423866a96e15af71f9cb3471
|
|
4
|
+
data.tar.gz: 14f6a3f5efaa6b829026b3d4d085c9b55d09236cb7b610bafa8f8834a534fb5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e16d066bfb55c97b959e92b4c25d1b2ac08404a728cadf077852a937a1a4c00ea17ca0c08dcfecc571654f836ba0aeccc6f81b08b6a96f7a4443093681d3b25d
|
|
7
|
+
data.tar.gz: f7fdc3e954463fed68f62934d153fbe4b086fb86f4be97c8f8e172b54c0bcc4d402587b99c2b171cc024f7ef53be14e950da311399e49ef0a491d696ac15dc64
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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
|
-
|
|
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
|
|
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
data/exe/wt
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
+
mise_toml_path && !mise_env_file_configured?
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def mise_env_file_configured?
|
|
28
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
data/lib/rails/worktrees/cli.rb
CHANGED
|
@@ -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
|
data/lib/rails/worktrees.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
|
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
|