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,975 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kettle
|
|
4
|
-
module Dev
|
|
5
|
-
module Tasks
|
|
6
|
-
# Thin wrapper to expose the kettle:dev:template task logic as a callable API
|
|
7
|
-
# for testability. The rake task should only call this method.
|
|
8
|
-
module TemplateTask
|
|
9
|
-
MODULAR_GEMFILE_DIR = "gemfiles/modular"
|
|
10
|
-
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
# Ensure every Markdown atx-style heading line has exactly one blank line
|
|
14
|
-
# before and after, skipping content inside fenced code blocks.
|
|
15
|
-
def normalize_heading_spacing(text)
|
|
16
|
-
lines = text.split("\n", -1)
|
|
17
|
-
out = []
|
|
18
|
-
in_fence = false
|
|
19
|
-
fence_re = /^\s*```/
|
|
20
|
-
heading_re = /^\s*#+\s+.+/
|
|
21
|
-
lines.each_with_index do |ln, idx|
|
|
22
|
-
if ln =~ fence_re
|
|
23
|
-
in_fence = !in_fence
|
|
24
|
-
out << ln
|
|
25
|
-
next
|
|
26
|
-
end
|
|
27
|
-
if !in_fence && ln =~ heading_re
|
|
28
|
-
prev_blank = out.empty? ? false : out.last.to_s.strip == ""
|
|
29
|
-
out << "" unless out.empty? || prev_blank
|
|
30
|
-
out << ln
|
|
31
|
-
nxt = lines[idx + 1]
|
|
32
|
-
out << "" unless nxt.to_s.strip == ""
|
|
33
|
-
else
|
|
34
|
-
out << ln
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
# Collapse accidental multiple blanks
|
|
38
|
-
collapsed = []
|
|
39
|
-
out.each do |l|
|
|
40
|
-
if l.strip == "" && collapsed.last.to_s.strip == ""
|
|
41
|
-
next
|
|
42
|
-
end
|
|
43
|
-
collapsed << l
|
|
44
|
-
end
|
|
45
|
-
collapsed.join("\n")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Abort wrapper that avoids terminating the entire process during specs
|
|
49
|
-
def task_abort(msg)
|
|
50
|
-
raise Kettle::Dev::Error, msg
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Execute the template operation into the current project.
|
|
54
|
-
# All options/IO are controlled via TemplateHelpers and ENV.
|
|
55
|
-
def run
|
|
56
|
-
# Inline the former rake task body, but using helpers directly.
|
|
57
|
-
helpers = Kettle::Dev::TemplateHelpers
|
|
58
|
-
|
|
59
|
-
project_root = helpers.project_root
|
|
60
|
-
gem_checkout_root = helpers.gem_checkout_root
|
|
61
|
-
|
|
62
|
-
# Ensure git working tree is clean before making changes (when run standalone)
|
|
63
|
-
helpers.ensure_clean_git!(root: project_root, task_label: "kettle:dev:template")
|
|
64
|
-
|
|
65
|
-
meta = helpers.gemspec_metadata(project_root)
|
|
66
|
-
gem_name = meta[:gem_name]
|
|
67
|
-
min_ruby = meta[:min_ruby]
|
|
68
|
-
forge_org = meta[:forge_org] || meta[:gh_org]
|
|
69
|
-
funding_org = helpers.opencollective_disabled? ? nil : meta[:funding_org] || forge_org
|
|
70
|
-
entrypoint_require = meta[:entrypoint_require]
|
|
71
|
-
namespace = meta[:namespace]
|
|
72
|
-
namespace_shield = meta[:namespace_shield]
|
|
73
|
-
gem_shield = meta[:gem_shield]
|
|
74
|
-
|
|
75
|
-
# 1) .devcontainer directory
|
|
76
|
-
helpers.copy_dir_with_prompt(File.join(gem_checkout_root, ".devcontainer"), File.join(project_root, ".devcontainer"))
|
|
77
|
-
|
|
78
|
-
# 2) .github/**/*.yml with FUNDING.yml customizations
|
|
79
|
-
source_github_dir = File.join(gem_checkout_root, ".github")
|
|
80
|
-
if Dir.exist?(source_github_dir)
|
|
81
|
-
# Build a unique set of logical .yml paths, preferring the .example variant when present
|
|
82
|
-
candidates = Dir.glob(File.join(source_github_dir, "**", "*.yml")) +
|
|
83
|
-
Dir.glob(File.join(source_github_dir, "**", "*.yml.example"))
|
|
84
|
-
selected = {}
|
|
85
|
-
candidates.each do |path|
|
|
86
|
-
# Key by the path without the optional .example suffix
|
|
87
|
-
key = path.sub(/\.example\z/, "")
|
|
88
|
-
# Prefer example: overwrite a plain selection with .example, but do not downgrade
|
|
89
|
-
if path.end_with?(".example")
|
|
90
|
-
selected[key] = path
|
|
91
|
-
else
|
|
92
|
-
selected[key] ||= path
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
# Parse optional include patterns (comma-separated globs relative to project root)
|
|
96
|
-
include_raw = ENV["include"].to_s
|
|
97
|
-
include_patterns = include_raw.split(",").map { |s| s.strip }.reject(&:empty?)
|
|
98
|
-
matches_include = lambda do |abs_dest|
|
|
99
|
-
return false if include_patterns.empty?
|
|
100
|
-
begin
|
|
101
|
-
rel_dest = abs_dest.to_s
|
|
102
|
-
proj = project_root.to_s
|
|
103
|
-
if rel_dest.start_with?(proj + "/")
|
|
104
|
-
rel_dest = rel_dest[(proj.length + 1)..-1]
|
|
105
|
-
elsif rel_dest == proj
|
|
106
|
-
rel_dest = ""
|
|
107
|
-
end
|
|
108
|
-
include_patterns.any? do |pat|
|
|
109
|
-
if pat.end_with?("/**")
|
|
110
|
-
base = pat[0..-4]
|
|
111
|
-
rel_dest == base || rel_dest.start_with?(base + "/")
|
|
112
|
-
else
|
|
113
|
-
File.fnmatch?(pat, rel_dest, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
118
|
-
false
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
selected.values.each do |orig_src|
|
|
123
|
-
src = helpers.prefer_example_with_osc_check(orig_src)
|
|
124
|
-
# Destination path should never include the .example suffix.
|
|
125
|
-
rel = orig_src.sub(/^#{Regexp.escape(gem_checkout_root)}\/?/, "").sub(/\.example\z/, "")
|
|
126
|
-
dest = File.join(project_root, rel)
|
|
127
|
-
|
|
128
|
-
# Skip opencollective-specific files when Open Collective is disabled
|
|
129
|
-
if helpers.skip_for_disabled_opencollective?(rel)
|
|
130
|
-
puts "Skipping #{rel} (Open Collective disabled)"
|
|
131
|
-
next
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Optional file: .github/workflows/discord-notifier.yml should NOT be copied by default.
|
|
135
|
-
# Only copy when --include matches it.
|
|
136
|
-
if rel == ".github/workflows/discord-notifier.yml"
|
|
137
|
-
unless matches_include.call(dest)
|
|
138
|
-
# Explicitly skip without prompting
|
|
139
|
-
next
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
if File.basename(rel) == "FUNDING.yml"
|
|
144
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
145
|
-
c = content.dup
|
|
146
|
-
# Effective funding handle should fall back to forge_org when funding_org is nil.
|
|
147
|
-
# This allows tests to stub FUNDING_ORG=false to bypass explicit funding detection
|
|
148
|
-
# while still templating the line with the derived organization (e.g., from homepage URL).
|
|
149
|
-
effective_funding = funding_org || forge_org
|
|
150
|
-
c = if helpers.opencollective_disabled?
|
|
151
|
-
c.gsub(/^open_collective:\s+.*$/i) { |line| "open_collective: # Replace with a single Open Collective username" }
|
|
152
|
-
else
|
|
153
|
-
c.gsub(/^open_collective:\s+.*$/i) { |line| effective_funding ? "open_collective: #{effective_funding}" : line }
|
|
154
|
-
end
|
|
155
|
-
if gem_name && !gem_name.empty?
|
|
156
|
-
c = c.gsub(/^tidelift:\s+.*$/i, "tidelift: rubygems/#{gem_name}")
|
|
157
|
-
end
|
|
158
|
-
helpers.apply_common_replacements(
|
|
159
|
-
c,
|
|
160
|
-
org: forge_org,
|
|
161
|
-
funding_org: effective_funding, # pass effective funding for downstream tokens
|
|
162
|
-
gem_name: gem_name,
|
|
163
|
-
namespace: namespace,
|
|
164
|
-
namespace_shield: namespace_shield,
|
|
165
|
-
gem_shield: gem_shield,
|
|
166
|
-
min_ruby: min_ruby,
|
|
167
|
-
)
|
|
168
|
-
end
|
|
169
|
-
else
|
|
170
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
171
|
-
helpers.apply_common_replacements(
|
|
172
|
-
content,
|
|
173
|
-
org: forge_org,
|
|
174
|
-
funding_org: funding_org,
|
|
175
|
-
gem_name: gem_name,
|
|
176
|
-
namespace: namespace,
|
|
177
|
-
namespace_shield: namespace_shield,
|
|
178
|
-
gem_shield: gem_shield,
|
|
179
|
-
min_ruby: min_ruby,
|
|
180
|
-
)
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# 3) .qlty/qlty.toml
|
|
187
|
-
helpers.copy_file_with_prompt(
|
|
188
|
-
helpers.prefer_example(File.join(gem_checkout_root, ".qlty/qlty.toml")),
|
|
189
|
-
File.join(project_root, ".qlty/qlty.toml"),
|
|
190
|
-
allow_create: true,
|
|
191
|
-
allow_replace: true,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# 4) gemfiles/modular/* and nested directories (delegated for DRYness)
|
|
195
|
-
Kettle::Dev::ModularGemfiles.sync!(
|
|
196
|
-
helpers: helpers,
|
|
197
|
-
project_root: project_root,
|
|
198
|
-
gem_checkout_root: gem_checkout_root,
|
|
199
|
-
min_ruby: min_ruby,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
# 5) spec/spec_helper.rb (no create)
|
|
203
|
-
dest_spec_helper = File.join(project_root, "spec/spec_helper.rb")
|
|
204
|
-
if File.file?(dest_spec_helper)
|
|
205
|
-
old = File.read(dest_spec_helper)
|
|
206
|
-
if old.include?('require "kettle/dev"') || old.include?("require 'kettle/dev'")
|
|
207
|
-
replacement = %(require "#{entrypoint_require}")
|
|
208
|
-
new_content = old.gsub(/require\s+["']kettle\/dev["']/, replacement)
|
|
209
|
-
if new_content != old
|
|
210
|
-
if helpers.ask("Replace require \"kettle/dev\" in spec/spec_helper.rb with #{replacement}?", true)
|
|
211
|
-
helpers.write_file(dest_spec_helper, new_content)
|
|
212
|
-
puts "Updated require in spec/spec_helper.rb"
|
|
213
|
-
else
|
|
214
|
-
puts "Skipped modifying spec/spec_helper.rb"
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# 6) .env.local special case: never read or touch .env.local from source; only copy .env.local.example to .env.local.example
|
|
221
|
-
begin
|
|
222
|
-
envlocal_src = File.join(gem_checkout_root, ".env.local.example")
|
|
223
|
-
envlocal_dest = File.join(project_root, ".env.local.example")
|
|
224
|
-
if File.exist?(envlocal_src)
|
|
225
|
-
helpers.copy_file_with_prompt(envlocal_src, envlocal_dest, allow_create: true, allow_replace: true)
|
|
226
|
-
end
|
|
227
|
-
rescue StandardError => e
|
|
228
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
229
|
-
puts "WARNING: Skipped .env.local example copy due to #{e.class}: #{e.message}"
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# 7) Root and other files
|
|
233
|
-
# 7a) Special-case: gemspec example must be renamed to destination gem's name
|
|
234
|
-
begin
|
|
235
|
-
# Prefer the .example variant when present
|
|
236
|
-
gemspec_template_src = helpers.prefer_example(File.join(gem_checkout_root, "kettle-dev.gemspec"))
|
|
237
|
-
if File.exist?(gemspec_template_src)
|
|
238
|
-
dest_gemspec = if gem_name && !gem_name.to_s.empty?
|
|
239
|
-
File.join(project_root, "#{gem_name}.gemspec")
|
|
240
|
-
else
|
|
241
|
-
# Fallback rules:
|
|
242
|
-
# 1) Prefer any existing gemspec in the destination project
|
|
243
|
-
existing = Dir.glob(File.join(project_root, "*.gemspec")).sort.first
|
|
244
|
-
if existing
|
|
245
|
-
existing
|
|
246
|
-
else
|
|
247
|
-
# 2) If none, use the example file's name with ".example" removed
|
|
248
|
-
fallback_name = File.basename(gemspec_template_src).sub(/\.example\z/, "")
|
|
249
|
-
File.join(project_root, fallback_name)
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# If a destination gemspec already exists, get metadata from GemSpecReader via helpers
|
|
254
|
-
orig_meta = nil
|
|
255
|
-
dest_existed = File.exist?(dest_gemspec)
|
|
256
|
-
if dest_existed
|
|
257
|
-
begin
|
|
258
|
-
orig_meta = helpers.gemspec_metadata(File.dirname(dest_gemspec))
|
|
259
|
-
rescue StandardError => e
|
|
260
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
261
|
-
orig_meta = nil
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
helpers.copy_file_with_prompt(gemspec_template_src, dest_gemspec, allow_create: true, allow_replace: true) do |content|
|
|
266
|
-
# First apply standard replacements from the template example, but only
|
|
267
|
-
# when we have a usable gem_name. If gem_name is unknown, leave content as-is
|
|
268
|
-
# to allow filename fallback behavior without raising.
|
|
269
|
-
c = if gem_name && !gem_name.to_s.empty?
|
|
270
|
-
helpers.apply_common_replacements(
|
|
271
|
-
content,
|
|
272
|
-
org: forge_org,
|
|
273
|
-
funding_org: funding_org,
|
|
274
|
-
gem_name: gem_name,
|
|
275
|
-
namespace: namespace,
|
|
276
|
-
namespace_shield: namespace_shield,
|
|
277
|
-
gem_shield: gem_shield,
|
|
278
|
-
min_ruby: min_ruby,
|
|
279
|
-
)
|
|
280
|
-
else
|
|
281
|
-
content.dup
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
if orig_meta
|
|
285
|
-
# Build replacements using AST-aware helper to carry over fields
|
|
286
|
-
repl = {}
|
|
287
|
-
if (name = orig_meta[:gem_name]) && !name.to_s.empty?
|
|
288
|
-
repl[:name] = name.to_s
|
|
289
|
-
end
|
|
290
|
-
repl[:authors] = Array(orig_meta[:authors]).map(&:to_s) if orig_meta[:authors]
|
|
291
|
-
repl[:email] = Array(orig_meta[:email]).map(&:to_s) if orig_meta[:email]
|
|
292
|
-
repl[:summary] = orig_meta[:summary].to_s if orig_meta[:summary]
|
|
293
|
-
repl[:description] = orig_meta[:description].to_s if orig_meta[:description]
|
|
294
|
-
repl[:licenses] = Array(orig_meta[:licenses]).map(&:to_s) if orig_meta[:licenses]
|
|
295
|
-
if orig_meta[:required_ruby_version]
|
|
296
|
-
repl[:required_ruby_version] = orig_meta[:required_ruby_version].to_s
|
|
297
|
-
end
|
|
298
|
-
repl[:require_paths] = Array(orig_meta[:require_paths]).map(&:to_s) if orig_meta[:require_paths]
|
|
299
|
-
repl[:bindir] = orig_meta[:bindir].to_s if orig_meta[:bindir]
|
|
300
|
-
repl[:executables] = Array(orig_meta[:executables]).map(&:to_s) if orig_meta[:executables]
|
|
301
|
-
|
|
302
|
-
begin
|
|
303
|
-
c = Kettle::Dev::PrismGemspec.replace_gemspec_fields(c, repl)
|
|
304
|
-
rescue StandardError => e
|
|
305
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
306
|
-
# Best-effort carry-over; ignore failure and keep c as-is
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Ensure we do not introduce a self-dependency when templating the gemspec.
|
|
311
|
-
# If the template included a dependency on the template gem (e.g., "kettle-dev"),
|
|
312
|
-
# the common replacements would have turned it into the destination gem's name.
|
|
313
|
-
# Strip any dependency lines that name the destination gem.
|
|
314
|
-
begin
|
|
315
|
-
if gem_name && !gem_name.to_s.empty?
|
|
316
|
-
begin
|
|
317
|
-
c = Kettle::Dev::PrismGemspec.remove_spec_dependency(c, gem_name)
|
|
318
|
-
rescue StandardError => e
|
|
319
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
rescue StandardError => e
|
|
323
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
324
|
-
# If anything goes wrong, keep the content as-is rather than failing the task
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
if dest_existed
|
|
328
|
-
begin
|
|
329
|
-
merged = helpers.apply_strategy(c, dest_gemspec)
|
|
330
|
-
c = merged if merged.is_a?(String) && !merged.empty?
|
|
331
|
-
rescue StandardError => e
|
|
332
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
c
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
rescue StandardError => e
|
|
340
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
341
|
-
# Do not fail the entire template task if gemspec copy has issues
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
files_to_copy = %w[
|
|
345
|
-
.aiignore
|
|
346
|
-
.envrc
|
|
347
|
-
.gitignore
|
|
348
|
-
.idea/.gitignore
|
|
349
|
-
.gitlab-ci.yml
|
|
350
|
-
.junie/guidelines-rbs.md
|
|
351
|
-
.junie/guidelines.md
|
|
352
|
-
.licenserc.yaml
|
|
353
|
-
.opencollective.yml
|
|
354
|
-
.rspec
|
|
355
|
-
.rubocop.yml
|
|
356
|
-
.rubocop_rspec.yml
|
|
357
|
-
.simplecov
|
|
358
|
-
.tool-versions
|
|
359
|
-
.yardopts
|
|
360
|
-
.yardignore
|
|
361
|
-
Appraisal.root.gemfile
|
|
362
|
-
Appraisals
|
|
363
|
-
CHANGELOG.md
|
|
364
|
-
CITATION.cff
|
|
365
|
-
CODE_OF_CONDUCT.md
|
|
366
|
-
CONTRIBUTING.md
|
|
367
|
-
FUNDING.md
|
|
368
|
-
Gemfile
|
|
369
|
-
README.md
|
|
370
|
-
RUBOCOP.md
|
|
371
|
-
Rakefile
|
|
372
|
-
SECURITY.md
|
|
373
|
-
]
|
|
374
|
-
|
|
375
|
-
# Snapshot existing README content once (for H1 prefix preservation after write)
|
|
376
|
-
existing_readme_before = begin
|
|
377
|
-
path = File.join(project_root, "README.md")
|
|
378
|
-
File.file?(path) ? File.read(path) : nil
|
|
379
|
-
rescue StandardError => e
|
|
380
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
381
|
-
nil
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
files_to_copy.each do |rel|
|
|
385
|
-
# Skip opencollective-specific files when Open Collective is disabled
|
|
386
|
-
if helpers.skip_for_disabled_opencollective?(rel)
|
|
387
|
-
puts "Skipping #{rel} (Open Collective disabled)"
|
|
388
|
-
next
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
src = helpers.prefer_example_with_osc_check(File.join(gem_checkout_root, rel))
|
|
392
|
-
dest = File.join(project_root, rel)
|
|
393
|
-
next unless File.exist?(src)
|
|
394
|
-
|
|
395
|
-
if File.basename(rel) == "README.md"
|
|
396
|
-
# Precompute destination README H1 prefix (emoji(s) or first grapheme) before any overwrite occurs
|
|
397
|
-
prev_readme = File.exist?(dest) ? File.read(dest) : nil
|
|
398
|
-
begin
|
|
399
|
-
if prev_readme
|
|
400
|
-
first_h1_prev = prev_readme.lines.find { |ln| ln =~ /^#\s+/ }
|
|
401
|
-
if first_h1_prev
|
|
402
|
-
emoji_re = Kettle::EmojiRegex::REGEX
|
|
403
|
-
tail = first_h1_prev.sub(/^#\s+/, "")
|
|
404
|
-
# Extract consecutive leading emoji graphemes
|
|
405
|
-
out = +""
|
|
406
|
-
s = tail.dup
|
|
407
|
-
loop do
|
|
408
|
-
cluster = s[/\A\X/u]
|
|
409
|
-
break if cluster.nil? || cluster.empty?
|
|
410
|
-
|
|
411
|
-
if emoji_re =~ cluster
|
|
412
|
-
out << cluster
|
|
413
|
-
s = s[cluster.length..-1].to_s
|
|
414
|
-
else
|
|
415
|
-
break
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
if !out.empty?
|
|
419
|
-
out
|
|
420
|
-
else
|
|
421
|
-
# Fallback to first grapheme
|
|
422
|
-
tail[/\A\X/u]
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
rescue StandardError => e
|
|
427
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
428
|
-
# ignore, leave dest_preserve_prefix as nil
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
432
|
-
# 1) Do token replacements on the template content (org/gem/namespace/shields)
|
|
433
|
-
c = helpers.apply_common_replacements(
|
|
434
|
-
content,
|
|
435
|
-
org: forge_org,
|
|
436
|
-
funding_org: funding_org,
|
|
437
|
-
gem_name: gem_name,
|
|
438
|
-
namespace: namespace,
|
|
439
|
-
namespace_shield: namespace_shield,
|
|
440
|
-
gem_shield: gem_shield,
|
|
441
|
-
min_ruby: min_ruby,
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
# 2) Merge specific sections from destination README, if present
|
|
445
|
-
begin
|
|
446
|
-
dest_existing = prev_readme
|
|
447
|
-
|
|
448
|
-
# Parse Markdown headings while ignoring fenced code blocks (``` ... ```)
|
|
449
|
-
build_sections = lambda do |md|
|
|
450
|
-
return {lines: [], sections: [], line_count: 0} unless md
|
|
451
|
-
|
|
452
|
-
lines = md.split("\n", -1)
|
|
453
|
-
line_count = lines.length
|
|
454
|
-
|
|
455
|
-
sections = []
|
|
456
|
-
in_code = false
|
|
457
|
-
fence_re = /^\s*```/ # start or end of fenced block
|
|
458
|
-
|
|
459
|
-
lines.each_with_index do |ln, i|
|
|
460
|
-
if ln =~ fence_re
|
|
461
|
-
in_code = !in_code
|
|
462
|
-
next
|
|
463
|
-
end
|
|
464
|
-
next if in_code
|
|
465
|
-
|
|
466
|
-
if (m = ln.match(/^(#+)\s+.+/))
|
|
467
|
-
level = m[1].length
|
|
468
|
-
title = ln.sub(/^#+\s+/, "")
|
|
469
|
-
base = title.sub(/\A[^\p{Alnum}]+/u, "").strip.downcase
|
|
470
|
-
sections << {start: i, level: level, heading: ln, base: base}
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# Compute stop indices based on next heading of same or higher level
|
|
475
|
-
sections.each_with_index do |sec, i|
|
|
476
|
-
j = i + 1
|
|
477
|
-
stop = line_count - 1
|
|
478
|
-
while j < sections.length
|
|
479
|
-
if sections[j][:level] <= sec[:level]
|
|
480
|
-
stop = sections[j][:start] - 1
|
|
481
|
-
break
|
|
482
|
-
end
|
|
483
|
-
j += 1
|
|
484
|
-
end
|
|
485
|
-
sec[:stop_to_next_any] = stop
|
|
486
|
-
body_lines_any = lines[(sec[:start] + 1)..stop] || []
|
|
487
|
-
sec[:body_to_next_any] = body_lines_any.join("\n")
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
{lines: lines, sections: sections, line_count: line_count}
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
# Helper: Compute the branch end (inclusive) for a section at index i
|
|
494
|
-
branch_end_index = lambda do |sections_arr, i, total_lines|
|
|
495
|
-
current = sections_arr[i]
|
|
496
|
-
j = i + 1
|
|
497
|
-
while j < sections_arr.length
|
|
498
|
-
return sections_arr[j][:start] - 1 if sections_arr[j][:level] <= current[:level]
|
|
499
|
-
|
|
500
|
-
j += 1
|
|
501
|
-
end
|
|
502
|
-
total_lines - 1
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
src_parsed = build_sections.call(c)
|
|
506
|
-
dest_parsed = build_sections.call(dest_existing)
|
|
507
|
-
|
|
508
|
-
# Build lookup for destination sections by base title, using full branch body (to next heading of same or higher level)
|
|
509
|
-
dest_lookup = {}
|
|
510
|
-
if dest_parsed && dest_parsed[:sections]
|
|
511
|
-
dest_parsed[:sections].each_with_index do |s, idx|
|
|
512
|
-
base = s[:base]
|
|
513
|
-
# Only set once (first occurrence wins)
|
|
514
|
-
next if dest_lookup.key?(base)
|
|
515
|
-
|
|
516
|
-
be = branch_end_index.call(dest_parsed[:sections], idx, dest_parsed[:line_count])
|
|
517
|
-
body_lines = dest_parsed[:lines][(s[:start] + 1)..be] || []
|
|
518
|
-
dest_lookup[base] = {body_branch: body_lines.join("\n"), level: s[:level]}
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
# Build targets to merge: existing curated list plus any NOTE sections at any level
|
|
523
|
-
note_bases = []
|
|
524
|
-
if src_parsed && src_parsed[:sections]
|
|
525
|
-
note_bases = src_parsed[:sections]
|
|
526
|
-
.select { |s| s[:heading] =~ /^#+\s+note:.*/i }
|
|
527
|
-
.map { |s| s[:base] }
|
|
528
|
-
end
|
|
529
|
-
targets = ["synopsis", "configuration", "basic usage"] + note_bases
|
|
530
|
-
|
|
531
|
-
# Replace matching sections in src using full branch ranges
|
|
532
|
-
if src_parsed && src_parsed[:sections] && !src_parsed[:sections].empty?
|
|
533
|
-
lines = src_parsed[:lines].dup
|
|
534
|
-
# Iterate in reverse to keep indices valid
|
|
535
|
-
src_parsed[:sections].reverse_each.with_index do |sec, rev_i|
|
|
536
|
-
next unless targets.include?(sec[:base])
|
|
537
|
-
|
|
538
|
-
# Determine branch range in src for this section
|
|
539
|
-
# rev_i is reverse index; compute forward index
|
|
540
|
-
i = src_parsed[:sections].length - 1 - rev_i
|
|
541
|
-
src_end = branch_end_index.call(src_parsed[:sections], i, src_parsed[:line_count])
|
|
542
|
-
dest_entry = dest_lookup[sec[:base]]
|
|
543
|
-
new_body = dest_entry ? dest_entry[:body_branch] : "\n\n"
|
|
544
|
-
new_block = [sec[:heading], new_body].join("\n")
|
|
545
|
-
range_start = sec[:start]
|
|
546
|
-
range_end = src_end
|
|
547
|
-
# Remove old range
|
|
548
|
-
lines.slice!(range_start..range_end)
|
|
549
|
-
# Insert new block (split preserves potential empty tail)
|
|
550
|
-
insert_lines = new_block.split("\n", -1)
|
|
551
|
-
lines.insert(range_start, *insert_lines)
|
|
552
|
-
end
|
|
553
|
-
c = lines.join("\n")
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
# 3) Preserve entire H1 line from destination README, if any
|
|
557
|
-
begin
|
|
558
|
-
if dest_existing
|
|
559
|
-
dest_h1 = dest_existing.lines.find { |ln| ln =~ /^#\s+/ }
|
|
560
|
-
if dest_h1
|
|
561
|
-
lines_new = c.split("\n", -1)
|
|
562
|
-
src_h1_idx = lines_new.index { |ln| ln =~ /^#\s+/ }
|
|
563
|
-
if src_h1_idx
|
|
564
|
-
# Replace the entire H1 line with the destination's H1 exactly
|
|
565
|
-
lines_new[src_h1_idx] = dest_h1.chomp
|
|
566
|
-
c = lines_new.join("\n")
|
|
567
|
-
end
|
|
568
|
-
end
|
|
569
|
-
end
|
|
570
|
-
rescue StandardError => e
|
|
571
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
572
|
-
# ignore H1 preservation errors
|
|
573
|
-
end
|
|
574
|
-
rescue StandardError => e
|
|
575
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
576
|
-
# Best effort; if anything fails, keep c as-is
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
c
|
|
580
|
-
end
|
|
581
|
-
elsif ["CHANGELOG.md", "CITATION.cff", "CONTRIBUTING.md", ".opencollective.yml", "FUNDING.md", ".junie/guidelines.md", ".envrc"].include?(rel)
|
|
582
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
|
583
|
-
c = helpers.apply_common_replacements(
|
|
584
|
-
content,
|
|
585
|
-
org: forge_org,
|
|
586
|
-
funding_org: funding_org,
|
|
587
|
-
gem_name: gem_name,
|
|
588
|
-
namespace: namespace,
|
|
589
|
-
namespace_shield: namespace_shield,
|
|
590
|
-
gem_shield: gem_shield,
|
|
591
|
-
min_ruby: min_ruby,
|
|
592
|
-
)
|
|
593
|
-
if File.basename(rel) == "CHANGELOG.md"
|
|
594
|
-
begin
|
|
595
|
-
# Special handling for CHANGELOG.md
|
|
596
|
-
# 1) Take template header through Unreleased section (inclusive)
|
|
597
|
-
src_lines = c.split("\n", -1)
|
|
598
|
-
tpl_unrel_idx = src_lines.index { |ln| ln =~ /^##\s*\[\s*Unreleased\s*\]/i }
|
|
599
|
-
if tpl_unrel_idx
|
|
600
|
-
# Find end of Unreleased in template (next ## or # heading)
|
|
601
|
-
tpl_end_idx = src_lines.length - 1
|
|
602
|
-
j = tpl_unrel_idx + 1
|
|
603
|
-
while j < src_lines.length
|
|
604
|
-
if src_lines[j] =~ /^##\s+\[/ || src_lines[j] =~ /^#\s+/ || src_lines[j] =~ /^##\s+[^\[]/
|
|
605
|
-
tpl_end_idx = j - 1
|
|
606
|
-
break
|
|
607
|
-
end
|
|
608
|
-
j += 1
|
|
609
|
-
end
|
|
610
|
-
tpl_header_pre = src_lines[0...tpl_unrel_idx] # lines before Unreleased heading
|
|
611
|
-
tpl_unrel_heading = src_lines[tpl_unrel_idx]
|
|
612
|
-
src_lines[(tpl_unrel_idx + 1)..tpl_end_idx] || []
|
|
613
|
-
|
|
614
|
-
# 2) Extract destination Unreleased content, preserving list items under any standard headings
|
|
615
|
-
dest_content = File.file?(dest) ? File.read(dest) : ""
|
|
616
|
-
dest_lines = dest_content.split("\n", -1)
|
|
617
|
-
dest_unrel_idx = dest_lines.index { |ln| ln =~ /^##\s*\[\s*Unreleased\s*\]/i }
|
|
618
|
-
dest_end_idx = if dest_unrel_idx
|
|
619
|
-
k = dest_unrel_idx + 1
|
|
620
|
-
e = dest_lines.length - 1
|
|
621
|
-
while k < dest_lines.length
|
|
622
|
-
if dest_lines[k] =~ /^##\s+\[/ || dest_lines[k] =~ /^#\s+/ || dest_lines[k] =~ /^##\s+[^\[]/
|
|
623
|
-
e = k - 1
|
|
624
|
-
break
|
|
625
|
-
end
|
|
626
|
-
k += 1
|
|
627
|
-
end
|
|
628
|
-
e
|
|
629
|
-
end
|
|
630
|
-
dest_unrel_body = dest_unrel_idx ? (dest_lines[(dest_unrel_idx + 1)..dest_end_idx] || []) : []
|
|
631
|
-
|
|
632
|
-
# Helper: parse body into map of heading=>items (only '- ' markdown items)
|
|
633
|
-
std_heads = [
|
|
634
|
-
"### Added",
|
|
635
|
-
"### Changed",
|
|
636
|
-
"### Deprecated",
|
|
637
|
-
"### Removed",
|
|
638
|
-
"### Fixed",
|
|
639
|
-
"### Security",
|
|
640
|
-
]
|
|
641
|
-
|
|
642
|
-
parse_items = lambda do |body_lines|
|
|
643
|
-
result = {}
|
|
644
|
-
cur = nil
|
|
645
|
-
i = 0
|
|
646
|
-
while i < body_lines.length
|
|
647
|
-
ln = body_lines[i]
|
|
648
|
-
if ln.start_with?("### ")
|
|
649
|
-
cur = ln.strip
|
|
650
|
-
result[cur] ||= []
|
|
651
|
-
i += 1
|
|
652
|
-
next
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
# Detect a list item bullet (allow optional indentation)
|
|
656
|
-
if (m = ln.match(/^(\s*)[-*]\s/))
|
|
657
|
-
result[cur] ||= []
|
|
658
|
-
base_indent = m[1].length
|
|
659
|
-
# Start a new item: include the bullet line
|
|
660
|
-
result[cur] << ln.rstrip
|
|
661
|
-
i += 1
|
|
662
|
-
|
|
663
|
-
# Include subsequent lines that belong to this list item:
|
|
664
|
-
# - blank lines
|
|
665
|
-
# - lines with indentation greater than the bullet's indentation
|
|
666
|
-
# - any lines inside fenced code blocks (```), regardless of indentation until fence closes
|
|
667
|
-
in_fence = false
|
|
668
|
-
fence_re = /^\s*```/
|
|
669
|
-
while i < body_lines.length
|
|
670
|
-
l2 = body_lines[i]
|
|
671
|
-
# Stop if next sibling/top-level bullet of same or smaller indent and not inside a fence
|
|
672
|
-
if !in_fence && l2 =~ /^(\s*)[-*]\s/
|
|
673
|
-
ind = Regexp.last_match(1).length
|
|
674
|
-
break if ind <= base_indent
|
|
675
|
-
end
|
|
676
|
-
# Break if a new section heading appears and we're not in a fence
|
|
677
|
-
break if !in_fence && l2.start_with?("### ")
|
|
678
|
-
|
|
679
|
-
if l2 =~ fence_re
|
|
680
|
-
in_fence = !in_fence
|
|
681
|
-
result[cur] << l2.rstrip
|
|
682
|
-
i += 1
|
|
683
|
-
next
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
# Include blanks and lines indented more than base indent, or anything while in fence
|
|
687
|
-
if in_fence || l2.strip.empty? || (l2[/^\s*/].length > base_indent)
|
|
688
|
-
result[cur] << l2.rstrip
|
|
689
|
-
i += 1
|
|
690
|
-
next
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
# Otherwise, this line does not belong to the current list item
|
|
694
|
-
break
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
next
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
# Non-bullet, non-heading line: just advance
|
|
701
|
-
i += 1
|
|
702
|
-
end
|
|
703
|
-
result
|
|
704
|
-
end
|
|
705
|
-
|
|
706
|
-
dest_items = parse_items.call(dest_unrel_body)
|
|
707
|
-
|
|
708
|
-
# 3) Build a single canonical Unreleased section: heading + the six standard subheads in order
|
|
709
|
-
new_unrel_block = []
|
|
710
|
-
new_unrel_block << tpl_unrel_heading
|
|
711
|
-
std_heads.each do |h|
|
|
712
|
-
new_unrel_block << h
|
|
713
|
-
if dest_items[h] && !dest_items[h].empty?
|
|
714
|
-
new_unrel_block.concat(dest_items[h])
|
|
715
|
-
end
|
|
716
|
-
end
|
|
717
|
-
|
|
718
|
-
# 4) Compose final content: template preface + new unreleased + rest of destination (after its unreleased)
|
|
719
|
-
tail_after_unrel = []
|
|
720
|
-
if dest_unrel_idx
|
|
721
|
-
tail_after_unrel = dest_lines[(dest_end_idx + 1)..-1] || []
|
|
722
|
-
end
|
|
723
|
-
|
|
724
|
-
# Ensure exactly one blank line between the Unreleased chunk and the next version chunk
|
|
725
|
-
# - Strip trailing blanks from the newly built Unreleased block
|
|
726
|
-
while new_unrel_block.any? && new_unrel_block.last.to_s.strip == ""
|
|
727
|
-
new_unrel_block.pop
|
|
728
|
-
end
|
|
729
|
-
# - Strip leading blanks from the tail
|
|
730
|
-
while tail_after_unrel.any? && tail_after_unrel.first.to_s.strip == ""
|
|
731
|
-
tail_after_unrel.shift
|
|
732
|
-
end
|
|
733
|
-
merged_lines = tpl_header_pre + new_unrel_block
|
|
734
|
-
# Insert a single separator blank line if there is any tail content
|
|
735
|
-
merged_lines << "" if tail_after_unrel.any?
|
|
736
|
-
merged_lines.concat(tail_after_unrel)
|
|
737
|
-
|
|
738
|
-
c = merged_lines.join("\n")
|
|
739
|
-
end
|
|
740
|
-
|
|
741
|
-
# Collapse repeated whitespace in release headers only
|
|
742
|
-
lines = c.split("\n", -1)
|
|
743
|
-
lines.map! do |ln|
|
|
744
|
-
if ln =~ /^##\s+\[.*\]/
|
|
745
|
-
ln.gsub(/[ \t]+/, " ")
|
|
746
|
-
else
|
|
747
|
-
ln
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
c = lines.join("\n")
|
|
751
|
-
rescue StandardError => e
|
|
752
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
753
|
-
# Fallback: whitespace normalization
|
|
754
|
-
lines = c.split("\n", -1)
|
|
755
|
-
lines.map! { |ln| (ln =~ /^##\s+\[.*\]/) ? ln.gsub(/[ \t]+/, " ") : ln }
|
|
756
|
-
c = lines.join("\n")
|
|
757
|
-
end
|
|
758
|
-
end
|
|
759
|
-
# Normalize spacing around Markdown headings for broad renderer compatibility
|
|
760
|
-
c = normalize_heading_spacing(c)
|
|
761
|
-
c
|
|
762
|
-
end
|
|
763
|
-
else
|
|
764
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true)
|
|
765
|
-
end
|
|
766
|
-
end
|
|
767
|
-
|
|
768
|
-
# Post-process README H1 preservation using snapshot (replace entire H1 line)
|
|
769
|
-
begin
|
|
770
|
-
if existing_readme_before
|
|
771
|
-
readme_path = File.join(project_root, "README.md")
|
|
772
|
-
if File.file?(readme_path)
|
|
773
|
-
prev = existing_readme_before
|
|
774
|
-
newc = File.read(readme_path)
|
|
775
|
-
prev_h1 = prev.lines.find { |ln| ln =~ /^#\s+/ }
|
|
776
|
-
lines = newc.split("\n", -1)
|
|
777
|
-
cur_h1_idx = lines.index { |ln| ln =~ /^#\s+/ }
|
|
778
|
-
if prev_h1 && cur_h1_idx
|
|
779
|
-
# Replace the entire H1 line with the previous README's H1 exactly
|
|
780
|
-
lines[cur_h1_idx] = prev_h1.chomp
|
|
781
|
-
File.open(readme_path, "w") { |f| f.write(lines.join("\n")) }
|
|
782
|
-
end
|
|
783
|
-
end
|
|
784
|
-
end
|
|
785
|
-
rescue StandardError => e
|
|
786
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
787
|
-
# ignore post-processing errors
|
|
788
|
-
end
|
|
789
|
-
|
|
790
|
-
# 7b) certs/pboling.pem
|
|
791
|
-
begin
|
|
792
|
-
cert_src = File.join(gem_checkout_root, "certs", "pboling.pem")
|
|
793
|
-
cert_dest = File.join(project_root, "certs", "pboling.pem")
|
|
794
|
-
if File.exist?(cert_src)
|
|
795
|
-
helpers.copy_file_with_prompt(cert_src, cert_dest, allow_create: true, allow_replace: true)
|
|
796
|
-
end
|
|
797
|
-
rescue StandardError => e
|
|
798
|
-
puts "WARNING: Skipped copying certs/pboling.pem due to #{e.class}: #{e.message}"
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
# After creating or replacing .envrc or .env.local.example, require review and exit unless allowed
|
|
802
|
-
begin
|
|
803
|
-
envrc_path = File.join(project_root, ".envrc")
|
|
804
|
-
envlocal_example_path = File.join(project_root, ".env.local.example")
|
|
805
|
-
changed_env_files = []
|
|
806
|
-
changed_env_files << envrc_path if helpers.modified_by_template?(envrc_path)
|
|
807
|
-
changed_env_files << envlocal_example_path if helpers.modified_by_template?(envlocal_example_path)
|
|
808
|
-
if !changed_env_files.empty?
|
|
809
|
-
if ENV.fetch("allowed", "").to_s =~ /\A(1|true|y|yes)\z/i
|
|
810
|
-
puts "Detected updates to #{changed_env_files.map { |p| File.basename(p) }.join(" and ")}. Proceeding because allowed=true."
|
|
811
|
-
else
|
|
812
|
-
puts
|
|
813
|
-
puts "IMPORTANT: The following environment-related files were created/updated:"
|
|
814
|
-
changed_env_files.each { |p| puts " - #{p}" }
|
|
815
|
-
puts
|
|
816
|
-
puts "Please review these files. If .envrc changed, run:"
|
|
817
|
-
puts " direnv allow"
|
|
818
|
-
puts
|
|
819
|
-
puts "After that, re-run to resume:"
|
|
820
|
-
puts " bundle exec rake kettle:dev:template allowed=true"
|
|
821
|
-
puts " # or to run the full install afterwards:"
|
|
822
|
-
puts " bundle exec rake kettle:dev:install allowed=true"
|
|
823
|
-
task_abort("Aborting: review of environment files required before continuing.")
|
|
824
|
-
end
|
|
825
|
-
end
|
|
826
|
-
rescue StandardError => e
|
|
827
|
-
# Do not swallow intentional task aborts
|
|
828
|
-
raise if e.is_a?(Kettle::Dev::Error)
|
|
829
|
-
|
|
830
|
-
puts "WARNING: Could not determine env file changes: #{e.class}: #{e.message}"
|
|
831
|
-
end
|
|
832
|
-
|
|
833
|
-
# Handle .git-hooks files (see original rake task for details)
|
|
834
|
-
source_hooks_dir = File.join(gem_checkout_root, ".git-hooks")
|
|
835
|
-
if Dir.exist?(source_hooks_dir)
|
|
836
|
-
# Honor ENV["only"]: skip entire .git-hooks handling unless patterns include .git-hooks
|
|
837
|
-
begin
|
|
838
|
-
only_raw = ENV["only"].to_s
|
|
839
|
-
if !only_raw.empty?
|
|
840
|
-
patterns = only_raw.split(",").map { |s| s.strip }.reject(&:empty?)
|
|
841
|
-
if !patterns.empty?
|
|
842
|
-
proj = helpers.project_root.to_s
|
|
843
|
-
target_dir = File.join(proj, ".git-hooks")
|
|
844
|
-
# Determine if any pattern would match either the directory itself (with /** semantics) or files within it
|
|
845
|
-
matches = patterns.any? do |pat|
|
|
846
|
-
if pat.end_with?("/**")
|
|
847
|
-
base = pat[0..-4]
|
|
848
|
-
base == ".git-hooks" || base == target_dir.sub(/^#{Regexp.escape(proj)}\/?/, "")
|
|
849
|
-
else
|
|
850
|
-
# Check for explicit .git-hooks or subpaths
|
|
851
|
-
File.fnmatch?(pat, ".git-hooks", File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH) ||
|
|
852
|
-
File.fnmatch?(pat, ".git-hooks/*", File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
|
|
853
|
-
end
|
|
854
|
-
end
|
|
855
|
-
unless matches
|
|
856
|
-
# No interest in .git-hooks => skip prompts and copies for hooks entirely
|
|
857
|
-
# Note: we intentionally do not record template_results for hooks
|
|
858
|
-
return
|
|
859
|
-
end
|
|
860
|
-
end
|
|
861
|
-
end
|
|
862
|
-
rescue StandardError => e
|
|
863
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
864
|
-
# If filter parsing fails, proceed as before
|
|
865
|
-
end
|
|
866
|
-
# Prefer .example variant when present for .git-hooks
|
|
867
|
-
goalie_src = helpers.prefer_example(File.join(source_hooks_dir, "commit-subjects-goalie.txt"))
|
|
868
|
-
footer_src = helpers.prefer_example(File.join(source_hooks_dir, "footer-template.erb.txt"))
|
|
869
|
-
hook_ruby_src = helpers.prefer_example(File.join(source_hooks_dir, "commit-msg"))
|
|
870
|
-
hook_sh_src = helpers.prefer_example(File.join(source_hooks_dir, "prepare-commit-msg"))
|
|
871
|
-
|
|
872
|
-
# First: templates (.txt) — ask local/global/skip
|
|
873
|
-
if File.file?(goalie_src) && File.file?(footer_src)
|
|
874
|
-
puts
|
|
875
|
-
puts "Git hooks templates found:"
|
|
876
|
-
puts " - #{goalie_src}"
|
|
877
|
-
puts " - #{footer_src}"
|
|
878
|
-
puts
|
|
879
|
-
puts "About these files:"
|
|
880
|
-
puts "- commit-subjects-goalie.txt:"
|
|
881
|
-
puts " Lists commit subject prefixes to look for; if a commit subject starts with any listed prefix,"
|
|
882
|
-
puts " kettle-commit-msg will append a footer to the commit message (when GIT_HOOK_FOOTER_APPEND=true)."
|
|
883
|
-
puts " Defaults include release prep (🔖 Prepare release v) and checksum commits (🔒️ Checksums for v)."
|
|
884
|
-
puts "- footer-template.erb.txt:"
|
|
885
|
-
puts " ERB template rendered to produce the footer. You can customize its contents and variables."
|
|
886
|
-
puts
|
|
887
|
-
puts "Where would you like to install these two templates?"
|
|
888
|
-
puts " [l] Local to this project (#{File.join(project_root, ".git-hooks")})"
|
|
889
|
-
puts " [g] Global for this user (#{File.join(ENV["HOME"], ".git-hooks")})"
|
|
890
|
-
puts " [s] Skip copying"
|
|
891
|
-
# Allow non-interactive selection via environment
|
|
892
|
-
# Precedence: CLI switch (hook_templates) > KETTLE_DEV_HOOK_TEMPLATES > prompt
|
|
893
|
-
env_choice = ENV["hook_templates"]
|
|
894
|
-
env_choice = ENV["KETTLE_DEV_HOOK_TEMPLATES"] if env_choice.nil? || env_choice.strip.empty?
|
|
895
|
-
choice = env_choice&.strip
|
|
896
|
-
unless choice && !choice.empty?
|
|
897
|
-
print("Choose (l/g/s) [l]: ")
|
|
898
|
-
choice = Kettle::Dev::InputAdapter.gets&.strip
|
|
899
|
-
end
|
|
900
|
-
choice = "l" if choice.nil? || choice.empty?
|
|
901
|
-
dest_dir = case choice.downcase
|
|
902
|
-
when "g", "global" then File.join(ENV["HOME"], ".git-hooks")
|
|
903
|
-
when "s", "skip" then nil
|
|
904
|
-
else File.join(project_root, ".git-hooks")
|
|
905
|
-
end
|
|
906
|
-
|
|
907
|
-
if dest_dir
|
|
908
|
-
FileUtils.mkdir_p(dest_dir)
|
|
909
|
-
[[goalie_src, "commit-subjects-goalie.txt"], [footer_src, "footer-template.erb.txt"]].each do |src, base|
|
|
910
|
-
dest = File.join(dest_dir, base)
|
|
911
|
-
# Allow create/replace prompts for these files (question applies to them)
|
|
912
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true)
|
|
913
|
-
# Ensure readable (0644). These are data/templates, not executables.
|
|
914
|
-
begin
|
|
915
|
-
File.chmod(0o644, dest) if File.exist?(dest)
|
|
916
|
-
rescue StandardError => e
|
|
917
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
918
|
-
# ignore permission issues
|
|
919
|
-
end
|
|
920
|
-
end
|
|
921
|
-
else
|
|
922
|
-
puts "Skipping copy of .git-hooks templates."
|
|
923
|
-
end
|
|
924
|
-
end
|
|
925
|
-
|
|
926
|
-
# Second: hook scripts — copy only to local project; prompt only on overwrite
|
|
927
|
-
hook_dests = [File.join(project_root, ".git-hooks")]
|
|
928
|
-
hook_pairs = [[hook_ruby_src, "commit-msg", 0o755], [hook_sh_src, "prepare-commit-msg", 0o755]]
|
|
929
|
-
hook_pairs.each do |src, base, mode|
|
|
930
|
-
next unless File.file?(src)
|
|
931
|
-
|
|
932
|
-
hook_dests.each do |dstdir|
|
|
933
|
-
begin
|
|
934
|
-
FileUtils.mkdir_p(dstdir)
|
|
935
|
-
dest = File.join(dstdir, base)
|
|
936
|
-
# Create without prompt if missing; if exists, ask to replace
|
|
937
|
-
if File.exist?(dest)
|
|
938
|
-
if helpers.ask("Overwrite existing #{dest}?", true)
|
|
939
|
-
content = File.read(src)
|
|
940
|
-
helpers.write_file(dest, content)
|
|
941
|
-
begin
|
|
942
|
-
File.chmod(mode, dest)
|
|
943
|
-
rescue StandardError => e
|
|
944
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
945
|
-
# ignore permission issues
|
|
946
|
-
end
|
|
947
|
-
puts "Replaced #{dest}"
|
|
948
|
-
else
|
|
949
|
-
puts "Kept existing #{dest}"
|
|
950
|
-
end
|
|
951
|
-
else
|
|
952
|
-
content = File.read(src)
|
|
953
|
-
helpers.write_file(dest, content)
|
|
954
|
-
begin
|
|
955
|
-
File.chmod(mode, dest)
|
|
956
|
-
rescue StandardError => e
|
|
957
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
958
|
-
# ignore permission issues
|
|
959
|
-
end
|
|
960
|
-
puts "Installed #{dest}"
|
|
961
|
-
end
|
|
962
|
-
rescue StandardError => e
|
|
963
|
-
puts "WARNING: Could not install hook #{base} to #{dstdir}: #{e.class}: #{e.message}"
|
|
964
|
-
end
|
|
965
|
-
end
|
|
966
|
-
end
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
# Done
|
|
970
|
-
nil
|
|
971
|
-
end
|
|
972
|
-
end
|
|
973
|
-
end
|
|
974
|
-
end
|
|
975
|
-
end
|