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
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
+
require_relative "sem_ver"
|
|
9
|
+
|
|
8
10
|
# Represents a file that contains a version number.
|
|
9
11
|
#
|
|
10
12
|
# Gems have version numbers in multiple places: version.rb, Cargo.toml, etc.
|
|
@@ -16,27 +18,16 @@
|
|
|
16
18
|
#
|
|
17
19
|
# [path] The file path.
|
|
18
20
|
# [pattern] A regex with lookarounds to match the version string.
|
|
19
|
-
# [primary] Whether this is the primary source of truth.
|
|
20
21
|
#
|
|
21
22
|
# === Example
|
|
22
23
|
#
|
|
23
24
|
# manifest = Manifest.new(
|
|
24
25
|
# path: "lib/my_gem/version.rb",
|
|
25
|
-
# pattern: /(?<=VERSION = ")[^"]+(?=")
|
|
26
|
-
# primary: true
|
|
26
|
+
# pattern: /(?<=VERSION = ")[^"]+(?=")/
|
|
27
27
|
# )
|
|
28
28
|
# manifest.version.to_s # => "1.2.3"
|
|
29
29
|
#
|
|
30
|
-
class Manifest < Data.define(:path, :pattern
|
|
31
|
-
# Creates a new Manifest.
|
|
32
|
-
#
|
|
33
|
-
# [path] The file path.
|
|
34
|
-
# [pattern] A regex with lookarounds to match the version string.
|
|
35
|
-
# [primary] Whether this is the primary source of truth.
|
|
36
|
-
def initialize(path:, pattern:, primary: false)
|
|
37
|
-
super
|
|
38
|
-
end
|
|
39
|
-
|
|
30
|
+
class Manifest < Data.define(:path, :pattern)
|
|
40
31
|
# Reads the file content.
|
|
41
32
|
def read
|
|
42
33
|
File.read(path)
|
|
@@ -44,11 +35,12 @@ class Manifest < Data.define(:path, :pattern, :primary)
|
|
|
44
35
|
|
|
45
36
|
# Returns the current version from this manifest.
|
|
46
37
|
def version
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
raise "Version missing in manifest #{path}" unless match
|
|
38
|
+
SemVer.parse(written)
|
|
39
|
+
end
|
|
50
40
|
|
|
51
|
-
|
|
41
|
+
# Returns the version exactly as this manifest writes it.
|
|
42
|
+
def written
|
|
43
|
+
read[pattern] or raise "Version missing in manifest #{path}"
|
|
52
44
|
end
|
|
53
45
|
|
|
54
46
|
# Writes a new version to this manifest.
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
# The Rust extension of a pure Ruby gem, which has none.
|
|
9
|
+
class NoCrate # :nodoc:
|
|
10
|
+
def manifests
|
|
11
|
+
[]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def refresh
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def release_native_gems(version:, sha:)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
# The links of a changelog that has none yet, as the scaffold writes it.
|
|
9
|
+
class NoLinks # :nodoc:
|
|
10
|
+
def release(_version)
|
|
11
|
+
self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# With no links, the history runs to the end of the changelog.
|
|
15
|
+
def start_in(content)
|
|
16
|
+
content.length
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def footer
|
|
20
|
+
""
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
# PatchRelease performs a patch release on the current release branch.
|
|
11
|
+
class PatchRelease < BumpWorkflow
|
|
12
|
+
private def assert_can_bump!
|
|
13
|
+
return unless @repository.on_trunk?
|
|
14
|
+
|
|
15
|
+
raise ArgumentError, "Cannot bump:#{@task} from trunk. Use a release branch."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private def prepare
|
|
19
|
+
puts "Bumping #{@task}: #{@gem.version} -> #{target}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private def finalize
|
|
23
|
+
puts "\nCommitted. Push and run: bundle exec rake release"
|
|
24
|
+
end
|
|
25
|
+
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
|
+
# ReleaseBranch represents a release/X.Y branch for a version series.
|
|
9
|
+
class ReleaseBranch < Data.define(:major, :minor) # :nodoc:
|
|
10
|
+
def self.for_version(semver)
|
|
11
|
+
new(semver.major, semver.minor)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name
|
|
15
|
+
"release/#{major}.#{minor}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
require_relative "release_branch"
|
|
10
|
+
|
|
11
|
+
# ReleaseFromTrunk creates a release branch, releases there, syncs trunk, returns to release branch.
|
|
12
|
+
class ReleaseFromTrunk < BumpWorkflow
|
|
13
|
+
private def prepare
|
|
14
|
+
@branch = ReleaseBranch.for_version(target)
|
|
15
|
+
|
|
16
|
+
puts "Creating release branch: #{@branch.name}"
|
|
17
|
+
puts "Bumping #{@task}: #{@gem.version} -> #{target}"
|
|
18
|
+
|
|
19
|
+
@repository.create_branch(@branch.name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private def release_on_branch
|
|
23
|
+
super
|
|
24
|
+
puts "\nCommitted on #{@branch.name}."
|
|
25
|
+
|
|
26
|
+
# Capture for trunk import
|
|
27
|
+
@released_changelog_content = File.read("CHANGELOG.md")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private def finalize
|
|
31
|
+
sync_trunk
|
|
32
|
+
return_to_release_branch
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private def sync_trunk
|
|
36
|
+
@repository.checkout("trunk")
|
|
37
|
+
trunk_changelog = Changelog.new
|
|
38
|
+
trunk_changelog.import_release(target, @released_changelog_content)
|
|
39
|
+
@gem.update_version(target)
|
|
40
|
+
@repository.commit_all("chore: import v#{target} to trunk")
|
|
41
|
+
puts "Committed on trunk."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private def return_to_release_branch
|
|
45
|
+
@repository.checkout(@branch.name)
|
|
46
|
+
puts "\nBack on #{@branch.name}. Review, push both branches, then: bundle exec rake release"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
require "rake/file_utils_ext"
|
|
10
|
+
require "tmpdir"
|
|
11
|
+
|
|
12
|
+
class Repository # :nodoc:
|
|
13
|
+
# Releasing shows each git command it runs, as rake tasks do.
|
|
14
|
+
include Rake::FileUtilsExt
|
|
15
|
+
|
|
16
|
+
TRUNK = "trunk"
|
|
17
|
+
STABLE = "stable"
|
|
18
|
+
|
|
19
|
+
class Author < Data.define(:name, :email) # :nodoc:
|
|
20
|
+
def to_s
|
|
21
|
+
"#{name} <#{email}>"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The identity your next commit would carry, from git config or the
|
|
26
|
+
# GIT_AUTHOR_* variables. Without useConfigOnly, git would guess an email
|
|
27
|
+
# from the hostname, and that guess would end up in copyright headers.
|
|
28
|
+
def author
|
|
29
|
+
ident, status = Open3.capture2("git", "-c", "user.useConfigOnly=true", "var", "GIT_AUTHOR_IDENT", err: File::NULL)
|
|
30
|
+
name, email = ident.match(/\A(.+) <(.*)> \d+ [+-]\d{4}\n?\z/)&.captures
|
|
31
|
+
return Author.new(name:, email:) if status.success? && name && email
|
|
32
|
+
|
|
33
|
+
raise ArgumentError, "Git doesn't know who you are. Run: git config --global user.name \"Your Name\" " \
|
|
34
|
+
"and git config --global user.email you@example.com"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def on_trunk? = current_branch == TRUNK
|
|
38
|
+
|
|
39
|
+
def current_branch
|
|
40
|
+
git("branch", "--show-current").strip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tracked_files(pattern)
|
|
44
|
+
git("ls-files", pattern).split("\n")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The tracked files a syntax covers, under the given paths, or everywhere
|
|
48
|
+
# it looks by default. A path that isn't a directory is taken as it is, if
|
|
49
|
+
# it's written in the syntax. In a git pathspec * crosses directories, so
|
|
50
|
+
# dir/*.rb reaches every depth, while dir/**/*.rb would skip the files
|
|
51
|
+
# directly inside dir.
|
|
52
|
+
def tracked_files_for(paths, syntax)
|
|
53
|
+
return syntax.tracked_patterns.flat_map { |pattern| tracked_files(pattern) }.uniq if paths.empty?
|
|
54
|
+
|
|
55
|
+
paths.flat_map do |path|
|
|
56
|
+
if File.directory?(path)
|
|
57
|
+
syntax.extensions.flat_map { |extension| tracked_files("#{path}/*.#{extension}") }
|
|
58
|
+
elsif syntax.extensions.include?(File.extname(path).delete_prefix("."))
|
|
59
|
+
[path]
|
|
60
|
+
else
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Added, copied, modified, or renamed since HEAD, staged or not, plus new
|
|
67
|
+
# untracked files. The staged diff also covers a repository with no commits.
|
|
68
|
+
def changed_files
|
|
69
|
+
[
|
|
70
|
+
git("diff", "--name-only", "--diff-filter=ACMR", "HEAD"),
|
|
71
|
+
git("diff", "--name-only", "--cached", "--diff-filter=ACMR"),
|
|
72
|
+
git("ls-files", "--others", "--exclude-standard"),
|
|
73
|
+
].flat_map { |output| output.split("\n") }.uniq
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def latest_tag
|
|
77
|
+
git("describe", "--tags", "--abbrev=0").strip
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# An annotated tag is an object of its own, and a branch can only point at
|
|
81
|
+
# a commit.
|
|
82
|
+
def released_commit(tag)
|
|
83
|
+
git("rev-parse", "#{tag}^{}").strip
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The file as the stable branch has it, or nil when there's no such branch
|
|
87
|
+
# or file.
|
|
88
|
+
def stable_file(path)
|
|
89
|
+
text = git("show", "#{STABLE}:#{path}")
|
|
90
|
+
text unless text.empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Stable is reset to each release rather than fast-forwarded, so origin's
|
|
94
|
+
# copy is overwritten. Git can't tell that a bare name is meant as a branch
|
|
95
|
+
# when origin has none yet, as after the scaffold, so the push names it in
|
|
96
|
+
# full.
|
|
97
|
+
def move_stable(commit)
|
|
98
|
+
sh "git", "branch", "-f", STABLE, commit
|
|
99
|
+
sh "git", "push", "--force", "origin", "#{commit}:refs/heads/#{STABLE}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Deletes the tag here and on origin.
|
|
103
|
+
def delete_tag(name)
|
|
104
|
+
sh "git", "tag", "-d", name
|
|
105
|
+
sh "git", "push", "origin", ":refs/tags/#{name}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Tags HEAD here and on origin.
|
|
109
|
+
def tag_head(name)
|
|
110
|
+
sh "git", "tag", name, "HEAD"
|
|
111
|
+
sh "git", "push", "origin", name
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create_branch(name)
|
|
115
|
+
system("git", "checkout", "-b", name, exception: true)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def checkout(name)
|
|
119
|
+
system("git", "checkout", name, exception: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def assert_pristine!
|
|
123
|
+
return if git("status", "--porcelain").strip.empty?
|
|
124
|
+
|
|
125
|
+
raise ArgumentError, "Working tree is not clean. Commit or stash changes first."
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def commit_all(message)
|
|
129
|
+
msg_file = File.join(Dir.tmpdir, "ratatui_ruby_commit_msg_#{$$}.txt")
|
|
130
|
+
system("git", "add", "-A", exception: true)
|
|
131
|
+
File.write(msg_file, message)
|
|
132
|
+
system("git", "commit", "-F", msg_file, exception: true)
|
|
133
|
+
ensure
|
|
134
|
+
File.delete(msg_file) if msg_file && File.exist?(msg_file)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Runs git directly rather than through a shell, whose quoting and
|
|
138
|
+
# redirection differ on Windows, and keeps its complaints out of the
|
|
139
|
+
# output it answers with.
|
|
140
|
+
private def git(*)
|
|
141
|
+
Open3.capture2("git", *, err: File::NULL).first
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -5,73 +5,79 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
raise
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
8
|
+
require "rubygems"
|
|
9
|
+
require_relative "manifest"
|
|
10
|
+
require_relative "rust_crate"
|
|
11
|
+
|
|
12
|
+
# A gem, found from its files wherever you haven't said otherwise.
|
|
13
|
+
class RubyGem < Data.define(:name, :gemspec_file, :version_file) # :nodoc:
|
|
14
|
+
VERSION = /(?<=VERSION = ")[^"]+(?=")/
|
|
15
|
+
|
|
16
|
+
def self.found(gemspec_file: nil, name: nil, version_file: nil)
|
|
17
|
+
gemspec_file ||= only_gemspec
|
|
18
|
+
name ||= File.basename(gemspec_file, ".gemspec")
|
|
19
|
+
new(name:, gemspec_file:, version_file: version_file || version_file_of(name))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.only_gemspec
|
|
23
|
+
gemspecs = Dir.glob("*.gemspec")
|
|
24
|
+
raise RatatuiRuby::Devtools::Error, "No *.gemspec found in #{Dir.pwd}" if gemspecs.empty?
|
|
25
|
+
raise RatatuiRuby::Devtools::Error, "Multiple gemspecs found: #{gemspecs.join(', ')}" if gemspecs.size > 1
|
|
26
|
+
|
|
27
|
+
gemspecs.first
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A gem named my_gem-kit keeps its version in lib/my_gem/kit/.
|
|
31
|
+
def self.version_file_of(name)
|
|
32
|
+
path = "lib/#{name.tr('-', '/')}/version.rb"
|
|
33
|
+
return path if File.exist?(path)
|
|
34
|
+
|
|
35
|
+
Dir.glob("lib/**/version.rb").first || path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# RubyGems caches a gemspec by the path it was loaded from, so a relative
|
|
39
|
+
# path would return another gem's spec loaded from another directory.
|
|
40
|
+
def spec
|
|
41
|
+
Gem::Specification.load(File.expand_path(gemspec_file))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def crate
|
|
45
|
+
RustCrate.of(name)
|
|
29
46
|
end
|
|
30
47
|
|
|
31
|
-
# Returns the current version from the primary manifest.
|
|
32
48
|
def version
|
|
33
|
-
|
|
49
|
+
manifest.version
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Bundler tags a release with the version as written, and the release
|
|
53
|
+
# tasks look for its SemVer tag. A bump reads 1.2 as 1.2.0 because it
|
|
54
|
+
# rewrites the file, but a release tags the file as it stands.
|
|
55
|
+
def release_version
|
|
56
|
+
written = manifest.written
|
|
57
|
+
version.tap do |full|
|
|
58
|
+
raise ArgumentError, "#{version_file} says #{written}. Write it as #{full} before releasing." if full.to_s != written
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# What the stable branch released, or nil before the first release. Stable
|
|
63
|
+
# can still hold a version from before the gem used SemVer, which only
|
|
64
|
+
# RubyGems reads.
|
|
65
|
+
def stable_version(repository)
|
|
66
|
+
written = repository.stable_file(version_file)&.[](VERSION)
|
|
67
|
+
Gem::Version.new(written) if written
|
|
34
68
|
end
|
|
35
69
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
# [segment] One of <tt>:major</tt>, <tt>:minor</tt>, or <tt>:patch</tt>.
|
|
42
|
-
def bump(segment)
|
|
43
|
-
target = version.next(segment)
|
|
44
|
-
commit_message = @changelog.commit_message(target)
|
|
45
|
-
|
|
46
|
-
puts "Bumping #{segment}: #{version} -> #{target}"
|
|
47
|
-
@changelog.release(target)
|
|
48
|
-
@manifests.each { |manifest| manifest.write(target) }
|
|
49
|
-
@lockfile.refresh
|
|
50
|
-
|
|
51
|
-
puts_commit_message(commit_message)
|
|
70
|
+
def update_version(target)
|
|
71
|
+
[manifest, *crate.manifests].each { |manifest| manifest.write(target) }
|
|
72
|
+
crate.refresh
|
|
73
|
+
refresh_bundler_lockfile
|
|
52
74
|
end
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Updates all manifests, refreshes lockfiles, and updates the changelog.
|
|
57
|
-
# Prints a suggested commit message.
|
|
58
|
-
#
|
|
59
|
-
# [version_string] A version string like <tt>"1.2.3"</tt>.
|
|
60
|
-
def set(version_string)
|
|
61
|
-
target = SemVer.parse(version_string)
|
|
62
|
-
commit_message = @changelog.commit_message(target)
|
|
63
|
-
|
|
64
|
-
puts "Setting version: #{version} -> #{target}"
|
|
65
|
-
@changelog.release(target)
|
|
66
|
-
@manifests.each { |manifest| manifest.write(target) }
|
|
67
|
-
@lockfile.refresh
|
|
68
|
-
|
|
69
|
-
puts_commit_message(commit_message)
|
|
76
|
+
private def manifest
|
|
77
|
+
Manifest.new(path: version_file, pattern: VERSION)
|
|
70
78
|
end
|
|
71
79
|
|
|
72
|
-
private def
|
|
73
|
-
|
|
74
|
-
puts message
|
|
75
|
-
puts "=" * 80
|
|
80
|
+
private def refresh_bundler_lockfile
|
|
81
|
+
system("bundle install", exception: true)
|
|
76
82
|
end
|
|
77
83
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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 "cargo_lockfile"
|
|
9
|
+
require_relative "manifest"
|
|
10
|
+
require_relative "no_crate"
|
|
11
|
+
require_relative "../release/native_gem_version"
|
|
12
|
+
|
|
13
|
+
# A gem's Rust extension. The scaffold names its directory and package after
|
|
14
|
+
# the gem with underscores, as Cargo wants; older gems used the gem's name.
|
|
15
|
+
class RustCrate < Data.define(:dir) # :nodoc:
|
|
16
|
+
VERSION = /(?<=^version = ")[^"]+(?=")/
|
|
17
|
+
|
|
18
|
+
def self.of(gem_name)
|
|
19
|
+
dir = [gem_name.tr("-", "_"), gem_name].map { |name| "ext/#{name}" }.find { |path| File.exist?("#{path}/Cargo.toml") }
|
|
20
|
+
dir ? new(dir:) : NoCrate.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def manifests
|
|
24
|
+
[Manifest.new(path: "#{dir}/Cargo.toml", pattern: VERSION)]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def refresh
|
|
28
|
+
lockfile.refresh
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def lockfile
|
|
32
|
+
CargoLockfile.new(path: "#{dir}/Cargo.lock", dir:, name: package)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def release_native_gems(version:, sha:)
|
|
36
|
+
NativeGemVersion.new(version:, sha:).release
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private def package
|
|
40
|
+
File.read("#{dir}/Cargo.toml")[/^name = "([^"]+)"/, 1]
|
|
41
|
+
end
|
|
42
|
+
end
|