hubtime 0.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.
@@ -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