github_issue_stats 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae17636f4ac0fbedef888f294dc9a493e2dd1c88
4
- data.tar.gz: 19b0639e7a6341f463dc784f631a9b193af550af
3
+ metadata.gz: af02c65f57d4da09083fcce26bc1f12fb88aec31
4
+ data.tar.gz: 45d24e5d5e66c55e9d67a044d6c2e7f7a54fd6d0
5
5
  SHA512:
6
- metadata.gz: ab26cc8b8bb2832c6a86fc35e400379e477455a142e06378915cdc4910cb2a6cff2d511b3de999967a3986dbba1e581ddee79d6249d028646cf6971a7a5faa3e
7
- data.tar.gz: 9eb615a02c1ffd81ba6621def0b510d219d693743e87f3df161898e83491ea4bacf99115de7f38be3a3ad2f3c11e90bc2b3e9c1801ea2a455c520dd8e31a8132
6
+ metadata.gz: 9bae404551ef009a2071c21ced6a293064607a8fba412e5be77dde329de90bdac5cf68b6308ab4190de95c9a4c3358ebfa1a89bfbbdee9f917cf1d7f3ad3234c
7
+ data.tar.gz: 3f3a9e7d886844979bbde704872cf1582b3c72364ac1f4b17071a32c07ba7299826d15839f6bdaaee9c1c6050cbee712526b2cad94e2dca83fdc60b4f543b682
@@ -16,6 +16,51 @@ Commander.configure do
16
16
  global_option '--verbose', "Enable output of detailed debugging information to STDERR"
17
17
  global_option '--token STRING', String, "GitHub OAuth token for making API calls. If not specified, the GITHUB_OAUTH_TOKEN environment variable is used. Create a token here: https://github.com/settings/token"
18
18
 
19
+ command :breakdown do |c|
20
+ c.syntax = 'github_issue_stats breakdown [options]'
21
+ c.description = 'Collect stats on number of open issues based on age'
22
+
23
+ c.option '-s', '--scopes x,y,z', Array, "(required) List of scopes for which stats will be collected. A scope is a username or repo name. Example: --scopes github,rails/rails"
24
+ c.option '-l', '--labels [x,y,z]', Array, "List of labels for which stats will be collected for each scope. A label is an issue or pull request label, or special values 'issues' and 'pulls' representing all issues and all pull requests within the scope respectively. Default: 'issues'."
25
+ c.option '-i', '--intervals [x,y,z]', Array, "List of intervals defining buckets into which issues will be grouped, relative to the current date and time. Intervals are defined with N[hdwmy], where h is hour, d is day, w is week m is month, y is year, and N is a positive integer used as a multiplier. Default: '1w,1m,3m,6m,12m,18m'."
26
+ c.option '-o', '--output_format [STRING]', String, "Format used for output tables with collected stats. Can be 'text' or 'markdown'. Default: 'text'."
27
+
28
+ c.example "Statistics for the atom organization, for issues, pull requests, bug and enhancement labels, with buckets defined by one month, three months, one year, and two years, with Markdown output", "github_issue_stats breakdown -s atom -l issues,bug,enhancement,pulls -i 1m,3m,1y,2y -o markdown"
29
+
30
+ c.action do |args, options|
31
+ options.default \
32
+ :verbose => false,
33
+ :token => ENV["GITHUB_OAUTH_TOKEN"],
34
+ :output_format => 'text',
35
+ :labels => "issues",
36
+ :intervals => ["1w", "1m", "3m", "6m", "12m", "18m"]
37
+
38
+ options.scopes = Array(options.scopes)
39
+ options.labels = Array(options.labels)
40
+ options.intervals = Array(options.intervals)
41
+
42
+ raise ArgumentError.new("--token is required") if options.token.nil?
43
+ raise ArgumentError.new("invalid --token format") unless /\A\h{40}\z/.match(options.token)
44
+ raise ArgumentError.new("--scopes is required") if options.scopes.nil?
45
+ raise ArgumentError.new("invalid --intervals format") if options.intervals.nil?
46
+ raise ArgumentError.new("invalid --output_format") unless /\A(text)|(markdown)\z/.match(options.output_format)
47
+
48
+ github_issue_stats = GitHubIssueStats.new(options.token, options.verbose)
49
+
50
+ STDERR.print "Collecting stats..."
51
+ STDERR.flush
52
+
53
+ stats = github_issue_stats.get_breakdown_statistics(options.__hash__)
54
+ tables = github_issue_stats.generate_breakdown_tables(stats, options.__hash__)
55
+
56
+ for scope, table in tables
57
+ puts "\n#{scope} stats:\n\n#{table}"
58
+ end
59
+
60
+ cli.say("Done!")
61
+ end
62
+ end
63
+
19
64
  command :history do |c|
