clockout 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/bin/clock +92 -90
  2. data/lib/clockout.rb +362 -286
  3. 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.2
7
- Usage:
8
- $ clock [options]
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 .clock file
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
- ; Ignore initial commit, if it's just template/boilerplate
20
- ; Type: BOOL
21
- ; Default: 0
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
- ;ignore_initial = 1
34
+ ### Time-estimation options for each first commit of a timeblock ###
24
35
 
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
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
- ;time_cutoff = 90
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
- ;; Time-estimation options for each first commit of a timeblock
49
+ #not_my_files: /SomeThirdPartyLibrary/
35
50
 
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
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
- ;my_files = /\\.(m|h|rb|txt)$/
56
+ #estimation_factor: 0.9
45
57
 
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
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
- ;not_my_files = /(ThisFile\\.cpp | SomeOtherClass\\.*)/
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
- ;7deec149 = 25
61
- ;5a6105e6 = 15
62
- ;4325de58 = 120
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
- 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
- else
80
- puts "#{colorize("Error:", RED)} invalid option '#{arg}'."
81
- puts "Try --help for help."
82
- exit
83
- end
84
- end
85
-
86
- opts
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
- clock_path = Clockout.clock_path(path)
91
- if (File.exists?(clock_path))
103
+ path = clock_path(path)
104
+ if (File.exists?(path))
92
105
  false
93
106
  else
