rails-worktrees 0.5.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/README.md +2 -2
- data/lib/generators/rails/worktrees/templates/rails_worktrees.rb.tt +16 -20
- data/lib/rails/worktrees/application_configuration.rb +36 -0
- data/lib/rails/worktrees/cli.rb +33 -7
- data/lib/rails/worktrees/configuration.rb +10 -0
- data/lib/rails/worktrees/initializer_updater.rb +52 -29
- data/lib/rails/worktrees/project_configuration_loader.rb +205 -0
- data/lib/rails/worktrees/project_maintenance.rb +4 -2
- data/lib/rails/worktrees/railtie.rb +4 -0
- data/lib/rails/worktrees/version.rb +1 -1
- data/lib/rails/worktrees.rb +6 -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: 698795e04a67284039d3beba2286f3a9aa156a5856832a345f22ffa57b6177e3
|
|
4
|
+
data.tar.gz: 0a57ef25c66c3226798498476abf8caf033c6a2989ae170b30a161fde66fddcf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06c8ae396186a19c08e45445e031a28198d48f7ea5125c02e63b2df39977f5399f75421e7f1897071d14b00d042051616ed4e447b83b570852d54756d8689377
|
|
7
|
+
data.tar.gz: 800a19ef60af420e03280b8a30c02c9012c4ded0d3eaecc421ed5e42e072b8024a0cbc93e7ba7aaf7d806dc972deacacbb0c5edfe45da40b2ab05659b1ccc5c5
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.5.
|
|
1
|
+
{".":"0.5.1"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [0.5.0](https://github.com/asjer/rails-worktrees/compare/v0.4.0...v0.5.0) (2026-04-02)
|
|
4
11
|
|
|
5
12
|
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ bin/rails generate worktrees:install --browser
|
|
|
19
19
|
bin/rails generate worktrees:install --yolo
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
The generated initializer
|
|
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
23
|
|
|
24
24
|
The installer adds:
|
|
25
25
|
|
|
@@ -148,7 +148,7 @@ Worktree names must not contain `/` or whitespace, must not be `.` or `..`, and
|
|
|
148
148
|
|
|
149
149
|
The installer generates `config/initializers/rails_worktrees.rb` where you can override:
|
|
150
150
|
|
|
151
|
-
The initializer
|
|
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
152
|
|
|
153
153
|
| Option | Default | Description |
|
|
154
154
|
|--------|---------|-------------|
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
defined?(Rails::Worktrees) &&
|
|
3
|
-
Rails::Worktrees.respond_to?(:configure)
|
|
4
|
-
Rails::Worktrees.configure do |config|
|
|
1
|
+
Rails.application.config.x.rails_worktrees.tap do |config|
|
|
5
2
|
<% if options['conductor'] -%>
|
|
6
|
-
|
|
3
|
+
config.workspace_root = <%= conductor_workspace_root %>
|
|
7
4
|
<% else -%>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
# By default, worktrees go in a sibling "<project>.worktrees" directory.
|
|
6
|
+
# Uncomment to override with a custom parent directory that uses <root>/<project>/<name>.
|
|
7
|
+
# config.workspace_root = File.expand_path('~/worktrees')
|
|
11
8
|
<% end -%>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end
|
|
9
|
+
# config.bootstrap_env = false
|
|
10
|
+
# config.dev_port_range = 3000..3999
|
|
11
|
+
# config.worktree_database_suffix_max_length = 18
|
|
12
|
+
# config.branch_prefix = '🚂'
|
|
13
|
+
# config.name_sources_path = Rails.root.join('config/worktree_names').to_s
|
|
14
|
+
# config.used_names_file = File.join(
|
|
15
|
+
# ENV.fetch('XDG_STATE_HOME', File.expand_path('~/.local/state')),
|
|
16
|
+
# 'rails-worktrees',
|
|
17
|
+
# 'used-names.tsv'
|
|
18
|
+
# )
|
|
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
|
data/lib/rails/worktrees/cli.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
@@ -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)
|
|
@@ -2,7 +2,7 @@ require 'erb'
|
|
|
2
2
|
|
|
3
3
|
module Rails
|
|
4
4
|
module Worktrees
|
|
5
|
-
# Safely updates the generated initializer to use the current
|
|
5
|
+
# Safely updates the generated initializer to use the current managed app-config format.
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class InitializerUpdater
|
|
8
8
|
Result = Struct.new(:content, :changed, :status, :messages) do
|
|
@@ -12,18 +12,13 @@ module Rails
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
TEMPLATE_PATH = File.expand_path('../../generators/rails/worktrees/templates/rails_worktrees.rb.tt', __dir__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
' defined?(Rails::Worktrees) &&',
|
|
18
|
-
' Rails::Worktrees.respond_to?(:configure)'
|
|
19
|
-
].freeze
|
|
15
|
+
CURRENT_WRAPPER_CALL = 'Rails.application.config.x.rails_worktrees.tap do |config|'.freeze
|
|
16
|
+
CONFIGURE_CALL = 'Rails::Worktrees.configure do |config|'.freeze
|
|
20
17
|
KNOWN_GUARD_FRAGMENTS = [
|
|
21
18
|
"Gem.loaded_specs.key?('rails-worktrees')",
|
|
22
19
|
'defined?(Rails::Worktrees)',
|
|
23
20
|
'Rails::Worktrees.respond_to?(:configure)'
|
|
24
21
|
].freeze
|
|
25
|
-
CONFIGURE_CALL = 'Rails::Worktrees.configure do |config|'.freeze
|
|
26
|
-
LEGACY_GUARD = /\Aif defined\?\(Rails::Worktrees\)\n(?<body>.*)\nend\z/m
|
|
27
22
|
|
|
28
23
|
def self.default_content = new(content: '').send(:render_default_template)
|
|
29
24
|
|
|
@@ -46,7 +41,7 @@ module Rails
|
|
|
46
41
|
@content,
|
|
47
42
|
false,
|
|
48
43
|
:identical,
|
|
49
|
-
['config/initializers/rails_worktrees.rb already uses the current
|
|
44
|
+
['config/initializers/rails_worktrees.rb already uses the current managed initializer format.']
|
|
50
45
|
)
|
|
51
46
|
end
|
|
52
47
|
|
|
@@ -55,7 +50,7 @@ module Rails
|
|
|
55
50
|
content,
|
|
56
51
|
content != @content,
|
|
57
52
|
:updated,
|
|
58
|
-
['Updated config/initializers/rails_worktrees.rb to use the current
|
|
53
|
+
['Updated config/initializers/rails_worktrees.rb to use the current managed initializer format.']
|
|
59
54
|
)
|
|
60
55
|
end
|
|
61
56
|
|
|
@@ -70,19 +65,20 @@ module Rails
|
|
|
70
65
|
|
|
71
66
|
def blank_content? = @content.to_s.strip.empty?
|
|
72
67
|
|
|
73
|
-
def
|
|
74
|
-
!
|
|
68
|
+
def current_wrapper_present?
|
|
69
|
+
!extract_current_wrapper_body(@content.to_s.strip.lines).nil?
|
|
75
70
|
end
|
|
76
71
|
|
|
77
|
-
def
|
|
72
|
+
def normalized_body = extract_existing_body&.then { |body| normalize_body(body) }
|
|
78
73
|
|
|
79
74
|
def extract_existing_body
|
|
80
75
|
stripped = @content.to_s.strip
|
|
81
76
|
return if stripped.empty?
|
|
82
77
|
|
|
83
|
-
|
|
78
|
+
body = extract_current_wrapper_body(stripped.lines)
|
|
79
|
+
return body if body
|
|
84
80
|
|
|
85
|
-
body =
|
|
81
|
+
body = extract_guarded_configure_body(stripped.lines)
|
|
86
82
|
return body if body
|
|
87
83
|
|
|
88
84
|
body = extract_plain_configure_body(stripped.lines)
|
|
@@ -91,25 +87,25 @@ module Rails
|
|
|
91
87
|
nil
|
|
92
88
|
end
|
|
93
89
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return if lines.empty? || lines.last.strip != 'end'
|
|
90
|
+
def extract_current_wrapper_body(lines)
|
|
91
|
+
return unless lines.first&.strip == CURRENT_WRAPPER_CALL && lines.last&.strip == 'end'
|
|
97
92
|
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
lines[1...-1].join.rstrip
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def extract_guarded_configure_body(lines)
|
|
97
|
+
return unless guarded_configure_block?(lines)
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
return unless
|
|
103
|
-
return if required_guard_lines && guard_lines != required_guard_lines
|
|
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) }
|
|
104
101
|
|
|
105
|
-
lines[configure_index...-
|
|
102
|
+
lines[(configure_index + 1)...-2].join.rstrip
|
|
106
103
|
end
|
|
107
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
108
104
|
|
|
109
105
|
def extract_plain_configure_body(lines)
|
|
110
106
|
return unless lines.first&.strip == CONFIGURE_CALL && lines.last&.strip == 'end'
|
|
111
107
|
|
|
112
|
-
lines.join.rstrip
|
|
108
|
+
lines[1...-1].join.rstrip
|
|
113
109
|
end
|
|
114
110
|
|
|
115
111
|
def known_guard_line?(line)
|
|
@@ -124,13 +120,17 @@ module Rails
|
|
|
124
120
|
body_lines = body.rstrip.lines
|
|
125
121
|
return '' if body_lines.empty?
|
|
126
122
|
|
|
127
|
-
|
|
123
|
+
minimum_indent = body_indent(body_lines)
|
|
128
124
|
|
|
129
|
-
body_lines.map
|
|
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
130
|
end
|
|
131
131
|
|
|
132
132
|
def rebuild_content(body)
|
|
133
|
-
content = [
|
|
133
|
+
content = [CURRENT_WRAPPER_CALL, body, 'end'].join("\n")
|
|
134
134
|
@content.end_with?("\n") || @content.empty? ? "#{content}\n" : content
|
|
135
135
|
end
|
|
136
136
|
|
|
@@ -142,6 +142,29 @@ module Rails
|
|
|
142
142
|
Struct.new(:options, :conductor_workspace_root).new({ 'conductor' => false },
|
|
143
143
|
"File.expand_path('~/Sites/conductor/workspaces')")
|
|
144
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
|
|
145
168
|
end
|
|
146
169
|
# rubocop:enable Metrics/ClassLength
|
|
147
170
|
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
|
|
@@ -151,7 +151,8 @@ module Rails
|
|
|
151
151
|
identifier: :initializer,
|
|
152
152
|
category: :install,
|
|
153
153
|
relative_path: 'config/initializers/rails_worktrees.rb',
|
|
154
|
-
identical_headline: 'config/initializers/rails_worktrees.rb already uses the current
|
|
154
|
+
identical_headline: 'config/initializers/rails_worktrees.rb already uses the current managed initializer ' \
|
|
155
|
+
'format.',
|
|
155
156
|
fixable_headline: 'config/initializers/rails_worktrees.rb can be updated automatically.',
|
|
156
157
|
warning_headline: 'config/initializers/rails_worktrees.rb needs manual review.'
|
|
157
158
|
}
|
|
@@ -162,7 +163,8 @@ module Rails
|
|
|
162
163
|
config,
|
|
163
164
|
"#{config.fetch(:relative_path)} is missing.",
|
|
164
165
|
updated_content: InitializerUpdater.default_content,
|
|
165
|
-
apply_messages: ['Created config/initializers/rails_worktrees.rb
|
|
166
|
+
apply_messages: ['Created config/initializers/rails_worktrees.rb in the current managed initializer ' \
|
|
167
|
+
'format.']
|
|
166
168
|
)
|
|
167
169
|
end
|
|
168
170
|
|
|
@@ -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
|
data/lib/rails/worktrees.rb
CHANGED
|
@@ -2,6 +2,7 @@ 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'
|
|
@@ -11,6 +12,7 @@ require_relative 'worktrees/initializer_updater'
|
|
|
11
12
|
require_relative 'worktrees/procfile_updater'
|
|
12
13
|
require_relative 'worktrees/mise_toml_updater'
|
|
13
14
|
require_relative 'worktrees/puma_config_updater'
|
|
15
|
+
require_relative 'worktrees/project_configuration_loader'
|
|
14
16
|
require_relative 'worktrees/project_maintenance'
|
|
15
17
|
|
|
16
18
|
module Rails
|
|
@@ -39,6 +41,10 @@ module Rails
|
|
|
39
41
|
@configuration = Configuration.new
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def apply_application_configuration(source, configuration: self.configuration)
|
|
45
|
+
ApplicationConfiguration.apply(source, configuration: configuration)
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
def installation_complete?(root = resolve_root)
|
|
43
49
|
return false unless root
|
|
44
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.5.
|
|
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
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- lib/rails/worktrees/mise_toml_updater.rb
|
|
71
72
|
- lib/rails/worktrees/names/cities.txt
|
|
72
73
|
- lib/rails/worktrees/procfile_updater.rb
|
|
74
|
+
- lib/rails/worktrees/project_configuration_loader.rb
|
|
73
75
|
- lib/rails/worktrees/project_maintenance.rb
|
|
74
76
|
- lib/rails/worktrees/puma_config_updater.rb
|
|
75
77
|
- lib/rails/worktrees/railtie.rb
|