ratatui_ruby-devtools 0.2.0 → 0.3.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 +63 -6
- data/README.md +49 -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/site_tasks/build.rake +10 -15
- 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 +72 -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 +31 -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 +49 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_files.rb +36 -0
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_header_syntax.rb +39 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rust_files.rb +27 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rust_header_syntax.rb +59 -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 +7 -1
- 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
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
require_relative "licensed_file"
|
|
9
|
+
require_relative "rbs_header_syntax"
|
|
10
|
+
|
|
11
|
+
class RbsFiles < Data.define(:files) # :nodoc:
|
|
12
|
+
def self.changed(repository, licensing)
|
|
13
|
+
new(files: LicensedFile.changed(RbsHeaderSyntax.new(licensing), repository:))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def label
|
|
17
|
+
"RBS"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def count
|
|
21
|
+
files.count
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def license
|
|
25
|
+
files.flat_map(&:annotate)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
require_relative "directory_licensing"
|
|
9
|
+
require_relative "ruby_comment_header"
|
|
10
|
+
|
|
11
|
+
# RBS has no magic comments, so its header opens the file.
|
|
12
|
+
class RbsHeaderSyntax # :nodoc:
|
|
13
|
+
include DirectoryLicensing
|
|
14
|
+
include RubyCommentHeader
|
|
15
|
+
|
|
16
|
+
def extensions
|
|
17
|
+
%w[rbs]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def directories
|
|
21
|
+
%w[sig]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def added(header, lines)
|
|
25
|
+
"#{block(header)}\n#{lines.drop_while { |line| line.strip.empty? }.join}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def replaced(header, lines, end_line)
|
|
29
|
+
"#{block(header)}\n#{after_header(lines, end_line).join}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
require "rdoc"
|
|
9
|
+
require_relative "rdoc_examples"
|
|
10
|
+
require_relative "snippet"
|
|
11
|
+
|
|
12
|
+
# A run of comment lines, read the way RDoc reads it. Indentation alone
|
|
13
|
+
# doesn't make an example: a list item's continuation and a call-seq are
|
|
14
|
+
# indented too, and blank lines after an example aren't part of it.
|
|
15
|
+
class RdocComment < Data.define(:first, :prefix, :lines) # :nodoc:
|
|
16
|
+
def self.all_in(file_lines)
|
|
17
|
+
file_lines.each_with_index
|
|
18
|
+
.select { |line, _| prefix_of(line) }
|
|
19
|
+
.chunk_while { |(line, index), (other, other_index)| other_index == index + 1 && prefix_of(other) == prefix_of(line) }
|
|
20
|
+
.map { |run| new(first: run.first.last, prefix: prefix_of(run.first.first), lines: run.map(&:first)) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.prefix_of(line)
|
|
24
|
+
line[/\A(\s*)#/, 1]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RDoc's markup carries no line numbers, and its own steps drop lines on
|
|
28
|
+
# the way, so each example is found again by all of its lines.
|
|
29
|
+
def examples
|
|
30
|
+
cursor = 0
|
|
31
|
+
RDoc::Markup.parse(markup).accept(RdocExamples.new).filter_map do |verbatim|
|
|
32
|
+
code = verbatim.text.lines.map(&:strip)
|
|
33
|
+
start = cursor.upto(lines.length - code.length).find { |index| code_at?(index, code) }
|
|
34
|
+
next unless start
|
|
35
|
+
|
|
36
|
+
cursor = start + code.length
|
|
37
|
+
Snippet.around(first + start, first + cursor - 1, prefix:)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# An :include: directive names a file RDoc would read in, but it holds no
|
|
42
|
+
# example from this one.
|
|
43
|
+
private def markup
|
|
44
|
+
text, = RDoc::Comment.parse(lines.map { |line| line.delete_prefix(prefix) }.join, nil, first, :ruby) { "" }
|
|
45
|
+
text
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private def code_at?(index, code)
|
|
49
|
+
lines[index, code.length].map { |line| line.sub(/\A\s*#+/, "").strip } == code
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
require "rdoc"
|
|
9
|
+
|
|
10
|
+
# The code blocks of an RDoc document, wherever they sit, as RDoc's own
|
|
11
|
+
# table of contents collects its headings.
|
|
12
|
+
class RdocExamples < RDoc::Markup::Formatter # :nodoc:
|
|
13
|
+
def start_accepting
|
|
14
|
+
@examples = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def end_accepting
|
|
18
|
+
@examples
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def accept_verbatim(verbatim)
|
|
22
|
+
@examples << verbatim
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ignore(*)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias accept_block_quote ignore
|
|
29
|
+
alias accept_raw ignore
|
|
30
|
+
alias accept_rule ignore
|
|
31
|
+
alias accept_blank_line ignore
|
|
32
|
+
alias accept_hard_break ignore
|
|
33
|
+
alias accept_heading ignore
|
|
34
|
+
alias accept_paragraph ignore
|
|
35
|
+
alias accept_indented_paragraph ignore
|
|
36
|
+
alias accept_list_start ignore
|
|
37
|
+
alias accept_list_end ignore
|
|
38
|
+
alias accept_list_item_start ignore
|
|
39
|
+
alias accept_list_item_end ignore
|
|
40
|
+
alias accept_table ignore
|
|
41
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
require_relative "rdoc_comment"
|
|
9
|
+
require_relative "snippet"
|
|
10
|
+
require_relative "snippet_opening"
|
|
11
|
+
require_relative "wrapped_snippet"
|
|
12
|
+
|
|
13
|
+
# The SPDX wrapper sits between #-- and #++ so RDoc leaves it out of the
|
|
14
|
+
# docs.
|
|
15
|
+
class RdocSnippetSyntax # :nodoc:
|
|
16
|
+
LICENSE = "MIT-0"
|
|
17
|
+
|
|
18
|
+
def extensions
|
|
19
|
+
%w[rb]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tracked_patterns
|
|
23
|
+
["*.rb"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Existing wrappers with their copyright notices under the snippet tag,
|
|
27
|
+
# without the blank lines that once followed their code inside them, and
|
|
28
|
+
# only around text RDoc shows as code.
|
|
29
|
+
def cleaned(lines)
|
|
30
|
+
in_tags = false
|
|
31
|
+
tagged = lines.map do |line|
|
|
32
|
+
in_tags = true if line.match?(/^\s*# SPDX-SnippetBegin\s*$/)
|
|
33
|
+
cleaned_line = in_tags ? Snippet.tag(line) : line
|
|
34
|
+
in_tags = false if line.match?(/^\s*#\+\+\s*$/)
|
|
35
|
+
cleaned_line
|
|
36
|
+
end
|
|
37
|
+
without_wrappers_around_prose(blanks_after_wrappers(tagged))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def license
|
|
41
|
+
LICENSE
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A wrapper marks its own snippet, whatever RDoc makes of the code inside
|
|
45
|
+
# it. RDoc finds the examples that have no wrapper yet.
|
|
46
|
+
def snippets_in(lines)
|
|
47
|
+
wrapped = wrapped_in(lines)
|
|
48
|
+
bare = examples(lines).reject { |example| wrapped.any? { |snippet| snippet.covers?(example) } }
|
|
49
|
+
(wrapped + bare).sort_by(&:first)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def opening(snippet, copyrights)
|
|
53
|
+
prefix = snippet.prefix
|
|
54
|
+
# REUSE-IgnoreStart
|
|
55
|
+
[
|
|
56
|
+
"#{prefix}#--\n",
|
|
57
|
+
"#{prefix}# SPDX-SnippetBegin\n",
|
|
58
|
+
*copyrights.map { |copyright| "#{prefix}# #{Snippet::COPYRIGHT_TAG}: #{copyright}\n" },
|
|
59
|
+
"#{prefix}# SPDX-License-Identifier: #{LICENSE}\n",
|
|
60
|
+
"#{prefix}#++\n",
|
|
61
|
+
]
|
|
62
|
+
# REUSE-IgnoreEnd
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def closing(snippet)
|
|
66
|
+
prefix = snippet.prefix
|
|
67
|
+
# REUSE-IgnoreStart
|
|
68
|
+
[
|
|
69
|
+
"#{prefix}#--\n",
|
|
70
|
+
"#{prefix}# SPDX-SnippetEnd\n",
|
|
71
|
+
"#{prefix}#++\n",
|
|
72
|
+
]
|
|
73
|
+
# REUSE-IgnoreEnd
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private def examples(lines)
|
|
77
|
+
RdocComment.all_in(lines).flat_map(&:examples)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The code between each SnippetBegin's #++ and its SnippetEnd's #--.
|
|
81
|
+
private def wrapped_in(lines)
|
|
82
|
+
lines.each_index.filter_map do |index|
|
|
83
|
+
next unless lines[index].match?(/^\s*# SPDX-SnippetBegin\s*$/)
|
|
84
|
+
|
|
85
|
+
prefix = lines[index][/\A\s*/]
|
|
86
|
+
opened = (index + 1).upto(lines.length - 1).find { |line| lines[line].match?(/^#{Regexp.escape(prefix)}#\+\+\s*$/) }
|
|
87
|
+
ended = opened && (opened + 1).upto(lines.length - 1).find { |line| lines[line].match?(/^\s*# SPDX-SnippetEnd\s*$/) }
|
|
88
|
+
next unless ended && ended - 2 > opened
|
|
89
|
+
|
|
90
|
+
snippet = Snippet.around(opened + 1, ended - 2, prefix:)
|
|
91
|
+
WrappedSnippet.new(snippet:, opening: SnippetOpening.in(lines, index - 1, opened))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Wrappers once went around every indented comment, list items' text
|
|
96
|
+
# included. A wrapper whose lines RDoc shows as prose comes off.
|
|
97
|
+
private def without_wrappers_around_prose(lines)
|
|
98
|
+
code = examples(lines).map(&:first)
|
|
99
|
+
prose = wrapped_in(lines).reject { |wrapped| code.include?(wrapped.first) }
|
|
100
|
+
prose.reverse_each.with_object(lines.dup) do |wrapped, unwrapped|
|
|
101
|
+
ended = wrapped.snippet.last + 1
|
|
102
|
+
unwrapped.slice!(ended..(ended + 2))
|
|
103
|
+
unwrapped.slice!(wrapped.opening.span)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Blank comment lines after an example aren't part of it, as RDoc reads it.
|
|
108
|
+
private def blanks_after_wrappers(lines)
|
|
109
|
+
lines = lines.dup
|
|
110
|
+
lines.each_index do |index|
|
|
111
|
+
next unless lines[index].match?(/^\s*#--\s*$/) && lines[index + 1]&.match?(/^\s*# SPDX-SnippetEnd\s*$/)
|
|
112
|
+
|
|
113
|
+
first_blank = index
|
|
114
|
+
first_blank -= 1 while first_blank.positive? && lines[first_blank - 1].match?(/^\s*#\s*$/)
|
|
115
|
+
lines.insert(first_blank + 3, *lines.slice!(first_blank...index)) if first_blank < index
|
|
116
|
+
end
|
|
117
|
+
lines
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
# Something brought up to date, with a notice for each change made to it.
|
|
9
|
+
class Renewal < Data.define(:value, :notices) # :nodoc:
|
|
10
|
+
def self.unchanged(value)
|
|
11
|
+
new(value:, notices: [])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def changed?
|
|
15
|
+
notices.any?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
class ReuseAnnotatedFile < Data.define(:path, :license, :copyright, :tool) # :nodoc:
|
|
9
|
+
def annotate
|
|
10
|
+
["Annotating #{path} with #{license}", *tool.annotate(path, license:, copyright:)]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
require "open3"
|
|
9
|
+
|
|
10
|
+
class ReuseTool # :nodoc:
|
|
11
|
+
# The lint report lists files with no copyright and licensing information
|
|
12
|
+
# under a heading of their own, up to the next heading.
|
|
13
|
+
def unlicensed_paths
|
|
14
|
+
listed = false
|
|
15
|
+
`reuse lint 2>&1`.lines.filter_map do |line|
|
|
16
|
+
listed = true if line.include?("no copyright and licensing")
|
|
17
|
+
listed = false if line.start_with?("# ") && !line.include?("copyright")
|
|
18
|
+
# On Windows the tool reports paths with backslashes.
|
|
19
|
+
line[/^\* (.+)/, 1]&.tr("\\", "/") if listed
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Lockfiles and plain text have no comments to hold a header, so they get
|
|
24
|
+
# a .license file beside them.
|
|
25
|
+
def annotate(path, license:, copyright:)
|
|
26
|
+
output, status = Open3.capture2e(
|
|
27
|
+
"reuse", "annotate", "--license", license, "--copyright", copyright,
|
|
28
|
+
"--fallback-dot-license", "--skip-existing", path
|
|
29
|
+
)
|
|
30
|
+
raise "reuse annotate failed for #{path}:\n#{output}" unless status.success?
|
|
31
|
+
|
|
32
|
+
output.lines(chomp: true)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
require_relative "copyright"
|
|
9
|
+
require_relative "existing_header"
|
|
10
|
+
require_relative "missing_header"
|
|
11
|
+
require_relative "spdx_header"
|
|
12
|
+
|
|
13
|
+
# Ruby and RBS share comments, so their headers sit between the same #-- and
|
|
14
|
+
# #++ that keep a Ruby header out of the docs.
|
|
15
|
+
module RubyCommentHeader # :nodoc:
|
|
16
|
+
# REUSE-IgnoreStart
|
|
17
|
+
COPYRIGHT = /^#\s*SPDX-FileCopyrightText:\s*(\d{4})\s+(.+)$/
|
|
18
|
+
LICENSE = /^#\s*SPDX-License-Identifier:\s*(.+)$/
|
|
19
|
+
# REUSE-IgnoreEnd
|
|
20
|
+
|
|
21
|
+
# The header is in the comments the file opens with. Without its #++, it
|
|
22
|
+
# ends at its last tag.
|
|
23
|
+
def header_in(lines)
|
|
24
|
+
copyrights = []
|
|
25
|
+
license = nil
|
|
26
|
+
last_tag = nil
|
|
27
|
+
lines.each_with_index do |line, index|
|
|
28
|
+
case line
|
|
29
|
+
when COPYRIGHT
|
|
30
|
+
copyrights << Copyright.new(year: $1.to_i, holder: $2.strip)
|
|
31
|
+
last_tag = index
|
|
32
|
+
when LICENSE
|
|
33
|
+
license = $1.strip
|
|
34
|
+
last_tag = index
|
|
35
|
+
when /^#\+\+\s*$/
|
|
36
|
+
return ExistingHeader.new(header: SpdxHeader.new(copyrights:, license:), end_line: index) if last_tag
|
|
37
|
+
when /^\s*[^#\s]/
|
|
38
|
+
break
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
return MissingHeader.new unless last_tag
|
|
42
|
+
|
|
43
|
+
ExistingHeader.new(header: SpdxHeader.new(copyrights:, license:), end_line: last_tag)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private def block(header)
|
|
47
|
+
"#--\n#{header.commented('#')}#++\n"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
require_relative "licensed_file"
|
|
9
|
+
require_relative "rdoc_snippet_syntax"
|
|
10
|
+
require_relative "ruby_header_syntax"
|
|
11
|
+
require_relative "snippet_file"
|
|
12
|
+
|
|
13
|
+
class RubyFiles < Data.define(:files) # :nodoc:
|
|
14
|
+
def self.changed(repository, licensing)
|
|
15
|
+
new(files: LicensedFile.changed(RubyHeaderSyntax.new(licensing), repository:))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def label
|
|
19
|
+
"Ruby"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def count
|
|
23
|
+
files.count
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def license
|
|
27
|
+
files.flat_map(&:annotate) + library.flat_map(&:annotate)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# RDoc examples only appear in the documented library.
|
|
31
|
+
private def library
|
|
32
|
+
syntax = RdocSnippetSyntax.new
|
|
33
|
+
files.select { |file| file.path.start_with?("lib/") && file.path.end_with?(".rb") }
|
|
34
|
+
.map { |file| SnippetFile.new(path: file.path, syntax:, author: file.author) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
require_relative "directory_licensing"
|
|
9
|
+
require_relative "ruby_comment_header"
|
|
10
|
+
|
|
11
|
+
class RubyHeaderSyntax # :nodoc:
|
|
12
|
+
include DirectoryLicensing
|
|
13
|
+
include RubyCommentHeader
|
|
14
|
+
|
|
15
|
+
FROZEN_STRING_LITERAL = "# frozen_string_literal: true\n"
|
|
16
|
+
|
|
17
|
+
def extensions
|
|
18
|
+
%w[rb]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def directories
|
|
22
|
+
%w[lib ext test examples tasks bin]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def added(header, lines)
|
|
26
|
+
frozen = frozen_string_literal_in(lines)
|
|
27
|
+
body = (frozen ? lines.drop(1) : lines).drop_while { |line| line.strip.empty? }
|
|
28
|
+
"#{frozen || FROZEN_STRING_LITERAL}\n#{block(header)}\n#{body.join}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def replaced(header, lines, end_line)
|
|
32
|
+
frozen = frozen_string_literal_in(lines)
|
|
33
|
+
"#{frozen || FROZEN_STRING_LITERAL}\n#{block(header)}\n#{after_header(lines, end_line).join}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def frozen_string_literal_in(lines)
|
|
37
|
+
lines.first if lines.first&.include?("frozen_string_literal")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
require_relative "licensed_file"
|
|
9
|
+
require_relative "rust_header_syntax"
|
|
10
|
+
|
|
11
|
+
class RustFiles < Data.define(:files) # :nodoc:
|
|
12
|
+
def self.changed(repository, licensing)
|
|
13
|
+
new(files: LicensedFile.changed(RustHeaderSyntax.new(licensing), repository:))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def label
|
|
17
|
+
"Rust"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def count
|
|
21
|
+
files.count
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def license
|
|
25
|
+
files.flat_map(&:annotate)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
require_relative "copyright"
|
|
9
|
+
require_relative "directory_licensing"
|
|
10
|
+
require_relative "existing_header"
|
|
11
|
+
require_relative "missing_header"
|
|
12
|
+
require_relative "spdx_header"
|
|
13
|
+
|
|
14
|
+
class RustHeaderSyntax # :nodoc:
|
|
15
|
+
include DirectoryLicensing
|
|
16
|
+
|
|
17
|
+
# REUSE-IgnoreStart
|
|
18
|
+
COPYRIGHT = %r{^//\s*SPDX-FileCopyrightText:\s*(\d{4})\s+(.+)$}
|
|
19
|
+
LICENSE = %r{^//\s*SPDX-License-Identifier:\s*(.+)$}
|
|
20
|
+
# REUSE-IgnoreEnd
|
|
21
|
+
|
|
22
|
+
def extensions
|
|
23
|
+
%w[rs]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def directories
|
|
27
|
+
%w[ext]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def header_in(lines)
|
|
31
|
+
copyrights = []
|
|
32
|
+
license = nil
|
|
33
|
+
last_tag = nil
|
|
34
|
+
lines.each_with_index do |line, index|
|
|
35
|
+
case line
|
|
36
|
+
when COPYRIGHT
|
|
37
|
+
copyrights << Copyright.new(year: $1.to_i, holder: $2.strip)
|
|
38
|
+
last_tag = index
|
|
39
|
+
when LICENSE
|
|
40
|
+
license = $1.strip
|
|
41
|
+
last_tag = index
|
|
42
|
+
else
|
|
43
|
+
next if line.start_with?("//")
|
|
44
|
+
break if license || !line.strip.empty?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
return MissingHeader.new unless last_tag
|
|
48
|
+
|
|
49
|
+
ExistingHeader.new(header: SpdxHeader.new(copyrights:, license:), end_line: last_tag)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def added(header, lines)
|
|
53
|
+
"#{header.commented('//')}\n#{lines.join}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def replaced(header, lines, end_line)
|
|
57
|
+
"#{header.commented('//')}\n#{after_header(lines, end_line).join}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
# A code example inside a file, wrapped from its first to its last line
|
|
9
|
+
# counting from 0, behind the given comment prefix. Its year comes from the
|
|
10
|
+
# code lines alone, which can sit inside markers the wrapper goes around.
|
|
11
|
+
class Snippet < Data.define(:first, :last, :prefix, :code) # :nodoc:
|
|
12
|
+
# REUSE 3.3 requires copyright notices inside a snippet to use the snippet
|
|
13
|
+
# tag. Earlier wrappers used the file tag.
|
|
14
|
+
# REUSE-IgnoreStart
|
|
15
|
+
COPYRIGHT_TAG = "SPDX-SnippetCopyrightText"
|
|
16
|
+
FILE_COPYRIGHT_TAG = "SPDX-FileCopyrightText"
|
|
17
|
+
# REUSE-IgnoreEnd
|
|
18
|
+
|
|
19
|
+
def self.around(first, last, prefix: "", code: [first, last])
|
|
20
|
+
new(first:, last:, prefix:, code:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A line of a wrapper's tags, using the snippet copyright tag.
|
|
24
|
+
def self.tag(line)
|
|
25
|
+
line.sub(FILE_COPYRIGHT_TAG, COPYRIGHT_TAG)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The code lines as git blame counts them, from 1.
|
|
29
|
+
def blame_range
|
|
30
|
+
code.map { |line| line + 1 }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def lines_in(file_lines)
|
|
34
|
+
file_lines[first..last]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bare?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def revise(revision)
|
|
42
|
+
revision.wrap(self)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
require_relative "../bump/repository"
|
|
9
|
+
require_relative "snippet_revision"
|
|
10
|
+
|
|
11
|
+
class SnippetFile < Data.define(:path, :syntax, :author) # :nodoc:
|
|
12
|
+
def self.tracked(paths, syntax, repository: Repository.new)
|
|
13
|
+
files = repository.tracked_files_for(paths, syntax)
|
|
14
|
+
return [] if files.empty?
|
|
15
|
+
|
|
16
|
+
author = repository.author
|
|
17
|
+
files.map { |path| new(path:, syntax:, author:) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Brings the file's snippets up to date and says what changed.
|
|
21
|
+
def annotate
|
|
22
|
+
# Binary mode keeps the file's own line endings, which text mode on
|
|
23
|
+
# Windows would turn into CRLF.
|
|
24
|
+
text = File.read(path, mode: "rb", encoding: "UTF-8")
|
|
25
|
+
revision = SnippetRevision.new(path:, text:, syntax:, author:).run
|
|
26
|
+
File.write(path, revision.text, mode: "wb") if revision.changed?
|
|
27
|
+
revision.messages
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
require_relative "copyright"
|
|
9
|
+
require_relative "renewal"
|
|
10
|
+
require_relative "spdx_header"
|
|
11
|
+
|
|
12
|
+
# The tags that open a snippet already wrapped, from their first to their last
|
|
13
|
+
# line counting from 0, with the notices they hold.
|
|
14
|
+
class SnippetOpening < Data.define(:first, :last, :header) # :nodoc:
|
|
15
|
+
# REUSE-IgnoreStart
|
|
16
|
+
COPYRIGHT = /SPDX-(?:Snippet|File)CopyrightText:\s*(\d{4})\s+(.+)$/
|
|
17
|
+
LICENSE = /SPDX-License-Identifier:\s*(\S+)/
|
|
18
|
+
# REUSE-IgnoreEnd
|
|
19
|
+
|
|
20
|
+
def self.in(lines, first, last)
|
|
21
|
+
tags = lines[first..last]
|
|
22
|
+
copyrights = tags.grep(COPYRIGHT) { Copyright.new(year: $1.to_i, holder: $2.strip) }
|
|
23
|
+
license = tags.grep(LICENSE) { $1 }.last
|
|
24
|
+
new(first:, last:, header: SpdxHeader.new(copyrights:, license:))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def span
|
|
28
|
+
first..last
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def copyrights
|
|
32
|
+
header.copyrights
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def renewal(contributions, license)
|
|
36
|
+
renewal = header.renewal(contributions, license)
|
|
37
|
+
Renewal.new(value: with(header: renewal.value), notices: renewal.notices)
|
|
38
|
+
end
|
|
39
|
+
end
|