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 new file mode 100644 index 0000000..d924a13 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +# A sample Gemfile +source "https://rubygems.org" + +# Specify your gem's dependencies in timecost.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d6ad7ab --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,19 @@ +PATH + remote: . + specs: + timecost (0.2.0) + +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/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 e9fc91c..03ab5a8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -TimeTrack-Log for Git -===================== - -Use git logs to give an estimation of spent time on your projects. +TimeCost for Git +================ +Use git logs to give an estimation of spent time & costs of your projects. Installation ------------ -* Clone the project somewhere -* Copy the ''bin/git-timetrack-log'' file in ''/usr/local/bin'' +Install the project with: + + $ gem install timecost 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,14 @@ TOTAL: 1.00 hours For other possibilities ``` -$ git timetrack-log -h +$ 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..d4b7f59 --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ + +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/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 + diff --git a/bin/git-timecost b/bin/git-timecost new file mode 100755 index 0000000..5319863 --- /dev/null +++ b/bin/git-timecost @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby +# vim: set syntax=ruby ts=4 sw=4 noet : + +require 'pp' +require 'date' +require 'optparse' +require 'yaml' + +require 'timecost' + +app = TimeCost::CLI.new +app.parse_cmdline ARGV +app.analyze +app.export +app.report +#app.report_ranges +#app.report_users + +exit 0 + diff --git a/bin/git-timetrack-log b/bin/git-timetrack-log deleted file mode 100755 index 12c879e..0000000 --- a/bin/git-timetrack-log +++ /dev/null @@ -1,355 +0,0 @@ -#!/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 - diff --git a/lib/timecost.rb b/lib/timecost.rb new file mode 100644 index 0000000..3775e42 --- /dev/null +++ b/lib/timecost.rb @@ -0,0 +1,7 @@ + +require 'timecost/commit' +require 'timecost/range' +require 'timecost/author_list' +require 'timecost/range_list' +require 'timecost/cli' + diff --git a/lib/timecost/author_list.rb b/lib/timecost/author_list.rb new file mode 100644 index 0000000..966eaaa --- /dev/null +++ b/lib/timecost/author_list.rb @@ -0,0 +1,38 @@ + +require 'pp' + +module TimeCost + class AuthorList + class UnknownAuthor < RuntimeError ; end + + # Prepare an empty index (local) + def initialize + @count = 0 + @author_to_id = {} + end + + def add author + if @author_to_id.include? author then + result = @author_to_id[author] + else + @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 + + def size + return @author_to_id.keys.size + end + end +end diff --git a/lib/timecost/cli.rb b/lib/timecost/cli.rb new file mode 100644 index 0000000..c2e96c3 --- /dev/null +++ b/lib/timecost/cli.rb @@ -0,0 +1,220 @@ +module TimeCost + class CLI + def initialize + # FIXME: accept multiple authors + @config = { + :author_filter_enable => false, + :author_filter => ".*?", + + :date_filter_enable => false, + :date_filter => [], + + :branches_filter_enable => true, + + :input_dump => [], + :output_dump => nil, + + :range_granularity => 0.5, # in decimal hours + + :verbose => false + } + @rangelist = {} + @authorlist = nil + end + + def parse_cmdline args + options = 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("--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("--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]}" + @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 + + 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| + puts "set scotch to #{granularity}" + @config[:range_granularity] = granularity.to_f + end + end + options.parse! args + + end + + + def analyze_git + # git log + # foreach, create time range (before) + logs + + 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 + 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 commit, granularity: @config[:range_granularity] + + 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}" + + 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 + + # 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 + 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 |author,rangelist| + rangelist.each do |range| + puts range.to_s(!@config[:author_filter_enable]) + "\n" + end + end + 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/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..ed0f11e --- /dev/null +++ b/lib/timecost/range.rb @@ -0,0 +1,117 @@ + +module TimeCost + class Range + attr_accessor :time_start, :time_stop, :commits, :author + + 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 - (@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 + + # return early result if ranges come from different authors + return false if (@author != range.author) + + # 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 + (@granularity/24.0) + end + + def diff + return ("%.2f" % ((@time_stop - fixed_start).to_f * 24)).to_f + end + + def to_s show_authors = true + val = "(%s)\t%s - %s\n" % [diff, fixed_start, @time_stop] + if show_authors then + val += "\tby %s\n" % @commits.first.author + end + @commits.each do |commit| + lines = [] + 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/range_list.rb b/lib/timecost/range_list.rb new file mode 100644 index 0000000..9352da6 --- /dev/null +++ b/lib/timecost/range_list.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 diff --git a/lib/timecost/version.rb b/lib/timecost/version.rb new file mode 100644 index 0000000..9d672f4 --- /dev/null +++ b/lib/timecost/version.rb @@ -0,0 +1,3 @@ +module Timecost + VERSION = "0.2.0" +end diff --git a/spec/author_list_spec.rb b/spec/author_list_spec.rb new file mode 100644 index 0000000..20ad4f3 --- /dev/null +++ b/spec/author_list_spec.rb @@ -0,0 +1,65 @@ + + +require_relative 'spec_helper' + +require 'timecost/author_list' + +describe TimeCost::AuthorList do + 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, list + end + end + + describe '.add' do + it "must accept adding authors" do + assert_respond_to list, :add + + list.add first + list.add second + end + + it "must assign a different id to different authors" do + 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 '.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 + + list.add first + list.alias first, second + end + + it "must assign the same id to aliases authors" do + list.add first + list.alias first, second + + id_foo = list.parse first + id_bar = list.parse second + refute_equal id_foo, id_bar + end + end +end 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 new file mode 100644 index 0000000..20e83b5 --- /dev/null +++ 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 new file mode 100644 index 0000000..17fba79 --- /dev/null +++ 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 + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100755 index 0000000..e208137 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,16 @@ + +#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 } +#end + + diff --git a/timecost.gemspec b/timecost.gemspec new file mode 100644 index 0000000..b244ab5 --- /dev/null +++ b/timecost.gemspec @@ -0,0 +1,24 @@ +# 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{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") + 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" + spec.add_development_dependency "minitest", "~> 4.7.5" +end