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.
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