Re-organize in subdirectories.
This commit is contained in:
parent
fed30d62a2
commit
c4536b8cc5
8 changed files with 367 additions and 340 deletions
3
Gemfile
Normal file
3
Gemfile
Normal file
|
@ -0,0 +1,3 @@
|
|||
# A sample Gemfile
|
||||
source "https://rubygems.org"
|
||||
|
8
Gemfile.lock
Normal file
8
Gemfile.lock
Normal file
|
@ -0,0 +1,8 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
344
bin/git-timecost
344
bin/git-timecost
|
@ -6,350 +6,14 @@ require 'date'
|
|||
require 'optparse'
|
||||
require 'yaml'
|
||||
|
||||
class GitExtractor
|
||||
$:.insert 0, 'lib'
|
||||
require 'timecost'
|
||||
|
||||
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 = TimeCost::CLI.new
|
||||
app.parse_cmdline ARGV
|
||||
app.analyze
|
||||
app.export
|
||||
app.report
|
||||
|
||||
exit 0
|
||||
|
||||
|
|
6
lib/timecost.rb
Normal file
6
lib/timecost.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
require 'timecost/commit'
|
||||
require 'timecost/range'
|
||||
require 'timecost/rangelist'
|
||||
require 'timecost/cli'
|
||||
|
179
lib/timecost/cli.rb
Normal file
179
lib/timecost/cli.rb
Normal file
|
@ -0,0 +1,179 @@
|
|||
module TimeCost
|
||||
class CLI
|
||||
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
|
||||
end
|
||||
|
14
lib/timecost/commit.rb
Normal file
14
lib/timecost/commit.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
module TimeCost
|
||||
|
||||
class Commit
|
||||
attr_accessor :author, :commit, :date, :note
|
||||
def initialize commit
|
||||
@commit = commit
|
||||
@note = nil
|
||||
@author = nil
|
||||
@date = nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
107
lib/timecost/range.rb
Normal file
107
lib/timecost/range.rb
Normal file
|
@ -0,0 +1,107 @@
|
|||
|
||||
module TimeCost
|
||||
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
|
||||
end
|
46
lib/timecost/rangelist.rb
Normal file
46
lib/timecost/rangelist.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
|
||||
module TimeCost
|
||||
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
|
||||
end
|
Loading…
Reference in a new issue