ratatui_ruby 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +2 -2
- data/.builds/ruby-3.3.yml +2 -2
- data/.builds/ruby-3.4.yml +2 -2
- data/.builds/ruby-4.0.0.yml +2 -2
- data/.pre-commit-config.yaml +1 -1
- data/AGENTS.md +3 -3
- data/CHANGELOG.md +77 -1
- data/LICENSES/LGPL-3.0-or-later.txt +304 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/README.md +33 -5
- data/Rakefile +1 -1
- data/doc/concepts/application_architecture.md +44 -3
- data/doc/concepts/application_testing.md +43 -1
- data/doc/concepts/async.md +32 -2
- data/doc/concepts/custom_widgets.md +247 -0
- data/doc/concepts/event_handling.md +32 -3
- data/doc/concepts/interactive_design.md +32 -2
- data/doc/contributors/auditing/parity.md +7 -1
- data/doc/contributors/design/ruby_frontend.md +85 -1
- data/doc/contributors/design/rust_backend.md +67 -1
- data/doc/contributors/developing_examples.md +56 -2
- data/doc/contributors/documentation_style.md +20 -3
- data/doc/contributors/future_work.md +169 -0
- data/doc/contributors/index.md +1 -1
- data/doc/contributors/v1.0.0_blockers.md +19 -185
- data/doc/getting_started/quickstart.md +22 -4
- data/doc/getting_started/why.md +1 -1
- data/doc/index.md +2 -1
- data/doc/troubleshooting/debugging.md +32 -2
- data/doc/troubleshooting/terminal_limitations.md +8 -2
- data/doc/troubleshooting/tui_output.md +42 -0
- data/examples/app_all_events/README.md +14 -2
- data/examples/app_all_events/app.rb +1 -1
- data/examples/app_all_events/model/app_model.rb +1 -1
- data/examples/app_all_events/model/event_color_cycle.rb +1 -1
- data/examples/app_all_events/model/event_entry.rb +1 -1
- data/examples/app_all_events/model/msg.rb +1 -1
- data/examples/app_all_events/model/timestamp.rb +1 -1
- data/examples/app_all_events/update.rb +1 -1
- data/examples/app_all_events/view/app_view.rb +1 -1
- data/examples/app_all_events/view/controls_view.rb +1 -1
- data/examples/app_all_events/view/counts_view.rb +1 -1
- data/examples/app_all_events/view/live_view.rb +1 -1
- data/examples/app_all_events/view/log_view.rb +1 -1
- data/examples/app_all_events/view.rb +1 -1
- data/examples/app_color_picker/README.md +20 -2
- data/examples/app_color_picker/app.rb +1 -1
- data/examples/app_color_picker/clipboard.rb +1 -1
- data/examples/app_color_picker/color.rb +1 -1
- data/examples/app_color_picker/controls.rb +1 -1
- data/examples/app_color_picker/copy_dialog.rb +1 -1
- data/examples/app_color_picker/export_pane.rb +1 -1
- data/examples/app_color_picker/harmony.rb +1 -1
- data/examples/app_color_picker/input.rb +1 -1
- data/examples/app_color_picker/main_container.rb +1 -1
- data/examples/app_color_picker/palette.rb +1 -1
- data/examples/app_login_form/README.md +8 -2
- data/examples/app_login_form/app.rb +1 -1
- data/examples/app_stateful_interaction/README.md +2 -2
- data/examples/app_stateful_interaction/app.rb +71 -17
- data/examples/timeout_demo.rb +1 -1
- data/examples/verify_quickstart_dsl/README.md +6 -0
- data/examples/verify_quickstart_dsl/app.rb +3 -3
- data/examples/verify_quickstart_layout/README.md +6 -0
- data/examples/verify_quickstart_layout/app.rb +3 -3
- data/examples/verify_quickstart_lifecycle/README.md +6 -0
- data/examples/verify_quickstart_lifecycle/app.rb +3 -3
- data/examples/verify_readme_usage/README.md +6 -0
- data/examples/verify_readme_usage/app.rb +3 -3
- data/examples/widget_barchart/README.md +6 -0
- data/examples/widget_barchart/app.rb +2 -2
- data/examples/widget_block/README.md +7 -1
- data/examples/widget_block/app.rb +2 -2
- data/examples/widget_box/README.md +6 -0
- data/examples/widget_box/app.rb +9 -6
- data/examples/widget_calendar/README.md +6 -0
- data/examples/widget_calendar/app.rb +2 -2
- data/examples/widget_canvas/README.md +4 -0
- data/examples/widget_canvas/app.rb +2 -2
- data/examples/widget_cell/README.md +6 -0
- data/examples/widget_cell/app.rb +2 -3
- data/examples/widget_center/README.md +4 -0
- data/examples/widget_center/app.rb +2 -2
- data/examples/widget_chart/README.md +6 -0
- data/examples/widget_chart/app.rb +2 -2
- data/examples/widget_gauge/README.md +6 -0
- data/examples/widget_gauge/app.rb +2 -2
- data/examples/widget_layout_split/README.md +6 -0
- data/examples/widget_layout_split/app.rb +9 -3
- data/examples/widget_line_gauge/README.md +6 -0
- data/examples/widget_line_gauge/app.rb +2 -2
- data/examples/widget_list/README.md +6 -0
- data/examples/widget_list/app.rb +2 -2
- data/examples/widget_map/README.md +8 -2
- data/examples/widget_map/app.rb +2 -2
- data/examples/widget_overlay/README.md +7 -1
- data/examples/widget_overlay/app.rb +2 -2
- data/examples/widget_popup/README.md +6 -0
- data/examples/widget_popup/app.rb +2 -2
- data/examples/widget_ratatui_logo/README.md +6 -0
- data/examples/widget_ratatui_logo/app.rb +2 -3
- data/examples/widget_ratatui_mascot/README.md +6 -0
- data/examples/widget_ratatui_mascot/app.rb +2 -2
- data/examples/widget_rect/README.md +12 -0
- data/examples/widget_rect/app.rb +40 -26
- data/examples/widget_render/README.md +6 -0
- data/examples/widget_render/app.rb +2 -2
- data/examples/widget_render/app.rbs +41 -0
- data/examples/widget_rich_text/README.md +6 -0
- data/examples/widget_rich_text/app.rb +2 -2
- data/examples/widget_scroll_text/README.md +6 -0
- data/examples/widget_scroll_text/app.rb +2 -2
- data/examples/widget_scrollbar/README.md +6 -0
- data/examples/widget_scrollbar/app.rb +2 -2
- data/examples/widget_sparkline/README.md +6 -0
- data/examples/widget_sparkline/app.rb +2 -2
- data/examples/widget_style_colors/README.md +6 -0
- data/examples/widget_style_colors/app.rb +2 -2
- data/examples/widget_table/README.md +8 -2
- data/examples/widget_table/app.rb +2 -2
- data/examples/widget_tabs/README.md +6 -0
- data/examples/widget_tabs/app.rb +2 -2
- data/examples/widget_text_width/README.md +6 -0
- data/examples/widget_text_width/app.rb +4 -4
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/extconf.rb +2 -2
- data/ext/ratatui_ruby/src/events.rs +1 -0
- data/ext/ratatui_ruby/src/rendering.rs +1 -1
- data/ext/ratatui_ruby/src/style.rs +0 -8
- data/ext/ratatui_ruby/src/widgets/chart.rs +0 -118
- data/ext/ratatui_ruby/src/widgets/list_state.rs +36 -0
- data/lib/ratatui_ruby/buffer/cell.rb +34 -2
- data/lib/ratatui_ruby/buffer.rb +2 -2
- data/lib/ratatui_ruby/cell.rb +34 -2
- data/lib/ratatui_ruby/event/focus_gained.rb +26 -2
- data/lib/ratatui_ruby/event/focus_lost.rb +26 -2
- data/lib/ratatui_ruby/event/key/character.rb +18 -2
- data/lib/ratatui_ruby/event/key/media.rb +2 -2
- data/lib/ratatui_ruby/event/key/modifier.rb +10 -2
- data/lib/ratatui_ruby/event/key/navigation.rb +2 -2
- data/lib/ratatui_ruby/event/key/system.rb +2 -2
- data/lib/ratatui_ruby/event/key.rb +114 -2
- data/lib/ratatui_ruby/event/mouse.rb +42 -2
- data/lib/ratatui_ruby/event/none.rb +10 -2
- data/lib/ratatui_ruby/event/paste.rb +34 -2
- data/lib/ratatui_ruby/event/resize.rb +34 -2
- data/lib/ratatui_ruby/event/sync.rb +52 -0
- data/lib/ratatui_ruby/event.rb +32 -2
- data/lib/ratatui_ruby/frame.rb +74 -2
- data/lib/ratatui_ruby/layout/constraint.rb +193 -2
- data/lib/ratatui_ruby/layout/layout.rb +47 -2
- data/lib/ratatui_ruby/layout/rect.rb +403 -2
- data/lib/ratatui_ruby/layout.rb +2 -2
- data/lib/ratatui_ruby/list_state.rb +113 -2
- data/lib/ratatui_ruby/output_guard.rb +26 -3
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +2 -2
- data/lib/ratatui_ruby/schema/bar_chart.rb +50 -2
- data/lib/ratatui_ruby/schema/block.rb +21 -15
- data/lib/ratatui_ruby/schema/calendar.rb +2 -2
- data/lib/ratatui_ruby/schema/canvas.rb +10 -2
- data/lib/ratatui_ruby/schema/center.rb +10 -2
- data/lib/ratatui_ruby/schema/chart.rb +2 -28
- data/lib/ratatui_ruby/schema/clear.rb +10 -2
- data/lib/ratatui_ruby/schema/constraint.rb +58 -2
- data/lib/ratatui_ruby/schema/cursor.rb +10 -2
- data/lib/ratatui_ruby/schema/draw.rb +10 -2
- data/lib/ratatui_ruby/schema/gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/layout.rb +18 -2
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +10 -2
- data/lib/ratatui_ruby/schema/list_item.rb +10 -2
- data/lib/ratatui_ruby/schema/overlay.rb +10 -2
- data/lib/ratatui_ruby/schema/paragraph.rb +10 -2
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +2 -2
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +2 -2
- data/lib/ratatui_ruby/schema/rect.rb +58 -2
- data/lib/ratatui_ruby/schema/row.rb +10 -2
- data/lib/ratatui_ruby/schema/scrollbar.rb +2 -2
- data/lib/ratatui_ruby/schema/shape/label.rb +10 -2
- data/lib/ratatui_ruby/schema/sparkline.rb +10 -2
- data/lib/ratatui_ruby/schema/style.rb +18 -2
- data/lib/ratatui_ruby/schema/table.rb +2 -2
- data/lib/ratatui_ruby/schema/tabs.rb +2 -2
- data/lib/ratatui_ruby/schema/text.rb +34 -2
- data/lib/ratatui_ruby/scrollbar_state.rb +10 -2
- data/lib/ratatui_ruby/style/style.rb +18 -2
- data/lib/ratatui_ruby/style.rb +2 -2
- data/lib/ratatui_ruby/synthetic_events.rb +86 -0
- data/lib/ratatui_ruby/table_state.rb +10 -2
- data/lib/ratatui_ruby/terminal_lifecycle.rb +18 -3
- data/lib/ratatui_ruby/test_helper/event_injection.rb +62 -2
- data/lib/ratatui_ruby/test_helper/snapshot.rb +74 -9
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +98 -2
- data/lib/ratatui_ruby/test_helper/terminal.rb +50 -2
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +18 -2
- data/lib/ratatui_ruby/test_helper.rb +10 -2
- data/lib/ratatui_ruby/tui/buffer_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/canvas_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/core.rb +2 -2
- data/lib/ratatui_ruby/tui/layout_factories.rb +32 -2
- data/lib/ratatui_ruby/tui/state_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/style_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/text_factories.rb +2 -2
- data/lib/ratatui_ruby/tui/widget_factories.rb +2 -2
- data/lib/ratatui_ruby/tui.rb +11 -3
- data/lib/ratatui_ruby/version.rb +3 -3
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -2
- data/lib/ratatui_ruby/widgets/bar_chart.rb +58 -2
- data/lib/ratatui_ruby/widgets/block.rb +37 -15
- data/lib/ratatui_ruby/widgets/calendar.rb +2 -2
- data/lib/ratatui_ruby/widgets/canvas.rb +10 -2
- data/lib/ratatui_ruby/widgets/cell.rb +10 -2
- data/lib/ratatui_ruby/widgets/center.rb +10 -2
- data/lib/ratatui_ruby/widgets/chart.rb +2 -28
- data/lib/ratatui_ruby/widgets/clear.rb +10 -2
- data/lib/ratatui_ruby/widgets/cursor.rb +10 -2
- data/lib/ratatui_ruby/widgets/gauge.rb +16 -2
- data/lib/ratatui_ruby/widgets/line_gauge.rb +16 -2
- data/lib/ratatui_ruby/widgets/list.rb +41 -2
- data/lib/ratatui_ruby/widgets/list_item.rb +10 -2
- data/lib/ratatui_ruby/widgets/overlay.rb +10 -2
- data/lib/ratatui_ruby/widgets/paragraph.rb +10 -2
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -2
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -2
- data/lib/ratatui_ruby/widgets/row.rb +10 -2
- data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -2
- data/lib/ratatui_ruby/widgets/shape/label.rb +10 -2
- data/lib/ratatui_ruby/widgets/sparkline.rb +10 -2
- data/lib/ratatui_ruby/widgets/table.rb +62 -2
- data/lib/ratatui_ruby/widgets/tabs.rb +2 -2
- data/lib/ratatui_ruby/widgets.rb +2 -2
- data/lib/ratatui_ruby.rb +101 -9
- data/sig/examples/app_all_events/view.rbs +7 -1
- data/sig/examples/app_all_events/view_state.rbs +7 -1
- data/sig/examples/app_color_picker/app.rbs +5 -0
- data/sig/examples/app_stateful_interaction/app.rbs +7 -1
- data/sig/examples/verify_quickstart_dsl/app.rbs +7 -1
- data/sig/examples/verify_quickstart_lifecycle/app.rbs +7 -1
- data/sig/examples/verify_readme_usage/app.rbs +7 -1
- data/sig/examples/widget_block_demo/app.rbs +6 -0
- data/sig/examples/widget_box_demo/app.rbs +7 -1
- data/sig/examples/widget_calendar_demo/app.rbs +7 -1
- data/sig/examples/widget_cell_demo/app.rbs +7 -1
- data/sig/examples/widget_chart_demo/app.rbs +7 -1
- data/sig/examples/widget_gauge_demo/app.rbs +7 -1
- data/sig/examples/widget_layout_split/app.rbs +7 -1
- data/sig/examples/widget_line_gauge_demo/app.rbs +7 -1
- data/sig/examples/widget_list_demo/app.rbs +5 -0
- data/sig/examples/widget_map_demo/app.rbs +7 -1
- data/sig/examples/widget_popup_demo/app.rbs +7 -1
- data/sig/examples/widget_ratatui_logo_demo/app.rbs +7 -1
- data/sig/examples/widget_ratatui_mascot_demo/app.rbs +7 -1
- data/sig/examples/widget_rect/app.rbs +7 -1
- data/sig/examples/widget_render/app.rbs +7 -1
- data/sig/examples/widget_rich_text/app.rbs +7 -1
- data/sig/examples/widget_scroll_text/app.rbs +7 -1
- data/sig/examples/widget_scrollbar_demo/app.rbs +7 -1
- data/sig/examples/widget_sparkline_demo/app.rbs +7 -1
- data/sig/examples/widget_style_colors/app.rbs +7 -1
- data/sig/examples/widget_table_demo/app.rbs +7 -1
- data/sig/examples/widget_text_width/app.rbs +7 -1
- data/sig/ratatui_ruby/event.rbs +7 -1
- data/sig/ratatui_ruby/frame.rbs +15 -3
- data/sig/ratatui_ruby/list_state.rbs +11 -1
- data/sig/ratatui_ruby/ratatui_ruby.rbs +8 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +7 -1
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +6 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +6 -0
- data/sig/ratatui_ruby/schema/block.rbs +7 -1
- data/sig/ratatui_ruby/schema/calendar.rbs +6 -0
- data/sig/ratatui_ruby/schema/canvas.rbs +6 -0
- data/sig/ratatui_ruby/schema/center.rbs +6 -0
- data/sig/ratatui_ruby/schema/chart.rbs +6 -9
- data/sig/ratatui_ruby/schema/constraint.rbs +14 -0
- data/sig/ratatui_ruby/schema/cursor.rbs +6 -0
- data/sig/ratatui_ruby/schema/draw.rbs +6 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +9 -1
- data/sig/ratatui_ruby/schema/layout.rbs +6 -0
- data/sig/ratatui_ruby/schema/line_gauge.rbs +9 -1
- data/sig/ratatui_ruby/schema/list.rbs +9 -1
- data/sig/ratatui_ruby/schema/list_item.rbs +7 -1
- data/sig/ratatui_ruby/schema/overlay.rbs +6 -0
- data/sig/ratatui_ruby/schema/paragraph.rbs +6 -0
- data/sig/ratatui_ruby/schema/ratatui_logo.rbs +6 -0
- data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +5 -0
- data/sig/ratatui_ruby/schema/rect.rbs +30 -0
- data/sig/ratatui_ruby/schema/row.rbs +7 -1
- data/sig/ratatui_ruby/schema/scrollbar.rbs +6 -0
- data/sig/ratatui_ruby/schema/sparkline.rbs +6 -0
- data/sig/ratatui_ruby/schema/style.rbs +7 -1
- data/sig/ratatui_ruby/schema/table.rbs +11 -1
- data/sig/ratatui_ruby/schema/tabs.rbs +6 -0
- data/sig/ratatui_ruby/schema/text.rbs +7 -1
- data/sig/ratatui_ruby/scrollbar_state.rbs +7 -1
- data/sig/ratatui_ruby/session.rbs +7 -1
- data/sig/ratatui_ruby/table_state.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/terminal.rbs +7 -1
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -1
- data/sig/ratatui_ruby/test_helper.rbs +7 -1
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/core.rbs +7 -1
- data/sig/ratatui_ruby/tui/layout_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/state_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/style_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/text_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui/widget_factories.rbs +7 -1
- data/sig/ratatui_ruby/tui.rbs +7 -1
- data/sig/ratatui_ruby/version.rbs +6 -0
- data/tasks/autodoc/examples.rb +1 -1
- data/tasks/autodoc/member.rb +1 -1
- data/tasks/autodoc/name.rb +1 -1
- data/tasks/bump/cargo_lockfile.rb +1 -1
- data/tasks/bump/changelog.rb +1 -1
- data/tasks/bump/header.rb +1 -1
- data/tasks/bump/history.rb +1 -1
- data/tasks/bump/links.rb +1 -1
- data/tasks/bump/manifest.rb +1 -1
- data/tasks/bump/ruby_gem.rb +1 -1
- data/tasks/bump/sem_ver.rb +1 -1
- data/tasks/bump/unreleased_section.rb +1 -1
- data/tasks/license/headers_md.rb +223 -0
- data/tasks/license/headers_rb.rb +210 -0
- data/tasks/license/license_utils.rb +130 -0
- data/tasks/license/snippets_md.rb +315 -0
- data/tasks/license/snippets_rdoc.rb +150 -0
- data/tasks/license.rake +91 -0
- data/tasks/rdoc_config.rb +1 -1
- data/tasks/resources/build.yml.erb +13 -7
- data/tasks/sourcehut.rake +3 -1
- data/tasks/terminal_preview/app_screenshot.rb +1 -1
- data/tasks/terminal_preview/crash_report.rb +1 -1
- data/tasks/terminal_preview/example_app.rb +1 -1
- data/tasks/terminal_preview/launcher_script.rb +1 -1
- data/tasks/terminal_preview/preview_collection.rb +1 -1
- data/tasks/terminal_preview/preview_timing.rb +1 -1
- data/tasks/terminal_preview/safety_confirmation.rb +1 -1
- data/tasks/terminal_preview/saved_screenshot.rb +1 -1
- data/tasks/terminal_preview/system_appearance.rb +1 -1
- data/tasks/terminal_preview/terminal_window.rb +1 -1
- data/tasks/terminal_preview/window_id.rb +1 -1
- data/tasks/website/index_page.rb +1 -1
- data/tasks/website/version.rb +1 -1
- data/tasks/website/version_menu.rb +1 -1
- data/tasks/website/versioned_documentation.rb +1 -1
- data/tasks/website/website.rb +1 -1
- metadata +15 -3
- data/doc/migration/v0_7_0.md +0 -236
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# Shared utility for detecting contributors from git blame and Co-Authored-By trailers.
|
|
9
|
+
#
|
|
10
|
+
# This module provides methods to:
|
|
11
|
+
# - Get all contributors (authors and co-authors) who touched specific lines
|
|
12
|
+
# - Track the latest year each contributor touched those lines
|
|
13
|
+
# - Parse Co-Authored-By trailers from commit messages
|
|
14
|
+
|
|
15
|
+
require "open3"
|
|
16
|
+
require "date"
|
|
17
|
+
|
|
18
|
+
module LicenseUtils
|
|
19
|
+
# Represents a contributor with their latest year of contribution
|
|
20
|
+
Contributor = Data.define(:name, :email, :year)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Get all contributors who touched lines in a file (or range of lines).
|
|
24
|
+
# Returns a Hash of { "Name <email>" => year } mapping each contributor to their latest year.
|
|
25
|
+
#
|
|
26
|
+
# This considers both the commit author AND any Co-Authored-By trailers in commit messages.
|
|
27
|
+
def get_contributors_for_lines(filepath, start_line = nil, end_line = nil)
|
|
28
|
+
blame_cmd = if start_line && end_line
|
|
29
|
+
%W[git blame -L #{start_line},#{end_line} --porcelain -- #{filepath}]
|
|
30
|
+
else
|
|
31
|
+
%W[git blame --porcelain -- #{filepath}]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
output, _status = Open3.capture2(*blame_cmd)
|
|
35
|
+
|
|
36
|
+
contributors = {} # "Name <email>" => year
|
|
37
|
+
commit_cache = {} # commit_hash => { year:, author:, co_authors: [] }
|
|
38
|
+
|
|
39
|
+
current_commit = nil
|
|
40
|
+
|
|
41
|
+
output.each_line do |line|
|
|
42
|
+
if line =~ /^([a-f0-9]{40})/
|
|
43
|
+
current_commit = $1
|
|
44
|
+
elsif line =~ /^author (.+)$/
|
|
45
|
+
commit_cache[current_commit] ||= {}
|
|
46
|
+
commit_cache[current_commit][:author_name] = $1
|
|
47
|
+
elsif line =~ /^author-mail <(.+)>$/
|
|
48
|
+
commit_cache[current_commit] ||= {}
|
|
49
|
+
commit_cache[current_commit][:author_email] = $1
|
|
50
|
+
elsif line =~ /^author-time (\d+)$/
|
|
51
|
+
commit_cache[current_commit] ||= {}
|
|
52
|
+
timestamp = $1.to_i
|
|
53
|
+
commit_cache[current_commit][:year] = Time.at(timestamp).year
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Now fetch co-authors for each unique commit
|
|
58
|
+
commit_cache.each do |commit_hash, data|
|
|
59
|
+
next if commit_hash == "0" * 40 # Skip uncommitted lines
|
|
60
|
+
|
|
61
|
+
# Get commit message for Co-Authored-By parsing
|
|
62
|
+
msg_output, _status = Open3.capture2("git", "log", "-1", "--format=%B", commit_hash)
|
|
63
|
+
co_authors = parse_co_authors(msg_output)
|
|
64
|
+
data[:co_authors] = co_authors
|
|
65
|
+
|
|
66
|
+
# Add author
|
|
67
|
+
if data[:author_name] && data[:author_email]
|
|
68
|
+
key = "#{data[:author_name]} <#{data[:author_email]}>"
|
|
69
|
+
year = data[:year] || Date.today.year
|
|
70
|
+
contributors[key] = [contributors[key] || 0, year].max
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Add co-authors with same year as commit
|
|
74
|
+
co_authors.each do |ca|
|
|
75
|
+
key = "#{ca[:name]} <#{ca[:email]}>"
|
|
76
|
+
year = data[:year] || Date.today.year
|
|
77
|
+
contributors[key] = [contributors[key] || 0, year].max
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
contributors
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get YOUR latest year contribution to the file/lines.
|
|
85
|
+
# your_identifiers should be an array of strings that identify you (name, email fragments).
|
|
86
|
+
def get_your_latest_year(filepath, your_identifiers, start_line = nil, end_line = nil)
|
|
87
|
+
contributors = get_contributors_for_lines(filepath, start_line, end_line)
|
|
88
|
+
|
|
89
|
+
your_year = nil
|
|
90
|
+
contributors.each do |contributor, year|
|
|
91
|
+
if your_identifiers.any? { |id| contributor.include?(id) }
|
|
92
|
+
your_year = [your_year || 0, year].max
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
your_year || Date.today.year
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get all contributors EXCEPT you, with their latest years.
|
|
100
|
+
# Returns array of { name:, email:, year: }
|
|
101
|
+
def get_other_contributors(filepath, your_identifiers, start_line = nil, end_line = nil)
|
|
102
|
+
contributors = get_contributors_for_lines(filepath, start_line, end_line)
|
|
103
|
+
|
|
104
|
+
others = []
|
|
105
|
+
contributors.each do |contributor, year|
|
|
106
|
+
next if your_identifiers.any? { |id| contributor.include?(id) }
|
|
107
|
+
|
|
108
|
+
# Parse "Name <email>" format
|
|
109
|
+
if contributor =~ /^(.+?)\s*<(.+)>$/
|
|
110
|
+
others << { name: $1.strip, email: $2.strip, year: }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
others
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private def parse_co_authors(message)
|
|
118
|
+
co_authors = []
|
|
119
|
+
|
|
120
|
+
message.each_line do |line|
|
|
121
|
+
# Match "Co-Authored-By: Name <email>" (case insensitive)
|
|
122
|
+
if line =~ /^Co-Authored-By:\s*(.+?)\s*<(.+?)>\s*$/i
|
|
123
|
+
co_authors << { name: $1.strip, email: $2.strip }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
co_authors
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# Script to add SPDX snippet headers to fenced code blocks in markdown files.
|
|
9
|
+
#
|
|
10
|
+
# Usage: ruby tasks/license/snippets_md.rb [path...]
|
|
11
|
+
#
|
|
12
|
+
# If no paths are given, processes all .md files via git ls-files.
|
|
13
|
+
#
|
|
14
|
+
# Rules:
|
|
15
|
+
# - Wraps all fenced code blocks (``` or ````) with SPDX snippet headers (MIT-0)
|
|
16
|
+
# - For SYNC:START/SYNC:END blocks, wraps AROUND the sync markers (not inside)
|
|
17
|
+
# - Uses git blame to determine the latest edit year for the code lines
|
|
18
|
+
# - Skips blocks that are already properly wrapped with MIT-0 and Kerrick Long
|
|
19
|
+
# - Removes malformed existing SPDX-Snippet blocks and replaces with correct ones
|
|
20
|
+
|
|
21
|
+
require "open3"
|
|
22
|
+
require "date"
|
|
23
|
+
|
|
24
|
+
COPYRIGHT_HOLDER = "Kerrick Long"
|
|
25
|
+
LICENSE = "MIT-0"
|
|
26
|
+
|
|
27
|
+
# Files to skip entirely (relative paths from repo root)
|
|
28
|
+
EXCLUDED_FILES = [
|
|
29
|
+
"doc/contributors/v1.0.0_blockers.md",
|
|
30
|
+
"doc/contributors/upstream_requests/tab_rects.md",
|
|
31
|
+
"doc/contributors/upstream_requests/title_rects.md",
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def get_latest_git_year(file, start_line, end_line)
|
|
35
|
+
cmd = %W[git blame -L #{start_line},#{end_line} --date=short -- #{file}]
|
|
36
|
+
output, _status = Open3.capture2(*cmd)
|
|
37
|
+
years = output.scan(/(\d{4})-\d{2}-\d{2}/).flatten.map(&:to_i)
|
|
38
|
+
years.empty? ? Date.today.year : years.max
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def is_our_snippet_header?(lines, idx)
|
|
42
|
+
# Check if the current SPDX-SnippetBegin block already has our copyright/license
|
|
43
|
+
i = idx + 1
|
|
44
|
+
has_our_copyright = false
|
|
45
|
+
has_mit0 = false
|
|
46
|
+
|
|
47
|
+
while i < lines.length && !lines[i].include?("-->")
|
|
48
|
+
line = lines[i]
|
|
49
|
+
has_our_copyright = true if line.include?(COPYRIGHT_HOLDER) && line.include?("SPDX-FileCopyrightText")
|
|
50
|
+
has_mit0 = true if line.include?("MIT-0") && line.include?("SPDX-License-Identifier")
|
|
51
|
+
i += 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
has_our_copyright && has_mit0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find_snippet_end(lines, start_idx)
|
|
58
|
+
i = start_idx
|
|
59
|
+
while i < lines.length
|
|
60
|
+
return i if lines[i].include?("SPDX-SnippetEnd")
|
|
61
|
+
i += 1
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def process_file(filepath)
|
|
67
|
+
# Skip excluded files
|
|
68
|
+
return if EXCLUDED_FILES.any? { |excluded| filepath.end_with?(excluded) }
|
|
69
|
+
|
|
70
|
+
content = File.read(filepath)
|
|
71
|
+
lines = content.lines
|
|
72
|
+
|
|
73
|
+
# Track code block ranges (to exclude from file header year calculation)
|
|
74
|
+
code_block_ranges = []
|
|
75
|
+
changes = []
|
|
76
|
+
removals = [] # existing malformed SPDX snippet blocks to remove
|
|
77
|
+
i = 0
|
|
78
|
+
|
|
79
|
+
while i < lines.length
|
|
80
|
+
line = lines[i]
|
|
81
|
+
|
|
82
|
+
# Check if we're at an existing SPDX-SnippetBegin
|
|
83
|
+
if line.include?("SPDX-SnippetBegin")
|
|
84
|
+
snippet_start = i
|
|
85
|
+
snippet_end = find_snippet_end(lines, i)
|
|
86
|
+
|
|
87
|
+
if snippet_end
|
|
88
|
+
# Check if this is already our proper snippet
|
|
89
|
+
if is_our_snippet_header?(lines, i)
|
|
90
|
+
# Skip this block entirely - it's already correct
|
|
91
|
+
i = snippet_end + 1
|
|
92
|
+
next
|
|
93
|
+
else
|
|
94
|
+
# Mark for removal - we'll re-wrap the inner content
|
|
95
|
+
removals << { start: snippet_start, end: snippet_end }
|
|
96
|
+
i = snippet_end + 1
|
|
97
|
+
next
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check for SYNC:START pattern
|
|
103
|
+
if line =~ /<!--\s*SYNC:START/
|
|
104
|
+
sync_start_line = i
|
|
105
|
+
j = i + 1
|
|
106
|
+
code_start = nil
|
|
107
|
+
code_end = nil
|
|
108
|
+
sync_end_line = nil
|
|
109
|
+
|
|
110
|
+
while j < lines.length
|
|
111
|
+
if lines[j] =~ /^(````*)(\w*)$/
|
|
112
|
+
if code_start.nil?
|
|
113
|
+
code_start = j
|
|
114
|
+
else
|
|
115
|
+
code_end = j
|
|
116
|
+
end
|
|
117
|
+
elsif lines[j] =~ /<!--\s*SYNC:END/
|
|
118
|
+
sync_end_line = j
|
|
119
|
+
break
|
|
120
|
+
end
|
|
121
|
+
j += 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if code_start && code_end && sync_end_line
|
|
125
|
+
year = get_latest_git_year(filepath, code_start + 1, code_end + 1)
|
|
126
|
+
changes << {
|
|
127
|
+
type: :sync_block,
|
|
128
|
+
start: sync_start_line,
|
|
129
|
+
end: sync_end_line,
|
|
130
|
+
year:,
|
|
131
|
+
}
|
|
132
|
+
code_block_ranges << (code_start..code_end)
|
|
133
|
+
i = sync_end_line + 1
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check for standalone fenced code block
|
|
139
|
+
if line =~ /^(````*)(\w*)$/
|
|
140
|
+
fence_marker = $1
|
|
141
|
+
fence_start = i
|
|
142
|
+
re_end = /^#{Regexp.escape(fence_marker)}$/
|
|
143
|
+
|
|
144
|
+
j = i + 1
|
|
145
|
+
fence_end = nil
|
|
146
|
+
while j < lines.length
|
|
147
|
+
if lines[j] =~ re_end
|
|
148
|
+
fence_end = j
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
j += 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if fence_end
|
|
155
|
+
year = get_latest_git_year(filepath, fence_start + 1, fence_end + 1)
|
|
156
|
+
changes << {
|
|
157
|
+
type: :code_block,
|
|
158
|
+
start: fence_start,
|
|
159
|
+
end: fence_end,
|
|
160
|
+
year:,
|
|
161
|
+
}
|
|
162
|
+
code_block_ranges << (fence_start..fence_end)
|
|
163
|
+
i = fence_end + 1
|
|
164
|
+
next
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
i += 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Handle removals and additions
|
|
172
|
+
has_changes = !changes.empty? || !removals.empty?
|
|
173
|
+
|
|
174
|
+
# Remove existing malformed SPDX blocks (in reverse order)
|
|
175
|
+
removals.sort_by { |r| -r[:start] }.each do |removal|
|
|
176
|
+
# Remove the SnippetEnd line
|
|
177
|
+
lines.delete_at(removal[:end])
|
|
178
|
+
# Remove lines from SnippetBegin through the --> closing the comment
|
|
179
|
+
close_idx = removal[:start]
|
|
180
|
+
while close_idx < lines.length && !lines[close_idx].include?("-->")
|
|
181
|
+
close_idx += 1
|
|
182
|
+
end
|
|
183
|
+
# Remove from start to close_idx inclusive
|
|
184
|
+
(close_idx - removal[:start] + 1).times { lines.delete_at(removal[:start]) }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Recalculate content after removals
|
|
188
|
+
content = lines.join
|
|
189
|
+
lines = content.lines
|
|
190
|
+
|
|
191
|
+
# Re-scan for code blocks that need wrapping
|
|
192
|
+
changes = []
|
|
193
|
+
i = 0
|
|
194
|
+
|
|
195
|
+
while i < lines.length
|
|
196
|
+
line = lines[i]
|
|
197
|
+
|
|
198
|
+
# Skip if already inside an SPDX-SnippetBegin block
|
|
199
|
+
if line.include?("SPDX-SnippetBegin")
|
|
200
|
+
while i < lines.length && !lines[i].include?("SPDX-SnippetEnd")
|
|
201
|
+
i += 1
|
|
202
|
+
end
|
|
203
|
+
i += 1
|
|
204
|
+
next
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Check for SYNC:START pattern
|
|
208
|
+
if line =~ /<!--\s*SYNC:START/
|
|
209
|
+
sync_start_line = i
|
|
210
|
+
j = i + 1
|
|
211
|
+
code_start = nil
|
|
212
|
+
code_end = nil
|
|
213
|
+
sync_end_line = nil
|
|
214
|
+
|
|
215
|
+
while j < lines.length
|
|
216
|
+
if lines[j] =~ /^(````*)(\w*)$/
|
|
217
|
+
if code_start.nil?
|
|
218
|
+
code_start = j
|
|
219
|
+
else
|
|
220
|
+
code_end = j
|
|
221
|
+
end
|
|
222
|
+
elsif lines[j] =~ /<!--\s*SYNC:END/
|
|
223
|
+
sync_end_line = j
|
|
224
|
+
break
|
|
225
|
+
end
|
|
226
|
+
j += 1
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if code_start && code_end && sync_end_line
|
|
230
|
+
year = get_latest_git_year(filepath, code_start + 1, code_end + 1)
|
|
231
|
+
changes << {
|
|
232
|
+
type: :sync_block,
|
|
233
|
+
start: sync_start_line,
|
|
234
|
+
end: sync_end_line,
|
|
235
|
+
year:,
|
|
236
|
+
}
|
|
237
|
+
i = sync_end_line + 1
|
|
238
|
+
next
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Check for standalone fenced code block
|
|
243
|
+
if line =~ /^(````*)(\w*)$/
|
|
244
|
+
fence_marker = $1
|
|
245
|
+
fence_start = i
|
|
246
|
+
re_end = /^#{Regexp.escape(fence_marker)}$/
|
|
247
|
+
|
|
248
|
+
j = i + 1
|
|
249
|
+
fence_end = nil
|
|
250
|
+
while j < lines.length
|
|
251
|
+
if lines[j] =~ re_end
|
|
252
|
+
fence_end = j
|
|
253
|
+
break
|
|
254
|
+
end
|
|
255
|
+
j += 1
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if fence_end
|
|
259
|
+
year = get_latest_git_year(filepath, fence_start + 1, fence_end + 1)
|
|
260
|
+
changes << {
|
|
261
|
+
type: :code_block,
|
|
262
|
+
start: fence_start,
|
|
263
|
+
end: fence_end,
|
|
264
|
+
year:,
|
|
265
|
+
}
|
|
266
|
+
i = fence_end + 1
|
|
267
|
+
next
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
i += 1
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
return if changes.empty? && !has_changes
|
|
275
|
+
|
|
276
|
+
# Apply changes in reverse order to preserve line numbers
|
|
277
|
+
changes.sort_by { |c| -c[:start] }.each do |change|
|
|
278
|
+
# REUSE-IgnoreStart
|
|
279
|
+
snippet_begin = "<!-- SPDX-SnippetBegin -->\n<!--\n SPDX-FileCopyrightText: #{change[:year]} #{COPYRIGHT_HOLDER}\n SPDX-License-Identifier: #{LICENSE}\n-->\n"
|
|
280
|
+
snippet_end = "<!-- SPDX-SnippetEnd -->\n"
|
|
281
|
+
# REUSE-IgnoreEnd
|
|
282
|
+
|
|
283
|
+
# Insert end marker after the block
|
|
284
|
+
lines.insert(change[:end] + 1, snippet_end)
|
|
285
|
+
# Insert begin marker before the block
|
|
286
|
+
lines.insert(change[:start], snippet_begin)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
File.write(filepath, lines.join)
|
|
290
|
+
puts "Updated: #{filepath} (#{changes.length} code block(s))"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def find_md_files(paths)
|
|
294
|
+
# Use git ls-files to respect .gitignore
|
|
295
|
+
if paths.empty?
|
|
296
|
+
`git ls-files '*.md'`.split("\n")
|
|
297
|
+
else
|
|
298
|
+
paths.flat_map do |path|
|
|
299
|
+
if File.directory?(path)
|
|
300
|
+
`git ls-files '#{path}/**/*.md'`.split("\n")
|
|
301
|
+
else
|
|
302
|
+
path
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
if __FILE__ == $0
|
|
309
|
+
paths = ARGV.empty? ? [] : ARGV
|
|
310
|
+
files = find_md_files(paths)
|
|
311
|
+
|
|
312
|
+
files.each do |file|
|
|
313
|
+
process_file(file)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# Script to add SPDX snippet headers to RDoc code examples in Ruby files.
|
|
9
|
+
#
|
|
10
|
+
# Usage: ruby scripts/add_spdx_rdoc_snippets.rb [path...]
|
|
11
|
+
#
|
|
12
|
+
# If no paths are given, processes all .rb files via git ls-files.
|
|
13
|
+
#
|
|
14
|
+
# Rules:
|
|
15
|
+
# - Wraps RDoc code examples (indented comment lines) with SPDX snippet headers
|
|
16
|
+
# - Uses #-- and #++ to hide the SPDX headers from RDoc rendering
|
|
17
|
+
# - Uses git blame to determine the latest edit year for the code lines
|
|
18
|
+
# - Skips blocks that are already wrapped with SPDX-SnippetBegin
|
|
19
|
+
|
|
20
|
+
require "open3"
|
|
21
|
+
require "date"
|
|
22
|
+
|
|
23
|
+
COPYRIGHT_HOLDER = "Kerrick Long"
|
|
24
|
+
LICENSE = "MIT-0"
|
|
25
|
+
|
|
26
|
+
def get_latest_git_year(file, start_line, end_line)
|
|
27
|
+
cmd = %W[git blame -L #{start_line},#{end_line} --date=short -- #{file}]
|
|
28
|
+
output, _status = Open3.capture2(*cmd)
|
|
29
|
+
years = output.scan(/(\d{4})-\d{2}-\d{2}/).flatten.map(&:to_i)
|
|
30
|
+
years.empty? ? Date.today.year : years.max
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_rdoc_code_blocks(lines)
|
|
34
|
+
# Find all RDoc code blocks (indented comment lines)
|
|
35
|
+
# Returns array of {start:, end:, indent:} where indent is the comment prefix
|
|
36
|
+
blocks = []
|
|
37
|
+
i = 0
|
|
38
|
+
|
|
39
|
+
while i < lines.length
|
|
40
|
+
line = lines[i]
|
|
41
|
+
|
|
42
|
+
# Check if this is an indented code line in a comment
|
|
43
|
+
# Pattern: optional leading whitespace, #, then 3+ spaces (RDoc code indent)
|
|
44
|
+
if line =~ /^(\s*)#( +)(\S.*)$/
|
|
45
|
+
prefix = $1 # leading whitespace before #
|
|
46
|
+
block_start = i
|
|
47
|
+
|
|
48
|
+
# Find the extent of this code block
|
|
49
|
+
j = i
|
|
50
|
+
while j < lines.length
|
|
51
|
+
current = lines[j]
|
|
52
|
+
# Code block continues if line is indented code OR empty comment line
|
|
53
|
+
if current =~ /^#{Regexp.escape(prefix)}#( +|\s*$)/
|
|
54
|
+
j += 1
|
|
55
|
+
else
|
|
56
|
+
break
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
block_end = j - 1
|
|
61
|
+
|
|
62
|
+
# Only count as a block if it has actual code (not just empty lines)
|
|
63
|
+
has_code = (block_start..block_end).any? { |k| lines[k] =~ /^#{Regexp.escape(prefix)}# +\S/ }
|
|
64
|
+
|
|
65
|
+
if has_code && block_end > block_start
|
|
66
|
+
blocks << { start: block_start, end: block_end, prefix: }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
i = j
|
|
70
|
+
else
|
|
71
|
+
i += 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
blocks
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def is_already_wrapped?(lines, block_start, prefix)
|
|
79
|
+
# Check if the line before the block is #++ (meaning it's already wrapped)
|
|
80
|
+
return false if block_start < 1
|
|
81
|
+
|
|
82
|
+
prev_line = lines[block_start - 1]
|
|
83
|
+
prev_line =~ /^#{Regexp.escape(prefix)}#\+\+\s*$/
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def process_file(filepath)
|
|
87
|
+
content = File.read(filepath)
|
|
88
|
+
lines = content.lines
|
|
89
|
+
|
|
90
|
+
blocks = find_rdoc_code_blocks(lines)
|
|
91
|
+
|
|
92
|
+
# Filter out already-wrapped blocks
|
|
93
|
+
blocks.reject! { |b| is_already_wrapped?(lines, b[:start], b[:prefix]) }
|
|
94
|
+
|
|
95
|
+
return if blocks.empty?
|
|
96
|
+
|
|
97
|
+
# Apply changes in reverse order to preserve line numbers
|
|
98
|
+
blocks.sort_by { |b| -b[:start] }.each do |block|
|
|
99
|
+
year = get_latest_git_year(filepath, block[:start] + 1, block[:end] + 1)
|
|
100
|
+
prefix = block[:prefix]
|
|
101
|
+
|
|
102
|
+
# Build the wrapper lines
|
|
103
|
+
# REUSE-IgnoreStart
|
|
104
|
+
begin_wrapper = [
|
|
105
|
+
"#{prefix}#--\n",
|
|
106
|
+
"#{prefix}# SPDX-SnippetBegin\n",
|
|
107
|
+
"#{prefix}# SPDX-FileCopyrightText: #{year} #{COPYRIGHT_HOLDER}\n",
|
|
108
|
+
"#{prefix}# SPDX-License-Identifier: #{LICENSE}\n",
|
|
109
|
+
"#{prefix}#++\n",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
end_wrapper = [
|
|
113
|
+
"#{prefix}#--\n",
|
|
114
|
+
"#{prefix}# SPDX-SnippetEnd\n",
|
|
115
|
+
"#{prefix}#++\n",
|
|
116
|
+
]
|
|
117
|
+
# REUSE-IgnoreEnd
|
|
118
|
+
|
|
119
|
+
# Insert end wrapper after the block
|
|
120
|
+
lines.insert(block[:end] + 1, *end_wrapper)
|
|
121
|
+
# Insert begin wrapper before the block
|
|
122
|
+
lines.insert(block[:start], *begin_wrapper)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
File.write(filepath, lines.join)
|
|
126
|
+
puts "Updated: #{filepath} (#{blocks.length} code block(s))"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def find_rb_files(paths)
|
|
130
|
+
if paths.empty?
|
|
131
|
+
`git ls-files '*.rb'`.split("\n")
|
|
132
|
+
else
|
|
133
|
+
paths.flat_map do |path|
|
|
134
|
+
if File.directory?(path)
|
|
135
|
+
`git ls-files '#{path}/**/*.rb'`.split("\n")
|
|
136
|
+
else
|
|
137
|
+
path
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if __FILE__ == $0
|
|
144
|
+
paths = ARGV.empty? ? [] : ARGV
|
|
145
|
+
files = find_rb_files(paths)
|
|
146
|
+
|
|
147
|
+
files.each do |file|
|
|
148
|
+
process_file(file)
|
|
149
|
+
end
|
|
150
|
+
end
|
data/tasks/license.rake
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
namespace :license do
|
|
9
|
+
namespace :headers do
|
|
10
|
+
desc "Ensure markdown files have correct CC-BY-SA-4.0 headers"
|
|
11
|
+
task :md, [:files] do |_t, args|
|
|
12
|
+
files = args[:files] || ""
|
|
13
|
+
ruby "tasks/license/headers_md.rb #{files}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Ensure Ruby files have correct AGPL-3.0-or-later headers"
|
|
17
|
+
task :rb, [:files] do |_t, args|
|
|
18
|
+
files = args[:files] || ""
|
|
19
|
+
ruby "tasks/license/headers_rb.rb #{files}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Ensure all files have correct license headers"
|
|
23
|
+
task :all do
|
|
24
|
+
Rake::Task["license:headers:md"].invoke
|
|
25
|
+
Rake::Task["license:headers:rb"].invoke
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
namespace :snippets do
|
|
30
|
+
desc "Add MIT-0 SPDX snippet headers to markdown fenced code blocks"
|
|
31
|
+
task :md, [:files] do |_t, args|
|
|
32
|
+
files = args[:files] || ""
|
|
33
|
+
ruby "tasks/license/snippets_md.rb #{files}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Add MIT-0 SPDX snippet headers to RDoc code examples in Ruby files"
|
|
37
|
+
task :rdoc, [:files] do |_t, args|
|
|
38
|
+
files = args[:files] || "lib/"
|
|
39
|
+
ruby "tasks/license/snippets_rdoc.rb #{files}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "Add MIT-0 SPDX snippet headers to all code examples"
|
|
43
|
+
task :all do
|
|
44
|
+
Rake::Task["license:snippets:md"].invoke
|
|
45
|
+
Rake::Task["license:snippets:rdoc"].invoke
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "Run all license tasks (headers + snippets)"
|
|
50
|
+
task all: ["headers:all", "snippets:all"]
|
|
51
|
+
|
|
52
|
+
desc "Run license tasks on changed files only (staged + unstaged)"
|
|
53
|
+
task :new do
|
|
54
|
+
# Get changed .md and .rb files (staged and unstaged)
|
|
55
|
+
changed_md = `git diff --name-only --diff-filter=ACMR HEAD -- '*.md' 2>/dev/null`.split("\n")
|
|
56
|
+
staged_md = `git diff --name-only --cached --diff-filter=ACMR -- '*.md' 2>/dev/null`.split("\n")
|
|
57
|
+
changed_rb = `git diff --name-only --diff-filter=ACMR HEAD -- '*.rb' 2>/dev/null`.split("\n")
|
|
58
|
+
staged_rb = `git diff --name-only --cached --diff-filter=ACMR -- '*.rb' 2>/dev/null`.split("\n")
|
|
59
|
+
|
|
60
|
+
# Also get untracked new files
|
|
61
|
+
untracked = `git ls-files --others --exclude-standard`.split("\n")
|
|
62
|
+
untracked_md = untracked.select { |f| f.end_with?(".md") }
|
|
63
|
+
untracked_rb = untracked.select { |f| f.end_with?(".rb") }
|
|
64
|
+
|
|
65
|
+
md_files = (changed_md + staged_md + untracked_md).uniq.join(" ")
|
|
66
|
+
rb_files = (changed_rb + staged_rb + untracked_rb).uniq
|
|
67
|
+
|
|
68
|
+
# Filter rb files to only lib/
|
|
69
|
+
lib_rb_files = rb_files.select { |f| f.start_with?("lib/") }.join(" ")
|
|
70
|
+
|
|
71
|
+
if md_files.empty? && lib_rb_files.empty?
|
|
72
|
+
puts "No changed .md or lib/*.rb files to process"
|
|
73
|
+
else
|
|
74
|
+
unless md_files.empty?
|
|
75
|
+
puts "Processing #{md_files.split.count} changed .md file(s)..."
|
|
76
|
+
Rake::Task["license:headers:md"].invoke(md_files)
|
|
77
|
+
Rake::Task["license:headers:md"].reenable
|
|
78
|
+
Rake::Task["license:snippets:md"].invoke(md_files)
|
|
79
|
+
Rake::Task["license:snippets:md"].reenable
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
unless lib_rb_files.empty?
|
|
83
|
+
puts "Processing #{lib_rb_files.split.count} changed lib/*.rb file(s)..."
|
|
84
|
+
Rake::Task["license:headers:rb"].invoke(lib_rb_files)
|
|
85
|
+
Rake::Task["license:headers:rb"].reenable
|
|
86
|
+
Rake::Task["license:snippets:rdoc"].invoke(lib_rb_files)
|
|
87
|
+
Rake::Task["license:snippets:rdoc"].reenable
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|