Class: GemHadar::ChangelogGenerator

Inherits:
Object
  • Object
show all
Includes:
PromptTemplate, Utils
Defined in:
lib/gem_hadar/changelog_generator.rb

Overview

A class that generates changelog entries by analyzing Git history and AI processing

The ChangelogGenerator class provides functionality to create structured changelog entries based on Git commit history. It can generate individual changelog entries for specific version ranges or create complete changelogs including all version entries. The class integrates with AI models to produce human-readable changelog content by processing Git logs through configured prompts and models.

Examples:

Generating a changelog entry for a version range

generator = GemHadar::ChangelogGenerator.new
entry = generator.generate('v1.0.0', 'v1.2.0')

Generating a complete changelog

generator = GemHadar::ChangelogGenerator.new
generator.generate_full(STDOUT)

Adding changelog entries to an existing file

GemHadar::ChangelogGenerator.add_to_file('CHANGELOG.md')

Instance Method Summary collapse

Methods included from PromptTemplate

#default_changelog_prompt, #default_changelog_system_prompt, #default_git_release_prompt, #default_git_release_system_prompt, #default_version_bump_prompt, #default_version_bump_system_prompt

Methods included from Utils

#ask?, #xdg_config, #xdg_config_dir, #xdg_config_filename, #xdg_config_home

Constructor Details

#initialize(gem_hadar) ⇒ ChangelogGenerator

Returns a new instance of ChangelogGenerator.



29
30
31
# File 'lib/gem_hadar/changelog_generator.rb', line 29

def initialize(gem_hadar)
  @gem_hadar = gem_hadar
end

Instance Method Details

#add_to_file(filename) ⇒ Integer?

The add_to_file method appends new changelog entries to an existing changelog file

This method identifies the highest version already present in the changelog file, retrieves all subsequent version tags from the Git repository, and generates changelog entries for each consecutive pair of versions. It then inserts these entries into the file after the existing content, maintaining the chronological order of changes.

Parameters:

  • filename (String)

    the path to the changelog file to which entries will be added

Returns:

  • (Integer)

    the count of changelog entries inserted into the file

  • (nil)

    if no entries were added or if the file was empty

Raises:

  • (ArgumentError)

    if the changelog file does not exist or if no highest version is found



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/gem_hadar/changelog_generator.rb', line 158

def add_to_file(filename)
  highest_version = find_highest_version(filename)

  if highest_version
    versions = read_versions
    versions = versions.drop_while { |t| t < highest_version }
  else
    raise ArgumentError, "Could not find highest version in #{filename.inspect}"
  end

  return if versions.size < 2

  changelog = generate_changelog(versions)
  return if changelog.empty?
  inject_into_filename(filename, changelog)
end

#changelog_exist?TrueClass, FalseClass

The changelog_exist? method checks whether a changelog file exists in the project.

This method verifies the presence of a changelog file by checking if the file path determined by changelog_filename exists in the filesystem.

Returns:

  • (TrueClass, FalseClass)

    true if the changelog file exists, false otherwise



183
184
185
# File 'lib/gem_hadar/changelog_generator.rb', line 183

def changelog_exist?
  changelog_filename.exist?
end

#changelog_filenamePathname (private)

The changelog_filename method returns the Pathname object for the changelog file path.

This method accesses the changelog_filename attribute from the associated GemHadar instance and wraps it in a Pathname object for convenient file path manipulation.

Returns:

  • (Pathname)

    the Pathname object representing the changelog file path



219
220
221
# File 'lib/gem_hadar/changelog_generator.rb', line 219

def changelog_filename
  Pathname.new(@gem_hadar.changelog.filename)
end

#changelog_version_added?(version) ⇒ TrueClass, FalseClass

The changelog_version_added? method checks whether a specific version has already been added to the changelog file.

This method verifies if a given version is present in the changelog file by examining each line for a match with the version tag.

Parameters:

  • version (String)

    the version to check for in the changelog

Returns:

  • (TrueClass, FalseClass)

    true if the version is found in the changelog, false otherwise

Raises:

  • (ArgumentError)

    if the changelog file does not exist