20
65
  c.syntax = 'github_issue_stats history [options]'
21
66
  c.description = 'Collect stats on number of open issues over time'
@@ -24,7 +69,7 @@ Commander.configure do
24
69
  c.option '-l', '--labels [x,y,z]', Array, "List of labels for which stats will be collected for each scope. A label is an issue or pull request label, or special values 'issues' and 'pulls' representing all issues and all pull requests within the scope respectively. Default: 'issues'."
25
70
  c.option '-i', '--interval_length [STRING]', String, "Size of interval for which stats will be aggregated. Intervals are defined with N[hdwmy], where h is hour, d is day, w is week m is month, y is year, and N is a positive integer used as a multiplier. Default: '1w'."
26
71
  c.option '-n', '--interval_count [INTEGER]', Integer, "Number of intervals for which stats will be collected. Default: 4."
27
- c.option '-o', '--output_format [STRING]', String, "Format used for output tables with collected stats. Can be 'text' or 'markdown'."
72
+ c.option '-o', '--output_format [STRING]', String, "Format used for output tables with collected stats. Can be 'text' or 'markdown'. Default: 'text'."
28
73
 
29
74
  c.example "Statistics for the atom organization, for issues, pull requests, bug and enhancement labels, going back four one-week intervals, with Markdown output", "github_issue_stats history -s atom -l issues,bug,enhancement,pulls -i 1w -n 4 -o markdown"
30
75
 
@@ -53,7 +98,7 @@ Commander.configure do
53
98
  STDERR.flush
54
99
 
55
100
  stats = github_issue_stats.get_history_statistics(options.__hash__)
56
- tables = github_issue_stats.generate_tables(stats, options.__hash__)
101
+ tables = github_issue_stats.generate_history_tables(stats, options.__hash__)
57
102
 
58
103
  for scope, table in tables
59
104
  puts "\n#{scope} stats:\n\n#{table}"
@@ -30,7 +30,7 @@ module Enumerable
30
30
  end
31
31
 
32
32
  class GitHubIssueStats
33
- VERSION = "0.3.0"
33
+ VERSION = "0.4.0"
34
34
 
35
35
  attr_accessor :client, # Octokit client for acesing the API
36
36
  :logger, # Logger for writing debugging info
@@ -214,17 +214,17 @@ class GitHubIssueStats
214
214
  period_type = period[1]
215
215
 
216
216
  if period_type == "h"
217
- return Time.new(current_time.year, current_time.month, current_time.day, current_time.hour, 0, 0, "+00:00")
217
+ return Time.utc(current_time.year, current_time.month, current_time.day, current_time.hour, 0, 0)
218
218
  elsif period_type == "d"
219
- return Time.new(current_time.year, current_time.month, current_time.day, 0, 0, 0, "+00:00")
219
+ return Time.utc(current_time.year, current_time.month, current_time.day, 0, 0, 0)
220
220
  elsif period_type == "w"
221
221
  current_date = Date.new(current_time.year, current_time.month, current_time.day)
222
222
  previous_date = current_date - (current_date.cwday - 1)
223
- previous_time = Time.new(previous_date.year, previous_date.month, previous_date.day, 0, 0, 0, "+00:00")
223
+ previous_time = Time.utc(previous_date.year, previous_date.month, previous_date.day, 0, 0, 0)
224
224
  elsif period_type == "m"
225
- return Time.new(current_time.year, current_time.month, 1, 0, 0, 0, "+00:00")
225
+ return Time.utc(current_time.year, current_time.month, 1, 0, 0, 0)
226
226
  elsif period_type == "y"
227
- return Time.new(current_time.year, 1, 1, 0, 0, 0, "+00:00")
227
+ return Time.utc(current_time.year, 1, 1, 0, 0, 0)
228
228
  else
229
229
  # TODO throw error
230
230
  end
@@ -234,7 +234,7 @@ class GitHubIssueStats
234
234
  # Computes the the beginning of the period based on the end of a period
235
235
  #
236
236
  def compute_previous_time(current_time, period)
