reissue 0.4.0 → 0.4.2
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/CHANGELOG.md +5 -11
- data/README.md +149 -80
- data/Rakefile +2 -0
- data/lib/reissue/changelog_updater.rb +26 -4
- data/lib/reissue/fragment_handler/directory_fragment_handler.rb +69 -0
- data/lib/reissue/fragment_handler/git_fragment_handler.rb +93 -0
- data/lib/reissue/fragment_handler/null_fragment_handler.rb +20 -0
- data/lib/reissue/fragment_handler.rb +45 -0
- data/lib/reissue/rake.rb +114 -5
- data/lib/reissue/version.rb +1 -1
- data/lib/reissue.rb +54 -3
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 58c406237f6e7ef96ad4a4e1ed1aa2ba3e4cb10145edd6d44bff791a8e37d5be
|
4
|
+
data.tar.gz: 26b103713df3d7de9c1f9af49df079a2e9954b9b7e96a58bc8e3e203607e531d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 01d186d7b5f87e5c3ef2be402eb758971ff4dec3626d2e2aed8d67d5fd6418d064687682176b509b038b39710e8c313279047e807e42d927c902e4b5bbee649e
|
7
|
+
data.tar.gz: 63d5c29928e104484ce0f1c5ce6ef101a8909ed8727ae441c3a5bb81c9aaf6ecbfb46b7f4af69540f6665fc71ea8e9f9a73d1ea928371d3f8fb7a84b7077198c
|
data/CHANGELOG.md
CHANGED
@@ -5,20 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
7
7
|
|
8
|
-
## [0.4.
|
8
|
+
## [0.4.2] - 2025-09-16
|
9
9
|
|
10
10
|
### Changed
|
11
11
|
|
12
|
-
-
|
12
|
+
- New version was never created after the last release.
|
13
13
|
|
14
|
-
## [0.
|
14
|
+
## [0.4.2] - 2025-09-16
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
### Added
|
19
|
-
|
20
|
-
- CODEOWNERS file
|
21
|
-
|
22
|
-
### Fixed
|
16
|
+
### Changed
|
23
17
|
|
24
|
-
-
|
18
|
+
- New version was never created after the last release.
|
data/README.md
CHANGED
@@ -1,142 +1,211 @@
|
|
1
1
|
# Reissue
|
2
2
|
|
3
|
-
|
3
|
+
Automate your Ruby gem releases with proper versioning and changelog management.
|
4
4
|
|
5
|
-
|
5
|
+
Keep your version numbers and changelogs consistent and up-to-date with minimal effort.
|
6
6
|
|
7
|
-
|
8
|
-
to capture the information about new changes.
|
7
|
+
## Bottom Line Up Front
|
9
8
|
|
10
|
-
|
11
|
-
|
9
|
+
When releasing gems, you typically run `rake build:checksum` to build the gem and generate the checksum,
|
10
|
+
then `rake release` to push the `.gem` file to [rubygems.org](https://rubygems.org).
|
12
11
|
|
13
|
-
|
12
|
+
With Reissue, the process remains the same, but you get automatic version bumping and changelog management included.
|
14
13
|
|
15
|
-
|
14
|
+
Also supports non-gem Ruby projects.
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
## How It Works
|
17
|
+
|
18
|
+
The workflow:
|
19
|
+
|
20
|
+
1. Start with a version number (e.g., 0.1.0) for your unreleased project.
|
21
|
+
2. Make commits and develop features.
|
22
|
+
3. Release the version, finalizing the changelog with the release date.
|
23
|
+
4. Reissue automatically bumps to the next version (e.g., 0.1.1) and prepares the changelog for future changes.
|
24
|
+
|
25
|
+
After each release, Reissue handles the version bump and changelog updates, so you're immediately ready for the next development cycle.
|
23
26
|
|
24
27
|
## Installation
|
25
28
|
|
26
|
-
|
29
|
+
Add to your application's Gemfile:
|
27
30
|
|
28
31
|
$ bundle add reissue
|
29
32
|
|
30
|
-
|
33
|
+
Or install directly:
|
31
34
|
|
32
35
|
$ gem install reissue
|
33
36
|
|
34
37
|
## Usage
|
35
38
|
|
36
|
-
|
39
|
+
### Gem Projects
|
40
|
+
|
41
|
+
Add to your Rakefile:
|
37
42
|
|
38
43
|
```ruby
|
39
44
|
require "reissue/gem"
|
40
45
|
|
41
46
|
Reissue::Task.create :reissue do |task|
|
42
|
-
# Required:
|
47
|
+
# Required: Path to your version file
|
43
48
|
task.version_file = "lib/my_gem/version.rb"
|
44
49
|
end
|
45
50
|
```
|
46
51
|
|
47
|
-
This
|
48
|
-
|
49
|
-
- `rake
|
50
|
-
version segment.
|
51
|
-
- `rake reissue:finalize[date]` - Update the CHANGELOG.md file with a date for
|
52
|
-
the latest version.
|
53
|
-
- `rake reissue:reformat[version_limit]` - Reformat the CHANGELOG.md file and
|
54
|
-
optionally limit the number of versions to maintain.
|
55
|
-
- `rake reissue:branch[branch-name]` - Create a new branch for the next version.
|
56
|
-
Controlled by the `push_finalize` and `commit_finalize` options.
|
57
|
-
- `rake reissue:push` - Push the changes to the remote repository. Controlled
|
58
|
-
by the `push_finalize` and `commit_finalize` options.
|
52
|
+
This integrates with standard gem tasks:
|
53
|
+
- `rake build` - Now finalizes the changelog before building
|
54
|
+
- `rake release` - Automatically bumps version after release
|
59
55
|
|
60
|
-
|
61
|
-
`reissue
|
62
|
-
|
56
|
+
Additional tasks (usually run automatically):
|
57
|
+
- `rake reissue[segment]` - Bump version (major, minor, patch)
|
58
|
+
- `rake reissue:finalize[date]` - Add release date to changelog
|
59
|
+
- `rake reissue:reformat[version_limit]` - Clean up changelog formatting
|
60
|
+
- `rake reissue:preview` - Preview changelog entries from fragments or git trailers
|
61
|
+
- `rake reissue:clear_fragments` - Clear changelog fragments after release
|
63
62
|
|
64
|
-
|
65
|
-
pushed to rubygems.
|
63
|
+
### Non-Gem Projects
|
66
64
|
|
67
|
-
|
68
|
-
|
69
|
-
Add the following to the Rakefile:
|
65
|
+
For non-gem Ruby projects, add to your Rakefile:
|
70
66
|
|
71
67
|
```ruby
|
72
68
|
require "reissue/rake"
|
73
69
|
|
74
70
|
Reissue::Task.create :reissue do |task|
|
75
|
-
# Required: The file to update with the new version number.
|
76
71
|
task.version_file = "path/to/version.rb"
|
77
72
|
end
|
78
73
|
```
|
79
74
|
|
80
|
-
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
require "reissue/rake"
|
84
|
-
|
85
|
-
Reissue::Task.create :your_name_and_namespace do |task|
|
75
|
+
Then use the rake tasks to manage your releases.
|
86
76
|
|
87
|
-
|
88
|
-
task.name = "your_name_and_namespace"
|
77
|
+
### Configuration Options
|
89
78
|
|
90
|
-
|
91
|
-
task.description = "Prepare the next version of the gem."
|
79
|
+
All available configuration options:
|
92
80
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
81
|
+
```ruby
|
82
|
+
Reissue::Task.create :reissue do |task|
|
83
|
+
# Required: The file to update with the new version number
|
84
|
+
task.version_file = "lib/my_gem/version.rb"
|
85
|
+
|
86
|
+
# Optional: The name of the task. Defaults to "reissue"
|
87
|
+
task.name = "reissue"
|
88
|
+
|
89
|
+
# Optional: The description of the main task
|
90
|
+
task.description = "Prepare the next version of the gem"
|
91
|
+
|
92
|
+
# Optional: The changelog file to update. Defaults to "CHANGELOG.md"
|
93
|
+
task.changelog_file = "CHANGELOG.md"
|
94
|
+
|
95
|
+
# Optional: The number of versions to maintain in the changelog. Defaults to 2
|
97
96
|
task.version_limit = 5
|
98
|
-
|
99
|
-
# Optional:
|
97
|
+
|
98
|
+
# Optional: Whether to commit the changes automatically. Defaults to true
|
99
|
+
task.commit = true
|
100
|
+
|
101
|
+
# Optional: Whether to commit the results of the finalize task. Defaults to true
|
102
|
+
task.commit_finalize = true
|
103
|
+
|
104
|
+
# Optional: Whether to push the changes automatically. Defaults to false
|
105
|
+
# Options: false, true (push working branch), :branch (create and push new branch)
|
106
|
+
task.push_finalize = :branch
|
107
|
+
|
108
|
+
# Optional: Configure fragment handling for changelog entries. Defaults to nil (disabled)
|
109
|
+
# Options:
|
110
|
+
# - nil or false: Fragments disabled
|
111
|
+
# - String path: Use directory-based fragments (e.g., "changelog_fragments")
|
112
|
+
# - :git: Extract changelog entries from git commit trailers
|
113
|
+
task.fragment = "changelog_fragments"
|
114
|
+
# task.fragment = :git # Use git trailers for changelog entries
|
115
|
+
|
116
|
+
# Optional: Whether to clear fragment files after releasing. Defaults to false
|
117
|
+
# When true, fragments are cleared after a release (only applies when using directory fragments)
|
118
|
+
# Note: Has no effect when using :git fragments
|
119
|
+
task.clear_fragments = true
|
120
|
+
|
121
|
+
# Deprecated: Use `fragment` instead of `fragment_directory`
|
122
|
+
# task.fragment_directory = "changelog_fragments" # DEPRECATED: Use task.fragment instead
|
123
|
+
|
124
|
+
# Optional: Retain changelog files for previous versions. Defaults to false
|
125
|
+
# Options: true (retain in "changelogs" directory), "path/to/archive", or a Proc
|
126
|
+
task.retain_changelogs = true
|
127
|
+
# task.retain_changelogs = "path/to/archive"
|
128
|
+
# task.retain_changelogs = ->(version, content) { # custom logic }
|
129
|
+
|
130
|
+
# Optional: Custom version formatting logic. Receives a Gem::Version object and segment
|
100
131
|
task.version_redo_proc = ->(version, segment) do
|
101
132
|
# your special versioning logic
|
133
|
+
version.bump
|
102
134
|
end
|
135
|
+
end
|
136
|
+
```
|
103
137
|
|
104
|
-
|
105
|
-
task.changelog_file = "path/to/CHANGELOG.md"
|
106
|
-
|
107
|
-
# Optional: A Boolean, String, or Proc to retain the changelog files for the previous versions. Defaults to false.
|
108
|
-
# Setting to true will retain the changelog files in the "changelogs" directory.
|
109
|
-
# Setting to a String will use that path as the directory to retain the changelog files.
|
110
|
-
# The Proc receives a version hash and the changelog content.
|
111
|
-
task.retain_changelogs = ->(version, content) do
|
112
|
-
# your special retention logic
|
113
|
-
end
|
114
|
-
# or task.retain_changelogs = "path/to/changelogs"
|
115
|
-
# or task.retain_changelogs = true
|
138
|
+
## Using Git Trailers for Changelog Entries
|
116
139
|
|
117
|
-
|
118
|
-
task.commit = false
|
140
|
+
Reissue can extract changelog entries directly from git commit messages using trailers. This keeps your changelog data close to the code changes.
|
119
141
|
|
120
|
-
|
121
|
-
task.commit_finalize = false
|
142
|
+
### Configuration
|
122
143
|
|
123
|
-
|
124
|
-
|
144
|
+
```ruby
|
145
|
+
Reissue::Task.create :reissue do |task|
|
146
|
+
task.version_file = "lib/my_gem/version.rb"
|
147
|
+
task.fragment = :git # Enable git trailer extraction
|
125
148
|
end
|
126
149
|
```
|
127
150
|
|
128
|
-
|
151
|
+
### Adding Trailers to Commits
|
152
|
+
|
153
|
+
Use changelog section names as trailer keys in your commit messages:
|
154
|
+
|
155
|
+
```bash
|
156
|
+
git commit -m "Implement user authentication
|
157
|
+
|
158
|
+
Added: User login and logout functionality
|
159
|
+
Added: Password reset via email
|
160
|
+
Fixed: Session timeout not working correctly
|
161
|
+
Security: Rate limiting on login attempts"
|
162
|
+
```
|
163
|
+
|
164
|
+
### Supported Sections
|
129
165
|
|
130
|
-
|
166
|
+
Git trailers use the standard Keep a Changelog sections:
|
167
|
+
- `Added:` for new features
|
168
|
+
- `Changed:` for changes in existing functionality
|
169
|
+
- `Deprecated:` for soon-to-be removed features
|
170
|
+
- `Removed:` for now removed features
|
171
|
+
- `Fixed:` for any bug fixes
|
172
|
+
- `Security:` for vulnerability fixes
|
131
173
|
|
132
|
-
|
174
|
+
### How It Works
|
175
|
+
|
176
|
+
1. When you run `rake reissue`, it finds all commits since the last version tag
|
177
|
+
2. Extracts trailers matching changelog sections from commit messages
|
178
|
+
3. Adds them to the appropriate sections in your CHANGELOG.md
|
179
|
+
4. Trailers are case-insensitive (e.g., `fixed:`, `Fixed:`, `FIXED:` all work)
|
180
|
+
|
181
|
+
### Example Workflow
|
182
|
+
|
183
|
+
```bash
|
184
|
+
# Make your changes
|
185
|
+
git add .
|
186
|
+
git commit -m "Add export functionality
|
187
|
+
|
188
|
+
Added: CSV export for user data
|
189
|
+
Added: PDF report generation
|
190
|
+
Fixed: Date formatting in exports"
|
191
|
+
|
192
|
+
# Release (trailers are automatically extracted)
|
193
|
+
rake build:checksum
|
194
|
+
rake release
|
195
|
+
```
|
196
|
+
|
197
|
+
The changelog will be updated with the entries from your commit trailers.
|
198
|
+
|
199
|
+
## Development
|
133
200
|
|
134
|
-
|
201
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt.
|
135
202
|
|
136
|
-
|
203
|
+
## Releasing This Gem
|
137
204
|
|
138
|
-
|
139
|
-
|
205
|
+
1. Run `rake build:checksum` to build the gem and generate checksums
|
206
|
+
2. Run `rake release` to push to [rubygems.org](https://rubygems.org)
|
207
|
+
3. The version will automatically bump and the changelog will be updated
|
208
|
+
4. Push the changes to the repository
|
140
209
|
|
141
210
|
## Contributing
|
142
211
|
|
data/Rakefile
CHANGED
@@ -16,5 +16,7 @@ require_relative "lib/reissue/gem"
|
|
16
16
|
|
17
17
|
Reissue::Task.create :reissue do |task|
|
18
18
|
task.version_file = "lib/reissue/version.rb"
|
19
|
+
task.fragment = :git # Use git trailers for changelog entries
|
19
20
|
task.push_finalize = :branch
|
21
|
+
# Note: clear_fragments has no effect with :git
|
20
22
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative "parser"
|
2
2
|
require_relative "printer"
|
3
|
+
require_relative "fragment_handler"
|
3
4
|
|
4
5
|
module Reissue
|
5
6
|
# Updates the changelog file with new versions and changes.
|
@@ -18,9 +19,18 @@ module Reissue
|
|
18
19
|
# @param changes [Hash] The changes for the version (default: {}).
|
19
20
|
# @param changelog_file [String] The path to the changelog file (default: @changelog_file).
|
20
21
|
# @param version_limit [Integer] The number of versions to keep (default: 2).
|
21
|
-
|
22
|
-
|
22
|
+
# @param fragment [String] The fragment source configuration (default: nil).
|
23
|
+
# @param fragment_directory [String] @deprecated Use fragment instead
|
24
|
+
def call(version, date: "Unreleased", changes: {}, changelog_file: @changelog_file, version_limit: 2, retain_changelogs: false, fragment: nil, fragment_directory: nil)
|
25
|
+
# Handle deprecation
|
26
|
+
if fragment_directory && !fragment
|
27
|
+
warn "[DEPRECATION] `fragment_directory` parameter is deprecated. Please use `fragment` instead."
|
28
|
+
fragment = fragment_directory
|
29
|
+
end
|
30
|
+
|
31
|
+
update(version, date:, changes:, version_limit:, fragment:)
|
23
32
|
write(changelog_file, retain_changelogs:)
|
33
|
+
|
24
34
|
changelog
|
25
35
|
end
|
26
36
|
|
@@ -45,11 +55,23 @@ module Reissue
|
|
45
55
|
# @param date [String] The release date (default: "Unreleased").
|
46
56
|
# @param changes [Hash] The changes for the version (default: {}).
|
47
57
|
# @param version_limit [Integer] The number of versions to keep (default: 2).
|
58
|
+
# @param fragment [String] The fragment source configuration (default: nil).
|
48
59
|
# @return [Hash] The updated changelog.
|
49
|
-
def update(version, date: "Unreleased", changes: {}, version_limit: 2)
|
60
|
+
def update(version, date: "Unreleased", changes: {}, version_limit: 2, fragment: nil)
|
50
61
|
@changelog = Parser.parse(File.read(@changelog_file))
|
51
62
|
|
52
|
-
|
63
|
+
# Merge fragment changes if source is provided
|
64
|
+
merged_changes = changes.dup
|
65
|
+
if fragment
|
66
|
+
handler = FragmentHandler.for(fragment)
|
67
|
+
fragment_changes = handler.read
|
68
|
+
fragment_changes.each do |section, entries|
|
69
|
+
merged_changes[section] ||= []
|
70
|
+
merged_changes[section].concat(entries)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
changelog["versions"].unshift({"version" => version, "date" => date, "changes" => merged_changes})
|
53
75
|
changelog["versions"] = changelog["versions"].first(version_limit)
|
54
76
|
changelog
|
55
77
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Reissue
|
6
|
+
# Handler for reading fragments from a directory
|
7
|
+
class DirectoryFragmentHandler < FragmentHandler
|
8
|
+
DEFAULT_VALID_SECTIONS = %w[added changed deprecated removed fixed security].freeze
|
9
|
+
|
10
|
+
attr_reader :directory, :valid_sections
|
11
|
+
|
12
|
+
# Initialize the handler with a directory path
|
13
|
+
#
|
14
|
+
# @param directory [String] The path to the fragments directory
|
15
|
+
# @param valid_sections [Array<String>, nil] List of valid section names, or nil to allow all
|
16
|
+
def initialize(directory, valid_sections: DEFAULT_VALID_SECTIONS)
|
17
|
+
@directory = directory
|
18
|
+
@fragment_directory = Pathname.new(directory)
|
19
|
+
@valid_sections = valid_sections
|
20
|
+
end
|
21
|
+
|
22
|
+
# Read fragments from the directory
|
23
|
+
#
|
24
|
+
# @return [Hash] A hash of changelog entries organized by category
|
25
|
+
def read
|
26
|
+
return {} unless @fragment_directory.exist?
|
27
|
+
|
28
|
+
fragments = {}
|
29
|
+
|
30
|
+
@fragment_directory.glob("*.*.md").each do |fragment_file|
|
31
|
+
filename = fragment_file.basename.to_s
|
32
|
+
parts = filename.split(".")
|
33
|
+
|
34
|
+
next unless parts.length == 3
|
35
|
+
|
36
|
+
section = parts[1].downcase
|
37
|
+
next unless valid_section?(section)
|
38
|
+
|
39
|
+
content = fragment_file.read.strip
|
40
|
+
next if content.empty?
|
41
|
+
|
42
|
+
# Capitalize section name for changelog format
|
43
|
+
section_key = section.capitalize
|
44
|
+
fragments[section_key] ||= []
|
45
|
+
fragments[section_key] << content
|
46
|
+
end
|
47
|
+
|
48
|
+
fragments
|
49
|
+
end
|
50
|
+
|
51
|
+
# Clear all fragment files from the directory
|
52
|
+
#
|
53
|
+
# @return [nil]
|
54
|
+
def clear
|
55
|
+
return unless @fragment_directory.exist?
|
56
|
+
|
57
|
+
@fragment_directory.glob("*.*.md").each(&:delete)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def valid_section?(section)
|
63
|
+
return true if @valid_sections.nil?
|
64
|
+
return false unless @valid_sections.is_a?(Array)
|
65
|
+
|
66
|
+
@valid_sections.map(&:downcase).include?(section.downcase)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reissue
|
4
|
+
class FragmentHandler
|
5
|
+
# Handles reading changelog entries from git commit trailers
|
6
|
+
class GitFragmentHandler < FragmentHandler
|
7
|
+
# Regex to match changelog section trailers in commit messages
|
8
|
+
TRAILER_REGEX = /^(Added|Changed|Deprecated|Removed|Fixed|Security):\s*(.+)$/i
|
9
|
+
|
10
|
+
# Valid changelog sections that can be used as trailers
|
11
|
+
VALID_SECTIONS = %w[Added Changed Deprecated Removed Fixed Security].freeze
|
12
|
+
|
13
|
+
# Read changelog entries from git commit trailers
|
14
|
+
#
|
15
|
+
# @return [Hash] A hash of changelog entries organized by section
|
16
|
+
def read
|
17
|
+
return {} unless git_available? && in_git_repo?
|
18
|
+
|
19
|
+
commits = commits_since_last_tag
|
20
|
+
parse_trailers_from_commits(commits)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Clear operation is a no-op for git trailers
|
24
|
+
#
|
25
|
+
# @return [nil]
|
26
|
+
def clear
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def git_available?
|
33
|
+
system("git --version", out: File::NULL, err: File::NULL)
|
34
|
+
end
|
35
|
+
|
36
|
+
def in_git_repo?
|
37
|
+
system("git rev-parse --git-dir", out: File::NULL, err: File::NULL)
|
38
|
+
end
|
39
|
+
|
40
|
+
def commits_since_last_tag
|
41
|
+
last_tag = find_last_tag
|
42
|
+
|
43
|
+
commit_range = if last_tag
|
44
|
+
# Get commits since the last tag
|
45
|
+
"#{last_tag}..HEAD"
|
46
|
+
else
|
47
|
+
# No tags found, get all commits
|
48
|
+
"HEAD"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get commit messages with trailers, in reverse order (oldest first)
|
52
|
+
output = `git log #{commit_range} --reverse --format=%B 2>/dev/null`
|
53
|
+
return [] if output.empty?
|
54
|
+
|
55
|
+
# Split by double newline to separate commits
|
56
|
+
output.split(/\n\n+/)
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_last_tag
|
60
|
+
# Try to find the most recent tag
|
61
|
+
tag = `git describe --tags --abbrev=0 2>/dev/null`.strip
|
62
|
+
tag.empty? ? nil : tag
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_trailers_from_commits(commits)
|
66
|
+
result = {}
|
67
|
+
|
68
|
+
commits.each do |commit|
|
69
|
+
# Split commit into lines and look for trailers
|
70
|
+
commit.lines.each do |line|
|
71
|
+
line = line.strip
|
72
|
+
next if line.empty?
|
73
|
+
|
74
|
+
if (match = line.match(TRAILER_REGEX))
|
75
|
+
section_name = normalize_section_name(match[1])
|
76
|
+
trailer_value = match[2].strip
|
77
|
+
|
78
|
+
result[section_name] ||= []
|
79
|
+
result[section_name] << trailer_value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
def normalize_section_name(name)
|
88
|
+
# Normalize to proper case (e.g., "FIXED" -> "Fixed", "added" -> "Added")
|
89
|
+
name.capitalize
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reissue
|
4
|
+
# Handler for when no fragment source is configured
|
5
|
+
class NullFragmentHandler < FragmentHandler
|
6
|
+
# Read fragments (returns empty hash since no source is configured)
|
7
|
+
#
|
8
|
+
# @return [Hash] An empty hash
|
9
|
+
def read
|
10
|
+
{}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Clear fragments (no-op since no source is configured)
|
14
|
+
#
|
15
|
+
# @return [nil]
|
16
|
+
def clear
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reissue
|
4
|
+
# Base class for handling fragment reading from various sources
|
5
|
+
class FragmentHandler
|
6
|
+
# Read fragments from the configured source
|
7
|
+
#
|
8
|
+
# @return [Hash] A hash of changelog entries organized by category
|
9
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
10
|
+
def read
|
11
|
+
raise NotImplementedError, "Subclasses must implement #read"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Clear fragments from the configured source
|
15
|
+
#
|
16
|
+
# @raise [NotImplementedError] Must be implemented by subclasses
|
17
|
+
def clear
|
18
|
+
raise NotImplementedError, "Subclasses must implement #clear"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Factory method to create the appropriate handler for the given option
|
22
|
+
#
|
23
|
+
# @param fragment_option [nil, String, Symbol] The fragment configuration
|
24
|
+
# @param valid_sections [Array<String>, nil] List of valid section names (for directory handler)
|
25
|
+
# @return [FragmentHandler] The appropriate handler instance
|
26
|
+
# @raise [ArgumentError] If the option is not supported
|
27
|
+
def self.for(fragment_option, valid_sections: nil)
|
28
|
+
case fragment_option
|
29
|
+
when nil
|
30
|
+
require_relative "fragment_handler/null_fragment_handler"
|
31
|
+
NullFragmentHandler.new
|
32
|
+
when String
|
33
|
+
require_relative "fragment_handler/directory_fragment_handler"
|
34
|
+
options = {}
|
35
|
+
options[:valid_sections] = valid_sections if valid_sections
|
36
|
+
DirectoryFragmentHandler.new(fragment_option, **options)
|
37
|
+
when :git
|
38
|
+
require_relative "fragment_handler/git_fragment_handler"
|
39
|
+
GitFragmentHandler.new
|
40
|
+
else
|
41
|
+
raise ArgumentError, "Invalid fragment option: #{fragment_option.inspect}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/reissue/rake.rb
CHANGED
@@ -36,6 +36,29 @@ module Reissue
|
|
36
36
|
# Provide a callable to decide how to store the files.
|
37
37
|
attr_accessor :retain_changelogs
|
38
38
|
|
39
|
+
# The fragment configuration for changelog entries.
|
40
|
+
# @return [String, nil] nil (disabled) or a directory path string for fragment files
|
41
|
+
# @example Using directory-based fragments
|
42
|
+
# task.fragment = "changelog_fragments"
|
43
|
+
# @note Default: nil (disabled)
|
44
|
+
attr_accessor :fragment
|
45
|
+
|
46
|
+
# @deprecated Use {#fragment} instead
|
47
|
+
def fragment_directory=(value)
|
48
|
+
warn "[DEPRECATION] `fragment_directory` is deprecated. Please use `fragment` instead."
|
49
|
+
self.fragment = value
|
50
|
+
end
|
51
|
+
|
52
|
+
# @deprecated Use {#fragment} instead
|
53
|
+
def fragment_directory
|
54
|
+
warn "[DEPRECATION] `fragment_directory` is deprecated. Please use `fragment` instead."
|
55
|
+
@fragment
|
56
|
+
end
|
57
|
+
|
58
|
+
# Whether to clear fragment files after processing.
|
59
|
+
# Default: false.
|
60
|
+
attr_accessor :clear_fragments
|
61
|
+
|
39
62
|
# Additional paths to add to the commit.
|
40
63
|
attr_writer :updated_paths
|
41
64
|
|
@@ -49,6 +72,9 @@ module Reissue
|
|
49
72
|
# Whether to commit the finalize change to the changelog. Default: true.
|
50
73
|
attr_accessor :commit_finalize
|
51
74
|
|
75
|
+
# Whether to commit the clear fragments change. Default: true.
|
76
|
+
attr_accessor :commit_clear_fragments
|
77
|
+
|
52
78
|
# Whether to branch and push the changes. Default: :branch.
|
53
79
|
# Requires commit_finialize to be true.
|
54
80
|
#
|
@@ -74,8 +100,11 @@ module Reissue
|
|
74
100
|
@updated_paths = []
|
75
101
|
@changelog_file = "CHANGELOG.md"
|
76
102
|
@retain_changelogs = false
|
103
|
+
@fragment = nil
|
104
|
+
@clear_fragments = false
|
77
105
|
@commit = true
|
78
106
|
@commit_finalize = true
|
107
|
+
@commit_clear_fragments = true
|
79
108
|
@push_finalize = false
|
80
109
|
@version_limit = 2
|
81
110
|
@version_redo_proc = nil
|
@@ -113,9 +142,17 @@ module Reissue
|
|
113
142
|
desc description
|
114
143
|
task name, [:segment] do |task, args|
|
115
144
|
segment = args[:segment] || "patch"
|
116
|
-
new_version = formatter.call(
|
145
|
+
new_version = formatter.call(
|
146
|
+
segment:,
|
147
|
+
version_file:,
|
148
|
+
version_limit:,
|
149
|
+
version_redo_proc:,
|
150
|
+
fragment: fragment
|
151
|
+
)
|
117
152
|
bundle
|
118
153
|
|
154
|
+
tasker["#{name}:clear_fragments"].invoke
|
155
|
+
|
119
156
|
system("git add -u")
|
120
157
|
if updated_paths&.any?
|
121
158
|
system("git add #{updated_paths.join(" ")}")
|
@@ -151,10 +188,17 @@ module Reissue
|
|
151
188
|
desc "Finalize the changelog for an unreleased version to set the release date."
|
152
189
|
task "#{name}:finalize", [:date] do |task, args|
|
153
190
|
date = args[:date] || Time.now.strftime("%Y-%m-%d")
|
154
|
-
version, date = formatter.finalize(
|
191
|
+
version, date = formatter.finalize(
|
192
|
+
date,
|
193
|
+
changelog_file:,
|
194
|
+
retain_changelogs:,
|
195
|
+
fragment: fragment
|
196
|
+
)
|
155
197
|
finalize_message = "Finalize the changelog for version #{version} on #{date}"
|
156
198
|
if commit_finalize
|
157
|
-
|
199
|
+
if finalize_with_branch?
|
200
|
+
tasker["#{name}:branch"].invoke("reissue/#{version}")
|
201
|
+
end
|
158
202
|
system("git add -u")
|
159
203
|
system("git commit -m '#{finalize_message}'")
|
160
204
|
tasker["#{name}:push"].invoke if push_finalize?
|
@@ -163,16 +207,81 @@ module Reissue
|
|
163
207
|
end
|
164
208
|
end
|
165
209
|
|
166
|
-
desc
|
210
|
+
desc <<~MSG
|
211
|
+
Create a new branch for the next version.
|
212
|
+
|
213
|
+
If the branch already exists it will be deleted and a new one will be created along with a new tag.
|
214
|
+
MSG
|
215
|
+
|
167
216
|
task "#{name}:branch", [:branch_name] do |task, args|
|
168
217
|
raise "No branch name specified" unless args[:branch_name]
|
169
|
-
|
218
|
+
branch_name = args[:branch_name]
|
219
|
+
# Force create branch by deleting if exists, then creating fresh
|
220
|
+
if system("git show-ref --verify --quiet refs/heads/#{branch_name}")
|
221
|
+
# Extract version from branch name (e.g., "reissue/0.4.1" -> "0.4.1")
|
222
|
+
version = branch_name.sub(/^reissue\//, "")
|
223
|
+
# Delete matching tag if it exists
|
224
|
+
system("git tag -d v#{version} 2>/dev/null || true")
|
225
|
+
# Delete the branch
|
226
|
+
system("git branch -D #{branch_name}")
|
227
|
+
end
|
228
|
+
system("git checkout -b #{branch_name}")
|
170
229
|
end
|
171
230
|
|
172
231
|
desc "Push the current branch to the remote repository."
|
173
232
|
task "#{name}:push" do
|
174
233
|
system("git push origin HEAD")
|
175
234
|
end
|
235
|
+
|
236
|
+
desc "Preview changelog entries that will be added from fragments or git trailers"
|
237
|
+
task "#{name}:preview" do
|
238
|
+
if fragment
|
239
|
+
require_relative "fragment_handler"
|
240
|
+
handler = Reissue::FragmentHandler.for(fragment)
|
241
|
+
entries = handler.read
|
242
|
+
|
243
|
+
if entries.empty?
|
244
|
+
puts "No changelog entries found."
|
245
|
+
if fragment == :git
|
246
|
+
puts " (No git trailers found since last version tag)"
|
247
|
+
else
|
248
|
+
puts " (No fragment files found in '#{fragment}')"
|
249
|
+
end
|
250
|
+
else
|
251
|
+
puts "Changelog entries that will be added:\n\n"
|
252
|
+
# Sort sections in Keep a Changelog order
|
253
|
+
section_order = %w[Added Changed Deprecated Removed Fixed Security]
|
254
|
+
sorted_sections = entries.keys.sort_by { |k| section_order.index(k) || 999 }
|
255
|
+
|
256
|
+
sorted_sections.each do |section|
|
257
|
+
items = entries[section]
|
258
|
+
puts "### #{section}\n"
|
259
|
+
items.each { |item| puts "- #{item}" }
|
260
|
+
puts
|
261
|
+
end
|
262
|
+
|
263
|
+
puts "Total: #{entries.values.flatten.count} entries across #{entries.keys.count} sections"
|
264
|
+
end
|
265
|
+
else
|
266
|
+
puts "Fragment handling is not configured."
|
267
|
+
puts "Set task.fragment to a directory path or :git to enable changelog fragments."
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
desc "Clear fragments"
|
272
|
+
task "#{name}:clear_fragments" do
|
273
|
+
# Clear fragments after release if configured
|
274
|
+
if fragment && clear_fragments
|
275
|
+
formatter.clear_fragments(fragment)
|
276
|
+
clear_message = "Clear changelog fragments"
|
277
|
+
if commit_clear_fragments
|
278
|
+
system("git add #{fragment}")
|
279
|
+
system("git commit -m '#{clear_message}'")
|
280
|
+
else
|
281
|
+
system("echo '#{clear_message}'")
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
176
285
|
end
|
177
286
|
end
|
178
287
|
end
|
data/lib/reissue/version.rb
CHANGED
data/lib/reissue.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative "reissue/version"
|
4
4
|
require_relative "reissue/version_updater"
|
5
5
|
require_relative "reissue/changelog_updater"
|
6
|
+
require_relative "reissue/fragment_handler"
|
6
7
|
|
7
8
|
# Reissue is a module that provides functionality for updating version numbers and changelogs.
|
8
9
|
module Reissue
|
@@ -14,6 +15,8 @@ module Reissue
|
|
14
15
|
# @param date [String] The release date. Default: Unreleased
|
15
16
|
# @param changes [Hash] The changes made in this release. Default: {}
|
16
17
|
# @param version_limit [Integer] The number of versions to retain in the changes. Default: 2
|
18
|
+
# @param fragment [String, nil] The fragment source configuration (directory path or nil to disable). Default: nil
|
19
|
+
# @param fragment_directory [String] @deprecated Use fragment parameter instead
|
17
20
|
#
|
18
21
|
# @return [String] The new version number.
|
19
22
|
def self.call(
|
@@ -24,13 +27,21 @@ module Reissue
|
|
24
27
|
date: "Unreleased",
|
25
28
|
changes: {},
|
26
29
|
version_limit: 2,
|
27
|
-
version_redo_proc: nil
|
30
|
+
version_redo_proc: nil,
|
31
|
+
fragment: nil,
|
32
|
+
fragment_directory: nil
|
28
33
|
)
|
34
|
+
# Handle deprecation
|
35
|
+
if fragment_directory && !fragment
|
36
|
+
warn "[DEPRECATION] `fragment_directory` parameter is deprecated. Please use `fragment` instead."
|
37
|
+
fragment = fragment_directory
|
38
|
+
end
|
39
|
+
|
29
40
|
version_updater = VersionUpdater.new(version_file, version_redo_proc:)
|
30
41
|
new_version = version_updater.call(segment, version_file:)
|
31
42
|
if changelog_file
|
32
43
|
changelog_updater = ChangelogUpdater.new(changelog_file)
|
33
|
-
changelog_updater.call(new_version, date:, changes:, changelog_file:, version_limit:, retain_changelogs:)
|
44
|
+
changelog_updater.call(new_version, date:, changes:, changelog_file:, version_limit:, retain_changelogs:, fragment:)
|
34
45
|
end
|
35
46
|
new_version
|
36
47
|
end
|
@@ -39,11 +50,41 @@ module Reissue
|
|
39
50
|
#
|
40
51
|
# @param date [String] The release date.
|
41
52
|
# @param changelog_file [String] The path to the changelog file.
|
53
|
+
# @param fragment [String, nil] The fragment source configuration (directory path or nil to disable). Default: nil
|
54
|
+
# @param fragment_directory [String] @deprecated Use fragment parameter instead
|
42
55
|
#
|
43
56
|
# @return [Array] The version number and release date.
|
44
|
-
def self.finalize(date = Date.today, changelog_file: "CHANGELOG.md", retain_changelogs: false)
|
57
|
+
def self.finalize(date = Date.today, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, fragment_directory: nil)
|
58
|
+
# Handle deprecation
|
59
|
+
if fragment_directory && !fragment
|
60
|
+
warn "[DEPRECATION] `fragment_directory` parameter is deprecated. Please use `fragment` instead."
|
61
|
+
fragment = fragment_directory
|
62
|
+
end
|
63
|
+
|
45
64
|
changelog_updater = ChangelogUpdater.new(changelog_file)
|
65
|
+
|
66
|
+
# If fragments are present, we need to update the unreleased version with them first
|
67
|
+
if fragment
|
68
|
+
# Get the current changelog to find the unreleased version
|
69
|
+
changelog = Parser.parse(File.read(changelog_file))
|
70
|
+
unreleased_version = changelog["versions"].find { |v| v["date"] == "Unreleased" }
|
71
|
+
|
72
|
+
if unreleased_version
|
73
|
+
# Update with fragment data
|
74
|
+
changelog_updater.update(
|
75
|
+
unreleased_version["version"],
|
76
|
+
date: "Unreleased",
|
77
|
+
changes: unreleased_version["changes"] || {},
|
78
|
+
fragment: fragment,
|
79
|
+
version_limit: changelog["versions"].size
|
80
|
+
)
|
81
|
+
changelog_updater.write(changelog_file, retain_changelogs: false)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Now finalize with the date
|
46
86
|
changelog = changelog_updater.finalize(date:, changelog_file:, retain_changelogs:)
|
87
|
+
|
47
88
|
changelog["versions"].first.slice("version", "date").values
|
48
89
|
end
|
49
90
|
|
@@ -86,4 +127,14 @@ module Reissue
|
|
86
127
|
retain_changelogs: false
|
87
128
|
)
|
88
129
|
end
|
130
|
+
|
131
|
+
# Clears all fragment files in the specified source.
|
132
|
+
#
|
133
|
+
# @param fragment [String] The fragment source configuration.
|
134
|
+
def self.clear_fragments(fragment)
|
135
|
+
return unless fragment
|
136
|
+
|
137
|
+
handler = FragmentHandler.for(fragment)
|
138
|
+
handler.clear
|
139
|
+
end
|
89
140
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reissue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jim Gay
|
@@ -36,6 +36,10 @@ files:
|
|
36
36
|
- Rakefile
|
37
37
|
- lib/reissue.rb
|
38
38
|
- lib/reissue/changelog_updater.rb
|
39
|
+
- lib/reissue/fragment_handler.rb
|
40
|
+
- lib/reissue/fragment_handler/directory_fragment_handler.rb
|
41
|
+
- lib/reissue/fragment_handler/git_fragment_handler.rb
|
42
|
+
- lib/reissue/fragment_handler/null_fragment_handler.rb
|
39
43
|
- lib/reissue/gem.rb
|
40
44
|
- lib/reissue/markdown.rb
|
41
45
|
- lib/reissue/parser.rb
|