Module: Reissue

Defined in:
lib/reissue.rb,
lib/reissue/gem.rb,
lib/reissue/rake.rb,
lib/reissue/parser.rb,
lib/reissue/printer.rb,
lib/reissue/version.rb,
lib/reissue/markdown.rb,
lib/reissue/version_updater.rb,
lib/reissue/fragment_handler.rb,
lib/reissue/changelog_updater.rb,
lib/reissue/fragment_handler/git_fragment_handler.rb,
lib/reissue/fragment_handler/null_fragment_handler.rb,
lib/reissue/fragment_handler/directory_fragment_handler.rb

Overview

Reissue is a module that provides functionality for updating version numbers and changelogs.

Defined Under Namespace

Modules: Gem, Versioning Classes: ChangelogUpdater, DirectoryFragmentHandler, FragmentHandler, Markdown, NullFragmentHandler, Parser, Printer, Task, VersionUpdater

Constant Summary collapse

DEFAULT_CHANGELOG_SECTIONS =
%w[Added Changed Deprecated Removed Fixed Security].freeze
INITIAL_CHANGES =
{
  "Added" => ["Initial release"]
}
VERSION =
"0.4.21"
RELEASE_DATE =
"2026-03-11"

Class Method Summary collapse

Class Method Details

.call(version_file:, changelog_file: "CHANGELOG.md", retain_changelogs: false, segment: "patch", date: "Unreleased", changes: {}, version_limit: 2, version_redo_proc: nil, fragment: nil, fragment_directory: nil, tag_pattern: nil) ⇒ String

Updates the version number and changelog.

Parameters:

  • version_file (String)

    The path to the version file.

  • changelog_file (String) (defaults to: "CHANGELOG.md")

    The path to the changelog file. Default: CHANGELOG.md

  • segment (String) (defaults to: "patch")

    The segment of the version number to update. Default: patch

  • date (String) (defaults to: "Unreleased")

    The release date. Default: Unreleased

  • changes (Hash) (defaults to: {})

    The changes made in this release. Default: {}

  • version_limit (Integer) (defaults to: 2)

    The number of versions to retain in the changes. Default: 2

  • fragment (String, nil) (defaults to: nil)

    The fragment source configuration (directory path or nil to disable). Default: nil

  • fragment_directory (String) (defaults to: nil)

    @deprecated Use fragment parameter instead

Returns:

  • (String)

    The new version number.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/reissue.rb', line 32

def self.call(
  version_file:,
  changelog_file: "CHANGELOG.md",
  retain_changelogs: false,
  segment: "patch",
  date: "Unreleased",
  changes: {},
  version_limit: 2,
  version_redo_proc: nil,
  fragment: nil,
  fragment_directory: nil,
  tag_pattern: nil
)
  # Handle deprecation
  if fragment_directory && !fragment
    warn "[DEPRECATION] `fragment_directory` parameter is deprecated. Please use `fragment` instead."
    fragment = fragment_directory
  end

  version_updater = VersionUpdater.new(version_file, version_redo_proc:)
  new_version = version_updater.call(segment, version_file:)
  version_updater.update_release_date("Unreleased", version_file:)
  if changelog_file
    changelog_updater = ChangelogUpdater.new(changelog_file)
    changelog_updater.call(new_version, date:, changes:, changelog_file:, version_limit:, retain_changelogs:, fragment:, tag_pattern:)
  end
  new_version
end

.changelog_sectionsObject



12
13
14
# File 'lib/reissue.rb', line 12

def self.changelog_sections
  @changelog_sections ||= DEFAULT_CHANGELOG_SECTIONS.dup
end

.changelog_sections=(sections) ⇒ Object



16
17
18
# File 'lib/reissue.rb', line 16

def self.changelog_sections=(sections)
  @changelog_sections = Array(sections).map(&:capitalize).uniq
end

.clear_fragments(fragment) ⇒ Object

Clears all fragment files in the specified source.

Parameters:

  • fragment (String)

    The fragment source configuration.



273
274
275
276
277
278
# File 'lib/reissue.rb', line 273

def self.clear_fragments(fragment)
  return unless fragment

  handler = FragmentHandler.for(fragment)
  handler.clear
end

