#!/usr/bin/env ruby # vim: set syntax=ruby ts=4 sw=4 noet : require 'pp' require 'date' require 'optparse' require 'yaml' class GitExtractor class Commit attr_accessor :author, :commit, :date, :note def initialize commit @commit = commit @note = nil @author = nil @date = nil end end class Range attr_accessor :time_start, :time_stop, :commits def initialize config, commit @config = config @time_stop = DateTime.parse(commit.date) @time_start = @time_stop - (@config[:range_granularity] * 3 / 24.0) @commits = [commit] self end def merge range # B -----[----]---- # A --[----]------ # = ---[------]---- # minimum of both new_start = if range.time_start < @time_start then range.time_start else @time_start end new_end = if range.time_stop >= @time_stop then range.time_stop else @time_stop end @time_start = new_start @time_stop = new_end @commits.concat range.commits end def overlap? range result = false # Ref ----[----]----- # overlapping : # A -[----]-------- # B -------[----]-- # C -[----------]-- # D ------[]------- # non-overlapping : # E -[]------------ # F -----------[]-- start_before_start = (range.time_start < @time_start) start_after_start = (range.time_start >= @time_start) start_after_stop = (range.time_start >= @time_stop) start_before_stop = (range.time_start < @time_stop) stop_before_stop = (range.time_stop < @time_stop) stop_after_stop = (range.time_stop >= @time_stop) stop_before_start = (range.time_stop < @time_start) stop_after_start = (range.time_stop >= @time_start) # A case if start_before_start and start_before_stop and stop_after_start and stop_before_stop then result = true end # B case if start_after_start and start_before_stop and stop_after_start and stop_after_stop then result = true end # C case if start_before_start and start_before_stop and stop_after_start and stop_after_stop then result = true end # D case if start_after_start and start_before_stop and stop_after_start and stop_before_stop then result = true end return result end def fixed_start return @time_start + (@config[:range_granularity]/24.0) end def diff return ("%.2f" % ((@time_stop - fixed_start).to_f * 24)).to_f end def to_s val = "(%s) %s - %s\n" % [diff, fixed_start, @time_stop] @commits.each do |commit| lines = [] unless @config[:author_filter_enable] then lines.push commit.author end lines.concat commit.note.split(/\n/) r = lines.map{ |s| "\t %s" % s }.join "\n" r[1] = '*' val += r + "\n" end return val end end class RangeList def initialize @ranges = [] end def add range merged = false merged_range = nil # merge @ranges.each do |old| #pp old if old.overlap? range then old.merge range merged_range = old merged = true break end end # add if needed if merged then @ranges.delete merged_range self.add merged_range else @ranges.push range end end def each @ranges.each do |r| yield r end end def sum result = 0 @ranges.each do |r| result += r.diff end return result end end def initialize # FIXME: accept multiple authors @config = { :author_filter_enable => false, :author_filter => ".*?", :date_filter_enable => false, :date_filter => ".*?", :input_dump => [], :output_dump => nil, :range_granularity => 0.5, # in decimal hours :verbose => false } @rangelist = nil end def parse_cmdline args opts = OptionParser.new do |opts| opts.banner = "Usage: #{File.basename $0} [options]" opts.on_tail("-v","--verbose", "Run verbosely") do |v| @config[:verbose] = true end opts.on_tail("-h","--help", "Show this help") do puts opts exit 0 end opts.on("-i","--input FILE", "Set input dump file") do |file| @config[:input_dump] << file end opts.on("-o","--output FILE", "Set output dump file") do |file| @config[:output_dump] = file end opts.on("-d","--date DATE", "Keep only commits since DATE") do |date| puts "set date filter to #{date}" @config[:date_filter] = DateTime.parse(date); @config[:date_filter_enable] = true end opts.on("-t","--time TIME", "Keep only commits on last TIME datys") do |time| puts "set time filter to latest #{time} days" @config[:date_filter] = DateTime.now - time.to_f; puts "set date filter to date = #{@config[:date_filter]}" @config[:date_filter_enable] = true end opts.on("-a","--author AUTHOR", "Keep only commits by AUTHOR") do |author| puts "set author filter to #{author}" @config[:author_filter] = author @config[:author_filter_enable] = true end # overlap : # opts.on("-s","--scotch GRANULARITY", "Use GRANULARITY (decimal hours) to merge ranges") do |granularity| puts "set scotch to #{granularity}" @config[:range_granularity] = granularity.to_f end end opts.parse! args end def analyze_git # git log # foreach, create time range (before) + logs process = IO.popen ["git", "log", "--date=iso", "--no-patch", "--","."] @rangelist = RangeList.new commit = nil loop do line = process.gets break if line.nil? # utf-8 fix ? # line.encode!( line.encoding, "binary", :invalid => :replace, :undef => :replace) line.strip! case line when /^commit (.*)$/ then id = $1 # merge ranges & push unless commit.nil? then range = Range.new @config, commit @rangelist.add range end commit = Commit.new id # puts "commit #{id}" when /^Author:\s*(.*?)\s*$/ then unless commit.nil? then commit.author = $1 if @config[:author_filter_enable] and (not commit.author =~ /#{@config[:author_filter]}/) then commit = nil # reject end end when /^Date:\s*(.*?)\s*$/ then unless commit.nil? then commit.date = $1 if @config[:date_filter_enable] and (DateTime.parse(commit.date) < @config[:date_filter]) then commit = nil # reject end end when /^\s*$/ then # skip else # add as note unless commit.nil? then commit.note = if commit.note.nil? then line else commit.note + "\n" + line end end end end end def analyze_dumps #read ranges @rangelist = RangeList.new @config[:input_dump].each do |filename| rangelist = YAML::load(File.open(filename,"r")) rangelist.each do |range| @rangelist.add range end end end def analyze if @config[:input_dump].empty? then analyze_git else analyze_dumps end end def export return if @config[:output_dump].nil? puts "Exporting to %s" % @config[:output_dump] File.open(@config[:output_dump], "w") do |file| file.puts YAML::dump(@rangelist) end end def report return if not @config[:output_dump].nil? @rangelist.each do |r| puts r.to_s + "\n" end puts "TOTAL: %.2f hours" % @rangelist.sum end end app = GitExtractor.new app.parse_cmdline ARGV app.analyze app.export app.report exit 0