199
200
201
202
203
204
205
206
207
# File 'lib/gem_hadar/changelog_generator.rb', line 199

def changelog_version_added?(version)
  version = GemHadar::VersionSpec[version]
  changelog_exist? or
    raise ArgumentError, "Changelog #{changelog_filename.to_s} doesn't exist!"
  File.new(changelog_filename).any? do |line|
    line =~ /#{version.tag}/ and return true
  end
  false
end

#find_highest_version(filename) ⇒ GemHadar::VersionSpec? (private)

The find_highest_version method extracts version specifications from a changelog file and returns the highest version found

This method reads through the specified file line by line, scanning for lines that match the pattern of a changelog entry header with a version number, and collects all found version specifications. It then determines and returns the version specification with the highest version number

Parameters:

  • filename (String)

    the path to the changelog file to process

Returns:

  • (GemHadar::VersionSpec, nil)

    the highest version specification found in the file, or nil if no versions are found



331
332
333
334
335
336
337
338
339
340
341
# File 'lib/gem_hadar/changelog_generator.rb', line 331

def find_highest_version(filename)
  File.open(filename, ?r) do |input|
    specs = []
    input.each do |line|
      line.scan(/^## \d{4}-\d{2}-\d{2} v(\d+\.\d+\.\d+)$/) do
        specs << GemHadar::VersionSpec[$1]
      end
    end
    specs.max_by(&:version)
  end
end

#generate(from, to = 'HEAD') ⇒ String

The generate method creates a changelog entry by analyzing Git history and AI processing.

This method retrieves the Git log for a specified range of commits, processes the log through an AI model using configured prompts, and formats the result into a markdown changelog entry with a date header.

Parameters:

  • from (String)

    the starting version or commit reference for the Git log range

  • to (String) (defaults to: 'HEAD')

    the ending version or commit reference for the Git log range, defaults to ‘HEAD’

Returns:

  • (String)

    a formatted markdown changelog entry including date and AI-generated content

  • (String)

    a minimal changelog entry with just date and version when no changes are found



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/gem_hadar/changelog_generator.rb', line 49

def generate(from, to = 'HEAD')
  from = GemHadar::VersionSpec[from]
  to   = GemHadar::VersionSpec[to]

  range = "#{from.tag}..#{to.tag}"

  log = `git log #{range}`
  $?.success? or raise "Failed to get git log for range #{range}"

  date = `git log -n1 --pretty='format:%cd' --date=short #{to.tag.inspect}`.chomp

  if log.strip.empty?
    return "\n## #{date} #{to.without_prefix.to_s}\n"
  end

  system          = xdg_config('gem_hadar', 'changelog_system_prompt.txt', default_changelog_system_prompt)
  prompt_template = xdg_config('gem_hadar', 'changelog_prompt.txt', default_changelog_prompt)
  prompt = prompt_template % { log_diff: log }

  response = ollama_generate(system:, prompt:)

  changes = response.gsub(/\t/, '  ')

  return "\n## #{date} #{to.tag}\n\n#{changes}\n"
end

#generate_changelog(versions, changelog: []) ⇒ Array<String> (private)

The generate_changelog method creates a series of changelog entries by processing consecutive version pairs.

This method takes an array of version specifications, iterates through them in pairs, and generates AI-powered changelog entries for each range. It uses the ollama_generate method to produce content for each version interval and collects the results in reverse order.

Parameters:

Returns:

  • (Array<String>)

    an array of changelog entry strings in reverse chronological order



270
271
272
273
274
275
276
277
278
279
# File 'lib/gem_hadar/changelog_generator.rb', line 270

def generate_changelog(versions, changelog: [])
  versions = versions.each_cons(2).
    with_infobar(total: versions.size - 1, label: 'Change')
  versions.each do |range_from, range_to|
    changelog << generate(range_from, range_to)
    +infobar
  end
  infobar.newline
  changelog.reverse
end

#generate_full(output) ⇒ String

The generate_full method creates a complete changelog by processing all version tags in the repository and generating entries for each consecutive pair of versions

This method retrieves all semantic version tags from the Git repository, sorts them, and generates changelog entries for each pair of consecutive versions. It also adds an initial entry for the first version with a “Start” marker

Parameters:

  • output (IO)

    the output stream to which the complete changelog will be written

Returns:

  • (String)

    a complete changelog including all version entries and a header

Raises:

  • (RuntimeError)

    if no version tags are found in the repository



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/gem_hadar/changelog_generator.rb', line 124

def generate_full(output)
  versions = read_versions

  unless versions.any?
    raise "No version tags found in repository"
  end

  first_version = versions.first
  date          = `git log -n1 --pretty='format:%cd' --date=short #{first_version.tag.to_s}`.chomp
  changelog     = ["\n## #{date} #{first_version.tag.to_s}\n\n* Start\n"]

  changelog = generate_changelog(versions, changelog:)

  changelog.unshift "# Changes\n"

  output << changelog.join("")
end

#generate_range(output, from, to) ⇒ Object

The generate_range method creates a changelog for a specific version range by processing Git log differences and AI-generated content

This method retrieves version tags within a specified range, filters them based on the provided version boundaries, generates changelog entries for each version in the range, and writes the complete changelog to the provided output stream

Parameters:

  • output (IO)

    the output stream to which the changelog will be written

  • from (String)

    the starting version or commit reference for the range

  • to (String)

    the ending version or commit reference for the range, defaults to ‘HEAD’



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/gem_hadar/changelog_generator.rb', line 89

def generate_range(output, from, to)
  from = GemHadar::VersionSpec[from]
  to   = GemHadar::VersionSpec[to]

  versions = read_versions

  unless versions.any?
    raise "No version tags found in repository"
  end

  versions = versions.select do |v|
    v >= from && v <= to
  end

  changelog = generate_changelog(versions)

  output << changelog.join("")
end

#inject_into_filename(filename, changelog) ⇒ Integer (private)

The inject_into_filename method inserts changelog entries into a specified file.

This method reads an existing file line by line and identifies the location of a “# Changes” header. When this header is found, it inserts the provided changelog entries immediately after the header and before the next empty line.

Parameters:

  • filename (String)

    the path to the file into which changelog entries will be injected

  • changelog (Array<String>)

    an array of changelog entry strings to be inserted into the file

Returns:

  • (Integer)

    the count of changelog entries inserted into the file



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/gem_hadar/changelog_generator.rb', line 293

def inject_into_filename(filename, changelog)
  File.open(filename) do |input|
    File.secure_write(filename) do |output|
      start_add = nil
      input.each do |line|
        if start_add.nil? && line =~ /^# Changes$/
          start_add = true
          output.puts line
          next
        end
        if start_add && line =~ /^$/
          changelog.each do |entry|
            output.puts entry
          end
          output.puts line
          start_add = false
          next
        end
        output.puts line
      end
    end
  end
  changelog.size
end

#ollama_generate(**opts) ⇒ String? (private)

The ollama_generate method delegates AI generation requests to the associated GemHadar instance.

This method acts as a proxy that forwards the provided options to the ollama_generate method of the parent GemHadar object, enabling AI-powered text generation using configured Ollama models.

Parameters:

  • opts (Hash)

    the options to pass to the AI generation method

Returns:

  • (String, nil)

    the generated response from the AI model or nil if generation fails



234
235
236
# File 'lib/gem_hadar/changelog_generator.rb', line 234

def ollama_generate(**opts)
  @gem_hadar.ollama_generate(**opts)
end

#read_versionsArray<GemHadar::VersionSpec> (private)

The read_versions method retrieves and processes semantic version tags from the Git repository.

This method fetches all Git tags from the repository, filters them to include only those that match semantic versioning patterns (containing three numeric components separated by dots), removes any ‘v’ prefix from the tags, and sorts the resulting version specifications in ascending order according to semantic versioning rules.

Returns:

  • (Array<GemHadar::VersionSpec>)

    an array of VersionSpec objects representing the semantic versions found in the repository, sorted in ascending order



250
251
252
253
254
255
# File 'lib/gem_hadar/changelog_generator.rb', line 250

def read_versions
  tags = `git tag`.lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp)

  versions = tags.map { |tag| GemHadar::VersionSpec[tag] }
  versions = versions.sort_by(&:version)
end