ratatui_ruby-devtools 0.2.1 → 0.4.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
- data/.rubocop.yml +1 -1
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +69 -7
- data/README.md +62 -21
- data/Rakefile +1 -1
- data/bin/setup +20 -6
- data/bin/setup.ps1 +74 -0
- data/doc/contributors/documentation_style.md +3 -4
- data/exe/scaffold +8 -342
- data/lib/ratatui_ruby/devtools/scaffold/scaffold_config.rb +67 -0
- data/lib/ratatui_ruby/devtools/scaffold/template_renderer.rb +282 -0
- data/lib/ratatui_ruby/devtools/task_scope.rb +24 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc_tasks.rb +31 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/bump_workflow.rb +71 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/changelog.rb +60 -3
- data/lib/ratatui_ruby/devtools/tasks/bump/github_project.rb +26 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/history.rb +3 -3
- data/lib/ratatui_ruby/devtools/tasks/bump/links.rb +39 -24
- data/lib/ratatui_ruby/devtools/tasks/bump/manifest.rb +9 -17
- data/lib/ratatui_ruby/devtools/tasks/bump/no_crate.rb +19 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/no_links.rb +22 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/patch_release.rb +25 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/release_branch.rb +17 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/release_from_trunk.rb +48 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/repository.rb +143 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/ruby_gem.rb +65 -59
- data/lib/ratatui_ruby/devtools/tasks/bump/rust_crate.rb +42 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/sem_ver.rb +127 -10
- data/lib/ratatui_ruby/devtools/tasks/bump/source_hut_project.rb +26 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unbranched_release.rb +20 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unreleased_section.rb +19 -1
- data/lib/ratatui_ruby/devtools/tasks/bump_tasks.rb +47 -0
- data/lib/ratatui_ruby/devtools/tasks/cargo_tasks.rb +57 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/documentation.rb +59 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link/file_url.rb +30 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link/relative_path.rb +61 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link/web_url.rb +55 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link.rb +52 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/link_audit.rb +116 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/problem.rb +40 -0
- data/lib/ratatui_ruby/devtools/tasks/doc/source_file.rb +93 -0
- data/lib/ratatui_ruby/devtools/tasks/doc_tasks.rb +937 -0
- data/lib/ratatui_ruby/devtools/tasks/license/blame.rb +78 -0
- data/lib/ratatui_ruby/devtools/tasks/license/changed_files.rb +34 -0
- data/lib/ratatui_ruby/devtools/tasks/license/contribution.rb +12 -0
- data/lib/ratatui_ruby/devtools/tasks/license/copyright.rb +47 -0
- data/lib/ratatui_ruby/devtools/tasks/license/directory_licensing.rb +42 -0
- data/lib/ratatui_ruby/devtools/tasks/license/existing_header.rb +21 -0
- data/lib/ratatui_ruby/devtools/tasks/license/header_revision.rb +45 -0
- data/lib/ratatui_ruby/devtools/tasks/license/licensed_file.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/license/licensing.rb +35 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_document.rb +23 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_files.rb +31 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_header_syntax.rb +104 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_snippet_syntax.rb +159 -0
- data/lib/ratatui_ruby/devtools/tasks/license/missing_header.rb +18 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rbs_files.rb +27 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rbs_header_syntax.rb +33 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_comment.rb +51 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_examples.rb +41 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_snippet_syntax.rb +119 -0
- data/lib/ratatui_ruby/devtools/tasks/license/renewal.rb +17 -0
- data/lib/ratatui_ruby/devtools/tasks/license/reuse_annotated_file.rb +12 -0
- data/lib/ratatui_ruby/devtools/tasks/license/reuse_tool.rb +34 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_comment_header.rb +57 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_files.rb +36 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_header_syntax.rb +47 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rust_files.rb +27 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rust_header_syntax.rb +68 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet.rb +44 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_file.rb +29 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_opening.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_revision.rb +75 -0
- data/lib/ratatui_ruby/devtools/tasks/license/spdx_header.rb +42 -0
- data/lib/ratatui_ruby/devtools/tasks/license/unknown_file.rb +12 -0
- data/lib/ratatui_ruby/devtools/tasks/license/unlicensed_files.rb +69 -0
- data/lib/ratatui_ruby/devtools/tasks/license/wrapped_snippet.rb +30 -0
- data/lib/ratatui_ruby/devtools/tasks/license_tasks.rb +71 -0
- data/lib/ratatui_ruby/devtools/tasks/lint/gem_lockfile.rb +29 -0
- data/lib/ratatui_ruby/devtools/tasks/lint/supported_rubies.rb +18 -0
- data/lib/ratatui_ruby/devtools/tasks/lint_tasks.rb +100 -0
- data/lib/ratatui_ruby/devtools/tasks/release/platform_gem.rb +21 -12
- data/lib/ratatui_ruby/devtools/tasks/release/versioned_binary.rb +1 -1
- data/lib/ratatui_ruby/devtools/tasks/release_tasks.rb +80 -0
- data/lib/ratatui_ruby/devtools/tasks/reuse_tasks.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/{test.rake → test_tasks.rb} +13 -4
- data/lib/ratatui_ruby/devtools/tasks/website/hosted_source_tree.rb +24 -0
- data/lib/ratatui_ruby/devtools/tasks/website/index_page.rb +1 -4
- data/lib/ratatui_ruby/devtools/tasks/website/source_tree.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/website/versioned_documentation.rb +14 -13
- data/lib/ratatui_ruby/devtools/tasks/website_tasks.rb +49 -0
- data/lib/ratatui_ruby/devtools/templates/.github/workflows/build-gems.yml.erb +15 -9
- data/lib/ratatui_ruby/devtools/templates/.github/workflows/ci.yml.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +2 -1
- data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/CHANGELOG.md.erb +3 -1
- data/lib/ratatui_ruby/devtools/templates/CONTRIBUTING.md.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +2 -3
- data/lib/ratatui_ruby/devtools/templates/README.md.erb +7 -7
- data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +5 -1
- data/lib/ratatui_ruby/devtools/templates/bin/setup.ps1.erb +5 -1
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_architecture.md.erb +2 -2
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_testing.md.erb +3 -3
- data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +4 -4
- data/lib/ratatui_ruby/devtools/templates/ext/Cargo.toml.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/gemspec.erb +1 -1
- data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +3 -3
- data/lib/ratatui_ruby/devtools/version.rb +1 -1
- data/lib/ratatui_ruby/devtools.rb +54 -71
- data/mise.toml +2 -2
- data/sig/ratatui_ruby/devtools.rbs +16 -1
- metadata +93 -34
- data/.builds/ruby-4.0.yml +0 -38
- data/lib/ratatui_ruby/devtools/tasks/autodoc.rake +0 -21
- data/lib/ratatui_ruby/devtools/tasks/bump.rake +0 -80
- data/lib/ratatui_ruby/devtools/tasks/cargo.rake +0 -47
- data/lib/ratatui_ruby/devtools/tasks/doc.rake +0 -887
- data/lib/ratatui_ruby/devtools/tasks/license/headers_md.rb +0 -276
- data/lib/ratatui_ruby/devtools/tasks/license/headers_rb.rb +0 -236
- data/lib/ratatui_ruby/devtools/tasks/license/license_utils.rb +0 -143
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_md.rb +0 -353
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_rdoc.rb +0 -186
- data/lib/ratatui_ruby/devtools/tasks/license.rake +0 -91
- data/lib/ratatui_ruby/devtools/tasks/lint.rake +0 -80
- data/lib/ratatui_ruby/devtools/tasks/release.rake +0 -87
- data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +0 -7
- data/lib/ratatui_ruby/devtools/tasks/reuse.rake +0 -104
- data/lib/ratatui_ruby/devtools/tasks/website.rake +0 -37
- data/lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb +0 -47
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +0 -45
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +0 -8
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
# Ensures markdown files have correct SPDX headers.
|
|
9
|
-
#
|
|
10
|
-
# Open source projects need license headers in every file. REUSE compliance
|
|
11
|
-
# requires SPDX format. Adding and updating these headers by hand is tedious.
|
|
12
|
-
# Years drift. Contributors are forgotten.
|
|
13
|
-
#
|
|
14
|
-
# This script processes markdown files. It adds CC-BY-SA-4.0 headers where
|
|
15
|
-
# missing. It updates copyright years based on git history. It preserves
|
|
16
|
-
# existing contributors.
|
|
17
|
-
#
|
|
18
|
-
# Run it as part of license:headers:md rake task.
|
|
19
|
-
#
|
|
20
|
-
# === Example
|
|
21
|
-
#
|
|
22
|
-
# ruby tasks/license/headers_md.rb README.md
|
|
23
|
-
# ruby tasks/license/headers_md.rb doc/
|
|
24
|
-
# ruby tasks/license/headers_md.rb # all .md files
|
|
25
|
-
#
|
|
26
|
-
# Rules:
|
|
27
|
-
# - Ensures file has CC-BY-SA-4.0 license header with YOUR copyright
|
|
28
|
-
# - Updates years for EXISTING contributors based on git blame + Co-Authored-By
|
|
29
|
-
# - Does NOT add new contributors from git history - only updates existing ones
|
|
30
|
-
# - Uses non-code-block lines for year calculation
|
|
31
|
-
# - Adds header with YOUR copyright if missing
|
|
32
|
-
|
|
33
|
-
require_relative "license_utils"
|
|
34
|
-
|
|
35
|
-
# Your name for copyright headers.
|
|
36
|
-
YOUR_NAME = "Kerrick Long"
|
|
37
|
-
|
|
38
|
-
# Your email for copyright headers.
|
|
39
|
-
YOUR_EMAIL = "me@kerricklong.com"
|
|
40
|
-
|
|
41
|
-
# Identifiers used to match your contributions in git history.
|
|
42
|
-
YOUR_IDENTIFIERS = [YOUR_NAME, YOUR_EMAIL].freeze
|
|
43
|
-
|
|
44
|
-
# Full copyright string for headers.
|
|
45
|
-
YOUR_COPYRIGHT = "#{YOUR_NAME} <#{YOUR_EMAIL}>"
|
|
46
|
-
|
|
47
|
-
# The SPDX license identifier for markdown documentation.
|
|
48
|
-
LICENSE = "CC-BY-SA-4.0"
|
|
49
|
-
|
|
50
|
-
# Identifies fenced code blocks in markdown content.
|
|
51
|
-
#
|
|
52
|
-
# Copyright years come from git blame. Code blocks contain pasted content,
|
|
53
|
-
# not original prose. Blaming code block lines produces wrong contributors.
|
|
54
|
-
# Exclude these ranges when calculating copyright years.
|
|
55
|
-
#
|
|
56
|
-
# [lines] Array of line strings from the file.
|
|
57
|
-
def find_code_blocks(lines)
|
|
58
|
-
blocks = []
|
|
59
|
-
i = 0
|
|
60
|
-
|
|
61
|
-
while i < lines.length
|
|
62
|
-
line = lines[i]
|
|
63
|
-
|
|
64
|
-
if line =~ /^(````*)(\w*)$/
|
|
65
|
-
fence_marker = $1
|
|
66
|
-
fence_start = i
|
|
67
|
-
re_end = /^#{Regexp.escape(fence_marker)}$/
|
|
68
|
-
|
|
69
|
-
j = i + 1
|
|
70
|
-
while j < lines.length
|
|
71
|
-
if lines[j] =~ re_end
|
|
72
|
-
blocks << { start: fence_start, end: j }
|
|
73
|
-
i = j
|
|
74
|
-
break
|
|
75
|
-
end
|
|
76
|
-
j += 1
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
i += 1
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
blocks
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Calculates line ranges outside code blocks.
|
|
87
|
-
#
|
|
88
|
-
# Copyright years come from git blame. Blaming the entire file includes code
|
|
89
|
-
# blocks. This function returns only prose ranges for accurate year lookup.
|
|
90
|
-
#
|
|
91
|
-
# [lines] Array of line strings from the file.
|
|
92
|
-
def get_non_code_line_ranges(lines)
|
|
93
|
-
header_end = 0
|
|
94
|
-
if lines[0]&.include?("<!--")
|
|
95
|
-
(0...(lines.length)).each do |i|
|
|
96
|
-
if lines[i].include?("-->")
|
|
97
|
-
header_end = i + 1
|
|
98
|
-
break
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
code_blocks = find_code_blocks(lines)
|
|
104
|
-
non_code_ranges = []
|
|
105
|
-
current_line = header_end
|
|
106
|
-
|
|
107
|
-
code_blocks.each do |block|
|
|
108
|
-
if current_line < block[:start]
|
|
109
|
-
non_code_ranges << [current_line + 1, block[:start]]
|
|
110
|
-
end
|
|
111
|
-
current_line = block[:end] + 1
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
if current_line < lines.length
|
|
115
|
-
non_code_ranges << [current_line + 1, lines.length]
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
non_code_ranges
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Extracts existing SPDX header from markdown content.
|
|
122
|
-
#
|
|
123
|
-
# Files may already have headers. Updating requires parsing existing copyright
|
|
124
|
-
# holders and years. This extracts them for comparison and update.
|
|
125
|
-
#
|
|
126
|
-
# [lines] Array of line strings from the file.
|
|
127
|
-
def parse_existing_header(lines)
|
|
128
|
-
return nil unless lines[0]&.include?("<!--")
|
|
129
|
-
|
|
130
|
-
header_end = nil
|
|
131
|
-
copyrights = []
|
|
132
|
-
license = nil
|
|
133
|
-
|
|
134
|
-
(0...(lines.length)).each do |i|
|
|
135
|
-
line = lines[i]
|
|
136
|
-
|
|
137
|
-
if line =~ /SPDX-FileCopyrightText:\s*(\d{4})\s+(.+)$/
|
|
138
|
-
copyrights << { year: $1.to_i, holder: $2.strip }
|
|
139
|
-
# REUSE-IgnoreStart
|
|
140
|
-
elsif line =~ /SPDX-License-Identifier:\s*(.+)$/
|
|
141
|
-
# REUSE-IgnoreEnd
|
|
142
|
-
license = $1.strip
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
if line.include?("-->")
|
|
146
|
-
header_end = i
|
|
147
|
-
break
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
return nil if header_end.nil?
|
|
152
|
-
return nil if copyrights.empty? && license.nil?
|
|
153
|
-
|
|
154
|
-
{ end_line: header_end, copyrights:, license: }
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Updates or adds SPDX headers for a single markdown file.
|
|
158
|
-
#
|
|
159
|
-
# Each file needs correct CC-BY-SA-4.0 headers with accurate copyright years.
|
|
160
|
-
# Processing involves reading, parsing, querying git, and rewriting. This
|
|
161
|
-
# function orchestrates that workflow.
|
|
162
|
-
#
|
|
163
|
-
# [filepath] Path to the markdown file.
|
|
164
|
-
def process_file(filepath)
|
|
165
|
-
content = File.read(filepath)
|
|
166
|
-
lines = content.lines
|
|
167
|
-
|
|
168
|
-
non_code_ranges = get_non_code_line_ranges(lines)
|
|
169
|
-
|
|
170
|
-
# Get contributors from non-code lines for year lookups
|
|
171
|
-
all_contributors = {}
|
|
172
|
-
non_code_ranges.each do |start_line, end_line|
|
|
173
|
-
range_contributors = LicenseUtils.get_contributors_for_lines(filepath, start_line, end_line)
|
|
174
|
-
range_contributors.each do |contributor, year|
|
|
175
|
-
all_contributors[contributor] = [all_contributors[contributor] || 0, year].max
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
your_year = nil
|
|
180
|
-
all_contributors.each do |contributor, year|
|
|
181
|
-
if YOUR_IDENTIFIERS.any? { |id| contributor.include?(id) }
|
|
182
|
-
your_year = [your_year || 0, year].max
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
your_year ||= Date.today.year
|
|
186
|
-
|
|
187
|
-
existing = parse_existing_header(lines)
|
|
188
|
-
|
|
189
|
-
if existing
|
|
190
|
-
# Only update years for EXISTING contributors
|
|
191
|
-
needs_update = false
|
|
192
|
-
updated_copyrights = []
|
|
193
|
-
|
|
194
|
-
existing[:copyrights].each do |c|
|
|
195
|
-
git_year = nil
|
|
196
|
-
all_contributors.each do |contributor, year|
|
|
197
|
-
if c[:holder].split.any? { |word| contributor.include?(word) }
|
|
198
|
-
git_year = [git_year || 0, year].max
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
if git_year && git_year != c[:year]
|
|
203
|
-
puts " Updated #{c[:holder].split.first}'s copyright year: #{c[:year]} -> #{git_year}"
|
|
204
|
-
updated_copyrights << { year: git_year, holder: c[:holder] }
|
|
205
|
-
needs_update = true
|
|
206
|
-
else
|
|
207
|
-
updated_copyrights << c
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Check if YOUR year needs updating
|
|
212
|
-
your_existing = updated_copyrights.find { |c| YOUR_IDENTIFIERS.any? { |id| c[:holder].include?(id) } }
|
|
213
|
-
if your_existing.nil?
|
|
214
|
-
puts " Adding your copyright"
|
|
215
|
-
updated_copyrights << { year: your_year, holder: YOUR_COPYRIGHT }
|
|
216
|
-
needs_update = true
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
if existing[:license] != LICENSE
|
|
220
|
-
puts " Fixing license: #{existing[:license]} -> #{LICENSE}"
|
|
221
|
-
needs_update = true
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
if needs_update
|
|
225
|
-
# REUSE-IgnoreStart
|
|
226
|
-
header_lines = ["<!--\n"]
|
|
227
|
-
updated_copyrights.each do |c|
|
|
228
|
-
header_lines << " SPDX-FileCopyrightText: #{c[:year]} #{c[:holder]}\n"
|
|
229
|
-
end
|
|
230
|
-
header_lines << " SPDX-License-Identifier: #{LICENSE}\n"
|
|
231
|
-
header_lines << "-->\n"
|
|
232
|
-
# REUSE-IgnoreEnd
|
|
233
|
-
|
|
234
|
-
remaining = lines[(existing[:end_line] + 1)..]
|
|
235
|
-
File.write(filepath, header_lines.join + remaining.join)
|
|
236
|
-
puts "Updated: #{filepath}"
|
|
237
|
-
end
|
|
238
|
-
else
|
|
239
|
-
# No header - add one with YOUR copyright only
|
|
240
|
-
# REUSE-IgnoreStart
|
|
241
|
-
header = "<!--\n SPDX-FileCopyrightText: #{your_year} #{YOUR_COPYRIGHT}\n SPDX-License-Identifier: #{LICENSE}\n-->\n"
|
|
242
|
-
# REUSE-IgnoreEnd
|
|
243
|
-
|
|
244
|
-
File.write(filepath, header + content)
|
|
245
|
-
puts "Added header: #{filepath}"
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Finds markdown files to process.
|
|
250
|
-
#
|
|
251
|
-
# License automation runs on file sets. Users may specify paths or want all
|
|
252
|
-
# files. This handles both cases using git ls-files for tracking.
|
|
253
|
-
#
|
|
254
|
-
# [paths] Explicit paths to process, or empty for all tracked .md files.
|
|
255
|
-
def find_md_files(paths)
|
|
256
|
-
if paths.empty?
|
|
257
|
-
`git ls-files '*.md'`.split("\n")
|
|
258
|
-
else
|
|
259
|
-
paths.flat_map do |path|
|
|
260
|
-
if File.directory?(path)
|
|
261
|
-
`git ls-files '#{path}/**/*.md'`.split("\n")
|
|
262
|
-
else
|
|
263
|
-
path
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
if __FILE__ == $0
|
|
270
|
-
paths = ARGV.empty? ? [] : ARGV
|
|
271
|
-
files = find_md_files(paths)
|
|
272
|
-
|
|
273
|
-
files.each do |file|
|
|
274
|
-
process_file(file)
|
|
275
|
-
end
|
|
276
|
-
end
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
# Script to ensure Ruby files have correct SPDX file headers.
|
|
9
|
-
#
|
|
10
|
-
# Usage: ruby tasks/license/headers_rb.rb [path...]
|
|
11
|
-
#
|
|
12
|
-
# If no paths are given, processes lib/, ext/, test/, examples/, tasks/, bin/.
|
|
13
|
-
#
|
|
14
|
-
# License selection by directory:
|
|
15
|
-
# - lib/, ext/, test/ → LGPL-3.0-or-later
|
|
16
|
-
# - examples/widget_*, examples/verify_* → MIT-0
|
|
17
|
-
# - examples/app_*, tasks/, bin/ → AGPL-3.0-or-later
|
|
18
|
-
|
|
19
|
-
require_relative "license_utils"
|
|
20
|
-
|
|
21
|
-
YOUR_NAME = "Kerrick Long"
|
|
22
|
-
YOUR_EMAIL = "me@kerricklong.com"
|
|
23
|
-
YOUR_IDENTIFIERS = [YOUR_NAME, YOUR_EMAIL].freeze
|
|
24
|
-
YOUR_COPYRIGHT = "#{YOUR_NAME} <#{YOUR_EMAIL}>"
|
|
25
|
-
|
|
26
|
-
# Selects the appropriate license based on file location.
|
|
27
|
-
#
|
|
28
|
-
# Different parts of the codebase have different licenses. Library code is
|
|
29
|
-
# LGPL. Examples are MIT-0 or AGPL. This function routes files to their
|
|
30
|
-
# correct license by path pattern.
|
|
31
|
-
#
|
|
32
|
-
# [filepath] Path to the Ruby file.
|
|
33
|
-
def license_for_file(filepath)
|
|
34
|
-
case filepath
|
|
35
|
-
when %r{^(lib|sig/ratatui_ruby|ext|test)/}
|
|
36
|
-
"LGPL-3.0-or-later"
|
|
37
|
-
when %r{^(examples|sig/examples)/(widget_|verify_)}
|
|
38
|
-
"MIT-0"
|
|
39
|
-
else
|
|
40
|
-
"AGPL-3.0-or-later"
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Extracts existing SPDX header from Ruby file content.
|
|
45
|
-
#
|
|
46
|
-
# Files may already have headers. Updating requires parsing existing copyright
|
|
47
|
-
# holders and years. This extracts them for comparison and update.
|
|
48
|
-
#
|
|
49
|
-
# [lines] Array of line strings from the file.
|
|
50
|
-
def parse_existing_header(lines)
|
|
51
|
-
# Returns { end_line:, copyrights: [{year:, holder:}], license: }
|
|
52
|
-
# REUSE-IgnoreStart
|
|
53
|
-
# Ruby files typically have:
|
|
54
|
-
# # frozen_string_literal: true
|
|
55
|
-
# (blank line)
|
|
56
|
-
# #--
|
|
57
|
-
# # SPDX-FileCopyrightText: YYYY Name
|
|
58
|
-
# # SPDX-License-Identifier: LICENSE
|
|
59
|
-
# #++
|
|
60
|
-
# REUSE-IgnoreEnd
|
|
61
|
-
|
|
62
|
-
copyrights = []
|
|
63
|
-
license = nil
|
|
64
|
-
header_end = nil
|
|
65
|
-
found_spdx = false
|
|
66
|
-
|
|
67
|
-
lines.each_with_index do |line, i|
|
|
68
|
-
if line =~ /^#\s*SPDX-FileCopyrightText:\s*(\d{4})\s+(.+)$/
|
|
69
|
-
copyrights << { year: $1.to_i, holder: $2.strip }
|
|
70
|
-
found_spdx = true
|
|
71
|
-
# REUSE-IgnoreStart
|
|
72
|
-
elsif line =~ /^#\s*SPDX-License-Identifier:\s*(.+)$/
|
|
73
|
-
# REUSE-IgnoreEnd
|
|
74
|
-
license = $1.strip
|
|
75
|
-
found_spdx = true
|
|
76
|
-
elsif line =~ /^#\+\+\s*$/ && found_spdx
|
|
77
|
-
header_end = i
|
|
78
|
-
break
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
return nil if copyrights.empty? && license.nil?
|
|
83
|
-
|
|
84
|
-
{ end_line: header_end || 0, copyrights:, license: }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Updates or adds SPDX headers for a single Ruby file.
|
|
88
|
-
#
|
|
89
|
-
# Each file needs correct license headers with accurate copyright years.
|
|
90
|
-
# Processing involves reading, parsing, querying git, and rewriting. This
|
|
91
|
-
# function orchestrates that workflow.
|
|
92
|
-
#
|
|
93
|
-
# [filepath] Path to the Ruby file.
|
|
94
|
-
def process_file(filepath)
|
|
95
|
-
content = File.read(filepath)
|
|
96
|
-
lines = content.lines
|
|
97
|
-
|
|
98
|
-
target_license = license_for_file(filepath)
|
|
99
|
-
|
|
100
|
-
# Get contributors from git for year lookups
|
|
101
|
-
all_contributors = LicenseUtils.get_contributors_for_lines(filepath)
|
|
102
|
-
your_year = LicenseUtils.get_your_latest_year(filepath, YOUR_IDENTIFIERS)
|
|
103
|
-
|
|
104
|
-
existing = parse_existing_header(lines)
|
|
105
|
-
|
|
106
|
-
if existing
|
|
107
|
-
# File has existing header - only update years for EXISTING contributors
|
|
108
|
-
needs_update = false
|
|
109
|
-
updated_copyrights = []
|
|
110
|
-
|
|
111
|
-
existing[:copyrights].each do |c|
|
|
112
|
-
# Find this contributor's latest year from git
|
|
113
|
-
git_year = nil
|
|
114
|
-
all_contributors.each do |contributor, year|
|
|
115
|
-
if c[:holder].split.any? { |word| contributor.include?(word) }
|
|
116
|
-
git_year = [git_year || 0, year].max
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
if git_year && git_year != c[:year]
|
|
121
|
-
puts " Updated #{c[:holder].split.first}'s copyright year: #{c[:year]} -> #{git_year}"
|
|
122
|
-
updated_copyrights << { year: git_year, holder: c[:holder] }
|
|
123
|
-
needs_update = true
|
|
124
|
-
else
|
|
125
|
-
updated_copyrights << c
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Check if YOUR year needs updating (if you're a contributor)
|
|
130
|
-
your_existing = updated_copyrights.find { |c| YOUR_IDENTIFIERS.any? { |id| c[:holder].include?(id) } }
|
|
131
|
-
if your_existing.nil?
|
|
132
|
-
puts " Adding your copyright"
|
|
133
|
-
updated_copyrights << { year: your_year, holder: YOUR_COPYRIGHT }
|
|
134
|
-
needs_update = true
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Check license
|
|
138
|
-
if existing[:license] != target_license
|
|
139
|
-
puts " Fixing license: #{existing[:license]} -> #{target_license}"
|
|
140
|
-
needs_update = true
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
if needs_update
|
|
144
|
-
frozen_string = lines[0].include?("frozen_string_literal") ? lines[0] : nil
|
|
145
|
-
|
|
146
|
-
header_lines = []
|
|
147
|
-
header_lines << "# frozen_string_literal: true\n" unless frozen_string
|
|
148
|
-
header_lines << "\n" if frozen_string.nil? && !lines[0].strip.empty?
|
|
149
|
-
header_lines << "#--\n"
|
|
150
|
-
|
|
151
|
-
# REUSE-IgnoreStart
|
|
152
|
-
updated_copyrights.each do |c|
|
|
153
|
-
header_lines << "# SPDX-FileCopyrightText: #{c[:year]} #{c[:holder]}\n"
|
|
154
|
-
end
|
|
155
|
-
header_lines << "# SPDX-License-Identifier: #{target_license}\n"
|
|
156
|
-
# REUSE-IgnoreEnd
|
|
157
|
-
header_lines << "#++\n"
|
|
158
|
-
|
|
159
|
-
content_start = existing[:end_line] + 1
|
|
160
|
-
while content_start < lines.length && lines[content_start].strip.empty?
|
|
161
|
-
content_start += 1
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
remaining = lines[content_start..]
|
|
165
|
-
|
|
166
|
-
new_content = if frozen_string
|
|
167
|
-
"#{frozen_string}\n#{header_lines.join}\n#{remaining.join}"
|
|
168
|
-
else
|
|
169
|
-
"#{header_lines.join}\n#{remaining.join}"
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
File.write(filepath, new_content)
|
|
173
|
-
puts "Updated: #{filepath}"
|
|
174
|
-
end
|
|
175
|
-
else
|
|
176
|
-
# No header - add one with YOUR copyright only
|
|
177
|
-
frozen_line = lines[0]&.include?("frozen_string_literal") ? lines.shift : nil
|
|
178
|
-
|
|
179
|
-
header = []
|
|
180
|
-
header << "# frozen_string_literal: true\n\n" unless frozen_line
|
|
181
|
-
header << "#--\n"
|
|
182
|
-
# REUSE-IgnoreStart
|
|
183
|
-
header << "# SPDX-FileCopyrightText: #{your_year} #{YOUR_COPYRIGHT}\n"
|
|
184
|
-
header << "# SPDX-License-Identifier: #{target_license}\n"
|
|
185
|
-
# REUSE-IgnoreEnd
|
|
186
|
-
header << "#++\n\n"
|
|
187
|
-
|
|
188
|
-
if frozen_line
|
|
189
|
-
File.write(filepath, "#{frozen_line}\n#{header.join}#{lines.join}")
|
|
190
|
-
else
|
|
191
|
-
File.write(filepath, header.join + lines.join)
|
|
192
|
-
end
|
|
193
|
-
puts "Added header: #{filepath}"
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Finds Ruby files to process.
|
|
198
|
-
#
|
|
199
|
-
# License automation runs on file sets. Users may specify paths or want all
|
|
200
|
-
# lib/ext/test files. This handles both cases using git ls-files for tracking.
|
|
201
|
-
#
|
|
202
|
-
# [paths] Explicit paths to process, or empty for default directories.
|
|
203
|
-
def find_rb_files(paths)
|
|
204
|
-
if paths.empty?
|
|
205
|
-
# Process all relevant directories
|
|
206
|
-
dirs = %w[lib ext test examples tasks bin sig]
|
|
207
|
-
files = dirs.flat_map do |dir|
|
|
208
|
-
# Include both root files and subdirectory files, for both .rb and .rbs
|
|
209
|
-
%w[rb rbs].flat_map do |ext|
|
|
210
|
-
root_files = `git ls-files '#{dir}/*.#{ext}' 2>/dev/null`.split("\n")
|
|
211
|
-
sub_files = `git ls-files '#{dir}/**/*.#{ext}' 2>/dev/null`.split("\n")
|
|
212
|
-
root_files + sub_files
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
files.uniq
|
|
216
|
-
else
|
|
217
|
-
paths.flat_map do |path|
|
|
218
|
-
if File.directory?(path)
|
|
219
|
-
rb_files = `git ls-files '#{path}/**/*.rb'`.split("\n")
|
|
220
|
-
rbs_files = `git ls-files '#{path}/**/*.rbs'`.split("\n")
|
|
221
|
-
rb_files + rbs_files
|
|
222
|
-
else
|
|
223
|
-
path
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
if __FILE__ == $0
|
|
230
|
-
paths = ARGV.empty? ? [] : ARGV
|
|
231
|
-
files = find_rb_files(paths)
|
|
232
|
-
|
|
233
|
-
files.each do |file|
|
|
234
|
-
process_file(file)
|
|
235
|
-
end
|
|
236
|
-
end
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
#--
|
|
4
|
-
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
-
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
-
#++
|
|
7
|
-
|
|
8
|
-
# Shared utility for detecting contributors from git blame and Co-Authored-By trailers.
|
|
9
|
-
#
|
|
10
|
-
# This module provides methods to:
|
|
11
|
-
# - Get all contributors (authors and co-authors) who touched specific lines
|
|
12
|
-
# - Track the latest year each contributor touched those lines
|
|
13
|
-
# - Parse Co-Authored-By trailers from commit messages
|
|
14
|
-
|
|
15
|
-
require "open3"
|
|
16
|
-
require "date"
|
|
17
|
-
|
|
18
|
-
# Extracts contributor information from git history.
|
|
19
|
-
#
|
|
20
|
-
# License headers need accurate copyright years and contributor names. Git
|
|
21
|
-
# blame provides line-level authorship. Commit messages contain Co-Authored-By
|
|
22
|
-
# trailers. Combining these sources manually is tedious.
|
|
23
|
-
#
|
|
24
|
-
# This module queries git blame and parses commit messages. It returns
|
|
25
|
-
# contributor names with their latest modification years. Use it when
|
|
26
|
-
# generating or updating SPDX headers.
|
|
27
|
-
module LicenseUtils
|
|
28
|
-
# A contributor with their latest modification year.
|
|
29
|
-
#
|
|
30
|
-
# [name] The contributor's display name.
|
|
31
|
-
# [email] The contributor's email address.
|
|
32
|
-
# [year] The most recent year they modified the file.
|
|
33
|
-
Contributor = Data.define(:name, :email, :year)
|
|
34
|
-
|
|
35
|
-
class << self
|
|
36
|
-
# Get all contributors who touched lines in a file (or range of lines).
|
|
37
|
-
# Returns a Hash of { "Name <email>" => year } mapping each contributor to their latest year.
|
|
38
|
-
#
|
|
39
|
-
# This considers both the commit author AND any Co-Authored-By trailers in commit messages.
|
|
40
|
-
def get_contributors_for_lines(filepath, start_line = nil, end_line = nil)
|
|
41
|
-
blame_cmd = if start_line && end_line
|
|
42
|
-
%W[git blame -L #{start_line},#{end_line} --porcelain -- #{filepath}]
|
|
43
|
-
else
|
|
44
|
-
%W[git blame --porcelain -- #{filepath}]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
output, _status = Open3.capture2(*blame_cmd)
|
|
48
|
-
|
|
49
|
-
contributors = {} # "Name <email>" => year
|
|
50
|
-
commit_cache = {} # commit_hash => { year:, author:, co_authors: [] }
|
|
51
|
-
|
|
52
|
-
current_commit = nil
|
|
53
|
-
|
|
54
|
-
output.each_line do |line|
|
|
55
|
-
if line =~ /^([a-f0-9]{40})/
|
|
56
|
-
current_commit = $1
|
|
57
|
-
elsif line =~ /^author (.+)$/
|
|
58
|
-
commit_cache[current_commit] ||= {}
|
|
59
|
-
commit_cache[current_commit][:author_name] = $1
|
|
60
|
-
elsif line =~ /^author-mail <(.+)>$/
|
|
61
|
-
commit_cache[current_commit] ||= {}
|
|
62
|
-
commit_cache[current_commit][:author_email] = $1
|
|
63
|
-
elsif line =~ /^author-time (\d+)$/
|
|
64
|
-
commit_cache[current_commit] ||= {}
|
|
65
|
-
timestamp = $1.to_i
|
|
66
|
-
commit_cache[current_commit][:year] = Time.at(timestamp).year
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Now fetch co-authors for each unique commit
|
|
71
|
-
commit_cache.each do |commit_hash, data|
|
|
72
|
-
next if commit_hash == "0" * 40 # Skip uncommitted lines
|
|
73
|
-
|
|
74
|
-
# Get commit message for Co-Authored-By parsing
|
|
75
|
-
msg_output, _status = Open3.capture2("git", "log", "-1", "--format=%B", commit_hash)
|
|
76
|
-
co_authors = parse_co_authors(msg_output)
|
|
77
|
-
data[:co_authors] = co_authors
|
|
78
|
-
|
|
79
|
-
# Add author
|
|
80
|
-
if data[:author_name] && data[:author_email]
|
|
81
|
-
key = "#{data[:author_name]} <#{data[:author_email]}>"
|
|
82
|
-
year = data[:year] || Date.today.year
|
|
83
|
-
contributors[key] = [contributors[key] || 0, year].max
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Add co-authors with same year as commit
|
|
87
|
-
co_authors.each do |ca|
|
|
88
|
-
key = "#{ca[:name]} <#{ca[:email]}>"
|
|
89
|
-
year = data[:year] || Date.today.year
|
|
90
|
-
contributors[key] = [contributors[key] || 0, year].max
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
contributors
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Get YOUR latest year contribution to the file/lines.
|
|
98
|
-
# your_identifiers should be an array of strings that identify you (name, email fragments).
|
|
99
|
-
def get_your_latest_year(filepath, your_identifiers, start_line = nil, end_line = nil)
|
|
100
|
-
contributors = get_contributors_for_lines(filepath, start_line, end_line)
|
|
101
|
-
|
|
102
|
-
your_year = nil
|
|
103
|
-
contributors.each do |contributor, year|
|
|
104
|
-
if your_identifiers.any? { |id| contributor.include?(id) }
|
|
105
|
-
your_year = [your_year || 0, year].max
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
your_year || Date.today.year
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Get all contributors EXCEPT you, with their latest years.
|
|
113
|
-
# Returns array of { name:, email:, year: }
|
|
114
|
-
def get_other_contributors(filepath, your_identifiers, start_line = nil, end_line = nil)
|
|
115
|
-
contributors = get_contributors_for_lines(filepath, start_line, end_line)
|
|
116
|
-
|
|
117
|
-
others = []
|
|
118
|
-
contributors.each do |contributor, year|
|
|
119
|
-
next if your_identifiers.any? { |id| contributor.include?(id) }
|
|
120
|
-
|
|
121
|
-
# Parse "Name <email>" format
|
|
122
|
-
if contributor =~ /^(.+?)\s*<(.+)>$/
|
|
123
|
-
others << { name: $1.strip, email: $2.strip, year: }
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
others
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
private def parse_co_authors(message)
|
|
131
|
-
co_authors = []
|
|
132
|
-
|
|
133
|
-
message.each_line do |line|
|
|
134
|
-
# Match "Co-Authored-By: Name <email>" (case insensitive)
|
|
135
|
-
if line =~ /^Co-Authored-By:\s*(.+?)\s*<(.+?)>\s*$/i
|
|
136
|
-
co_authors << { name: $1.strip, email: $2.strip }
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
co_authors
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|