clockout 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/clock +136 -0
- data/lib/clockout.rb +248 -0
- metadata +65 -0
data/bin/clock
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'clockout'
|
4
|
+
|
5
|
+
HELP_BANNER = <<-EOS
|
6
|
+
Clockout v0.1
|
7
|
+
Usage:
|
8
|
+
$ clock [options]
|
9
|
+
|
10
|
+
Options:
|
11
|
+
--estimations, -e: Show estimations made for first commit of each block
|
12
|
+
--condensed, -c: Condense output (don't show the timeline for each day)
|
13
|
+
--generate-clock, -g: Generate .clock file
|
14
|
+
--see-clock, -s: See options specified in .clock file
|
15
|
+
--help, -h: Show this message
|
16
|
+
EOS
|
17
|
+
|
18
|
+
TEMPLATE_CLOCKFILE = <<-EOF
|
19
|
+
; Ignore initial commit, if it's just template/boilerplate
|
20
|
+
; Type: BOOL
|
21
|
+
; Default: 0
|
22
|
+
|
23
|
+
;ignore_initial = 1
|
24
|
+
|
25
|
+
; Minimum time between blocks of commits
|
26
|
+
; Type: Int (in minutes)
|
27
|
+
; Default: 120
|
28
|
+
;
|
29
|
+
; Default is 120, so if you think some commits took you more than 2 hours, you should shorten this
|
30
|
+
|
31
|
+
;time_cutoff = 90
|
32
|
+
|
33
|
+
|
34
|
+
;; Time-estimation options for each first commit of a timeblock
|
35
|
+
|
36
|
+
; Diffs of files matched by this regex will be included in commit time estimation
|
37
|
+
; Type: Regex (Ruby)
|
38
|
+
; Default: /.*/ (matches everything)
|
39
|
+
;
|
40
|
+
; With some projects, there are diffs to binaries, images, files modified by an IDE, etc,
|
41
|
+
; that you don't want to report as your own work.
|
42
|
+
; Below is an example regex that will only calculate changes made to files with those extensions
|
43
|
+
|
44
|
+
;my_files = /\.(m|h|rb|txt)$/
|
45
|
+
|
46
|
+
; Diffs of files matched by this regex will NOT be included in commit time estimation
|
47
|
+
; Type: Regex (Ruby)
|
48
|
+
; Default: <nothing>
|
49
|
+
;
|
50
|
+
; You also have the option of defining a negative regex match, to ignore certain files.
|
51
|
+
; For example, if you added an external library or something, you should ignore those additions
|
52
|
+
|
53
|
+
;not_my_files = /(ThisFile\.cpp | SomeOtherClass\.*)/
|
54
|
+
|
55
|
+
; Completion time overrides for commit estimations
|
56
|
+
; Type: Int (in minutes)
|
57
|
+
;
|
58
|
+
; Override times for specific commits here.
|
59
|
+
|
60
|
+
;7deec149 = 25
|
61
|
+
;5a6105e6 = 15
|
62
|
+
;4325de58 = 120
|
63
|
+
EOF
|
64
|
+
|
65
|
+
def parse_options(args)
|
66
|
+
opts = {}
|
67
|
+
|
68
|
+
args.each do |arg|
|
69
|
+
if (arg == "-h" || arg == "--help")
|
70
|
+
opts[:help] = true
|
71
|
+
elsif (arg == "-s" || arg == "--see-clock")
|
72
|
+
opts[:see_clock] = true
|
73
|
+
elsif (arg == "-e" || arg == "--estimations")
|
74
|
+
opts[:estimations] = true
|
75
|
+
elsif (arg == "-c" || arg == "--condensed")
|
76
|
+
opts[:condensed] = true
|
77
|
+
elsif (arg == "-g" || arg == "--generate-clock")
|
78
|
+
opts[:generate_clock] = true
|
79
|
+
elsif (arg[0] == "-")
|
80
|
+
puts "Error: invalid option '#{arg}'."
|
81
|
+
puts "Try --help for help."
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
opts
|
87
|
+
end
|
88
|
+
|
89
|
+
path = Dir.pwd
|
90
|
+
|
91
|
+
# Default options
|
92
|
+
opts = {time_cutoff:120, my_files:"/.*/"}
|
93
|
+
opts.merge!(parse_options(ARGV))
|
94
|
+
|
95
|
+
if opts[:generate_clock]
|
96
|
+
clock_path = path+"/.clock"
|
97
|
+
if (File.exists?(clock_path))
|
98
|
+
puts "#{colorize("Error:", RED)} .clock file already exists, ignoring --generate-clock option.\n"
|
99
|
+
else
|
100
|
+
File.open(clock_path, "w") do |file|
|
101
|
+
file.write(TEMPLATE_CLOCKFILE)
|
102
|
+
end
|
103
|
+
puts "Generated .clock file at #{clock_path}.\n"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
clock = Clockout.new(path, opts)
|
108
|
+
|
109
|
+
if opts[:see_clock]
|
110
|
+
if !clock.clock_opts
|
111
|
+
puts "No .clock file found. Run `clock -g` to generate one."
|
112
|
+
else
|
113
|
+
if clock.clock_opts.size == 0
|
114
|
+
puts "No clock options."
|
115
|
+
else
|
116
|
+
puts "Clock options:"
|
117
|
+
clock.clock_opts.each do |k, v|
|
118
|
+
key = k[0..19]
|
119
|
+
puts " #{key}:#{' '*(20-key.length)}#{v}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
exit
|
124
|
+
end
|
125
|
+
|
126
|
+
if opts[:help]
|
127
|
+
puts HELP_BANNER
|
128
|
+
exit
|
129
|
+
end
|
130
|
+
|
131
|
+
if (opts[:estimations])
|
132
|
+
clock.print_estimations
|
133
|
+
else
|
134
|
+
clock.print_chart
|
135
|
+
end
|
136
|
+
|
data/lib/clockout.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'grit'
|
2
|
+
|
3
|
+
class Commit
|
4
|
+
attr_accessor :message, :minutes, :date, :diffs, :sha
|
5
|
+
end
|
6
|
+
|
7
|
+
RED = 31
|
8
|
+
YELLOW = 33
|
9
|
+
MAGENTA = 35
|
10
|
+
LIGHT_BLUE = 94
|
11
|
+
def colorize(str, color)
|
12
|
+
"\e[0;#{color};49m#{str}\e[0m"
|
13
|
+
end
|
14
|
+
|
15
|
+
class Clockout
|
16
|
+
COLS = 80
|
17
|
+
DAY_FORMAT = '%B %e, %Y'
|
18
|
+
|
19
|
+
attr_accessor :blocks, :time_per_day, :clock_opts
|
20
|
+
|
21
|
+
def diffs(commit)
|
22
|
+
plus, minus = 0, 0
|
23
|
+
|
24
|
+
commit.stats.to_diffstat.each do |diff_stat|
|
25
|
+
my_files = $opts[:my_files]
|
26
|
+
not_my_files = $opts[:not_my_files]
|
27
|
+
should_include = (diff_stat.filename =~ eval(my_files))
|
28
|
+
should_ignore = not_my_files && (diff_stat.filename =~ eval(not_my_files))
|
29
|
+
if should_include && !should_ignore
|
30
|
+
plus += diff_stat.additions
|
31
|
+
minus += diff_stat.deletions
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Weight deletions half as much, since they are typically
|
36
|
+
# faster to do & also are 1:1 with additions when changing a line
|
37
|
+
plus+minus/2
|
38
|
+
end
|
39
|
+
|
40
|
+
def seperate_into_blocks(repo, commits)
|
41
|
+
blocks = []
|
42
|
+
block = []
|
43
|
+
|
44
|
+
total_diffs, total_mins = 0, 0
|
45
|
+
|
46
|
+
commits.each do |commit|
|
47
|
+
|
48
|
+
c = Commit.new
|
49
|
+
c.date = commit.committed_date
|
50
|
+
c.message = commit.message.gsub("\n",' ')
|
51
|
+
c.diffs = diffs(commit)
|
52
|
+
c.sha = commit.id[0..7]
|
53
|
+
|
54
|
+
if block.size > 0
|
55
|
+
time_since_last = (block.last.date - commit.committed_date).abs/60
|
56
|
+
|
57
|
+
if (time_since_last > $opts[:time_cutoff])
|
58
|
+
blocks << block
|
59
|
+
block = []
|
60
|
+
else
|
61
|
+
c.minutes = time_since_last
|
62
|
+
|
63
|
+
@time_per_day[c.date.strftime(DAY_FORMAT)] += c.minutes
|
64
|
+
|
65
|
+
total_diffs += c.diffs
|
66
|
+
total_mins += c.minutes
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
block << c
|
71
|
+
end
|
72
|
+
|
73
|
+
blocks << block
|
74
|
+
|
75
|
+
# Now go through each block's first commit and estimate the time it took
|
76
|
+
blocks.each do |block|
|
77
|
+
first = block.first
|
78
|
+
|
79
|
+
# See if they were overriden in the .clock file
|
80
|
+
if ($opts[first.sha.to_sym])
|
81
|
+
first.minutes = $opts[first.sha.to_sym].to_i
|
82
|
+
elsif ($opts[:ignore_initial] && block == blocks.first) || total_diffs == 0
|
83
|
+
first.minutes = 0
|
84
|
+
else
|
85
|
+
# Underestimate by a factor of 0.9
|
86
|
+
first.minutes = 0.9*first.diffs*(1.0*total_mins/total_diffs)
|
87
|
+
end
|
88
|
+
|
89
|
+
@time_per_day[first.date.strftime(DAY_FORMAT)] += first.minutes
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def print_chart
|
94
|
+
cols = ($opts[:condensed] ? 30 : COLS)
|
95
|
+
total_sum = 0
|
96
|
+
current_day = nil
|
97
|
+
@blocks.each do |block|
|
98
|
+
date = block.first.date.strftime(DAY_FORMAT)
|
99
|
+
if date != current_day
|
100
|
+
puts if (!$opts[:condensed])
|
101
|
+
|
102
|
+
current_day = date
|
103
|
+
|
104
|
+
sum = @time_per_day[date]
|
105
|
+
total_sum += sum
|
106
|
+
|
107
|
+
sum_str = "#{(sum/60.0).round(2)} hrs"
|
108
|
+
print colorize(date,MAGENTA)
|
109
|
+
print colorize("."*(cols - date.length - sum_str.length),MAGENTA)
|
110
|
+
print colorize(sum_str,RED)
|
111
|
+
puts
|
112
|
+
end
|
113
|
+
|
114
|
+
print_timeline(block) if (!$opts[:condensed])
|
115
|
+
end
|
116
|
+
|
117
|
+
puts " "*(cols-10) + colorize("-"*10,MAGENTA)
|
118
|
+
sum_str = "#{(total_sum/60.0).round(2)} hrs"
|
119
|
+
puts " "*(cols-sum_str.length) + colorize(sum_str,RED)
|
120
|
+
end
|
121
|
+
|
122
|
+
def print_timeline(block)
|
123
|
+
# subtract from the time it took for first commit
|
124
|
+
time = (block.first.date - block.first.minutes*60).strftime('%l:%M %p')+": "
|
125
|
+
print colorize(time,YELLOW)
|
126
|
+
|
127
|
+
char_count = time.length
|
128
|
+
|
129
|
+
block.each do |commit|
|
130
|
+
if commit.minutes < 60
|
131
|
+
c_mins = "#{commit.minutes.round(0)}m"
|
132
|
+
else
|
133
|
+
c_mins = "#{(commit.minutes/60.0).round(1)}h"
|
134
|
+
end
|
135
|
+
|
136
|
+
seperator = " | "
|
137
|
+
|
138
|
+
add = c_mins.length+seperator.length
|
139
|
+
if char_count + add > COLS-5
|
140
|
+
puts
|
141
|
+
char_count = time.length # indent by the length of the time label on left
|
142
|
+
print " "*char_count
|
143
|
+
end
|
144
|
+
|
145
|
+
char_count += add
|
146
|
+
|
147
|
+
print c_mins+colorize(seperator,RED)
|
148
|
+
end
|
149
|
+
puts
|
150
|
+
end
|
151
|
+
|
152
|
+
def print_estimations
|
153
|
+
sum = 0
|
154
|
+
@blocks.each do |block|
|
155
|
+
first = block.first
|
156
|
+
date = first.date.strftime('%b %e')+": "
|
157
|
+
sha = first.sha+" "
|
158
|
+
if first.minutes < 60
|
159
|
+
time = "#{first.minutes.round(0)} min"
|
160
|
+
else
|
161
|
+
time = "#{(first.minutes/60.0).round(2)} hrs"
|
162
|
+
end
|
163
|
+
|
164
|
+
print colorize(date,YELLOW)
|
165
|
+
print colorize(sha,RED)
|
166
|
+
|
167
|
+
cutoff = COLS-time.length-date.length-6-sha.length
|
168
|
+
message = first.message[0..cutoff]
|
169
|
+
message += "..." if first.message.length > cutoff
|
170
|
+
print message
|
171
|
+
|
172
|
+
print " "*(COLS-message.length-time.length-date.length-sha.length)
|
173
|
+
puts colorize(time, LIGHT_BLUE)
|
174
|
+
|
175
|
+
sum += first.minutes
|
176
|
+
end
|
177
|
+
|
178
|
+
puts " "*(COLS-10) + colorize("-"*10,LIGHT_BLUE)
|
179
|
+
sum_str = "#{(sum/60.0).round(2)} hrs"
|
180
|
+
puts " "*(COLS-sum_str.length) + colorize(sum_str, LIGHT_BLUE)
|
181
|
+
end
|
182
|
+
|
183
|
+
def get_repo(path)
|
184
|
+
begin
|
185
|
+
return Grit::Repo.new(path)
|
186
|
+
rescue Exception => e
|
187
|
+
if e.class == Grit::NoSuchPathError
|
188
|
+
puts "Error: Path '#{path}' could not be found."
|
189
|
+
else
|
190
|
+
puts "Error: '#{path}' is not a Git repository."
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def parse_clockfile(file)
|
196
|
+
return nil if !File.exists?(file)
|
197
|
+
|
198
|
+
opts = {}
|
199
|
+
|
200
|
+
line_num = 0
|
201
|
+
File.foreach(file) do |line|
|
202
|
+
line_num += 1
|
203
|
+
line.strip!
|
204
|
+
|
205
|
+
next if line[0] == ";" || line.length == 0
|
206
|
+
|
207
|
+
sides = line.split("=",2)
|
208
|
+
|
209
|
+
if sides.length != 2
|
210
|
+
puts "Error: bad syntax on line #{line_num} of .clock file:"
|
211
|
+
puts " #{line}"
|
212
|
+
puts ""
|
213
|
+
puts "Line must be of form:"
|
214
|
+
puts " KEY = VALUE"
|
215
|
+
|
216
|
+
exit
|
217
|
+
end
|
218
|
+
|
219
|
+
left = sides[0].strip
|
220
|
+
right = sides[1].strip
|
221
|
+
|
222
|
+
if left == "ignore_initial"
|
223
|
+
right = (right != "0")
|
224
|
+
elsif left == "time_cutoff"
|
225
|
+
right = right.to_i
|
226
|
+
end
|
227
|
+
|
228
|
+
opts[left.to_sym] = right
|
229
|
+
end
|
230
|
+
|
231
|
+
opts
|
232
|
+
end
|
233
|
+
|
234
|
+
def initialize(path, options)
|
235
|
+
$opts = options
|
236
|
+
|
237
|
+
@clock_opts = parse_clockfile(path+"/.clock")
|
238
|
+
$opts.merge!(@clock_opts) if @clock_opts
|
239
|
+
|
240
|
+
repo = get_repo(path) || exit
|
241
|
+
|
242
|
+
commits = repo.commits('master', 500)
|
243
|
+
commits.reverse!
|
244
|
+
|
245
|
+
@time_per_day = Hash.new(0)
|
246
|
+
@blocks = seperate_into_blocks(repo, commits)
|
247
|
+
end
|
248
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clockout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dan Hassin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: grit
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: An sort of extension to Git to support clocking hours worked on a project.
|
31
|
+
email:
|
32
|
+
- danhassin@mac.com
|
33
|
+
executables:
|
34
|
+
- clock
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- lib/clockout.rb
|
39
|
+
- !binary |-
|
40
|
+
YmluL2Nsb2Nr
|
41
|
+
homepage: http://rubygems.org/gems/clockout
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.8.24
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Clock your hours worked using Git
|
65
|
+
test_files: []
|