.deferred_call(version_file:, changelog_file: "CHANGELOG.md", version_limit: 2, retain_changelogs: false, fragment: nil, tag_pattern: nil) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/reissue.rb', line 61

def self.deferred_call(version_file:, changelog_file: "CHANGELOG.md", version_limit: 2, retain_changelogs: false, fragment: nil, tag_pattern: nil)
  version_updater = VersionUpdater.new(version_file)
  version_updater.set_version("Unreleased", version_file:)
  version_updater.update_release_date("Unreleased", version_file:)

  if changelog_file
    changelog_updater = ChangelogUpdater.new(changelog_file)
    changelog_updater.call("Unreleased", date: nil, changes: {}, changelog_file:, version_limit:, retain_changelogs:, fragment:, tag_pattern:)
  end

  "Unreleased"
end

.deferred_finalize(date = Date.today, version: nil, segment: nil, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, tag_pattern: nil, version_file: nil, version_redo_proc: nil) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/reissue.rb', line 74

def self.deferred_finalize(date = Date.today, version: nil, segment: nil, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, tag_pattern: nil, version_file: nil, version_redo_proc: nil)
  resolved_version = if version&.match?(/^\d/)
    version
  elsif segment || version
    seg = segment || version
    resolved = resolve_version_from_tag(seg.to_s, tag_pattern: tag_pattern, version_redo_proc: version_redo_proc)
    unless resolved
      raise "Cannot determine next version. No git tags found and no explicit version provided. " \
            "Usage: rake reissue:finalize[patch] or rake reissue:finalize[1.2.0]"
    end
    resolved
  elsif fragment == :git
    handler = FragmentHandler.for(:git, tag_pattern: tag_pattern)
    bump = handler.read_version_bump
    if bump
      resolved = resolve_version_from_tag(bump.to_s, tag_pattern: tag_pattern, version_redo_proc: version_redo_proc)
      unless resolved
        raise "Cannot determine next version. No git tags found and no explicit version provided. " \
              "Usage: rake reissue:finalize[patch] or rake reissue:finalize[1.2.0]"
      end
      resolved
    else
      raise "Cannot determine next version. No git tags found and no version argument provided. " \
            "Usage: rake reissue:finalize[patch] or rake reissue:finalize[1.2.0]"
    end
  else
    raise "Cannot determine next version. No version argument provided. " \
          "Usage: rake reissue:finalize[patch] or rake reissue:finalize[1.2.0]"
  end

  if version_file
    version_updater = VersionUpdater.new(version_file)
    version_updater.set_version(resolved_version, version_file: version_file)
    version_updater.update_release_date(date.to_s, version_file: version_file)
  end

  changelog_updater = ChangelogUpdater.new(changelog_file)

  if fragment
    changelog = Parser.parse(File.read(changelog_file))
    unreleased = changelog["versions"].find { |v|
      v["version"] == "Unreleased" ||
        v["date"].nil? ||
        v["date"] == "Unreleased"
    }

    if unreleased
      changelog["versions"].delete(unreleased)
      changelog_updater.instance_variable_set(:@changelog, changelog)
      changelog_updater.write(changelog_file, retain_changelogs: false)

      handler = FragmentHandler.for(fragment, tag_pattern: tag_pattern)
      fragment_changes = handler.read

      merged_changes = (unreleased["changes"] || {}).dup
      fragment_changes.each do |section, entries|
        merged_changes[section] ||= []
        entries.each do |entry|
          merged_changes[section] << entry unless merged_changes[section].include?(entry)
        end
      end

      changelog_updater.update(
        "Unreleased",
        date: nil,
        changes: merged_changes,
        fragment: nil,
        version_limit: changelog["versions"].size + 1
      )
      changelog_updater.write(changelog_file, retain_changelogs: false)
    end
  end

  changelog = changelog_updater.finalize(date: date, changelog_file: changelog_file, retain_changelogs: retain_changelogs, resolved_version: resolved_version)
  changelog["versions"].first.slice("version", "date").values
end

.finalize(date = Date.today, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, fragment_directory: nil, tag_pattern: nil, version_file: nil) ⇒ Array

Finalizes the changelog for an unreleased version to set the release date.

