rails-worktrees 0.7.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eed6634afda26cca90bd9f6e2540a7ace8f865ec53f97a283fc873e137cffe8
4
- data.tar.gz: 6a2541a2a026af4ed0ccd8e14f3d1ceac78e0da7c5e4d1fc01c1fb6d4b34e0ad
3
+ metadata.gz: 6f0667430464dedbea994c715a32e64f226048502f6037186f3c90692c5df6e5
4
+ data.tar.gz: 1f47027f6cdc16431356c43e4809d801a7537db1558d3ab9b25c1299874d1389
5
5
  SHA512:
6
- metadata.gz: 0a8cf44f39ec11d98faa0d7e3b18badb5b1c4a8dcb3c31c67a0f26d4442656554cb5658275ccc96df69364d5a46572ab2408f57ea08deb31a0568bf284daf2da
7
- data.tar.gz: 588ba219467bee9baa350d06d44a8eaf56fc1eab7e361bb1ff233cd7b9efa1e0e7041ad9f96d90d1bd059ae378789388caf0d4b7c2556cd29b6f7c28cfc63c07
6
+ metadata.gz: 81a835017246e1cab5fd6acf6ef311b4f9beef54912675602fd454c8ed2aa56904b514be908f69cdc2b84110ec8c38d9d49c9abbc974eea1b309aec2fed3c3f9
7
+ data.tar.gz: 5aad32825be11a64a0095233ed4068eda727da0af5ca0456657e5a4f0aa33f8aa04c0a6a48929a0e7abe97cbbbff9197aa49845c5b6a4acdf207d6c48e8e0796
@@ -1 +1 @@
1
- {".":"0.7.4"}
1
+ {".":"0.9.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/asjer/rails-worktrees/compare/v0.8.0...v0.9.0) (2026-11-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * **config:** support worktrees inside the repo via worktrees_dir ([#61](https://github.com/asjer/rails-worktrees/issues/61)) ([681f325](https://github.com/asjer/rails-worktrees/commit/681f325584b69fc675f7fce045c5cd8baf36eb83))
9
+ * **setup:** install Playwright Chromium for the pinned version ([#66](https://github.com/asjer/rails-worktrees/issues/66)) ([cdbef7c](https://github.com/asjer/rails-worktrees/commit/cdbef7c24f738674dd209af35da25d4f85e038ed))
10
+ * **wt:** add guarded worktree teardown with dry-run ([#63](https://github.com/asjer/rails-worktrees/issues/63)) ([5592830](https://github.com/asjer/rails-worktrees/commit/5592830849b098e0d904d70cf96390dc55083356))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **wt:** reserve help and version instead of using them as names ([#60](https://github.com/asjer/rails-worktrees/issues/60)) ([d52dba8](https://github.com/asjer/rails-worktrees/commit/d52dba83525ef21f11840db0b691174ccf896a33))
16
+
17
+ ## [0.8.0](https://github.com/asjer/rails-worktrees/compare/v0.7.4...v0.8.0) (2026-10-24)
18
+
19
+
20
+ ### Features
21
+
22
+ * **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)
23
+
3
24
  ## [0.7.4](https://github.com/asjer/rails-worktrees/compare/v0.7.3...v0.7.4) (2026-08-15)
4
25
 
5
26
 
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,10 +54,15 @@ 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
54
- bin/wt remove my-feature # remove a worktree and delete its local branch
57
+ bin/wt update --pi # also add the pi-worktree hook in .pi/worktree.json
58
+ bin/wt teardown --dry-run # preview this worktree's Overmind and development/test databases
59
+ bin/wt teardown # stop Overmind and drop worktree databases; keep checkout/branch
60
+ bin/wt remove my-feature # teardown a worktree, remove it and delete its local branch
55
61
  bin/wt delete my-feature # alias for `bin/wt remove`
56
62
  bin/wt remove --force my-feature # also delete an unmerged local branch
57
63
  bin/wt prune # remove merged worktrees created by wt
64
+ bin/wt help # same as --help; never creates a worktree
65
+ bin/wt version # same as --version
58
66
 
59
67
  bin/ob # open http://localhost:$DEV_PORT/
60
68
  bin/ob contact # open http://localhost:$DEV_PORT/contact
@@ -66,12 +74,13 @@ bin/ob --print-url '?from=nav' # print the resolved URL without opening a brows
66
74
 
67
75
  | Flag | Description |
68
76
  |------|-------------|
69
- | `-h`, `--help` | Show the help message |
70
- | `-v`, `--version` | Show the script version |
77
+ | `-h`, `--help`, `help` | Show the help message |
78
+ | `-v`, `--version`, `version` | Show the script version |
71
79
  | `--dry-run [name]` | Preview worktree creation or cleanup without changing anything |
72
80
  | `--skip-setup` | Create a worktree without running setup steps |
73
81
  | `--force` | Force branch deletion for `bin/wt remove` / `bin/wt delete` |
74
82
  | `--env`, `--print-env <name>` | Preview `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX` |
83
+ | `--pi` | With `bin/wt update`, create or merge `.pi/worktree.json` for the pi-worktree extension |
75
84
 
76
85
  ### Default behavior
77
86
 
@@ -83,7 +92,7 @@ By default `bin/wt`:
83
92
  - auto-picks names from bundled `.txt` files when no explicit name is given
84
93
  - retires bundled names so they are not picked twice
85
94
  - bootstraps a worktree-local `.env` with deterministic `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX` values
86
- - runs setup automatically after creation: credential linking, `bundle install`, `yarn install` when applicable, both `db:prepare` steps, test asset precompile, and a final `bin/rails assets:clobber`
95
+ - runs setup automatically after creation: credential linking, `bundle install`, `yarn install` when applicable, the Playwright Chromium build for the pinned `playwright` package when it is missing, both `db:prepare` steps, test asset precompile, and a final `bin/rails assets:clobber`
87
96
 
88
97
  ```text
89
98
  workspace/
@@ -96,6 +105,32 @@ workspace/
96
105
 
97
106
  `WT_WORKSPACES_ROOT` or `config.workspace_root` overrides the destination root and uses the layout `<root>/<project>/<name>`.
98
107
 
108
+ ### Worktrees inside the repository
109
+
110
+ Set `config.worktrees_dir` to keep worktrees inside the main checkout instead, in `<main checkout>/<worktrees_dir>/<name>`. This is the layout the [pi-worktree](https://www.npmjs.com/package/pi-worktree) extension uses, so `bin/wt` and Pi share one place for worktrees:
111
+
112
+ ```ruby
113
+ Rails.application.config.x.rails_worktrees.tap do |config|
114
+ config.worktrees_dir = '.worktrees'
115
+ config.branch_prefix = 'worktree' # wt and pi-worktree both create worktree/<name>
116
+ end
117
+ ```
118
+
119
+ ```text
120
+ my-project/
121
+ ├── app/
122
+ ├── config/
123
+ └── .worktrees/
124
+ ├── feature-auth/
125
+ └── bugfix-123/
126
+ ```
127
+
128
+ - `worktrees_dir` must be a relative path inside the repository, such as `.worktrees` or `tmp/worktrees`. It always resolves against the main checkout, also when you run `bin/wt` from inside a worktree.
129
+ - If `.gitignore` does not already ignore the directory, `bin/wt` adds `/.worktrees/` to `.git/info/exclude` when it creates a worktree, so `git status` in the main checkout stays clean. When it cannot write that file it prints a warning; add the directory to `.gitignore` yourself.
130
+ - `bin/wt remove`, `bin/wt prune` and `bin/wt doctor` work in this layout too. `bin/wt doctor` warns when the directory is not ignored.
131
+ - `worktrees_dir` cannot be combined with `workspace_root`. `WT_WORKSPACES_ROOT` still overrides both for a single run.
132
+ - Tools that scan the main checkout's file tree (for example RuboCop, file watchers or Tailwind content globs) can pick up files from the worktrees. Exclude the directory there if needed.
133
+
99
134
  ### Setup command
100
135
 
101
136
  `bin/wt setup` reruns setup for the **current checkout**. Run it from inside a linked worktree created by `bin/wt`, a worktree created manually with `git worktree`, or a checkout prepared by another tool.
@@ -126,7 +161,8 @@ bin/wt setup
126
161
 
127
162
  `bin/wt` also supports cleanup commands for worktrees it manages:
128
163
 
129
- - `bin/wt remove <name>` — remove the named worktree and delete its local branch
164
+ - `bin/wt teardown` — stop the current worktree's Overmind and drop its development/test databases, keeping the checkout and branch
165
+ - `bin/wt remove <name>` — run the same teardown, remove the named worktree and delete its local branch
130
166
  - `bin/wt delete <name>` — alias for `bin/wt remove <name>`
131
167
  - `bin/wt prune` — remove merged worktrees created by `bin/wt` in bulk
132
168
 
@@ -136,7 +172,17 @@ bin/wt setup
136
172
 
137
173
  `bin/wt prune` only targets linked worktrees created by `bin/wt` whose local branches are already merged into `origin`'s default branch. It skips the main checkout, skips the checkout you're currently in, and asks for confirmation before deleting the batch.
138
174
 
139
- If you want to preview a single removal first, use `--dry-run` with `bin/wt remove`:
175
+ ### Teardown command
176
+
177
+ Run `bin/wt teardown` **inside a linked worktree** (including manually created or Pi worktrees). It boots that app separately in test and development (so environment-specific files and database URLs are resolved correctly), checks **all** task-enabled development/test database configurations and Rails' protected-environment metadata, stops Overmind with `overmind quit --socket .overmind.sock` if that local socket exists, and drops only those validated databases through Rails' database tasks. Production databases and the checkout/branch are left alone. Multi-database configurations (primary, cache, queue, cable, etc.) are supported; replicas and configurations with `database_tasks: false` are not dropped.
178
+
179
+ Teardown refuses the main checkout, a missing/blank `WORKTREE_DATABASE_SUFFIX`, and any database whose name does not contain that suffix. For SQLite, the suffix must be in the filename, not merely a parent directory. **All names are validated before Overmind or any database is touched.** A working app bundle for both environments and `bin/rails` are required; install missing gems or fix unsafe database configuration before retrying. Both environments are booted even for a dry-run, so application initializers still execute. Rails' production/environment-mismatch protection cannot be bypassed with `DISABLE_DATABASE_ENVIRONMENT_CHECK`. The resolved test configuration is passed privately to the development process; all safety checks complete before Overmind is stopped or any database is dropped.
180
+
181
+ The worktree's `.env` may define a literal `WORKTREE_DATABASE_SUFFIX` (unquoted or single/double quoted, optionally with `export`). Teardown never generates a suffix. When run inside the worktree it can also use the current environment if `.env` does not define one. `remove` reads the **target's** `.env` and starts Rails with only the caller's OS/Ruby runtime variables, never its suffix, database URLs, application settings or bundle. Any other required application settings/credentials must therefore be loadable by the target app itself (for example with dotenv or Rails credentials). Ensure that target file contains the actual suffix used to create its databases.
182
+
183
+ `bin/wt remove` / `delete` run this same teardown **before** deleting the worktree or branch. If validation, Overmind shutdown, or a database drop fails, removal aborts and keeps the checkout and branch. Database drops are not transactional: databases already dropped before a later failure cannot be restored by this command. If only the branch remains (the worktree directory is already gone), removal only deletes the branch. `prune` retains its existing Git-only behavior; run teardown on its candidates first if they have databases.
184
+
185
+ Use `bin/wt teardown --dry-run` to list the local Overmind socket and exact development/test database names without stopping or dropping them. To preview both teardown and checkout/branch removal, use `--dry-run` with `bin/wt remove`:
140
186
 
141
187
  ```bash
142
188
  bin/wt remove --dry-run feature-auth
@@ -160,6 +206,53 @@ bin/wt prune --dry-run
160
206
 
161
207
  `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
208
 
209
+ `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`.
210
+
211
+ ### Pi worktrees (pi-worktree extension)
212
+
213
+ 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:
214
+
215
+ ```bash
216
+ bin/rails generate worktrees:install --pi
217
+ # or, in an app that already has rails-worktrees installed:
218
+ bin/wt update --pi
219
+ ```
220
+
221
+ Both commands create or update `.pi/worktree.json`:
222
+
223
+ ```json
224
+ {
225
+ "linkEnvFiles": false,
226
+ "postCreate": [
227
+ "bin/wt setup"
228
+ ]
229
+ }
230
+ ```
231
+
232
+ `.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. Set `config.worktrees_dir = '.worktrees'` and `config.branch_prefix = 'worktree'` to have `bin/wt` use the same directory and branch names; see [Worktrees inside the repository](#worktrees-inside-the-repository).
233
+
234
+ `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.
235
+
236
+ Existing configuration is merged, not replaced:
237
+
238
+ - other keys (`dir`, `branchPrefix`, `preRemove`, …) and custom `postCreate` hooks are preserved; `bin/wt setup` is appended once and never duplicated
239
+ - `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
240
+ - no `preRemove` hook is added automatically, and the worktree directory and branch prefix stay under your control
241
+
242
+ To opt into environment cleanup when Pi's pi-worktree extension destroys a worktree, add `bin/wt teardown` to `preRemove` (preserve any existing hooks):
243
+
244
+ ```json
245
+ {
246
+ "linkEnvFiles": false,
247
+ "postCreate": ["bin/wt setup"],
248
+ "preRemove": ["bin/wt teardown"]
249
+ }
250
+ ```
251
+
252
+ The extension runs `preRemove` from inside the worktree. Teardown only cleans the environment; the calling tool remains responsible for removing the checkout and branch. Keep `linkEnvFiles: false` so a worktree does not share the main checkout's `.env`. Preview the cleanup inside the worktree with `bin/wt teardown --dry-run` first.
253
+
254
+ 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.
255
+
163
256
  ### Updating old `bin/wt` wrappers
164
257
 
165
258
  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.
@@ -207,6 +300,8 @@ New installs and repaired apps use a bash bootstrap wrapper that activates mise
207
300
 
208
301
  Worktree names must not contain `/` or whitespace, must not be `.` or `..`, and must be a valid Git ref component.
209
302
 
303
+ These reserved words are commands, so `bin/wt <word>` never creates or reuses a worktree with that name and auto-picking skips them: `help`, `version`, `doctor`, `setup`, `update`, `prune`, `remove` and `delete`. `bin/wt help` and `bin/wt version` behave like `--help` and `--version`. An existing worktree with such a name (for example one created by `bin/wt help` in an older version) can still be cleaned up with `bin/wt remove help` or set up with `bin/wt setup ../<project>.worktrees/help`.
304
+
210
305
  ### Configuration
211
306
 
212
307
  The installer generates `config/initializers/rails_worktrees.rb` where you can override:
@@ -217,6 +312,7 @@ The initializer stays app-owned and gem-agnostic; `rails-worktrees` reads those
217
312
  |--------|---------|-------------|
218
313
  | `bootstrap_env` | `true` | Write `.env` when creating a worktree |
219
314
  | `workspace_root` | `nil` | Override the destination root (sibling layout when `nil`) |
315
+ | `worktrees_dir` | `nil` | Create worktrees inside the main checkout, e.g. `'.worktrees'`; see [Worktrees inside the repository](#worktrees-inside-the-repository) |
220
316
  | `dev_port_range` | `3000..3999` | Port range for deterministic `DEV_PORT` allocation |
221
317
  | `branch_prefix` | `🚂` | Prefix for worktree branch names |
222
318
  | `name_sources_path` | bundled `names/` | Directory containing `.txt` name lists |
@@ -77,14 +77,14 @@ exec ruby /dev/fd/3 "$@" 3<<'RUBY'
77
77
  def rails_worktrees_bootstrap_safe_invocation?(argv)
78
78
  args = argv.map(&:to_s)
79
79
  return true if args.empty?
80
- return true if %w[-h --help -v --version].include?(args.first)
80
+ return true if %w[-h --help help -v --version 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)
87
- return false if %w[remove delete prune].include?(command)
87
+ return false if %w[remove delete prune teardown].include?(command)
88
88
 
89
89
  command_args.length <= 1
90
90
  end
@@ -5,6 +5,13 @@ Rails.application.config.x.rails_worktrees.tap do |config|
5
5
  # By default, worktrees go in a sibling "<project>.worktrees" directory.
6
6
  # Uncomment to override with a custom parent directory that uses <root>/<project>/<name>.
7
7
  # config.workspace_root = File.expand_path('~/worktrees')
8
+ #
9
+ # Or keep worktrees inside this checkout in <app>/.worktrees/<name>, like Pi's pi-worktree
10
+ # extension. wt adds the directory to .git/info/exclude unless .gitignore already covers it.
11
+ # Combine with branch_prefix 'worktree' so wt and Pi share worktree/<name> branch names.
12
+ # Cannot be combined with workspace_root.
13
+ # config.worktrees_dir = '.worktrees'
14
+ # config.branch_prefix = 'worktree'
8
15
  <% end -%>
9
16
  # config.bootstrap_env = false
10
17
  # config.dev_port_range = 3000..3999
@@ -21,6 +28,7 @@ Rails.application.config.x.rails_worktrees.tap do |config|
21
28
  # config.post_create_command = nil # nil = built-in steps; false = skip all; 'script/worktree-setup' = custom command
22
29
  # config.run_bundle_install = true
23
30
  # config.run_yarn_install = true # auto-skipped when yarn.lock is absent
31
+ # config.run_playwright_install = true # installs Playwright Chromium when package.json uses playwright
24
32
  # config.run_db_prepare = true
25
33
  # config.run_test_db_prepare = true
26
34
  # config.run_test_assets_precompile = true
@@ -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
@@ -2,8 +2,10 @@ module Rails
2
2
  module Worktrees
3
3
  # Shell entrypoint for the wt executable.
4
4
  class CLI
5
- LOADER_OPTIONAL_COMMANDS = %w[doctor update -h --help -v --version].freeze
6
- LOADER_IGNORED_FLAGS = %w[--dry-run --force --skip-setup].freeze
5
+ LOADER_OPTIONAL_COMMANDS = %w[doctor update -h --help help -v --version version].freeze
6
+ # Commands that use project configuration when it loads, but must keep working when it does not.
7
+ LOADER_BEST_EFFORT_COMMANDS = %w[doctor].freeze
8
+ LOADER_IGNORED_FLAGS = %w[--dry-run --force --skip-setup --pi].freeze
7
9
  SETUP_SUBCOMMAND = 'setup'.freeze
8
10
 
9
11
  def initialize(
@@ -20,7 +22,11 @@ module Rails
20
22
 
21
23
  def start
22
24
  configuration = ::Rails::Worktrees.configuration
23
- load_project_configuration(configuration) if should_load_project_configuration?
25
+ if should_load_project_configuration?
26
+ load_project_configuration(configuration)
27
+ elsif best_effort_configuration_command?
28
+ load_project_configuration_best_effort(configuration)
29
+ end
24
30
  command_for(configuration).run
25
31
  rescue ::Rails::Worktrees::Error => e
26
32
  @io.fetch(:stderr).puts("Error: #{e.message}")
@@ -35,6 +41,17 @@ module Rails
35
41
  raise ::Rails::Worktrees::Error, "Failed to load worktrees configuration: #{e.class}: #{e.message}"
36
42
  end
37
43
 
44
+ # A broken initializer must not block recovery; wt doctor then reports with default settings.
45
+ def load_project_configuration_best_effort(configuration)
46
+ load_project_configuration(configuration)
47
+ rescue ::Rails::Worktrees::Error
48
+ configuration
49
+ end
50
+
51
+ def best_effort_configuration_command?
52
+ LOADER_BEST_EFFORT_COMMANDS.include?(argv_without_flags.first)
53
+ end
54
+
38
55
  def configuration_root
39
56
  return expand_setup_target_path if explicit_setup_path_target?
40
57
 
@@ -0,0 +1,85 @@
1
+ require 'fileutils'
2
+
3
+ module Rails
4
+ module Worktrees
5
+ class Command
6
+ # Keeps an in-repo worktrees_dir out of the main checkout's untracked files.
7
+ module InRepoWorktrees
8
+ private
9
+
10
+ def in_repo_layout?(workspaces) = workspaces[:layout] == :in_repo
11
+
12
+ def ensure_worktrees_dir_ignored(context)
13
+ workspaces = context[:workspaces]
14
+ return unless in_repo_layout?(workspaces)
15
+ return if worktrees_dir_ignored?(context[:primary_root], workspaces)
16
+
17
+ entry = worktrees_dir_exclude_entry(workspaces)
18
+ exclude_path = info_exclude_path(context[:primary_root])
19
+ return info("Would add '#{entry}' to #{exclude_path}") if dry_run?
20
+
21
+ append_exclude_entry(exclude_path, entry)
22
+ end
23
+
24
+ def worktrees_dir_ignored?(primary_root, workspaces)
25
+ git_success?('-C', primary_root, 'check-ignore', '-q', "#{workspaces[:relative_dir]}/")
26
+ end
27
+
28
+ # Escapes gitignore metacharacters so the entry matches only the literal directory.
29
+ def worktrees_dir_exclude_entry(workspaces)
30
+ escaped = workspaces[:relative_dir]
31
+ .gsub(/[\\*?\[\]]/) { |char| "\\#{char}" }
32
+ .sub(/ +\z/) { |spaces| spaces.gsub(' ', '\ ') }
33
+ "/#{escaped}/"
34
+ end
35
+
36
+ def info_exclude_path(primary_root)
37
+ git_path = git_capture('-C', primary_root, 'rev-parse', '--git-path', 'info/exclude').strip
38
+ expand_git_path(git_path, base_dir: primary_root)
39
+ end
40
+
41
+ def append_exclude_entry(exclude_path, entry)
42
+ FileUtils.mkdir_p(File.dirname(exclude_path))
43
+ separator = missing_trailing_newline?(exclude_path) ? "\n" : ''
44
+ File.open(exclude_path, 'a') { |file| file.write("#{separator}#{entry}\n") }
45
+ info("Added '#{entry}' to #{exclude_path} so the main checkout ignores in-repo worktrees")
46
+ rescue SystemCallError => e
47
+ warning(
48
+ "Could not update #{exclude_path} (#{e.class}); add '#{entry}' to .gitignore " \
49
+ 'so the main checkout does not show the worktrees as untracked files.'
50
+ )
51
+ end
52
+
53
+ def missing_trailing_newline?(path)
54
+ File.file?(path) && !File.empty?(path) && !File.read(path).end_with?("\n")
55
+ end
56
+
57
+ def in_repo_worktrees_doctor_check(repository)
58
+ workspaces = repository[:workspaces]
59
+ return unless in_repo_layout?(workspaces)
60
+
61
+ if worktrees_dir_ignored?(repository[:primary_root], workspaces)
62
+ ignored_worktrees_dir_check(workspaces)
63
+ else
64
+ unignored_worktrees_dir_check(workspaces)
65
+ end
66
+ end
67
+
68
+ def ignored_worktrees_dir_check(workspaces)
69
+ doctor_check(category: :worktree, status: :ok,
70
+ headline: "In-repo worktrees directory #{workspaces[:root]} is ignored by Git.")
71
+ end
72
+
73
+ def unignored_worktrees_dir_check(workspaces)
74
+ doctor_check(
75
+ category: :worktree,
76
+ status: :warning,
77
+ headline: "In-repo worktrees directory #{workspaces[:root]} is not ignored by Git.",
78
+ messages: ["Add '#{worktrees_dir_exclude_entry(workspaces)}' to .gitignore, " \
79
+ 'or create a worktree with wt to add it to .git/info/exclude.']
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -45,7 +45,8 @@ module Rails
45
45
 
46
46
  def list_available_names(source_file, project_name, workspaces)
47
47
  source_names(source_file).select do |candidate|
48
- valid_auto_name?(candidate) && available_for_auto_pick?(candidate, project_name, workspaces)
48
+ !RESERVED_WORDS.include?(candidate) && valid_auto_name?(candidate) &&
49
+ available_for_auto_pick?(candidate, project_name, workspaces)
49
50
  end
50
51
  end
51
52
 
@@ -8,10 +8,10 @@ module Rails
8
8
 
9
9
  def handle_meta_command
10
10
  case @argv.first
11
- when '-h', '--help'
11
+ when '-h', '--help', 'help'
12
12
  @stdout.print(usage)
13
13
  0
14
- when '-v', '--version'
14
+ when '-v', '--version', 'version'
15
15
  @stdout.puts("wt #{Rails::Worktrees::VERSION}")
16
16
  0
17
17
  when '--env', '--print-env'
@@ -30,23 +30,26 @@ module Rails
30
30
  Create and clean up Git worktrees for the current repository.
31
31
 
32
32
  Usage: wt [worktree-name]
33
+ wt help | wt version
33
34
  wt [--skip-setup] [worktree-name]
34
35
  wt --dry-run [worktree-name]
35
36
  wt --print-env <worktree-name>
36
37
  wt doctor
37
38
  wt setup [--dry-run] [path|name]
38
- wt update [--dry-run]
39
+ wt update [--dry-run] [--pi]
40
+ wt teardown [--dry-run]
39
41
  wt remove [--dry-run] [--force] <worktree-name>
40
42
  wt delete [--dry-run] [--force] <worktree-name>
41
43
  wt prune [--dry-run]
42
44
 
43
45
  Options:
44
- -h, --help Show this help message
45
- -v, --version Show the script version
46
+ -h, --help, help Show this help message
47
+ -v, --version, version Show the script version
46
48
  --dry-run [name] Preview worktree creation or cleanup without changing anything
47
49
  --skip-setup Create the worktree without running setup steps
48
50
  --force Delete an unmerged local branch with wt remove/delete
49
51
  --env, --print-env <name> Preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
52
+ --pi With wt update, add the pi-worktree bin/wt setup hook
50
53
 
51
54
  Quick start:
52
55
  wt Auto-pick a name from a bundled *.txt list
@@ -59,6 +62,8 @@ module Rails
59
62
  wt setup my-feature
60
63
  wt setup ../my-project.worktrees/my-feature
61
64
  wt update --dry-run
65
+ wt teardown --dry-run
66
+ wt teardown
62
67
  wt remove my-feature
63
68
  wt remove --force my-feature
64
69
  wt prune
@@ -66,12 +71,17 @@ module Rails
66
71
  How it works:
67
72
  - by default creates worktrees beside the repo in ../<project>.worktrees/<name>
68
73
  - when workspace_root or WT_WORKSPACES_ROOT is set, creates worktrees in <root>/<project>/<name>
74
+ - when worktrees_dir is set (for example '.worktrees'), creates worktrees inside the main checkout in <repo>/<worktrees_dir>/<name>
75
+ - reserved words are commands, never new worktree names: #{RESERVED_WORDS.join(', ')}
69
76
  - always uses the branch name #{@configuration.branch_prefix}/<name>
70
77
  - bases new branches on the repository's origin default branch
71
78
  - by default wt <name> both creates the worktree and runs setup automatically
72
79
  - wt setup reruns setup for the current checkout, a managed worktree name, or a specific checkout path, including manually-created worktrees
73
80
  - wt doctor audits install/config drift plus basic worktree health without changing files
74
81
  - wt update applies safe file-based fixes for managed installer artifacts and config hints
82
+ - wt update --pi also writes .pi/worktree.json for the pi-worktree extension (not Pi core)
83
+ - wt teardown stops local Overmind and drops suffix-validated development/test databases, keeping the checkout
84
+ - wt remove/delete run teardown before removing the checkout and local branch
75
85
  - wt remove/delete can run from the main checkout or any sibling worktree, but never remove the worktree you're currently in
76
86
  - wt prune removes merged worktrees created by wt while skipping the main checkout and the checkout you're in
77
87
  - auto-discovers bundled *.txt files from #{@configuration.name_sources_path}