94
- File.open(clock_path, "w") do |file|
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
- # Append "in <current date>" or "out <current date>" to the clockfile
108
- File.open(Clockout.clock_path(path), "a") do |file|
109
- file.puts ARGV[0] + " " + Time.new.to_s
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
- puts "Generated .clock file at #{Clockout.clock_path(path)}.\n"
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
- attr_accessor :message, :minutes, :date, :diffs, :sha
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
- RED = 31
9
- YELLOW = 33
10
- MAGENTA = 35
11
- LIGHT_BLUE = 94
12
- def colorize(str, color)
13
- "\e[0;#{color};49m#{str}\e[0m"
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
- COLS = 80
18
- DAY_FORMAT = '%B %e, %Y'
19
-
20
- attr_accessor :blocks
21
-
22
- def diffs(commit)
23
- plus, minus = 0, 0
24
-
25
- commit.stats.to_diffstat.each do |diff_stat|
26
- my_files = $opts[:my_files]
27
- not_my_files = $opts[:not_my_files]
28
- should_include = (diff_stat.filename =~ eval(my_files))
29
- should_ignore = not_my_files && (diff_stat.filename =~ eval(not_my_files))
30
- if should_include && !should_ignore
31
- plus += diff_stat.additions
32
- minus += diff_stat.deletions
33
- end
34
- end
35
-
36
- # Weight deletions half as much, since they are typically
37
- # faster to do & also are 1:1 with additions when changing a line
38
- plus+minus/2
39
- end
40
-
41
- def seperate_into_blocks(repo, commits)
42
- blocks = []
43
- block = []
44
-
45
- total_diffs, total_mins = 0, 0
46
-
47
- prev = nil
48
- commits.each do |commit|
49
-
50
- c = Commit.new
51
- c.date = commit.committed_date
52
- c.message = commit.message.gsub("\n",' ')
53
- c.diffs = diffs(commit)
54
- c.sha = commit.id[0..7]
55
-
56
- if block.size > 0
57
- last_date = block.last.date
58
-
59
- @clockins.each do |clockin|
60
- if clockin > last_date && clockin < c.date
61
- last_date = clockin
62
- @clockins.delete(clockin)
63
- break
64
- end
65
- end
66
-
67
- time_since_last = (last_date - commit.committed_date).abs/60
68
-
69
- if (time_since_last > $opts[:time_cutoff])
70
- blocks << block
71
- block = []
72
- else
73
- c.minutes = time_since_last
74
-
75
- @time_per_day[c.date.strftime(DAY_FORMAT)] += c.minutes
76
-
77
- total_diffs += c.diffs
78
- total_mins += c.minutes
79
- end
80
- end
81
-
82
- block << c
83
- end
84
-
85
- blocks << block
86
-
87
- # Now go through each block's first commit and estimate the time it took
88
- blocks.each do |block|
89
- first = block.first
90
-
91
- # See if they were overriden in the .clock file
92
- if ($opts[first.sha.to_sym])
93
- first.minutes = $opts[first.sha.to_sym].to_i
94
- elsif ($opts[:ignore_initial] && block == blocks.first) || total_diffs == 0
95
- first.minutes = 0
96
- else
97
- first.minutes = $opts[:estimation_factor]*first.diffs*(1.0*total_mins/total_diffs)
98
- end
99
-
100
- @time_per_day[first.date.strftime(DAY_FORMAT)] += first.minutes
101
- end
102
- end
103
-
104
- def print_chart(condensed)
105
- cols = (condensed ? 30 : COLS)
106
- total_sum = 0
107
- current_day = nil
108
- @blocks.each do |block|
109
- date = block.first.date.strftime(DAY_FORMAT)
110
- if date != current_day
111
- puts if (!condensed)
112
-
113
- current_day = date
114
-
115
- sum = @time_per_day[date]
116
- total_sum += sum
117
-
118
- sum_str = "#{(sum/60.0).round(2)} hrs"
119
- print colorize(date,MAGENTA)
120
- print colorize("."*(cols - date.length - sum_str.length),MAGENTA)
121
- print colorize(sum_str,RED)
122
- puts
123
- end
124
-
125
- print_timeline(block) if (!condensed)
126
- end
127
-
128
- puts " "*(cols-10) + colorize("-"*10,MAGENTA)
129
- sum_str = "#{(total_sum/60.0).round(2)} hrs"
130
- puts " "*(cols-sum_str.length) + colorize(sum_str,RED)
131
- end
132
-
133
- def print_timeline(block)
134
- # subtract from the time it took for first commit
135
- time = (block.first.date - block.first.minutes*60).strftime('%l:%M %p')+": "
136
- print colorize(time,YELLOW)
137
-
138
- char_count = time.length
139
-
140
- block.each do |commit|
141
- if commit.minutes < 60
142
- c_mins = "#{commit.minutes.round(0)}m"
143
- else
144
- c_mins = "#{(commit.minutes/60.0).round(1)}h"
145
- end
146
-
147
- seperator = " | "
148
-
149
- add = c_mins.length+seperator.length
150
- if char_count + add > COLS-5
151
- puts
152
- char_count = time.length # indent by the length of the time label on left
153
- print " "*char_count
154
- end
155
-
156
- char_count += add
157
-
158
- print c_mins+colorize(seperator,RED)
159
- end
160
- puts
161
- end
162
-
163
- def print_estimations
164
- sum = 0
165
- @blocks.each do |block|
166
- first = block.first
167
- date = first.date.strftime('%b %e')+": "
168
- sha = first.sha+" "
169
- if first.minutes < 60
170
- time = "#{first.minutes.round(0)} min"
171
- else
172
- time = "#{(first.minutes/60.0).round(2)} hrs"
173
- end
174
-
175
- print colorize(date,YELLOW)
176
- print colorize(sha,RED)
177
-
178
- cutoff = COLS-time.length-date.length-6-sha.length
179
- message = first.message[0..cutoff]
180
- message += "..." if first.message.length > cutoff
181
- print message
182
-
183
- print " "*(COLS-message.length-time.length-date.length-sha.length)
184
- puts colorize(time, LIGHT_BLUE)
185
-
186
- sum += first.minutes
187
- end
188
-
189
- puts " "*(COLS-10) + colorize("-"*10,LIGHT_BLUE)
190
- sum_str = "#{(sum/60.0).round(2)} hrs"
191
- puts " "*(COLS-sum_str.length) + colorize(sum_str, LIGHT_BLUE)
192
- end
193
-
194
- def get_repo(path)
195
- begin
196
- return Grit::Repo.new(path)
197
- rescue Exception => e
198
- print colorize("Error: ", RED)
199
- if e.class == Grit::NoSuchPathError
200
- puts "Path '#{path}' could not be found."
201
- else
202
- puts "'#{path}' is not a Git repository."
203
- end
204
- end
205
- end
206
-
207
- def self.parse_clockfile(file)
208
- return nil if !File.exists?(file)
209
-
210
- opts = {}
211
-
212
- line_num = 0
213
- File.foreach(file) do |line|
214
- line_num += 1
215
- #Strip whitespace
216
- line.strip!
217
- #Strip comments
218
- line = line.split(";",2)[0]
219
-
220
- next if !line || line.length == 0
221
-
222
- sides = line.split("=",2)
223
-
224
- clock_split = sides[0].split(" ",2)
225
- if (clock_split[0] == "in" || clock_split[0] == "out")
226
-
227
- begin
228
- date = Time.parse(clock_split[1])
229
- rescue Exception => e
230
- puts "#{colorize("Error:", RED)} invalid date for '#{clock_split[0]}' on line #{line_num} of .clock file:"
231
- puts " #{line}"
232
-
233
- exit
234
- end
235
-
236
- key = (clock_split[0] == "out") ? :clockouts : :clockins
237
-
238
- opts[key] ||= []
239
- opts[key] << date
240
- else
241
- if sides.length != 2
242
- puts "#{colorize("Error:", RED)} bad syntax on line #{line_num} of .clock file:"
243
- puts " #{line}"
244
- puts ""
245
- puts "Line must be of form:"
246
- puts " KEY = VALUE"
247
-
248
- exit
249
- end
250
-
251
- left = sides[0].strip
252
- right = sides[1].strip
253
-
254
- if left == "ignore_initial"
255
- right = (right != "0")
256
- elsif left == "time_cutoff"
257
- right = right.to_i
258
- elsif left == "estimation_factor"
259
- right = right.to_f
260
- end
261
-
262
- opts[left.to_sym] = right
263
- end
264
- end
265
-
266
- opts
267
- end
268
-
269
- def self.clock_path(path)
270
- path+"/.clock"
271
- end
272
-
273
- def initialize(path)
274
- # Default options
275
- $opts = {time_cutoff:120, my_files:"/.*/", estimation_factor:0.9}
276
-
277
- # Parse .clock options
278
- clock_opts = Clockout.parse_clockfile(Clockout.clock_path(path))
279
-
280
- if clock_opts
281
- @clockins = clock_opts[:clockins] || []
282
- @clockouts = clock_opts[:clockouts] || []
283
-
284
- # Merge with .clock override options
285
- $opts.merge!(clock_opts)
286
- end
287
-
288
- repo = get_repo(path) || exit
289
-
290
- commits = repo.commits('master', 500)
291
- commits.reverse!
292
-
293
- @time_per_day = Hash.new(0)
294
- @blocks = seperate_into_blocks(repo, commits)
295
- end
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.2'
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-07 00:00:00.000000000 Z
12
+ date: 2013-05-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: grit