From c9a413a1d6be6cc2cb992f9e0a9dd872a38f81bb Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Wed, 24 Sep 2014 09:17:51 +0200 Subject: [PATCH 01/20] Rename project for better visibility. --- README.md | 12 ++++++------ bin/{git-timetrack-log => git-timecost} | 0 2 files changed, 6 insertions(+), 6 deletions(-) rename bin/{git-timetrack-log => git-timecost} (100%) diff --git a/README.md b/README.md index e9fc91c..52e2bc4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -TimeTrack-Log for Git -===================== +TimeCost for Git +================ Use git logs to give an estimation of spent time on your projects. @@ -8,7 +8,7 @@ Installation ------------ * Clone the project somewhere -* Copy the ''bin/git-timetrack-log'' file in ''/usr/local/bin'' +* Copy the ''bin/git-timecost'' file in ''/usr/local/bin'' Usage ----- @@ -16,7 +16,7 @@ Usage To get the total time spent on your git project ``` -$ git timetrack-log +$ git timecost [...] @@ -32,7 +32,7 @@ TOTAL: 3.36 hours To get the time spent on your project since a given date ``` -$ git timetrack-log -d 2013-03-01 +$ git timecost -d 2013-03-01 set date filter to 2013-03-01 (1.0) 2013-09-23T13:02:39+02:00 - 2013-09-23T14:02:39+02:00 * Glenn Y. Rolland @@ -43,6 +43,6 @@ TOTAL: 1.00 hours For other possibilities ``` -$ git timetrack-log -h +$ git timecost -h ``` diff --git a/bin/git-timetrack-log b/bin/git-timecost similarity index 100% rename from bin/git-timetrack-log rename to bin/git-timecost From c4536b8cc5b8807ede0650172eb460e33b529f65 Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Wed, 24 Sep 2014 09:28:17 +0200 Subject: [PATCH 02/20] Re-organize in subdirectories. --- Gemfile | 3 + Gemfile.lock | 8 + bin/git-timecost | 344 +------------------------------------- lib/timecost.rb | 6 + lib/timecost/cli.rb | 179 ++++++++++++++++++++ lib/timecost/commit.rb | 14 ++ lib/timecost/range.rb | 107 ++++++++++++ lib/timecost/rangelist.rb | 46 +++++ 8 files changed, 367 insertions(+), 340 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 lib/timecost.rb create mode 100644 lib/timecost/cli.rb create mode 100644 lib/timecost/commit.rb create mode 100644 lib/timecost/range.rb create mode 100644 lib/timecost/rangelist.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..912a1f1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +# A sample Gemfile +source "https://rubygems.org" + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..fe80d20 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,8 @@ +GEM + remote: https://rubygems.org/ + specs: + +PLATFORMS + ruby + +DEPENDENCIES diff --git a/bin/git-timecost b/bin/git-timecost index 12c879e..4acc0b2 100755 --- a/bin/git-timecost +++ b/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 diff --git a/lib/timecost.rb b/lib/timecost.rb new file mode 100644 index 0000000..107d9fb --- /dev/null +++ b/lib/timecost.rb @@ -0,0 +1,6 @@ + +require 'timecost/commit' +require 'timecost/range' +require 'timecost/rangelist' +require 'timecost/cli' + diff --git a/lib/timecost/cli.rb b/lib/timecost/cli.rb new file mode 100644 index 0000000..b40d089 --- /dev/null +++ b/lib/timecost/cli.rb @@ -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 + diff --git a/lib/timecost/commit.rb b/lib/timecost/commit.rb new file mode 100644 index 0000000..27c2370 --- /dev/null +++ b/lib/timecost/commit.rb @@ -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 diff --git a/lib/timecost/range.rb b/lib/timecost/range.rb new file mode 100644 index 0000000..212ba50 --- /dev/null +++ b/lib/timecost/range.rb @@ -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 diff --git a/lib/timecost/rangelist.rb b/lib/timecost/rangelist.rb new file mode 100644 index 0000000..9352da6 --- /dev/null +++ b/lib/timecost/rangelist.rb @@ -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 From 1f5d7d66f0652f864f95285eac60c6203bc71100 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 09:31:45 +0200 Subject: [PATCH 03/20] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 52e2bc4..fffef6b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ TimeCost for Git ================ -Use git logs to give an estimation of spent time on your projects. - +Use git logs to give an estimation of spent time & costs of your projects. Installation ------------ From 91b37ffaa552760f8e7b5ab9eab802e2b0badc6e Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 15:52:12 +0200 Subject: [PATCH 04/20] Transform to gem. --- .gitignore | 14 ++++++++++++++ Gemfile | 2 ++ LICENSE.txt | 22 ++++++++++++++++++++++ README.md | 13 +++++++++++-- Rakefile | 2 ++ lib/timecost/version.rb | 3 +++ timecost.gemspec | 23 +++++++++++++++++++++++ 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 Rakefile create mode 100644 lib/timecost/version.rb create mode 100644 timecost.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae3fdc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.bundle +*.so +*.o +*.a +mkmf.log diff --git a/Gemfile b/Gemfile index 912a1f1..d924a13 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ # A sample Gemfile source "https://rubygems.org" +# Specify your gem's dependencies in timecost.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c67d230 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Glenn Y. Rolland + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index fffef6b..03ab5a8 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ Use git logs to give an estimation of spent time & costs of your projects. Installation ------------ -* Clone the project somewhere -* Copy the ''bin/git-timecost'' file in ''/usr/local/bin'' +Install the project with: + + $ gem install timecost Usage ----- @@ -45,3 +46,11 @@ For other possibilities $ git timecost -h ``` +Contributing +------------ + +1. Fork it ( https://github.com/[my-github-username]/timecost/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..809eb56 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" + diff --git a/lib/timecost/version.rb b/lib/timecost/version.rb new file mode 100644 index 0000000..4eec771 --- /dev/null +++ b/lib/timecost/version.rb @@ -0,0 +1,3 @@ +module Timecost + VERSION = "0.0.1" +end diff --git a/timecost.gemspec b/timecost.gemspec new file mode 100644 index 0000000..a053c91 --- /dev/null +++ b/timecost.gemspec @@ -0,0 +1,23 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'timecost/version' + +Gem::Specification.new do |spec| + spec.name = "timecost" + spec.version = Timecost::VERSION + spec.authors = ["Glenn Y. Rolland"] + spec.email = ["glenux@glenux.net"] + spec.summary = %q{TODO: Write a short summary. Required.} + spec.description = %q{TODO: Write a longer description. Optional.} + spec.homepage = "" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0") + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.6" + spec.add_development_dependency "rake", "~> 10.0" +end From 02010868fd9989bc626c3d6fcf6ee4a75b4b210d Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 19:46:02 +0200 Subject: [PATCH 05/20] Prepare for authors & costs managements. --- lib/timecost/range.rb | 12 ++++++++++-- lib/timecost/user.rb | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 lib/timecost/user.rb diff --git a/lib/timecost/range.rb b/lib/timecost/range.rb index 212ba50..b3b3a8b 100644 --- a/lib/timecost/range.rb +++ b/lib/timecost/range.rb @@ -1,10 +1,15 @@ module TimeCost class Range - attr_accessor :time_start, :time_stop, :commits + attr_accessor :time_start, :time_stop, :commits, :author def initialize config, commit @config = config + + # FIXME: First approximation for users + # later, we'll replace with @user = User.parse(commit.author) + @author = commit.author + @time_stop = DateTime.parse(commit.date) @time_start = @time_stop - (@config[:range_granularity] * 3 / 24.0) @commits = [commit] @@ -33,6 +38,9 @@ module TimeCost def overlap? range result = false + # return early result if ranges come from different authors + return false if (@author != range.author) + # Ref ----[----]----- # overlapping : # A -[----]-------- @@ -53,7 +61,6 @@ module TimeCost 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 @@ -105,3 +112,4 @@ module TimeCost end end end + diff --git a/lib/timecost/user.rb b/lib/timecost/user.rb new file mode 100644 index 0000000..8335fde --- /dev/null +++ b/lib/timecost/user.rb @@ -0,0 +1,8 @@ + +module TimeCost + class Range + # Return local user id for git user + def self.parse gituser + end + end +end From 2c4a1c0654cc151d2e437348e141378b9d5db50a Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 19:47:23 +0200 Subject: [PATCH 06/20] Prepare for TDD. --- Gemfile.lock | 11 +++++++++++ timecost.gemspec | 7 ++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index fe80d20..b9d44be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,19 @@ +PATH + remote: . + specs: + timecost (0.0.1) + GEM remote: https://rubygems.org/ specs: + minitest (4.7.5) + rake (10.1.0) PLATFORMS ruby DEPENDENCIES + bundler (~> 1.6) + minitest (~> 4.7.5) + rake (~> 10.0) + timecost! diff --git a/timecost.gemspec b/timecost.gemspec index a053c91..a0803f0 100644 --- a/timecost.gemspec +++ b/timecost.gemspec @@ -8,9 +8,9 @@ Gem::Specification.new do |spec| spec.version = Timecost::VERSION spec.authors = ["Glenn Y. Rolland"] spec.email = ["glenux@glenux.net"] - spec.summary = %q{TODO: Write a short summary. Required.} - spec.description = %q{TODO: Write a longer description. Optional.} - spec.homepage = "" + spec.summary = %q{Use GIT logs to give an estimation of spent time & costs of your projects.} + spec.description = %q{Use GIT logs to give an estimation of spent time & costs of your projects.} + spec.homepage = "https//github.com/glenux/git-timecost" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") @@ -20,4 +20,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.6" spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "minitest", "~> 4.7.5" end From 40f6a3350542cca50a92859bf5f7d7c8522d6752 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 20:10:12 +0200 Subject: [PATCH 07/20] Prepare for TDD (suite). --- Rakefile | 12 +++++++++++- specs/author_list_spec.rb | 10 ++++++++++ specs/spec_helper.rb | 12 ++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 specs/author_list_spec.rb create mode 100755 specs/spec_helper.rb diff --git a/Rakefile b/Rakefile index 809eb56..1c2876c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,12 @@ -require "bundler/gem_tasks" +require 'rake' +require "bundler/gem_tasks" +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.warning = true + t.verbose = true + t.libs << "spec" + t.test_files = FileList['spec/**/*_spec.rb'] +end +task :default => :test diff --git a/specs/author_list_spec.rb b/specs/author_list_spec.rb new file mode 100644 index 0000000..ed525b1 --- /dev/null +++ b/specs/author_list_spec.rb @@ -0,0 +1,10 @@ + + +require_relative 'spec_helper' +require 'minitest/spec' + +describe "simple failing test" do + it "fails" do + assert 1 < 0 + end +end diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100755 index 0000000..77bb0e4 --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,12 @@ + +require 'mark' +require 'minitest/unit' +require 'minitest/autorun' +require 'minitest/pride' + +#if __FILE__ == $0 +# $LOAD_PATH.unshift('lib', 'spec') +# Dir.glob('./spec/**/*_spec.rb') { |f| require f } +#end + + From 9b4c1418a73bae86b2dec7d205e8e220f3b92d78 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 20:11:05 +0200 Subject: [PATCH 08/20] Add basic support for author_list. --- bin/git-timecost | 2 ++ lib/timecost/author_list.rb | 24 ++++++++++++++++++++ lib/timecost/cli.rb | 1 + lib/timecost/{rangelist.rb => range_list.rb} | 0 lib/timecost/user.rb | 8 ------- 5 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 lib/timecost/author_list.rb rename lib/timecost/{rangelist.rb => range_list.rb} (100%) delete mode 100644 lib/timecost/user.rb diff --git a/bin/git-timecost b/bin/git-timecost index 4acc0b2..1258eff 100755 --- a/bin/git-timecost +++ b/bin/git-timecost @@ -14,6 +14,8 @@ app.parse_cmdline ARGV app.analyze app.export app.report +#app.report_ranges +#app.report_users exit 0 diff --git a/lib/timecost/author_list.rb b/lib/timecost/author_list.rb new file mode 100644 index 0000000..06ba002 --- /dev/null +++ b/lib/timecost/author_list.rb @@ -0,0 +1,24 @@ + +module TimeCost + class AuthorList + # Prepare an empty index (local) + def initialize + @count = 0 + @store = {} + end + + # Return local user id for git user + # FIXME: should handle multiple names for same user + def parse gitauthor + invert_store = @store.invert + result = 0 + if invert_store.include? gitauthor then + result = invert_store[gitauthor] + else + @store[gitauthor] = @count + result = @count + @count += 1 + end + end + end +end diff --git a/lib/timecost/cli.rb b/lib/timecost/cli.rb index b40d089..de2183b 100644 --- a/lib/timecost/cli.rb +++ b/lib/timecost/cli.rb @@ -17,6 +17,7 @@ module TimeCost :verbose => false } @rangelist = nil + @authorlist = nil end def parse_cmdline args diff --git a/lib/timecost/rangelist.rb b/lib/timecost/range_list.rb similarity index 100% rename from lib/timecost/rangelist.rb rename to lib/timecost/range_list.rb diff --git a/lib/timecost/user.rb b/lib/timecost/user.rb deleted file mode 100644 index 8335fde..0000000 --- a/lib/timecost/user.rb +++ /dev/null @@ -1,8 +0,0 @@ - -module TimeCost - class Range - # Return local user id for git user - def self.parse gituser - end - end -end From 41ad1e589287e76832fc33e500e9f1b7340b861a Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 20:12:04 +0200 Subject: [PATCH 09/20] Fix errors due to renaming *list to *_list. --- lib/timecost.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/timecost.rb b/lib/timecost.rb index 107d9fb..3775e42 100644 --- a/lib/timecost.rb +++ b/lib/timecost.rb @@ -1,6 +1,7 @@ require 'timecost/commit' require 'timecost/range' -require 'timecost/rangelist' +require 'timecost/author_list' +require 'timecost/range_list' require 'timecost/cli' From d82e0fcb5ce8aa834fd0a88f6c8f120e0ca519a0 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 20:13:11 +0200 Subject: [PATCH 10/20] Rename spec dir. --- {specs => spec}/author_list_spec.rb | 0 {specs => spec}/spec_helper.rb | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {specs => spec}/author_list_spec.rb (100%) rename {specs => spec}/spec_helper.rb (100%) diff --git a/specs/author_list_spec.rb b/spec/author_list_spec.rb similarity index 100% rename from specs/author_list_spec.rb rename to spec/author_list_spec.rb diff --git a/specs/spec_helper.rb b/spec/spec_helper.rb similarity index 100% rename from specs/spec_helper.rb rename to spec/spec_helper.rb From dc8f578396cc76d36493b4c3af1c34388f662fb6 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 24 Sep 2014 20:13:58 +0200 Subject: [PATCH 11/20] Disable dependencies to make TDD env ready. --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 77bb0e4..f31447e 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,5 @@ -require 'mark' +#require 'mark' require 'minitest/unit' require 'minitest/autorun' require 'minitest/pride' From 229fd0a42fed5fb5340996beaed4130db2bcc369 Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Thu, 25 Sep 2014 08:05:05 +0200 Subject: [PATCH 12/20] Added spec for author_list. --- Rakefile | 4 +-- lib/timecost/author_list.rb | 28 ++++++++++++++------- spec/author_list_spec.rb | 49 ++++++++++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Rakefile b/Rakefile index 1c2876c..d4b7f59 100644 --- a/Rakefile +++ b/Rakefile @@ -4,8 +4,8 @@ require "bundler/gem_tasks" require 'rake/testtask' Rake::TestTask.new do |t| - t.warning = true - t.verbose = true + #t.warning = true + #t.verbose = true t.libs << "spec" t.test_files = FileList['spec/**/*_spec.rb'] end diff --git a/lib/timecost/author_list.rb b/lib/timecost/author_list.rb index 06ba002..355952e 100644 --- a/lib/timecost/author_list.rb +++ b/lib/timecost/author_list.rb @@ -1,24 +1,34 @@ +require 'pp' + module TimeCost class AuthorList + class UnknownAuthor < RuntimeError ; end + # Prepare an empty index (local) def initialize @count = 0 - @store = {} + @author_to_id = {} end - # Return local user id for git user - # FIXME: should handle multiple names for same user - def parse gitauthor - invert_store = @store.invert - result = 0 - if invert_store.include? gitauthor then - result = invert_store[gitauthor] + def add author + if @author_to_id.include? author then + result = @author_to_id[author] else - @store[gitauthor] = @count + @author_to_id[author] = @count result = @count @count += 1 end end + + def alias author_ref, author_new + raise UnknownAuthor unless @author_to_id.include? author_ref + end + + # Return local user id for git user + # FIXME: should handle multiple names for same user + def parse author + return @author_to_id[author] + end end end diff --git a/spec/author_list_spec.rb b/spec/author_list_spec.rb index ed525b1..0f26e61 100644 --- a/spec/author_list_spec.rb +++ b/spec/author_list_spec.rb @@ -3,8 +3,51 @@ require_relative 'spec_helper' require 'minitest/spec' -describe "simple failing test" do - it "fails" do - assert 1 < 0 +require 'timecost' + +describe TimeCost::AuthorList do + let(:author_list) { TimeCost::AuthorList.new } + let(:author_first) { "foo@example.com" } + let(:author_second) { "bar@example.com" } + + describe '.new' do + it "can be created without arguments" do + assert_instance_of TimeCost::AuthorList, author_list + end + end + + describe '.add' do + it "must accept adding authors" do + assert_respond_to author_list, :add + + author_list.add "foo@example.com" + author_list.add "bar@example.com" + end + + it "must assign a different id to different authors" do + author_list.add "foo@example.com" + author_list.add "bar@example.com" + id_foo = author_list.parse "foo@example.com" + id_bar = author_list.parse "bar@example.com" + refute_equal id_foo, id_bar + end + end + + describe '.alias' do + it "must accept aliases for authors" do + assert_respond_to author_list, :alias + + author_list.add author_first + author_list.alias author_first, author_second + end + + it "must assign the same id to aliases authors" do + author_list.add author_first + author_list.alias author_first, author_second + + id_foo = author_list.parse author_first + id_bar = author_list.parse author_second + refute_equal id_foo, id_bar + end end end From d4efb11e4459e65b5b88d3b54600a0d4ed27a825 Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Thu, 25 Sep 2014 09:13:05 +0200 Subject: [PATCH 13/20] Final cleanup for per-author range. --- lib/timecost/cli.rb | 24 ++++++++++++++++++------ lib/timecost/range.rb | 8 ++++---- timecost.gemspec | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/timecost/cli.rb b/lib/timecost/cli.rb index de2183b..fad7298 100644 --- a/lib/timecost/cli.rb +++ b/lib/timecost/cli.rb @@ -16,7 +16,7 @@ module TimeCost :verbose => false } - @rangelist = nil + @rangelist = {} @authorlist = nil end @@ -81,7 +81,7 @@ module TimeCost "--","."] - @rangelist = RangeList.new + @rangelist = {} commit = nil loop do line = process.gets @@ -96,7 +96,11 @@ module TimeCost # merge ranges & push unless commit.nil? then range = Range.new @config, commit - @rangelist.add range + + if not @rangelist.include? commit.author then + @rangelist[commit.author] = RangeList.new + end + @rangelist[commit.author].add range end commit = Commit.new id # puts "commit #{id}" @@ -110,6 +114,7 @@ module TimeCost commit = nil # reject end + end when /^Date:\s*(.*?)\s*$/ then @@ -170,10 +175,17 @@ module TimeCost def report return if not @config[:output_dump].nil? - @rangelist.each do |r| - puts r.to_s + "\n" + @rangelist.each do |author,rangelist| + rangelist.each do |range| + puts range.to_s + "\n" + end end - puts "TOTAL: %.2f hours" % @rangelist.sum + total = 0 + @rangelist.each do |author,rangelist| + puts "SUB-TOTAL for %s: %.2f hours\n" % [author, rangelist.sum] + total += rangelist.sum + end + puts "TOTAL: %.2f hours" % total end end end diff --git a/lib/timecost/range.rb b/lib/timecost/range.rb index b3b3a8b..627d69f 100644 --- a/lib/timecost/range.rb +++ b/lib/timecost/range.rb @@ -97,12 +97,12 @@ module TimeCost end def to_s - val = "(%s) %s - %s\n" % [diff, fixed_start, @time_stop] + val = "(%s)\t%s - %s\n" % [diff, fixed_start, @time_stop] + unless @config[:author_filter_enable] then + val += "\tby %s\n" % @commits.first.author + end @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] = '*' diff --git a/timecost.gemspec b/timecost.gemspec index a0803f0..b244ab5 100644 --- a/timecost.gemspec +++ b/timecost.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.email = ["glenux@glenux.net"] spec.summary = %q{Use GIT logs to give an estimation of spent time & costs of your projects.} spec.description = %q{Use GIT logs to give an estimation of spent time & costs of your projects.} - spec.homepage = "https//github.com/glenux/git-timecost" + spec.homepage = "https://github.com/glenux/git-timecost" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") From 2c28d6bc421aee516341e11e7ec76569e50b5e88 Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Thu, 25 Sep 2014 09:19:36 +0200 Subject: [PATCH 14/20] Add todo-list. --- TODO.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..987d342 --- /dev/null +++ b/TODO.md @@ -0,0 +1,19 @@ +TODO +==== + +Fixes and ideas for the future + +## Per user scotch + +Different users have a different commit style & frequency. +We should be able to define a per-user scotch. + + +## Automatic scotch : Use median time between consecutive commits, per user + + def median(array) + sorted = array.sort + len = sorted.length + return (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0 + end + From 10392185893fb4f95f7e7b055396b48dad048cb6 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Thu, 25 Sep 2014 10:02:08 +0200 Subject: [PATCH 15/20] Improve readability of author_list spec. --- spec/author_list_spec.rb | 39 +++++++++++++++++++-------------------- spec/spec_helper.rb | 4 ++++ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/spec/author_list_spec.rb b/spec/author_list_spec.rb index 0f26e61..6c8d96e 100644 --- a/spec/author_list_spec.rb +++ b/spec/author_list_spec.rb @@ -1,52 +1,51 @@ require_relative 'spec_helper' -require 'minitest/spec' -require 'timecost' +require 'timecost/author_list' describe TimeCost::AuthorList do - let(:author_list) { TimeCost::AuthorList.new } - let(:author_first) { "foo@example.com" } - let(:author_second) { "bar@example.com" } + let(:list) { TimeCost::AuthorList.new } + let(:first) { "Foo " } + let(:second) { "Bar " } describe '.new' do it "can be created without arguments" do - assert_instance_of TimeCost::AuthorList, author_list + assert_instance_of TimeCost::AuthorList, list end end describe '.add' do it "must accept adding authors" do - assert_respond_to author_list, :add + assert_respond_to list, :add - author_list.add "foo@example.com" - author_list.add "bar@example.com" + list.add first + list.add second end it "must assign a different id to different authors" do - author_list.add "foo@example.com" - author_list.add "bar@example.com" - id_foo = author_list.parse "foo@example.com" - id_bar = author_list.parse "bar@example.com" + list.add first + list.add second + id_foo = list.parse first + id_bar = list.parse second refute_equal id_foo, id_bar end end describe '.alias' do it "must accept aliases for authors" do - assert_respond_to author_list, :alias + assert_respond_to list, :alias - author_list.add author_first - author_list.alias author_first, author_second + list.add first + list.alias first, second end it "must assign the same id to aliases authors" do - author_list.add author_first - author_list.alias author_first, author_second + list.add first + list.alias first, second - id_foo = author_list.parse author_first - id_bar = author_list.parse author_second + id_foo = list.parse first + id_bar = list.parse second refute_equal id_foo, id_bar end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f31447e..e208137 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,13 @@ #require 'mark' +# require 'minitest/unit' require 'minitest/autorun' +require 'minitest/spec' require 'minitest/pride' +$LOAD_PATH.unshift('../lib') + #if __FILE__ == $0 # $LOAD_PATH.unshift('lib', 'spec') # Dir.glob('./spec/**/*_spec.rb') { |f| require f } From b9b64d9e2d1fdad356d8626665c00cfc57e41c6f Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Fri, 26 Sep 2014 08:35:26 +0200 Subject: [PATCH 16/20] Add more specs. --- spec/author_list_spec.rb | 13 +++++++++++++ spec/range_list_spec.rb | 0 spec/range_spec.rb | 0 3 files changed, 13 insertions(+) create mode 100644 spec/range_list_spec.rb create mode 100644 spec/range_spec.rb diff --git a/spec/author_list_spec.rb b/spec/author_list_spec.rb index 6c8d96e..20ad4f3 100644 --- a/spec/author_list_spec.rb +++ b/spec/author_list_spec.rb @@ -32,6 +32,19 @@ describe TimeCost::AuthorList do end end + describe '.size' do + it "must be zero in the beginning" do + assert_equal list.size, 0 + end + + it "must grow while adding authors" do + list.add first + assert_equal list.size, 1 + list.add second + assert_equal list.size, 2 + end + end + describe '.alias' do it "must accept aliases for authors" do assert_respond_to list, :alias diff --git a/spec/range_list_spec.rb b/spec/range_list_spec.rb new file mode 100644 index 0000000..e69de29 diff --git a/spec/range_spec.rb b/spec/range_spec.rb new file mode 100644 index 0000000..e69de29 From df545555cbef6c7dedcbdd1f3ca685322c0a9e46 Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Fri, 26 Sep 2014 08:35:40 +0200 Subject: [PATCH 17/20] Add size method. --- lib/timecost/author_list.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/timecost/author_list.rb b/lib/timecost/author_list.rb index 355952e..966eaaa 100644 --- a/lib/timecost/author_list.rb +++ b/lib/timecost/author_list.rb @@ -30,5 +30,9 @@ module TimeCost def parse author return @author_to_id[author] end + + def size + return @author_to_id.keys.size + end end end From 8dafbeea991506ae1ef151b5a4c7b73931a9d1dc Mon Sep 17 00:00:00 2001 From: "@@@No user configured@@@" <@@@No user configured@@@> Date: Fri, 26 Sep 2014 09:42:09 +0200 Subject: [PATCH 18/20] Add more tests, for range, range_list and cli. --- bin/git-timecost | 1 - lib/timecost/cli.rb | 4 ++-- lib/timecost/range.rb | 14 ++++++++------ spec/cli_spec.rb | 16 ++++++++++++++++ spec/range_list_spec.rb | 26 ++++++++++++++++++++++++++ spec/range_spec.rb | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 spec/cli_spec.rb diff --git a/bin/git-timecost b/bin/git-timecost index 1258eff..5319863 100755 --- a/bin/git-timecost +++ b/bin/git-timecost @@ -6,7 +6,6 @@ require 'date' require 'optparse' require 'yaml' -$:.insert 0, 'lib' require 'timecost' app = TimeCost::CLI.new diff --git a/lib/timecost/cli.rb b/lib/timecost/cli.rb index fad7298..6a8b28b 100644 --- a/lib/timecost/cli.rb +++ b/lib/timecost/cli.rb @@ -95,7 +95,7 @@ module TimeCost id = $1 # merge ranges & push unless commit.nil? then - range = Range.new @config, commit + range = Range.new commit, granularity: @config[:range_granularity] if not @rangelist.include? commit.author then @rangelist[commit.author] = RangeList.new @@ -177,7 +177,7 @@ module TimeCost @rangelist.each do |author,rangelist| rangelist.each do |range| - puts range.to_s + "\n" + puts range.to_s(!@config[:author_filter_enable]) + "\n" end end total = 0 diff --git a/lib/timecost/range.rb b/lib/timecost/range.rb index 627d69f..ed0f11e 100644 --- a/lib/timecost/range.rb +++ b/lib/timecost/range.rb @@ -3,15 +3,17 @@ module TimeCost class Range attr_accessor :time_start, :time_stop, :commits, :author - def initialize config, commit - @config = config + GRANULARITY_DEFAULT = 0.5 + + def initialize commit, options = {} + @granularity = options[:granularity] || GRANULARITY_DEFAULT # FIXME: First approximation for users # later, we'll replace with @user = User.parse(commit.author) @author = commit.author @time_stop = DateTime.parse(commit.date) - @time_start = @time_stop - (@config[:range_granularity] * 3 / 24.0) + @time_start = @time_stop - (@granularity * 3 / 24.0) @commits = [commit] self end @@ -89,16 +91,16 @@ module TimeCost end def fixed_start - return @time_start + (@config[:range_granularity]/24.0) + return @time_start + (@granularity/24.0) end def diff return ("%.2f" % ((@time_stop - fixed_start).to_f * 24)).to_f end - def to_s + def to_s show_authors = true val = "(%s)\t%s - %s\n" % [diff, fixed_start, @time_stop] - unless @config[:author_filter_enable] then + if show_authors then val += "\tby %s\n" % @commits.first.author end @commits.each do |commit| diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb new file mode 100644 index 0000000..6e15f64 --- /dev/null +++ b/spec/cli_spec.rb @@ -0,0 +1,16 @@ + + +require_relative 'spec_helper' + +require 'timecost/cli' + +describe TimeCost::CLI do + let(:cli) { TimeCost::CLI.new } + + describe '.new' do + it "can be created without arguments" do + assert_instance_of TimeCost::CLI, cli + end + end + +end diff --git a/spec/range_list_spec.rb b/spec/range_list_spec.rb index e69de29..20e83b5 100644 --- a/spec/range_list_spec.rb +++ b/spec/range_list_spec.rb @@ -0,0 +1,26 @@ + +require_relative 'spec_helper' + +require 'timecost/range_list' + +describe TimeCost::RangeList do + let(:list) { TimeCost::RangeList.new } + + describe '.new' do + it "can be created without arguments" do + assert_instance_of TimeCost::RangeList, list + end + end + + it "is empty at start" do + end + + it "can insert ranges" do + end + + it "can merge overlapping ranges" do + end + + it "cumulates non-overlapping ranges" do + end +end diff --git a/spec/range_spec.rb b/spec/range_spec.rb index e69de29..17fba79 100644 --- a/spec/range_spec.rb +++ b/spec/range_spec.rb @@ -0,0 +1,35 @@ + +require_relative 'spec_helper' + +require 'timecost/range' + +describe TimeCost::Range do + let(:config) do { granularity: 0.5 } end + + let(:commitA) { nil } + let(:commitB) { nil } + let(:commitC) { nil } + let(:commitD) { nil } + + let(:rangeA) { + TimeCost::Range.new commitA, config + } + + describe '.new' do + it "can be created from " do + assert_instance_of TimeCost::Range, rangeA + end + end + + describe '.overlap?' do + it "must respond to .overlap?" do + end + + it "must return false when ranges are not overlapping" do + end + + it "must return true when ranges are overlapping" do + end + end +end + From c2d9165394b068dc28c9aff15fcd3c0ea4a14984 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 25 Feb 2015 17:15:35 +0100 Subject: [PATCH 19/20] Add better date filters & bump version. --- lib/timecost/cli.rb | 56 ++++++++++++++++++++++++++++++----------- lib/timecost/version.rb | 2 +- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lib/timecost/cli.rb b/lib/timecost/cli.rb index 6a8b28b..c2e96c3 100644 --- a/lib/timecost/cli.rb +++ b/lib/timecost/cli.rb @@ -7,7 +7,9 @@ module TimeCost :author_filter => ".*?", :date_filter_enable => false, - :date_filter => ".*?", + :date_filter => [], + + :branches_filter_enable => true, :input_dump => [], :output_dump => nil, @@ -21,7 +23,7 @@ module TimeCost end def parse_cmdline args - opts = OptionParser.new do |opts| + options = OptionParser.new do |opts| opts.banner = "Usage: #{File.basename $0} [options]" opts.on_tail("-v","--verbose", "Run verbosely") do |v| @@ -33,6 +35,7 @@ module TimeCost exit 0 end + opts.on("-i","--input FILE", "Set input dump file") do |file| @config[:input_dump] << file end @@ -41,13 +44,23 @@ module TimeCost @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); + opts.on("--before DATE", "Keep only commits before DATE") do |date| + puts "set date filter to <= #{date}" + @config[:date_filter] << lambda { |other| + return (other <= DateTime.parse(date)) + } @config[:date_filter_enable] = true end - opts.on("-t","--time TIME", "Keep only commits on last TIME datys") do |time| + opts.on("--after DATE", "Keep only commits after DATE") do |date| + puts "set date filter to >= #{date}" + @config[:date_filter] << lambda { |other| + return (other >= DateTime.parse(date)) + } + @config[:date_filter_enable] = true + end + + opts.on("-t","--time TIME", "Keep only commits on last TIME days") 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]}" @@ -60,6 +73,10 @@ module TimeCost @config[:author_filter_enable] = true end + opts.on_tail("--all", "Collect from all branches and refs") do + @config[:branches_filter_enable] = false + end + # overlap : # opts.on("-s","--scotch GRANULARITY", "Use GRANULARITY (decimal hours) to merge ranges") do |granularity| @@ -67,7 +84,7 @@ module TimeCost @config[:range_granularity] = granularity.to_f end end - opts.parse! args + options.parse! args end @@ -75,11 +92,17 @@ module TimeCost def analyze_git # git log # foreach, create time range (before) + logs - process = IO.popen ["git", "log", - "--date=iso", - "--no-patch", - "--","."] + cmd = [ + "git", "log", + "--date=iso", + "--no-patch" + ] + if not @config[:branches_filter_enable] then + cmd << "--all" + end + cmd.concat ["--", "."] + process = IO.popen cmd @rangelist = {} commit = nil @@ -121,10 +144,15 @@ module TimeCost unless commit.nil? then commit.date = $1 - if @config[:date_filter_enable] and - (DateTime.parse(commit.date) < @config[:date_filter]) then + # reject if a some filter does not validate date + filter_keep = true + filters = @config[:date_filter] + filters.each do |f| + filter_keep &= f.call(DateTime.parse(commit.date)) + end + + if not filter_keep then commit = nil - # reject end end diff --git a/lib/timecost/version.rb b/lib/timecost/version.rb index 4eec771..9d672f4 100644 --- a/lib/timecost/version.rb +++ b/lib/timecost/version.rb @@ -1,3 +1,3 @@ module Timecost - VERSION = "0.0.1" + VERSION = "0.2.0" end From c7577f8981697f1fad32cc33264ca5ca2d367b4b Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Wed, 25 Feb 2015 17:17:21 +0100 Subject: [PATCH 20/20] Update. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b9d44be..d6ad7ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - timecost (0.0.1) + timecost (0.2.0) GEM remote: https://rubygems.org/