clockout 0.3.2 → 0.4
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 +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
|