clockout 0.1

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 +136 -0
  2. data/lib/clockout.rb +248 -0
  3. metadata +65 -0
data/bin/clock ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'clockout'
4
+
5
+ HELP_BANNER = <<-EOS
6
+ Clockout v0.1
7
+ Usage:
8
+ $ clock [options]
9
+
10
+ Options:
11
+ --estimations, -e: Show estimations made for first commit of each block
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
15
+ --help, -h: Show this message
16
+ EOS
17
+
18
+ TEMPLATE_CLOCKFILE = <<-EOF
19
+ ; Ignore initial commit, if it's just template/boilerplate
20
+ ; Type: BOOL
21
+ ; Default: 0
22
+
23
+ ;ignore_initial = 1
24
+
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
30
+
31
+ ;time_cutoff = 90
32
+
33
+
34
+ ;; Time-estimation options for each first commit of a timeblock
35
+
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
43
+
44
+ ;my_files = /\.(m|h|rb|txt)$/
45
+
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
52
+
53
+ ;not_my_files = /(ThisFile\.cpp | SomeOtherClass\.*)/
54
+
55
+ ; Completion time overrides for commit estimations
56
+ ; Type: Int (in minutes)
57
+ ;
58
+ ; Override times for specific commits here.
59
+
60
+ ;7deec149 = 25
61
+ ;5a6105e6 = 15
62
+ ;4325de58 = 120
63
+ EOF
64
+
65
+ def parse_options(args)
66
+ opts = {}
67
+
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
+ elsif (arg[0] == "-")
80
+ puts "Error: invalid option '#{arg}'."
81
+ puts "Try --help for help."
82
+ exit
83
+ end
84
+ end
85
+
86
+ opts
87
+ end
88
+
89
+ path = Dir.pwd
90
+
91
+ # Default options
92
+ opts = {time_cutoff:120, my_files:"/.*/"}
93
+ opts.merge!(parse_options(ARGV))
94
+
95
+ if opts[:generate_clock]
96
+ clock_path = path+"/.clock"
97
+ if (File.exists?(clock_path))
98
+ puts "#{colorize("Error:", RED)} .clock file already exists, ignoring --generate-clock option.\n"
99
+ else
100
+ File.open(clock_path, "w") do |file|
101
+ file.write(TEMPLATE_CLOCKFILE)
102
+ end
103
+ puts "Generated .clock file at #{clock_path}.\n"
104
+ end
105
+ end
106
+
107
+ clock = Clockout.new(path, opts)
108
+
109
+ if opts[:see_clock]
110
+ if !clock.clock_opts
111
+ puts "No .clock file found. Run `clock -g` to generate one."
112
+ else
113
+ if clock.clock_opts.size == 0
114
+ puts "No clock options."
115
+ else
116
+ puts "Clock options:"
117
+ clock.clock_opts.each do |k, v|
118
+ key = k[0..19]
119
+ puts " #{key}:#{' '*(20-key.length)}#{v}"
120
+ end
121
+ end
122
+ end
123
+ exit
124
+ end
125
+
126
+ if opts[:help]
127
+ puts HELP_BANNER
128
+ exit
129
+ end
130
+
131
+ if (opts[:estimations])
132
+ clock.print_estimations
133
+ else
134
+ clock.print_chart
135
+ end
136
+
data/lib/clockout.rb ADDED
@@ -0,0 +1,248 @@
1
+ require 'grit'
2
+
3
+ class Commit
4
+ attr_accessor :message, :minutes, :date, :diffs, :sha
5
+ end
6
+
7
+ RED = 31
8
+ YELLOW = 33
9
+ MAGENTA = 35
10
+ LIGHT_BLUE = 94
11
+ def colorize(str, color)
12
+ "\e[0;#{color};49m#{str}\e[0m"
13
+ end
14
+
15
+ class Clockout
16
+ COLS = 80
17
+ DAY_FORMAT = '%B %e, %Y'
18
+
19
+ attr_accessor :blocks, :time_per_day, :clock_opts
20
+
21
+ def diffs(commit)
22
+ plus, minus = 0, 0
23
+
24
+ commit.stats.to_diffstat.each do |diff_stat|
25
+ my_files = $opts[:my_files]
26
+ not_my_files = $opts[:not_my_files]
27
+ should_include = (diff_stat.filename =~ eval(my_files))
28
+ should_ignore = not_my_files && (diff_stat.filename =~ eval(not_my_files))
29
+ if should_include && !should_ignore
30
+ plus += diff_stat.additions
31
+ minus += diff_stat.deletions
32
+ end
33
+ end
34
+
35
+ # Weight deletions half as much, since they are typically
36
+ # faster to do & also are 1:1 with additions when changing a line
37
+ plus+minus/2
38
+ end
39
+
40
+ def seperate_into_blocks(repo, commits)
41
+ blocks = []
42
+ block = []
43
+
44
+ total_diffs, total_mins = 0, 0
45
+
46
+ commits.each do |commit|
47
+
48
+ c = Commit.new
49
+ c.date = commit.committed_date
50
+ c.message = commit.message.gsub("\n",' ')
51
+ c.diffs = diffs(commit)
52
+ c.sha = commit.id[0..7]
53
+
54
+ if block.size > 0
55
+ time_since_last = (block.last.date - commit.committed_date).abs/60
56
+
57
+ if (time_since_last > $opts[:time_cutoff])
58
+ blocks << block
59
+ block = []
60
+ else
61
+ c.minutes = time_since_last
62
+
63
+ @time_per_day[c.date.strftime(DAY_FORMAT)] += c.minutes
64
+
65
+ total_diffs += c.diffs
66
+ total_mins += c.minutes
67
+ end
68
+ end
69
+
70
+ block << c
71
+ end
72
+
73
+ blocks << block
74
+
75
+ # Now go through each block's first commit and estimate the time it took
76
+ blocks.each do |block|
77
+ first = block.first
78
+
79
+ # See if they were overriden in the .clock file
80
+ if ($opts[first.sha.to_sym])
81
+ first.minutes = $opts[first.sha.to_sym].to_i
82
+ elsif ($opts[:ignore_initial] && block == blocks.first) || total_diffs == 0
83
+ first.minutes = 0
84
+ else
85
+ # Underestimate by a factor of 0.9
86
+ first.minutes = 0.9*first.diffs*(1.0*total_mins/total_diffs)
87
+ end
88
+
89
+ @time_per_day[first.date.strftime(DAY_FORMAT)] += first.minutes
90
+ end
91
+ end
92
+
93
+ def print_chart
94
+ cols = ($opts[:condensed] ? 30 : COLS)
95
+ total_sum = 0
96
+ current_day = nil
97
+ @blocks.each do |block|
98
+ date = block.first.date.strftime(DAY_FORMAT)
99
+ if date != current_day
100
+ puts if (!$opts[:condensed])
101
+
102
+ current_day = date
103
+
104
+ sum = @time_per_day[date]
105
+ total_sum += sum
106
+
107
+ sum_str = "#{(sum/60.0).round(2)} hrs"
108
+ print colorize(date,MAGENTA)
109
+ print colorize("."*(cols - date.length - sum_str.length),MAGENTA)
110
+ print colorize(sum_str,RED)
111
+ puts
112
+ end
113
+
114
+ print_timeline(block) if (!$opts[:condensed])
115
+ end
116
+
117
+ puts " "*(cols-10) + colorize("-"*10,MAGENTA)
118
+ sum_str = "#{(total_sum/60.0).round(2)} hrs"
119
+ puts " "*(cols-sum_str.length) + colorize(sum_str,RED)
120
+ end
121
+
122
+ def print_timeline(block)
123
+ # subtract from the time it took for first commit
124
+ time = (block.first.date - block.first.minutes*60).strftime('%l:%M %p')+": "
125
+ print colorize(time,YELLOW)
126
+
127
+ char_count = time.length
128
+
129
+ block.each do |commit|
130
+ if commit.minutes < 60
131
+ c_mins = "#{commit.minutes.round(0)}m"
132
+ else
133
+ c_mins = "#{(commit.minutes/60.0).round(1)}h"
134
+ end
135
+
136
+ seperator = " | "
137
+
138
+ add = c_mins.length+seperator.length
139
+ if char_count + add > COLS-5
140
+ puts
141
+ char_count = time.length # indent by the length of the time label on left
142
+ print " "*char_count
143
+ end
144
+
145
+ char_count += add
146
+
147
+ print c_mins+colorize(seperator,RED)
148
+ end
149
+ puts
150
+ end
151
+
152
+ def print_estimations
153
+ sum = 0
154
+ @blocks.each do |block|
155
+ first = block.first
156
+ date = first.date.strftime('%b %e')+": "
157
+ sha = first.sha+" "
158
+ if first.minutes < 60
159
+ time = "#{first.minutes.round(0)} min"
160
+ else
161
+ time = "#{(first.minutes/60.0).round(2)} hrs"
162
+ end
163
+
164
+ print colorize(date,YELLOW)
165
+ print colorize(sha,RED)
166
+
167
+ cutoff = COLS-time.length-date.length-6-sha.length
168
+ message = first.message[0..cutoff]
169
+ message += "..." if first.message.length > cutoff
170
+ print message
171
+
172
+ print " "*(COLS-message.length-time.length-date.length-sha.length)
173
+ puts colorize(time, LIGHT_BLUE)
174
+
175
+ sum += first.minutes
176
+ end
177
+
178
+ puts " "*(COLS-10) + colorize("-"*10,LIGHT_BLUE)
179
+ sum_str = "#{(sum/60.0).round(2)} hrs"
180
+ puts " "*(COLS-sum_str.length) + colorize(sum_str, LIGHT_BLUE)
181
+ end
182
+
183
+ def get_repo(path)
184
+ begin
185
+ return Grit::Repo.new(path)
186
+ rescue Exception => e
187
+ if e.class == Grit::NoSuchPathError
188
+ puts "Error: Path '#{path}' could not be found."
189
+ else
190
+ puts "Error: '#{path}' is not a Git repository."
191
+ end
192
+ end
193
+ end
194
+
195
+ def parse_clockfile(file)
196
+ return nil if !File.exists?(file)
197
+
198
+ opts = {}
199
+
200
+ line_num = 0
201
+ File.foreach(file) do |line|
202
+ line_num += 1
203
+ line.strip!
204
+
205
+ next if line[0] == ";" || line.length == 0
206
+
207
+ sides = line.split("=",2)
208
+
209
+ if sides.length != 2
210
+ puts "Error: bad syntax on line #{line_num} of .clock file:"
211
+ puts " #{line}"
212
+ puts ""
213
+ puts "Line must be of form:"
214
+ puts " KEY = VALUE"
215
+
216
+ exit
217
+ end
218
+
219
+ left = sides[0].strip
220
+ right = sides[1].strip
221
+
222
+ if left == "ignore_initial"
223
+ right = (right != "0")
224
+ elsif left == "time_cutoff"
225
+ right = right.to_i
226
+ end
227
+
228
+ opts[left.to_sym] = right
229
+ end
230
+
231
+ opts
232
+ end
233
+
234
+ def initialize(path, options)
235
+ $opts = options
236
+
237
+ @clock_opts = parse_clockfile(path+"/.clock")
238
+ $opts.merge!(@clock_opts) if @clock_opts
239
+
240
+ repo = get_repo(path) || exit
241
+
242
+ commits = repo.commits('master', 500)
243
+ commits.reverse!
244
+
245
+ @time_per_day = Hash.new(0)
246
+ @blocks = seperate_into_blocks(repo, commits)
247
+ end
248
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clockout
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dan Hassin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: grit
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: An sort of extension to Git to support clocking hours worked on a project.
31
+ email:
32
+ - danhassin@mac.com
33
+ executables:
34
+ - clock
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - lib/clockout.rb
39
+ - !binary |-
40
+ YmluL2Nsb2Nr
41
+ homepage: http://rubygems.org/gems/clockout
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.24
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Clock your hours worked using Git
65
+ test_files: []