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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ec9aad4eae0a3eaf769c1ae42cc58ca21ad704ff0b82697782fc882600902e7
4
- data.tar.gz: d98907cd5df4104320b6793caafe4d33333be534c253172b7b6bb7090ff2504e
3
+ metadata.gz: 58c406237f6e7ef96ad4a4e1ed1aa2ba3e4cb10145edd6d44bff791a8e37d5be
4
+ data.tar.gz: 26b103713df3d7de9c1f9af49df079a2e9954b9b7e96a58bc8e3e203607e531d
5
5
  SHA512:
6
- metadata.gz: 16072744735112068c9b5a4475d2d6d917fad5ffa82772ea59989380f85672a32fe4c6b226cb8db00eef74b7dd1bf328c23f4f60cef42228eb79058cb3f29568
7
- data.tar.gz: 5554eec13624f10d14bc59fd2b2428f7a1e5803d3a0af15c90810d62490dc8875eba2d57eb8966ec88dd900607dc14c83ae967dba9fb2f658c1e0d3df05724b3
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.0] - 2025-05-08
8
+ ## [0.4.2] - 2025-09-16
9
9
 
10
10
  ### Changed
11
11
 
12
- - Update reformat task to create a new changelog if it doesn't exist
12
+ - New version was never created after the last release.
13
13
 
14
- ## [0.3.4] - Unreleased
14
+ ## [0.4.2] - 2025-09-16
15
15
 
16
- ## [0.3.3] - 2025-01-17
17
-
18
- ### Added
19
-
20
- - CODEOWNERS file
21
-
22
- ### Fixed
16
+ ### Changed
23
17
 
24
- - Properly retain changelog on `reissue:finalize`
18
+ - New version was never created after the last release.
data/README.md CHANGED
@@ -1,142 +1,211 @@
1
1
  # Reissue
2
2
 
3
- Prepare the releases of your Ruby gems with ease.
3
+ Automate your Ruby gem releases with proper versioning and changelog management.
4
4
 
5
- When creating versioned software, it is important to keep track of the changes and the versions that are released.
5
+ Keep your version numbers and changelogs consistent and up-to-date with minimal effort.
6
6
 
7
- After each release of a gem, you should immediatly bump the version with a new number and update the changelog with a place
8
- to capture the information about new changes.
7
+ ## Bottom Line Up Front
9
8
 
10
- Reissue helps you to prepare the next version of your project by providing tools which will update version numbers and
11
- update the changelog with a new version and a date.
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
- Use Reissue to prepare your first commit going into the new version.
12
+ With Reissue, the process remains the same, but you get automatic version bumping and changelog management included.
14
13
 
15
- Standard procedure for releasing projects with Reissue:
14
+ Also supports non-gem Ruby projects.
16
15
 
17
- 1. Create a version with some number like 0.1.0.
18
- 2. Add commits to the project. These will be associated with the version 0.1.0.
19
- 3. When you are releasing your project, finalize it by running
20
- `rake reissue:finalize` to update the Unreleased version in your changelog.
21
- 4. Bump the version to 0.1.1 with `rake reissue[patch]` and commit those changes.
22
- Future commits will be associated with the version 0.1.1 until your next release.
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
- Install the gem and add to the application's Gemfile by executing:
29
+ Add to your application's Gemfile:
27
30
 
28
31
  $ bundle add reissue
29
32
 
30
- If bundler is not being used to manage dependencies, install the gem by executing:
33
+ Or install directly:
31
34
 
32
35
  $ gem install reissue
33
36
 
34
37
  ## Usage
35
38
 
36
- If you are working with a gem, you can add the following to the Rakefile:
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: The file to update with the new version number.
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 will add the following rake tasks:
48
-
49
- - `rake reissue[segment]` - Prepare a new version for future work for the given
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
- This will also update the `build` task from rubygems to first run
61
- `reissue:finalize` and then build the gem, ensuring that your changelog is
62
- up-to-date before the gem is built.
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
- It updates the `release` task from rubygems to run `reissue` after the gem is
65
- pushed to rubygems.
63
+ ### Non-Gem Projects
66
64
 
67
- Build your own release with rake tasks provided by the gem.
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
- When creating your task, you have additional options to customize the behavior:
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
- # Optional: The name of the task. Defaults to "reissue".
88
- task.name = "your_name_and_namespace"
77
+ ### Configuration Options
89
78
 
90
- # Optional: The description of the main task.
91
- task.description = "Prepare the next version of the gem."
79
+ All available configuration options:
92
80
 
93
- # Required: The file to update with the new version number.
94
- task.version_file = "path/to/version.rb"
95
-
96
- # Optional: The number of versions to maintain in the changelog. Defaults to 2.
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: A Proc to format the version number. Receives a Gem::Version object, and segment.
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
- # Optional: The file to update with the new version number. Defaults to "CHANGELOG.md".
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
- # Optional: Whether to commit the changes automatically. Defaults to true.
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
- # Optional: Whether or not to commit the results of the finalize task. Defaults to true.
121
- task.commit_finalize = false
142
+ ### Configuration
122
143
 
123
- # Optional: Whether to push the changes automatically. Defaults to false.
124
- task.push_finalize = :branch # or false, or true to push the working branch
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
- ## Development
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
- 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 that will allow you to experiment.
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
- ## Releasing
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
- Run `rake build:checksum` to build the gem and generate the checksum. This will also update the version number in the gemspec file.
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
- Run `rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
203
+ ## Releasing This Gem
137
204
 
138
- This will leave a new commit with the version number incremented in the version file and the changelog updated with the new version.
139
- Push the changes to the repository.
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
- def call(version, date: "Unreleased", changes: {}, changelog_file: @changelog_file, version_limit: 2, retain_changelogs: false)
22
- update(version, date:, changes:, version_limit:)
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
- changelog["versions"].unshift({"version" => version, "date" => date, "changes" => changes})
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(segment:, version_file:, version_limit:, version_redo_proc:)
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(date, changelog_file:, retain_changelogs:)
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
- tasker["#{name}:branch"].invoke("reissue/#{version}") if finalize_with_branch?
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 "Create a new branch for the next version."
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
- system("git checkout -b #{args[:branch_name]}")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reissue
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
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.0
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