kettle-dev 1.1.60 → 1.2.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 +15 -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/appraisals_ast_merger.rb +383 -0
- data/lib/kettle/dev/changelog_cli.rb +13 -0
- data/lib/kettle/dev/modular_gemfiles.rb +11 -3
- data/lib/kettle/dev/prism_utils.rb +188 -0
- data/lib/kettle/dev/source_merger.rb +345 -0
- data/lib/kettle/dev/tasks/template_task.rb +11 -1
- data/lib/kettle/dev/template_helpers.rb +70 -226
- data/lib/kettle/dev/version.rb +1 -1
- data/lib/kettle/dev.rb +2 -0
- 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 +14 -5
- metadata.gz.sig +0 -0
- /data/sig/kettle/dev/{dvcscli.rbs → dvcs_cli.rbs} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5ef625cbc6016859cd2c88105ce957e3719465cee97356df1fc77a30c8504c1
|
|
4
|
+
data.tar.gz: e18d15a0d1da90c31991b4b6601b698633dad3e377145724f8fb277ec7b2a75a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 227cb01388512c937d73fddf46deac2edf2b062f93a97b8e788ac0af4e21f8bd1b3a941dc4bf8eabfd5b494401e935e08a8d28d10fc1954b728e7148953e531b
|
|
7
|
+
data.tar.gz: 336fe91aab36fd2ca958967fbc3cfaaa2d6cea7dbb5b1e4d63882526b1271561920d41c016f301b93eb533cb636d252e923882859c9be58fbe4bd586b29d870d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,18 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.2.0] - 2025-11-25
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.2.0][1.2.0t]
|
|
36
|
+
- COVERAGE: 94.38% -- 4066/4308 lines in 26 files
|
|
37
|
+
- BRANCH COVERAGE: 78.81% -- 1674/2124 branches in 26 files
|
|
38
|
+
- 69.14% documented
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Source merging switched from Regex-based string manipulation to Prism AST-based manipulation
|
|
43
|
+
- Comments are preserved in the resulting file
|
|
44
|
+
|
|
33
45
|
## [1.1.60] - 2025-11-23
|
|
34
46
|
|
|
35
47
|
- TAG: [v1.1.60][1.1.60t]
|
|
@@ -1458,7 +1470,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
1458
1470
|
- Selecting will run the selected workflow via `act`
|
|
1459
1471
|
- This may move to its own gem in the future.
|
|
1460
1472
|
|
|
1461
|
-
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.
|
|
1473
|
+
[Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.0...HEAD
|
|
1474
|
+
[1.2.0]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.60...v1.2.0
|
|
1475
|
+
[1.2.0t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.2.0
|
|
1462
1476
|
[1.1.60]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.59...v1.1.60
|
|
1463
1477
|
[1.1.60t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.1.60
|
|
1464
1478
|
[1.1.59]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.58...v1.1.59
|
data/Gemfile
CHANGED
|
@@ -12,6 +12,9 @@ git_source(:gitlab) { |repo_name| "https://gitlab.com/#{repo_name}" }
|
|
|
12
12
|
# Include dependencies from <gem name>.gemspec
|
|
13
13
|
gemspec
|
|
14
14
|
|
|
15
|
+
# Templating
|
|
16
|
+
eval_gemfile "gemfiles/modular/templating.gemfile"
|
|
17
|
+
|
|
15
18
|
# Debugging
|
|
16
19
|
eval_gemfile "gemfiles/modular/debug.gemfile"
|
|
17
20
|
|
data/Gemfile.example
CHANGED
|
@@ -12,6 +12,9 @@ git_source(:gitlab) { |repo_name| "https://gitlab.com/#{repo_name}" }
|
|
|
12
12
|
# Include dependencies from <gem name>.gemspec
|
|
13
13
|
gemspec
|
|
14
14
|
|
|
15
|
+
# Templating
|
|
16
|
+
eval_gemfile "gemfiles/modular/templating.gemfile"
|
|
17
|
+
|
|
15
18
|
# Debugging
|
|
16
19
|
eval_gemfile "gemfiles/modular/debug.gemfile"
|
|
17
20
|
|
data/README.md
CHANGED
|
@@ -574,41 +574,84 @@ What it does:
|
|
|
574
574
|
- Stages and commits any bootstrap changes with message: `🎨 Template bootstrap by kettle-dev-setup v<version>`.
|
|
575
575
|
- Executes `bin/rake kettle:dev:install` with the parsed passthrough args.
|
|
576
576
|
|
|
577
|
+
### Template Manifest and AST Strategies
|
|
578
|
+
|
|
579
|
+
`kettle:dev:template` looks at `template_manifest.yml` to determine how each file should be updated. Each entry has a `path` (exact file or glob) and a `strategy`:
|
|
580
|
+
|
|
581
|
+
| Strategy | Behavior |
|
|
582
|
+
| --- | --- |
|
|
583
|
+
| `skip` | Legacy behavior: template content is copied with token replacements and any bespoke merge logic already in place. |
|
|
584
|
+
| `replace` | Template AST replaces the destination outside of `kettle-dev:freeze` sections. |
|
|
585
|
+
| `append` | Only missing AST nodes (e.g., `gem` or `task` declarations) are appended; existing nodes remain untouched. |
|
|
586
|
+
| `merge` | Destination nodes are updated in-place using the template AST (used for `Gemfile`, `*.gemspec`, and `Rakefile`). |
|
|
587
|
+
|
|
588
|
+
All Ruby files receive this reminder (inserted after shebang/frozen-string-literal lines):
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
# To force retention during kettle-dev templating:
|
|
592
|
+
# kettle-dev:freeze
|
|
593
|
+
# # ... your code
|
|
594
|
+
# kettle-dev:unfreeze
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Wrap any code you never want rewritten between `kettle-dev:freeze` / `kettle-dev:unfreeze` comments. When an AST merge fails, the task emits an error asking you to file an issue at https://github.com/kettle-rb/kettle-dev/issues and then aborts—there is no regex fallback.
|
|
598
|
+
|
|
599
|
+
### Template Example
|
|
600
|
+
|
|
601
|
+
Here is an example `template_manifest.yml`:
|
|
602
|
+
|
|
603
|
+
```yaml
|
|
604
|
+
# For each file or glob, specify a strategy for how it should be managed.
|
|
605
|
+
# See https://github.com/kettle-rb/kettle-dev/blob/main/docs/README.md#template-manifest-and-ast-strategies
|
|
606
|
+
# for details on each strategy.
|
|
607
|
+
files:
|
|
608
|
+
- path: "Gemfile"
|
|
609
|
+
strategy: "merge"
|
|
610
|
+
- path: "*.gemspec"
|
|
611
|
+
strategy: "merge"
|
|
612
|
+
- path: "Rakefile"
|
|
613
|
+
strategy: "merge"
|
|
614
|
+
- path: "README.md"
|
|
615
|
+
strategy: "replace"
|
|
616
|
+
- path: ".env.local"
|
|
617
|
+
strategy: "skip"
|
|
618
|
+
```
|
|
619
|
+
|
|
577
620
|
### Open Collective README updater
|
|
578
621
|
|
|
579
622
|
- Script: `exe/kettle-readme-backers` (run as `kettle-readme-backers`)
|
|
580
623
|
- Purpose: Updates README sections for Open Collective backers (individuals) and sponsors (organizations) by fetching live data from your collective.
|
|
581
624
|
- Tags updated in README.md (first match wins for backers):
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
625
|
+
- The default tag prefix is `OPENCOLLECTIVE`, and it is configurable:
|
|
626
|
+
- ENV: `KETTLE_DEV_BACKER_README_OSC_TAG="OPENCOLLECTIVE"`
|
|
627
|
+
- YAML (.opencollective.yml): `readme-osc-tag: "OPENCOLLECTIVE"`
|
|
628
|
+
- The resulting markers become: `<!-- <TAG>:START --> … <!-- <TAG>:END -->`, `<!-- <TAG>-INDIVIDUALS:START --> … <!-- <TAG>-INDIVIDUALS:END -->`, and `<!-- <TAG>-ORGANIZATIONS:START --> … <!-- <TAG>-ORGANIZATIONS:END -->`.
|
|
629
|
+
- ENV overrides YAML.
|
|
630
|
+
- Backers (Individuals): `<!-- <TAG>:START --> … <!-- <TAG>:END -->` or `<!-- <TAG>-INDIVIDUALS:START --> … <!-- <TAG>-INDIVIDUALS:END -->`
|
|
631
|
+
- Sponsors (Organizations): `<!-- <TAG>-ORGANIZATIONS:START --> … <!-- <TAG>-ORGANIZATIONS:END -->`
|
|
589
632
|
- Handle resolution:
|
|
590
|
-
|
|
591
|
-
|
|
633
|
+
1. `OPENCOLLECTIVE_HANDLE` environment variable, if set
|
|
634
|
+
2. `opencollective.yml` in the project root (e.g., `collective: "kettle-rb"` in this repo)
|
|
592
635
|
- Usage:
|
|
593
|
-
|
|
594
|
-
|
|
636
|
+
- `exe/kettle-readme-backers`
|
|
637
|
+
- `OPENCOLLECTIVE_HANDLE=my-collective exe/kettle-readme-backers`
|
|
595
638
|
- Behavior:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
639
|
+
- Writes to README.md only if content between the tags would change.
|
|
640
|
+
- If neither the backers nor sponsors tags are present, prints a helpful warning and exits with status 2.
|
|
641
|
+
- When there are no entries, inserts a friendly placeholder: "No backers yet. Be the first!" or "No sponsors yet. Be the first!".
|
|
642
|
+
- When updates are written and the repository is a git work tree, the script stages README.md and commits with a message thanking new backers and subscribers, including mentions for any newly added backers and subscribers (GitHub @handles when their website/profile is a github.com URL; otherwise their name).
|
|
643
|
+
- Customize the commit subject via env var: `KETTLE_README_BACKERS_COMMIT_SUBJECT="💸 Thanks 🙏 to our new backers 🎒 and subscribers 📜"`.
|
|
644
|
+
- Or via .opencollective.yml: set `readme-backers-commit-subject: "💸 Thanks 🙏 to our new backers 🎒 and subscribers 📜"`.
|
|
645
|
+
- Precedence: ENV overrides .opencollective.yml; if neither is set, a sensible default is used.
|
|
646
|
+
- Note: When used with the provided `.git-hooks`, the subject should start with a gitmoji character (see [gitmoji][📌gitmoji]).
|
|
604
647
|
- Tip:
|
|
605
|
-
|
|
606
|
-
|
|
648
|
+
- Run this locally before committing to keep your README current, or schedule it in CI to refresh periodically.
|
|
649
|
+
- It runs automatically on a once-a-week schedule by the .github/workflows/opencollective.yml workflow that is part of the kettle-dev template.
|
|
607
650
|
- Authentication requirement:
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
651
|
+
- When running in CI with the provided workflow, you must provide an organization-level Actions secret named `README_UPDATER_TOKEN`.
|
|
652
|
+
- Create it under your GitHub organization settings: `https://github.com/organizations/<YOUR_ORG>/settings/secrets/actions`.
|
|
653
|
+
- The updater will look for `REPO` or `GITHUB_REPOSITORY` (both usually set by GitHub Actions) to infer `<YOUR_ORG>` for guidance.
|
|
654
|
+
- If `README_UPDATER_TOKEN` is missing, the tool prints a helpful error to STDERR and aborts, including a direct link to the expected org settings page.
|
|
612
655
|
|
|
613
656
|
## 🦷 FLOSS Funding
|
|
614
657
|
|
|
@@ -679,6 +722,12 @@ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you m
|
|
|
679
722
|
|
|
680
723
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
681
724
|
|
|
725
|
+
### Roadmap
|
|
726
|
+
|
|
727
|
+
- [ ] Template the RSpec test harness.
|
|
728
|
+
- [ ] Enhance gitlab pipeline configuration.
|
|
729
|
+
- [ ] Add focused, packaged, named, templating strategies, allowing, for example, only refreshing the Appraisals related template files.
|
|
730
|
+
|
|
682
731
|
### 🚀 Release Instructions
|
|
683
732
|
|
|
684
733
|
See [CONTRIBUTING.md][🤝contributing].
|
data/Rakefile.example
CHANGED
|
@@ -13,7 +13,7 @@ gem "rubocop-on-rbs", "~> 1.8" # ruby >= 3.1.0
|
|
|
13
13
|
gem "benchmark", "~> 0.4", ">= 0.4.1" # Removed from Std Lib in Ruby 3.5
|
|
14
14
|
|
|
15
15
|
if ENV.fetch("RUBOCOP_LTS_LOCAL", "false").casecmp("true").zero?
|
|
16
|
-
home = ENV["HOME"]
|
|
16
|
+
home = ENV["HOME"] || Dir.home
|
|
17
17
|
gem "rubocop-lts", path: "#{home}/src/rubocop-lts/rubocop-lts"
|
|
18
18
|
gem "rubocop-lts-rspec", path: "#{home}/src/rubocop-lts/rubocop-lts-rspec"
|
|
19
19
|
gem "{RUBOCOP|RUBY|GEM}", path: "#{home}/src/rubocop-lts/{RUBOCOP|RUBY|GEM}"
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "kettle/dev/prism_utils"
|
|
5
|
+
|
|
6
|
+
module Kettle
|
|
7
|
+
module Dev
|
|
8
|
+
# AST-driven merger for Appraisals files using Prism.
|
|
9
|
+
# Preserves all comments: preamble headers, block headers, and inline comments.
|
|
10
|
+
# Uses PrismUtils for shared Prism AST operations.
|
|
11
|
+
module AppraisalsAstMerger
|
|
12
|
+
TRACKED_METHODS = [:gem, :eval_gemfile, :gemfile].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Merge template and destination Appraisals files preserving comments
|
|
17
|
+
#
|
|
18
|
+
# Merges appraise blocks by name, preserving:
|
|
19
|
+
# - Preamble comments (deduplicated)
|
|
20
|
+
# - Block headers (comments before each appraise block)
|
|
21
|
+
# - Inline comments on gem/eval_gemfile statements
|
|
22
|
+
# - Leading comments before statements
|
|
23
|
+
#
|
|
24
|
+
# @param template_content [String] Template Appraisals content
|
|
25
|
+
# @param dest_content [String] Destination Appraisals content
|
|
26
|
+
# @return [String] Merged Appraisals content with preserved comments
|
|
27
|
+
# @example
|
|
28
|
+
# AppraisalsAstMerger.merge(
|
|
29
|
+
# 'appraise "rails-7" { gem "rails", "~> 7.0" }',
|
|
30
|
+
# 'appraise "custom" { gem "foo" }'
|
|
31
|
+
# )
|
|
32
|
+
def merge(template_content, dest_content)
|
|
33
|
+
template_content ||= ""
|
|
34
|
+
dest_content ||= ""
|
|
35
|
+
|
|
36
|
+
return template_content if dest_content.strip.empty?
|
|
37
|
+
return dest_content if template_content.strip.empty?
|
|
38
|
+
|
|
39
|
+
# Parse with Prism to get AST and comments
|
|
40
|
+
tmpl_result = PrismUtils.parse_with_comments(template_content)
|
|
41
|
+
dest_result = PrismUtils.parse_with_comments(dest_content)
|
|
42
|
+
|
|
43
|
+
# Extract preamble and blocks with their headers
|
|
44
|
+
tmpl_preamble, tmpl_blocks = extract_blocks(tmpl_result, template_content)
|
|
45
|
+
dest_preamble, dest_blocks = extract_blocks(dest_result, dest_content)
|
|
46
|
+
|
|
47
|
+
# Merge preambles
|
|
48
|
+
merged_preamble = merge_preambles(tmpl_preamble, dest_preamble)
|
|
49
|
+
|
|
50
|
+
# Merge blocks
|
|
51
|
+
merged_blocks = merge_blocks(tmpl_blocks, dest_blocks, tmpl_result, dest_result)
|
|
52
|
+
|
|
53
|
+
# Build output
|
|
54
|
+
build_output(merged_preamble, merged_blocks)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_blocks(parse_result, source_content)
|
|
58
|
+
root = parse_result.value
|
|
59
|
+
return [[], []] unless root&.statements&.body
|
|
60
|
+
|
|
61
|
+
source_lines = source_content.lines
|
|
62
|
+
blocks = []
|
|
63
|
+
first_appraise_line = nil
|
|
64
|
+
|
|
65
|
+
# Find all appraise blocks
|
|
66
|
+
root.statements.body.each do |node|
|
|
67
|
+
if appraise_call?(node)
|
|
68
|
+
first_appraise_line ||= node.location.start_line
|
|
69
|
+
name = extract_appraise_name(node)
|
|
70
|
+
next unless name
|
|
71
|
+
|
|
72
|
+
# Extract block header (comments immediately before this block)
|
|
73
|
+
block_header = extract_block_header(node, source_lines, blocks)
|
|
74
|
+
|
|
75
|
+
blocks << {
|
|
76
|
+
node: node,
|
|
77
|
+
name: name,
|
|
78
|
+
header: block_header,
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Preamble is all comments before first appraise block
|
|
84
|
+
preamble_comments = if first_appraise_line
|
|
85
|
+
parse_result.comments.select { |c| c.location.start_line < first_appraise_line }
|
|
86
|
+
else
|
|
87
|
+
parse_result.comments
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Filter out comments that are part of block headers
|
|
91
|
+
block_header_lines = blocks.flat_map { |b| b[:header].lines.map { |l| l.strip } }.to_set
|
|
92
|
+
preamble_comments = preamble_comments.reject { |c| block_header_lines.include?(c.slice.strip) }
|
|
93
|
+
|
|
94
|
+
[preamble_comments, blocks]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def appraise_call?(node)
|
|
98
|
+
PrismUtils.block_call_to?(node, :appraise)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def extract_appraise_name(node)
|
|
102
|
+
return unless node.is_a?(Prism::CallNode)
|
|
103
|
+
# Use PrismUtils for extracting literal values
|
|
104
|
+
PrismUtils.extract_literal_value(node.arguments&.arguments&.first)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def merge_preambles(tmpl_comments, dest_comments)
|
|
108
|
+
tmpl_lines = tmpl_comments.map { |c| c.slice.strip }
|
|
109
|
+
dest_lines = dest_comments.map { |c| c.slice.strip }
|
|
110
|
+
|
|
111
|
+
# Remove magic comments from dest if template has them
|
|
112
|
+
magic_pattern = /^#.*frozen_string_literal/
|
|
113
|
+
if tmpl_lines.any? { |line| line.match?(magic_pattern) }
|
|
114
|
+
dest_lines.reject! { |line| line.match?(magic_pattern) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Merge unique lines (case-insensitive), template first
|
|
118
|
+
merged = []
|
|
119
|
+
seen = Set.new
|
|
120
|
+
|
|
121
|
+
(tmpl_lines + dest_lines).each do |line|
|
|
122
|
+
normalized = line.downcase
|
|
123
|
+
unless seen.include?(normalized)
|
|
124
|
+
merged << line
|
|
125
|
+
seen << normalized
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
merged
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def extract_block_header(node, source_lines, previous_blocks)
|
|
133
|
+
# Get the line number where this block starts (1-indexed from Prism)
|
|
134
|
+
begin_line = node.location.start_line
|
|
135
|
+
|
|
136
|
+
# Find the end of the previous block or start of file
|
|
137
|
+
min_line = if previous_blocks.empty?
|
|
138
|
+
1
|
|
139
|
+
else
|
|
140
|
+
previous_blocks.last[:node].location.end_line + 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Convert to 0-indexed and look at the line before the block
|
|
144
|
+
check_line = begin_line - 2 # e.g., if block is on line 2 (1-indexed), check line 0 (0-indexed, which is line 1 in file)
|
|
145
|
+
|
|
146
|
+
# Look backwards from the block start to find contiguous comment lines
|
|
147
|
+
header_lines = []
|
|
148
|
+
|
|
149
|
+
while check_line >= 0 && (check_line + 1) >= min_line
|
|
150
|
+
line = source_lines[check_line]
|
|
151
|
+
break unless line
|
|
152
|
+
|
|
153
|
+
# Stop at empty line
|
|
154
|
+
if line.strip.empty?
|
|
155
|
+
break
|
|
156
|
+
elsif line.lstrip.start_with?("#")
|
|
157
|
+
header_lines.unshift(line)
|
|
158
|
+
check_line -= 1
|
|
159
|
+
else
|
|
160
|
+
# Non-comment, non-blank line - stop
|
|
161
|
+
break
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
header_lines.join
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error)
|
|
168
|
+
""
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def merge_blocks(template_blocks, dest_blocks, tmpl_result, dest_result)
|
|
172
|
+
merged = []
|
|
173
|
+
dest_by_name = dest_blocks.each_with_object({}) { |b, h| h[b[:name]] = b }
|
|
174
|
+
template_names = template_blocks.map { |b| b[:name] }.to_set
|
|
175
|
+
|
|
176
|
+
# Track which dest blocks we've already placed
|
|
177
|
+
placed_dest = Set.new
|
|
178
|
+
|
|
179
|
+
# For each template block, merge it and insert any dest-only blocks that come before it
|
|
180
|
+
template_blocks.each_with_index do |tmpl_block, idx|
|
|
181
|
+
name = tmpl_block[:name]
|
|
182
|
+
|
|
183
|
+
# Find dest-only blocks that should come before this template block
|
|
184
|
+
# (i.e., blocks that appear in dest before this shared block)
|
|
185
|
+
if idx == 0 || dest_by_name[name]
|
|
186
|
+
# For first template block or when we have a dest version of this block,
|
|
187
|
+
# check if there are dest-only blocks to insert before it
|
|
188
|
+
dest_blocks.each do |db|
|
|
189
|
+
next if template_names.include?(db[:name])
|
|
190
|
+
next if placed_dest.include?(db[:name])
|
|
191
|
+
|
|
192
|
+
# Check if this dest-only block comes before current shared block in dest
|
|
193
|
+
dest_idx_of_shared = dest_blocks.index { |b| b[:name] == name }
|
|
194
|
+
dest_idx_of_only = dest_blocks.index { |b| b[:name] == db[:name] }
|
|
195
|
+
|
|
196
|
+
if dest_idx_of_only && dest_idx_of_shared && dest_idx_of_only < dest_idx_of_shared
|
|
197
|
+
merged << db
|
|
198
|
+
placed_dest << db[:name]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Now add the template block (merged or template-only)
|
|
204
|
+
dest_block = dest_by_name[name]
|
|
205
|
+
if dest_block
|
|
206
|
+
# Merge this block
|
|
207
|
+
merged_header = merge_block_headers(tmpl_block[:header], dest_block[:header])
|
|
208
|
+
merged_statements = merge_block_statements(
|
|
209
|
+
tmpl_block[:node].block.body,
|
|
210
|
+
dest_block[:node].block.body,
|
|
211
|
+
dest_result,
|
|
212
|
+
)
|
|
213
|
+
merged << {
|
|
214
|
+
name: name,
|
|
215
|
+
header: merged_header,
|
|
216
|
+
node: tmpl_block[:node],
|
|
217
|
+
statements: merged_statements,
|
|
218
|
+
}
|
|
219
|
+
placed_dest << name
|
|
220
|
+
else
|
|
221
|
+
# Template-only block
|
|
222
|
+
merged << tmpl_block
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Add any remaining destination-only blocks that haven't been placed
|
|
227
|
+
dest_blocks.each do |dest_block|
|
|
228
|
+
next if placed_dest.include?(dest_block[:name])
|
|
229
|
+
next if template_names.include?(dest_block[:name])
|
|
230
|
+
merged << dest_block
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
merged
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def merge_block_headers(tmpl_header, dest_header)
|
|
237
|
+
tmpl_lines = tmpl_header.to_s.lines.map(&:strip).reject(&:empty?)
|
|
238
|
+
dest_lines = dest_header.to_s.lines.map(&:strip).reject(&:empty?)
|
|
239
|
+
|
|
240
|
+
# Merge without duplicates (case-insensitive comparison), template first
|
|
241
|
+
merged = []
|
|
242
|
+
seen = Set.new
|
|
243
|
+
|
|
244
|
+
(tmpl_lines + dest_lines).each do |line|
|
|
245
|
+
normalized = line.downcase
|
|
246
|
+
unless seen.include?(normalized)
|
|
247
|
+
merged << line
|
|
248
|
+
seen << normalized
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
return "" if merged.empty?
|
|
253
|
+
merged.join("\n") + "\n"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def merge_block_statements(tmpl_body, dest_body, dest_result)
|
|
257
|
+
tmpl_stmts = PrismUtils.extract_statements(tmpl_body)
|
|
258
|
+
dest_stmts = PrismUtils.extract_statements(dest_body)
|
|
259
|
+
|
|
260
|
+
# Build statement keys for both
|
|
261
|
+
tmpl_keys = Set.new
|
|
262
|
+
tmpl_key_to_node = {}
|
|
263
|
+
tmpl_stmts.each do |stmt|
|
|
264
|
+
key = statement_key(stmt)
|
|
265
|
+
if key
|
|
266
|
+
tmpl_keys << key
|
|
267
|
+
tmpl_key_to_node[key] = stmt
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
dest_keys = Set.new
|
|
272
|
+
dest_stmts.each do |stmt|
|
|
273
|
+
key = statement_key(stmt)
|
|
274
|
+
dest_keys << key if key
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Process dest statements in order, preserving their position and comments
|
|
278
|
+
merged = []
|
|
279
|
+
dest_stmts.each_with_index do |dest_stmt, idx|
|
|
280
|
+
dest_key = statement_key(dest_stmt)
|
|
281
|
+
|
|
282
|
+
if dest_key && tmpl_keys.include?(dest_key)
|
|
283
|
+
# Shared statement - use template version but preserve dest position
|
|
284
|
+
# Add it with no comments (template version is canonical)
|
|
285
|
+
merged << {node: tmpl_key_to_node[dest_key], inline_comments: [], leading_comments: [], shared: true, key: dest_key}
|
|
286
|
+
else
|
|
287
|
+
# Dest-only statement - preserve with all comments
|
|
288
|
+
inline_comments = PrismUtils.inline_comments_for_node(dest_result, dest_stmt)
|
|
289
|
+
|
|
290
|
+
prev_stmt = (idx > 0) ? dest_stmts[idx - 1] : nil
|
|
291
|
+
leading_comments = PrismUtils.find_leading_comments(dest_result, dest_stmt, prev_stmt, dest_body)
|
|
292
|
+
|
|
293
|
+
merged << {node: dest_stmt, inline_comments: inline_comments, leading_comments: leading_comments, shared: false}
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Add template-only statements (those not in dest) at the end
|
|
298
|
+
tmpl_stmts.each do |tmpl_stmt|
|
|
299
|
+
tmpl_key = statement_key(tmpl_stmt)
|
|
300
|
+
unless tmpl_key && dest_keys.include?(tmpl_key)
|
|
301
|
+
merged << {node: tmpl_stmt, inline_comments: [], leading_comments: [], shared: false}
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Clean up - remove the tracking fields
|
|
306
|
+
merged.each do |item|
|
|
307
|
+
item.delete(:shared)
|
|
308
|
+
item.delete(:key)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
merged
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def statement_key(node)
|
|
315
|
+
# Use PrismUtils for statement key generation with Appraisals-specific tracked methods
|
|
316
|
+
PrismUtils.statement_key(node, tracked_methods: TRACKED_METHODS)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def build_output(preamble_lines, blocks)
|
|
320
|
+
output = []
|
|
321
|
+
|
|
322
|
+
# Add preamble
|
|
323
|
+
preamble_lines.each { |line| output << line }
|
|
324
|
+
output << "" unless preamble_lines.empty?
|
|
325
|
+
|
|
326
|
+
# Add blocks
|
|
327
|
+
blocks.each do |block|
|
|
328
|
+
# Add block header (no blank line before it)
|
|
329
|
+
header = block[:header]
|
|
330
|
+
if header && !header.strip.empty?
|
|
331
|
+
output << header.rstrip
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Add appraise call - using parentheses and curly braces format
|
|
335
|
+
name = block[:name]
|
|
336
|
+
output << "appraise(\"#{name}\") {"
|
|
337
|
+
|
|
338
|
+
# Add statements
|
|
339
|
+
statements = block[:statements] || extract_original_statements(block[:node])
|
|
340
|
+
statements.each do |stmt_info|
|
|
341
|
+
# Add any leading comments before this statement
|
|
342
|
+
leading = stmt_info[:leading_comments] || []
|
|
343
|
+
leading.each do |comment|
|
|
344
|
+
output << " #{comment.slice.strip}"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
node = stmt_info[:node]
|
|
348
|
+
# Normalize the statement to use parentheses
|
|
349
|
+
line = normalize_statement(node)
|
|
350
|
+
|
|
351
|
+
inline = stmt_info[:inline_comments] || []
|
|
352
|
+
inline_str = inline.map { |c| c.slice.strip }.join(" ")
|
|
353
|
+
output << " #{line}#{" #{inline_str}" unless inline_str.empty?}"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
output << "}"
|
|
357
|
+
output << ""
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
output.join("\n").strip + "\n"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def normalize_statement(node)
|
|
364
|
+
# Use PrismUtils for normalizing call nodes
|
|
365
|
+
return PrismUtils.node_to_source(node) unless node.is_a?(Prism::CallNode)
|
|
366
|
+
PrismUtils.normalize_call_node(node)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def normalize_argument(arg)
|
|
370
|
+
# Use PrismUtils for argument normalization
|
|
371
|
+
PrismUtils.normalize_argument(arg)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def extract_original_statements(node)
|
|
375
|
+
body = node.block&.body
|
|
376
|
+
return [] unless body
|
|
377
|
+
|
|
378
|
+
statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
|
|
379
|
+
statements.compact.map { |stmt| {node: stmt, inline_comments: [], leading_comments: []} }
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module Kettle
|
|
4
4
|
module Dev
|
|
5
|
+
# CLI for updating CHANGELOG.md with new version sections
|
|
6
|
+
#
|
|
7
|
+
# Automatically extracts unreleased changes, formats them into a new version section,
|
|
8
|
+
# includes coverage and YARD stats, and updates link references.
|
|
5
9
|
class ChangelogCLI
|
|
6
10
|
UNRELEASED_SECTION_HEADING = "[Unreleased]:"
|
|
11
|
+
|
|
12
|
+
# Initialize the changelog CLI
|
|
13
|
+
# Sets up paths for CHANGELOG.md and coverage.json
|
|
7
14
|
def initialize
|
|
8
15
|
@root = Kettle::Dev::CIHelpers.project_root
|
|
9
16
|
@changelog_path = File.join(@root, "CHANGELOG.md")
|
|
10
17
|
@coverage_path = File.join(@root, "coverage", "coverage.json")
|
|
11
18
|
end
|
|
12
19
|
|
|
20
|
+
# Main entry point to update CHANGELOG.md
|
|
21
|
+
#
|
|
22
|
+
# Detects current version, extracts unreleased changes, formats them into
|
|
23
|
+
# a new version section with coverage/YARD stats, and updates all link references.
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
13
26
|
def run
|
|
14
27
|
version = Kettle::Dev::Versioning.detect_version(@root)
|
|
15
28
|
today = Time.now.strftime("%Y-%m-%d")
|