kettle-dev 1.1.11 → 1.1.13
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/.envrc +1 -1
- data/.rubocop_rspec.yml +30 -0
- data/Appraisals +1 -1
- data/CHANGELOG.md +23 -1
- data/README.md +1 -1
- data/README.md.example +1 -1
- data/Rakefile.example +1 -1
- data/gemfiles/modular/injected.gemfile +1 -1
- data/lib/kettle/dev/changelog_cli.rb +5 -0
- data/lib/kettle/dev/ci_helpers.rb +11 -0
- data/lib/kettle/dev/ci_monitor.rb +8 -2
- data/lib/kettle/dev/commit_msg.rb +1 -0
- data/lib/kettle/dev/dvcs_cli.rb +10 -1
- data/lib/kettle/dev/git_adapter.rb +1 -0
- data/lib/kettle/dev/git_commit_footer.rb +1 -0
- data/lib/kettle/dev/input_adapter.rb +1 -0
- data/lib/kettle/dev/modular_gemfiles.rb +110 -0
- data/lib/kettle/dev/pre_release_cli.rb +2 -0
- data/lib/kettle/dev/readme_backers.rb +14 -0
- data/lib/kettle/dev/release_cli.rb +19 -0
- data/lib/kettle/dev/setup_cli.rb +162 -1
- data/lib/kettle/dev/tasks/install_task.rb +31 -18
- data/lib/kettle/dev/tasks/template_task.rb +16 -85
- data/lib/kettle/dev/template_helpers.rb +4 -0
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev/versioning.rb +2 -0
- data/lib/kettle/dev.rb +1 -0
- data/sig/kettle/dev/modular_gemfiles.rbs +12 -0
- data.tar.gz.sig +0 -0
- metadata +8 -5
- metadata.gz.sig +0 -0
@@ -283,6 +283,7 @@ module Kettle
|
|
283
283
|
# Example match: "- COVERAGE: 97.70% -- 2125/2175 lines in 20 files"
|
284
284
|
m = section.lines.find { |l| l =~ /-\s*COVERAGE:\s*.+--\s*\d+\/(\d+)\s+lines/i }
|
285
285
|
return unless m
|
286
|
+
|
286
287
|
denom = m.match(/-\s*COVERAGE:\s*.+--\s*\d+\/(\d+)\s+lines/i)[1].to_i
|
287
288
|
kloc = denom.to_f / 1000.0
|
288
289
|
kloc_str = format("%.3f", kloc)
|
@@ -296,6 +297,7 @@ module Kettle
|
|
296
297
|
# Replaces only the numeric portion after "KLOC-" keeping other URL parts intact.
|
297
298
|
def update_badge_number_in_file(path, kloc_str)
|
298
299
|
return unless File.file?(path)
|
300
|
+
|
299
301
|
content = File.read(path)
|
300
302
|
# Match the specific reference line, capture groups around the number
|
301
303
|
# Example: [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.175-FFDD67.svg?style=...
|
@@ -310,6 +312,7 @@ module Kettle
|
|
310
312
|
def update_rakefile_example_header!(version)
|
311
313
|
path = File.join(@root, "Rakefile.example")
|
312
314
|
return unless File.file?(path)
|
315
|
+
|
313
316
|
content = File.read(path)
|
314
317
|
today = Time.now.strftime("%Y-%m-%d")
|
315
318
|
new_line = "# kettle-dev Rakefile v#{version} - #{today}"
|
@@ -394,6 +397,7 @@ module Kettle
|
|
394
397
|
def collapse_years(enum)
|
395
398
|
arr = enum.to_a.map(&:to_i).uniq.sort
|
396
399
|
return "" if arr.empty?
|
400
|
+
|
397
401
|
segments = []
|
398
402
|
start = arr.first
|
399
403
|
prev = start
|
@@ -422,10 +426,12 @@ module Kettle
|
|
422
426
|
unless line =~ /copyright/i
|
423
427
|
next line
|
424
428
|
end
|
429
|
+
|
425
430
|
m = line.match(/\A(?<pre>.*?copyright[^0-9]*)(?<years>(?:\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)(?:\s*,\s*\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)*)(?<post>.*)\z/i)
|
426
431
|
unless m
|
427
432
|
next line
|
428
433
|
end
|
434
|
+
|
429
435
|
new_line = "#{m[:pre]}#{canonical_all}#{m[:post]}"
|
430
436
|
changed ||= (new_line != line)
|
431
437
|
new_line
|
@@ -444,12 +450,14 @@ module Kettle
|
|
444
450
|
unless line =~ /copyright/i
|
445
451
|
next line
|
446
452
|
end
|
453
|
+
|
447
454
|
# Capture three parts: prefix up to first year, the year blob, and the rest
|
448
455
|
m = line.match(/\A(?<pre>.*?copyright[^0-9]*)(?<years>(?:\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)(?:\s*,\s*\b(?:19|20)\d{2}\b(?:\s*[\-–]\s*\b(?:19|20)\d{2}\b)?)*)(?<post>.*)\z/i)
|
449
456
|
unless m
|
450
457
|
# No parsable year sequence on this line; leave as-is
|
451
458
|
next line
|
452
459
|
end
|
460
|
+
|
453
461
|
years_blob = m[:years]
|
454
462
|
# Reuse extraction logic on just the years blob
|
455
463
|
years = []
|
@@ -731,15 +739,18 @@ module Kettle
|
|
731
739
|
def preferred_github_remote
|
732
740
|
cands = github_remote_candidates
|
733
741
|
return if cands.empty?
|
742
|
+
|
734
743
|
# Prefer explicitly named GitHub remotes first, then origin (only if it points to GitHub), else the first candidate
|
735
744
|
explicit = cands.find { |n| n == "github" } || cands.find { |n| n == "gh" }
|
736
745
|
return explicit if explicit
|
737
746
|
return "origin" if cands.include?("origin")
|
747
|
+
|
738
748
|
cands.first
|
739
749
|
end
|
740
750
|
|
741
751
|
def parse_github_owner_repo(url)
|
742
752
|
return [nil, nil] unless url
|
753
|
+
|
743
754
|
if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
|
744
755
|
[Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
|
745
756
|
elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
|
@@ -761,6 +772,7 @@ module Kettle
|
|
761
772
|
def ahead_behind_counts(local_ref, remote_ref)
|
762
773
|
out, ok = git_output(["rev-list", "--left-right", "--count", "#{local_ref}...#{remote_ref}"])
|
763
774
|
return [0, 0] unless ok && !out.empty?
|
775
|
+
|
764
776
|
parts = out.split
|
765
777
|
left = parts[0].to_i
|
766
778
|
right = parts[1].to_i
|
@@ -769,6 +781,7 @@ module Kettle
|
|
769
781
|
|
770
782
|
def trunk_behind_remote?(trunk, remote)
|
771
783
|
return false unless remote_branch_exists?(remote, trunk)
|
784
|
+
|
772
785
|
_ahead, behind = ahead_behind_counts(trunk, "#{remote}/#{trunk}")
|
773
786
|
behind.positive?
|
774
787
|
end
|
@@ -781,6 +794,7 @@ module Kettle
|
|
781
794
|
missing_from = []
|
782
795
|
remotes.each do |r|
|
783
796
|
next if r == "all"
|
797
|
+
|
784
798
|
if remote_branch_exists?(r, trunk)
|
785
799
|
_ahead, behind = ahead_behind_counts(trunk, "#{r}/#{trunk}")
|
786
800
|
missing_from << r if behind.positive?
|
@@ -853,6 +867,7 @@ module Kettle
|
|
853
867
|
|
854
868
|
def merge_feature_into_trunk_and_push!(trunk, feature)
|
855
869
|
return if feature.nil? || feature == trunk
|
870
|
+
|
856
871
|
puts "Merging #{feature} into #{trunk} (after CI success)..."
|
857
872
|
checkout!(trunk)
|
858
873
|
run_cmd!("git pull --rebase origin #{Shellwords.escape(trunk)}")
|
@@ -971,12 +986,14 @@ module Kettle
|
|
971
986
|
def extract_changelog_for_version(version)
|
972
987
|
path = File.join(@root, "CHANGELOG.md")
|
973
988
|
return [nil, nil, nil] unless File.file?(path)
|
989
|
+
|
974
990
|
content = File.read(path)
|
975
991
|
lines = content.lines
|
976
992
|
|
977
993
|
# Find section start
|
978
994
|
start_idx = lines.index { |l| l.start_with?("## [#{version}]") }
|
979
995
|
return [nil, nil, nil] unless start_idx
|
996
|
+
|
980
997
|
i = start_idx + 1
|
981
998
|
# Find next section heading or EOF
|
982
999
|
while i < lines.length && !lines[i].start_with?("## [")
|
@@ -999,12 +1016,14 @@ module Kettle
|
|
999
1016
|
def extract_release_notes_footer
|
1000
1017
|
path = File.join(@root, "FUNDING.md")
|
1001
1018
|
return unless File.file?(path)
|
1019
|
+
|
1002
1020
|
content = File.read(path)
|
1003
1021
|
start_tag = "<!-- RELEASE-NOTES-FOOTER-START -->"
|
1004
1022
|
end_tag = "<!-- RELEASE-NOTES-FOOTER-END -->"
|
1005
1023
|
s = content.index(start_tag)
|
1006
1024
|
e = content.index(end_tag)
|
1007
1025
|
return unless s && e && e > s
|
1026
|
+
|
1008
1027
|
# Extract between tags, excluding the tags themselves
|
1009
1028
|
block = content[(s + start_tag.length)...e]
|
1010
1029
|
# Normalize: trim trailing whitespace but keep internal formatting
|
data/lib/kettle/dev/setup_cli.rb
CHANGED
@@ -31,6 +31,8 @@ module Kettle
|
|
31
31
|
say("Starting kettle-dev setup…")
|
32
32
|
prechecks!
|
33
33
|
ensure_dev_deps!
|
34
|
+
ensure_gemfile_from_example!
|
35
|
+
ensure_modular_gemfiles!
|
34
36
|
ensure_bin_setup!
|
35
37
|
ensure_rakefile!
|
36
38
|
run_bin_setup!
|
@@ -44,14 +46,72 @@ module Kettle
|
|
44
46
|
|
45
47
|
def debug(msg)
|
46
48
|
return if ENV.fetch("DEBUG", "false").casecmp("true").nonzero?
|
49
|
+
|
47
50
|
$stderr.puts("[kettle-dev-setup] DEBUG: #{msg}")
|
48
51
|
end
|
49
52
|
|
53
|
+
# Attempt to derive a funding organization from the git remote 'origin' when
|
54
|
+
# not explicitly provided via env or .opencollective.yml.
|
55
|
+
# This is a soft helper that only sets ENV["FUNDING_ORG"] if a plausible
|
56
|
+
# GitHub org can be parsed from the origin URL.
|
57
|
+
# @return [void]
|
58
|
+
def derive_funding_org_from_git_if_missing!
|
59
|
+
# Respect explicit bypass
|
60
|
+
env_val = ENV["FUNDING_ORG"]
|
61
|
+
return if env_val && env_val.to_s.strip.casecmp("false").zero?
|
62
|
+
|
63
|
+
# If already provided via env, do nothing
|
64
|
+
return if ENV["FUNDING_ORG"].to_s.strip != ""
|
65
|
+
return if ENV["OPENCOLLECTIVE_HANDLE"].to_s.strip != ""
|
66
|
+
|
67
|
+
# If project provides an .opencollective.yml with org, do nothing
|
68
|
+
begin
|
69
|
+
oc_path = File.join(Dir.pwd, ".opencollective.yml")
|
70
|
+
if File.file?(oc_path)
|
71
|
+
txt = File.read(oc_path)
|
72
|
+
return if txt =~ /\borg:\s*([\w\-]+)/i
|
73
|
+
end
|
74
|
+
rescue StandardError => e
|
75
|
+
debug("Reading .opencollective.yml failed: #{e.class}: #{e.message}")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Attempt to get origin URL and parse GitHub org
|
79
|
+
begin
|
80
|
+
ga = Kettle::Dev::GitAdapter.new
|
81
|
+
origin_url = nil
|
82
|
+
origin_url = ga.remote_url("origin") if ga.respond_to?(:remote_url)
|
83
|
+
if origin_url.nil? && ga.respond_to?(:remotes_with_urls)
|
84
|
+
begin
|
85
|
+
urls = ga.remotes_with_urls
|
86
|
+
origin_url = urls["origin"] if urls
|
87
|
+
rescue StandardError => e
|
88
|
+
# graceful fallback if adapter backend errs; keep silent behavior
|
89
|
+
debug("remotes_with_urls failed: #{e.class}: #{e.message}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
origin_url = origin_url.to_s.strip
|
93
|
+
if (m = origin_url.match(%r{github\.com[/:]([^/]+)/}i))
|
94
|
+
org = m[1].to_s
|
95
|
+
if !org.empty?
|
96
|
+
ENV["FUNDING_ORG"] = org
|
97
|
+
debug("Derived FUNDING_ORG from git origin: #{org}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
rescue StandardError => e
|
101
|
+
# Be silent; this is a best-effort and shouldn't fail setup
|
102
|
+
debug("Could not derive funding org from git: #{e.class}: #{e.message}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
50
106
|
def parse!
|
51
107
|
parser = OptionParser.new do |opts|
|
52
108
|
opts.banner = "Usage: kettle-dev-setup [options]"
|
53
109
|
opts.on("--allowed=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "allowed=#{v}" }
|
54
|
-
opts.on("--force", "Pass through to kettle:dev:install")
|
110
|
+
opts.on("--force", "Pass through to kettle:dev:install") do
|
111
|
+
# Ensure in-process helpers (TemplateHelpers.ask) also see force mode
|
112
|
+
ENV["force"] = "true"
|
113
|
+
@passthrough << "force=true"
|
114
|
+
end
|
55
115
|
opts.on("--hook_templates=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "hook_templates=#{v}" }
|
56
116
|
opts.on("--only=VAL", "Pass through to kettle:dev:install") { |v| @passthrough << "only=#{v}" }
|
57
117
|
opts.on("-h", "--help", "Show help") do
|
@@ -110,6 +170,9 @@ module Kettle
|
|
110
170
|
|
111
171
|
# Gemfile
|
112
172
|
abort!("No Gemfile found; bundler is required.") unless File.exist?("Gemfile")
|
173
|
+
|
174
|
+
# Seed FUNDING_ORG from git remote origin org when not provided elsewhere
|
175
|
+
derive_funding_org_from_git_if_missing!
|
113
176
|
end
|
114
177
|
|
115
178
|
# 3. Sync dev dependencies from this gem's example gemspec into target gemspec
|
@@ -181,6 +244,103 @@ module Kettle
|
|
181
244
|
say("Copied bin/setup.")
|
182
245
|
end
|
183
246
|
|
247
|
+
# 3b. Ensure Gemfile contains required lines from example without duplicating directives
|
248
|
+
# - Copies source, git_source, gemspec, and eval_gemfile lines that are missing
|
249
|
+
# - Idempotent (running multiple times does not duplicate entries)
|
250
|
+
def ensure_gemfile_from_example!
|
251
|
+
source_path = installed_path("Gemfile.example")
|
252
|
+
abort!("Internal error: Gemfile.example not found within installed gem.") unless source_path && File.exist?(source_path)
|
253
|
+
|
254
|
+
example = File.read(source_path)
|
255
|
+
target_path = "Gemfile"
|
256
|
+
target = File.exist?(target_path) ? File.read(target_path) : ""
|
257
|
+
|
258
|
+
# Extract interesting lines from example
|
259
|
+
ex_sources = []
|
260
|
+
ex_git_sources = [] # names (e.g., :github)
|
261
|
+
ex_git_source_lines = {}
|
262
|
+
ex_has_gemspec = false
|
263
|
+
ex_eval_paths = []
|
264
|
+
|
265
|
+
example.each_line do |ln|
|
266
|
+
s = ln.strip
|
267
|
+
next if s.empty?
|
268
|
+
|
269
|
+
if s.start_with?("source ")
|
270
|
+
ex_sources << ln.rstrip
|
271
|
+
elsif (m = s.match(/^git_source\(\s*:(\w+)\s*\)/))
|
272
|
+
name = m[1]
|
273
|
+
ex_git_sources << name
|
274
|
+
ex_git_source_lines[name] = ln.rstrip
|
275
|
+
elsif s.start_with?("gemspec")
|
276
|
+
ex_has_gemspec = true
|
277
|
+
elsif (m = s.match(/^eval_gemfile\s+["']([^"']+)["']/))
|
278
|
+
ex_eval_paths << m[1]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Scan target for presence
|
283
|
+
tg_sources = target.each_line.map(&:rstrip).select { |l| l.strip.start_with?("source ") }
|
284
|
+
tg_git_sources = {}
|
285
|
+
target.each_line do |ln|
|
286
|
+
if (m = ln.strip.match(/^git_source\(\s*:(\w+)\s*\)/))
|
287
|
+
tg_git_sources[m[1]] = true
|
288
|
+
end
|
289
|
+
end
|
290
|
+
tg_has_gemspec = !!target.each_line.find { |l| l.strip.start_with?("gemspec") }
|
291
|
+
tg_eval_paths = target.each_line.map do |ln|
|
292
|
+
if (m = ln.strip.match(/^eval_gemfile\s+["']([^"']+)["']/))
|
293
|
+
m[1]
|
294
|
+
end
|
295
|
+
end.compact
|
296
|
+
|
297
|
+
additions = []
|
298
|
+
# Add missing sources (exact line match)
|
299
|
+
ex_sources.each do |src_line|
|
300
|
+
additions << src_line unless tg_sources.include?(src_line)
|
301
|
+
end
|
302
|
+
# Add missing git_source by name
|
303
|
+
ex_git_sources.each do |name|
|
304
|
+
additions << ex_git_source_lines[name] unless tg_git_sources[name]
|
305
|
+
end
|
306
|
+
# Add gemspec if example has it and target lacks it
|
307
|
+
additions << "gemspec" if ex_has_gemspec && !tg_has_gemspec
|
308
|
+
# Add missing eval_gemfile paths (recreate the exact example line when possible)
|
309
|
+
ex_eval_paths.each do |path|
|
310
|
+
next if tg_eval_paths.include?(path)
|
311
|
+
|
312
|
+
additions << "eval_gemfile \"#{path}\""
|
313
|
+
end
|
314
|
+
|
315
|
+
return say("Gemfile already contains required entries from example.") if additions.empty?
|
316
|
+
|
317
|
+
# Ensure file ends with a newline
|
318
|
+
target << "\n" unless target.end_with?("\n") || target.empty?
|
319
|
+
new_content = target + additions.join("\n") + "\n"
|
320
|
+
File.write(target_path, new_content)
|
321
|
+
say("Updated Gemfile with entries from Gemfile.example (added #{additions.size}).")
|
322
|
+
end
|
323
|
+
|
324
|
+
# 3c. Ensure gemfiles/modular/* are present (copied like template task)
|
325
|
+
def ensure_modular_gemfiles!
|
326
|
+
helpers = Kettle::Dev::TemplateHelpers
|
327
|
+
project_root = helpers.project_root
|
328
|
+
gem_checkout_root = helpers.gem_checkout_root
|
329
|
+
# Gather min_ruby for style.gemfile adjustments
|
330
|
+
min_ruby = begin
|
331
|
+
md = helpers.gemspec_metadata(project_root)
|
332
|
+
md[:min_ruby]
|
333
|
+
rescue StandardError
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
Kettle::Dev::ModularGemfiles.sync!(
|
337
|
+
helpers: helpers,
|
338
|
+
project_root: project_root,
|
339
|
+
gem_checkout_root: gem_checkout_root,
|
340
|
+
min_ruby: min_ruby,
|
341
|
+
)
|
342
|
+
end
|
343
|
+
|
184
344
|
# 5. Ensure Rakefile matches example (replace or create)
|
185
345
|
def ensure_rakefile!
|
186
346
|
source = installed_path("Rakefile.example")
|
@@ -245,6 +405,7 @@ module Kettle
|
|
245
405
|
here = File.expand_path(File.join(__dir__, "..", "..", "..")) # lib/kettle/dev/ -> project root
|
246
406
|
path = File.join(here, rel)
|
247
407
|
return path if File.exist?(path)
|
408
|
+
|
248
409
|
nil
|
249
410
|
end
|
250
411
|
end
|
@@ -174,14 +174,19 @@ module Kettle
|
|
174
174
|
end
|
175
175
|
end
|
176
176
|
|
177
|
-
# If no grapheme found in README H1, ask the user
|
177
|
+
# If no grapheme found in README H1, either use a default in force mode, or ask the user.
|
178
178
|
if chosen_grapheme.nil? || chosen_grapheme.empty?
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
179
|
+
if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
|
180
|
+
# Non-interactive install: default to pizza slice to match template style.
|
181
|
+
chosen_grapheme = "🍕"
|
182
|
+
else
|
183
|
+
puts "No grapheme found after README H1. Enter a grapheme (emoji/symbol) to use for README, summary, and description:"
|
184
|
+
print("Grapheme: ")
|
185
|
+
ans = Kettle::Dev::InputAdapter.gets&.strip.to_s
|
186
|
+
chosen_grapheme = ans[/\A\X/u].to_s
|
187
|
+
# If still empty, skip synchronization silently
|
188
|
+
chosen_grapheme = nil if chosen_grapheme.empty?
|
189
|
+
end
|
185
190
|
end
|
186
191
|
|
187
192
|
if chosen_grapheme
|
@@ -311,15 +316,18 @@ module Kettle
|
|
311
316
|
|
312
317
|
github_repo_from_url = lambda do |url|
|
313
318
|
return unless url
|
319
|
+
|
314
320
|
url = url.strip
|
315
321
|
m = url.match(%r{github\.com[/:]([^/\s:]+)/([^/\s]+?)(?:\.git)?/?\z}i)
|
316
322
|
return unless m
|
323
|
+
|
317
324
|
[m[1], m[2]]
|
318
325
|
end
|
319
326
|
|
320
327
|
github_homepage_literal = lambda do |val|
|
321
328
|
return false unless val
|
322
329
|
return false if val.include?('#{')
|
330
|
+
|
323
331
|
v = val.to_s.strip
|
324
332
|
if (v.start_with?("\"") && v.end_with?("\"")) || (v.start_with?("'") && v.end_with?("'"))
|
325
333
|
v = begin
|
@@ -329,6 +337,7 @@ module Kettle
|
|
329
337
|
end
|
330
338
|
end
|
331
339
|
return false unless v =~ %r{\Ahttps?://github\.com/}i
|
340
|
+
|
332
341
|
!!github_repo_from_url.call(v)
|
333
342
|
end
|
334
343
|
|
@@ -364,12 +373,13 @@ module Kettle
|
|
364
373
|
puts "Current spec.homepage appears #{interpolated ? "interpolated" : "invalid"}: #{assigned}"
|
365
374
|
puts "Suggested literal homepage: \"#{suggested}\""
|
366
375
|
print("Update #{File.basename(gemspec_path)} to use this homepage? [Y/n]: ")
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
376
|
+
do_update =
|
377
|
+
if ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
|
378
|
+
true
|
379
|
+
else
|
380
|
+
ans = Kettle::Dev::InputAdapter.gets&.strip
|
381
|
+
ans.nil? || ans.empty? || ans =~ /\Ay(es)?\z/i
|
382
|
+
end
|
373
383
|
|
374
384
|
if do_update
|
375
385
|
new_line = homepage_line.sub(/=.*/, "= \"#{suggested}\"\n")
|
@@ -385,6 +395,7 @@ module Kettle
|
|
385
395
|
rescue StandardError => e
|
386
396
|
# Do not swallow intentional task aborts signaled via Kettle::Dev::Error
|
387
397
|
raise if e.is_a?(Kettle::Dev::Error)
|
398
|
+
|
388
399
|
puts "WARNING: An error occurred while checking gemspec homepage: #{e.class}: #{e.message}"
|
389
400
|
end
|
390
401
|
|
@@ -406,6 +417,7 @@ module Kettle
|
|
406
417
|
[:create, :replace, :dir_create, :dir_replace].each do |sym|
|
407
418
|
items = meaningful.select { |_, rec| rec[:action] == sym }.map { |path, _| path }
|
408
419
|
next if items.empty?
|
420
|
+
|
409
421
|
puts " #{action_labels[sym]}:"
|
410
422
|
items.sort.each do |abs|
|
411
423
|
rel = begin
|
@@ -472,10 +484,8 @@ module Kettle
|
|
472
484
|
|
473
485
|
if defined?(updated_envrc_by_install) && updated_envrc_by_install
|
474
486
|
allowed_truthy = ENV.fetch("allowed", "").to_s =~ ENV_TRUE_RE
|
475
|
-
|
476
|
-
|
477
|
-
reason = allowed_truthy ? "allowed=true" : "force=true"
|
478
|
-
puts "Proceeding after .envrc update because #{reason}."
|
487
|
+
if allowed_truthy
|
488
|
+
puts "Proceeding after .envrc update because allowed=true."
|
479
489
|
else
|
480
490
|
puts
|
481
491
|
puts "IMPORTANT: .envrc was updated during kettle:dev:install."
|
@@ -509,7 +519,10 @@ module Kettle
|
|
509
519
|
puts "Would you like to add '.env.local' to #{gitignore_path}?"
|
510
520
|
print("Add to .gitignore now [Y/n]: ")
|
511
521
|
answer = Kettle::Dev::InputAdapter.gets&.strip
|
512
|
-
|
522
|
+
# Respect an explicit negative answer even when force=true
|
523
|
+
add_it = if answer && answer =~ /\An(o)?\z/i
|
524
|
+
false
|
525
|
+
elsif ENV.fetch("force", "").to_s =~ ENV_TRUE_RE
|
513
526
|
true
|
514
527
|
else
|
515
528
|
answer.nil? || answer.empty? || answer =~ /\Ay(es)?\z/i
|
@@ -104,91 +104,13 @@ module Kettle
|
|
104
104
|
allow_replace: true,
|
105
105
|
)
|
106
106
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
injected
|
115
|
-
optional
|
116
|
-
runtime_heads
|
117
|
-
x_std_libs
|
118
|
-
]
|
119
|
-
modular_gemfiles.each do |base|
|
120
|
-
modular_gemfile = "#{base}.gemfile"
|
121
|
-
src = helpers.prefer_example(File.join(gem_checkout_root, MODULAR_GEMFILE_DIR, modular_gemfile))
|
122
|
-
dest = File.join(project_root, MODULAR_GEMFILE_DIR, modular_gemfile)
|
123
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true)
|
124
|
-
end
|
125
|
-
|
126
|
-
# 4b) gemfiles/modular/style.gemfile
|
127
|
-
modular_gemfile = "style.gemfile"
|
128
|
-
src = helpers.prefer_example(File.join(gem_checkout_root, MODULAR_GEMFILE_DIR, modular_gemfile))
|
129
|
-
dest = File.join(project_root, MODULAR_GEMFILE_DIR, modular_gemfile)
|
130
|
-
if File.basename(src).sub(/\.example\z/, "") == "style.gemfile"
|
131
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true) do |content|
|
132
|
-
# Adjust rubocop-lts constraint based on min_ruby
|
133
|
-
version_map = [
|
134
|
-
[Gem::Version.new("1.8"), "~> 0.1"],
|
135
|
-
[Gem::Version.new("1.9"), "~> 2.0"],
|
136
|
-
[Gem::Version.new("2.0"), "~> 4.0"],
|
137
|
-
[Gem::Version.new("2.1"), "~> 6.0"],
|
138
|
-
[Gem::Version.new("2.2"), "~> 8.0"],
|
139
|
-
[Gem::Version.new("2.3"), "~> 10.0"],
|
140
|
-
[Gem::Version.new("2.4"), "~> 12.0"],
|
141
|
-
[Gem::Version.new("2.5"), "~> 14.0"],
|
142
|
-
[Gem::Version.new("2.6"), "~> 16.0"],
|
143
|
-
[Gem::Version.new("2.7"), "~> 18.0"],
|
144
|
-
[Gem::Version.new("3.0"), "~> 20.0"],
|
145
|
-
[Gem::Version.new("3.1"), "~> 22.0"],
|
146
|
-
[Gem::Version.new("3.2"), "~> 24.0"],
|
147
|
-
[Gem::Version.new("3.3"), "~> 26.0"],
|
148
|
-
[Gem::Version.new("3.4"), "~> 28.0"],
|
149
|
-
]
|
150
|
-
new_constraint = nil
|
151
|
-
rubocop_ruby_gem_version = nil
|
152
|
-
ruby1_8 = version_map.first
|
153
|
-
begin
|
154
|
-
if min_ruby
|
155
|
-
version_map.reverse_each do |min, req|
|
156
|
-
if min_ruby >= min
|
157
|
-
new_constraint = req
|
158
|
-
rubocop_ruby_gem_version = min.segments.join("_")
|
159
|
-
break
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
if !new_constraint || !rubocop_ruby_gem_version
|
164
|
-
# A gem with no declared minimum ruby is effectively >= 1.8.7
|
165
|
-
new_constraint = ruby1_8[1]
|
166
|
-
rubocop_ruby_gem_version = ruby1_8[0].segments.join("_")
|
167
|
-
end
|
168
|
-
rescue StandardError => e
|
169
|
-
Kettle::Dev.debug_error(e, __method__)
|
170
|
-
# ignore, use default
|
171
|
-
ensure
|
172
|
-
new_constraint ||= ruby1_8[1]
|
173
|
-
rubocop_ruby_gem_version ||= ruby1_8[0].segments.join("_")
|
174
|
-
end
|
175
|
-
if new_constraint && rubocop_ruby_gem_version
|
176
|
-
token = "{RUBOCOP|LTS|CONSTRAINT}"
|
177
|
-
content.gsub!(token, new_constraint) if content.include?(token)
|
178
|
-
token = "{RUBOCOP|RUBY|GEM}"
|
179
|
-
content.gsub!(token, "rubocop-ruby#{rubocop_ruby_gem_version}") if content.include?(token)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
else
|
183
|
-
helpers.copy_file_with_prompt(src, dest, allow_create: true, allow_replace: true)
|
184
|
-
end
|
185
|
-
|
186
|
-
# 4c) Copy modular directories with nested/versioned files
|
187
|
-
%w[erb mutex_m stringio x_std_libs].each do |dir|
|
188
|
-
src_dir = File.join(gem_checkout_root, MODULAR_GEMFILE_DIR, dir)
|
189
|
-
dest_dir = File.join(project_root, MODULAR_GEMFILE_DIR, dir)
|
190
|
-
helpers.copy_dir_with_prompt(src_dir, dest_dir)
|
191
|
-
end
|
107
|
+
# 4) gemfiles/modular/* and nested directories (delegated for DRYness)
|
108
|
+
Kettle::Dev::ModularGemfiles.sync!(
|
109
|
+
helpers: helpers,
|
110
|
+
project_root: project_root,
|
111
|
+
gem_checkout_root: gem_checkout_root,
|
112
|
+
min_ruby: min_ruby,
|
113
|
+
)
|
192
114
|
|
193
115
|
# 5) spec/spec_helper.rb (no create)
|
194
116
|
dest_spec_helper = File.join(project_root, "spec/spec_helper.rb")
|
@@ -427,6 +349,7 @@ module Kettle
|
|
427
349
|
src = helpers.prefer_example(File.join(gem_checkout_root, rel))
|
428
350
|
dest = File.join(project_root, rel)
|
429
351
|
next unless File.exist?(src)
|
352
|
+
|
430
353
|
if File.basename(rel) == "README.md"
|
431
354
|
# Precompute destination README H1 prefix (emoji(s) or first grapheme) before any overwrite occurs
|
432
355
|
prev_readme = File.exist?(dest) ? File.read(dest) : nil
|
@@ -442,6 +365,7 @@ module Kettle
|
|
442
365
|
loop do
|
443
366
|
cluster = s[/\A\X/u]
|
444
367
|
break if cluster.nil? || cluster.empty?
|
368
|
+
|
445
369
|
if emoji_re =~ cluster
|
446
370
|
out << cluster
|
447
371
|
s = s[cluster.length..-1].to_s
|
@@ -481,6 +405,7 @@ module Kettle
|
|
481
405
|
# Parse Markdown headings while ignoring fenced code blocks (``` ... ```)
|
482
406
|
build_sections = lambda do |md|
|
483
407
|
return {lines: [], sections: [], line_count: 0} unless md
|
408
|
+
|
484
409
|
lines = md.split("\n", -1)
|
485
410
|
line_count = lines.length
|
486
411
|
|
@@ -494,6 +419,7 @@ module Kettle
|
|
494
419
|
next
|
495
420
|
end
|
496
421
|
next if in_code
|
422
|
+
|
497
423
|
if (m = ln.match(/^(#+)\s+.+/))
|
498
424
|
level = m[1].length
|
499
425
|
title = ln.sub(/^#+\s+/, "")
|
@@ -527,6 +453,7 @@ module Kettle
|
|
527
453
|
j = i + 1
|
528
454
|
while j < sections_arr.length
|
529
455
|
return sections_arr[j][:start] - 1 if sections_arr[j][:level] <= current[:level]
|
456
|
+
|
530
457
|
j += 1
|
531
458
|
end
|
532
459
|
total_lines - 1
|
@@ -542,6 +469,7 @@ module Kettle
|
|
542
469
|
base = s[:base]
|
543
470
|
# Only set once (first occurrence wins)
|
544
471
|
next if dest_lookup.key?(base)
|
472
|
+
|
545
473
|
be = branch_end_index.call(dest_parsed[:sections], idx, dest_parsed[:line_count])
|
546
474
|
body_lines = dest_parsed[:lines][(s[:start] + 1)..be] || []
|
547
475
|
dest_lookup[base] = {body_branch: body_lines.join("\n"), level: s[:level]}
|
@@ -563,6 +491,7 @@ module Kettle
|
|
563
491
|
# Iterate in reverse to keep indices valid
|
564
492
|
src_parsed[:sections].reverse_each.with_index do |sec, rev_i|
|
565
493
|
next unless targets.include?(sec[:base])
|
494
|
+
|
566
495
|
# Determine branch range in src for this section
|
567
496
|
# rev_i is reverse index; compute forward index
|
568
497
|
i = src_parsed[:sections].length - 1 - rev_i
|
@@ -730,6 +659,7 @@ module Kettle
|
|
730
659
|
rescue StandardError => e
|
731
660
|
# Do not swallow intentional task aborts
|
732
661
|
raise if e.is_a?(Kettle::Dev::Error)
|
662
|
+
|
733
663
|
puts "WARNING: Could not determine env file changes: #{e.class}: #{e.message}"
|
734
664
|
end
|
735
665
|
|
@@ -831,6 +761,7 @@ module Kettle
|
|
831
761
|
hook_pairs = [[hook_ruby_src, "commit-msg", 0o755], [hook_sh_src, "prepare-commit-msg", 0o755]]
|
832
762
|
hook_pairs.each do |src, base, mode|
|
833
763
|
next unless File.file?(src)
|
764
|
+
|
834
765
|
hook_dests.each do |dstdir|
|
835
766
|
begin
|
836
767
|
FileUtils.mkdir_p(dstdir)
|
@@ -42,9 +42,13 @@ module Kettle
|
|
42
42
|
print("#{prompt} #{default ? "[Y/n]" : "[y/N]"}: ")
|
43
43
|
ans = Kettle::Dev::InputAdapter.gets&.strip
|
44
44
|
ans = "" if ans.nil?
|
45
|
+
# Normalize explicit no first
|
46
|
+
return false if ans =~ /\An(o)?\z/i
|
45
47
|
if default
|
48
|
+
# Empty -> default true; explicit yes -> true; anything else -> false
|
46
49
|
ans.empty? || ans =~ /\Ay(es)?\z/i
|
47
50
|
else
|
51
|
+
# Empty -> default false; explicit yes -> true; others (including garbage) -> false
|
48
52
|
ans =~ /\Ay(es)?\z/i
|
49
53
|
end
|
50
54
|
end
|
data/lib/kettle/dev/version.rb
CHANGED
@@ -16,6 +16,7 @@ module Kettle
|
|
16
16
|
content = File.read(path)
|
17
17
|
m = content.match(/VERSION\s*=\s*(["'])([^"']+)\1/)
|
18
18
|
next unless m
|
19
|
+
|
19
20
|
m[2]
|
20
21
|
end.compact
|
21
22
|
abort!("VERSION constant not found in #{root}/lib/**/version.rb") if versions.none?
|
@@ -39,6 +40,7 @@ module Kettle
|
|
39
40
|
|
40
41
|
if cmaj > pmaj
|
41
42
|
return :epic if cmaj && cmaj > 1000
|
43
|
+
|
42
44
|
:major
|
43
45
|
elsif cmin > pmin
|
44
46
|
:minor
|
data/lib/kettle/dev.rb
CHANGED
@@ -26,6 +26,7 @@ module Kettle
|
|
26
26
|
autoload :PreReleaseCLI, "kettle/dev/pre_release_cli"
|
27
27
|
autoload :SetupCLI, "kettle/dev/setup_cli"
|
28
28
|
autoload :TemplateHelpers, "kettle/dev/template_helpers"
|
29
|
+
autoload :ModularGemfiles, "kettle/dev/modular_gemfiles"
|
29
30
|
autoload :Version, "kettle/dev/version"
|
30
31
|
autoload :Versioning, "kettle/dev/versioning"
|
31
32
|
|