rails-worktrees 0.4.0 → 0.5.1

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: 46dd1369d93b3281d8182a90812164a1a245ff778b5af05306908989563e264e
4
- data.tar.gz: 0d7e33ae49a56ea819e3a07ebfde966e1b1c18677af8c406bd15e93628ba8aff
3
+ metadata.gz: 698795e04a67284039d3beba2286f3a9aa156a5856832a345f22ffa57b6177e3
4
+ data.tar.gz: 0a57ef25c66c3226798498476abf8caf033c6a2989ae170b30a161fde66fddcf
5
5
  SHA512:
6
- metadata.gz: 3d9e25b8d52ec84eb983d5b5ba28679394396232f61b8e9b626cf8a8f464b49e53d9f1898d0591bcd78e66620fa2b59869b1c781ed03b7a8a5f176f973a2ebbb
7
- data.tar.gz: 795403173d6b6c8c375119b7427bebc620cc73deb9402a8c1c6aa25c863b703af882b4e4b01dd9bdbd123696f1949a1e00f830a92a227e93ddff3e09a1c763ee
6
+ metadata.gz: 06c8ae396186a19c08e45445e031a28198d48f7ea5125c02e63b2df39977f5399f75421e7f1897071d14b00d042051616ed4e447b83b570852d54756d8689377
7
+ data.tar.gz: 800a19ef60af420e03280b8a30c02c9012c4ded0d3eaecc421ed5e42e072b8024a0cbc93e7ba7aaf7d806dc972deacacbb0c5edfe45da40b2ab05659b1ccc5c5
@@ -1 +1 @@
1
- {".":"0.4.0"}
1
+ {".":"0.5.1"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1](https://github.com/asjer/rails-worktrees/compare/v0.5.0...v0.5.1) (2026-04-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **initializer:** simplify generated config wiring ([bd7fd50](https://github.com/asjer/rails-worktrees/commit/bd7fd506f2c0106a0bd188e36b31329a9de2acbf))
9
+
10
+ ## [0.5.0](https://github.com/asjer/rails-worktrees/compare/v0.4.0...v0.5.0) (2026-04-02)
11
+
12
+
13
+ ### Features
14
+
15
+ * **maintenance:** add doctor and update maintenance commands ([fce44f7](https://github.com/asjer/rails-worktrees/commit/fce44f757ce9835eb663010f30a192c23f82080c))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **install:** support development-only gem installs ([bf9e742](https://github.com/asjer/rails-worktrees/commit/bf9e74260a88ba37674a5318b48ad5c93f1c0399))
21
+
3
22
  ## [0.4.0](https://github.com/asjer/rails-worktrees/compare/v0.3.0...v0.4.0) (2026-03-30)
4
23
 
5
24
 
@@ -52,12 +71,3 @@
52
71
  ### Features
53
72
 
54
73
  * add wt command and rails installer ([5d798d5](https://github.com/asjer/rails-worktrees/commit/5d798d5129585331780f0259b39061194feb66e3))
55
-
56
- ## [Unreleased]
57
-
58
- - Add a gem-managed `wt` CLI for creating Rails worktrees.
59
- - Add an optional gem-managed `ob` CLI plus generated `bin/ob` wrapper for opening `localhost:$DEV_PORT` routes.
60
- - Add a Rails installer generator that creates `bin/wt` and `config/initializers/rails_worktrees.rb`.
61
- - Add conservative `config/database.yml` patching for common development/test database names.
62
- - Add a manual-dispatch GitHub Actions workflow for the disposable Rails smoke test.
63
- - Add smoke-test workflow debug controls for retained artifacts and verbose output.
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ## Installation
12
12
 
13
13
  ```bash
14
- bundle add rails-worktrees
14
+ bundle add rails-worktrees --group development
15
15
  bin/rails generate worktrees:install
16
16
  # or, to also generate bin/ob without the yolo follow-ups:
17
17
  bin/rails generate worktrees:install --browser
@@ -19,6 +19,8 @@ bin/rails generate worktrees:install --browser
19
19
  bin/rails generate worktrees:install --yolo
20
20
  ```
21
21
 
22
+ 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
+
22
24
  The installer adds:
23
25
 
24
26
  - `bin/wt` — a thin wrapper that executes the gem-owned CLI
@@ -42,6 +44,9 @@ bin/wt # auto-pick a name from bundled *.txt lists
42
44
  bin/wt my-feature # use an explicit worktree name
43
45
  bin/wt --dry-run my-feature # preview the full setup without changing anything
44
46
  bin/wt --print-env my-feature # preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
47
+ bin/wt doctor # audit install/config drift and basic worktree health
48
+ bin/wt update --dry-run # preview safe maintenance fixes
49
+ bin/wt update # apply safe maintenance fixes for managed files
45
50
  bin/wt remove my-feature # remove a worktree and delete its local branch
46
51
  bin/wt delete my-feature # alias for `bin/wt remove`
47
52
  bin/wt remove --force my-feature # also delete an unmerged local branch
@@ -111,6 +116,18 @@ If you want to see what `bin/wt prune` would clean up before saying yes, use `--
111
116
  bin/wt prune --dry-run
112
117
  ```
113
118
 
119
+ ### Maintenance commands
120
+
121
+ `bin/wt` also includes a small maintenance surface for installer drift and checkout health:
122
+
123
+ - `bin/wt doctor` — audit managed files plus basic Git/worktree health without changing anything
124
+ - `bin/wt update --dry-run` — preview safe file-based fixes
125
+ - `bin/wt update` — apply safe file-based fixes for supported managed files
126
+
127
+ `bin/wt doctor` checks the generated initializer, `bin/wt`, optional `bin/ob`, supported config files such as `config/database.yml`, `Procfile.dev`, `config/puma.rb`, and `mise.toml`/`.mise.toml`, plus basic repository/worktree conditions like resolving `origin`'s default branch and spotting stale registered worktree paths.
128
+
129
+ `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.
130
+
114
131
 
115
132
  ### Interactive prompts
116
133
 
@@ -131,6 +148,8 @@ Worktree names must not contain `/` or whitespace, must not be `.` or `..`, and
131
148
 
132
149
  The installer generates `config/initializers/rails_worktrees.rb` where you can override:
133
150
 
151
+ The initializer stays app-owned and gem-agnostic; `rails-worktrees` reads those settings whenever the gem is present in the current bundle groups.
152
+
134
153
  | Option | Default | Description |
135
154
  |--------|---------|-------------|
136
155
  | `bootstrap_env` | `true` | Write `.env` when creating a worktree |
@@ -1,4 +1,4 @@
1
- Rails::Worktrees.configure do |config|
1
+ Rails.application.config.x.rails_worktrees.tap do |config|
2
2
  <% if options['conductor'] -%>
3
3
  config.workspace_root = <%= conductor_workspace_root %>
4
4
  <% else -%>
@@ -16,4 +16,4 @@ Rails::Worktrees.configure do |config|
16
16
  # 'rails-worktrees',
17
17
  # 'used-names.tsv'
18
18
  # )
19
- end
19
+ end
@@ -0,0 +1,36 @@
1
+ module Rails
2
+ module Worktrees
3
+ # Copies explicit app configuration values onto the runtime configuration object.
4
+ module ApplicationConfiguration
5
+ module_function
6
+
7
+ def apply(source, configuration:)
8
+ return configuration unless source
9
+
10
+ Configuration::CONFIGURABLE_ATTRIBUTES.each do |attribute|
11
+ next unless assigned?(source, attribute)
12
+
13
+ configuration.public_send("#{attribute}=", value_for(source, attribute))
14
+ end
15
+
16
+ configuration
17
+ end
18
+
19
+ def assigned?(source, attribute)
20
+ key = attribute.to_sym
21
+ hash = source.is_a?(Hash) ? source : source.to_h if source.respond_to?(:to_h)
22
+ return hash.key?(key) || hash.key?(attribute.to_s) if hash
23
+
24
+ source.respond_to?(:key?) && (source.key?(key) || source.key?(attribute.to_s))
25
+ end
26
+
27
+ def value_for(source, attribute)
28
+ key = attribute.to_sym
29
+ hash = source.is_a?(Hash) ? source : source.to_h if source.respond_to?(:to_h)
30
+ return hash.fetch(key) { hash[attribute.to_s] } if hash
31
+
32
+ source.public_send(attribute)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,6 +2,9 @@ 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].freeze
7
+
5
8
  def initialize(
6
9
  argv: ARGV,
7
10
  io: { stdin: $stdin, stdout: $stdout, stderr: $stderr },
@@ -15,13 +18,36 @@ module Rails
15
18
  end
16
19
 
17
20
  def start
18
- Command.new(
19
- argv: @argv,
20
- io: @io,
21
- env: @env,
22
- cwd: @cwd,
23
- configuration: ::Rails::Worktrees.configuration
24
- ).run
21
+ configuration = ::Rails::Worktrees.configuration
22
+ load_project_configuration(configuration) if should_load_project_configuration?
23
+ command_for(configuration).run
24
+ rescue ::Rails::Worktrees::Error => e
25
+ @io.fetch(:stderr).puts("Error: #{e.message}")
26
+ 1
27
+ end
28
+
29
+ private
30
+
31
+ def load_project_configuration(configuration)
32
+ ::Rails::Worktrees::ProjectConfigurationLoader.new(root: @cwd, configuration: configuration).call
33
+ rescue StandardError, ScriptError => e
34
+ raise ::Rails::Worktrees::Error, "Failed to load worktrees configuration: #{e.class}: #{e.message}"
35
+ end
36
+
37
+ def should_load_project_configuration?
38
+ argv_without_flags.empty? || !loader_optional_command?(argv_without_flags.first)
39
+ end
40
+
41
+ def argv_without_flags
42
+ @argv.reject { |arg| LOADER_IGNORED_FLAGS.include?(arg) }
43
+ end
44
+
45
+ def loader_optional_command?(command)
46
+ LOADER_OPTIONAL_COMMANDS.include?(command)
47
+ end
48
+
49
+ def command_for(configuration)
50
+ Command.new(argv: @argv, io: @io, env: @env, cwd: @cwd, configuration: configuration)
25
51
  end
26
52
  end
27
53
  end
@@ -32,6 +32,8 @@ module Rails
32
32
  Usage: wt [worktree-name]
33
33
  wt --dry-run [worktree-name]
34
34
  wt --print-env <worktree-name>
35
+ wt doctor
36
+ wt update [--dry-run]
35
37
  wt remove [--dry-run] [--force] <worktree-name>
36
38
  wt delete [--dry-run] [--force] <worktree-name>
37
39
  wt prune [--dry-run]
@@ -48,6 +50,8 @@ module Rails
48
50
  wt my-feature Use an explicit worktree name
49
51
  wt --dry-run my-feature
50
52
  wt --print-env my-feature
53
+ wt doctor
54
+ wt update --dry-run
51
55
  wt remove my-feature
52
56
  wt remove --force my-feature
53
57
  wt prune
@@ -57,6 +61,8 @@ module Rails
57
61
  - when workspace_root or WT_WORKSPACES_ROOT is set, creates worktrees in <root>/<project>/<name>
58
62
  - always uses the branch name #{@configuration.branch_prefix}/<name>
59
63
  - bases new branches on the repository's origin default branch
64
+ - wt doctor audits install/config drift plus basic worktree health without changing files
65
+ - wt update applies safe file-based fixes for managed installer artifacts and config hints
60
66
  - wt remove/delete can run from the main checkout or any sibling worktree, but never remove the worktree you're currently in
61
67
  - wt prune removes merged worktrees created by wt while skipping the main checkout and the checkout you're in
62
68
  - auto-discovers bundled *.txt files from #{@configuration.name_sources_path}
@@ -147,6 +153,29 @@ module Rails
147
153
  0
148
154
  end
149
155
 
156
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
157
+ def print_doctor_report(checks)
158
+ checks.each do |check|
159
+ printer = check.ok? ? :info : :warning
160
+ send(printer, "#{check.category}: #{check.headline}")
161
+ Array(check.messages).each { |message| info(message) }
162
+ end
163
+
164
+ fixable_count = checks.count(&:fixable?)
165
+ warning_count = checks.count(&:warning?)
166
+
167
+ if fixable_count.zero? && warning_count.zero?
168
+ success('Doctor found no issues.')
169
+ else
170
+ warning(
171
+ "Doctor found #{fixable_count} fixable issue#{'s' unless fixable_count == 1} and " \
172
+ "#{warning_count} warning#{'s' unless warning_count == 1}."
173
+ )
174
+ info('Run `wt update --dry-run` to preview safe fixes.') if fixable_count.positive?
175
+ end
176
+ end
177
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
178
+
150
179
  def warning(message)
151
180
  @stderr.puts("⚠️ #{message}")
152
181
  end
@@ -11,6 +11,8 @@ module Rails
11
11
  # rubocop:disable Metrics/ClassLength
12
12
  class Command
13
13
  REMOVE_SUBCOMMANDS = %w[remove delete].freeze
14
+ DOCTOR_SUBCOMMAND = 'doctor'.freeze
15
+ UPDATE_SUBCOMMAND = 'update'.freeze
14
16
 
15
17
  include GitOperations
16
18
  include EnvironmentSupport
@@ -69,7 +71,17 @@ module Rails
69
71
  @argv.first == 'prune'
70
72
  end
71
73
 
74
+ def doctor_subcommand?
75
+ @argv.first == DOCTOR_SUBCOMMAND
76
+ end
77
+
78
+ def update_subcommand?
79
+ @argv.first == UPDATE_SUBCOMMAND
80
+ end
81
+
72
82
  def execute_requested_command
83
+ return execute_doctor_command if doctor_subcommand?
84
+ return execute_update_command if update_subcommand?
73
85
  return execute_remove_command if remove_subcommand?
74
86
  return execute_prune_command if prune_subcommand?
75
87
  return usage_error if @argv.length > 1 || force?
@@ -144,11 +156,71 @@ module Rails
144
156
  0
145
157
  end
146
158
 
159
+ # rubocop:disable Metrics/MethodLength
160
+ def execute_doctor_command
161
+ validate_doctor_args!
162
+
163
+ unless git_success?('rev-parse', '--is-inside-work-tree')
164
+ checks = [doctor_check(category: :git, status: :warning,
165
+ headline: 'Run wt doctor from inside a Git repository.')]
166
+ print_doctor_report(checks)
167
+ return 1
168
+ end
169
+
170
+ repository = resolve_repository_context
171
+ checks = project_maintenance_report(repository[:current_root]).checks + doctor_worktree_checks(repository)
172
+ print_doctor_report(checks)
173
+
174
+ checks.any? { |check| check.fixable? || check.warning? } ? 1 : 0
175
+ end
176
+ # rubocop:enable Metrics/MethodLength
177
+
178
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
179
+ def execute_update_command
180
+ require_git_repo
181
+ validate_update_args!
182
+ announce_dry_run if dry_run?
183
+
184
+ current_root = resolve_repository_context[:current_root]
185
+ report = project_maintenance_report(current_root)
186
+ updated_count = 0
187
+ identical_count = 0
188
+ skipped_count = 0
189
+
190
+ report.checks.each do |check|
191
+ if check.fixable?
192
+ apply_maintenance_check(check, current_root: current_root)
193
+ updated_count += 1
194
+ elsif check.ok?
195
+ identical_count += 1
196
+ info(check.headline)
197
+ else
198
+ skipped_count += 1
199
+ warning(check.headline)
200
+ check.messages.each { |message| info(message) }
201
+ end
202
+ end
203
+
204
+ complete_update(updated_count:, identical_count:, skipped_count:)
205
+ end
206
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
207
+
147
208
  def validate_prune_args!
148
209
  raise Error, 'Usage: wt prune' unless @argv.length == 1
149
210
  raise Error, 'The --force flag is only supported with wt remove.' if force?
150
211
  end
151
212
 
213
+ def validate_doctor_args!
214
+ raise Error, 'Usage: wt doctor' unless @argv.length == 1
215
+ raise Error, 'wt doctor does not support --dry-run.' if dry_run?
216
+ raise Error, 'The --force flag is only supported with wt remove.' if force?
217
+ end
218
+
219
+ def validate_update_args!
220
+ raise Error, 'Usage: wt update [--dry-run]' unless @argv.length == 1
221
+ raise Error, 'The --force flag is only supported with wt remove.' if force?
222
+ end
223
+
152
224
  def announce_prune_candidates(candidates)
153
225
  info("Found #{candidates.length} merged worktree#{'s' unless candidates.length == 1} created by wt:")
154
226
  end
@@ -343,6 +415,145 @@ module Rails
343
415
  def worktree_name_for_branch(branch_name)
344
416
  branch_name.delete_prefix("#{@configuration.branch_prefix}/")
345
417
  end
418
+
419
+ def project_maintenance_report(root)
420
+ ::Rails::Worktrees::ProjectMaintenance.new(root: root).call
421
+ end
422
+
423
+ def doctor_worktree_checks(repository)
424
+ [
425
+ doctor_check(
426
+ category: :git,
427
+ status: :ok,
428
+ headline: "Git repository detected at #{repository[:current_root]}."
429
+ ),
430
+ default_branch_doctor_check,
431
+ stale_worktree_doctor_check
432
+ ]
433
+ end
434
+
435
+ def default_branch_doctor_check
436
+ doctor_check(
437
+ category: :git,
438
+ status: :ok,
439
+ headline: "origin default branch resolves to '#{resolve_default_branch}'."
440
+ )
441
+ rescue Error => e
442
+ doctor_check(category: :git, status: :warning, headline: e.message)
443
+ end
444
+
445
+ # rubocop:disable Metrics/MethodLength
446
+ def stale_worktree_doctor_check
447
+ stale_paths = worktree_entries.reject { |entry| File.directory?(entry[:path]) }
448
+ if stale_paths.empty?
449
+ doctor_check(category: :worktree, status: :ok, headline: 'No stale registered worktree paths found.')
450
+ else
451
+ doctor_check(
452
+ category: :worktree,
453
+ status: :warning,
454
+ headline: "Found #{stale_paths.length} stale registered worktree " \
455
+ "path#{'s' unless stale_paths.length == 1}.",
456
+ messages: stale_paths.map { |entry| entry[:path] }
457
+ )
458
+ end
459
+ end
460
+ # rubocop:enable Metrics/MethodLength
461
+
462
+ # rubocop:disable Metrics/MethodLength
463
+ def doctor_check(category:, status:, headline:, messages: [])
464
+ ::Rails::Worktrees::ProjectMaintenance::Check.new(
465
+ identifier: nil,
466
+ category: category,
467
+ status: status,
468
+ headline: headline,
469
+ messages: messages,
470
+ relative_path: nil,
471
+ updated_content: nil,
472
+ make_executable: false,
473
+ apply_messages: []
474
+ )
475
+ end
476
+ # rubocop:enable Metrics/MethodLength
477
+
478
+ # rubocop:disable Metrics/AbcSize
479
+ def apply_maintenance_check(check, current_root:)
480
+ if dry_run?
481
+ info("Would update #{check.relative_path}")
482
+ Array(check.apply_messages).each { |message| info(message) }
483
+ return
484
+ end
485
+
486
+ path = maintenance_destination_path(check.relative_path, current_root)
487
+ FileUtils.mkdir_p(File.dirname(path))
488
+ File.write(path, check.updated_content)
489
+ FileUtils.chmod(0o755, path) if check.make_executable
490
+
491
+ Array(check.apply_messages).each { |message| info(message) }
492
+ end
493
+
494
+ def maintenance_destination_path(relative_path, current_root)
495
+ root_path = File.realpath(current_root)
496
+ path = File.expand_path(relative_path, root_path)
497
+ assert_within_root!(path, root_path, "Refusing to write outside of repository root: #{path}")
498
+ parent_path = nearest_existing_parent(File.dirname(path))
499
+ assert_parent_within_root!(parent_path, root_path, path)
500
+ assert_not_symlink!(path)
501
+ path
502
+ end
503
+
504
+ def assert_within_root!(path, root_path, message)
505
+ raise Error, message unless within_root?(path, root_path)
506
+ end
507
+
508
+ def assert_parent_within_root!(parent_path, root_path, path)
509
+ assert_within_root!(
510
+ parent_path,
511
+ root_path,
512
+ "Refusing to write through symlinked directory outside repository root: #{path}"
513
+ )
514
+ end
515
+
516
+ def assert_not_symlink!(path)
517
+ raise Error, "Refusing to overwrite symlinked path: #{path}" if File.symlink?(path)
518
+ end
519
+
520
+ def within_root?(path, root_path)
521
+ path == root_path || path.start_with?("#{root_path}#{File::SEPARATOR}")
522
+ end
523
+
524
+ def nearest_existing_parent(path)
525
+ candidate = path
526
+ until File.exist?(candidate)
527
+ parent = File.dirname(candidate)
528
+ return candidate if parent == candidate
529
+
530
+ candidate = parent
531
+ end
532
+
533
+ File.realpath(candidate)
534
+ end
535
+ # rubocop:enable Metrics/AbcSize
536
+
537
+ # rubocop:disable Metrics/MethodLength
538
+ def complete_update(updated_count:, identical_count:, skipped_count:)
539
+ if dry_run?
540
+ success('Dry run complete')
541
+ info("Would update #{updated_count} file#{'s' unless updated_count == 1}.")
542
+ info('No changes were made.')
543
+ return 0
544
+ end
545
+
546
+ success('Update complete')
547
+ info(
548
+ [
549
+ "updated: #{updated_count}",
550
+ "already up to date: #{identical_count}",
551
+ "skipped: #{skipped_count}"
552
+ ].join(', ')
553
+ )
554
+ 0
555
+ end
556
+ # rubocop:enable Metrics/MethodLength
346
557
  end
347
558
  # rubocop:enable Metrics/ClassLength
348
559
  end
@@ -2,6 +2,16 @@ module Rails
2
2
  module Worktrees
3
3
  # Stores application-level settings for the wt command.
4
4
  class Configuration
5
+ CONFIGURABLE_ATTRIBUTES = %i[
6
+ bootstrap_env
7
+ workspace_root
8
+ dev_port_range
9
+ branch_prefix
10
+ name_sources_path
11
+ used_names_file
12
+ worktree_database_suffix_max_length
13
+ ].freeze
14
+
5
15
  DEFAULT_BOOTSTRAP_ENV = true
6
16
  DEFAULT_BRANCH_PREFIX = '🚂'.freeze
7
17
  DEFAULT_DEV_PORT_RANGE = (3000..3999)
@@ -0,0 +1,171 @@
1
+ require 'erb'
2
+
3
+ module Rails
4
+ module Worktrees
5
+ # Safely updates the generated initializer to use the current managed app-config format.
6
+ # rubocop:disable Metrics/ClassLength
7
+ class InitializerUpdater
8
+ Result = Struct.new(:content, :changed, :status, :messages) do
9
+ def changed?
10
+ changed
11
+ end
12
+ end
13
+
14
+ TEMPLATE_PATH = File.expand_path('../../generators/rails/worktrees/templates/rails_worktrees.rb.tt', __dir__)
15
+ CURRENT_WRAPPER_CALL = 'Rails.application.config.x.rails_worktrees.tap do |config|'.freeze
16
+ CONFIGURE_CALL = 'Rails::Worktrees.configure do |config|'.freeze
17
+ KNOWN_GUARD_FRAGMENTS = [
18
+ "Gem.loaded_specs.key?('rails-worktrees')",
19
+ 'defined?(Rails::Worktrees)',
20
+ 'Rails::Worktrees.respond_to?(:configure)'
21
+ ].freeze
22
+
23
+ def self.default_content = new(content: '').send(:render_default_template)
24
+
25
+ def initialize(content:) = @content = content
26
+
27
+ def call
28
+ return identical_result if current_guard_present?
29
+ return updated_result(self.class.default_content) if blank_content?
30
+
31
+ body = wrapped_body
32
+ return skip_result unless body
33
+
34
+ updated_result(rebuild_content(body))
35
+ end
36
+
37
+ private
38
+
39
+ def identical_result
40
+ Result.new(
41
+ @content,
42
+ false,
43
+ :identical,
44
+ ['config/initializers/rails_worktrees.rb already uses the current managed initializer format.']
45
+ )
46
+ end
47
+
48
+ def updated_result(content)
49
+ Result.new(
50
+ content,
51
+ content != @content,
52
+ :updated,
53
+ ['Updated config/initializers/rails_worktrees.rb to use the current managed initializer format.']
54
+ )
55
+ end
56
+
57
+ def skip_result
58
+ Result.new(
59
+ @content,
60
+ false,
61
+ :skip,
62
+ ['config/initializers/rails_worktrees.rb is too custom to update automatically; review it manually.']
63
+ )
64
+ end
65
+
66
+ def blank_content? = @content.to_s.strip.empty?
67
+
68
+ def current_wrapper_present?
69
+ !extract_current_wrapper_body(@content.to_s.strip.lines).nil?
70
+ end
71
+
72
+ def normalized_body = extract_existing_body&.then { |body| normalize_body(body) }
73
+
74
+ def extract_existing_body
75
+ stripped = @content.to_s.strip
76
+ return if stripped.empty?
77
+
78
+ body = extract_current_wrapper_body(stripped.lines)
79
+ return body if body
80
+
81
+ body = extract_guarded_configure_body(stripped.lines)
82
+ return body if body
83
+
84
+ body = extract_plain_configure_body(stripped.lines)
85
+ return body if body
86
+
87
+ nil
88
+ end
89
+
90
+ def extract_current_wrapper_body(lines)
91
+ return unless lines.first&.strip == CURRENT_WRAPPER_CALL && lines.last&.strip == 'end'
92
+
93
+ lines[1...-1].join.rstrip
94
+ end
95
+
96
+ def extract_guarded_configure_body(lines)
97
+ return unless guarded_configure_block?(lines)
98
+
99
+ configure_index = lines.index { |line| line.strip == CONFIGURE_CALL }
100
+ return unless guarded_configure_lines(lines, configure_index).all? { |line| known_guard_line?(line) }
101
+
102
+ lines[(configure_index + 1)...-2].join.rstrip
103
+ end
104
+
105
+ def extract_plain_configure_body(lines)
106
+ return unless lines.first&.strip == CONFIGURE_CALL && lines.last&.strip == 'end'
107
+
108
+ lines[1...-1].join.rstrip
109
+ end
110
+
111
+ def known_guard_line?(line)
112
+ stripped = line.strip
113
+ return true if stripped.empty?
114
+
115
+ normalized = stripped.delete_suffix('&&').strip.sub(/\Aif\s+/, '')
116
+ KNOWN_GUARD_FRAGMENTS.include?(normalized)
117
+ end
118
+
119
+ def normalize_body(body)
120
+ body_lines = body.rstrip.lines
121
+ return '' if body_lines.empty?
122
+
123
+ minimum_indent = body_indent(body_lines)
124
+
125
+ body_lines.map do |line|
126
+ next line if line.strip.empty?
127
+
128
+ normalize_body_line(line, minimum_indent)
129
+ end.join.rstrip
130
+ end
131
+
132
+ def rebuild_content(body)
133
+ content = [CURRENT_WRAPPER_CALL, body, 'end'].join("\n")
134
+ @content.end_with?("\n") || @content.empty? ? "#{content}\n" : content
135
+ end
136
+
137
+ def render_default_template
138
+ ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-').result(template_context.instance_eval { binding })
139
+ end
140
+
141
+ def template_context
142
+ Struct.new(:options, :conductor_workspace_root).new({ 'conductor' => false },
143
+ "File.expand_path('~/Sites/conductor/workspaces')")
144
+ end
145
+
146
+ def guarded_configure_block?(lines)
147
+ lines.last(2).map(&:strip) == %w[end end] && lines.any? { |line| line.strip == CONFIGURE_CALL }
148
+ end
149
+
150
+ def guarded_configure_lines(lines, configure_index)
151
+ lines[0...configure_index].reject { |line| line.strip.empty? }.map(&:rstrip)
152
+ end
153
+
154
+ def body_indent(body_lines)
155
+ body_lines.reject { |line| line.strip.empty? }
156
+ .map { |line| line[/\A\s*/].length }
157
+ .min || 0
158
+ end
159
+
160
+ def normalize_body_line(line, minimum_indent)
161
+ trimmed = line.sub(/\A\s{0,#{minimum_indent}}/, '')
162
+ " #{trimmed}"
163
+ end
164
+
165
+ def current_guard_present? = current_wrapper_present?
166
+
167
+ def wrapped_body = normalized_body
168
+ end
169
+ # rubocop:enable Metrics/ClassLength
170
+ end
171
+ end
@@ -0,0 +1,205 @@
1
+ require 'pathname'
2
+
3
+ module Rails
4
+ module Worktrees
5
+ # Loads project-level configuration from the generated initializer without booting the full app.
6
+ class ProjectConfigurationLoader
7
+ CURRENT_WRAPPER_CALL = 'Rails.application.config.x.rails_worktrees.tap do |config|'.freeze
8
+ CONFIGURE_CALL = 'Rails::Worktrees.configure do |config|'.freeze
9
+ INITIALIZER_RELATIVE_PATH = 'config/initializers/rails_worktrees.rb'.freeze
10
+ KNOWN_GUARD_FRAGMENTS = [
11
+ "Gem.loaded_specs.key?('rails-worktrees')",
12
+ 'defined?(Rails::Worktrees)',
13
+ 'Rails::Worktrees.respond_to?(:configure)'
14
+ ].freeze
15
+ TEMP_RAILS_ROOT_MUTEX = Mutex.new
16
+
17
+ def initialize(root:, configuration: Rails::Worktrees.configuration)
18
+ @root = root
19
+ @configuration = configuration
20
+ end
21
+
22
+ def call
23
+ return configuration unless initializer_path && File.file?(initializer_path)
24
+
25
+ body = extract_configuration_body(File.read(initializer_path))
26
+ return configuration unless body
27
+
28
+ recorder = AssignmentRecorder.new(configuration)
29
+
30
+ with_temporary_rails_root do
31
+ evaluate_configuration_body(body, recorder)
32
+ end
33
+
34
+ Rails::Worktrees.apply_application_configuration(recorder.values, configuration: configuration)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :configuration, :root
40
+
41
+ def initializer_path
42
+ return @initializer_path if defined?(@initializer_path)
43
+
44
+ @initializer_path = project_root&.join(INITIALIZER_RELATIVE_PATH)&.to_s
45
+ end
46
+
47
+ def project_root
48
+ return @project_root if defined?(@project_root)
49
+
50
+ @project_root = discover_project_root
51
+ end
52
+
53
+ def discover_project_root
54
+ current = Pathname(root).expand_path
55
+
56
+ current.ascend do |path|
57
+ return path if File.file?(path.join(INITIALIZER_RELATIVE_PATH))
58
+ end
59
+
60
+ nil
61
+ end
62
+
63
+ def evaluate_configuration_body(body, recorder)
64
+ # Evaluates a managed initializer body like:
65
+ #
66
+ # proc do |config|
67
+ # config.branch_prefix = '🌿'
68
+ # end
69
+ # rubocop:disable Security/Eval, Style/DocumentDynamicEvalDefinition, Style/EvalWithLocation
70
+ Kernel.eval(
71
+ "proc do |config|\n#{body.rstrip}\nend",
72
+ TOPLEVEL_BINDING,
73
+ initializer_path,
74
+ 1
75
+ ).call(recorder)
76
+ # rubocop:enable Security/Eval, Style/DocumentDynamicEvalDefinition, Style/EvalWithLocation
77
+ end
78
+
79
+ def extract_configuration_body(content)
80
+ stripped = content.to_s.strip
81
+ return if stripped.empty?
82
+
83
+ lines = stripped.lines
84
+
85
+ extract_single_block_body(lines, CURRENT_WRAPPER_CALL) ||
86
+ extract_guarded_configure_body(lines) ||
87
+ extract_single_block_body(lines, CONFIGURE_CALL)
88
+ end
89
+
90
+ def extract_single_block_body(lines, opening_line)
91
+ return unless lines.first&.strip == opening_line && lines.last&.strip == 'end'
92
+
93
+ lines[1...-1].join
94
+ end
95
+
96
+ def extract_guarded_configure_body(lines)
97
+ return unless guarded_configure_block?(lines)
98
+
99
+ configure_index = lines.index { |line| line.strip == CONFIGURE_CALL }
100
+ return unless guarded_configure_lines(lines, configure_index).all? { |line| known_guard_line?(line) }
101
+
102
+ lines[(configure_index + 1)...-2].join
103
+ end
104
+
105
+ def known_guard_line?(line)
106
+ normalized = line.strip.delete_suffix('&&').strip.sub(/\Aif\s+/, '')
107
+ KNOWN_GUARD_FRAGMENTS.include?(normalized)
108
+ end
109
+
110
+ def guarded_configure_block?(lines)
111
+ lines.last(2).map(&:strip) == %w[end end] && lines.any? { |line| line.strip == CONFIGURE_CALL }
112
+ end
113
+
114
+ def guarded_configure_lines(lines, configure_index)
115
+ lines[0...configure_index].reject { |line| line.strip.empty? }.map(&:rstrip)
116
+ end
117
+
118
+ def with_temporary_rails_root
119
+ self.class::TEMP_RAILS_ROOT_MUTEX.synchronize do
120
+ override_state = build_rails_root_override_state
121
+
122
+ apply_temporary_rails_root(override_state, project_root)
123
+ yield
124
+ ensure
125
+ restore_rails_root(override_state)
126
+ end
127
+ end
128
+
129
+ def build_rails_root_override_state
130
+ had_root = Rails.respond_to?(:root)
131
+ { singleton_class: Rails.singleton_class, had_root: had_root,
132
+ previous_root: (Rails.method(:root) if had_root), overridden: false }
133
+ end
134
+
135
+ def apply_temporary_rails_root(override_state, resolved_project_root)
136
+ override_state[:singleton_class].send(:define_method, :root) { resolved_project_root }
137
+ override_state[:overridden] = true
138
+ end
139
+
140
+ def restore_rails_root(override_state)
141
+ return unless override_state
142
+
143
+ override_state[:singleton_class].send(:remove_method, :root) if override_state[:overridden]
144
+ return unless override_state[:had_root]
145
+
146
+ override_state[:singleton_class].send(:define_method, :root, override_state[:previous_root])
147
+ end
148
+
149
+ # Records config.<name> = value assignments without raising on unknown keys.
150
+ class AssignmentRecorder
151
+ attr_reader :values
152
+
153
+ def initialize(configuration = nil)
154
+ @configuration = configuration
155
+ @values = {}
156
+ end
157
+
158
+ def method_missing(method_name, *args)
159
+ name = method_name.to_s
160
+
161
+ if setter_call?(name, args)
162
+ values[name.delete_suffix('=').to_sym] = args.first
163
+ elsif getter_call?(name, args)
164
+ values.fetch(name.to_sym) { configuration_value_for(method_name) }
165
+ else
166
+ super
167
+ end
168
+ end
169
+
170
+ def respond_to_missing?(method_name, include_private = false)
171
+ name = method_name.to_s
172
+
173
+ setter_name?(name) || getter_name?(name) || super
174
+ end
175
+
176
+ private
177
+
178
+ attr_reader :configuration
179
+
180
+ def setter_call?(name, args)
181
+ setter_name?(name) && args.length == 1
182
+ end
183
+
184
+ def getter_call?(name, args)
185
+ getter_name?(name) && args.empty?
186
+ end
187
+
188
+ def setter_name?(name)
189
+ name.end_with?('=')
190
+ end
191
+
192
+ def getter_name?(name)
193
+ !setter_name?(name)
194
+ end
195
+
196
+ def configuration_value_for(method_name)
197
+ fallback_configuration = configuration
198
+ return unless fallback_configuration.respond_to?(method_name)
199
+
200
+ fallback_configuration.public_send(method_name)
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,328 @@
1
+ module Rails
2
+ module Worktrees
3
+ # Audits and prepares safe file-based maintenance updates for the current checkout.
4
+ # rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize
5
+ class ProjectMaintenance
6
+ # rubocop:disable Style/RedundantStructKeywordInit
7
+ Check = Struct.new(
8
+ :identifier,
9
+ :category,
10
+ :headline,
11
+ :messages,
12
+ :relative_path,
13
+ :status,
14
+ :updated_content,
15
+ :make_executable,
16
+ :apply_messages,
17
+ keyword_init: true
18
+ ) do
19
+ def ok?
20
+ status == :ok
21
+ end
22
+
23
+ def fixable?
24
+ status == :fixable
25
+ end
26
+
27
+ def warning?
28
+ status == :warning
29
+ end
30
+
31
+ def updatable?
32
+ !updated_content.nil?
33
+ end
34
+ end
35
+
36
+ Report = Struct.new(:checks, keyword_init: true) do
37
+ def fixable_checks
38
+ checks.select(&:fixable?)
39
+ end
40
+
41
+ def warning_checks
42
+ checks.select(&:warning?)
43
+ end
44
+
45
+ def ok?
46
+ fixable_checks.empty? && warning_checks.empty?
47
+ end
48
+ end
49
+ # rubocop:enable Style/RedundantStructKeywordInit
50
+
51
+ TEMPLATE_ROOT = File.expand_path('../../generators/rails/worktrees/templates', __dir__)
52
+
53
+ def initialize(root:)
54
+ @root = root
55
+ end
56
+
57
+ def call
58
+ Report.new(checks: maintenance_checks.compact)
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :root
64
+
65
+ def maintenance_checks
66
+ [
67
+ template_file_check(wrapper_config),
68
+ initializer_check,
69
+ database_check,
70
+ optional_updater_check(procfile_config) { |content| ProcfileUpdater.new(content: content).call },
71
+ optional_updater_check(puma_config) { |content| PumaConfigUpdater.new(content: content).call },
72
+ mise_check,
73
+ template_file_check(browser_wrapper_config)
74
+ ]
75
+ end
76
+
77
+ def wrapper_config
78
+ {
79
+ identifier: :bin_wt,
80
+ category: :install,
81
+ relative_path: 'bin/wt',
82
+ template_path: File.join(TEMPLATE_ROOT, 'bin/wt'),
83
+ make_executable: true,
84
+ optional: false
85
+ }
86
+ end
87
+
88
+ def browser_wrapper_config
89
+ {
90
+ identifier: :bin_ob,
91
+ category: :install,
92
+ relative_path: 'bin/ob',
93
+ template_path: File.join(TEMPLATE_ROOT, 'bin/ob'),
94
+ make_executable: true,
95
+ optional: true
96
+ }
97
+ end
98
+
99
+ def template_file_check(config)
100
+ path = absolute_path(config.fetch(:relative_path))
101
+ return nil if config.fetch(:optional) && !File.exist?(path)
102
+
103
+ desired_content = File.read(config.fetch(:template_path))
104
+ return template_missing_check(config, desired_content) unless File.exist?(path)
105
+
106
+ current_content = File.read(path)
107
+ if current_content == desired_content
108
+ if executable_mode_current?(config, path)
109
+ return ok_check(config, "#{config.fetch(:relative_path)} is up to date.")
110
+ end
111
+
112
+ return executable_permission_check(config, desired_content)
113
+ end
114
+
115
+ fixable_check(
116
+ config,
117
+ "#{config.fetch(:relative_path)} differs from the managed template.",
118
+ updated_content: desired_content,
119
+ apply_messages: ["Updated #{config.fetch(:relative_path)} to match the managed template."],
120
+ make_executable: config.fetch(:make_executable, false)
121
+ )
122
+ end
123
+
124
+ def executable_mode_current?(config, path)
125
+ !config.fetch(:make_executable, false) || File.executable?(path)
126
+ end
127
+
128
+ def executable_permission_check(config, desired_content)
129
+ relative_path = config.fetch(:relative_path)
130
+ fixable_check(
131
+ config,
132
+ "#{relative_path} needs its executable bit restored.",
133
+ updated_content: desired_content,
134
+ apply_messages: ["Restored executable permissions on #{relative_path}."],
135
+ make_executable: true
136
+ )
137
+ end
138
+
139
+ def template_missing_check(config, desired_content)
140
+ fixable_check(
141
+ config,
142
+ "#{config.fetch(:relative_path)} is missing.",
143
+ updated_content: desired_content,
144
+ apply_messages: ["Created #{config.fetch(:relative_path)} from the managed template."],
145
+ make_executable: config.fetch(:make_executable, false)
146
+ )
147
+ end
148
+
149
+ def initializer_check
150
+ config = {
151
+ identifier: :initializer,
152
+ category: :install,
153
+ relative_path: 'config/initializers/rails_worktrees.rb',
154
+ identical_headline: 'config/initializers/rails_worktrees.rb already uses the current managed initializer ' \
155
+ 'format.',
156
+ fixable_headline: 'config/initializers/rails_worktrees.rb can be updated automatically.',
157
+ warning_headline: 'config/initializers/rails_worktrees.rb needs manual review.'
158
+ }
159
+ path = absolute_path(config.fetch(:relative_path))
160
+
161
+ unless File.exist?(path)
162
+ return fixable_check(
163
+ config,
164
+ "#{config.fetch(:relative_path)} is missing.",
165
+ updated_content: InitializerUpdater.default_content,
166
+ apply_messages: ['Created config/initializers/rails_worktrees.rb in the current managed initializer ' \
167
+ 'format.']
168
+ )
169
+ end
170
+
171
+ updater_result_check(config, InitializerUpdater.new(content: File.read(path)).call)
172
+ end
173
+
174
+ def database_check
175
+ config = {
176
+ identifier: :database,
177
+ category: :config,
178
+ relative_path: 'config/database.yml'
179
+ }
180
+ path = absolute_path(config.fetch(:relative_path))
181
+
182
+ unless File.exist?(path)
183
+ return warning_check(
184
+ config,
185
+ 'config/database.yml is missing.',
186
+ ['Add WORKTREE_DATABASE_SUFFIX support manually if this app uses a custom database setup.']
187
+ )
188
+ end
189
+
190
+ result = DatabaseConfigUpdater.new(content: File.read(path)).call
191
+ return updated_database_check(config, result) if result.changed?
192
+
193
+ if database_configured?(result)
194
+ return ok_check(
195
+ config,
196
+ 'config/database.yml already includes WORKTREE_DATABASE_SUFFIX in supported entries.'
197
+ )
198
+ end
199
+
200
+ warning_check(
201
+ config,
202
+ result.messages.first || 'config/database.yml needs manual review.',
203
+ result.messages.drop(1)
204
+ )
205
+ end
206
+
207
+ def updated_database_check(config, result)
208
+ fixable_check(
209
+ config,
210
+ 'config/database.yml can be updated automatically.',
211
+ messages: result.messages.drop(1),
212
+ updated_content: result.content,
213
+ apply_messages: result.messages
214
+ )
215
+ end
216
+
217
+ def database_configured?(result)
218
+ result.messages.first == 'Development/test database names already include WORKTREE_DATABASE_SUFFIX.'
219
+ end
220
+
221
+ def procfile_config
222
+ {
223
+ identifier: :procfile,
224
+ category: :config,
225
+ relative_path: 'Procfile.dev',
226
+ identical_headline: 'Procfile.dev already uses the DEV_PORT-aware web entry.',
227
+ fixable_headline: 'Procfile.dev can be updated automatically.',
228
+ warning_headline: 'Procfile.dev needs manual review.'
229
+ }
230
+ end
231
+
232
+ def puma_config
233
+ {
234
+ identifier: :puma,
235
+ category: :config,
236
+ relative_path: 'config/puma.rb',
237
+ identical_headline: 'config/puma.rb already prefers DEV_PORT.',
238
+ fixable_headline: 'config/puma.rb can be updated automatically.',
239
+ warning_headline: 'config/puma.rb needs manual review.'
240
+ }
241
+ end
242
+
243
+ def optional_updater_check(config)
244
+ path = absolute_path(config.fetch(:relative_path))
245
+ return unless File.exist?(path)
246
+
247
+ updater_result_check(config, yield(File.read(path)))
248
+ end
249
+
250
+ def mise_check
251
+ relative_path = %w[mise.toml .mise.toml].find { |candidate| File.exist?(absolute_path(candidate)) }
252
+ return unless relative_path
253
+
254
+ config = {
255
+ identifier: :mise,
256
+ category: :config,
257
+ relative_path: relative_path,
258
+ identical_headline: "#{relative_path} already loads .env from [env].",
259
+ fixable_headline: "#{relative_path} can be updated automatically.",
260
+ warning_headline: "#{relative_path} needs manual review."
261
+ }
262
+
263
+ updater_result_check(config, mise_updater_result(relative_path))
264
+ end
265
+
266
+ def mise_updater_result(relative_path)
267
+ MiseTomlUpdater.new(
268
+ content: File.read(absolute_path(relative_path)),
269
+ file_name: File.basename(relative_path)
270
+ ).call
271
+ end
272
+
273
+ def updater_result_check(config, result)
274
+ case result.status
275
+ when :identical
276
+ ok_check(config, config.fetch(:identical_headline))
277
+ when :updated
278
+ fixable_check(
279
+ config,
280
+ config.fetch(:fixable_headline),
281
+ updated_content: result.content,
282
+ apply_messages: result.messages
283
+ )
284
+ else
285
+ warning_check(config, config.fetch(:warning_headline), result.messages)
286
+ end
287
+ end
288
+
289
+ def ok_check(config, headline)
290
+ build_check(config, status: :ok, headline: headline)
291
+ end
292
+
293
+ def warning_check(config, headline, messages = [])
294
+ build_check(config, status: :warning, headline: headline, messages: messages)
295
+ end
296
+
297
+ def fixable_check(config, headline, updated_content:, apply_messages:, **attributes)
298
+ build_check(
299
+ config,
300
+ status: :fixable,
301
+ headline: headline,
302
+ updated_content: updated_content,
303
+ apply_messages: apply_messages,
304
+ **attributes
305
+ )
306
+ end
307
+
308
+ def build_check(config, status:, headline:, **attributes)
309
+ Check.new(
310
+ identifier: config.fetch(:identifier),
311
+ category: config.fetch(:category),
312
+ relative_path: config.fetch(:relative_path),
313
+ status: status,
314
+ headline: headline,
315
+ messages: attributes.fetch(:messages, []),
316
+ updated_content: attributes.fetch(:updated_content, nil),
317
+ make_executable: attributes.fetch(:make_executable, false),
318
+ apply_messages: attributes.fetch(:apply_messages, [])
319
+ )
320
+ end
321
+
322
+ def absolute_path(relative_path)
323
+ File.join(root, relative_path)
324
+ end
325
+ end
326
+ # rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize
327
+ end
328
+ end
@@ -5,6 +5,10 @@ module Rails
5
5
  initializer 'rails_worktrees.installation_hint' do
6
6
  Rails::Worktrees.warn_about_missing_installation
7
7
  end
8
+
9
+ initializer 'rails_worktrees.apply_application_config', after: :load_config_initializers do |app|
10
+ Rails::Worktrees.apply_application_configuration(app.config.x.rails_worktrees)
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,5 +1,5 @@
1
1
  module Rails
2
2
  module Worktrees
3
- VERSION = '0.4.0'.freeze
3
+ VERSION = '0.5.1'.freeze
4
4
  end
5
5
  end
@@ -2,14 +2,18 @@ require 'pathname'
2
2
 
3
3
  require_relative 'worktrees/version'
4
4
  require_relative 'worktrees/configuration'
5
+ require_relative 'worktrees/application_configuration'
5
6
  require_relative 'worktrees/env_bootstrapper'
6
7
  require_relative 'worktrees/command'
7
8
  require_relative 'worktrees/cli'
8
9
  require_relative 'worktrees/browser_command'
9
10
  require_relative 'worktrees/database_config_updater'
11
+ require_relative 'worktrees/initializer_updater'
10
12
  require_relative 'worktrees/procfile_updater'
11
13
  require_relative 'worktrees/mise_toml_updater'
12
14
  require_relative 'worktrees/puma_config_updater'
15
+ require_relative 'worktrees/project_configuration_loader'
16
+ require_relative 'worktrees/project_maintenance'
13
17
 
14
18
  module Rails
15
19
  # Rails-specific git worktree helpers and installer support.
@@ -37,6 +41,10 @@ module Rails
37
41
  @configuration = Configuration.new
38
42
  end
39
43
 
44
+ def apply_application_configuration(source, configuration: self.configuration)
45
+ ApplicationConfiguration.apply(source, configuration: configuration)
46
+ end
47
+
40
48
  def installation_complete?(root = resolve_root)
41
49
  return false unless root
42
50
 
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.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asjer Querido
@@ -55,6 +55,7 @@ files:
55
55
  - lib/generators/rails/worktrees/templates/rails_worktrees.rb.tt
56
56
  - lib/generators/worktrees/install/install_generator.rb
57
57
  - lib/rails/worktrees.rb
58
+ - lib/rails/worktrees/application_configuration.rb
58
59
  - lib/rails/worktrees/browser_command.rb
59
60
  - lib/rails/worktrees/cli.rb
60
61
  - lib/rails/worktrees/command.rb
@@ -66,9 +67,12 @@ files:
66
67
  - lib/rails/worktrees/configuration.rb
67
68
  - lib/rails/worktrees/database_config_updater.rb
68
69
  - lib/rails/worktrees/env_bootstrapper.rb
70
+ - lib/rails/worktrees/initializer_updater.rb
69
71
  - lib/rails/worktrees/mise_toml_updater.rb
70
72
  - lib/rails/worktrees/names/cities.txt
71
73
  - lib/rails/worktrees/procfile_updater.rb
74
+ - lib/rails/worktrees/project_configuration_loader.rb
75
+ - lib/rails/worktrees/project_maintenance.rb
72
76
  - lib/rails/worktrees/puma_config_updater.rb
73
77
  - lib/rails/worktrees/railtie.rb
74
78
  - lib/rails/worktrees/version.rb