Skip to content

Instantly share code, notes, and snippets.

@jesperronn
Last active November 17, 2022 06:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jesperronn/8418030 to your computer and use it in GitHub Desktop.
Save jesperronn/8418030 to your computer and use it in GitHub Desktop.
releasenotes script (with git)
#!/usr/bin/ruby
#
# release notes script
# takes two git tags and prints any changes between them
#
# usage:
# ./releasenotes.sh [from] [to] [releasename]
#
# with [from] and [to] being git tags
#configuration section
#if not empty, will have a separate section for special words, listed if there is something to pay attention to
SPECIAL_WORDS = %w{sonar delete reverted cleanup fix accident problem}
RELATIVE_PATH_ROOT='..'
#ignored words when counting word occurrences
IGNORED_WORDS = %w(a are and as be by can for from have i id if in is it new no not of on so the that this to we with)
WORD_OCCURENCE_THRESHOLD=3
WORD_OCCURENCE_MAX=50
#########################
#find the following mentions of 'user story'
user_story_expr=/(RQ|U\d|US|QC|bug|XL|DE) ?\d\d\d\d?/i
#historically mentions of user stories:
#RQ1200
#RQ 1200
#RQ:1211
#RQ-1539
#US2704
#U34623
#QC 272
#bug 274
#XL-2862
#DE160
#########################
require 'date'
from_tag=ARGV[0]
to_tag=ARGV[1]
release_date=ARGV[2]
raise "FATAL! Missing arguments. Usage: ./releasenotes [from] [to] " unless [2,3].include? ARGV.size
raise "FATAL! [from] and [to] must not be the same!" if ARGV[0] == ARGV[1]
puts "RELEASE NOTES"
puts "============="
puts "RELEASE Notes Here are some key figures derived from code changes in the given period."
puts
%x(mkdir -p .metadata)
FROMDATE = %x(git log -1 --pretty="%ad" --date=short #{from_tag}).strip
TODATE = %x(git log -1 --pretty="%ad" --date=short #{to_tag}).strip
TOTALCOMMITS = %x(git log --oneline --no-merges #{from_tag}..#{to_tag} | wc -l).strip
MOSTACTIVE = %x(git shortlog --summary --numbered --no-merges #{from_tag}..#{to_tag} > .metadata/AUTHORS.#{to_tag}.txt).strip
ALL_CHANGES = %x( git log --oneline --pretty="%s" #{from_tag}..#{to_tag} )
SHORTSTAT = %x( git diff --shortstat -w #{from_tag}..#{to_tag} )
dev_duration = Date.parse(TODATE) - Date.parse(FROMDATE)
release_delay = Date.parse(release_date) - Date.parse(TODATE) unless release_date.nil?
puts "From tag: #{from_tag}"
puts "To tag: #{to_tag}"
puts "Timespan: #{FROMDATE}..#{TODATE} (#{dev_duration.to_i} days)"
puts "Release date: #{release_date.nil? ? '?': release_date}"
puts " (released #{release_delay} days after last commit)" unless release_date.nil?
print ""
puts
puts "Number of commits: #{TOTALCOMMITS}"
puts " #{SHORTSTAT}"
puts;puts;puts
directories = %x(cat folderchanges.txt | grep -v "#" | grep -vE '^\s*$' )
puts
puts "Number of changes in specific areas:"
puts "===================================="
result = []
directories.lines.to_a.each do |dir|
out = %x(git log --oneline #{from_tag}..#{to_tag} -- "#{dir.strip}" | wc -l).strip
#puts dir
result << "#{dir.strip}: #{out}"
end
#puts result
inverted = []
result.each do |line|
items = line.split(": ")
inverted << "#{items[1].rjust(6)} #{items[0]}"
end
STARTS_WITH_ZERO = /^\s*0\s/
STRIP_COUNT=/^\s+\d+\s+/
TRAILING_SLASH=/\/$/
unknown_folders = inverted.select{|x| x.match(STARTS_WITH_ZERO)}
puts inverted.reject{|x| x.match(STARTS_WITH_ZERO)}.sort.reverse
puts
puts " Folders with no changes (or were not existing) for this period:"
puts unknown_folders.sort.map{|x| " " + x.gsub(STRIP_COUNT, '').sub(TRAILING_SLASH, '')}
puts;puts;puts
puts "User stories/defects mentioned in this release (#{TOTALCOMMITS} commits)"
puts "===================================="
matches = {}#hashmap with all RQ numbers as keys. Values [] of strings
ALL_CHANGES.split("\n").each do |line|
if line.match(user_story_expr)
rq_number = Regexp.last_match(0).upcase.gsub(/(:|-| )/, '')
matches[rq_number] = [] if matches[rq_number].nil?
matches[rq_number] << line
end
end
require 'yaml'
sorted_rqs = Hash[matches.sort]
puts sorted_rqs.map{|k,v| "#{k.rjust(8)} #{v.size.to_s.rjust(2)} changes"}
puts;puts;puts
puts "Most active committers:"
puts "===================================="
puts %x(cat .metadata/AUTHORS.#{to_tag}.txt)
puts;puts;puts
#awesome new way to get total number of changed lines
#first, generate list of authors which committed in this release:
# %x(git shortlog --summary --numbered --no-merges #{from_tag}..#{to_tag} | awk '{print $2}' > AUTHORS.txt)
#puts %x
#then sum up how many changes added from each commit (ignoring whitespace with '-w')
# lines from log is like this:
# 1 files changed, 8 insertions(+), 2 deletions(-)
# 1 files changed, 3 insertions(+), 6 deletions(-)
# 2 files changed, 33 insertions(+), 1 deletions(-)
#sum up these lines with awk '+='
puts "Active committers -- changes in individual commits, ignoring whitespace"
puts "===================================="
puts %x(for NAME in `cat .metadata/AUTHORS.#{to_tag}.txt| awk '{print $2}'`; do git log --pretty="%h %an" -w --shortstat --author $NAME #{from_tag}..#{to_tag} | grep "(+)" | awk -v n=$NAME '{changed += $1} {added += $4} { removed += $6} END {print n, changed, "files changed" , added, "insertions(+)", removed, "deletions(-)" }' ; done)
puts;puts;puts
puts "#hashtags mentioned this release (#{TOTALCOMMITS} commits)"
puts "===================================="
matches = {}
ALL_CHANGES.split("\n").each do |line|
if line.match(/#\w+/)
hashtag = Regexp.last_match(0)
matches[hashtag] = [] if matches[hashtag].nil?
matches[hashtag] << line
end
end
sorted_hashtags = Hash[matches.sort].map{|k,v| "#{v.size.to_s.rjust(5)} #{k}"}.sort.reverse
#mentioned_once = sorted_hashtags.reject!{|x| x.match(/^\s*1\s/)}
puts sorted_hashtags
#puts " [#{mentioned_once.size} other hashtags mentioned only once]"
puts;puts;puts
puts "#{WORD_OCCURENCE_MAX} Most used words in this release (in commit comments)"
puts "===================================="
raw = %x(git log --oneline --pretty="%s" #{from_tag}..#{to_tag} )
#to count occurrences
#example {"i"=>1, "was"=>2, "09809"=>1, "home"=>1, "yes"=>2, "you"=>1}
#from http://stackoverflow.com/questions/9675146/how-to-get-words-frequency-in-efficient-way-with-ruby
def count_words(words)
#words = string.split(' ')
frequency = Hash.new(0)
words.each { |word| frequency[word.downcase] += 1 }
frequency
end
arr = raw.scan(/\w+/)
arr2 = arr - IGNORED_WORDS
occurrences = count_words(arr2)
#occurrences = raw.scan(/\w+/).reduce(Hash.new{|i|0}){|res,w| break if IGNORED_WORDS.include? w; res[w.downcase]+=1;res}
#occurrences = raw.scan(/\w+/).each_with_object(Hash.new{|i|0}){|w,h| break if IGNORED_WORDS.include? w; h[w.downcase]+=1}
res = occurrences.sort_by{|k,v| -v}.select{|k,v| v >= WORD_OCCURENCE_THRESHOLD }
puts res[0..WORD_OCCURENCE_MAX].map{|a| "#{a[0]}:#{a[1]}" }.join(', ')
puts;puts;puts
puts "Full list of changes for this release"
puts "===================================="
%x{git log --oneline #{from_tag}..#{to_tag} --pretty="%s (%an) > .metadata/COMMITS.#{to_tag}.txt"}
puts "(will be in separate file)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment