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
|
@@ -17,37 +17,124 @@
|
|
|
17
17
|
#
|
|
18
18
|
# === Example
|
|
19
19
|
#
|
|
20
|
-
# SemVer.parse("1.2.3").next(:minor).to_s
|
|
20
|
+
# SemVer.parse("1.2.3").next(:minor).to_s # => "1.3.0"
|
|
21
|
+
# SemVer.parse("1.2.3").next(:minor, "rc").to_s # => "1.3.0-rc.1"
|
|
21
22
|
#
|
|
22
23
|
# See https://semver.org/spec/v2.0.0.html
|
|
23
24
|
class SemVer
|
|
25
|
+
include Comparable
|
|
26
|
+
|
|
24
27
|
# The valid segment names for version bumping.
|
|
25
28
|
SEGMENTS = [:major, :minor, :patch].freeze
|
|
26
29
|
|
|
30
|
+
# Dot-separated prerelease identifiers. Numeric identifiers have no leading
|
|
31
|
+
# zeroes.
|
|
32
|
+
PRERELEASE = /\A(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*\z/
|
|
33
|
+
|
|
27
34
|
# Parses a version string into a SemVer instance.
|
|
28
35
|
#
|
|
29
|
-
#
|
|
36
|
+
# Raises ArgumentError for a prerelease that isn't SemVer, such as
|
|
37
|
+
# <tt>"1.0.0.beta.1"</tt> or <tt>"1.0.0-alpha.001"</tt>.
|
|
38
|
+
#
|
|
39
|
+
# [string] A version string like <tt>"1.2.3"</tt> or <tt>"1.0.0-rc.1"</tt>.
|
|
30
40
|
def self.parse(string)
|
|
31
41
|
require "rubygems"
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
# Extract prerelease suffix (e.g., "-beta.1", "-alpha.2", "-rc.1")
|
|
43
|
+
base, prerelease = string.split("-", 2)
|
|
44
|
+
version = Gem::Version.new(base)
|
|
45
|
+
if version.prerelease?
|
|
46
|
+
raise ArgumentError, "#{string} is not SemVer. Put the prerelease after a hyphen, like 1.0.0-beta.1."
|
|
47
|
+
end
|
|
48
|
+
if prerelease && !prerelease.match?(PRERELEASE)
|
|
49
|
+
raise ArgumentError, "#{string} has an invalid prerelease. Use dot-separated letters, digits, and hyphens, " \
|
|
50
|
+
"with no leading zeroes in numbers, like 1.0.0-alpha.1."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
segments = (version.segments + [0, 0, 0]).first(3)
|
|
54
|
+
new(segments, prerelease:)
|
|
34
55
|
end
|
|
35
56
|
|
|
36
57
|
# Creates a new SemVer from an array of segments.
|
|
37
58
|
#
|
|
38
59
|
# [segments] An array of three integers: <tt>[major, minor, patch]</tt>.
|
|
39
|
-
def initialize(segments)
|
|
60
|
+
def initialize(segments, prerelease: nil)
|
|
40
61
|
@segments = segments
|
|
62
|
+
@prerelease = prerelease
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def major = @segments[0] # :nodoc:
|
|
66
|
+
def minor = @segments[1] # :nodoc:
|
|
67
|
+
def patch = @segments[2] # :nodoc:
|
|
68
|
+
|
|
69
|
+
# Whether this version promises a stable public API: 1.0 or later, and not a
|
|
70
|
+
# prerelease, which SemVer marks as unstable.
|
|
71
|
+
def stable?
|
|
72
|
+
major >= 1 && @prerelease.nil?
|
|
41
73
|
end
|
|
42
74
|
|
|
43
75
|
# Returns the next version after bumping the given segment.
|
|
44
76
|
#
|
|
45
|
-
# Bumping a segment resets all following segments to zero.
|
|
77
|
+
# Bumping a segment resets all following segments to zero. A prerelease
|
|
78
|
+
# comes before its release, so bumping it to a segment it already leads up
|
|
79
|
+
# to releases it: <tt>1.0.0-rc.1</tt> becomes <tt>1.0.0</tt> on any segment,
|
|
80
|
+
# and <tt>1.2.3-rc.1</tt> becomes <tt>1.2.3</tt> on <tt>:patch</tt>.
|
|
81
|
+
#
|
|
82
|
+
# Pass a +label+ to get a prerelease of that version instead. Its number
|
|
83
|
+
# counts up from the prerelease you're on when that leads to the same
|
|
84
|
+
# version under the same label, and starts at 1 otherwise:
|
|
85
|
+
# <tt>1.2.0-rc.1</tt> becomes <tt>1.2.0-rc.2</tt> on <tt>:minor</tt> with
|
|
86
|
+
# <tt>"rc"</tt>, and <tt>1.2.0-beta.3</tt> becomes <tt>1.2.0-rc.1</tt>.
|
|
87
|
+
#
|
|
88
|
+
# Raises ArgumentError when the labelled version would come before this
|
|
89
|
+
# one. SemVer compares labels as text, so <tt>beta</tt> can't follow
|
|
90
|
+
# <tt>rc</tt>.
|
|
46
91
|
#
|
|
47
92
|
# [segment] One of <tt>:major</tt>, <tt>:minor</tt>, or <tt>:patch</tt>.
|
|
48
|
-
|
|
93
|
+
# [label] A prerelease label such as <tt>"beta"</tt>, or nil.
|
|
94
|
+
def next(segment, label = nil)
|
|
95
|
+
release = release_after(segment)
|
|
96
|
+
return release unless label
|
|
97
|
+
|
|
98
|
+
candidate = SemVer.parse("#{release}-#{label}.#{count_toward(release, label) + 1}")
|
|
99
|
+
return candidate if candidate > self
|
|
100
|
+
|
|
101
|
+
raise ArgumentError, "#{candidate} would come before #{self}. SemVer compares prerelease labels as text, " \
|
|
102
|
+
"so pick a label that sorts after #{@prerelease.split('.').first}."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Compares versions as SemVer orders them: by segment, then a prerelease
|
|
106
|
+
# before its release, then by prerelease identifiers.
|
|
107
|
+
def <=>(other)
|
|
108
|
+
return unless other.is_a?(SemVer)
|
|
109
|
+
|
|
110
|
+
by_segments = @segments <=> other.segments
|
|
111
|
+
by_segments.zero? ? prerelease_order(other.prerelease) : by_segments
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns the git tag that names this version, such as <tt>v1.0.0-beta.1</tt>.
|
|
115
|
+
def to_tag
|
|
116
|
+
"v#{self}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns the version as RubyGems orders it. RubyGems writes
|
|
120
|
+
# <tt>1.0.0-beta.1</tt> as <tt>1.0.0.pre.beta.1</tt>, and puts it before
|
|
121
|
+
# <tt>1.0.0</tt>.
|
|
122
|
+
def to_gem_version
|
|
123
|
+
Gem::Version.new(to_s)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Returns the version as a string.
|
|
127
|
+
def to_s
|
|
128
|
+
base = @segments.join(".")
|
|
129
|
+
@prerelease ? "#{base}-#{@prerelease}" : base
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
protected attr_reader :segments, :prerelease # :nodoc:
|
|
133
|
+
|
|
134
|
+
private def release_after(segment)
|
|
49
135
|
index = SEGMENTS.index(segment)
|
|
50
136
|
raise ArgumentError, "Invalid segment: #{segment}" unless index
|
|
137
|
+
return SemVer.new(@segments) if @prerelease && @segments[(index + 1)..].all?(&:zero?)
|
|
51
138
|
|
|
52
139
|
new_segments = @segments.dup
|
|
53
140
|
new_segments[index] += 1
|
|
@@ -56,8 +143,38 @@ class SemVer
|
|
|
56
143
|
SemVer.new(new_segments)
|
|
57
144
|
end
|
|
58
145
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
146
|
+
# How many prereleases labelled +label+ this one has counted toward
|
|
147
|
+
# +release+: 1 for rc.1, none for beta.3 or for another version's rc.
|
|
148
|
+
private def count_toward(release, label)
|
|
149
|
+
return 0 unless @prerelease && @segments == release.segments
|
|
150
|
+
|
|
151
|
+
@prerelease.match(/\A#{Regexp.escape(label)}(?:\.(\d+))?\z/)&.[](1).to_i
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Numeric identifiers compare as numbers and come before words, and a
|
|
155
|
+
# prerelease that runs out of identifiers first comes first.
|
|
156
|
+
private def prerelease_order(theirs)
|
|
157
|
+
return 0 if @prerelease == theirs
|
|
158
|
+
return 1 unless @prerelease
|
|
159
|
+
return -1 unless theirs
|
|
160
|
+
|
|
161
|
+
ours = @prerelease.split(".")
|
|
162
|
+
theirs = theirs.split(".")
|
|
163
|
+
ours.zip(theirs) do |mine, other|
|
|
164
|
+
return 1 unless other
|
|
165
|
+
|
|
166
|
+
order = identifier_order(mine, other)
|
|
167
|
+
return order unless order.zero?
|
|
168
|
+
end
|
|
169
|
+
ours.size <=> theirs.size
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private def identifier_order(mine, other)
|
|
173
|
+
numbers = [mine, other].map { |identifier| identifier.to_i if identifier.match?(/\A\d+\z/) }
|
|
174
|
+
return numbers[0] <=> numbers[1] if numbers.all?
|
|
175
|
+
return -1 if numbers[0]
|
|
176
|
+
return 1 if numbers[1]
|
|
177
|
+
|
|
178
|
+
mine <=> other
|
|
62
179
|
end
|
|
63
180
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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 project on SourceHut, found from its refs link, such as
|
|
9
|
+
# https://git.sr.ht/~me/gem/refs/HEAD. SourceHut has no comparison view, so
|
|
10
|
+
# each link shows the tree at one ref.
|
|
11
|
+
class SourceHutProject < Data.define(:refs) # :nodoc:
|
|
12
|
+
LINK = %r{\A(.*?/refs/)HEAD\z}
|
|
13
|
+
|
|
14
|
+
def self.linked_from(link)
|
|
15
|
+
refs = link[LINK, 1]
|
|
16
|
+
new(refs:) if refs
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unreleased(_version)
|
|
20
|
+
"#{refs}HEAD"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def release(version)
|
|
24
|
+
"#{refs}v#{version}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
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_workflow"
|
|
9
|
+
|
|
10
|
+
# Before 1.0 there is no older series to patch, so every release happens on
|
|
11
|
+
# trunk and no release branch is cut.
|
|
12
|
+
class UnbranchedRelease < BumpWorkflow # :nodoc:
|
|
13
|
+
private def prepare
|
|
14
|
+
puts "Bumping #{@task}: #{@gem.version} -> #{target}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private def finalize
|
|
18
|
+
puts "\nCommitted. Push and run: bundle exec rake release"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -20,7 +20,7 @@ require "rdoc"
|
|
|
20
20
|
# Use it during release preparation.
|
|
21
21
|
class UnreleasedSection
|
|
22
22
|
# Regex to match the Unreleased section.
|
|
23
|
-
PATTERN = /^(## \[Unreleased\].*?)(
|
|
23
|
+
PATTERN = /^(## \[Unreleased\].*?)(?=^## \[\d|^\[Unreleased\]: |\z)/m
|
|
24
24
|
|
|
25
25
|
# Parses the Unreleased section from changelog content.
|
|
26
26
|
#
|
|
@@ -72,4 +72,22 @@ class UnreleasedSection
|
|
|
72
72
|
.map { |line| formatter.wrap(line, 72) }
|
|
73
73
|
.join("\n\n")
|
|
74
74
|
end
|
|
75
|
+
|
|
76
|
+
# Returns all changelog entry lines (lines starting with "- ")
|
|
77
|
+
def entries
|
|
78
|
+
@content.lines.select { |line| line.strip.start_with?("- ") }.map(&:strip)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns a new UnreleasedSection with entries removed that appear in the given list.
|
|
82
|
+
def without_entries(entries_to_remove)
|
|
83
|
+
return self if entries_to_remove.empty?
|
|
84
|
+
|
|
85
|
+
new_lines = @content.lines.reject do |line|
|
|
86
|
+
stripped = line.strip
|
|
87
|
+
entries_to_remove.include?(stripped)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A removed entry leaves the blank lines that set it apart.
|
|
91
|
+
self.class.new(new_lines.join.gsub(/\n{3,}/, "\n\n").strip)
|
|
92
|
+
end
|
|
75
93
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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 "rake/tasklib"
|
|
9
|
+
require "rubygems"
|
|
10
|
+
|
|
11
|
+
require_relative "bump/sem_ver"
|
|
12
|
+
require_relative "bump/release_from_trunk"
|
|
13
|
+
require_relative "bump/patch_release"
|
|
14
|
+
require_relative "bump/unbranched_release"
|
|
15
|
+
|
|
16
|
+
# The tasks that bump a gem's version.
|
|
17
|
+
class BumpTasks < Rake::TaskLib # :nodoc:
|
|
18
|
+
def initialize(gem)
|
|
19
|
+
super()
|
|
20
|
+
@gem = gem
|
|
21
|
+
define
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private def define
|
|
25
|
+
namespace :bump do
|
|
26
|
+
desc "Bump major version, or to a prerelease with a label (e.g. rake bump:major[beta])"
|
|
27
|
+
task :major, [:label] do |_, args|
|
|
28
|
+
BumpWorkflow.for(@gem, :major, args[:label]).call
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Bump minor version, or to a prerelease with a label (e.g. rake bump:minor[rc])"
|
|
32
|
+
task :minor, [:label] do |_, args|
|
|
33
|
+
BumpWorkflow.for(@gem, :minor, args[:label]).call
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Bump patch version, or to a prerelease with a label (e.g. rake bump:patch[rc])"
|
|
37
|
+
task :patch, [:label] do |_, args|
|
|
38
|
+
BumpWorkflow.for(@gem, :patch, args[:label]).call
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Set exact version (e.g. rake bump:exact[0.1.0])"
|
|
42
|
+
task :exact, [:version] do |_, args|
|
|
43
|
+
BumpWorkflow.to(@gem, SemVer.parse(args[:version]), :exact).call
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
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 "rake/tasklib"
|
|
9
|
+
|
|
10
|
+
# Cargo tasks for Rust extensions.
|
|
11
|
+
# Only registered if ext/ directory exists.
|
|
12
|
+
class CargoTasks < Rake::TaskLib # :nodoc:
|
|
13
|
+
def initialize
|
|
14
|
+
super
|
|
15
|
+
define
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private def define
|
|
19
|
+
if Dir.exist?("ext")
|
|
20
|
+
namespace :cargo do
|
|
21
|
+
desc "Run cargo fmt"
|
|
22
|
+
task :fmt do
|
|
23
|
+
ext_dirs = Dir.glob("ext/*").select { |d| File.directory?(d) && File.exist?("#{d}/Cargo.toml") }
|
|
24
|
+
ext_dirs.each do |dir|
|
|
25
|
+
sh "cd #{dir} && cargo fmt --all -- --check"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Run cargo clippy"
|
|
30
|
+
task :clippy do
|
|
31
|
+
ext_dirs = Dir.glob("ext/*").select { |d| File.directory?(d) && File.exist?("#{d}/Cargo.toml") }
|
|
32
|
+
ext_dirs.each do |dir|
|
|
33
|
+
sh "cd #{dir} && cargo clippy -- -D warnings"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Run cargo test"
|
|
38
|
+
task :test do
|
|
39
|
+
ext_dirs = Dir.glob("ext/*").select { |d| File.directory?(d) && File.exist?("#{d}/Cargo.toml") }
|
|
40
|
+
ext_dirs.each do |dir|
|
|
41
|
+
sh "cd #{dir} && cargo test"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
namespace :fix do
|
|
46
|
+
desc "Auto-fix Clippy warnings (most aggressive)"
|
|
47
|
+
task :clippy do
|
|
48
|
+
ext_dirs = Dir.glob("ext/*").select { |d| File.directory?(d) && File.exist?("#{d}/Cargo.toml") }
|
|
49
|
+
ext_dirs.each do |dir|
|
|
50
|
+
sh "cd #{dir} && cargo clippy --fix --allow-dirty --allow-staged"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
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 "pathname"
|
|
9
|
+
require_relative "source_file"
|
|
10
|
+
|
|
11
|
+
# All documentation files in the project.
|
|
12
|
+
#
|
|
13
|
+
# Projects contain many files. Only some are documentation: Ruby source with
|
|
14
|
+
# RDoc, Markdown guides, RDoc text files. Finding them manually is tedious.
|
|
15
|
+
# Missing files means missing broken links.
|
|
16
|
+
#
|
|
17
|
+
# Documentation enumerates all relevant files. It excludes vendor and temp
|
|
18
|
+
# directories. It wraps each path in a SourceFile for link extraction.
|
|
19
|
+
#
|
|
20
|
+
# === Example
|
|
21
|
+
#
|
|
22
|
+
# docs = Documentation.new("/project/root")
|
|
23
|
+
# docs.each do |file|
|
|
24
|
+
# file.links.each { |link| puts link.raw }
|
|
25
|
+
# end
|
|
26
|
+
# docs.count # => 392
|
|
27
|
+
#
|
|
28
|
+
class Documentation
|
|
29
|
+
include Enumerable
|
|
30
|
+
|
|
31
|
+
# File extensions to scan for links.
|
|
32
|
+
EXTENSIONS = %w[rb md rdoc].freeze
|
|
33
|
+
|
|
34
|
+
# Directories to skip.
|
|
35
|
+
EXCLUDES = %w[/vendor/ /tmp/].freeze
|
|
36
|
+
|
|
37
|
+
# Creates a new Documentation collection.
|
|
38
|
+
#
|
|
39
|
+
# [root] Project root directory to scan.
|
|
40
|
+
def initialize(root)
|
|
41
|
+
@root = Pathname.new(root)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Yields each SourceFile in the project.
|
|
45
|
+
def each(&)
|
|
46
|
+
files.each(&)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private def files # :nodoc:
|
|
50
|
+
@files ||= EXTENSIONS.flat_map { |ext| glob(ext) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def glob(extension) # :nodoc:
|
|
54
|
+
Dir.glob(@root.join("**/*.#{extension}")).filter_map do |path|
|
|
55
|
+
next if EXCLUDES.any? { |exclude| path.include?(exclude) }
|
|
56
|
+
SourceFile.new(path, @root)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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 "../problem"
|
|
9
|
+
|
|
10
|
+
# A <tt>file://</tt> URL in documentation.
|
|
11
|
+
#
|
|
12
|
+
# IDEs generate these links for local file access. They work on your machine.
|
|
13
|
+
# They break for everyone else. Published docs cannot contain local paths.
|
|
14
|
+
#
|
|
15
|
+
# FileUrl always returns a Problem. There is no valid use case for
|
|
16
|
+
# <tt>file://</tt> URLs in published documentation.
|
|
17
|
+
#
|
|
18
|
+
# === Example
|
|
19
|
+
#
|
|
20
|
+
# link = FileUrl.new("file:///path/to/file.rb", 10, source_file)
|
|
21
|
+
# link.problem(root) # => Problem (always)
|
|
22
|
+
#
|
|
23
|
+
class FileUrl < Link
|
|
24
|
+
# Returns a Problem. <tt>file://</tt> URLs never work in published docs.
|
|
25
|
+
#
|
|
26
|
+
# [_root] Unused. Present for interface compatibility.
|
|
27
|
+
def problem(_root)
|
|
28
|
+
Problem.new(self, "file:// URLs won't work when published")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
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 "pathname"
|
|
9
|
+
|
|
10
|
+
require_relative "../problem"
|
|
11
|
+
|
|
12
|
+
# A relative path to a local file.
|
|
13
|
+
#
|
|
14
|
+
# Documentation references sibling files: images, other docs, source code.
|
|
15
|
+
# Files get renamed. Directories restructure. These paths silently break.
|
|
16
|
+
#
|
|
17
|
+
# RelativePath resolves the path against the project root and checks if the
|
|
18
|
+
# target exists. It also handles RDoc's HTML naming convention (converting
|
|
19
|
+
# <tt>foo_rb.html</tt> back to <tt>foo.rb</tt> for validation).
|
|
20
|
+
#
|
|
21
|
+
# === Example
|
|
22
|
+
#
|
|
23
|
+
# link = RelativePath.new("../images/diagram.png", 20, source_file)
|
|
24
|
+
# link.problem(root) # => nil (file exists) or Problem (missing)
|
|
25
|
+
#
|
|
26
|
+
class RelativePath < Link
|
|
27
|
+
# Returns a Problem if the target file does not exist. Returns <tt>nil</tt>
|
|
28
|
+
# if the path resolves to an existing file or directory.
|
|
29
|
+
#
|
|
30
|
+
# [root] Project root directory for resolving absolute paths.
|
|
31
|
+
def problem(root)
|
|
32
|
+
root = Pathname.new(root)
|
|
33
|
+
resolved = resolve(root)
|
|
34
|
+
return nil unless resolved
|
|
35
|
+
|
|
36
|
+
Problem.new(self, "File not found: #{resolved.relative_path_from(root)}") unless exists?(resolved)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private def exists?(resolved) # :nodoc:
|
|
40
|
+
return true if resolved.exist?
|
|
41
|
+
|
|
42
|
+
# RDoc generates foo_rb.html from foo.rb
|
|
43
|
+
if raw.end_with?("_rb.html")
|
|
44
|
+
source = Pathname.new(resolved.to_s.sub(/_rb\.html$/, ".rb"))
|
|
45
|
+
return true if source.exist?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
resolved.directory? if raw.end_with?("/")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private def resolve(root) # :nodoc:
|
|
52
|
+
path = raw.split("#").first&.split("?")&.first
|
|
53
|
+
return nil if path.nil? || path.empty? || path.length < 4
|
|
54
|
+
|
|
55
|
+
if path.start_with?("/")
|
|
56
|
+
root.join(path.delete_prefix("/"))
|
|
57
|
+
else
|
|
58
|
+
source_file.path.dirname.join(path)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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 "net/http"
|
|
9
|
+
require "uri"
|
|
10
|
+
|
|
11
|
+
require_relative "../problem"
|
|
12
|
+
|
|
13
|
+
# An HTTP or HTTPS URL in documentation.
|
|
14
|
+
#
|
|
15
|
+
# External links rot. Servers go offline. Pages move. A link that worked last
|
|
16
|
+
# year may 404 today. Manual verification is slow and error-prone.
|
|
17
|
+
#
|
|
18
|
+
# WebUrl checks reachability by making a HEAD request. It returns a Problem if
|
|
19
|
+
# the server responds with an error or is unreachable.
|
|
20
|
+
#
|
|
21
|
+
# === Example
|
|
22
|
+
#
|
|
23
|
+
# link = WebUrl.new("https://example.com", 15, source_file)
|
|
24
|
+
# link.web? # => true
|
|
25
|
+
# link.problem(root) # => nil (site is up) or Problem (site is down)
|
|
26
|
+
#
|
|
27
|
+
class WebUrl < Link
|
|
28
|
+
# Whether this link points to a web URL. Always <tt>true</tt> for WebUrl.
|
|
29
|
+
def web?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns a Problem if the URL is unreachable. Returns <tt>nil</tt> if the
|
|
34
|
+
# server responds with a 2xx or 3xx status.
|
|
35
|
+
#
|
|
36
|
+
# [_root] Unused. Present for interface compatibility.
|
|
37
|
+
def problem(_root)
|
|
38
|
+
Problem.new(self, "URL returned error or is unreachable") unless reachable?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private def reachable? # :nodoc:
|
|
42
|
+
uri = URI.parse(raw)
|
|
43
|
+
return false unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
44
|
+
|
|
45
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
46
|
+
http.use_ssl = uri.scheme == "https"
|
|
47
|
+
http.open_timeout = 5
|
|
48
|
+
http.read_timeout = 5
|
|
49
|
+
|
|
50
|
+
response = http.request_head(uri.request_uri)
|
|
51
|
+
response.code.to_i < 400
|
|
52
|
+
rescue
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
# Base class for links found in documentation.
|
|
9
|
+
#
|
|
10
|
+
# Documentation contains links: URLs, file paths, images. These links break.
|
|
11
|
+
# Pages move. Files get renamed. Servers go offline. Manual checking is tedious.
|
|
12
|
+
#
|
|
13
|
+
# Link is the base type. It holds the raw text, line number, and source file.
|
|
14
|
+
# Subclasses (FileUrl, WebUrl, RelativePath) implement <tt>problem</tt> to
|
|
15
|
+
# detect specific breakage.
|
|
16
|
+
#
|
|
17
|
+
# Use <tt>Link.build</tt> to create the correct subclass based on the raw text.
|
|
18
|
+
#
|
|
19
|
+
# === Example
|
|
20
|
+
#
|
|
21
|
+
# link = Link.build("https://example.com", 42, source_file)
|
|
22
|
+
# link.web? # => true
|
|
23
|
+
# link.problem(root) # => nil or Problem
|
|
24
|
+
#
|
|
25
|
+
class Link < Data.define(:raw, :line, :source_file)
|
|
26
|
+
# Creates the appropriate Link subclass based on the raw text.
|
|
27
|
+
#
|
|
28
|
+
# [raw] The raw link text extracted from the file.
|
|
29
|
+
# [line] Line number where the link appears.
|
|
30
|
+
# [source_file] The SourceFile containing this link.
|
|
31
|
+
def self.build(raw, line, source_file)
|
|
32
|
+
if raw.start_with?("file://")
|
|
33
|
+
FileUrl.new(raw, line, source_file)
|
|
34
|
+
elsif raw.start_with?("https://", "http://")
|
|
35
|
+
WebUrl.new(raw, line, source_file)
|
|
36
|
+
else
|
|
37
|
+
RelativePath.new(raw, line, source_file)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Whether this link points to a web URL.
|
|
42
|
+
#
|
|
43
|
+
# WebUrl overrides this to return <tt>true</tt>. Other link types return
|
|
44
|
+
# <tt>false</tt>. The audit uses this to decide whether to skip verification.
|
|
45
|
+
def web?
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
require_relative "link/file_url"
|
|
51
|
+
require_relative "link/web_url"
|
|
52
|
+
require_relative "link/relative_path"
|