237
- period_number, period_type = period.chars
237
+ period_number, period_type = [period[0..-2], period[-1]]
238
238
  period_number = Integer(period_number)
239
239
 
240
240
  if period_type == "h"
@@ -242,13 +242,19 @@ class GitHubIssueStats
242
242
  elsif period_type == "d"
243
243
  return current_time - period_number * 3600 * 24
244
244
  elsif period_type == "w"
245
- return current_time - 7 * 3600 * 24
245
+ return current_time - period_number * 7 * 3600 * 24
246
246
  elsif period_type == "m"
247
247
  current_date = Date.new(current_time.year, current_time.month, current_time.day)
248
- previous_date = current_date.prev_month
249
- previous_time = Time.new(previous_date.year, previous_date.month, previous_date.day, current_time.hour, current_time.min, current_time.sec, "+00:00")
248
+
249
+ temp_date = current_date
250
+ for i in 1..period_number
251
+ previous_date = temp_date.prev_month
252
+ temp_date = previous_date
253
+ end
254
+
255
+ previous_time = Time.utc(previous_date.year, previous_date.month, previous_date.day, current_time.hour, current_time.min, current_time.sec)
250
256
  elsif period_type == "y"
251
- return Time.new(current_time.year - 1, current_time.month, current_time.day, current_time.hour, current_time.min, current_time.sec, "+00:00")
257
+ return Time.utc(current_time.year - period_number, current_time.month, current_time.day, current_time.hour, current_time.min, current_time.sec)
252
258
  else
253
259
  # TODO throw error
254
260
  end
@@ -330,7 +336,7 @@ class GitHubIssueStats
330
336
  #
331
337
  # Generates tables for collected statistics, for easy copy-pasting
332
338
  #
333
- def generate_tables(stats, options)
339
+ def generate_history_tables(stats, options)
334
340
  def get_headers(labels, scope, output_format)
335
341
  if output_format == "markdown"
336
342
  return labels.map do |label|
@@ -417,7 +423,170 @@ class GitHubIssueStats
417
423
  data << [get_period_name(slice, options[:interval_length], index, options[:output_format])] + get_period_stats(slice, options[:labels], scope, options[:output_format])
418
424
  end
419
425
 
