rails-worktrees 0.7.4 → 0.8.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 +7 -0
- data/README.md +40 -0
- data/lib/generators/rails/worktrees/templates/bin/wt +1 -1
- data/lib/generators/worktrees/install/install_generator.rb +59 -1
- data/lib/rails/worktrees/cli.rb +1 -1
- data/lib/rails/worktrees/command/output.rb +3 -1
- data/lib/rails/worktrees/command.rb +14 -2
- data/lib/rails/worktrees/pi_worktree_config_path.rb +44 -0
- data/lib/rails/worktrees/pi_worktree_config_updater.rb +123 -0
- data/lib/rails/worktrees/project_maintenance.rb +30 -3
- data/lib/rails/worktrees/version.rb +1 -1
- data/lib/rails/worktrees.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0a2cb7e1644d4ec842daf14fcdf96d6b53d810e548d908a6ea651df4ebaface
|
|
4
|
+
data.tar.gz: 05ae0690974fed85a0862b0fc296681d7de1b037c03d973bef746a1872b62f41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d23f7e9e0e070624ebff11c04f050a88e11ea43b61b893139d337c3df992a4ac3642db5344c491923ee36e94b6864d903dec72cbe8bbbf16c899dd3714bad6c7
|
|
7
|
+
data.tar.gz: 919e046dd0c73ad18ae90a9dc6745dbe5484f00766c8c58302f4981fa44b9e0a730455fa9eb5c7c07b929d02abb1c448e150e587495941ac4c8a9dcc20c5dc73
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.8.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/asjer/rails-worktrees/compare/v0.7.4...v0.8.0) (2026-10-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **pi:** add opt-in pi-worktree setup hook generation ([ccaa0ea](https://github.com/asjer/rails-worktrees/commit/ccaa0ead9fe31a01ac2c8016a51fd60b1611daef)), closes [#53](https://github.com/asjer/rails-worktrees/issues/53)
|
|
9
|
+
|
|
3
10
|
## [0.7.4](https://github.com/asjer/rails-worktrees/compare/v0.7.3...v0.7.4) (2026-08-15)
|
|
4
11
|
|
|
5
12
|
|
data/README.md
CHANGED
|
@@ -17,6 +17,8 @@ bin/rails generate worktrees:install
|
|
|
17
17
|
bin/rails generate worktrees:install --browser
|
|
18
18
|
# or, to apply the common Procfile.dev + Puma + mise follow-ups automatically:
|
|
19
19
|
bin/rails generate worktrees:install --yolo
|
|
20
|
+
# optionally, to let the pi-worktree extension run bin/wt setup in new Pi worktrees:
|
|
21
|
+
bin/rails generate worktrees:install --pi
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
The generated initializer writes app config under `Rails.application.config.x.rails_worktrees`, so the file stays safe to load even in environments like `test` where the gem is only bundled for `:development`. When `rails-worktrees` is loaded, the gem applies those settings for both the CLI and in-process Rails usage.
|
|
@@ -28,6 +30,7 @@ The installer adds:
|
|
|
28
30
|
- `config/initializers/rails_worktrees.rb` — optional configuration
|
|
29
31
|
- `Procfile.dev.worktree.example` — a copy-paste helper for `${DEV_PORT:-3000}` in `Procfile.dev` on regular installs
|
|
30
32
|
- a safe update to `config/database.yml` for common development/test database names
|
|
33
|
+
- `.pi/worktree.json` — only with `--pi`; see [Pi worktrees](#pi-worktrees-pi-worktree-extension)
|
|
31
34
|
|
|
32
35
|
With `--yolo`, the installer also:
|
|
33
36
|
|
|
@@ -51,6 +54,7 @@ bin/wt setup ../my-project.worktrees/my-feature # setup a specific checkout with
|
|
|
51
54
|
bin/wt doctor # audit install/config drift and basic worktree health
|
|
52
55
|
bin/wt update --dry-run # preview safe maintenance fixes
|
|
53
56
|
bin/wt update # apply safe maintenance fixes for managed files
|
|
57
|
+
bin/wt update --pi # also add the pi-worktree hook in .pi/worktree.json
|
|
54
58
|
bin/wt remove my-feature # remove a worktree and delete its local branch
|
|
55
59
|
bin/wt delete my-feature # alias for `bin/wt remove`
|
|
56
60
|
bin/wt remove --force my-feature # also delete an unmerged local branch
|
|
@@ -72,6 +76,7 @@ bin/ob --print-url '?from=nav' # print the resolved URL without opening a brows
|
|
|
72
76
|
| `--skip-setup` | Create a worktree without running setup steps |
|
|
73
77
|
| `--force` | Force branch deletion for `bin/wt remove` / `bin/wt delete` |
|
|
74
78
|
| `--env`, `--print-env <name>` | Preview `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX` |
|
|
79
|
+
| `--pi` | With `bin/wt update`, create or merge `.pi/worktree.json` for the pi-worktree extension |
|
|
75
80
|
|
|
76
81
|
### Default behavior
|
|
77
82
|
|
|
@@ -160,6 +165,41 @@ bin/wt prune --dry-run
|
|
|
160
165
|
|
|
161
166
|
`bin/wt update` is intentionally conservative: it only applies safe file-based fixes for managed templates and supported config shapes. It does **not** delete branches, remove worktrees, or rewrite ambiguous custom files automatically.
|
|
162
167
|
|
|
168
|
+
`bin/wt doctor` and `bin/wt update` only look at `.pi/worktree.json` once it already runs `bin/wt setup`, or when you pass `bin/wt update --pi`.
|
|
169
|
+
|
|
170
|
+
### Pi worktrees (pi-worktree extension)
|
|
171
|
+
|
|
172
|
+
If you create worktrees from [Pi](https://github.com/badlogic/pi-mono) with `pi --worktree` or `/worktree`, rails-worktrees can configure them to run `bin/wt setup` automatically. This is opt-in:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
bin/rails generate worktrees:install --pi
|
|
176
|
+
# or, in an app that already has rails-worktrees installed:
|
|
177
|
+
bin/wt update --pi
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Both commands create or update `.pi/worktree.json`:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"linkEnvFiles": false,
|
|
185
|
+
"postCreate": [
|
|
186
|
+
"bin/wt setup"
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`.pi/worktree.json` is read by the [pi-worktree](https://www.npmjs.com/package/pi-worktree) npm extension (`pi install pi-worktree`), **not** by Pi core. Without that extension, the file has no effect. pi-worktree places worktrees in `.worktrees/` inside the repository by default, so add that directory to `.gitignore`.
|
|
192
|
+
|
|
193
|
+
`linkEnvFiles: false` matters. By default, pi-worktree symlinks the main checkout's gitignored `.env*` files into a new worktree before `postCreate` runs. `bin/wt setup` keeps existing `.env` values, so every Pi worktree would then reuse the main checkout's `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX`, and setup would write through the symlink into the main checkout. With linking disabled, each worktree gets its own `.env` and database identity.
|
|
194
|
+
|
|
195
|
+
Existing configuration is merged, not replaced:
|
|
196
|
+
|
|
197
|
+
- other keys (`dir`, `branchPrefix`, `preRemove`, …) and custom `postCreate` hooks are preserved; `bin/wt setup` is appended once and never duplicated
|
|
198
|
+
- `linkEnvFiles` is only added when it is absent; an explicit `"linkEnvFiles": true`, invalid JSON, or a non-list `postCreate` is left untouched with a message explaining what to change by hand
|
|
199
|
+
- no `preRemove` hook is added, and the worktree directory and branch prefix stay under your control
|
|
200
|
+
|
|
201
|
+
pi-worktree runs each `postCreate` step with a 120-second timeout. If a first `bundle install` or `db:prepare` takes longer, run `bin/wt setup` again inside the worktree.
|
|
202
|
+
|
|
163
203
|
### Updating old `bin/wt` wrappers
|
|
164
204
|
|
|
165
205
|
Versions before 0.7.1 generated `bin/wt` as a Ruby shebang wrapper. On hosts where Ruby is provided by mise but not already on `PATH`, that legacy wrapper can fail with `/usr/bin/env: ruby: No such file or directory` before rails-worktrees can activate mise.
|
|
@@ -80,7 +80,7 @@ def rails_worktrees_bootstrap_safe_invocation?(argv)
|
|
|
80
80
|
return true if %w[-h --help -v --version].include?(args.first)
|
|
81
81
|
return true if %w[--env --print-env].include?(args.first)
|
|
82
82
|
|
|
83
|
-
command_args = args.reject { |arg| %w[--dry-run --skip-setup --env --print-env].include?(arg) }
|
|
83
|
+
command_args = args.reject { |arg| %w[--dry-run --skip-setup --pi --env --print-env].include?(arg) }
|
|
84
84
|
command = command_args.first
|
|
85
85
|
|
|
86
86
|
return true if %w[setup doctor update].include?(command)
|
|
@@ -7,6 +7,7 @@ require_relative '../../../rails/worktrees/database_config_updater'
|
|
|
7
7
|
require_relative '../../../rails/worktrees/procfile_updater'
|
|
8
8
|
require_relative '../../../rails/worktrees/mise_toml_updater'
|
|
9
9
|
require_relative '../../../rails/worktrees/puma_config_updater'
|
|
10
|
+
require_relative '../../../rails/worktrees/pi_worktree_config_updater'
|
|
10
11
|
|
|
11
12
|
module Worktrees
|
|
12
13
|
module Generators
|
|
@@ -35,8 +36,13 @@ module Worktrees
|
|
|
35
36
|
'Apply common Procfile.dev, config/puma.rb, and mise .env',
|
|
36
37
|
'follow-up edits when safe; also generate bin/ob'
|
|
37
38
|
].join(' ')
|
|
39
|
+
class_option :pi,
|
|
40
|
+
type: :boolean,
|
|
41
|
+
default: false,
|
|
42
|
+
desc: 'Generate .pi/worktree.json so the pi-worktree extension runs bin/wt setup in new worktrees'
|
|
38
43
|
|
|
39
44
|
FILE_ENCODING = 'UTF-8'.freeze
|
|
45
|
+
PI_WORKTREE_CONFIG = ::Rails::Worktrees::PiWorktreeConfigUpdater::RELATIVE_PATH
|
|
40
46
|
|
|
41
47
|
FOLLOW_UP_TEMPLATE = <<~TEXT.freeze
|
|
42
48
|
============================================
|
|
@@ -97,6 +103,21 @@ module Worktrees
|
|
|
97
103
|
announce_database_update(result)
|
|
98
104
|
end
|
|
99
105
|
|
|
106
|
+
def configure_pi_worktree
|
|
107
|
+
return unless options[:pi]
|
|
108
|
+
|
|
109
|
+
path = pi_worktree_config_path
|
|
110
|
+
unsafe_reason = ::Rails::Worktrees::PiWorktreeConfigPath.conflict(destination_root)
|
|
111
|
+
return announce_unsafe_pi_worktree_config(unsafe_reason) if unsafe_reason
|
|
112
|
+
|
|
113
|
+
result = ::Rails::Worktrees::PiWorktreeConfigUpdater.new(
|
|
114
|
+
content: File.exist?(path) ? File.read(path, encoding: FILE_ENCODING) : nil
|
|
115
|
+
).call
|
|
116
|
+
write_pi_worktree_config(path, result.content) if result.changed?
|
|
117
|
+
@pi_worktree_outcome = result.status
|
|
118
|
+
announce_updater_result(PI_WORKTREE_CONFIG, result)
|
|
119
|
+
end
|
|
120
|
+
|
|
100
121
|
def verify_installation
|
|
101
122
|
if git_repo?
|
|
102
123
|
say_status(:ok, 'git repository detected', :green)
|
|
@@ -130,6 +151,21 @@ module Worktrees
|
|
|
130
151
|
]
|
|
131
152
|
end
|
|
132
153
|
|
|
154
|
+
def pi_worktree_config_path
|
|
155
|
+
File.join(destination_root, PI_WORKTREE_CONFIG)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def write_pi_worktree_config(path, content)
|
|
159
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
160
|
+
File.write(path, content)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def announce_unsafe_pi_worktree_config(reason)
|
|
164
|
+
@pi_worktree_outcome = :conflict
|
|
165
|
+
say_status(:skip, PI_WORKTREE_CONFIG, :yellow)
|
|
166
|
+
say("#{PI_WORKTREE_CONFIG} #{reason}; leaving it unchanged. Add the bin/wt setup hook manually.")
|
|
167
|
+
end
|
|
168
|
+
|
|
133
169
|
def database_update_result
|
|
134
170
|
result = ::Rails::Worktrees::DatabaseConfigUpdater.new(
|
|
135
171
|
content: File.read(database_config_path, encoding: FILE_ENCODING)
|
|
@@ -145,7 +181,7 @@ module Worktrees
|
|
|
145
181
|
end
|
|
146
182
|
|
|
147
183
|
def follow_up_notes_text
|
|
148
|
-
[super, browser_follow_up_notes_text, puma_follow_up_notes_text].join
|
|
184
|
+
[super, browser_follow_up_notes_text, puma_follow_up_notes_text, pi_worktree_follow_up_notes_text].join
|
|
149
185
|
end
|
|
150
186
|
|
|
151
187
|
def installed_items_text
|
|
@@ -156,9 +192,31 @@ module Worktrees
|
|
|
156
192
|
items << ' • bin/ob' if install_browser_wrapper?
|
|
157
193
|
items << ' • Procfile.dev.worktree.example' unless options[:yolo]
|
|
158
194
|
items << database_follow_up_line if database_follow_up_line
|
|
195
|
+
items << pi_worktree_follow_up_line if pi_worktree_follow_up_line
|
|
159
196
|
items.join("\n")
|
|
160
197
|
end
|
|
161
198
|
|
|
199
|
+
def pi_worktree_follow_up_line
|
|
200
|
+
case @pi_worktree_outcome
|
|
201
|
+
when :updated, :identical
|
|
202
|
+
" • #{PI_WORKTREE_CONFIG} (pi-worktree postCreate runs bin/wt setup)"
|
|
203
|
+
when :conflict
|
|
204
|
+
" • #{PI_WORKTREE_CONFIG} needs manual review — see the message above"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def pi_worktree_follow_up_notes_text
|
|
209
|
+
return '' unless options[:pi]
|
|
210
|
+
|
|
211
|
+
[
|
|
212
|
+
'',
|
|
213
|
+
' Pi worktrees:',
|
|
214
|
+
" #{PI_WORKTREE_CONFIG} is read by the pi-worktree extension",
|
|
215
|
+
' ($ pi install pi-worktree); it is not native Pi core configuration.',
|
|
216
|
+
' Add the worktree directory (default .worktrees/) to .gitignore.'
|
|
217
|
+
].join("\n")
|
|
218
|
+
end
|
|
219
|
+
|
|
162
220
|
def database_follow_up_line
|
|
163
221
|
case @database_outcome
|
|
164
222
|
when :updated
|
data/lib/rails/worktrees/cli.rb
CHANGED
|
@@ -3,7 +3,7 @@ module Rails
|
|
|
3
3
|
# Shell entrypoint for the wt executable.
|
|
4
4
|
class CLI
|
|
5
5
|
LOADER_OPTIONAL_COMMANDS = %w[doctor update -h --help -v --version].freeze
|
|
6
|
-
LOADER_IGNORED_FLAGS = %w[--dry-run --force --skip-setup].freeze
|
|
6
|
+
LOADER_IGNORED_FLAGS = %w[--dry-run --force --skip-setup --pi].freeze
|
|
7
7
|
SETUP_SUBCOMMAND = 'setup'.freeze
|
|
8
8
|
|
|
9
9
|
def initialize(
|
|
@@ -35,7 +35,7 @@ module Rails
|
|
|
35
35
|
wt --print-env <worktree-name>
|
|
36
36
|
wt doctor
|
|
37
37
|
wt setup [--dry-run] [path|name]
|
|
38
|
-
wt update [--dry-run]
|
|
38
|
+
wt update [--dry-run] [--pi]
|
|
39
39
|
wt remove [--dry-run] [--force] <worktree-name>
|
|
40
40
|
wt delete [--dry-run] [--force] <worktree-name>
|
|
41
41
|
wt prune [--dry-run]
|
|
@@ -47,6 +47,7 @@ module Rails
|
|
|
47
47
|
--skip-setup Create the worktree without running setup steps
|
|
48
48
|
--force Delete an unmerged local branch with wt remove/delete
|
|
49
49
|
--env, --print-env <name> Preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
|
|
50
|
+
--pi With wt update, add the pi-worktree bin/wt setup hook
|
|
50
51
|
|
|
51
52
|
Quick start:
|
|
52
53
|
wt Auto-pick a name from a bundled *.txt list
|
|
@@ -72,6 +73,7 @@ module Rails
|
|
|
72
73
|
- wt setup reruns setup for the current checkout, a managed worktree name, or a specific checkout path, including manually-created worktrees
|
|
73
74
|
- wt doctor audits install/config drift plus basic worktree health without changing files
|
|
74
75
|
- wt update applies safe file-based fixes for managed installer artifacts and config hints
|
|
76
|
+
- wt update --pi also writes .pi/worktree.json for the pi-worktree extension (not Pi core)
|
|
75
77
|
- wt remove/delete can run from the main checkout or any sibling worktree, but never remove the worktree you're currently in
|
|
76
78
|
- wt prune removes merged worktrees created by wt while skipping the main checkout and the checkout you're in
|
|
77
79
|
- auto-discovers bundled *.txt files from #{@configuration.name_sources_path}
|
|
@@ -15,6 +15,7 @@ module Rails
|
|
|
15
15
|
DOCTOR_SUBCOMMAND = 'doctor'.freeze
|
|
16
16
|
SETUP_SUBCOMMAND = 'setup'.freeze
|
|
17
17
|
UPDATE_SUBCOMMAND = 'update'.freeze
|
|
18
|
+
PI_FLAG_EXEMPT_META_COMMANDS = %w[-h --help -v --version].freeze
|
|
18
19
|
|
|
19
20
|
include GitOperations
|
|
20
21
|
include EnvironmentSupport
|
|
@@ -35,6 +36,7 @@ module Rails
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def run
|
|
39
|
+
validate_pi_flag!
|
|
38
40
|
meta_command_result = handle_meta_command
|
|
39
41
|
return meta_command_result unless meta_command_result.nil?
|
|
40
42
|
|
|
@@ -52,10 +54,13 @@ module Rails
|
|
|
52
54
|
|
|
53
55
|
def skip_setup? = @skip_setup
|
|
54
56
|
|
|
57
|
+
def pi? = @pi
|
|
58
|
+
|
|
55
59
|
def extract_flags!
|
|
56
60
|
@dry_run = extract_flag!('--dry-run')
|
|
57
61
|
@force = extract_flag!('--force')
|
|
58
62
|
@skip_setup = extract_flag!('--skip-setup')
|
|
63
|
+
@pi = extract_flag!('--pi')
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
def extract_flag!(flag)
|
|
@@ -89,6 +94,13 @@ module Rails
|
|
|
89
94
|
@argv.first == UPDATE_SUBCOMMAND
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
def validate_pi_flag!
|
|
98
|
+
return unless pi?
|
|
99
|
+
return if update_subcommand? || PI_FLAG_EXEMPT_META_COMMANDS.include?(@argv.first)
|
|
100
|
+
|
|
101
|
+
raise Error, 'The --pi flag is only supported with wt update.'
|
|
102
|
+
end
|
|
103
|
+
|
|
92
104
|
def execute_requested_command
|
|
93
105
|
handler = requested_command_handler
|
|
94
106
|
return handler.call if handler
|
|
@@ -259,7 +271,7 @@ module Rails
|
|
|
259
271
|
end
|
|
260
272
|
|
|
261
273
|
def validate_update_args!
|
|
262
|
-
raise Error, 'Usage: wt update [--dry-run] [path]' unless [1, 2].include?(@argv.length)
|
|
274
|
+
raise Error, 'Usage: wt update [--dry-run] [--pi] [path]' unless [1, 2].include?(@argv.length)
|
|
263
275
|
raise Error, 'The --force flag is only supported with wt remove.' if force?
|
|
264
276
|
raise Error, 'The --skip-setup flag is only supported when creating a worktree.' if skip_setup?
|
|
265
277
|
end
|
|
@@ -638,7 +650,7 @@ module Rails
|
|
|
638
650
|
end
|
|
639
651
|
|
|
640
652
|
def project_maintenance_report(root)
|
|
641
|
-
::Rails::Worktrees::ProjectMaintenance.new(root: root).call
|
|
653
|
+
::Rails::Worktrees::ProjectMaintenance.new(root: root, pi_worktree: pi?).call
|
|
642
654
|
end
|
|
643
655
|
|
|
644
656
|
def doctor_worktree_checks(repository)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Worktrees
|
|
3
|
+
# Decides whether .pi/worktree.json under an app root can be safely read and written in place.
|
|
4
|
+
class PiWorktreeConfigPath
|
|
5
|
+
RELATIVE_PATH = '.pi/worktree.json'.freeze
|
|
6
|
+
|
|
7
|
+
def self.conflict(root)
|
|
8
|
+
new(root).conflict
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(root)
|
|
12
|
+
@root = root
|
|
13
|
+
@path = File.join(root, RELATIVE_PATH)
|
|
14
|
+
@parent = File.dirname(@path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns why the config path is unsafe, or nil when it can be used.
|
|
18
|
+
def conflict
|
|
19
|
+
return 'is a symlink' if File.symlink?(@path)
|
|
20
|
+
return 'is not a regular file' if File.exist?(@path) && !File.file?(@path)
|
|
21
|
+
return "cannot be created because #{File.basename(@parent)} is not a directory" if parent_not_directory?
|
|
22
|
+
return if within_root?(nearest_existing_realpath(@parent))
|
|
23
|
+
|
|
24
|
+
'resolves outside the app through a symlinked directory'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def parent_not_directory?
|
|
30
|
+
(File.exist?(@parent) || File.symlink?(@parent)) && !File.directory?(@parent)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def nearest_existing_realpath(path)
|
|
34
|
+
path = File.dirname(path) until File.exist?(path)
|
|
35
|
+
File.realpath(path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def within_root?(path)
|
|
39
|
+
root = File.realpath(@root)
|
|
40
|
+
path == root || path.start_with?("#{root}#{File::SEPARATOR}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative 'pi_worktree_config_path'
|
|
3
|
+
|
|
4
|
+
module Rails
|
|
5
|
+
module Worktrees
|
|
6
|
+
# Safely merges the bin/wt setup hook into the pi-worktree extension's .pi/worktree.json.
|
|
7
|
+
class PiWorktreeConfigUpdater
|
|
8
|
+
Result = Struct.new(:content, :changed, :status, :messages) do
|
|
9
|
+
def changed?
|
|
10
|
+
changed
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RELATIVE_PATH = PiWorktreeConfigPath::RELATIVE_PATH
|
|
15
|
+
SETUP_COMMAND = 'bin/wt setup'.freeze
|
|
16
|
+
# Horizontal whitespace only: a newline would make the shell run two separate commands.
|
|
17
|
+
SETUP_COMMAND_PATTERN = %r{\A[ \t]*(?:\./)?bin/wt[ \t]+setup[ \t]*\z}
|
|
18
|
+
|
|
19
|
+
def self.configured?(content)
|
|
20
|
+
return false if content.nil? || !content.valid_encoding?
|
|
21
|
+
|
|
22
|
+
config = JSON.parse(content)
|
|
23
|
+
config.is_a?(Hash) && setup_hook?(config['postCreate'])
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.setup_hook?(commands)
|
|
29
|
+
return false unless commands.is_a?(Array)
|
|
30
|
+
|
|
31
|
+
commands.any? { |command| command.is_a?(String) && command.match?(SETUP_COMMAND_PATTERN) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(content:)
|
|
35
|
+
@content = content
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call
|
|
39
|
+
config = parse_config
|
|
40
|
+
return config if config.is_a?(Result)
|
|
41
|
+
|
|
42
|
+
conflict = conflict_message(config)
|
|
43
|
+
return conflict_result(conflict) if conflict
|
|
44
|
+
|
|
45
|
+
merge_setup_hook(config)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def parse_config
|
|
51
|
+
return {} if @content.nil?
|
|
52
|
+
unless @content.valid_encoding?
|
|
53
|
+
return conflict_result("#{RELATIVE_PATH} is not valid UTF-8; leaving it unchanged.")
|
|
54
|
+
end
|
|
55
|
+
return {} if @content.strip.empty?
|
|
56
|
+
|
|
57
|
+
config = JSON.parse(@content)
|
|
58
|
+
return config if config.is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
conflict_result("#{RELATIVE_PATH} must contain a JSON object; leaving it unchanged.")
|
|
61
|
+
rescue JSON::ParserError => e
|
|
62
|
+
conflict_result("#{RELATIVE_PATH} is not valid JSON (#{e.message.lines.first.strip}); leaving it unchanged.")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def conflict_message(config)
|
|
66
|
+
if config.key?('linkEnvFiles') && config['linkEnvFiles'] != false
|
|
67
|
+
return [
|
|
68
|
+
"#{RELATIVE_PATH} sets \"linkEnvFiles\": #{JSON.generate(config['linkEnvFiles'])}, so pi-worktree " \
|
|
69
|
+
"would symlink the main checkout's .env into new worktrees before bin/wt setup runs.",
|
|
70
|
+
'Set "linkEnvFiles": false yourself to give each worktree its own DEV_PORT and ' \
|
|
71
|
+
'WORKTREE_DATABASE_SUFFIX, then rerun this command; the file was left unchanged.'
|
|
72
|
+
]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
return if !config.key?('postCreate') || string_list?(config['postCreate'])
|
|
76
|
+
|
|
77
|
+
"#{RELATIVE_PATH} postCreate must be a list of shell commands; leaving it unchanged."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def merge_setup_hook(config)
|
|
81
|
+
updated = config.dup
|
|
82
|
+
messages = [add_setup_hook(updated), disable_env_file_linking(updated)].compact
|
|
83
|
+
return identical_result if messages.empty?
|
|
84
|
+
|
|
85
|
+
Result.new("#{JSON.pretty_generate(ordered_new_config(config, updated))}\n", true, :updated, messages)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_setup_hook(config)
|
|
89
|
+
return if self.class.setup_hook?(config['postCreate'])
|
|
90
|
+
|
|
91
|
+
config['postCreate'] = Array(config['postCreate']) + [SETUP_COMMAND]
|
|
92
|
+
"Added \"#{SETUP_COMMAND}\" to postCreate in #{RELATIVE_PATH}."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def disable_env_file_linking(config)
|
|
96
|
+
return if config.key?('linkEnvFiles')
|
|
97
|
+
|
|
98
|
+
config['linkEnvFiles'] = false
|
|
99
|
+
"Disabled linkEnvFiles in #{RELATIVE_PATH} so each worktree keeps its own .env."
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Fresh configs list linkEnvFiles first so the safety switch is visible above the hooks.
|
|
103
|
+
def ordered_new_config(original, updated)
|
|
104
|
+
return updated unless original.empty?
|
|
105
|
+
|
|
106
|
+
{ 'linkEnvFiles' => updated['linkEnvFiles'], 'postCreate' => updated['postCreate'] }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def string_list?(value)
|
|
110
|
+
value.is_a?(Array) && value.all?(String)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def identical_result
|
|
114
|
+
Result.new(@content, false, :identical,
|
|
115
|
+
["#{RELATIVE_PATH} already runs bin/wt setup with env-file linking disabled."])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def conflict_result(messages)
|
|
119
|
+
Result.new(@content, false, :conflict, Array(messages))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -50,8 +50,9 @@ module Rails
|
|
|
50
50
|
TEMPLATE_ROOT = File.expand_path('../../generators/rails/worktrees/templates', __dir__)
|
|
51
51
|
FILE_ENCODING = 'UTF-8'.freeze
|
|
52
52
|
|
|
53
|
-
def initialize(root:)
|
|
53
|
+
def initialize(root:, pi_worktree: false)
|
|
54
54
|
@root = root
|
|
55
|
+
@pi_worktree = pi_worktree
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def call
|
|
@@ -60,7 +61,7 @@ module Rails
|
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
63
|
-
attr_reader :root
|
|
64
|
+
attr_reader :root, :pi_worktree
|
|
64
65
|
|
|
65
66
|
def maintenance_checks
|
|
66
67
|
[
|
|
@@ -70,7 +71,8 @@ module Rails
|
|
|
70
71
|
optional_updater_check(procfile_config) { |content| ProcfileUpdater.new(content: content).call },
|
|
71
72
|
optional_updater_check(puma_config) { |content| PumaConfigUpdater.new(content: content).call },
|
|
72
73
|
mise_check,
|
|
73
|
-
template_file_check(browser_wrapper_config)
|
|
74
|
+
template_file_check(browser_wrapper_config),
|
|
75
|
+
pi_worktree_check
|
|
74
76
|
]
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -263,6 +265,31 @@ module Rails
|
|
|
263
265
|
updater_result_check(config, mise_updater_result(relative_path))
|
|
264
266
|
end
|
|
265
267
|
|
|
268
|
+
# Only audit .pi/worktree.json when explicitly requested or when it already runs bin/wt setup,
|
|
269
|
+
# so configs owned purely by other pi-worktree users stay untouched.
|
|
270
|
+
def pi_worktree_check
|
|
271
|
+
relative_path = PiWorktreeConfigUpdater::RELATIVE_PATH
|
|
272
|
+
path = absolute_path(relative_path)
|
|
273
|
+
content = File.file?(path) ? read_text(path) : nil
|
|
274
|
+
return unless pi_worktree || PiWorktreeConfigUpdater.configured?(content)
|
|
275
|
+
|
|
276
|
+
config = {
|
|
277
|
+
identifier: :pi_worktree,
|
|
278
|
+
category: :config,
|
|
279
|
+
relative_path: relative_path,
|
|
280
|
+
identical_headline: "#{relative_path} already runs bin/wt setup with env-file linking disabled.",
|
|
281
|
+
fixable_headline: "#{relative_path} can be updated automatically.",
|
|
282
|
+
warning_headline: "#{relative_path} needs manual review."
|
|
283
|
+
}
|
|
284
|
+
path_conflict = PiWorktreeConfigPath.conflict(root)
|
|
285
|
+
if path_conflict
|
|
286
|
+
return warning_check(config, config.fetch(:warning_headline),
|
|
287
|
+
["#{relative_path} #{path_conflict}; leaving it unchanged."])
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
updater_result_check(config, PiWorktreeConfigUpdater.new(content: content).call)
|
|
291
|
+
end
|
|
292
|
+
|
|
266
293
|
def mise_updater_result(relative_path)
|
|
267
294
|
MiseTomlUpdater.new(
|
|
268
295
|
content: read_text(absolute_path(relative_path)),
|
data/lib/rails/worktrees.rb
CHANGED
|
@@ -15,6 +15,8 @@ require_relative 'worktrees/initializer_updater'
|
|
|
15
15
|
require_relative 'worktrees/procfile_updater'
|
|
16
16
|
require_relative 'worktrees/mise_toml_updater'
|
|
17
17
|
require_relative 'worktrees/puma_config_updater'
|
|
18
|
+
require_relative 'worktrees/pi_worktree_config_path'
|
|
19
|
+
require_relative 'worktrees/pi_worktree_config_updater'
|
|
18
20
|
require_relative 'worktrees/project_configuration_loader'
|
|
19
21
|
require_relative 'worktrees/project_maintenance'
|
|
20
22
|
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asjer Querido
|
|
@@ -73,6 +73,8 @@ files:
|
|
|
73
73
|
- lib/rails/worktrees/mise_environment.rb
|
|
74
74
|
- lib/rails/worktrees/mise_toml_updater.rb
|
|
75
75
|
- lib/rails/worktrees/names/cities.txt
|
|
76
|
+
- lib/rails/worktrees/pi_worktree_config_path.rb
|
|
77
|
+
- lib/rails/worktrees/pi_worktree_config_updater.rb
|
|
76
78
|
- lib/rails/worktrees/post_create_runner.rb
|
|
77
79
|
- lib/rails/worktrees/procfile_updater.rb
|
|
78
80
|
- lib/rails/worktrees/project_configuration_loader.rb
|