rails-worktrees 0.7.3 → 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 +14 -0
- data/README.md +72 -1
- data/lib/generators/rails/worktrees/templates/bin/wt +20 -9
- 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 +30 -3
- data/lib/rails/worktrees/mise_environment.rb +1 -1
- 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/post_create_runner.rb +52 -4
- 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,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [0.7.4](https://github.com/asjer/rails-worktrees/compare/v0.7.3...v0.7.4) (2026-08-15)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **wt:** bootstrap Ruby through mise for legacy wrappers ([#45](https://github.com/asjer/rails-worktrees/issues/45)) ([b3326ab](https://github.com/asjer/rails-worktrees/commit/b3326ab737fb814d826a1b4870433028a455f582))
|
|
16
|
+
|
|
3
17
|
## [0.7.3](https://github.com/asjer/rails-worktrees/compare/v0.7.2...v0.7.3) (2026-07-08)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -17,17 +17,20 @@ 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.
|
|
23
25
|
|
|
24
26
|
The installer adds:
|
|
25
27
|
|
|
26
|
-
- `bin/wt` — a
|
|
28
|
+
- `bin/wt` — a mise-aware bootstrap wrapper that executes the gem-owned CLI
|
|
27
29
|
- `bin/ob` — an optional browser helper generated by `--browser` or `--yolo`
|
|
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,72 @@ 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
|
+
|
|
203
|
+
### Updating old `bin/wt` wrappers
|
|
204
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
Use the current gem executable once to repair the app-owned wrapper. Scope mise discovery to the checkout and treat in-repository mise configs as trusted for this recovery shell, so `mise exec` loads Ruby from the same configuration the repaired wrapper will use:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# From the Rails app root
|
|
211
|
+
root=$(pwd -P)
|
|
212
|
+
trust_root=$(
|
|
213
|
+
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE \
|
|
214
|
+
GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
|
|
215
|
+
git -C "$root" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$root"
|
|
216
|
+
)
|
|
217
|
+
trust_root=$(cd "$trust_root" && pwd -P)
|
|
218
|
+
MISE_CEILING_PATHS=$(dirname "$trust_root")
|
|
219
|
+
MISE_TRUSTED_CONFIG_PATHS="$trust_root"
|
|
220
|
+
export MISE_CEILING_PATHS MISE_TRUSTED_CONFIG_PATHS
|
|
221
|
+
|
|
222
|
+
mise exec -C "$root" -- ruby -v
|
|
223
|
+
mise exec -C "$root" -- gem install rails-worktrees --no-document
|
|
224
|
+
mise exec -C "$root" -- ruby -e 'load Gem.bin_path("rails-worktrees", "wt")' -- update --dry-run "$root"
|
|
225
|
+
mise exec -C "$root" -- ruby -e 'load Gem.bin_path("rails-worktrees", "wt")' -- update "$root"
|
|
226
|
+
git status --short
|
|
227
|
+
git diff --stat
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Review the diff before committing. `wt update` applies every safe managed maintenance fix it finds, so the resulting commit can include `bin/wt` plus other generated installer drift such as the initializer, database configuration, Procfile, Puma config, mise config, or `bin/ob`.
|
|
231
|
+
|
|
232
|
+
New installs and repaired apps use a bash bootstrap wrapper that activates mise from the app root before loading Ruby/Bundler, trusting mise configs only inside the repository root and bounding discovery above the checkout. That covers app-local, in-repository higher-scope, local override, and environment-specific mise configuration without trusting an unrelated caller directory or configs above the checkout. The wrapper also falls back to the globally installed `wt` executable for bootstrap-safe commands such as `wt setup --dry-run` when the app bundle is incomplete.
|
|
233
|
+
|
|
163
234
|
|
|
164
235
|
### Interactive prompts
|
|
165
236
|
|
|
@@ -47,18 +47,29 @@ fi
|
|
|
47
47
|
WT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
48
48
|
WT_SCRIPT_PATH="$WT_SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")"
|
|
49
49
|
WT_APP_ROOT="$(cd "$WT_SCRIPT_DIR/.." && pwd -P)"
|
|
50
|
-
WT_MISE_CONFIG=""
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
wt_path_within() {
|
|
52
|
+
case "$1" in
|
|
53
|
+
"$2"|"$2"/*) return 0 ;;
|
|
54
|
+
*) return 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
WT_MISE_TRUST_ROOT="$WT_APP_ROOT"
|
|
59
|
+
if command -v git >/dev/null 2>&1; then
|
|
60
|
+
WT_GIT_ROOT="$(unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES; git -C "$WT_APP_ROOT" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
61
|
+
if [ -n "$WT_GIT_ROOT" ]; then
|
|
62
|
+
WT_GIT_ROOT="$(cd "$WT_GIT_ROOT" && pwd -P)"
|
|
63
|
+
wt_path_within "$WT_APP_ROOT" "$WT_GIT_ROOT" && WT_MISE_TRUST_ROOT="$WT_GIT_ROOT"
|
|
64
|
+
fi
|
|
56
65
|
fi
|
|
66
|
+
WT_MISE_CEILING_PATH="$(dirname "$WT_MISE_TRUST_ROOT")"
|
|
57
67
|
|
|
58
|
-
if [ -
|
|
59
|
-
|
|
68
|
+
if [ -z "${RAILS_WORKTREES_BOOTSTRAPPED:-}" ] && command -v mise >/dev/null 2>&1; then
|
|
69
|
+
export MISE_CEILING_PATHS="$WT_MISE_CEILING_PATH"
|
|
70
|
+
export MISE_TRUSTED_CONFIG_PATHS="$WT_MISE_TRUST_ROOT"
|
|
60
71
|
export RAILS_WORKTREES_BOOTSTRAPPED=1
|
|
61
|
-
exec mise exec -- "$WT_SCRIPT_PATH" "$@"
|
|
72
|
+
exec mise exec -C "$WT_APP_ROOT" -- "$WT_SCRIPT_PATH" "$@"
|
|
62
73
|
fi
|
|
63
74
|
|
|
64
75
|
export BUNDLE_GEMFILE="${BUNDLE_GEMFILE:-$WT_APP_ROOT/Gemfile}"
|
|
@@ -69,7 +80,7 @@ def rails_worktrees_bootstrap_safe_invocation?(argv)
|
|
|
69
80
|
return true if %w[-h --help -v --version].include?(args.first)
|
|
70
81
|
return true if %w[--env --print-env].include?(args.first)
|
|
71
82
|
|
|
72
|
-
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) }
|
|
73
84
|
command = command_args.first
|
|
74
85
|
|
|
75
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
|
|
@@ -214,7 +226,8 @@ module Rails
|
|
|
214
226
|
validate_update_args!
|
|
215
227
|
announce_dry_run if dry_run?
|
|
216
228
|
|
|
217
|
-
|
|
229
|
+
repository_root = resolve_repository_context[:current_root]
|
|
230
|
+
current_root = update_target_root(repository_root)
|
|
218
231
|
report = project_maintenance_report(current_root)
|
|
219
232
|
updated_count = 0
|
|
220
233
|
identical_count = 0
|
|
@@ -258,11 +271,25 @@ module Rails
|
|
|
258
271
|
end
|
|
259
272
|
|
|
260
273
|
def validate_update_args!
|
|
261
|
-
raise Error, 'Usage: wt update [--dry-run]' unless @argv.length
|
|
274
|
+
raise Error, 'Usage: wt update [--dry-run] [--pi] [path]' unless [1, 2].include?(@argv.length)
|
|
262
275
|
raise Error, 'The --force flag is only supported with wt remove.' if force?
|
|
263
276
|
raise Error, 'The --skip-setup flag is only supported when creating a worktree.' if skip_setup?
|
|
264
277
|
end
|
|
265
278
|
|
|
279
|
+
def update_target_root(repository_root)
|
|
280
|
+
return repository_root unless @argv[1]
|
|
281
|
+
|
|
282
|
+
target_root = canonical_path(File.expand_path(@argv[1], @cwd))
|
|
283
|
+
raise Error, "Update target does not exist: #{target_root}" unless File.directory?(target_root)
|
|
284
|
+
|
|
285
|
+
assert_within_root!(
|
|
286
|
+
target_root,
|
|
287
|
+
repository_root,
|
|
288
|
+
"Refusing to update a path outside the current Git repository: #{target_root}"
|
|
289
|
+
)
|
|
290
|
+
target_root
|
|
291
|
+
end
|
|
292
|
+
|
|
266
293
|
def announce_prune_candidates(candidates)
|
|
267
294
|
info("Found #{candidates.length} merged worktree#{'s' unless candidates.length == 1} created by wt:")
|
|
268
295
|
end
|
|
@@ -623,7 +650,7 @@ module Rails
|
|
|
623
650
|
end
|
|
624
651
|
|
|
625
652
|
def project_maintenance_report(root)
|
|
626
|
-
::Rails::Worktrees::ProjectMaintenance.new(root: root).call
|
|
653
|
+
::Rails::Worktrees::ProjectMaintenance.new(root: root, pi_worktree: pi?).call
|
|
627
654
|
end
|
|
628
655
|
|
|
629
656
|
def doctor_worktree_checks(repository)
|
|
@@ -7,7 +7,7 @@ module Rails
|
|
|
7
7
|
class MiseEnvironment
|
|
8
8
|
Result = Struct.new(:env, :messages)
|
|
9
9
|
|
|
10
|
-
CONFIG_FILES = %w[mise.toml .mise.toml].freeze
|
|
10
|
+
CONFIG_FILES = %w[mise.toml .mise.toml mise.local.toml .mise.local.toml].freeze
|
|
11
11
|
|
|
12
12
|
def initialize(target_dir:, env:)
|
|
13
13
|
@target_dir = target_dir
|
|
@@ -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
|
|
@@ -164,18 +164,66 @@ module Rails
|
|
|
164
164
|
attr_reader :runtime_env
|
|
165
165
|
|
|
166
166
|
def resolved_runtime_env(dry_run:)
|
|
167
|
-
env = shell_env.merge(@bootstrapped_env)
|
|
167
|
+
env = shell_env.merge(@bootstrapped_env).merge(target_mise_scope_env)
|
|
168
168
|
return env if dry_run
|
|
169
169
|
|
|
170
|
-
env.merge(toolchain_env)
|
|
170
|
+
env.merge(toolchain_env(env))
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
-
def toolchain_env
|
|
174
|
-
result = MiseEnvironment.new(target_dir: @target_dir, env:
|
|
173
|
+
def toolchain_env(env)
|
|
174
|
+
result = MiseEnvironment.new(target_dir: @target_dir, env: env).call
|
|
175
175
|
result.messages.each { |message| info(message) }
|
|
176
176
|
result.env
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
+
def target_mise_scope_env
|
|
180
|
+
trust_root = target_git_root || canonical_target_dir
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
'MISE_CEILING_PATHS' => File.dirname(trust_root),
|
|
184
|
+
'MISE_TRUSTED_CONFIG_PATHS' => trust_root
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def target_git_root
|
|
189
|
+
stdout_str, status = capture_target_git_root
|
|
190
|
+
return nil unless status.success?
|
|
191
|
+
|
|
192
|
+
root = File.realpath(stdout_str.strip)
|
|
193
|
+
target_within_root?(root) ? root : nil
|
|
194
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def capture_target_git_root
|
|
199
|
+
stdout_str, _stderr_str, status = Open3.capture3(
|
|
200
|
+
git_discovery_env,
|
|
201
|
+
'git', '-C', @target_dir, 'rev-parse', '--show-toplevel'
|
|
202
|
+
)
|
|
203
|
+
[stdout_str, status]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def git_discovery_env
|
|
207
|
+
shell_env.merge(
|
|
208
|
+
'GIT_DIR' => nil,
|
|
209
|
+
'GIT_WORK_TREE' => nil,
|
|
210
|
+
'GIT_COMMON_DIR' => nil,
|
|
211
|
+
'GIT_INDEX_FILE' => nil,
|
|
212
|
+
'GIT_OBJECT_DIRECTORY' => nil,
|
|
213
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES' => nil
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def target_within_root?(root)
|
|
218
|
+
canonical_target_dir.start_with?("#{root}#{File::SEPARATOR}") || canonical_target_dir == root
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def canonical_target_dir
|
|
222
|
+
@canonical_target_dir ||= File.realpath(@target_dir)
|
|
223
|
+
rescue Errno::ENOENT
|
|
224
|
+
File.expand_path(@target_dir)
|
|
225
|
+
end
|
|
226
|
+
|
|
179
227
|
def yarn_lock_present?
|
|
180
228
|
File.exist?(File.join(@target_dir, 'yarn.lock'))
|
|
181
229
|
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
|