-
-
Save Razoxane/f8ac0f01dbc34562e2df34fc1a583a37 to your computer and use it in GitHub Desktop.
Generate release notes from git commit messages
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
# A script to automate changelog generation from Git commit messages | |
# | |
# For use with a git-flow workflow, it will take changes from the last tagged release | |
# where commit messages contain NEW, FIXED, and IMPROVED keywords and sort and fromat | |
# them into a Markdown release note list. | |
# | |
# The script takes version information from the macOS command agvtool and bases | |
# the product name on the first matching Xcode Info.plist found | |
# Constant CL_STRINGS: Strings for section titles and keywords | |
# :title is what is displayed in output | |
# :rx is the regex search to match each type, case sensitive | |
CL_STRINGS = { | |
'new' => { :title => "NEW", :rx => "(NEW|ADD(ED)?)" }, | |
'improved' => { :title => "IMPROVED", :rx => "IMPROV(MENT|ED)?" }, | |
'fixed' => { :title => "FIXED", :rx => "FIX(ED)?" } | |
} | |
class String | |
def cap_first | |
self.sub(/^([a-z])(.*)$/) { $1.upcase << $2 } | |
end | |
def clean_entry | |
rx = "(?:%s)" % CL_STRINGS.map{|k,v| | |
v[:rx] | |
}.join('|') | |
return self.sub(/(- )?#{rx}:? */,'').cap_first | |
end | |
end | |
class Change < Hash | |
attr_accessor :githash, :date, :title, :changes | |
def initialize(githash, date, title, changes) | |
@githash = githash | |
@date = date | |
@title = title | |
@changes = changes | |
self | |
end | |
end | |
class ChangeSet | |
attr_accessor :new, :improved, :fixed | |
def initialize | |
@new = [] | |
@improved = [] | |
@fixed = [] | |
end | |
def add(type, change) | |
case type | |
when 'new' | |
@new.push change | |
when 'improved' | |
@improved.push change | |
when 'fixed' | |
@fixed.push change | |
end | |
end | |
def get(type) | |
case type | |
when 'new' | |
return @new | |
when 'improved' | |
return @improved | |
when 'fixed' | |
return @fixed | |
end | |
return nil | |
end | |
end | |
class ChangeLog < Array | |
def initialize(change_array=[]) | |
return change_array | |
end | |
# returns array of :changes values | |
def changes | |
res = [] | |
self.each {|v| | |
chgs = v.changes.strip.split(/\n/) | |
chgs.delete_if {|e| e =~ /^\s*$/ } | |
res = res.concat(chgs) | |
} | |
res | |
end | |
def changes! | |
replace self.changes | |
end | |
end | |
class ChangeLogger | |
attr_reader :changes | |
def initialize | |
@changes = ChangeSet.new | |
@log = gitlog | |
sort_changes | |
end | |
def to_s | |
output = '' | |
res = {} | |
CL_STRINGS.each {|k,v| | |
res[k] = @changes.get(k) | |
} | |
res.each {|k,v| | |
output += "#### #{CL_STRINGS[k][:title]}\n\n" | |
output += "- #{v.join("\n- ")}\n\n" | |
} | |
header + output | |
end | |
private | |
def header | |
parts = %x{agvtool mvers -terse}.match(/".*?\/([^\/]+?)\.plist"=(\d+\.\d+\.\d+)/)[1,2] | |
header = "#{parts[0].sub(/-info/i,'').strip} #{parts[1].strip}" | |
build = %x{agvtool vers -terse}.strip | |
return header << %Q{ (#{build})\n-------------------------\n\n} | |
end | |
# returns ChangeLog | |
def gitlog | |
log = %x{git log \ | |
--pretty=format:'===%h%n%ci%n%s%n%b'\ | |
--since="$(git show -s --format=%ad $(git rev-list --tags --max-count=1))"}.strip | |
if (log && log.length > 0) | |
cl = ChangeLog.new | |
log.split(/^===/).each {|entry| | |
e = split_gitlog(entry.strip) | |
cl.push(e) if e.githash | |
} | |
return cl | |
end | |
raise "Error reading log items" | |
end | |
def split_gitlog(entry) | |
lines = entry.split(/\n/) | |
loghash = lines.shift | |
date = lines.shift | |
title = lines.shift | |
changes = lines.delete_if {|l| l.strip.empty? }.join("\n") | |
return Change.new(loghash, date, title, changes) | |
end | |
def sort_changes | |
rx = "(%s)" % CL_STRINGS.map {|k,v| v[:rx]}.join('|') | |
chgs = [] | |
@log.changes.each {|l| | |
chgs.concat(l.split("\n").delete_if {|ch| ch !~ /#{rx}/ }) | |
} | |
chgs.each {|change| | |
CL_STRINGS.each {|k,v| | |
if change =~ /#{v[:rx]}/ | |
@changes.add(k, change.clean_entry) | |
end | |
} | |
} | |
end | |
end | |
cl = ChangeLogger.new | |
$stdout.puts cl.to_s |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment