#!/usr/bin/ruby # Creates a compact overview of recent changes in an Subversion repository. # # Author:: Martin Ankerl (mailto:martin.ankerl@gmail.com) # Copyright:: Copyright (c) 2006-2009 Martin Ankerl # License:: New BSD License # # Homepage:: https://code.google.com/p/svn-shortlog/ # user configuration BEGIN user_config = { :repository => "http://svn.boost.org/svn/boost", :url => "http://svn.boost.org/svn/boost/trunk", # how to extract a library name from a path. Stops after first regexp matches :lib_regexp => [ /trunk\/libs\/([^\/]*)/, /trunk\/boost\/([^\/]*)/, /trunk\/tools\/([^\/]*)/ ], # replacements for in the content message :msg_gsubs => [ [ /\#(\d+)/, "\\0"] ], # start revision # A revision argument can be one of: # NUMBER revision number # '{' DATE '}' revision at start of the date # 'HEAD' latest in repository # 'BASE' base rev of item's working copy # 'COMMITTED' last commit at or before BASE # 'PREV' revision just before COMMITTED :start_rev => "{2009-12-01}", # stop revision :stop_rev => "{2009-12-31}", # footer :copyright => "Copyright © #{Time.now.year} Martin Ankerl", } # user config END # HTML header with CSS head = <<-'EOF'
EOF # here be dragons, modify only if you think you know what you are doing :-) require 'rexml/document' require 'date' require 'iconv' require 'set' # extend Date to get local time (hack for Ruby 1.8) class Date def to_gm_time to_time(new_offset, :gm) end def to_local_time to_time_hack(new_offset(DateTime.now.offset-offset), :local) end private def to_time_hack(dest, method) #Convert a fraction of a day to a number of microseconds usec = (dest.sec_fraction * 60 * 60 * 24 * (10**6)).to_i Time.send(method, dest.year, dest.month, dest.day, dest.hour, dest.min, dest.sec) end end class SvnShortlog include REXML def initialize(user_config, head) @start_time = Time.now @user_config = user_config @head = head end class Entry attr_accessor :author, :time, :paths, :msg, :rev end # h, s, v are between [0, 1[ def hsv_to_rgb(h, s, v) h_i = (h*6).to_i f = h*6 - h_i p = v * (1 - s) q = v * (1 - f*s) t = v * (1 - (1 - f) * s) r, g, b = v, t, p if h_i==0 r, g, b = q, v, p if h_i==1 r, g, b = p, v, t if h_i==2 r, g, b = p, q, v if h_i==3 r, g, b = t, p, v if h_i==4 r, g, b = v, p, q if h_i==5 r = (r*256).to_i g = (g*256).to_i b = (b*256).to_i return sprintf("%02x%02x%02x", r, g, b) end def htmlize(str, gsubs) str = str.gsub("<", "<") str.gsub!(">", ">") gsubs.each do |regexp, replacement| str.gsub!(regexp, replacement) end str end def rev_to_s(str) str = str.gsub(/[{}]/, "") end # creates array of Entry data blob from SVN XML. def parse_xml(doc) data = [] doc.elements.each('log/logentry') do |le| e = Entry.new e.author = le.elements["author"].text e.time = DateTime.parse(le.elements["date"].text).to_local_time e.paths = [] le.elements.each('paths/path') do |pa| e.paths.push [pa.attributes["action"], pa.text] end e.paths.sort! do |a, b| a[1] <=> b[1] end e.msg = le.elements["msg"].text e.msg = htmlize(e.msg, @user_config[:msg_gsubs]) if e.msg e.rev = le.attributes["revision"] data.push e end data end # reformat data blob def restructure(d) r = Hash.new { |h,k| h[k] = [] } d.each do |e| r[e.author].push e end r = r.to_a.sort end def path_to_lib(path) r = @user_config[:lib_regexp].find do |r| path.match(r) end if r m = path.match(r) m[1] else nil end end # create tag cloud using LIB_REGEX based on number of time used def tag_cloud(entries) # collect counts for each library h = Hash.new(0) entries.each do |e| e.paths.each do |kind, path| lib = path_to_lib(path) h[lib] += 1 if lib end end min_size = 11 max_size = 30 max = h.values.max h = h.to_a.sort.map do |lib, count| # s = min_size + count * (max_size - min_size) / max # linear s = (min_size + Math.sqrt(count * max) * (max_size - min_size) / max).to_i # quadratic "#{lib}" end ""
out.puts "#{e.rev} by #{e.author} on #{e.time.strftime("%b %d %H:%M")}"
out.print "
#{e.msg.strip}
"
e.paths.each do |kind, path|
out.puts "#{kind} #{path}"
end
out.puts ""
out.puts "