kettle-dev 1.1.60 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +22 -1
- data/Gemfile +3 -0
- data/Gemfile.example +3 -0
- data/README.md +74 -25
- data/Rakefile.example +1 -1
- data/gemfiles/modular/style.gemfile.example +1 -1
- data/gemfiles/modular/templating.gemfile +3 -0
- data/lib/kettle/dev/changelog_cli.rb +13 -0
- data/lib/kettle/dev/modular_gemfiles.rb +12 -3
- data/lib/kettle/dev/prism_appraisals.rb +306 -0
- data/lib/kettle/dev/prism_gemfile.rb +136 -0
- data/lib/kettle/dev/prism_gemspec.rb +284 -0
- data/lib/kettle/dev/prism_utils.rb +201 -0
- data/lib/kettle/dev/readme_backers.rb +1 -4
- data/lib/kettle/dev/setup_cli.rb +17 -28
- data/lib/kettle/dev/source_merger.rb +458 -0
- data/lib/kettle/dev/tasks/template_task.rb +32 -86
- data/lib/kettle/dev/template_helpers.rb +74 -338
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +23 -3
- data/sig/kettle/dev/appraisals_ast_merger.rbs +72 -0
- data/sig/kettle/dev/changelog_cli.rbs +64 -0
- data/sig/kettle/dev/prism_utils.rbs +56 -0
- data/sig/kettle/dev/source_merger.rbs +86 -0
- data/sig/kettle/dev/versioning.rbs +21 -0
- data.tar.gz.sig +0 -0
- metadata +16 -5
- metadata.gz.sig +0 -0
- /data/sig/kettle/dev/{dvcscli.rbs → dvcs_cli.rbs} +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# External stdlibs
|
|
4
4
|
require "find"
|
|
5
5
|
require "set"
|
|
6
|
+
require "yaml"
|
|
6
7
|
|
|
7
8
|
module Kettle
|
|
8
9
|
module Dev
|
|
@@ -17,6 +18,12 @@ module Kettle
|
|
|
17
18
|
# The minimum Ruby supported by setup-ruby GHA
|
|
18
19
|
MIN_SETUP_RUBY = Gem::Version.create("2.3")
|
|
19
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
|
+
|
|
20
27
|
module_function
|
|
21
28
|
|
|
22
29
|
# Root of the host project where Rake was invoked
|
|
@@ -270,52 +277,25 @@ module Kettle
|
|
|
270
277
|
|
|
271
278
|
content = File.read(src_path)
|
|
272
279
|
content = yield(content) if block_given?
|
|
273
|
-
#
|
|
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.
|
|
274
283
|
begin
|
|
275
284
|
token = "{KETTLE|DEV|GEM}"
|
|
276
285
|
content = content.gsub(token, "kettle-dev") if content.include?(token)
|
|
277
286
|
rescue StandardError => e
|
|
278
287
|
Kettle::Dev.debug_error(e, __method__)
|
|
279
|
-
# If replacement fails unexpectedly, proceed with content as-is
|
|
280
288
|
end
|
|
281
289
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
""
|
|
291
|
-
end
|
|
292
|
-
content = merge_appraisals(content, existing)
|
|
293
|
-
end
|
|
294
|
-
rescue StandardError => e
|
|
295
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
296
|
-
# On any error, fall back to generated content
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
# If updating a Gemfile or modular .gemfile and the destination already exists,
|
|
300
|
-
# merge dependency lines from the source into the destination to preserve any
|
|
301
|
-
# user-defined gem entries. We append missing `gem "name"` lines; we never
|
|
302
|
-
# alter or remove existing gem lines in the destination.
|
|
303
|
-
begin
|
|
304
|
-
if dest_exists
|
|
305
|
-
dest_str = dest_path.to_s
|
|
306
|
-
is_gemfile_like = File.basename(dest_str) == "Gemfile" || dest_str.end_with?(".gemfile")
|
|
307
|
-
if is_gemfile_like && File.file?(dest_str)
|
|
308
|
-
begin
|
|
309
|
-
existing = File.read(dest_str)
|
|
310
|
-
content = merge_gemfile_dependencies(content, existing)
|
|
311
|
-
rescue StandardError => e
|
|
312
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
313
|
-
# If merging fails, fall back to writing generated content
|
|
314
|
-
end
|
|
315
|
-
end
|
|
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__)
|
|
316
298
|
end
|
|
317
|
-
rescue StandardError => e
|
|
318
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
319
299
|
end
|
|
320
300
|
|
|
321
301
|
write_file(dest_path, content)
|
|
@@ -342,311 +322,24 @@ module Kettle
|
|
|
342
322
|
# @return [String] merged content
|
|
343
323
|
def merge_gemfile_dependencies(src_content, dest_content)
|
|
344
324
|
begin
|
|
345
|
-
|
|
346
|
-
# Collect first occurrence of each gem line in source
|
|
347
|
-
src_gems = {}
|
|
348
|
-
src_content.each_line do |ln|
|
|
349
|
-
next if ln.strip.start_with?("#")
|
|
350
|
-
if (m = ln.match(gem_re))
|
|
351
|
-
name = m[1]
|
|
352
|
-
src_gems[name] ||= ln.rstrip
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# --- Handle `source` replacement/insertion ---
|
|
357
|
-
src_source_line = nil
|
|
358
|
-
src_content.each_line do |ln|
|
|
359
|
-
next if ln.strip.start_with?("#")
|
|
360
|
-
if ln =~ /^\s*source\s+['"][^'"]+['"]\s*$/
|
|
361
|
-
src_source_line = ln.rstrip + "\n"
|
|
362
|
-
break
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
dest_lines = dest_content.lines.dup
|
|
367
|
-
|
|
368
|
-
if src_source_line
|
|
369
|
-
dest_source_idx = dest_lines.index do |ln|
|
|
370
|
-
!ln.strip.start_with?("#") && ln =~ /^\s*source\s+['"][^'"]+['"]\s*$/
|
|
371
|
-
end
|
|
372
|
-
if dest_source_idx
|
|
373
|
-
dest_lines[dest_source_idx] = src_source_line
|
|
374
|
-
else
|
|
375
|
-
# Insert after any leading contiguous comment/blank block at top of file
|
|
376
|
-
insert_idx = 0
|
|
377
|
-
while insert_idx < dest_lines.length && (dest_lines[insert_idx].strip.empty? || dest_lines[insert_idx].lstrip.start_with?("#"))
|
|
378
|
-
insert_idx += 1
|
|
379
|
-
end
|
|
380
|
-
dest_lines.insert(insert_idx, src_source_line)
|
|
381
|
-
end
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# --- Handle `git_source` replacement/insertion ---
|
|
385
|
-
# Collect non-comment git_source lines from source (preserve order)
|
|
386
|
-
src_git_lines = src_content.each_line.select { |ln| !ln.strip.start_with?("#") && ln =~ /^\s*git_source\s*\(/ }
|
|
387
|
-
if src_git_lines.any?
|
|
388
|
-
# Insert new git_source lines in the same order as they appear in the source
|
|
389
|
-
# When inserting (not replacing), place them immediately after the source line if present
|
|
390
|
-
insert_after_source_idx = dest_lines.index { |ln| !ln.strip.start_with?("#") && ln =~ /^\s*source\s+['"][^'"]+['"]\s*$/ }
|
|
391
|
-
|
|
392
|
-
# Iterate source git lines in reverse for insertion so order is preserved when inserting at same index
|
|
393
|
-
src_git_lines.reverse_each do |gln|
|
|
394
|
-
# Attempt to extract the git_source "name" (handles forms like git_source(:github) or git_source :github)
|
|
395
|
-
name_match = gln.match(/^\s*git_source\s*\(?\s*:?(\w+)\b/)
|
|
396
|
-
name = name_match ? name_match[1].to_s : nil
|
|
397
|
-
|
|
398
|
-
replaced = false
|
|
399
|
-
if name
|
|
400
|
-
# Try to find a git_source in destination with the same name
|
|
401
|
-
dest_same_idx = dest_lines.index do |dln|
|
|
402
|
-
!dln.strip.start_with?("#") && dln =~ /^\s*git_source\s*\(?\s*:?#{Regexp.escape(name)}\b/
|
|
403
|
-
end
|
|
404
|
-
if dest_same_idx
|
|
405
|
-
dest_lines[dest_same_idx] = gln.rstrip + "\n"
|
|
406
|
-
replaced = true
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
unless replaced
|
|
411
|
-
# If destination has a github git_source, replace that
|
|
412
|
-
dest_github_idx = dest_lines.index do |dln|
|
|
413
|
-
!dln.strip.start_with?("#") && dln =~ /^\s*git_source\s*\(?\s*:?github\b/
|
|
414
|
-
end
|
|
415
|
-
if dest_github_idx
|
|
416
|
-
dest_lines[dest_github_idx] = gln.rstrip + "\n"
|
|
417
|
-
else
|
|
418
|
-
# Insert below the source line if present, otherwise at top (after comments)
|
|
419
|
-
insert_idx =
|
|
420
|
-
if insert_after_source_idx
|
|
421
|
-
insert_after_source_idx + 1
|
|
422
|
-
else
|
|
423
|
-
0
|
|
424
|
-
end
|
|
425
|
-
dest_lines.insert(insert_idx, gln.rstrip + "\n")
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
# Index existing gems in destination (after potential source/git_source changes)
|
|
432
|
-
dest_gems = {}
|
|
433
|
-
dest_lines.join.each_line do |ln|
|
|
434
|
-
next if ln.strip.start_with?("#")
|
|
435
|
-
if (m = ln.match(gem_re))
|
|
436
|
-
dest_gems[m[1]] = true
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
missing = src_gems.keys.reject { |n| dest_gems.key?(n) }
|
|
441
|
-
# If nothing to change, return original destination content
|
|
442
|
-
if missing.empty? && src_source_line.nil? && src_git_lines.empty?
|
|
443
|
-
return dest_content
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
out = dest_lines.join
|
|
447
|
-
out << "\n" unless out.end_with?("\n") || out.empty?
|
|
448
|
-
if missing.any?
|
|
449
|
-
out << missing.map { |n| src_gems[n] }.join("\n")
|
|
450
|
-
out << "\n"
|
|
451
|
-
end
|
|
452
|
-
out
|
|
325
|
+
Kettle::Dev::PrismGemfile.merge_gem_calls(src_content.to_s, dest_content.to_s)
|
|
453
326
|
rescue StandardError => e
|
|
454
327
|
Kettle::Dev.debug_error(e, __method__)
|
|
455
328
|
dest_content
|
|
456
329
|
end
|
|
457
330
|
end
|
|
458
331
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
# if any; otherwise retain destination's header comments.
|
|
466
|
-
# * If destination lacks the block, add the full template block (with its header).
|
|
467
|
-
# - Preserve destination-only blocks (not present in template) unchanged and after
|
|
468
|
-
# the merged template-ordered blocks.
|
|
469
|
-
# - Preamble (content before first appraise) comes from template when present, else destination.
|
|
470
|
-
def merge_appraisals(template_content, dest_content)
|
|
471
|
-
begin
|
|
472
|
-
# Helper: extract contiguous leading header lines (comments and blank lines)
|
|
473
|
-
extract_leading_header = lambda do |text|
|
|
474
|
-
lines = text.lines
|
|
475
|
-
header_lines = []
|
|
476
|
-
idx = 0
|
|
477
|
-
while idx < lines.length
|
|
478
|
-
ln = lines[idx]
|
|
479
|
-
break unless ln.strip.empty? || ln.lstrip.start_with?("#")
|
|
480
|
-
header_lines << ln
|
|
481
|
-
idx += 1
|
|
482
|
-
end
|
|
483
|
-
body = (idx < lines.length) ? lines[idx..-1].join : ""
|
|
484
|
-
{header: header_lines.join, body: body}
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
parse_blocks = lambda do |text|
|
|
488
|
-
lines = text.lines
|
|
489
|
-
blocks = []
|
|
490
|
-
i = 0
|
|
491
|
-
while i < lines.length
|
|
492
|
-
line = lines[i]
|
|
493
|
-
if line =~ /^\s*appraise\s+["']([^"']+)["']\s+do\s*$/
|
|
494
|
-
name = $1
|
|
495
|
-
# collect header comment lines immediately preceding (contiguous, no blank between comment group and appraise line)
|
|
496
|
-
header_lines = []
|
|
497
|
-
j = i - 1
|
|
498
|
-
while j >= 0
|
|
499
|
-
prev = lines[j]
|
|
500
|
-
break if prev.strip.empty?
|
|
501
|
-
if prev.lstrip.start_with?("#")
|
|
502
|
-
header_lines.unshift(prev)
|
|
503
|
-
j -= 1
|
|
504
|
-
else
|
|
505
|
-
break
|
|
506
|
-
end
|
|
507
|
-
end
|
|
508
|
-
body_lines = []
|
|
509
|
-
i += 1
|
|
510
|
-
while i < lines.length
|
|
511
|
-
l2 = lines[i]
|
|
512
|
-
if l2 =~ /^\s*end\s*$/
|
|
513
|
-
end_line = l2
|
|
514
|
-
blocks << {
|
|
515
|
-
name: name,
|
|
516
|
-
header: header_lines.dup,
|
|
517
|
-
body: body_lines.dup,
|
|
518
|
-
end_line: end_line,
|
|
519
|
-
raw_order: blocks.length,
|
|
520
|
-
original_indices: (j ? (j + 1)..i : i),
|
|
521
|
-
}
|
|
522
|
-
break
|
|
523
|
-
else
|
|
524
|
-
body_lines << l2
|
|
525
|
-
end
|
|
526
|
-
i += 1
|
|
527
|
-
end
|
|
528
|
-
end
|
|
529
|
-
i += 1
|
|
530
|
-
end
|
|
531
|
-
preamble = if blocks.empty?
|
|
532
|
-
text
|
|
533
|
-
else
|
|
534
|
-
# preamble = lines from start up to first block start (exclusive)
|
|
535
|
-
first_block = blocks.first
|
|
536
|
-
# Take lines up to first occurrence of the appraise line (supports either quote type)
|
|
537
|
-
re = /^\s*appraise\s+["']#{Regexp.escape(first_block[:name])}["']\s+do\s*$/
|
|
538
|
-
idx = lines.index { |l| l =~ re } || 0
|
|
539
|
-
lines[0...idx].join
|
|
540
|
-
end
|
|
541
|
-
{blocks: blocks, preamble: preamble}
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
# Extract leading headers from template and destination and parse their bodies
|
|
545
|
-
tmpl_parts = extract_leading_header.call(template_content)
|
|
546
|
-
dest_parts = extract_leading_header.call(dest_content)
|
|
547
|
-
|
|
548
|
-
# If the template does not provide a leading header, preserve the destination as-is
|
|
549
|
-
# so that per-block adjacent header comments (including those at the top of file)
|
|
550
|
-
# are still parsed and preserved. Only strip the destination leading header when
|
|
551
|
-
# the template has a leading header to replace it.
|
|
552
|
-
dest_parse_source = if tmpl_parts[:header].to_s.strip.empty?
|
|
553
|
-
dest_content
|
|
554
|
-
else
|
|
555
|
-
dest_parts[:body]
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
tmpl = parse_blocks.call(tmpl_parts[:body])
|
|
559
|
-
dest = parse_blocks.call(dest_parse_source)
|
|
560
|
-
tmpl_blocks = tmpl[:blocks]
|
|
561
|
-
dest_blocks = dest[:blocks]
|
|
562
|
-
dest_by_name = dest_blocks.map { |b| [b[:name], b] }.to_h
|
|
563
|
-
|
|
564
|
-
merged_blocks_strings = []
|
|
565
|
-
gem_or_eval_re = /^\s*(?:gem|eval_gemfile)\b/
|
|
566
|
-
|
|
567
|
-
tmpl_blocks.each do |tb|
|
|
568
|
-
if (db = dest_by_name[tb[:name]])
|
|
569
|
-
# Merge lines
|
|
570
|
-
existing_lines = db[:body].map(&:rstrip)
|
|
571
|
-
existing_set = existing_lines.to_set
|
|
572
|
-
# Collect template gem/eval lines
|
|
573
|
-
tmpl_needed = tb[:body].select { |l| gem_or_eval_re =~ l }
|
|
574
|
-
additions = []
|
|
575
|
-
tmpl_needed.each do |l|
|
|
576
|
-
line_key = l.rstrip
|
|
577
|
-
additions << l unless existing_set.include?(line_key)
|
|
578
|
-
end
|
|
579
|
-
merged_body = db[:body].dup
|
|
580
|
-
unless additions.empty?
|
|
581
|
-
# insert before end (just append; body excludes 'end')
|
|
582
|
-
merged_body += additions
|
|
583
|
-
end
|
|
584
|
-
header = tb[:header].any? ? tb[:header] : db[:header]
|
|
585
|
-
# If the template provides no leading header and the destination preamble
|
|
586
|
-
# already ends with this header, skip emitting the header for this block
|
|
587
|
-
# to avoid duplicating it (it will already be present at the top of the file).
|
|
588
|
-
header_to_emit = if tmpl_parts[:header].to_s.strip.empty? && !dest[:preamble].to_s.strip.empty? && !header.empty? && dest[:preamble].to_s.end_with?(header.join)
|
|
589
|
-
[]
|
|
590
|
-
else
|
|
591
|
-
header
|
|
592
|
-
end
|
|
593
|
-
block_text = +""
|
|
594
|
-
block_text << "\n" unless merged_blocks_strings.empty?
|
|
595
|
-
header_to_emit.each { |hl| block_text << hl } if header_to_emit.any?
|
|
596
|
-
block_text << "appraise \"#{tb[:name]}\" do\n"
|
|
597
|
-
merged_body.each { |bl| block_text << bl }
|
|
598
|
-
block_text << db[:end_line]
|
|
599
|
-
merged_blocks_strings << block_text
|
|
600
|
-
dest_by_name.delete(tb[:name])
|
|
601
|
-
else
|
|
602
|
-
# New block from template
|
|
603
|
-
block_text = +""
|
|
604
|
-
block_text << "\n" unless merged_blocks_strings.empty?
|
|
605
|
-
tb[:header].each { |hl| block_text << hl } if tb[:header].any?
|
|
606
|
-
block_text << "appraise \"#{tb[:name]}\" do\n"
|
|
607
|
-
tb[:body].each { |bl| block_text << bl }
|
|
608
|
-
block_text << tb[:end_line]
|
|
609
|
-
merged_blocks_strings << block_text
|
|
610
|
-
end
|
|
611
|
-
end
|
|
612
|
-
# Append destination-only blocks preserving their original text
|
|
613
|
-
dest_remaining_order = dest_blocks.select { |b| dest_by_name.key?(b[:name]) }
|
|
614
|
-
dest_remaining_order.each do |b|
|
|
615
|
-
block_text = +""
|
|
616
|
-
block_text << "\n" unless merged_blocks_strings.empty?
|
|
617
|
-
b[:header].each { |hl| block_text << hl } if b[:header].any?
|
|
618
|
-
block_text << "appraise \"#{b[:name]}\" do\n"
|
|
619
|
-
b[:body].each { |bl| block_text << bl }
|
|
620
|
-
block_text << b[:end_line]
|
|
621
|
-
merged_blocks_strings << block_text
|
|
622
|
-
end
|
|
623
|
-
|
|
624
|
-
# Build final preamble:
|
|
625
|
-
# - If template provides a leading header, use that header and then prefer the
|
|
626
|
-
# template's body preamble when present, otherwise fall back to the
|
|
627
|
-
# destination's body preamble.
|
|
628
|
-
# - If template does NOT provide a leading header, leave the destination
|
|
629
|
-
# preamble (including any leading header/comments) intact.
|
|
630
|
-
preamble = +""
|
|
631
|
-
if tmpl_parts[:header].to_s.strip.empty?
|
|
632
|
-
preamble << dest[:preamble].to_s
|
|
633
|
-
else
|
|
634
|
-
body_preamble = tmpl[:preamble].to_s.strip.empty? ? dest[:preamble].to_s : tmpl[:preamble].to_s
|
|
635
|
-
preamble << tmpl_parts[:header].to_s
|
|
636
|
-
preamble << body_preamble.to_s
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
out = +""
|
|
640
|
-
out << preamble unless preamble.nil? || preamble.empty?
|
|
641
|
-
out << "\n" unless out.end_with?("\n")
|
|
642
|
-
out << merged_blocks_strings.join
|
|
643
|
-
out << "\n" unless out.end_with?("\n")
|
|
644
|
-
out
|
|
645
|
-
rescue StandardError => e
|
|
646
|
-
Kettle::Dev.debug_error(e, __method__)
|
|
647
|
-
# Fallback: prefer destination (user changes) and append template content to allow manual reconciliation
|
|
648
|
-
dest_content + "\n# --- TEMPLATE APPRAISALS (unmerged) ---\n" + template_content
|
|
332
|
+
def apply_appraisals_merge(content, dest_path)
|
|
333
|
+
dest = dest_path.to_s
|
|
334
|
+
existing = if File.exist?(dest)
|
|
335
|
+
File.read(dest)
|
|
336
|
+
else
|
|
337
|
+
""
|
|
649
338
|
end
|
|
339
|
+
Kettle::Dev::PrismAppraisals.merge(content, existing)
|
|
340
|
+
rescue StandardError => e
|
|
341
|
+
Kettle::Dev.debug_error(e, __method__)
|
|
342
|
+
content
|
|
650
343
|
end
|
|
651
344
|
|
|
652
345
|
# Copy a directory tree, prompting before creating or overwriting.
|
|
@@ -883,8 +576,8 @@ module Kettle
|
|
|
883
576
|
# ignore
|
|
884
577
|
end
|
|
885
578
|
|
|
886
|
-
# Replace occurrences of the template gem name
|
|
887
|
-
#
|
|
579
|
+
# Replace occurrences of the literal template gem name ("kettle-dev")
|
|
580
|
+
# with the destination gem name.
|
|
888
581
|
c = c.gsub("kettle-dev", gem_name)
|
|
889
582
|
c = c.gsub(/\bKettle::Dev\b/u, namespace) unless namespace.empty?
|
|
890
583
|
c = c.gsub("Kettle%3A%3ADev", namespace_shield) unless namespace_shield.empty?
|
|
@@ -899,6 +592,49 @@ module Kettle
|
|
|
899
592
|
def gemspec_metadata(root = project_root)
|
|
900
593
|
Kettle::Dev::GemSpecReader.load(root)
|
|
901
594
|
end
|
|
595
|
+
|
|
596
|
+
def apply_strategy(content, dest_path)
|
|
597
|
+
return content unless ruby_template?(dest_path)
|
|
598
|
+
strategy = strategy_for(dest_path)
|
|
599
|
+
dest_content = File.exist?(dest_path) ? File.read(dest_path) : ""
|
|
600
|
+
Kettle::Dev::SourceMerger.apply(strategy: strategy, src: content, dest: dest_content, path: rel_path(dest_path))
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def manifestation
|
|
604
|
+
@@manifestation ||= load_manifest
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def strategy_for(dest_path)
|
|
608
|
+
relative = rel_path(dest_path)
|
|
609
|
+
manifestation.find do |entry|
|
|
610
|
+
File.fnmatch?(entry[:path], relative, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
|
|
611
|
+
end&.fetch(:strategy, :skip) || :skip
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def rel_path(path)
|
|
615
|
+
project = project_root.to_s
|
|
616
|
+
path.to_s.sub(/^#{Regexp.escape(project)}\/?/, "")
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def ruby_template?(dest_path)
|
|
620
|
+
base = File.basename(dest_path.to_s)
|
|
621
|
+
return true if RUBY_BASENAMES.include?(base)
|
|
622
|
+
return true if RUBY_SUFFIXES.any? { |suffix| base.end_with?(suffix) }
|
|
623
|
+
ext = File.extname(base)
|
|
624
|
+
RUBY_EXTENSIONS.include?(ext)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def load_manifest
|
|
628
|
+
raw = YAML.load_file(TEMPLATE_MANIFEST_PATH)
|
|
629
|
+
raw.map do |entry|
|
|
630
|
+
{
|
|
631
|
+
path: entry["path"],
|
|
632
|
+
strategy: entry["strategy"].to_s.strip.downcase.to_sym,
|
|
633
|
+
}
|
|
634
|
+
end
|
|
635
|
+
rescue Errno::ENOENT
|
|
636
|
+
[]
|
|
637
|
+
end
|
|
902
638
|
end
|
|
903
639
|
end
|
|
904
640
|
end
|
data/lib/kettle/dev/version.rb
CHANGED
data/lib/kettle/dev.rb
CHANGED
|
@@ -21,8 +21,13 @@ module Kettle
|
|
|
21
21
|
autoload :GitAdapter, "kettle/dev/git_adapter"
|
|
22
22
|
autoload :GitCommitFooter, "kettle/dev/git_commit_footer"
|
|
23
23
|
autoload :InputAdapter, "kettle/dev/input_adapter"
|
|
24
|
+
autoload :PrismUtils, "kettle/dev/prism_utils"
|
|
25
|
+
autoload :PrismGemspec, "kettle/dev/prism_gemspec"
|
|
26
|
+
autoload :PrismGemfile, "kettle/dev/prism_gemfile"
|
|
27
|
+
autoload :PrismAppraisals, "kettle/dev/prism_appraisals"
|
|
24
28
|
autoload :ReadmeBackers, "kettle/dev/readme_backers"
|
|
25
29
|
autoload :OpenCollectiveConfig, "kettle/dev/open_collective_config"
|
|
30
|
+
autoload :SourceMerger, "kettle/dev/source_merger"
|
|
26
31
|
autoload :ReleaseCLI, "kettle/dev/release_cli"
|
|
27
32
|
autoload :PreReleaseCLI, "kettle/dev/pre_release_cli"
|
|
28
33
|
autoload :SetupCLI, "kettle/dev/setup_cli"
|
|
@@ -74,15 +79,30 @@ module Kettle
|
|
|
74
79
|
@defaults = []
|
|
75
80
|
|
|
76
81
|
class << self
|
|
77
|
-
# Emit a debug warning for rescued errors when
|
|
82
|
+
# Emit a debug warning for rescued errors when kettle-dev debugging is enabled.
|
|
83
|
+
# Controlled by KETTLE_DEV_DEBUG=true (or DEBUG=true as fallback).
|
|
78
84
|
# @param error [Exception]
|
|
79
85
|
# @param context [String, Symbol, nil] optional label, often __method__
|
|
80
86
|
# @return [void]
|
|
81
87
|
def debug_error(error, context = nil)
|
|
82
88
|
return unless DEBUGGING
|
|
83
|
-
|
|
89
|
+
|
|
90
|
+
ctx = context ? context.to_s : "KETTLE-DEV-RESCUE"
|
|
84
91
|
Kernel.warn("[#{ctx}] #{error.class}: #{error.message}")
|
|
85
|
-
rescue
|
|
92
|
+
rescue StandardError
|
|
93
|
+
# never raise from debug logging
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Emit a debug log line when kettle-dev debugging is enabled.
|
|
97
|
+
# Controlled by KETTLE_DEV_DEBUG=true (or DEBUG=true as fallback).
|
|
98
|
+
# @param msg [String]
|
|
99
|
+
# @return [void]
|
|
100
|
+
def debug_log(msg, context = nil)
|
|
101
|
+
return unless DEBUGGING
|
|
102
|
+
|
|
103
|
+
ctx = context ? context.to_s : "KETTLE-DEV-DEBUG"
|
|
104
|
+
Kernel.warn("[#{ctx}] #{msg}")
|
|
105
|
+
rescue StandardError
|
|
86
106
|
# never raise from debug logging
|
|
87
107
|
end
|
|
88
108
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# TypeProf 0.21.11
|
|
2
|
+
|
|
3
|
+
module Kettle
|
|
4
|
+
module Dev
|
|
5
|
+
# AST-driven merger for Appraisals files using Prism
|
|
6
|
+
module AppraisalsAstMerger
|
|
7
|
+
TRACKED_METHODS: Array[Symbol]
|
|
8
|
+
|
|
9
|
+
# Merge template and destination Appraisals files preserving comments
|
|
10
|
+
def self.merge: (String template_content, String dest_content) -> String
|
|
11
|
+
|
|
12
|
+
# Extract blocks and preamble from parse result
|
|
13
|
+
def self.extract_blocks: (
|
|
14
|
+
Prism::ParseResult parse_result,
|
|
15
|
+
String source_content
|
|
16
|
+
) -> [Array[Prism::Comment], Array[Hash[Symbol, untyped]]]
|
|
17
|
+
|
|
18
|
+
# Check if node is an appraise call
|
|
19
|
+
def self.appraise_call?: (Prism::Node node) -> bool
|
|
20
|
+
|
|
21
|
+
# Extract appraise block name from node
|
|
22
|
+
def self.extract_appraise_name: (Prism::Node? node) -> String?
|
|
23
|
+
|
|
24
|
+
# Merge preamble comments from template and destination
|
|
25
|
+
def self.merge_preambles: (
|
|
26
|
+
Array[Prism::Comment] tmpl_comments,
|
|
27
|
+
Array[Prism::Comment] dest_comments
|
|
28
|
+
) -> Array[String]
|
|
29
|
+
|
|
30
|
+
# Extract block header comments
|
|
31
|
+
def self.extract_block_header: (
|
|
32
|
+
Prism::Node node,
|
|
33
|
+
Array[String] source_lines,
|
|
34
|
+
Array[Hash[Symbol, untyped]] previous_blocks
|
|
35
|
+
) -> String
|
|
36
|
+
|
|
37
|
+
# Merge blocks from template and destination
|
|
38
|
+
def self.merge_blocks: (
|
|
39
|
+
Array[Hash[Symbol, untyped]] tmpl_blocks,
|
|
40
|
+
Array[Hash[Symbol, untyped]] dest_blocks,
|
|
41
|
+
Prism::ParseResult tmpl_result,
|
|
42
|
+
Prism::ParseResult dest_result
|
|
43
|
+
) -> Array[Hash[Symbol, untyped]]
|
|
44
|
+
|
|
45
|
+
# Merge statements within a block
|
|
46
|
+
def self.merge_block_statements: (
|
|
47
|
+
Prism::Node? tmpl_body,
|
|
48
|
+
Prism::Node? dest_body,
|
|
49
|
+
Prism::ParseResult dest_result
|
|
50
|
+
) -> Array[Hash[Symbol, untyped]]
|
|
51
|
+
|
|
52
|
+
# Generate statement key for deduplication
|
|
53
|
+
def self.statement_key: (Prism::Node node) -> [Symbol, String?]?
|
|
54
|
+
|
|
55
|
+
# Build output from preamble and blocks
|
|
56
|
+
def self.build_output: (
|
|
57
|
+
Array[String] preamble_lines,
|
|
58
|
+
Array[Hash[Symbol, untyped]] blocks
|
|
59
|
+
) -> String
|
|
60
|
+
|
|
61
|
+
# Normalize statement to use parentheses
|
|
62
|
+
def self.normalize_statement: (Prism::Node node) -> String
|
|
63
|
+
|
|
64
|
+
# Normalize argument to canonical format
|
|
65
|
+
def self.normalize_argument: (Prism::Node arg) -> String
|
|
66
|
+
|
|
67
|
+
# Extract original statements from node
|
|
68
|
+
def self.extract_original_statements: (Prism::Node node) -> Array[Hash[Symbol, untyped]]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# TypeProf 0.21.11
|
|
2
|
+
|
|
3
|
+
module Kettle
|
|
4
|
+
module Dev
|
|
5
|
+
# CLI for updating CHANGELOG.md with new version sections
|
|
6
|
+
class ChangelogCLI
|
|
7
|
+
UNRELEASED_SECTION_HEADING: String
|
|
8
|
+
|
|
9
|
+
@root: String
|
|
10
|
+
@changelog_path: String
|
|
11
|
+
@coverage_path: String
|
|
12
|
+
|
|
13
|
+
def initialize: () -> void
|
|
14
|
+
|
|
15
|
+
# Main entry point that updates CHANGELOG.md
|
|
16
|
+
def run: () -> void
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Abort with error message
|
|
21
|
+
def abort: (String msg) -> void
|
|
22
|
+
|
|
23
|
+
# Detect version from lib/**/version.rb
|
|
24
|
+
def detect_version: () -> String
|
|
25
|
+
|
|
26
|
+
# Extract unreleased section from changelog
|
|
27
|
+
def extract_unreleased: (String content) -> [String?, String?, String?]
|
|
28
|
+
|
|
29
|
+
# Detect previous version from after text
|
|
30
|
+
def detect_previous_version: (String after_text) -> String?
|
|
31
|
+
|
|
32
|
+
# Filter unreleased sections keeping only those with content
|
|
33
|
+
def filter_unreleased_sections: (String unreleased_block) -> String
|
|
34
|
+
|
|
35
|
+
# Get coverage lines from coverage.json
|
|
36
|
+
def coverage_lines: () -> [String?, String?]
|
|
37
|
+
|
|
38
|
+
# Get YARD documentation percentage
|
|
39
|
+
def yard_percent_documented: () -> String?
|
|
40
|
+
|
|
41
|
+
# Convert legacy heading tag suffix to list format
|
|
42
|
+
def convert_heading_tag_suffix_to_list: (String text) -> String
|
|
43
|
+
|
|
44
|
+
# Update link references in changelog
|
|
45
|
+
def update_link_refs: (
|
|
46
|
+
String content,
|
|
47
|
+
String? owner,
|
|
48
|
+
String? repo,
|
|
49
|
+
String? prev_version,
|
|
50
|
+
String new_version
|
|
51
|
+
) -> String
|
|
52
|
+
|
|
53
|
+
# Normalize spacing around headings
|
|
54
|
+
def normalize_heading_spacing: (String text) -> String
|
|
55
|
+
|
|
56
|
+
# Ensure proper footer spacing
|
|
57
|
+
def ensure_footer_spacing: (String text) -> String
|
|
58
|
+
|
|
59
|
+
# Detect initial compare base from changelog
|
|
60
|
+
def detect_initial_compare_base: (Array[String] lines) -> String
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|