clockout 0.3.2 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/clock +3 -2
- data/lib/clockout.rb +60 -211
- data/lib/printer.rb +141 -0
- data/lib/record.rb +54 -0
- metadata +21 -3
data/bin/clock
CHANGED
@@ -177,10 +177,11 @@ else
|
|
177
177
|
end
|
178
178
|
|
179
179
|
clock = Clockout.new(path, user)
|
180
|
+
printer = Printer.new(clock)
|
180
181
|
|
181
182
|
if (opts[:estimations])
|
182
|
-
|
183
|
+
printer.print_estimations
|
183
184
|
else
|
184
|
-
|
185
|
+
printer.print_chart(opts[:condensed])
|
185
186
|
end
|
186
187
|
end
|
data/lib/clockout.rb
CHANGED
@@ -1,131 +1,45 @@
|
|
1
1
|
require 'grit'
|
2
|
-
require 'time'
|
3
2
|
require 'yaml'
|
4
3
|
|
4
|
+
require 'printer'
|
5
|
+
require 'record'
|
6
|
+
|
5
7
|
COLS = 80
|
6
8
|
DAY_FORMAT = '%B %e, %Y'
|
7
9
|
|
8
|
-
class
|
9
|
-
attr_accessor :
|
10
|
-
def initialize(commit = nil)
|
11
|
-
@addition = 0
|
12
|
-
if commit
|
13
|
-
@author = commit.author.email
|
14
|
-
@date = commit.committed_date
|
15
|
-
@message = commit.message.gsub("\n",' ')
|
16
|
-
@sha = commit.id
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class Clock
|
22
|
-
attr_accessor :in, :out, :date, :author
|
23
|
-
def initialize(type, date, auth)
|
24
|
-
@in = (type == :in)
|
25
|
-
@out = (type == :out)
|
26
|
-
@date = date
|
27
|
-
@author = auth
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def puts_error(str)
|
32
|
-
puts "Error: ".red + str
|
33
|
-
end
|
10
|
+
class Clockout
|
11
|
+
attr_accessor :blocks, :time_per_day
|
34
12
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
out = string
|
43
|
-
out += " " unless (ultimate || penultimate)
|
44
|
-
|
45
|
-
if ultimate
|
46
|
-
# Add seperator
|
47
|
-
cols_left = cols - size - out.length
|
48
|
-
ret += sep*cols_left if cols_left > 0
|
49
|
-
elsif penultimate
|
50
|
-
last = strings.keys.last.length
|
51
|
-
max_len = cols - size - last - 1
|
52
|
-
if string.length > max_len
|
53
|
-
# Truncate
|
54
|
-
out = string[0..max_len-5].strip + "... "
|
55
|
-
end
|
13
|
+
def commits_to_records(grit_commits)
|
14
|
+
my_files = eval($opts[:my_files])
|
15
|
+
not_my_files = eval($opts[:not_my_files] || "")
|
16
|
+
grit_commits.map do |commit|
|
17
|
+
c = Commit.new(commit)
|
18
|
+
c.calculate_diffs(my_files, not_my_files)
|
19
|
+
c
|
56
20
|
end
|
57
|
-
|
58
|
-
# Apply color & print
|
59
|
-
ret += method.to_proc.call(out)
|
60
|
-
|
61
|
-
size += out.length
|
62
21
|
end
|
63
22
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
class String
|
68
|
-
def colorize(color)
|
69
|
-
"\e[0;#{color};49m#{self}\e[0m"
|
70
|
-
end
|
71
|
-
|
72
|
-
def red() colorize(31) end
|
73
|
-
def yellow() colorize(33) end
|
74
|
-
def magenta() colorize(35) end
|
75
|
-
def light_blue() colorize(94) end
|
76
|
-
end
|
77
|
-
|
78
|
-
class Numeric
|
79
|
-
def as_time(type = nil, min_s = " min", hr_s = " hrs")
|
80
|
-
type = (self < 60) ? :minutes : :hours if !type
|
81
|
-
if type == :minutes
|
82
|
-
"#{self.round(0)}#{min_s}"
|
83
|
-
else
|
84
|
-
"#{(self/60.0).round(2)}#{hr_s}"
|
23
|
+
def clocks_to_records(clocks, in_out)
|
24
|
+
clocks.map do |c|
|
25
|
+
Clock.new(in_out, c.first[0], c.first[1])
|
85
26
|
end
|
86
27
|
end
|
87
|
-
end
|
88
28
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
should_include = (diff_stat.filename =~ eval(my_files))
|
97
|
-
should_ignore = not_my_files && (diff_stat.filename =~ eval(not_my_files))
|
98
|
-
if should_include && !should_ignore
|
99
|
-
plus += diff_stat.additions
|
100
|
-
minus += diff_stat.deletions
|
29
|
+
def try_overriding_record(record)
|
30
|
+
overrides = $opts[:overrides]
|
31
|
+
overrides.each do |k, v|
|
32
|
+
if record.sha.start_with? k
|
33
|
+
record.minutes = v
|
34
|
+
record.overriden = true
|
35
|
+
return true
|
101
36
|
end
|
102
|
-
end
|
37
|
+
end if overrides
|
103
38
|
|
104
|
-
|
105
|
-
# faster to do & also are 1:1 with additions when changing a line
|
106
|
-
plus+minus/2
|
39
|
+
false
|
107
40
|
end
|
108
41
|
|
109
|
-
def
|
110
|
-
clockins = $opts[:in] || {}
|
111
|
-
clockouts = $opts[:out] || {}
|
112
|
-
|
113
|
-
# Convert clock-in/-outs into Clock objs & commits into Commit objs
|
114
|
-
clocks = []
|
115
|
-
clockins.each { |c| clocks << Clock.new(:in, c.first[0], c.first[1]) }
|
116
|
-
clockouts.each { |c| clocks << Clock.new(:out, c.first[0], c.first[1]) }
|
117
|
-
commits_in.map! do |commit|
|
118
|
-
c = Commit.new(commit)
|
119
|
-
c.diffs = diffs(commit)
|
120
|
-
c
|
121
|
-
end
|
122
|
-
|
123
|
-
# Merge & sort everything by date
|
124
|
-
data = (commits_in + clocks).sort { |a,b| a.date <=> b.date }
|
125
|
-
|
126
|
-
# If author is specified, delete everything not by that author
|
127
|
-
data.delete_if { |c| c.author != author } if author
|
128
|
-
|
42
|
+
def run(data)
|
129
43
|
blocks = []
|
130
44
|
total_diffs, total_mins = 0, 0
|
131
45
|
|
@@ -193,25 +107,18 @@ class Clockout
|
|
193
107
|
end
|
194
108
|
else
|
195
109
|
# See if this commit was overriden in the config file
|
196
|
-
|
197
|
-
|
198
|
-
if
|
199
|
-
curr_c.minutes =
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
curr_c.minutes = 0
|
208
|
-
elsif !curr_c.overriden && prev_c
|
209
|
-
curr_c.clocked_in = true if prev_c.class == Clock && prev_c.in
|
210
|
-
# If it added successfully into a block (or was clocked in), we can calculate based on last commit
|
211
|
-
if add_commit.call(curr_c) || curr_c.clocked_in
|
212
|
-
curr_c.minutes = (curr_c.date - prev_c.date)/60.0 # clock or commit, doesn't matter
|
110
|
+
if !try_overriding_record(curr_c)
|
111
|
+
# Otherwise, if we're ignoring initial & it's initial, set minutes to 0
|
112
|
+
if $opts[:ignore_initial] && !prev_c
|
113
|
+
curr_c.minutes = 0
|
114
|
+
else
|
115
|
+
curr_c.clocked_in = true if prev_c && prev_c.class == Clock && prev_c.in
|
116
|
+
# If it added successfully into a block (or was clocked in), we can calculate based on last commit
|
117
|
+
if add_commit.call(curr_c) || curr_c.clocked_in
|
118
|
+
curr_c.minutes = (curr_c.date - prev_c.date)/60.0 # clock or commit, doesn't matter
|
119
|
+
end
|
120
|
+
# Otherwise, we'll do an estimation later, once we have more data
|
213
121
|
end
|
214
|
-
# Otherwise, we'll do an estimation later, once we have more data
|
215
122
|
end
|
216
123
|
|
217
124
|
if curr_c.minutes
|
@@ -246,82 +153,21 @@ class Clockout
|
|
246
153
|
blocks
|
247
154
|
end
|
248
155
|
|
249
|
-
def
|
250
|
-
|
251
|
-
|
252
|
-
current_day = nil
|
253
|
-
@blocks.each do |block|
|
254
|
-
date = block.first.date.strftime(DAY_FORMAT)
|
255
|
-
if date != current_day
|
256
|
-
puts if (!condensed && current_day)
|
257
|
-
|
258
|
-
current_day = date
|
259
|
-
|
260
|
-
sum = @time_per_day[date]
|
261
|
-
total_sum += sum
|
262
|
-
|
263
|
-
puts align({date => :magenta, sum.as_time(:hours) => :red}, cols, ".".magenta)
|
264
|
-
end
|
265
|
-
|
266
|
-
print_timeline(block) if (!condensed)
|
267
|
-
end
|
268
|
-
|
269
|
-
puts align({"-"*10 => :magenta}, cols)
|
270
|
-
puts align({total_sum.as_time(:hours) => :red}, cols)
|
271
|
-
end
|
272
|
-
|
273
|
-
def print_timeline(block)
|
274
|
-
# subtract from the time it took for first commit
|
275
|
-
time = (block.first.date - block.first.minutes*60).strftime('%l:%M %p')+": "
|
276
|
-
print time.yellow
|
277
|
-
|
278
|
-
char_count = time.length
|
279
|
-
|
280
|
-
block.each do |commit|
|
281
|
-
c_mins = commit.minutes.as_time(nil, "m", "h")
|
282
|
-
c_mins = "*#{c_mins}" if commit.clocked_in
|
283
|
-
c_mins += "*" if commit.clocked_out
|
284
|
-
|
285
|
-
seperator = " | "
|
286
|
-
|
287
|
-
add = c_mins.length+seperator.length
|
288
|
-
if char_count + add > COLS-5
|
289
|
-
puts
|
290
|
-
char_count = time.length # indent by the length of the time label on left
|
291
|
-
print " "*char_count
|
292
|
-
end
|
293
|
-
|
294
|
-
char_count += add
|
295
|
-
|
296
|
-
# Blue for clockin/out commits
|
297
|
-
print c_mins+(commit.message ? seperator.red : seperator.light_blue)
|
298
|
-
end
|
299
|
-
puts
|
300
|
-
end
|
301
|
-
|
302
|
-
def print_estimations
|
303
|
-
sum = 0
|
304
|
-
estimations = []
|
305
|
-
@blocks.each do |block|
|
306
|
-
estimations << block.first if block.first.estimated
|
307
|
-
end
|
156
|
+
def prepare_blocks(commits_in, author)
|
157
|
+
clockins = $opts[:in] || {}
|
158
|
+
clockouts = $opts[:out] || {}
|
308
159
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
estimations.each do |c|
|
313
|
-
date = c.date.strftime('%b %e')+":"
|
314
|
-
sha = c.sha[0..7]
|
315
|
-
time = c.minutes.as_time
|
160
|
+
# Convert clock-in/-outs into Clock objs & commits into Commit objs
|
161
|
+
clocks = clocks_to_records(clockins, :in) + clocks_to_records(clockouts, :out)
|
162
|
+
commits = commits_to_records(commits_in)
|
316
163
|
|
317
|
-
|
164
|
+
# Merge & sort everything by date
|
165
|
+
data = (commits + clocks).sort { |a,b| a.date <=> b.date }
|
318
166
|
|
319
|
-
|
320
|
-
|
167
|
+
# If author is specified, delete everything not by that author
|
168
|
+
data.delete_if { |c| c.author != author } if author
|
321
169
|
|
322
|
-
|
323
|
-
puts align({sum.as_time(:hours) => :light_blue})
|
324
|
-
end
|
170
|
+
@blocks = run(data)
|
325
171
|
end
|
326
172
|
|
327
173
|
def self.get_repo(path, original_path = nil)
|
@@ -372,22 +218,25 @@ class Clockout
|
|
372
218
|
root_path
|
373
219
|
end
|
374
220
|
|
375
|
-
def initialize(path, author = nil)
|
376
|
-
|
221
|
+
def initialize(path = nil, author = nil)
|
222
|
+
@time_per_day = Hash.new(0)
|
377
223
|
|
378
224
|
# Default options
|
379
225
|
$opts = {time_cutoff:120, my_files:"/.*/", estimation_factor:1.0}
|
380
226
|
|
381
|
-
|
382
|
-
|
227
|
+
if path
|
228
|
+
repo, root_path = Clockout.get_repo(path) || exit
|
383
229
|
|
384
|
-
|
385
|
-
|
230
|
+
# Parse config options
|
231
|
+
clock_opts = Clockout.parse_clockfile(Clockout.clock_path(root_path))
|
386
232
|
|
387
|
-
|
388
|
-
|
233
|
+
# Merge with config override options
|
234
|
+
$opts.merge!(clock_opts) if clock_opts
|
389
235
|
|
390
|
-
|
391
|
-
|
236
|
+
commits = repo.commits('master', 500)
|
237
|
+
commits.reverse!
|
238
|
+
|
239
|
+
prepare_blocks(commits, author)
|
240
|
+
end
|
392
241
|
end
|
393
242
|
end
|
data/lib/printer.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
def puts_error(str)
|
2
|
+
puts "Error: ".red + str
|
3
|
+
end
|
4
|
+
|
5
|
+
class String
|
6
|
+
def colorize(color)
|
7
|
+
"\e[0;#{color};49m#{self}\e[0m"
|
8
|
+
end
|
9
|
+
|
10
|
+
def red() colorize(31) end
|
11
|
+
def yellow() colorize(33) end
|
12
|
+
def magenta() colorize(35) end
|
13
|
+
def light_blue() colorize(94) end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Numeric
|
17
|
+
def as_time(type = nil, min_s = " min", hr_s = " hrs")
|
18
|
+
type = (self < 60) ? :minutes : :hours if !type
|
19
|
+
if type == :minutes
|
20
|
+
"#{self.round(0)}#{min_s}"
|
21
|
+
else
|
22
|
+
"#{(self/60.0).round(2)}#{hr_s}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Printer
|
28
|
+
def initialize(clockout)
|
29
|
+
@clockout = clockout
|
30
|
+
end
|
31
|
+
|
32
|
+
def align(strings, cols = COLS, sep = " ")
|
33
|
+
ret = ""
|
34
|
+
size = 0
|
35
|
+
strings.each do |string, method|
|
36
|
+
ultimate = (string == strings.keys[-1])
|
37
|
+
penultimate = (string == strings.keys[-2])
|
38
|
+
|
39
|
+
out = string
|
40
|
+
out += " " unless (ultimate || penultimate)
|
41
|
+
|
42
|
+
if ultimate
|
43
|
+
# Add seperator
|
44
|
+
cols_left = cols - size - out.length
|
45
|
+
ret += sep*cols_left if cols_left > 0
|
46
|
+
elsif penultimate
|
47
|
+
last = strings.keys.last.length
|
48
|
+
max_len = cols - size - last - 1
|
49
|
+
if string.length > max_len
|
50
|
+
# Truncate
|
51
|
+
out = string[0..max_len-5].strip + "... "
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Apply color & print
|
56
|
+
ret += method.to_proc.call(out)
|
57
|
+
|
58
|
+
size += out.length
|
59
|
+
end
|
60
|
+
|
61
|
+
ret
|
62
|
+
end
|
63
|
+
|
64
|
+
def print_chart(condensed)
|
65
|
+
cols = condensed ? 30 : COLS
|
66
|
+
total_sum = 0
|
67
|
+
current_day = nil
|
68
|
+
@clockout.blocks.each do |block|
|
69
|
+
date = block.first.date.strftime(DAY_FORMAT)
|
70
|
+
if date != current_day
|
71
|
+
puts if (!condensed && current_day)
|
72
|
+
|
73
|
+
current_day = date
|
74
|
+
|
75
|
+
sum = @clockout.time_per_day[date]
|
76
|
+
total_sum += sum
|
77
|
+
|
78
|
+
puts align({date => :magenta, sum.as_time(:hours) => :red}, cols, ".".magenta)
|
79
|
+
end
|
80
|
+
|
81
|
+
print_timeline(block) if (!condensed)
|
82
|
+
end
|
83
|
+
|
84
|
+
puts align({"-"*10 => :magenta}, cols)
|
85
|
+
puts align({total_sum.as_time(:hours) => :red}, cols)
|
86
|
+
end
|
87
|
+
|
88
|
+
def print_timeline(block)
|
89
|
+
# subtract from the time it took for first commit
|
90
|
+
time = (block.first.date - block.first.minutes*60).strftime('%l:%M %p')+": "
|
91
|
+
print time.yellow
|
92
|
+
|
93
|
+
char_count = time.length
|
94
|
+
|
95
|
+
block.each do |commit|
|
96
|
+
c_mins = commit.minutes.as_time(nil, "m", "h")
|
97
|
+
c_mins = "*#{c_mins}" if commit.clocked_in
|
98
|
+
c_mins += "*" if commit.clocked_out
|
99
|
+
|
100
|
+
seperator = " | "
|
101
|
+
|
102
|
+
add = c_mins.length+seperator.length
|
103
|
+
if char_count + add > COLS-5
|
104
|
+
puts
|
105
|
+
char_count = time.length # indent by the length of the time label on left
|
106
|
+
print " "*char_count
|
107
|
+
end
|
108
|
+
|
109
|
+
char_count += add
|
110
|
+
|
111
|
+
# Blue for clockin/out commits
|
112
|
+
print c_mins+(commit.message ? seperator.red : seperator.light_blue)
|
113
|
+
end
|
114
|
+
puts
|
115
|
+
end
|
116
|
+
|
117
|
+
def print_estimations
|
118
|
+
sum = 0
|
119
|
+
estimations = []
|
120
|
+
@clockout.blocks.each do |block|
|
121
|
+
estimations << block.first if block.first.estimated
|
122
|
+
end
|
123
|
+
|
124
|
+
if estimations.empty?
|
125
|
+
puts "No estimations made."
|
126
|
+
else
|
127
|
+
estimations.each do |c|
|
128
|
+
date = c.date.strftime('%b %e')+":"
|
129
|
+
sha = c.sha[0..7]
|
130
|
+
time = c.minutes.as_time
|
131
|
+
|
132
|
+
puts align({date => :yellow, sha => :red, c.message => :to_s, time => :light_blue})
|
133
|
+
|
134
|
+
sum += c.minutes
|
135
|
+
end
|
136
|
+
|
137
|
+
puts align({"-"*10 => :light_blue})
|
138
|
+
puts align({sum.as_time(:hours) => :light_blue})
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/record.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
class Record
|
2
|
+
attr_accessor :date, :author
|
3
|
+
end
|
4
|
+
|
5
|
+
class Commit < Record
|
6
|
+
# From Grit::Commit object
|
7
|
+
attr_accessor :message, :stats, :diffs, :sha
|
8
|
+
# Time calc
|
9
|
+
attr_accessor :minutes, :addition, :overriden, :estimated
|
10
|
+
# Whether it's been padded by a clock in/out
|
11
|
+
attr_accessor :clocked_in, :clocked_out
|
12
|
+
|
13
|
+
def initialize(commit = nil, date = nil)
|
14
|
+
@addition = 0
|
15
|
+
@date = date
|
16
|
+
if commit
|
17
|
+
@author = commit.author.email
|
18
|
+
@date = commit.committed_date
|
19
|
+
@message = commit.message.gsub("\n",' ')
|
20
|
+
@sha = commit.id
|
21
|
+
@stats = commit.stats
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def calculate_diffs(my_files, not_my_files)
|
26
|
+
return @diffs if @diffs
|
27
|
+
|
28
|
+
plus, minus = 0, 0
|
29
|
+
|
30
|
+
@stats.to_diffstat.each do |diff_stat|
|
31
|
+
should_include = (diff_stat.filename =~ my_files)
|
32
|
+
should_ignore = not_my_files && (diff_stat.filename =~ not_my_files)
|
33
|
+
if should_include && !should_ignore
|
34
|
+
plus += diff_stat.additions
|
35
|
+
minus += diff_stat.deletions
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Weight deletions half as much, since they are typically
|
40
|
+
# faster to do & also are 1:1 with additions when changing a line
|
41
|
+
@diffs = plus+minus/2
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Clock < Record
|
46
|
+
# Whether its in or out
|
47
|
+
attr_accessor :in, :out
|
48
|
+
def initialize(type, date, auth)
|
49
|
+
@in = (type == :in)
|
50
|
+
@out = (type == :out)
|
51
|
+
@date = date
|
52
|
+
@author = auth
|
53
|
+
end
|
54
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clockout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.4'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-06-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: grit
|
@@ -27,7 +27,23 @@ dependencies:
|
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '0'
|
30
|
-
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Sort of an extension to Git to support clocking hours worked on a project.
|
31
47
|
email:
|
32
48
|
- danhassin@mac.com
|
33
49
|
executables:
|
@@ -35,7 +51,9 @@ executables:
|
|
35
51
|
extensions: []
|
36
52
|
extra_rdoc_files: []
|
37
53
|
files:
|
54
|
+
- lib/record.rb
|
38
55
|
- lib/clockout.rb
|
56
|
+
- lib/printer.rb
|
39
57
|
- !binary |-
|
40
58
|
YmluL2Nsb2Nr
|
41
59
|
homepage: http://rubygems.org/gems/clockout
|