Merge branch 'release/v0.2.0'

This commit is contained in:
Glenn Y. Rolland 2015-02-25 17:17:37 +01:00
commit 60ba644fb3
22 changed files with 755 additions and 364 deletions

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
*.bundle
*.so
*.o
*.a
mkmf.log

5
Gemfile Normal file
View file

@ -0,0 +1,5 @@
# A sample Gemfile
source "https://rubygems.org"
# Specify your gem's dependencies in timecost.gemspec
gemspec

19
Gemfile.lock Normal file
View file

@ -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!

22
LICENSE.txt Normal file
View file

@ -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.

View file

@ -1,14 +1,14 @@
TimeTrack-Log for Git 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 Installation
------------ ------------
* Clone the project somewhere Install the project with:
* Copy the ''bin/git-timetrack-log'' file in ''/usr/local/bin''
$ gem install timecost
Usage Usage
----- -----
@ -16,7 +16,7 @@ Usage
To get the total time spent on your git project 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 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 set date filter to 2013-03-01
(1.0) 2013-09-23T13:02:39+02:00 - 2013-09-23T14:02:39+02:00 (1.0) 2013-09-23T13:02:39+02:00 - 2013-09-23T14:02:39+02:00
* Glenn Y. Rolland <glenux@glenux.net> * Glenn Y. Rolland <glenux@glenux.net>
@ -43,6 +43,14 @@ TOTAL: 1.00 hours
For other possibilities 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

12
Rakefile Normal file
View file

@ -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

19
TODO.md Normal file
View file

@ -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

20
bin/git-timecost Executable file
View file

@ -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

View file

@ -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

7
lib/timecost.rb Normal file
View file

@ -0,0 +1,7 @@
require 'timecost/commit'
require 'timecost/range'
require 'timecost/author_list'
require 'timecost/range_list'
require 'timecost/cli'

View file

@ -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

220
lib/timecost/cli.rb Normal file
View file

@ -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

14
lib/timecost/commit.rb Normal file
View 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

117
lib/timecost/range.rb Normal file
View file

@ -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

View 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

3
lib/timecost/version.rb Normal file
View file

@ -0,0 +1,3 @@
module Timecost
VERSION = "0.2.0"
end

65
spec/author_list_spec.rb Normal file
View file

@ -0,0 +1,65 @@
require_relative 'spec_helper'
require 'timecost/author_list'
describe TimeCost::AuthorList do
let(:list) { TimeCost::AuthorList.new }
let(:first) { "Foo <foo@example.com>" }
let(:second) { "Bar <bar@example.com>" }
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

16
spec/cli_spec.rb Normal file
View file

@ -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

26
spec/range_list_spec.rb Normal file
View file

@ -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

35
spec/range_spec.rb Normal file
View file

@ -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

16
spec/spec_helper.rb Executable file
View file

@ -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

24
timecost.gemspec Normal file
View file

@ -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