git-timecost/bin/git-timetrack-log

343 lines
6.7 KiB
Ruby
Executable file

#!/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 - (1.5 / 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 + (0.5/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,
: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
end
opts.parse! args
end
def analyze_git
# git log
# foreach, create time range (before) + logs
process = IO.popen ["git", "log", "--date=iso"]
@rangelist = RangeList.new
commit = nil
loop do
line = process.gets
break if line.nil?
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
def exec
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