kettle-dev 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +176 -3
- data/CITATION.cff +2 -2
- data/CONTRIBUTING.md +11 -17
- data/README.md +390 -319
- data/exe/kettle-dev-setup +12 -63
- data/exe/kettle-gh-release +82 -0
- data/lib/kettle/dev/gem_spec_reader.rb +2 -2
- data/lib/kettle/dev/open_collective_config.rb +12 -0
- data/lib/kettle/dev/rakelib/yard.rake +15 -0
- data/lib/kettle/dev/tasks/ci_task.rb +4 -4
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +4 -12
- data/sig/kettle/dev/source_merger.rbs +40 -56
- data.tar.gz.sig +0 -0
- metadata +15 -144
- metadata.gz.sig +0 -0
- data/.aiignore.example +0 -19
- data/.devcontainer/apt-install/devcontainer-feature.json +0 -9
- data/.devcontainer/apt-install/install.sh +0 -11
- data/.devcontainer/devcontainer.json +0 -28
- data/.env.local.example +0 -31
- data/.envrc +0 -47
- data/.envrc.example +0 -51
- data/.envrc.no-osc.example +0 -51
- data/.git-hooks/commit-msg +0 -54
- data/.git-hooks/commit-subjects-goalie.txt +0 -8
- data/.git-hooks/footer-template.erb.txt +0 -16
- data/.git-hooks/prepare-commit-msg +0 -8
- data/.github/.codecov.yml.example +0 -14
- data/.github/FUNDING.yml +0 -13
- data/.github/FUNDING.yml.no-osc.example +0 -13
- data/.github/dependabot.yml +0 -13
- data/.github/workflows/ancient.yml +0 -83
- data/.github/workflows/ancient.yml.example +0 -81
- data/.github/workflows/auto-assign.yml +0 -21
- data/.github/workflows/codeql-analysis.yml +0 -70
- data/.github/workflows/coverage.yml +0 -127
- data/.github/workflows/coverage.yml.example +0 -127
- data/.github/workflows/current.yml +0 -116
- data/.github/workflows/current.yml.example +0 -115
- data/.github/workflows/dep-heads.yml +0 -117
- data/.github/workflows/dependency-review.yml +0 -20
- data/.github/workflows/discord-notifier.yml.example +0 -39
- data/.github/workflows/heads.yml +0 -117
- data/.github/workflows/heads.yml.example +0 -116
- data/.github/workflows/jruby.yml +0 -82
- data/.github/workflows/jruby.yml.example +0 -72
- data/.github/workflows/legacy.yml +0 -76
- data/.github/workflows/license-eye.yml +0 -40
- data/.github/workflows/locked_deps.yml +0 -85
- data/.github/workflows/opencollective.yml +0 -40
- data/.github/workflows/style.yml +0 -67
- data/.github/workflows/supported.yml +0 -75
- data/.github/workflows/truffle.yml +0 -99
- data/.github/workflows/unlocked_deps.yml +0 -84
- data/.github/workflows/unsupported.yml +0 -76
- data/.gitignore +0 -50
- data/.gitlab-ci.yml.example +0 -134
- data/.idea/.gitignore +0 -45
- data/.junie/guidelines-rbs.md +0 -49
- data/.junie/guidelines.md +0 -141
- data/.junie/guidelines.md.example +0 -140
- data/.licenserc.yaml +0 -7
- data/.opencollective.yml +0 -3
- data/.opencollective.yml.example +0 -3
- data/.qlty/qlty.toml +0 -79
- data/.rspec +0 -9
- data/.rubocop.yml +0 -13
- data/.rubocop_rspec.yml +0 -33
- data/.simplecov +0 -16
- data/.simplecov.example +0 -11
- data/.tool-versions +0 -1
- data/.yardignore +0 -13
- data/.yardopts +0 -14
- data/Appraisal.root.gemfile +0 -10
- data/Appraisals +0 -151
- data/Appraisals.example +0 -102
- data/CHANGELOG.md.example +0 -47
- data/CONTRIBUTING.md.example +0 -227
- data/FUNDING.md.no-osc.example +0 -63
- data/Gemfile +0 -40
- data/Gemfile.example +0 -34
- data/README.md.example +0 -570
- data/README.md.no-osc.example +0 -536
- data/Rakefile.example +0 -68
- data/gemfiles/modular/coverage.gemfile +0 -6
- data/gemfiles/modular/debug.gemfile +0 -13
- data/gemfiles/modular/documentation.gemfile +0 -14
- data/gemfiles/modular/erb/r2/v3.0.gemfile +0 -1
- data/gemfiles/modular/erb/r2.3/default.gemfile +0 -6
- data/gemfiles/modular/erb/r2.6/v2.2.gemfile +0 -3
- data/gemfiles/modular/erb/r3/v5.0.gemfile +0 -1
- data/gemfiles/modular/erb/r3.1/v4.0.gemfile +0 -2
- data/gemfiles/modular/erb/vHEAD.gemfile +0 -2
- data/gemfiles/modular/injected.gemfile +0 -60
- data/gemfiles/modular/mutex_m/r2/v0.3.gemfile +0 -2
- data/gemfiles/modular/mutex_m/r2.4/v0.1.gemfile +0 -3
- data/gemfiles/modular/mutex_m/r3/v0.3.gemfile +0 -2
- data/gemfiles/modular/mutex_m/vHEAD.gemfile +0 -2
- data/gemfiles/modular/optional.gemfile +0 -8
- data/gemfiles/modular/optional.gemfile.example +0 -5
- data/gemfiles/modular/runtime_heads.gemfile +0 -10
- data/gemfiles/modular/runtime_heads.gemfile.example +0 -8
- data/gemfiles/modular/stringio/r2/v3.0.gemfile +0 -5
- data/gemfiles/modular/stringio/r2.4/v0.0.2.gemfile +0 -4
- data/gemfiles/modular/stringio/r3/v3.0.gemfile +0 -5
- data/gemfiles/modular/stringio/vHEAD.gemfile +0 -2
- data/gemfiles/modular/style.gemfile +0 -25
- data/gemfiles/modular/style.gemfile.example +0 -25
- data/gemfiles/modular/templating.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2.3/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2.4/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r2.6/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r3/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/r3.1/libs.gemfile +0 -3
- data/gemfiles/modular/x_std_libs/vHEAD.gemfile +0 -3
- data/gemfiles/modular/x_std_libs.gemfile +0 -2
- data/kettle-dev.gemspec.example +0 -154
- data/lib/kettle/dev/modular_gemfiles.rb +0 -119
- data/lib/kettle/dev/prism_appraisals.rb +0 -351
- data/lib/kettle/dev/prism_gemfile.rb +0 -177
- data/lib/kettle/dev/prism_gemspec.rb +0 -284
- data/lib/kettle/dev/prism_utils.rb +0 -201
- data/lib/kettle/dev/rakelib/install.rake +0 -10
- data/lib/kettle/dev/rakelib/template.rake +0 -10
- data/lib/kettle/dev/setup_cli.rb +0 -403
- data/lib/kettle/dev/source_merger.rb +0 -622
- data/lib/kettle/dev/tasks/install_task.rb +0 -553
- data/lib/kettle/dev/tasks/template_task.rb +0 -975
- data/lib/kettle/dev/template_helpers.rb +0 -685
|
@@ -1,685 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# External stdlibs
|
|
4
|
-
require "find"
|
|
5
|
-
require "set"
|
|
6
|
-
require "yaml"
|
|
7
|
-
|
|
8
|
-
module Kettle
|
|
9
|
-
module Dev
|
|
10
|
-
# Helpers shared by kettle:dev Rake tasks for templating and file ops.
|
|
11
|
-
module TemplateHelpers
|
|
12
|
-
# Track results of templating actions across a single process run.
|
|
13
|
-
# Keys: absolute destination paths (String)
|
|
14
|
-
# Values: Hash with keys: :action (Symbol, one of :create, :replace, :skip, :dir_create, :dir_replace), :timestamp (Time)
|
|
15
|
-
@@template_results = {}
|
|
16
|
-
|
|
17
|
-
EXECUTABLE_GIT_HOOKS_RE = %r{[\\/]\.git-hooks[\\/](commit-msg|prepare-commit-msg)\z}
|
|
18
|
-
# The minimum Ruby supported by setup-ruby GHA
|
|
19
|
-
MIN_SETUP_RUBY = Gem::Version.create("2.3")
|
|
20
|
-
|
|
21
|
-
TEMPLATE_MANIFEST_PATH = File.expand_path("../../..", __dir__) + "/template_manifest.yml"
|
|
22
|
-
RUBY_BASENAMES = %w[Gemfile Rakefile Appraisals Appraisal.root.gemfile .simplecov].freeze
|
|
23
|
-
RUBY_SUFFIXES = %w[.gemspec .gemfile].freeze
|
|
24
|
-
RUBY_EXTENSIONS = %w[.rb .rake].freeze
|
|
25
|
-
@@manifestation = nil
|
|
26
|
-
|
|
27
|
-
module_function
|
|
28
|
-
|
|
29
|
-
# Root of the host project where Rake was invoked
|
|
30
|
-
# @return [String]
|
|
31
|
-
def project_root
|
|
32
|
-
CIHelpers.project_root
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Root of this gem's checkout (repository root when working from source)
|
|
36
|
-
# Calculated relative to lib/kettle/dev/
|
|
37
|
-
# @return [String]
|
|
38
|
-
def gem_checkout_root
|
|
39
|
-
File.expand_path("../../..", __dir__)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Simple yes/no prompt.
|
|
43
|
-
# @param prompt [String]
|
|
44
|
-
# @param default [Boolean]
|
|
45
|
-
# @return [Boolean]
|
|
46
|
-
def ask(prompt, default)
|
|
47
|
-
# Force mode: any prompt resolves to Yes when ENV["force"] is set truthy
|
|
48
|
-
if ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
|
|
49
|
-
puts "#{prompt} #{default ? "[Y/n]" : "[y/N]"}: Y (forced)"
|
|
50
|
-
return true
|
|
51
|
-
end
|
|
52
|
-
print("#{prompt} #{default ? "[Y/n]" : "[y/N]"}: ")
|
|
53
|
-
ans = Kettle::Dev::InputAdapter.gets&.strip
|
|
54
|
-
ans = "" if ans.nil?
|
|
55
|
-
# Normalize explicit no first
|
|
56
|
-
return false if ans =~ /\An(o)?\z/i
|
|
57
|
-
if default
|
|
58
|
-
# Empty -> default true; explicit yes -> true; anything else -> false
|
|
59
|
-
ans.empty? || ans =~ /\Ay(es)?\z/i
|
|
60
|
-
else
|
|
61
|
-
# Empty -> default false; explicit yes -> true; others (including garbage) -> false
|
|
62
|
-
ans =~ /\Ay(es)?\z/i
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Write file content creating directories as needed
|
|
67
|
-
# @param dest_path [String]
|
|
68
|
-
# @param content [String]
|
|
69
|
-
# @return [void]
|
|
70
|
-
def write_file(dest_path, content)
|
|
71
|
-
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
72
|
-
File.open(dest_path, "w") { |f| f.write(content) }
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Prefer an .example variant for a given source path when present
|
|
76
|
-
# For a given intended source path (e.g., "/src/Rakefile"), this will return
|
|
77
|
-
# "/src/Rakefile.example" if it exists, otherwise returns the original path.
|
|
78
|
-
# If the given path already ends with .example, it is returned as-is.
|
|
79
|
-
# @param src_path [String]
|
|
80
|
-
# @return [String]
|
|
81
|
-
def prefer_example(src_path)
|
|
82
|
-
return src_path if src_path.end_with?(".example")
|
|
83
|
-
example = src_path + ".example"
|
|
84
|
-
File.exist?(example) ? example : src_path
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Check if Open Collective is disabled via environment variable.
|
|
88
|
-
# Returns true when OPENCOLLECTIVE_HANDLE or FUNDING_ORG is explicitly set to a falsey value.
|
|
89
|
-
# @return [Boolean]
|
|
90
|
-
def opencollective_disabled?
|
|
91
|
-
oc_handle = ENV["OPENCOLLECTIVE_HANDLE"]
|
|
92
|
-
funding_org = ENV["FUNDING_ORG"]
|
|
93
|
-
|
|
94
|
-
# Check if either variable is explicitly set to false
|
|
95
|
-
[oc_handle, funding_org].any? do |val|
|
|
96
|
-
val && val.to_s.strip.match(Kettle::Dev::ENV_FALSE_RE)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Prefer a .no-osc.example variant when Open Collective is disabled.
|
|
101
|
-
# Otherwise, falls back to prefer_example behavior.
|
|
102
|
-
# For a given source path, this will return:
|
|
103
|
-
# - "path.no-osc.example" if opencollective_disabled? and it exists
|
|
104
|
-
# - Otherwise delegates to prefer_example
|
|
105
|
-
# @param src_path [String]
|
|
106
|
-
# @return [String]
|
|
107
|
-
def prefer_example_with_osc_check(src_path)
|
|
108
|
-
if opencollective_disabled?
|
|
109
|
-
# Try .no-osc.example first
|
|
110
|
-
base = src_path.sub(/\.example\z/, "")
|
|
111
|
-
no_osc = base + ".no-osc.example"
|
|
112
|
-
return no_osc if File.exist?(no_osc)
|
|
113
|
-
end
|
|
114
|
-
prefer_example(src_path)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Check if a file should be skipped when Open Collective is disabled.
|
|
118
|
-
# Returns true for opencollective-specific files when opencollective_disabled? is true.
|
|
119
|
-
# @param relative_path [String] relative path from gem checkout root
|
|
120
|
-
# @return [Boolean]
|
|
121
|
-
def skip_for_disabled_opencollective?(relative_path)
|
|
122
|
-
return false unless opencollective_disabled?
|
|
123
|
-
|
|
124
|
-
opencollective_files = [
|
|
125
|
-
".opencollective.yml",
|
|
126
|
-
".github/workflows/opencollective.yml",
|
|
127
|
-
]
|
|
128
|
-
|
|
129
|
-
opencollective_files.include?(relative_path)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Record a template action for a destination path
|
|
133
|
-
# @param dest_path [String]
|
|
134
|
-
# @param action [Symbol] one of :create, :replace, :skip, :dir_create, :dir_replace
|
|
135
|
-
# @return [void]
|
|
136
|
-
def record_template_result(dest_path, action)
|
|
137
|
-
abs = File.expand_path(dest_path.to_s)
|
|
138
|
-
if action == :skip && @@template_results.key?(abs)
|
|
139
|
-
# Preserve the last meaningful action; do not downgrade to :skip
|
|
140
|
-
return
|
|
141
|
-
end
|
|
142
|
-
@@template_results[abs] = {action: action, timestamp: Time.now}
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Access all template results (read-only clone)
|
|
146
|
-
# @return [Hash]
|
|
147
|
-
def template_results
|
|
148
|
-
@@template_results.clone
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Returns true if the given path was created or replaced by the template task in this run
|
|
152
|
-
# @param dest_path [String]
|
|
153
|
-
# @return [Boolean]
|
|
154
|
-
def modified_by_template?(dest_path)
|
|
155
|
-
rec = @@template_results[File.expand_path(dest_path.to_s)]
|
|
156
|
-
return false unless rec
|
|
157
|
-
[:create, :replace, :dir_create, :dir_replace].include?(rec[:action])
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Ensure git working tree is clean before making changes in a task.
|
|
161
|
-
# If not a git repo, this is a no-op.
|
|
162
|
-
# @param root [String] project root to run git commands in
|
|
163
|
-
# @param task_label [String] name of the rake task for user-facing messages (e.g., "kettle:dev:install")
|
|
164
|
-
# @return [void]
|
|
165
|
-
def ensure_clean_git!(root:, task_label:)
|
|
166
|
-
inside_repo = begin
|
|
167
|
-
system("git", "-C", root.to_s, "rev-parse", "--is-inside-work-tree", out: File::NULL, err: File::NULL)
|
|
168
|
-
rescue StandardError => e
|
|
169
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
170
|
-
false
|
|
171
|
-
end
|
|
172
|
-
return unless inside_repo
|
|
173
|
-
|
|
174
|
-
# Prefer GitAdapter for cleanliness check; fallback to porcelain output
|
|
175
|
-
clean = begin
|
|
176
|
-
Dir.chdir(root.to_s) { Kettle::Dev::GitAdapter.new.clean? }
|
|
177
|
-
rescue StandardError => e
|
|
178
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
179
|
-
nil
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
if clean.nil?
|
|
183
|
-
# Fallback to using the GitAdapter to get both status and preview
|
|
184
|
-
status_output = begin
|
|
185
|
-
ga = Kettle::Dev::GitAdapter.new
|
|
186
|
-
out, ok = ga.capture(["-C", root.to_s, "status", "--porcelain"]) # adapter can use CLI safely
|
|
187
|
-
ok ? out.to_s : ""
|
|
188
|
-
rescue StandardError => e
|
|
189
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
190
|
-
""
|
|
191
|
-
end
|
|
192
|
-
return if status_output.strip.empty?
|
|
193
|
-
preview = status_output.lines.take(10).map(&:rstrip)
|
|
194
|
-
else
|
|
195
|
-
return if clean
|
|
196
|
-
# For messaging, provide a small preview using GitAdapter even when using the adapter
|
|
197
|
-
status_output = begin
|
|
198
|
-
ga = Kettle::Dev::GitAdapter.new
|
|
199
|
-
out, ok = ga.capture(["-C", root.to_s, "status", "--porcelain"]) # read-only query
|
|
200
|
-
ok ? out.to_s : ""
|
|
201
|
-
rescue StandardError => e
|
|
202
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
203
|
-
""
|
|
204
|
-
end
|
|
205
|
-
preview = status_output.lines.take(10).map(&:rstrip)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
puts "ERROR: Your git working tree has uncommitted changes."
|
|
209
|
-
puts "#{task_label} may modify files (e.g., .github/, .gitignore, *.gemspec)."
|
|
210
|
-
puts "Please commit or stash your changes, then re-run: rake #{task_label}"
|
|
211
|
-
unless preview.empty?
|
|
212
|
-
puts "Detected changes:"
|
|
213
|
-
preview.each { |l| puts " #{l}" }
|
|
214
|
-
puts "(showing up to first 10 lines)"
|
|
215
|
-
end
|
|
216
|
-
raise Kettle::Dev::Error, "Aborting: git working tree is not clean."
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Copy a single file with interactive prompts for create/replace.
|
|
220
|
-
# Yields content for transformation when block given.
|
|
221
|
-
# @return [void]
|
|
222
|
-
def copy_file_with_prompt(src_path, dest_path, allow_create: true, allow_replace: true)
|
|
223
|
-
return unless File.exist?(src_path)
|
|
224
|
-
|
|
225
|
-
# Apply optional inclusion filter via ENV["only"] (comma-separated glob patterns relative to project root)
|
|
226
|
-
begin
|
|
227
|
-
only_raw = ENV["only"].to_s
|
|
228
|
-
if !only_raw.empty?
|
|
229
|
-
patterns = only_raw.split(",").map { |s| s.strip }.reject(&:empty?)
|
|
230
|
-
if !patterns.empty?
|
|
231
|
-
proj = project_root.to_s
|
|
232
|
-
rel_dest = dest_path.to_s
|
|
233
|
-
if rel_dest.start_with?(proj + "/")
|
|
234
|
-
rel_dest = rel_dest[(proj.length + 1)..-1]
|
|
235
|
-
elsif rel_dest == proj
|
|
236
|
-
rel_dest = ""
|
|
237
|
-
end
|
|
238
|
-
matched = patterns.any? do |pat|
|
|
239
|
-
if pat.end_with?("/**")
|
|
240
|
-
base = pat[0..-4]
|
|
241
|
-
rel_dest == base || rel_dest.start_with?(base + "/")
|
|
242
|
-
else
|
|
243
|
-
File.fnmatch?(pat, rel_dest, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
unless matched
|
|
247
|
-
record_template_result(dest_path, :skip)
|
|
248
|
-
puts "Skipping #{dest_path} (excluded by only filter)"
|
|
249
|
-
return
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
rescue StandardError => e
|
|
254
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
255
|
-
# If anything goes wrong parsing/matching, ignore the filter and proceed.
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
dest_exists = File.exist?(dest_path)
|
|
259
|
-
action = nil
|
|
260
|
-
if dest_exists
|
|
261
|
-
if allow_replace
|
|
262
|
-
action = ask("Replace #{dest_path}?", true) ? :replace : :skip
|
|
263
|
-
else
|
|
264
|
-
puts "Skipping #{dest_path} (replace not allowed)."
|
|
265
|
-
action = :skip
|
|
266
|
-
end
|
|
267
|
-
elsif allow_create
|
|
268
|
-
action = ask("Create #{dest_path}?", true) ? :create : :skip
|
|
269
|
-
else
|
|
270
|
-
puts "Skipping #{dest_path} (create not allowed)."
|
|
271
|
-
action = :skip
|
|
272
|
-
end
|
|
273
|
-
if action == :skip
|
|
274
|
-
record_template_result(dest_path, :skip)
|
|
275
|
-
return
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
content = File.read(src_path)
|
|
279
|
-
content = yield(content) if block_given?
|
|
280
|
-
# Replace the explicit template token with the literal "kettle-dev"
|
|
281
|
-
# after upstream/template-specific replacements (i.e. after the yield),
|
|
282
|
-
# so the token itself is not altered by those replacements.
|
|
283
|
-
begin
|
|
284
|
-
token = "{KETTLE|DEV|GEM}"
|
|
285
|
-
content = content.gsub(token, "kettle-dev") if content.include?(token)
|
|
286
|
-
rescue StandardError => e
|
|
287
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
basename = File.basename(dest_path.to_s)
|
|
291
|
-
content = apply_appraisals_merge(content, dest_path) if basename == "Appraisals"
|
|
292
|
-
if basename == "Appraisal.root.gemfile" && File.exist?(dest_path)
|
|
293
|
-
begin
|
|
294
|
-
prior = File.read(dest_path)
|
|
295
|
-
content = merge_gemfile_dependencies(content, prior)
|
|
296
|
-
rescue StandardError => e
|
|
297
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
# Apply self-dependency removal for all gem-related files
|
|
302
|
-
# This ensures we don't introduce a self-dependency when templating
|
|
303
|
-
begin
|
|
304
|
-
meta = gemspec_metadata
|
|
305
|
-
gem_name = meta[:gem_name]
|
|
306
|
-
if gem_name && !gem_name.to_s.empty?
|
|
307
|
-
content = remove_self_dependency(content, gem_name, dest_path)
|
|
308
|
-
end
|
|
309
|
-
rescue StandardError => e
|
|
310
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
311
|
-
# If metadata extraction or removal fails, proceed with content as-is
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
write_file(dest_path, content)
|
|
315
|
-
begin
|
|
316
|
-
# Ensure executable bit for git hook scripts when writing under .git-hooks
|
|
317
|
-
if EXECUTABLE_GIT_HOOKS_RE =~ dest_path.to_s
|
|
318
|
-
File.chmod(0o755, dest_path) if File.exist?(dest_path)
|
|
319
|
-
end
|
|
320
|
-
rescue StandardError => e
|
|
321
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
322
|
-
# ignore permission issues
|
|
323
|
-
end
|
|
324
|
-
record_template_result(dest_path, dest_exists ? :replace : :create)
|
|
325
|
-
puts "Wrote #{dest_path}"
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Merge gem dependency lines from a source Gemfile-like content into an existing
|
|
329
|
-
# destination Gemfile-like content. Existing gem lines in the destination win;
|
|
330
|
-
# we only append missing gem declarations from the source at the end of the file.
|
|
331
|
-
# This is deliberately conservative and avoids attempting to relocate gems inside
|
|
332
|
-
# group/platform blocks or reconcile version constraints.
|
|
333
|
-
# @param src_content [String]
|
|
334
|
-
# @param dest_content [String]
|
|
335
|
-
# @return [String] merged content
|
|
336
|
-
def merge_gemfile_dependencies(src_content, dest_content)
|
|
337
|
-
begin
|
|
338
|
-
Kettle::Dev::PrismGemfile.merge_gem_calls(src_content.to_s, dest_content.to_s)
|
|
339
|
-
rescue StandardError => e
|
|
340
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
341
|
-
dest_content
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def apply_appraisals_merge(content, dest_path)
|
|
346
|
-
dest = dest_path.to_s
|
|
347
|
-
existing = if File.exist?(dest)
|
|
348
|
-
File.read(dest)
|
|
349
|
-
else
|
|
350
|
-
""
|
|
351
|
-
end
|
|
352
|
-
Kettle::Dev::PrismAppraisals.merge(content, existing)
|
|
353
|
-
rescue StandardError => e
|
|
354
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
355
|
-
content
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# Remove self-referential gem dependencies from content based on file type.
|
|
359
|
-
# Applies to gemspec, Gemfile, modular gemfiles, Appraisal.root.gemfile, and Appraisals.
|
|
360
|
-
# @param content [String] file content
|
|
361
|
-
# @param gem_name [String] the gem name to remove
|
|
362
|
-
# @param file_path [String] path to the file (used to determine type)
|
|
363
|
-
# @return [String] content with self-dependencies removed
|
|
364
|
-
def remove_self_dependency(content, gem_name, file_path)
|
|
365
|
-
return content if gem_name.to_s.strip.empty?
|
|
366
|
-
|
|
367
|
-
basename = File.basename(file_path.to_s)
|
|
368
|
-
|
|
369
|
-
begin
|
|
370
|
-
case basename
|
|
371
|
-
when /\.gemspec$/
|
|
372
|
-
# Use PrismGemspec for gemspec files
|
|
373
|
-
Kettle::Dev::PrismGemspec.remove_spec_dependency(content, gem_name)
|
|
374
|
-
when "Gemfile", "Appraisal.root.gemfile", /\.gemfile$/
|
|
375
|
-
# Use PrismGemfile for Gemfile-like files
|
|
376
|
-
Kettle::Dev::PrismGemfile.remove_gem_dependency(content, gem_name)
|
|
377
|
-
when "Appraisals"
|
|
378
|
-
# Use PrismAppraisals for Appraisals files
|
|
379
|
-
Kettle::Dev::PrismAppraisals.remove_gem_dependency(content, gem_name)
|
|
380
|
-
else
|
|
381
|
-
# Return content unchanged for unknown file types
|
|
382
|
-
content
|
|
383
|
-
end
|
|
384
|
-
rescue StandardError => e
|
|
385
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
386
|
-
content
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Copy a directory tree, prompting before creating or overwriting.
|
|
391
|
-
# @return [void]
|
|
392
|
-
def copy_dir_with_prompt(src_dir, dest_dir)
|
|
393
|
-
return unless Dir.exist?(src_dir)
|
|
394
|
-
|
|
395
|
-
# Build a matcher for ENV["only"], relative to project root, that can be reused within this method
|
|
396
|
-
only_raw = ENV["only"].to_s
|
|
397
|
-
patterns = only_raw.split(",").map { |s| s.strip }.reject(&:empty?) unless only_raw.nil?
|
|
398
|
-
patterns ||= []
|
|
399
|
-
proj_root = project_root.to_s
|
|
400
|
-
matches_only = lambda do |abs_dest|
|
|
401
|
-
return true if patterns.empty?
|
|
402
|
-
begin
|
|
403
|
-
rel_dest = abs_dest.to_s
|
|
404
|
-
if rel_dest.start_with?(proj_root + "/")
|
|
405
|
-
rel_dest = rel_dest[(proj_root.length + 1)..-1]
|
|
406
|
-
elsif rel_dest == proj_root
|
|
407
|
-
rel_dest = ""
|
|
408
|
-
end
|
|
409
|
-
patterns.any? do |pat|
|
|
410
|
-
if pat.end_with?("/**")
|
|
411
|
-
base = pat[0..-4]
|
|
412
|
-
rel_dest == base || rel_dest.start_with?(base + "/")
|
|
413
|
-
else
|
|
414
|
-
File.fnmatch?(pat, rel_dest, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
rescue StandardError => e
|
|
418
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
419
|
-
# On any error, do not filter out (act as matched)
|
|
420
|
-
true
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
# Early exit: if an only filter is present and no files inside this directory would match,
|
|
425
|
-
# do not prompt to create/replace this directory at all.
|
|
426
|
-
begin
|
|
427
|
-
if !patterns.empty?
|
|
428
|
-
any_match = false
|
|
429
|
-
Find.find(src_dir) do |path|
|
|
430
|
-
rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
|
|
431
|
-
next if rel.empty?
|
|
432
|
-
next if File.directory?(path)
|
|
433
|
-
target = File.join(dest_dir, rel)
|
|
434
|
-
if matches_only.call(target)
|
|
435
|
-
any_match = true
|
|
436
|
-
break
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
unless any_match
|
|
440
|
-
record_template_result(dest_dir, :skip)
|
|
441
|
-
return
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
rescue StandardError => e
|
|
445
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
446
|
-
# If determining matches fails, fall through to prompting logic
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
dest_exists = Dir.exist?(dest_dir)
|
|
450
|
-
if dest_exists
|
|
451
|
-
if ask("Replace directory #{dest_dir} (will overwrite files)?", true)
|
|
452
|
-
Find.find(src_dir) do |path|
|
|
453
|
-
rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
|
|
454
|
-
next if rel.empty?
|
|
455
|
-
target = File.join(dest_dir, rel)
|
|
456
|
-
if File.directory?(path)
|
|
457
|
-
FileUtils.mkdir_p(target)
|
|
458
|
-
else
|
|
459
|
-
# Per-file inclusion filter
|
|
460
|
-
next unless matches_only.call(target)
|
|
461
|
-
|
|
462
|
-
FileUtils.mkdir_p(File.dirname(target))
|
|
463
|
-
if File.exist?(target)
|
|
464
|
-
|
|
465
|
-
# Skip only if contents are identical. If source and target paths are the same,
|
|
466
|
-
# avoid FileUtils.cp (which raises) and do an in-place rewrite to satisfy "copy".
|
|
467
|
-
begin
|
|
468
|
-
if FileUtils.compare_file(path, target)
|
|
469
|
-
next
|
|
470
|
-
elsif path == target
|
|
471
|
-
data = File.binread(path)
|
|
472
|
-
File.open(target, "wb") { |f| f.write(data) }
|
|
473
|
-
next
|
|
474
|
-
end
|
|
475
|
-
rescue StandardError => e
|
|
476
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
477
|
-
# ignore compare errors; fall through to copy
|
|
478
|
-
end
|
|
479
|
-
end
|
|
480
|
-
FileUtils.cp(path, target)
|
|
481
|
-
begin
|
|
482
|
-
# Ensure executable bit for git hook scripts when copying under .git-hooks
|
|
483
|
-
if target.end_with?("/.git-hooks/commit-msg", "/.git-hooks/prepare-commit-msg") ||
|
|
484
|
-
EXECUTABLE_GIT_HOOKS_RE =~ target
|
|
485
|
-
File.chmod(0o755, target)
|
|
486
|
-
end
|
|
487
|
-
rescue StandardError => e
|
|
488
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
489
|
-
# ignore permission issues
|
|
490
|
-
end
|
|
491
|
-
end
|
|
492
|
-
end
|
|
493
|
-
puts "Updated #{dest_dir}"
|
|
494
|
-
record_template_result(dest_dir, :dir_replace)
|
|
495
|
-
else
|
|
496
|
-
puts "Skipped #{dest_dir}"
|
|
497
|
-
record_template_result(dest_dir, :skip)
|
|
498
|
-
end
|
|
499
|
-
elsif ask("Create directory #{dest_dir}?", true)
|
|
500
|
-
FileUtils.mkdir_p(dest_dir)
|
|
501
|
-
Find.find(src_dir) do |path|
|
|
502
|
-
rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
|
|
503
|
-
next if rel.empty?
|
|
504
|
-
target = File.join(dest_dir, rel)
|
|
505
|
-
if File.directory?(path)
|
|
506
|
-
FileUtils.mkdir_p(target)
|
|
507
|
-
else
|
|
508
|
-
# Per-file inclusion filter
|
|
509
|
-
next unless matches_only.call(target)
|
|
510
|
-
|
|
511
|
-
FileUtils.mkdir_p(File.dirname(target))
|
|
512
|
-
if File.exist?(target)
|
|
513
|
-
# Skip only if contents are identical. If source and target paths are the same,
|
|
514
|
-
# avoid FileUtils.cp (which raises) and do an in-place rewrite to satisfy "copy".
|
|
515
|
-
begin
|
|
516
|
-
if FileUtils.compare_file(path, target)
|
|
517
|
-
next
|
|
518
|
-
elsif path == target
|
|
519
|
-
data = File.binread(path)
|
|
520
|
-
File.open(target, "wb") { |f| f.write(data) }
|
|
521
|
-
next
|
|
522
|
-
end
|
|
523
|
-
rescue StandardError => e
|
|
524
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
525
|
-
# ignore compare errors; fall through to copy
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
FileUtils.cp(path, target)
|
|
529
|
-
begin
|
|
530
|
-
# Ensure executable bit for git hook scripts when copying under .git-hooks
|
|
531
|
-
if target.end_with?("/.git-hooks/commit-msg", "/.git-hooks/prepare-commit-msg") ||
|
|
532
|
-
EXECUTABLE_GIT_HOOKS_RE =~ target
|
|
533
|
-
File.chmod(0o755, target)
|
|
534
|
-
end
|
|
535
|
-
rescue StandardError => e
|
|
536
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
537
|
-
# ignore permission issues
|
|
538
|
-
end
|
|
539
|
-
end
|
|
540
|
-
end
|
|
541
|
-
puts "Created #{dest_dir}"
|
|
542
|
-
record_template_result(dest_dir, :dir_create)
|
|
543
|
-
end
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
# Apply common token replacements used when templating text files
|
|
547
|
-
# @param content [String]
|
|
548
|
-
# @param org [String, nil]
|
|
549
|
-
# @param gem_name [String]
|
|
550
|
-
# @param namespace [String]
|
|
551
|
-
# @param namespace_shield [String]
|
|
552
|
-
# @param gem_shield [String]
|
|
553
|
-
# @param funding_org [String, nil]
|
|
554
|
-
# @return [String]
|
|
555
|
-
def apply_common_replacements(content, org:, gem_name:, namespace:, namespace_shield:, gem_shield:, funding_org: nil, min_ruby: nil)
|
|
556
|
-
raise Error, "Org could not be derived" unless org && !org.empty?
|
|
557
|
-
raise Error, "Gem name could not be derived" unless gem_name && !gem_name.empty?
|
|
558
|
-
|
|
559
|
-
funding_org ||= org
|
|
560
|
-
# Derive min_ruby if not provided
|
|
561
|
-
mr = begin
|
|
562
|
-
meta = gemspec_metadata
|
|
563
|
-
meta[:min_ruby]
|
|
564
|
-
rescue StandardError => e
|
|
565
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
566
|
-
# leave min_ruby as-is (possibly nil)
|
|
567
|
-
end
|
|
568
|
-
if min_ruby.nil? || min_ruby.to_s.strip.empty?
|
|
569
|
-
min_ruby = mr.respond_to?(:to_s) ? mr.to_s : mr
|
|
570
|
-
end
|
|
571
|
-
|
|
572
|
-
# Derive min_dev_ruby from min_ruby
|
|
573
|
-
# min_dev_ruby is the greater of min_dev_ruby and ruby 2.3,
|
|
574
|
-
# because ruby 2.3 is the minimum ruby supported by setup-ruby GHA
|
|
575
|
-
min_dev_ruby = begin
|
|
576
|
-
[mr, MIN_SETUP_RUBY].max
|
|
577
|
-
rescue StandardError => e
|
|
578
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
579
|
-
MIN_SETUP_RUBY
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
c = content.dup
|
|
583
|
-
c = c.gsub("kettle-rb", org.to_s)
|
|
584
|
-
c = c.gsub("{OPENCOLLECTIVE|ORG_NAME}", funding_org || "opencollective")
|
|
585
|
-
# Replace min ruby token if present
|
|
586
|
-
begin
|
|
587
|
-
if min_ruby && !min_ruby.to_s.empty? && c.include?("{K_D_MIN_RUBY}")
|
|
588
|
-
c = c.gsub("{K_D_MIN_RUBY}", min_ruby.to_s)
|
|
589
|
-
end
|
|
590
|
-
rescue StandardError => e
|
|
591
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
592
|
-
# ignore
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
# Replace min ruby dev token if present
|
|
596
|
-
begin
|
|
597
|
-
if min_dev_ruby && !min_dev_ruby.to_s.empty? && c.include?("{K_D_MIN_DEV_RUBY}")
|
|
598
|
-
c = c.gsub("{K_D_MIN_DEV_RUBY}", min_dev_ruby.to_s)
|
|
599
|
-
end
|
|
600
|
-
rescue StandardError => e
|
|
601
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
602
|
-
# ignore
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
# Replace target gem name token if present
|
|
606
|
-
begin
|
|
607
|
-
token = "{TARGET|GEM|NAME}"
|
|
608
|
-
c = c.gsub(token, gem_name) if c.include?(token)
|
|
609
|
-
rescue StandardError => e
|
|
610
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
611
|
-
# If replacement fails unexpectedly, proceed with content as-is
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
# Special-case: yard-head link uses the gem name as a subdomain and must be dashes-only.
|
|
615
|
-
# Apply this BEFORE other generic replacements so it isn't altered incorrectly.
|
|
616
|
-
begin
|
|
617
|
-
dashed = gem_name.tr("_", "-")
|
|
618
|
-
c = c.gsub("[🚎yard-head]: https://kettle-dev.galtzo.com", "[🚎yard-head]: https://#{dashed}.galtzo.com")
|
|
619
|
-
rescue StandardError => e
|
|
620
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
621
|
-
# ignore
|
|
622
|
-
end
|
|
623
|
-
|
|
624
|
-
# Replace occurrences of the literal template gem name ("kettle-dev")
|
|
625
|
-
# with the destination gem name.
|
|
626
|
-
c = c.gsub("kettle-dev", gem_name)
|
|
627
|
-
c = c.gsub(/\bKettle::Dev\b/u, namespace) unless namespace.empty?
|
|
628
|
-
c = c.gsub("Kettle%3A%3ADev", namespace_shield) unless namespace_shield.empty?
|
|
629
|
-
c = c.gsub("kettle--dev", gem_shield)
|
|
630
|
-
# Replace require and path structures with gem_name, modifying - to / if needed
|
|
631
|
-
c.gsub("kettle/dev", gem_name.tr("-", "/"))
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
# Parse gemspec metadata and derive useful strings
|
|
635
|
-
# @param root [String] project root
|
|
636
|
-
# @return [Hash]
|
|
637
|
-
def gemspec_metadata(root = project_root)
|
|
638
|
-
Kettle::Dev::GemSpecReader.load(root)
|
|
639
|
-
end
|
|
640
|
-
|
|
641
|
-
def apply_strategy(content, dest_path)
|
|
642
|
-
return content unless ruby_template?(dest_path)
|
|
643
|
-
strategy = strategy_for(dest_path)
|
|
644
|
-
dest_content = File.exist?(dest_path) ? File.read(dest_path) : ""
|
|
645
|
-
Kettle::Dev::SourceMerger.apply(strategy: strategy, src: content, dest: dest_content, path: rel_path(dest_path))
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
def manifestation
|
|
649
|
-
@@manifestation ||= load_manifest
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
def strategy_for(dest_path)
|
|
653
|
-
relative = rel_path(dest_path)
|
|
654
|
-
manifestation.find do |entry|
|
|
655
|
-
File.fnmatch?(entry[:path], relative, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
|
|
656
|
-
end&.fetch(:strategy, :skip) || :skip
|
|
657
|
-
end
|
|
658
|
-
|
|
659
|
-
def rel_path(path)
|
|
660
|
-
project = project_root.to_s
|
|
661
|
-
path.to_s.sub(/^#{Regexp.escape(project)}\/?/, "")
|
|
662
|
-
end
|
|
663
|
-
|
|
664
|
-
def ruby_template?(dest_path)
|
|
665
|
-
base = File.basename(dest_path.to_s)
|
|
666
|
-
return true if RUBY_BASENAMES.include?(base)
|
|
667
|
-
return true if RUBY_SUFFIXES.any? { |suffix| base.end_with?(suffix) }
|
|
668
|
-
ext = File.extname(base)
|
|
669
|
-
RUBY_EXTENSIONS.include?(ext)
|
|
670
|
-
end
|
|
671
|
-
|
|
672
|
-
def load_manifest
|
|
673
|
-
raw = YAML.load_file(TEMPLATE_MANIFEST_PATH)
|
|
674
|
-
raw.map do |entry|
|
|
675
|
-
{
|
|
676
|
-
path: entry["path"],
|
|
677
|
-
strategy: entry["strategy"].to_s.strip.downcase.to_sym,
|
|
678
|
-
}
|
|
679
|
-
end
|
|
680
|
-
rescue Errno::ENOENT
|
|
681
|
-
[]
|
|
682
|
-
end
|
|
683
|
-
end
|
|
684
|
-
end
|
|
685
|
-
end
|