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 +4 -4
- data/bin/github_issue_stats +47 -2
- data/lib/github_issue_stats.rb +182 -13
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af02c65f57d4da09083fcce26bc1f12fb88aec31
|
4
|
+
data.tar.gz: 45d24e5d5e66c55e9d67a044d6c2e7f7a54fd6d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bae404551ef009a2071c21ced6a293064607a8fba412e5be77dde329de90bdac5cf68b6308ab4190de95c9a4c3358ebfa1a89bfbbdee9f917cf1d7f3ad3234c
|
7
|
+
data.tar.gz: 3f3a9e7d886844979bbde704872cf1582b3c72364ac1f4b17071a32c07ba7299826d15839f6bdaaee9c1c6050cbee712526b2cad94e2dca83fdc60b4f543b682
|
data/bin/github_issue_stats
CHANGED
@@ -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.
|
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}"
|
data/lib/github_issue_stats.rb
CHANGED
@@ -30,7 +30,7 @@ module Enumerable
|
|
30
30
|
end
|
31
31
|
|
32
32
|
class GitHubIssueStats
|
33
|
-
VERSION = "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.
|
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.
|
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.
|
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.
|
225
|
+
return Time.utc(current_time.year, current_time.month, 1, 0, 0, 0)
|
226
226
|
elsif period_type == "y"
|
227
|
-
return Time.
|
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
|
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
|
-
|
249
|
-
|
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.
|
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
|
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
|
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.
|
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-
|
11
|
+
date: 2015-11-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: commander
|