clockout 0.1

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 +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: []