Parameters:

  • date (String) (defaults to: Date.today)

    The release date.

  • changelog_file (String) (defaults to: "CHANGELOG.md")

    The path to the changelog file.

  • fragment (String, nil) (defaults to: nil)

    The fragment source configuration (directory path or nil to disable). Default: nil

  • fragment_directory (String) (defaults to: nil)

    @deprecated Use fragment parameter instead

Returns:

  • (Array)

    The version number and release date.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/reissue.rb', line 168

def self.finalize(date = Date.today, changelog_file: "CHANGELOG.md", retain_changelogs: false, fragment: nil, fragment_directory: nil, tag_pattern: nil, version_file: nil)
  # Handle deprecation
  if fragment_directory && !fragment
    warn "[DEPRECATION] `fragment_directory` parameter is deprecated. Please use `fragment` instead."
    fragment = fragment_directory
  end

  changelog_updater = ChangelogUpdater.new(changelog_file)

  # If fragments are present, we need to update the unreleased version with them first
  if fragment
    # Get the current changelog to find the unreleased version
    changelog = Parser.parse(File.read(changelog_file))
    unreleased_version = changelog["versions"].find { |v| v["date"] == "Unreleased" }

    if unreleased_version
      # Remove the unreleased version from the changelog to avoid duplication
      # when we call update (which does unshift)
      changelog["versions"].delete(unreleased_version)

      # Write the modified changelog (with unreleased version removed) back to file
      # This is necessary because update() re-parses from the file
      changelog_updater.instance_variable_set(:@changelog, changelog)
      changelog_updater.write(changelog_file, retain_changelogs: false)

      # Get fragment changes
      handler = FragmentHandler.for(fragment, tag_pattern:)
      fragment_changes = handler.read

      # Merge existing changes with fragment changes, deduplicating entries
      merged_changes = (unreleased_version["changes"] || {}).dup
      fragment_changes.each do |section, entries|
        merged_changes[section] ||= []
        # Only add entries that don't already exist
        entries.each do |entry|
          merged_changes[section] << entry unless merged_changes[section].include?(entry)
        end
      end

      # Update with merged data (this will unshift the version back into the array)
      changelog_updater.update(
        unreleased_version["version"],
        date: "Unreleased",
        changes: merged_changes,
        fragment: nil,  # Don't read fragments again since we already merged them
        version_limit: changelog["versions"].size + 1  # +1 because we removed one
      )
      changelog_updater.write(changelog_file, retain_changelogs: false)
    end
  end

  # Now finalize with the date
  changelog = changelog_updater.finalize(date:, changelog_file:, retain_changelogs:)

  if version_file
    version_updater = VersionUpdater.new(version_file)
    version_updater.update_release_date(date.to_s, version_file:)
  end

  changelog["versions"].first.slice("version", "date").values
end

.generate_changelog(location, changes: {}) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/reissue.rb', line 244

def self.generate_changelog(location, changes: {})
  template = <<~EOF
    # Changelog

    All notable changes to this project will be documented in this file.

    The format is based on [Keep a Changelog](http://keepachangelog.com/)
    and this project adheres to [Semantic Versioning](http://semver.org/).

    ## Unreleased

    ## [0.1.0]
  EOF

  File.write(location, template)
  changelog_updater = ChangelogUpdater.new(location)
  changelog_updater.call(
    "0.1.0",
    date: "Unreleased",
    changes: changes.empty? ? INITIAL_CHANGES : changes,
    changelog_file: location,
    version_limit: 1,
    retain_changelogs: false
  )
end

.greeksObject



2
3
4
# File 'lib/reissue/version_updater.rb', line 2

module_function def greeks
  %w[alpha beta gamma delta epsilon zeta eta theta kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega]
end

.reformat(file, version_limit: 2, retain_changelogs: false) ⇒ Object

Reformats the changelog file to ensure it is correctly formatted.

Parameters:

  • file (String)

    The path to the changelog file.

  • version_limit (Integer) (defaults to: 2)

    The number of versions to retain in the changelog. Default: 2

  • retain_changelogs (Boolean, String, Proc) (defaults to: false)

    Whether to retain the changelog files for the previous versions.



235
236
237
238
# File 'lib/reissue.rb', line 235

def self.reformat(file, version_limit: 2, retain_changelogs: false)
  changelog_updater = ChangelogUpdater.new(file)
  changelog_updater.reformat(version_limit:, retain_changelogs:)
end