420
- tables[scope] = options[:output_format] == "markdown" ? data.to_markdown_table : data.to_table(:first_row_is_head => true).to_s
426
+ tables[scope] = options[:output_format] == "markdown" ? generate_markdown_table_string(data) : generate_text_table_string(data)
427
+ end
428
+
429
+ return tables
430
+ end
431
+
432
+ def generate_text_table_string(data)
433
+ return data.to_table(:first_row_is_head => true).to_s
434
+ end
435
+
436
+ def generate_markdown_table_string(data)
437
+ data.to_markdown_table
438
+ end
439
+
440
+ def get_breakdown_statistics(options)
441
+ stats = []
442
+ current_timestamp = Time.now.utc
443
+ for interval in options[:intervals]
444
+ stats << get_breakdown_stats_for_interval(interval, current_timestamp, stats[-1], options)
445
+ end
446
+
447
+ return stats
448
+ end
449
+
450
+ def get_breakdown_stats_for_interval(interval, current_timestamp, previous_slice, options)
451
+ slice = {}
452
+
453
+ # set timestamps
454
+
455
+ if previous_slice.nil? # initial
456
+ slice[:current_timestamp] = current_timestamp
457
+ else # not initial
458
+ slice[:current_timestamp] = previous_slice[:previous_timestamp]
459
+ end
460
+
461
+ slice[:previous_timestamp] = compute_previous_time(current_timestamp, interval)
462
+
463
+ for scope in options[:scopes]
464
+ scope_stats = {}
465
+ slice[scope] = scope_stats
466
+
467
+ for label in options[:labels]
468
+ label_stats = {}
469
+ scope_stats[label] = label_stats
470
+
471
+ # number of open issues in period
472
+
473
+ search_options = {
474
+ :scope => scope,
475
+ :label => label,
476
+ :state => "open",
477
+ :created_at => {
478
+ :from => slice[:previous_timestamp],
479
+ :until => slice[:current_timestamp]
480
+ }
481
+ }
482
+
483
+ query_string = get_search_query_string(search_options)
484
+
485
+ label_stats[:interval_still_open_total_url] = get_search_url(query_string)
486
+ label_stats[:interval_still_open_total] = get_search_total_results(query_string)
487
+
488
+ @logger.debug "Computed total for interval: #{label_stats[:interval_still_open_total]}"
489
+ end
490
+ end
491
+
492
+ return slice
493
+ end
494
+
495
+ def generate_breakdown_tables(stats, options)
496
+ def get_headers(labels, scope, output_format)
497
+ if output_format == "markdown"
498
+ return labels.map do |label|
499
+ query_string = get_search_query_string({:scope => scope, :label => label, :state => "open"})
500
+ "[#{label}](#{get_search_url(query_string)})"
501
+ end
502
+ else
503
+ return labels
504
+ end
505
+ end
506
+
507
+ def get_interval_humanized_name(interval)
508
+ period_number, period_type = [interval[0..-2], interval[-1]]
509
+ period_number = Integer(period_number)
510
+
511
+ names = {
512
+ "h" => ["hour", "hours"],
513
+ "d" => ["day", "days"],
514
+ "w" => ["week", "weeks"],
515
+ "m" => ["month", "months"],
516
+ "y" => ["year", "years"]
517
+ }
518
+
519
+ if period_number == 1
520
+ return "#{period_number} #{names[period_type][0]}"
521
+ else
522
+ return "#{period_number} #{names[period_type][1]}"
523
+ end
524
+ end
525
+
526
+ def get_period_date(timestamp, period_type)
527
+ if period_type == "h"
528
+ return timestamp.strftime "%Y-%m-%d %H:00"
529
+ elsif period_type == "d"
530
+ return timestamp.strftime "%Y-%m-%d"
531
+ elsif period_type == "w"
532
+ return timestamp.strftime "%Y-%m-%d"
533
+ elsif period_type == "m"
534
+ return timestamp.strftime "%Y-%m"
535
+ elsif period_type == "y"
536
+ return timestamp.strftime "%Y"
537
+ else
538
+ # TODO throw error
539
+ end
540
+ end
541
+
542
+ def get_smallest_period_type(interval_types)
543
+ intervals = ["h", "d", "w", "m", "y"]
544
+ interval_types_indexes = interval_types.map { |interval_type| intervals.index(interval_type) }
545
+ interval_types_indexes << 1
546
+
547
+ return intervals[interval_types_indexes.min]
548
+ end
549
+
550
+ def get_period_name(slice, intervals, index, type)
551
+ current_interval = intervals[index]
552
+ current_period_number, current_period_type = [current_interval[0..-2], current_interval[-1]]
553
+ current_period_number = Integer(current_period_number)
554
+
555
+ if index == 0
556
+ smaller_period_type = get_smallest_period_type([current_period_type])
557
+
558
+ return "< #{get_interval_humanized_name(current_interval)} (#{get_period_date(slice[:previous_timestamp], smaller_period_type)})"
559
+ else
560
+ previous_interval = intervals[index-1]
561
+ previous_period_number, previous_period_type = [previous_interval[0..-2], previous_interval[-1]]
562
+ previous_period_number = Integer(previous_period_number)
563
+ smaller_period_type = get_smallest_period_type([current_period_type, previous_period_type])
564
+
565
+ return "> #{get_interval_humanized_name(previous_interval)}, < #{get_interval_humanized_name(current_interval)} (#{get_period_date(slice[:previous_timestamp], smaller_period_type)})"
566
+ end
567
+ end
568
+
569
+ def get_period_stats(slice, labels, scope, type)
570
+ return labels.map do |label|
571
+ if type == "markdown"
572
+ "[#{slice[scope][label][:interval_still_open_total]}](#{slice[scope][label][:interval_still_open_total_url]})"
573
+ else
574
+ "#{slice[scope][label][:interval_still_open_total]}"
575
+ end
576
+ end
577
+ end
578
+
579
+ tables = {}
580
+
581
+ for scope in options[:scopes]
582
+ data = []
583
+
584
+ data << ["period"] + get_headers(options[:labels], scope, options[:output_format])
585
+ stats.each_with_index do |slice, index|
586
+ data << [get_period_name(slice, options[:intervals], index, options[:output_format])] + get_period_stats(slice, options[:labels], scope, options[:output_format])
587
+ end
588
+
589
+ tables[scope] = options[:output_format] == "markdown" ? generate_markdown_table_string(data) : generate_text_table_string(data)
421
590
  end
422
591
 
423
592
  return tables
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: github_issue_stats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Zuzak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-08 00:00:00.000000000 Z
11
+ date: 2015-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: commander