#!/usr/bin/env ruby require 'ollama' include Ollama require 'tins/xt' def x(cmd) output = `#{cmd}` $?.success? or fail "failed to execute #{cmd.inspect}" output end def find_highest_version_tag(filename) File.open(filename, ?r) do |input| tags = [] input.each do |line| line.scan(/^## \d{4}-\d{2}-\d{2} v(\d+\.\d+\.\d+)$/) do tags << $1 end end tags.map(&:version).max end end def compute_change(range_from, range_to) range_from = range_from.to_s.sub(/\Av?/, ?v) if range_to.to_s == 'HEAD' range = "#{range_from}..HEAD" else range_to = range_to.to_s.sub(/\Av?/, ?v) range = "#{range_from}..#{range_to}" end log = x("git log #{range}") $?.success? or exit 1 date = x("git log -n1 --pretty='format:%cd' --date=short #{range_to}") if log.strip.empty? return <<~EOT ## #{date} #{range_to} EOT end base_url = ENV['OLLAMA_URL'] || 'http://%s' % ENV.fetch('OLLAMA_HOST') model = ENV.fetch('OLLAMA_MODEL', 'llama3.1') system = <<~EOT You are a Ruby programmer generating a change log entry in markdown syntax, summarizing the code changes for a new version in a professional way. EOT prompt = <<~EOT - Summarize the changes in the following git log messages as bullet points. - List significant changes as bullet points using markdown when applicable. - Mark all names and values for variables, methods, functions, and constants, you see in the messages as markdown code surrounded by backtick characters. - Mark all version numbers you see in the messages as markdown bold surrounded by two asterisk characters. - Don't refer to single commits by sha1 hash. - Don't add information about changes you are not sure about. - Don't output any additional chatty remarks, notes, introductions, communications, etc. #{log} EOT if ENV['DEBUG'].to_i == 1 STDERR.puts "system:\n#{system}" STDERR.puts "prompt:\n#{prompt}" end options = Ollama::Options.new( num_ctx: 8192, num_predict: 1024, temperature: 0, #seed: 1337, top_p: 1, min_p: 0.1, ) ollama = Client.new(base_url:, read_timeout: 120) changes = ollama.generate(model:, system:, prompt:, options:, stream: false) changes = changes.response.gsub(/\t/, ' ') return <<~EOT ## #{date} #{range_to} #{changes} EOT end x("git fetch --tags") case command = ARGV.shift when 'pending' last_version = x("git tag").lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map { _1.sub(/\Av/, '').version }.max if last_version puts compute_change(last_version, :HEAD) else fail 'need at least one version tag to work' end when 'current' version1, version2 = x("git tag").lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map { _1.sub(/\Av/, '').version }.sort.last(2) if version1 and version2 puts compute_change(version1, version2) else fail 'need at least two version tags to work' end when 'range' range = ARGV.shift if range =~ /\A(.+)\.\.(.+)\z/ range_from, range_to = $1, $2 puts compute_change(range_from, range_to) else fail "need range of the form v1.2.3..v1.2.4" end when 'full', 'add' ary = [] tags = x("git tag").lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map { _1.sub(/\Av/, '').version }.sort if command == 'full' date = x("git log -n1 --pretty='format:%cd' --date=short v#{tags.first}").chomp ary << <<~EOT ## #{date} v#{tags.first} * Start EOT tags.each_cons(2) do |range_from, range_to| ary << compute_change(range_from, range_to) end ary.reverse! ary.unshift <<~EOT # Changes EOT puts ary else filename = ARGV.shift or fail 'need file to add to' start_tag = find_highest_version_tag(filename) tags = tags.drop_while { |t| t < start_tag } ary = [] tags.each_cons(2) do |range_from, range_to| ary << compute_change(range_from, range_to) end ary.empty? and exit ary.reverse! 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 =~ /^$/ ary.each do |change| STDERR.puts change output.puts change end output.puts line start_add = false next end output.puts line end end end end else puts <<~end Usage: #{File.basename($0)} help|range|full|add|pending end end