clockout 0.2 → 0.3
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 +92 -90
- data/lib/clockout.rb +362 -286
- metadata +2 -2
data/bin/clock
CHANGED
@@ -3,97 +3,111 @@
|
|
3
3
|
require 'clockout'
|
4
4
|
|
5
5
|
HELP_BANNER = <<-EOS
|
6
|
-
Clockout v0.
|
7
|
-
|
8
|
-
|
6
|
+
Clockout v0.3
|
7
|
+
See hours:
|
8
|
+
$ clock [options]
|
9
9
|
|
10
10
|
Options:
|
11
11
|
--estimations, -e: Show estimations made for first commit of each block
|
12
12
|
--condensed, -c: Condense output (don't show the timeline for each day)
|
13
|
-
--generate-clock, -g: Generate
|
14
|
-
--see-clock, -s: See options specified in .clock file
|
13
|
+
--generate-clock, -g: Generate config file
|
15
14
|
--help, -h: Show this message
|
15
|
+
|
16
|
+
$ clock in Clock-in (when you start working, preceding a commit)
|
17
|
+
$ clock out Clock-out (after you keep working overtime after a commit)
|
16
18
|
EOS
|
17
19
|
|
18
20
|
TEMPLATE_CLOCKFILE = <<-EOF
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
### General options ###
|
22
|
+
|
23
|
+
# Ignore initial commit, if it's just template/boilerplate
|
24
|
+
# - Default: false
|
25
|
+
|
26
|
+
#ignore_initial: true
|
27
|
+
|
28
|
+
# Minimum time between blocks of commits
|
29
|
+
# - Default: 120 min (change if you think some commits took you more than 2 hours)
|
30
|
+
|
31
|
+
#time_cutoff: 90
|
32
|
+
|
22
33
|
|
23
|
-
|
34
|
+
### Time-estimation options for each first commit of a timeblock ###
|
24
35
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
36
|
+
# Diffs of files matched by this regex will be included in commit time estimation.
|
37
|
+
# With some projects, there are diffs to binaries, images, files modified by an IDE, etc,
|
38
|
+
# that you don't want to report as your own work. Below is an example regex that will only
|
39
|
+
# calculate changes made to files with those extensions
|
40
|
+
# - Default: /.*/ (matches everything)
|
30
41
|
|
31
|
-
|
42
|
+
#my_files: /\\.(m|h|txt)$/
|
32
43
|
|
44
|
+
# Diffs of files matched by this regex will NOT be included in commit time estimation
|
45
|
+
# You also have the option of defining a negative regex match, to ignore certain files.
|
46
|
+
# For example, if you added an external library or something, you should ignore it
|
47
|
+
# - Default: <nothing>
|
33
48
|
|
34
|
-
|
49
|
+
#not_my_files: /SomeThirdPartyLibrary/
|
35
50
|
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
51
|
+
# Estimation factor
|
52
|
+
# Use the -e option to see the estimations Clockout makes, and tweak them using this
|
53
|
+
# value as necessary.
|
54
|
+
# - Default: 1.0
|
43
55
|
|
44
|
-
|
56
|
+
#estimation_factor: 0.9
|
45
57
|
|
46
|
-
|
47
|
-
|
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
|
58
|
+
# Put any override times for specific commits here (minutes). (You can use SHA1 hashes of any length)
|
59
|
+
# Note: keep a space before each hash for the YAML to parse properly.
|
52
60
|
|
53
|
-
|
61
|
+
#overrides:
|
62
|
+
# 48f63a7b: 90
|
63
|
+
# d354a31a: 30
|
54
64
|
|
55
|
-
; Completion time overrides for commit estimations
|
56
|
-
; Type: Int (in minutes)
|
57
|
-
;
|
58
|
-
; Override times for specific commits here.
|
59
65
|
|
60
|
-
|
61
|
-
|
62
|
-
;
|
66
|
+
### Clock-ins/-outs ###
|
67
|
+
|
68
|
+
# Do not modify these directly; use the `clock in` and `clock out` commands.
|
69
|
+
# See the readme for more details.
|
70
|
+
|
71
|
+
out:
|
72
|
+
in:
|
73
|
+
|
63
74
|
EOF
|
64
75
|
|
65
76
|
def parse_options(args)
|
66
77
|
opts = {}
|
67
78
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
elsif (arg == "-
|
72
|
-
opts[:
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
79
|
+
args.each do |arg|
|
80
|
+
if (arg == "-h" || arg == "--help")
|
81
|
+
opts[:help] = true
|
82
|
+
elsif (arg == "-e" || arg == "--estimations")
|
83
|
+
opts[:estimations] = true
|
84
|
+
elsif (arg == "-c" || arg == "--condensed")
|
85
|
+
opts[:condensed] = true
|
86
|
+
elsif (arg == "-g" || arg == "--generate-clock")
|
87
|
+
opts[:generate_clock] = true
|
88
|
+
else
|
89
|
+
puts_error "invalid option '#{arg}'."
|
90
|
+
puts "Try --help for help."
|
91
|
+
exit
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
opts
|
96
|
+
end
|
97
|
+
|
98
|
+
def clock_path(path = nil)
|
99
|
+
@clock_path ||= Clockout.clock_path(Clockout.root_path(path))
|
87
100
|
end
|
88
101
|
|
89
102
|
def generate_clock_file(path)
|
90
|
-
|
91
|
-
if (File.exists?(
|
103
|
+
path = clock_path(path)
|
104
|
+
if (File.exists?(path))
|
92
105
|
false
|
93
106
|
else
|
94
|
-
File.open(
|
107
|
+
File.open(path, "w") do |file|
|
95
108
|
file.write(TEMPLATE_CLOCKFILE)
|
96
109
|
end
|
110
|
+
puts "Generated config file at #{path}."
|
97
111
|
true
|
98
112
|
end
|
99
113
|
end
|
@@ -104,9 +118,22 @@ if (ARGV[0] == "in" || ARGV[0] == "out")
|
|
104
118
|
# Generate a clock file if one doesn't already exist
|
105
119
|
generate_clock_file(path)
|
106
120
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
121
|
+
# Add the in/out date to the file
|
122
|
+
buf = ""
|
123
|
+
mod = false
|
124
|
+
seek_line = ARGV[0] + ":"
|
125
|
+
time_line = "- " + Time.new.to_s + "\n"
|
126
|
+
File.foreach(clock_path) do |line|
|
127
|
+
buf += line
|
128
|
+
if line.strip == seek_line
|
129
|
+
mod = true
|
130
|
+
buf += time_line
|
131
|
+
end
|
132
|
+
end
|
133
|
+
File.open(clock_path, "w") do |file|
|
134
|
+
file << buf
|
135
|
+
# Add in: or out:, along with the time if it doesn't exist
|
136
|
+
file << "\n#{seek_line}\n#{time_line}" if !mod
|
110
137
|
end
|
111
138
|
else
|
112
139
|
opts = parse_options(ARGV)
|
@@ -117,33 +144,8 @@ else
|
|
117
144
|
end
|
118
145
|
|
119
146
|
if opts[:generate_clock]
|
120
|
-
if generate_clock_file(path)
|
121
|
-
|
122
|
-
else
|
123
|
-
puts "#{colorize("Error:", RED)} .clock file already exists for this repo.\n"
|
124
|
-
end
|
125
|
-
exit
|
126
|
-
end
|
127
|
-
|
128
|
-
if opts[:see_clock]
|
129
|
-
clock_opts = Clockout.parse_clockfile(Clockout.clock_path(path))
|
130
|
-
if !clock_opts
|
131
|
-
puts "No .clock file found. Run `clock -g` to generate one."
|
132
|
-
else
|
133
|
-
if clock_opts.size == 0
|
134
|
-
puts "No clock options."
|
135
|
-
else
|
136
|
-
puts "Clock options:"
|
137
|
-
clock_opts.each do |k, v|
|
138
|
-
space = 23
|
139
|
-
width = space-6
|
140
|
-
key = k[0..width]
|
141
|
-
key += "..." if k.length > width
|
142
|
-
# For clockins/outs, display the number of them instead of all the dates
|
143
|
-
v = v.length if k == :clockins || k == :clockouts
|
144
|
-
puts " #{key}:#{' '*(space-key.length)}#{v}"
|
145
|
-
end
|
146
|
-
end
|
147
|
+
if !generate_clock_file(path) && clock_path
|
148
|
+
puts_error "config file already exists for this repo: #{clock_path}"
|
147
149
|
end
|
148
150
|
exit
|
149
151
|
end
|
data/lib/clockout.rb
CHANGED
@@ -1,296 +1,372 @@
|
|
1
1
|
require 'grit'
|
2
2
|
require 'time'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
COLS = 80
|
6
|
+
DAY_FORMAT = '%B %e, %Y'
|
3
7
|
|
4
8
|
class Commit
|
5
|
-
|
9
|
+
attr_accessor :message, :minutes, :date, :diffs, :sha, :clocked_in, :clocked_out, :addition, :overriden
|
10
|
+
def initialize(commit = nil)
|
11
|
+
@addition = 0
|
12
|
+
if commit
|
13
|
+
@date = commit.committed_date
|
14
|
+
@message = commit.message.gsub("\n",' ')
|
15
|
+
@sha = commit.id
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Clock
|
21
|
+
attr_accessor :in, :out, :date
|
22
|
+
def initialize(type, date)
|
23
|
+
@in = (type == :in)
|
24
|
+
@out = (type == :out)
|
25
|
+
@date = date
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def puts_error(str)
|
30
|
+
puts "Error: ".red + str
|
31
|
+
end
|
32
|
+
|
33
|
+
def align(strings, cols = COLS, sep = " ")
|
34
|
+
ret = ""
|
35
|
+
size = 0
|
36
|
+
strings.each do |string, method|
|
37
|
+
ultimate = (string == strings.keys[-1])
|
38
|
+
penultimate = (string == strings.keys[-2])
|
39
|
+
|
40
|
+
out = string
|
41
|
+
out += " " unless (ultimate || penultimate)
|
42
|
+
|
43
|
+
if ultimate
|
44
|
+
# Add seperator
|
45
|
+
cols_left = cols - size - out.length
|
46
|
+
ret += sep*cols_left if cols_left > 0
|
47
|
+
elsif penultimate
|
48
|
+
last = strings.keys.last.length
|
49
|
+
max_len = cols - size - last - 1
|
50
|
+
if string.length > max_len
|
51
|
+
# Truncate
|
52
|
+
out = string[0..max_len-5].strip + "... "
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Apply color & print
|
57
|
+
ret += method.to_proc.call(out)
|
58
|
+
|
59
|
+
size += out.length
|
60
|
+
end
|
61
|
+
|
62
|
+
ret
|
63
|
+
end
|
64
|
+
|
65
|
+
class String
|
66
|
+
def colorize(color)
|
67
|
+
"\e[0;#{color};49m#{self}\e[0m"
|
68
|
+
end
|
69
|
+
|
70
|
+
def red() colorize(31) end
|
71
|
+
def yellow() colorize(33) end
|
72
|
+
def magenta() colorize(35) end
|
73
|
+
def light_blue() colorize(94) end
|
6
74
|
end
|
7
75
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
76
|
+
class Numeric
|
77
|
+
def as_time(type = nil, min_s = " min", hr_s = " hrs")
|
78
|
+
type = (self < 60) ? :minutes : :hours if !type
|
79
|
+
if type == :minutes
|
80
|
+
"#{self.round(0)}#{min_s}"
|
81
|
+
else
|
82
|
+
"#{(self/60.0).round(2)}#{hr_s}"
|
83
|
+
end
|
84
|
+
end
|
14
85
|
end
|
15
86
|
|
16
87
|
class Clockout
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
88
|
+
def diffs(commit)
|
89
|
+
plus, minus = 0, 0
|
90
|
+
|
91
|
+
commit.stats.to_diffstat.each do |diff_stat|
|
92
|
+
my_files = $opts[:my_files]
|
93
|
+
not_my_files = $opts[:not_my_files]
|
94
|
+
should_include = (diff_stat.filename =~ eval(my_files))
|
95
|
+
should_ignore = not_my_files && (diff_stat.filename =~ eval(not_my_files))
|
96
|
+
if should_include && !should_ignore
|
97
|
+
plus += diff_stat.additions
|
98
|
+
minus += diff_stat.deletions
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Weight deletions half as much, since they are typically
|
103
|
+
# faster to do & also are 1:1 with additions when changing a line
|
104
|
+
plus+minus/2
|
105
|
+
end
|
106
|
+
|
107
|
+
def prepare_data(commits_in)
|
108
|
+
clockins = $opts[:in] || []
|
109
|
+
clockouts = $opts[:out] || []
|
110
|
+
|
111
|
+
# Convert clock-in/-outs into Clock objs & commits into Commit objs
|
112
|
+
clockins.map! { |date| Clock.new(:in, date) }
|
113
|
+
clockouts.map! { |date| Clock.new(:out, date) }
|
114
|
+
commits_in.map! do |commit|
|
115
|
+
c = Commit.new(commit)
|
116
|
+
c.diffs = diffs(commit)
|
117
|
+
c
|
118
|
+
end
|
119
|
+
|
120
|
+
# Merge & sort everything by date
|
121
|
+
data = (commits_in + clockins + clockouts).sort { |a,b| a.date <=> b.date }
|
122
|
+
|
123
|
+
blocks = []
|
124
|
+
total_diffs, total_mins = 0, 0
|
125
|
+
|
126
|
+
add_commit = lambda do |commit|
|
127
|
+
last = blocks.last
|
128
|
+
if !last || (commit.date - last.last.date)/60.0 > $opts[:time_cutoff]
|
129
|
+
blocks << [commit]
|
130
|
+
false
|
131
|
+
else
|
132
|
+
last << commit
|
133
|
+
true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
add_time_to_day = lambda do |time, date|
|
138
|
+
@time_per_day[date.strftime(DAY_FORMAT)] += time
|
139
|
+
end
|
140
|
+
|
141
|
+
# Now go through and coalesce Clocks into Commits, while also splitting into blocks
|
142
|
+
i = 0
|
143
|
+
while i < data.size
|
144
|
+
prev_c = (i == 0) ? nil : data[i-1]
|
145
|
+
next_c = data[i+1]
|
146
|
+
curr_c = data[i]
|
147
|
+
|
148
|
+
if curr_c.class == Clock
|
149
|
+
# If next is also a clock and it's the same type, delete this & use that one instead
|
150
|
+
if next_c && next_c.class == Clock && next_c.in == curr_c.in
|
151
|
+
data.delete_at(i)
|
152
|
+
next
|
153
|
+
end
|
154
|
+
|
155
|
+
# Clock in doesn't do anything, a commit will pick them up
|
156
|
+
# For a clock out...
|
157
|
+
if curr_c.out && prev_c
|
158
|
+
# If previous is an IN, delete both and make a new commit
|
159
|
+
if prev_c.class == Clock && prev_c.in
|
160
|
+
c = Commit.new
|
161
|
+
c.date = curr_c.date # date is "commit date", so on clockout
|
162
|
+
c.minutes = (curr_c.date - prev_c.date)/60.0
|
163
|
+
c.clocked_in, c.clocked_out = true, true
|
164
|
+
|
165
|
+
data.insert(i, c)
|
166
|
+
data.delete(prev_c)
|
167
|
+
data.delete(curr_c)
|
168
|
+
|
169
|
+
add_commit.call(c)
|
170
|
+
add_time_to_day.call(c.minutes, c.date)
|
171
|
+
|
172
|
+
#i is already incremented (we deleted 2 & added 1)
|
173
|
+
next
|
174
|
+
elsif !prev_c.overriden
|
175
|
+
#Otherwise, append time onto the last commit (if it's time wasn't overriden)
|
176
|
+
addition = (curr_c.date - prev_c.date)/60.0
|
177
|
+
if prev_c.minutes
|
178
|
+
prev_c.minutes += addition
|
179
|
+
add_time_to_day.call(addition, prev_c.date)
|
180
|
+
else
|
181
|
+
# This means it's an estimation commit (first one)
|
182
|
+
# Mark how much we shoul add after we've estimated
|
183
|
+
prev_c.addition = addition
|
184
|
+
end
|
185
|
+
prev_c.clocked_out = true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
else
|
189
|
+
# See if this commit was overriden in the config file
|
190
|
+
overrides = $opts[:overrides]
|
191
|
+
overrides.each do |k, v|
|
192
|
+
if curr_c.sha.start_with? k
|
193
|
+
curr_c.minutes = v
|
194
|
+
curr_c.overriden = true
|
195
|
+
break
|
196
|
+
end
|
197
|
+
end if overrides
|
198
|
+
|
199
|
+
if !curr_c.overriden && prev_c
|
200
|
+
curr_c.clocked_in = true if prev_c.class == Clock && prev_c.in
|
201
|
+
# If it added successfully into a block (or was clocked in), we can calculate based on last commit
|
202
|
+
if add_commit.call(curr_c) || curr_c.clocked_in
|
203
|
+
curr_c.minutes = (curr_c.date - prev_c.date)/60.0 # clock or commit, doesn't matter
|
204
|
+
end
|
205
|
+
# Otherwise, we'll do an estimation later, once we have more data
|
206
|
+
end
|
207
|
+
|
208
|
+
if curr_c.minutes
|
209
|
+
add_time_to_day.call(curr_c.minutes, curr_c.date)
|
210
|
+
|
211
|
+
if curr_c.diffs
|
212
|
+
total_diffs += curr_c.diffs
|
213
|
+
total_mins += curr_c.minutes
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
i += 1
|
219
|
+
end
|
220
|
+
|
221
|
+
diffs_per_min = (1.0*total_diffs/total_mins)
|
222
|
+
if !diffs_per_min.nan? && !diffs_per_min.infinite?
|
223
|
+
# Do estimation for all `nil` minutes.
|
224
|
+
blocks.each do |block|
|
225
|
+
first = block.first
|
226
|
+
if !first.minutes
|
227
|
+
first.minutes = first.diffs/diffs_per_min * $opts[:estimation_factor] + first.addition
|
228
|
+
add_time_to_day.call(first.minutes, first.date)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
blocks
|
234
|
+
end
|
235
|
+
|
236
|
+
def print_chart(condensed)
|
237
|
+
cols = condensed ? 30 : COLS
|
238
|
+
total_sum = 0
|
239
|
+
current_day = nil
|
240
|
+
@blocks.each do |block|
|
241
|
+
date = block.first.date.strftime(DAY_FORMAT)
|
242
|
+
if date != current_day
|
243
|
+
puts if (!condensed)
|
244
|
+
|
245
|
+
current_day = date
|
246
|
+
|
247
|
+
sum = @time_per_day[date]
|
248
|
+
total_sum += sum
|
249
|
+
|
250
|
+
puts align({date => :magenta, sum.as_time(:hours) => :red}, cols, ".".magenta)
|
251
|
+
end
|
252
|
+
|
253
|
+
print_timeline(block) if (!condensed)
|
254
|
+
end
|
255
|
+
|
256
|
+
puts align({"-"*10 => :magenta}, cols)
|
257
|
+
puts align({total_sum.as_time(:hours) => :red}, cols)
|
258
|
+
end
|
259
|
+
|
260
|
+
def print_timeline(block)
|
261
|
+
# subtract from the time it took for first commit
|
262
|
+
time = (block.first.date - block.first.minutes*60).strftime('%l:%M %p')+": "
|
263
|
+
print time.yellow
|
264
|
+
|
265
|
+
char_count = time.length
|
266
|
+
|
267
|
+
block.each do |commit|
|
268
|
+
c_mins = commit.minutes.as_time(nil, "m", "h")
|
269
|
+
c_mins = "*#{c_mins}" if commit.clocked_in
|
270
|
+
c_mins += "*" if commit.clocked_out
|
271
|
+
|
272
|
+
seperator = " | "
|
273
|
+
|
274
|
+
add = c_mins.length+seperator.length
|
275
|
+
if char_count + add > COLS-5
|
276
|
+
puts
|
277
|
+
char_count = time.length # indent by the length of the time label on left
|
278
|
+
print " "*char_count
|
279
|
+
end
|
280
|
+
|
281
|
+
char_count += add
|
282
|
+
|
283
|
+
# Blue for clockin/out commits
|
284
|
+
print c_mins+(commit.message ? seperator.red : seperator.light_blue)
|
285
|
+
end
|
286
|
+
puts
|
287
|
+
end
|
288
|
+
|
289
|
+
def print_estimations
|
290
|
+
sum = 0
|
291
|
+
@blocks.each do |block|
|
292
|
+
first = block.first
|
293
|
+
date = first.date.strftime('%b %e')+":"
|
294
|
+
sha = first.sha[0..7]
|
295
|
+
time = first.minutes.as_time
|
296
|
+
|
297
|
+
puts align({date => :yellow, sha => :red, first.message => :to_s, time => :light_blue})
|
298
|
+
|
299
|
+
sum += first.minutes
|
300
|
+
end
|
301
|
+
|
302
|
+
puts align({"-"*10 => :light_blue})
|
303
|
+
puts align({sum.as_time(:hours) => :light_blue})
|
304
|
+
end
|
305
|
+
|
306
|
+
def self.get_repo(path, original_path = nil)
|
307
|
+
begin
|
308
|
+
return Grit::Repo.new(path), path
|
309
|
+
rescue Exception => e
|
310
|
+
if e.class == Grit::NoSuchPathError
|
311
|
+
puts_error "Path '#{path}' could not be found."
|
312
|
+
return nil
|
313
|
+
else
|
314
|
+
# Must have drilled down to /
|
315
|
+
if (path.length <= 1)
|
316
|
+
puts_error "'#{original_path}' is not a Git repository."
|
317
|
+
return nil
|
318
|
+
end
|
319
|
+
|
320
|
+
# Could be that we're in a directory inside the repo
|
321
|
+
# Strip off last directory
|
322
|
+
one_up = path
|
323
|
+
while ((one_up = one_up[0..-2])[-1] != '/') do end
|
324
|
+
|
325
|
+
# Recursively try one level higher
|
326
|
+
return get_repo(one_up[0..-2], path)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.parse_clockfile(file)
|
332
|
+
return nil if !File.exists?(file)
|
333
|
+
|
334
|
+
begin
|
335
|
+
opts = YAML.load_file(file)
|
336
|
+
rescue Exception => e
|
337
|
+
puts_error e.to_s
|
338
|
+
exit
|
339
|
+
end
|
340
|
+
|
341
|
+
# Symbolizes keys
|
342
|
+
Hash[opts.map{|k,v| [k.to_sym, v]}]
|
343
|
+
end
|
344
|
+
|
345
|
+
def self.clock_path(path)
|
346
|
+
path+"/clock.yaml"
|
347
|
+
end
|
348
|
+
|
349
|
+
def self.root_path(path)
|
350
|
+
repo, root_path = get_repo(path)
|
351
|
+
root_path
|
352
|
+
end
|
353
|
+
|
354
|
+
def initialize(path)
|
355
|
+
repo, root_path = Clockout.get_repo(path) || exit
|
356
|
+
|
357
|
+
# Default options
|
358
|
+
$opts = {time_cutoff:120, my_files:"/.*/", estimation_factor:1.0}
|
359
|
+
|
360
|
+
# Parse config options
|
361
|
+
clock_opts = Clockout.parse_clockfile(Clockout.clock_path(root_path))
|
362
|
+
|
363
|
+
# Merge with config override options
|
364
|
+
$opts.merge!(clock_opts) if clock_opts
|
365
|
+
|
366
|
+
commits = repo.commits('master', 500)
|
367
|
+
commits.reverse!
|
368
|
+
|
369
|
+
@time_per_day = Hash.new(0)
|
370
|
+
@blocks = prepare_data(commits)
|
371
|
+
end
|
296
372
|
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.3'
|
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-05-
|
12
|
+
date: 2013-05-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: grit
|