hubtime 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,494 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Hubtime
4
+ class Activity
5
+ class Period
6
+ attr_reader :example, :label, :compiled, :children
7
+ attr_reader :total_stats, :repo_stats
8
+ def initialize(label, example_time=nil)
9
+ @example = example_time
10
+ @label = label
11
+ @children = {}
12
+ @total_stats = default_stats
13
+ @repo_stats = Hash.new{ |hash, key| hash[key] = default_stats }
14
+ @compiled = nil
15
+ end
16
+
17
+ def add(commit)
18
+ self.commits << commit
19
+ self.total_stats.keys.each do |key|
20
+ self.total_stats[key] += commit.send(key)
21
+ end
22
+ self.repo_stats[commit.repo_name].keys.each do |key|
23
+ self.repo_stats[commit.repo_name][key] += commit.send(key)
24
+ end
25
+
26
+ if self.class.child_class
27
+ klass = self.class.child_class
28
+ key = klass.display(commit.time)
29
+ @children[key] ||= klass.new(key, commit.time)
30
+ @children[key].add(commit)
31
+ end
32
+ end
33
+
34
+ def compiled?
35
+ !!@compiled
36
+ end
37
+
38
+ def compile!(parent)
39
+ return if compiled?
40
+ first = nil
41
+ last = nil
42
+ filled = {}
43
+ child_keys(parent).each do |key|
44
+ filled[key] = @children[key]
45
+ if filled[key]
46
+ first ||= filled[key]
47
+ last = filled[key]
48
+ else
49
+ filled[key] = self.class.child_class.new(key)
50
+ end
51
+ end
52
+
53
+ first.first! if first
54
+ last.last! if last
55
+
56
+ filled.values.each do |child|
57
+ child.compile!(self)
58
+ end
59
+
60
+ @children = filled
61
+
62
+ @compiled = {}
63
+ @compiled["total_stats"] = self.total_stats
64
+ @compiled["repo_stats"] = self.repo_stats
65
+
66
+ @compiled["children"] = {}
67
+ @children.each do |key, period|
68
+ @compiled["children"][key] = period.compiled
69
+ end
70
+ end
71
+
72
+ def import(stats)
73
+ child_list = stats.delete("children")
74
+ @compiled = stats
75
+ @total_stats = stats["total_stats"]
76
+ @repo_stats = stats["repo_stats"]
77
+ @children = {}
78
+ child_list.each do |key, child_stats|
79
+ @children[key] = self.class.child_class.new(key)
80
+ @children[key].import(child_stats)
81
+ end
82
+ end
83
+
84
+ def commits
85
+ @commits ||= []
86
+ end
87
+
88
+ def repositories
89
+ repo_stats.keys
90
+ end
91
+
92
+ def count(repo=nil)
93
+ key_value("count", repo)
94
+ end
95
+
96
+ def additions(repo=nil)
97
+ key_value("additions", repo)
98
+ end
99
+
100
+ def deletions(repo=nil)
101
+ key_value("deletions", repo)
102
+ end
103
+
104
+ def impact(repo=nil)
105
+ key_value("impact", repo)
106
+ end
107
+
108
+ def key_value(key, repo = nil)
109
+ if repo.nil? || repo == "total"
110
+ hash = total_stats
111
+ else
112
+ hash = repo_stats[repo]
113
+ end
114
+ return 0 unless hash
115
+ hash[key.to_s]
116
+ end
117
+
118
+ def first!
119
+ @first = true
120
+ end
121
+
122
+ def first?
123
+ !!@first
124
+ end
125
+
126
+ def last!
127
+ @last = true
128
+ end
129
+
130
+ def last?
131
+ !!@last
132
+ end
133
+
134
+ def child_keys(parent)
135
+ return [] if self.class.child_class.nil?
136
+
137
+ first = self._first_child_key(parent)
138
+ last = self._last_child_key(parent)
139
+ (first..last).to_a
140
+ end
141
+
142
+ def _first_child_key(parent)
143
+ if first? && (!parent || parent.first?) && children.keys.size > 0
144
+ children.keys.sort.first
145
+ else
146
+ first_child_key
147
+ end
148
+ end
149
+
150
+ def _last_child_key(parent)
151
+ if last? && (!parent || parent.last?) && children.keys.size > 0
152
+ children.keys.sort.last
153
+ else
154
+ last_child_key
155
+ end
156
+ end
157
+
158
+ def default_stats
159
+ {"additions" => 0, "deletions" => 0, "impact" => 0, "count" => 0}
160
+ end
161
+ end
162
+
163
+ class Forever < Period
164
+ def self.display(time)
165
+ "All"
166
+ end
167
+
168
+ def self.child_class
169
+ Year
170
+ end
171
+
172
+ attr_reader :cache_key
173
+ def initialize(cache_key)
174
+ @cache_key = cache_key
175
+ super(self.class.display(Time.zone.now), Time.zone.now)
176
+ end
177
+
178
+ def self.cacher
179
+ @cacher ||= Cacher.new("activity")
180
+ end
181
+
182
+ def self.load(username, start_time, end_time)
183
+ key = "#{username}/#{start_time.to_i}-#{end_time.to_i}"
184
+ out = self.new(key)
185
+ if stats = cacher.read(key)
186
+ out.import(stats)
187
+ end
188
+ out
189
+ end
190
+
191
+ def store!
192
+ raise "not compiled" unless compiled?
193
+ self.class.cacher.write(cache_key, @compiled)
194
+ end
195
+
196
+ def first?
197
+ true
198
+ end
199
+
200
+ def last?
201
+ true
202
+ end
203
+
204
+ def each(unit, &block)
205
+ unit = unit.to_s
206
+ if unit == "all"
207
+ yield "All", self
208
+ else
209
+ self.children.each do |year_key, year|
210
+ if unit == "year"
211
+ yield "#{year_key}", year
212
+ else
213
+ year.children.each do |month_key, month|
214
+ if unit == "month"
215
+ yield "#{year_key}-#{month_key}", month
216
+ else
217
+ month.children.each do |day_key, day|
218
+ yield "#{year_key}-#{month_key}-#{day_key}", day
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ class Year < Period
229
+ def self.display(time)
230
+ time.strftime("%Y")
231
+ end
232
+
233
+ def self.child_class
234
+ Month
235
+ end
236
+
237
+ def first_child_key
238
+ "01"
239
+ end
240
+
241
+ def last_child_key
242
+ "12"
243
+ end
244
+ end
245
+
246
+ class Month < Period
247
+ def self.display(time)
248
+ time.strftime("%m")
249
+ end
250
+
251
+ def self.child_class
252
+ Day
253
+ end
254
+
255
+ def first_child_key
256
+ "01"
257
+ end
258
+
259
+ def last_child_key
260
+ if self.example
261
+ # how many days in this month
262
+ self.class.child_class.display(self.example.end_of_month)
263
+ else
264
+ # TODO: nothing this month?
265
+ "30"
266
+ end
267
+ end
268
+ end
269
+
270
+ class Day < Period
271
+ def self.display(time)
272
+ time.strftime("%d")
273
+ end
274
+
275
+ def self.child_class
276
+ nil
277
+ end
278
+ end
279
+
280
+ attr_reader :username, :start_time, :end_time
281
+ def initialize(cli, username, num_months)
282
+ num_months = num_months.to_i
283
+ num_months = 12 if num_months <= 0
284
+ username ||= HubConfig.user
285
+ raise("Need github user name. Use hubtime auth") unless username
286
+
287
+ Time.zone = "Pacific Time (US & Canada)" # TODO: command to allow this being set
288
+ @cli = cli
289
+ @username = username
290
+ @start_time = (num_months-1).months.ago.beginning_of_month
291
+ @end_time = Time.zone.now.end_of_month
292
+
293
+ @time = Forever.load(username, start_time, end_time)
294
+ end
295
+
296
+ def compile!
297
+ unless @time.compiled?
298
+ GithubService.owner.commits(username, start_time, end_time) do |commit|
299
+ @time.add(commit)
300
+ end
301
+ puts "... compiling data for #{username}"
302
+ @time.compile!(nil)
303
+ @time.store!
304
+ end
305
+ end
306
+
307
+ def table(unit = :month)
308
+ compile!
309
+ table = Terminal::Table.new(:headings => [unit.to_s.titleize, 'Commits', 'Impact', 'Additions', 'Deletions'])
310
+
311
+ @time.each(unit) do |label, period|
312
+ table.add_row [label, period.count, period.impact, period.additions, period.deletions]
313
+ end
314
+ table
315
+ end
316
+
317
+ def spark(unit = :month, type = :impact)
318
+ compile!
319
+ data = []
320
+ @time.each(unit) do |label, period|
321
+ data << period.send(type)
322
+ end
323
+
324
+ ticks=%w[ ▁ ▂ ▃ ▄ ▅ ▆ ▇ ]
325
+
326
+ return "" if data.size == 0
327
+ return ticks.last if data.size == 1
328
+
329
+ distance = data.max.to_f / ticks.size
330
+
331
+ str = ''
332
+ data.each do |val|
333
+ if val == 0
334
+ str << " "
335
+ else
336
+ tick = (val / distance).round - 1
337
+ str << ticks[tick]
338
+ end
339
+ end
340
+ str
341
+ end
342
+
343
+ def impact
344
+ compile!
345
+
346
+ additions = []
347
+ deletions = []
348
+ impacts = []
349
+ labels = []
350
+
351
+ week_additions = 0
352
+ week_deletions = 0
353
+ week_label = nil
354
+
355
+ total_impact = 0
356
+
357
+ day = 0
358
+ @time.each(:day) do |label, period|
359
+ day += 1
360
+ week_label ||= label
361
+ week_additions += period.additions
362
+ week_deletions += period.deletions
363
+ total_impact += period.impact
364
+
365
+ if (day % 7) == 0
366
+ additions << week_additions
367
+ deletions << week_deletions
368
+ impacts << total_impact
369
+
370
+ if (day % 28) == 0
371
+ labels << week_label
372
+ else
373
+ labels << ""
374
+ end
375
+
376
+ week_additions = 0
377
+ week_deletions = 0
378
+ week_label = nil
379
+ end
380
+ end
381
+
382
+ charts = File.join(File.dirname(__FILE__), "charts")
383
+ template = File.read(File.join(charts, "impact.erb"))
384
+ template = Erubis::Eruby.new(template)
385
+ html = template.result(:additions => additions, :deletions => deletions, :impacts => impacts, :labels => labels, :username => username)
386
+
387
+ root = File.join(File.expand_path("."), "data", "charts")
388
+ path = "#{username}-impact-#{start_time.to_i}-#{end_time.to_i}.html"
389
+ file_name = File.join(root, path)
390
+ directory = File.dirname(file_name)
391
+ FileUtils.mkdir_p(directory) unless File.exist?(directory)
392
+ File.open(file_name, 'w') {|f| f.write(html) }
393
+ file_name
394
+ end
395
+
396
+ def default_week(keys)
397
+ default_week = {}
398
+ keys.each do |key|
399
+ default_week[key] = {"data" => 0}
400
+ end
401
+ default_week
402
+ end
403
+
404
+ def graph(type = :impact, stacked=false)
405
+ compile!
406
+
407
+ type = type.to_s
408
+ other = "count"
409
+ other = "impact" if type == "count"
410
+
411
+ others = []
412
+ labels = []
413
+
414
+ data = {}
415
+ if stacked
416
+ keys = @time.repositories
417
+ else
418
+ keys = ["total"]
419
+ end
420
+
421
+ keys.each do |key|
422
+ data[key] = []
423
+ end
424
+
425
+ week = default_week(keys)
426
+ week_other = 0
427
+ week_label = nil
428
+ day = 0
429
+ @time.each(:day) do |label, period|
430
+ day += 1
431
+ week_label ||= label
432
+ keys.each do |key|
433
+ week[key]["data"] += period.send(type, key)
434
+ end
435
+ week_other += period.send(other)
436
+
437
+ if (day % 7) == 0
438
+ keys.each do |key|
439
+ data[key] << week[key]["data"]
440
+ end
441
+ others << week_other
442
+
443
+ if (day % 28) == 0
444
+ labels << week_label
445
+ else
446
+ labels << ""
447
+ end
448
+
449
+ week = default_week(keys)
450
+ week_other = 0
451
+ week_label = nil
452
+ end
453
+ end
454
+
455
+ charts = File.join(File.dirname(__FILE__), "charts")
456
+ template = File.read(File.join(charts, "graph.erb"))
457
+ template = Erubis::Eruby.new(template)
458
+ html = template.result(:data => data, :other => others, :labels => labels, :data_type => type, :other_type => other, :username => username)
459
+
460
+ root = File.join(File.expand_path("."), "data", "charts")
461
+ path = "#{username}-graph-#{type}-#{start_time.to_i}-#{end_time.to_i}.html"
462
+ file_name = File.join(root, path)
463
+ directory = File.dirname(file_name)
464
+ FileUtils.mkdir_p(directory) unless File.exist?(directory)
465
+ File.open(file_name, 'w') {|f| f.write(html) }
466
+ file_name
467
+ end
468
+
469
+ def pie(type = :impact)
470
+ compile!
471
+
472
+ type = type.to_s
473
+ data = []
474
+
475
+ @time.repositories.each do |repo_name|
476
+ value = @time.send(type, repo_name)
477
+ data << [repo_name, value] if value > 0
478
+ end
479
+
480
+ charts = File.join(File.dirname(__FILE__), "charts")
481
+ template = File.read(File.join(charts, "pie.erb"))
482
+ template = Erubis::Eruby.new(template)
483
+ html = template.result(:data => data, :data_type => type, :username => username)
484
+
485
+ root = File.join(File.expand_path("."), "data", "charts")
486
+ path = "#{username}-pie-#{type}-#{start_time.to_i}-#{end_time.to_i}.html"
487
+ file_name = File.join(root, path)
488
+ directory = File.dirname(file_name)
489
+ FileUtils.mkdir_p(directory) unless File.exist?(directory)
490
+ File.open(file_name, 'w') {|f| f.write(html) }
491
+ file_name
492
+ end
493
+ end
494
+ end