kettle-changelog 1.0.0 → 1.0.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 +18 -3
- data/README.md +1 -1
- data/lib/kettle/changelog/transfer_merger.rb +227 -0
- data/lib/kettle/changelog/version.rb +1 -1
- data/lib/kettle/changelog.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8737c49a9337bff6a8234bea1914fd8a054ca66a9fe1107b020cc80ef52205d2
|
|
4
|
+
data.tar.gz: e15afde1f514972d8417785a657c1a0b5c618fd3edca8a4e5be3c4f59aa7ff75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8afab816b2675e3ca97449dd50176be20d4ec15723fa4107ccb98f6a6d0c8779467b823f5f472c569d9b856ba6229217c2e5ca7b5e2d300058453a1d79182197
|
|
7
|
+
data.tar.gz: fd565677b152771afcc28db39a541c061fe58d026c3dd9f571d97cab4801dd8d7b9365678c7fc8d23f5c5340882cce55c977c69e58ff5c109e1bbb3f85f19e28
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -28,10 +28,23 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
28
28
|
|
|
29
29
|
### Fixed
|
|
30
30
|
|
|
31
|
-
- Preserve family-local dependency wiring during aggregate coverage runs.
|
|
32
|
-
|
|
33
31
|
### Security
|
|
34
32
|
|
|
33
|
+
## [1.0.1] - 2026-08-11
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.0.1][1.0.1t]
|
|
36
|
+
- COVERAGE: 93.56% -- 944/1009 lines in 5 files
|
|
37
|
+
- BRANCH COVERAGE: 77.39% -- 332/429 branches in 5 files
|
|
38
|
+
- 42.31% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Expose structural transfer-entry merging for consumers that replay kettle-jem template history without duplicating Markdown traversal.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Preserve family-local dependency wiring during aggregate coverage runs.
|
|
47
|
+
|
|
35
48
|
## [1.0.0] - 2026-08-10
|
|
36
49
|
|
|
37
50
|
- TAG: [v1.0.0][1.0.0t]
|
|
@@ -114,6 +127,8 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
114
127
|
|
|
115
128
|
- Initial release
|
|
116
129
|
|
|
117
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.
|
|
130
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.1...HEAD
|
|
131
|
+
[1.0.1]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.0...v1.0.1
|
|
132
|
+
[1.0.1t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.1
|
|
118
133
|
[1.0.0]: https://github.com/kettle-dev/kettle-changelog/compare/v0.1.0...v1.0.0
|
|
119
134
|
[1.0.0t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -395,7 +395,7 @@ Thanks for RTFM. ☺️
|
|
|
395
395
|
[📌gitmoji]: https://gitmoji.dev
|
|
396
396
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
397
397
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
398
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
398
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.009-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
399
399
|
[🔐security]: https://github.com/kettle-dev/kettle-changelog/blob/main/SECURITY.md
|
|
400
400
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
401
401
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kettle
|
|
4
|
+
module Changelog
|
|
5
|
+
class TransferMerger
|
|
6
|
+
STANDARD_HEADINGS = %w[Added Changed Deprecated Removed Fixed Security].freeze
|
|
7
|
+
|
|
8
|
+
def self.apply(content:, entries:, excluded_keys: [])
|
|
9
|
+
new(content: content, entries: entries, excluded_keys: excluded_keys).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(content:, entries:, excluded_keys: [])
|
|
13
|
+
@content = content.to_s
|
|
14
|
+
@entries = Array(entries).filter_map do |entry|
|
|
15
|
+
next unless entry.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
key = entry_value(entry, :key).to_s
|
|
18
|
+
lines = Array(entry_value(entry, :lines))
|
|
19
|
+
next if key.empty? || lines.empty?
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
key: key,
|
|
23
|
+
section: entry_value(entry, :section),
|
|
24
|
+
lines: lines
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
@excluded_keys = Array(excluded_keys).map(&:to_s).reject(&:empty?).to_set
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run
|
|
31
|
+
normalized = remove_excluded_entries(@content)
|
|
32
|
+
existing_keys = transfer_occurrences(normalized).map { |occurrence| occurrence.fetch(:key) }.to_set
|
|
33
|
+
missing = @entries.reject { |entry| existing_keys.include?(entry.fetch(:key).to_s) }
|
|
34
|
+
return ensure_trailing_newline(normalized) if missing.empty?
|
|
35
|
+
|
|
36
|
+
lines = source_lines(normalized)
|
|
37
|
+
unreleased = unreleased_line_index(lines)
|
|
38
|
+
return ensure_trailing_newline(normalized) unless unreleased
|
|
39
|
+
|
|
40
|
+
destination_end = unreleased_end_index(lines, unreleased)
|
|
41
|
+
destination_body = lines[(unreleased + 1)...destination_end] || []
|
|
42
|
+
items = changelog_items(destination_body)
|
|
43
|
+
missing.group_by { |entry| entry.fetch(:section, "### Changed") }.each do |section, section_entries|
|
|
44
|
+
section = STANDARD_HEADINGS.include?(section.to_s.delete_prefix("### ")) ? "### #{section.to_s.delete_prefix("### ").strip}" : "### Changed"
|
|
45
|
+
items[section] ||= []
|
|
46
|
+
items[section].pop while items[section].any? && items[section].last.to_s.strip.empty?
|
|
47
|
+
items[section] << "" if items[section].any?
|
|
48
|
+
section_entries.each do |entry|
|
|
49
|
+
items[section].concat(Array(entry.fetch(:lines)).map(&:rstrip))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
merged_lines = lines[0...unreleased] +
|
|
54
|
+
build_unreleased_section(lines.fetch(unreleased), items) +
|
|
55
|
+
lines[destination_end..].to_a
|
|
56
|
+
ensure_trailing_newline(merged_lines.join("\n").gsub(/\n{3,}/, "\n\n"))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def remove_excluded_entries(content)
|
|
62
|
+
return content if @excluded_keys.empty?
|
|
63
|
+
|
|
64
|
+
lines = source_lines(content)
|
|
65
|
+
removals = transfer_occurrences(content).filter_map do |occurrence|
|
|
66
|
+
next unless @excluded_keys.include?(occurrence.fetch(:key).to_s)
|
|
67
|
+
|
|
68
|
+
(occurrence.fetch(:start_line)..occurrence.fetch(:end_line))
|
|
69
|
+
end
|
|
70
|
+
return content if removals.empty?
|
|
71
|
+
|
|
72
|
+
lines.each_with_index.reject do |_line, index|
|
|
73
|
+
removals.any? { |range| range.cover?(index + 1) }
|
|
74
|
+
end.map(&:first).join("\n").then { |text| ensure_trailing_newline(text.gsub(/\n{3,}/, "\n\n")) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def transfer_occurrences(content)
|
|
78
|
+
context = markdown_context(content)
|
|
79
|
+
release_sections = context.structural_owners(owner_scope: :heading_sections)
|
|
80
|
+
.select { |owner| owner.level == 2 }
|
|
81
|
+
.sort_by { |owner| owner.location.start_line }
|
|
82
|
+
list_items = context.structural_owners(owner_scope: :list_items)
|
|
83
|
+
list_items.filter_map do |item|
|
|
84
|
+
next unless item.depth == 1
|
|
85
|
+
|
|
86
|
+
key = transfer_key(item.source)
|
|
87
|
+
next unless key
|
|
88
|
+
|
|
89
|
+
release = release_section_for(release_sections, item)
|
|
90
|
+
next unless release
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
key: key,
|
|
94
|
+
release_heading: "## #{release.heading_text}",
|
|
95
|
+
start_line: item.location.start_line,
|
|
96
|
+
end_line: item.location.end_line
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def transfer_key(source)
|
|
102
|
+
source.to_s.lines.first.to_s.strip[/\A[-*] +((?:kettle-jem-template)-\d{8}-\d{3})\b/, 1]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def release_section_for(sections, item)
|
|
106
|
+
sections.each_with_index.find do |section, index|
|
|
107
|
+
next_section = sections[index + 1]
|
|
108
|
+
section_end_line = next_section ? next_section.location.start_line - 1 : section.location.end_line
|
|
109
|
+
item.location.start_line > section.location.start_line && item.location.end_line <= section_end_line
|
|
110
|
+
end&.first
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def changelog_items(body_lines)
|
|
114
|
+
items = {}
|
|
115
|
+
heading = nil
|
|
116
|
+
index = 0
|
|
117
|
+
while index < body_lines.length
|
|
118
|
+
line = body_lines.fetch(index)
|
|
119
|
+
if line.start_with?("### ")
|
|
120
|
+
heading = line.strip
|
|
121
|
+
items[heading] ||= []
|
|
122
|
+
index += 1
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if bullet_line?(line)
|
|
127
|
+
collected, index = collect_list_item(body_lines, index)
|
|
128
|
+
items[heading] ||= []
|
|
129
|
+
items[heading].concat(collected)
|
|
130
|
+
next
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
index += 1
|
|
134
|
+
end
|
|
135
|
+
items
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_unreleased_section(heading, items)
|
|
139
|
+
lines = [heading]
|
|
140
|
+
STANDARD_HEADINGS.each do |standard_heading|
|
|
141
|
+
heading_line = "### #{standard_heading}"
|
|
142
|
+
lines << ""
|
|
143
|
+
lines << heading_line
|
|
144
|
+
lines << ""
|
|
145
|
+
section_items = items[heading_line].to_a.dup
|
|
146
|
+
section_items.pop while section_items.any? && section_items.last.to_s.strip.empty?
|
|
147
|
+
lines.concat(section_items) if section_items.any?
|
|
148
|
+
end
|
|
149
|
+
lines << ""
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def source_lines(content)
|
|
153
|
+
content.to_s.split("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def unreleased_line_index(lines)
|
|
157
|
+
lines.index { |line| line.to_s.strip == "## [Unreleased]" }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def unreleased_end_index(lines, unreleased_index)
|
|
161
|
+
index = unreleased_index + 1
|
|
162
|
+
while index < lines.length
|
|
163
|
+
line = lines.fetch(index)
|
|
164
|
+
return index if link_reference_line?(line)
|
|
165
|
+
return index if line.start_with?("# ") || (line.start_with?("## ") && line.to_s.strip != "## [Unreleased]")
|
|
166
|
+
|
|
167
|
+
index += 1
|
|
168
|
+
end
|
|
169
|
+
lines.length
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def link_reference_line?(line)
|
|
173
|
+
line.to_s.lstrip.start_with?("[") && line.to_s.include?("]:")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def entry_value(entry, key)
|
|
177
|
+
entry[key] || entry[key.to_s]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def bullet_line?(line)
|
|
181
|
+
line.to_s.lstrip.start_with?("- ", "* ")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def collect_list_item(lines, start_index)
|
|
185
|
+
line = lines.fetch(start_index).to_s
|
|
186
|
+
base_indent = line.length - line.lstrip.length
|
|
187
|
+
item_lines = [line.rstrip]
|
|
188
|
+
index = start_index + 1
|
|
189
|
+
in_fence = false
|
|
190
|
+
while index < lines.length
|
|
191
|
+
current = lines.fetch(index).to_s
|
|
192
|
+
current_indent = current.length - current.lstrip.length
|
|
193
|
+
break if !in_fence && heading_line?(current)
|
|
194
|
+
break if !in_fence && bullet_line?(current) && current_indent <= base_indent
|
|
195
|
+
|
|
196
|
+
if current.lstrip.start_with?("```")
|
|
197
|
+
in_fence = !in_fence
|
|
198
|
+
item_lines << current.rstrip
|
|
199
|
+
index += 1
|
|
200
|
+
next
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
break unless in_fence || current.strip.empty? || current_indent > base_indent
|
|
204
|
+
|
|
205
|
+
item_lines << current.rstrip
|
|
206
|
+
index += 1
|
|
207
|
+
end
|
|
208
|
+
[item_lines, index]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def heading_line?(line)
|
|
212
|
+
line.to_s.lstrip.start_with?("#")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def markdown_context(content)
|
|
216
|
+
require "ast/crispr/markdown/markly"
|
|
217
|
+
Ast::Crispr::Markdown::Markly.document_context(content: content, source_label: "CHANGELOG.md")
|
|
218
|
+
rescue LoadError => error
|
|
219
|
+
raise Error, "kettle-changelog transfer merging requires ast-crispr-markdown-markly (#{error.message})"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def ensure_trailing_newline(content)
|
|
223
|
+
"#{content.to_s.rstrip}\n"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -5,7 +5,7 @@ module Kettle
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = "1.0.
|
|
8
|
+
VERSION = "1.0.1"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/kettle/changelog.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-changelog
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -295,6 +295,7 @@ files:
|
|
|
295
295
|
- lib/kettle/changelog.rb
|
|
296
296
|
- lib/kettle/changelog/cli.rb
|
|
297
297
|
- lib/kettle/changelog/entry_adder.rb
|
|
298
|
+
- lib/kettle/changelog/transfer_merger.rb
|
|
298
299
|
- lib/kettle/changelog/version.rb
|
|
299
300
|
- sig/kettle/changelog.rbs
|
|
300
301
|
homepage: https://github.com/kettle-dev/kettle-changelog
|
|
@@ -302,10 +303,10 @@ licenses:
|
|
|
302
303
|
- AGPL-3.0-only
|
|
303
304
|
metadata:
|
|
304
305
|
homepage_uri: https://kettle-changelog.galtzo.com
|
|
305
|
-
source_code_uri: https://github.com/kettle-dev/kettle-changelog/tree/v1.0.
|
|
306
|
-
changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.
|
|
306
|
+
source_code_uri: https://github.com/kettle-dev/kettle-changelog/tree/v1.0.1
|
|
307
|
+
changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.1/CHANGELOG.md
|
|
307
308
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-changelog/issues
|
|
308
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-changelog/1.0.
|
|
309
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-changelog/1.0.1
|
|
309
310
|
funding_uri: https://github.com/sponsors/pboling
|
|
310
311
|
wiki_uri: https://github.com/kettle-dev/kettle-changelog/wiki
|
|
311
312
|
news_uri: https://www.railsbling.com/tags/kettle-changelog
|
metadata.gz.sig
CHANGED
|
Binary file
|