github-stats 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -6
- data/bin/github-stats +1 -1
- data/lib/github_stats.rb +2 -0
- data/lib/github_stats/cli.rb +9 -6
- data/lib/github_stats/closed_by_week_report.rb +17 -56
- data/lib/github_stats/created_by_week_report.rb +29 -0
- data/lib/github_stats/cycle_time_report.rb +36 -0
- data/lib/github_stats/github_client.rb +2 -23
- data/lib/github_stats/issue_ingester.rb +21 -7
- data/lib/github_stats/report.rb +34 -0
- data/lib/github_stats/reports.rb +25 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab422a50b60641cae6051a695a86876b5e5ad707
|
4
|
+
data.tar.gz: 43dfaa3d02fe256f39641d7f132afd9879b724c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c43a0a82b19ca8e1db9301965e89c0577da86e1253314da443873e0ab95fc689cb289833ba1cb99ad1052ac532b2bce397fbb04f908f8c051c7c1a2fe1dd54a9
|
7
|
+
data.tar.gz: 83a13c2de52cfa8b2a56af2f0db89057a39047d25ef2d1ee9a7ebe6f034403acac89bb158df4c3c261d958c222815cce760185571f56b48f35c4f7121b7a3749
|
data/README.md
CHANGED
@@ -1,22 +1,23 @@
|
|
1
|
-
# Github Issue Stats
|
1
|
+
# Github Issue Stats [![Gem Version](https://badge.fury.io/rb/github-stats.svg)](https://badge.fury.io/rb/github-stats)
|
2
|
+
|
2
3
|
|
3
4
|
Github issues are a decent way to track work on a small project; and with the rise of tools such as Waffle.io and ZenHub, it appears that they will slowly but surely become better and better for managing long term projects.
|
4
5
|
|
5
|
-
However statistical analysis of github issues is still very much lacking. `github-
|
6
|
+
However statistical analysis of github issues is still very much lacking. `github-stats` is a command line tool / gem that takes github searches and converts them into useful project reports.
|
6
7
|
|
7
8
|
## Installation & Usage
|
8
|
-
Assuming you are using Ruby 1.9.3 or above: `gem install github-
|
9
|
+
Assuming you are using Ruby 1.9.3 or above: `gem install github-stats` will install the tool. Running `github-stats "whatever github search string you want"` will run the search, outputting a closed by week report.
|
9
10
|
|
10
|
-
For a detailed list of options and command line flags, please refer to `github-
|
11
|
+
For a detailed list of options and command line flags, please refer to `github-stats --help`.
|
11
12
|
|
12
13
|
**Please Note!** Github's Search API restricts searches to [the first 1,000 results](https://developer.github.com/v3/search/#about-the-search-api) and limits [unauthenticated requests to 10 per minute](https://developer.github.com/v3/search/#rate-limit). This means you can run 1 report per-minute that would return a full 1,000 issues.
|
13
14
|
|
14
|
-
I recommend using filters such as [`state:closed`](https://help.github.com/articles/searching-issues/#search-based-on-whether-
|
15
|
+
I recommend using filters such as [`state:closed`](https://help.github.com/articles/searching-issues/#search-based-on-whether-anor-pull-request-is-open) and/or [`updated:>=2016-01-01`](https://help.github.com/articles/searching-issues/#search-based-on-when-anor-pull-request-was-created-or-last-updated) to scope your requests down, based upon the report type.
|
15
16
|
|
16
17
|
### Example
|
17
18
|
The following example shows how to get a closed by week report for Rails for the first 4 weeks of May 2016
|
18
19
|
```
|
19
|
-
$ github-
|
20
|
+
$ github-stats "repo:rails/rails type:issue is:closed closed:2016-05-01..2016-05-28"
|
20
21
|
2016-17 3 1
|
21
22
|
2016-18 27 10
|
22
23
|
2016-19 30 20
|
data/bin/github-stats
CHANGED
@@ -28,7 +28,7 @@ OptionParser.new do |opts|
|
|
28
28
|
options[:ingest] = ingest
|
29
29
|
end
|
30
30
|
|
31
|
-
opts.on('-r REPORT_TYPE', '--report REPORT_TYPE', '
|
31
|
+
opts.on('-r REPORT_TYPE', '--report REPORT_TYPE', 'Default is closed_by_week; may be cycle_time, closed_by_week, or created_by_week') do |report_type|
|
32
32
|
options[:report_type] = report_type
|
33
33
|
end
|
34
34
|
end.parse!
|
data/lib/github_stats.rb
CHANGED
@@ -3,6 +3,8 @@ require 'sequel'
|
|
3
3
|
|
4
4
|
require_relative 'github_stats/cli'
|
5
5
|
require_relative 'github_stats/database'
|
6
|
+
require_relative 'github_stats/reports'
|
7
|
+
require_relative 'github_stats/created_by_week_report'
|
6
8
|
require_relative 'github_stats/closed_by_week_report'
|
7
9
|
require_relative 'github_stats/issue_ingester'
|
8
10
|
|
data/lib/github_stats/cli.rb
CHANGED
@@ -19,7 +19,7 @@ module GithubStats
|
|
19
19
|
def run
|
20
20
|
setup_db
|
21
21
|
ingest
|
22
|
-
|
22
|
+
results
|
23
23
|
end
|
24
24
|
|
25
25
|
private def setup_db
|
@@ -38,13 +38,16 @@ module GithubStats
|
|
38
38
|
end
|
39
39
|
|
40
40
|
private def report
|
41
|
-
|
42
|
-
|
41
|
+
Reports.for(options[:report_type]).new(search_string, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
private def results
|
45
|
+
CommaSeperatedLinePerResultResultsView.new(report.results)
|
43
46
|
end
|
44
47
|
|
45
48
|
# Transforms a result set into a space-seperated table the results hash
|
46
49
|
# keys becoming the table headers and line breaks between rows.
|
47
|
-
class
|
50
|
+
class CommaSeperatedLinePerResultResultsView
|
48
51
|
attr_accessor :results
|
49
52
|
def initialize(results)
|
50
53
|
self.results = results
|
@@ -55,8 +58,8 @@ module GithubStats
|
|
55
58
|
end
|
56
59
|
|
57
60
|
def to_s
|
58
|
-
fields.join('
|
59
|
-
fields.map(&result.method(:fetch)).join('
|
61
|
+
fields.join(',') + "\n" + results.map do |result|
|
62
|
+
fields.map(&result.method(:fetch)).join(',')
|
60
63
|
end.join("\n")
|
61
64
|
end
|
62
65
|
end
|
@@ -1,65 +1,26 @@
|
|
1
1
|
require_relative 'database'
|
2
|
+
require_relative 'report'
|
2
3
|
module GithubStats
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
results = with_velocity(with_week_closed(with_qty(issues)))
|
15
|
-
Results.new(results)
|
16
|
-
end
|
17
|
-
|
18
|
-
private def with_velocity(issues)
|
19
|
-
closed_two_weeks_ago = 0
|
20
|
-
closed_last_week = 0
|
21
|
-
issues.map do |issue|
|
22
|
-
issue[:velocity] = average(closed_two_weeks_ago, closed_last_week,
|
23
|
-
issue[:qty])
|
24
|
-
closed_two_weeks_ago = closed_last_week
|
25
|
-
closed_last_week = issue[:qty]
|
26
|
-
issue
|
4
|
+
module Reports
|
5
|
+
# Provides week-by-week breakdown of issues closed, grouped by week
|
6
|
+
# with a 3 week moving average.
|
7
|
+
class ClosedByWeekReport
|
8
|
+
attr_accessor :search_string, :options
|
9
|
+
include Report
|
10
|
+
|
11
|
+
def results
|
12
|
+
results = with_moving_average(:velocity,
|
13
|
+
by_week_closed(with_qty(issues)))
|
14
|
+
Reports::Results.new(results, keys: [:week_closed, :qty, :velocity])
|
27
15
|
end
|
28
|
-
end
|
29
|
-
|
30
|
-
private def average(*numbers)
|
31
|
-
numbers.reduce(:+) / numbers.length
|
32
|
-
end
|
33
|
-
|
34
|
-
private def with_qty(dataset)
|
35
|
-
dataset.select { count(:id).as qty }
|
36
|
-
end
|
37
|
-
|
38
|
-
private def with_week_closed(dataset)
|
39
|
-
dataset.select_append { strftime('%Y-%W', closed_at).as(week_closed) }
|
40
|
-
.group_by(:week_closed)
|
41
|
-
end
|
42
|
-
|
43
|
-
private def issues
|
44
|
-
db.issues.where(search_string: search_string).where { closed_at !~ nil }
|
45
|
-
end
|
46
|
-
|
47
|
-
private def db
|
48
|
-
@db ||= Database.new(options)
|
49
|
-
end
|
50
|
-
|
51
|
-
# Provides enumerable access to the results
|
52
|
-
class Results
|
53
|
-
attr_accessor :data
|
54
|
-
extend Forwardable
|
55
|
-
def_delegators :data, :each, :map
|
56
16
|
|
57
|
-
def
|
58
|
-
|
17
|
+
private def by_week_closed(dataset)
|
18
|
+
dataset.select_append { strftime('%Y-%W', closed_at).as(week_closed) }
|
19
|
+
.group_by(:week_closed)
|
59
20
|
end
|
60
21
|
|
61
|
-
def
|
62
|
-
|
22
|
+
private def issues
|
23
|
+
db.issues.where(search_string: search_string).where { closed_at !~ nil }
|
63
24
|
end
|
64
25
|
end
|
65
26
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'database'
|
2
|
+
require_relative 'report'
|
3
|
+
module GithubStats
|
4
|
+
module Reports
|
5
|
+
# Provides week-by-week breakdown of issues created, grouped by week
|
6
|
+
# with a 3 week moving average.
|
7
|
+
class CreatedByWeekReport
|
8
|
+
attr_accessor :search_string, :options
|
9
|
+
|
10
|
+
include Report
|
11
|
+
|
12
|
+
def results
|
13
|
+
results = with_moving_average(:add_rate,
|
14
|
+
by_week_created(with_qty(issues)))
|
15
|
+
Reports::Results.new(results, keys: [:week_created, :qty, :add_rate])
|
16
|
+
end
|
17
|
+
|
18
|
+
private def by_week_created(dataset)
|
19
|
+
dataset.select_append { strftime('%Y-%W', created_at).as(week_created) }
|
20
|
+
.group_by(:week_created)
|
21
|
+
end
|
22
|
+
|
23
|
+
private def issues
|
24
|
+
db.issues.where(search_string: search_string)
|
25
|
+
.where { created_at !~ nil }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'database'
|
2
|
+
require_relative 'report'
|
3
|
+
module GithubStats
|
4
|
+
module Reports
|
5
|
+
# Provides issue cycle time from started at to done
|
6
|
+
class CycleTimeReport
|
7
|
+
attr_accessor :search_string, :options
|
8
|
+
include Report
|
9
|
+
|
10
|
+
def results
|
11
|
+
Results.new(with_cycle_time(issues), keys: [:week_closed, :created_at, :started_at, :closed_at, :cycle_time, :url])
|
12
|
+
end
|
13
|
+
|
14
|
+
private def with_cycle_time(dataset)
|
15
|
+
dataset.all.map do |item|
|
16
|
+
item[:week_closed] = (item[:closed_at].strftime("%Y-%W"))
|
17
|
+
item[:cycle_time] = work_days_between(item[:closed_at], item[:started_at])
|
18
|
+
item
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private def work_days_between(end_time, start_time)
|
23
|
+
hours_between = (end_time - start_time) / 60 / 60
|
24
|
+
return hours_between.to_f / 24.to_f if hours_between < 12
|
25
|
+
days_between = (end_time.strftime('%j').to_i - start_time.strftime('%j').to_i)
|
26
|
+
weeks_between = (end_time.strftime('%W').to_i - start_time.strftime('%W').to_i)
|
27
|
+
return (hours_between - (days_between * 10 + weeks_between * 28)).to_f / 24.to_f
|
28
|
+
end
|
29
|
+
|
30
|
+
private def issues
|
31
|
+
db.issues.where(search_string: search_string).where { closed_at !~ nil }
|
32
|
+
.where { started_at !~ nil }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -6,33 +6,12 @@ module GithubStats
|
|
6
6
|
|
7
7
|
def initialize(options)
|
8
8
|
self.octokit = Octokit::Client.new(access_token:
|
9
|
-
options[:github_access_token]
|
9
|
+
options[:github_access_token],
|
10
|
+
auto_paginate: true)
|
10
11
|
end
|
11
12
|
|
12
13
|
def search_issues(query, options = { per_page: 100 })
|
13
14
|
octokit.search_issues(query, options)
|
14
|
-
Response.new(self)
|
15
|
-
end
|
16
|
-
|
17
|
-
def last_response
|
18
|
-
octokit.last_response
|
19
|
-
end
|
20
|
-
|
21
|
-
# Auto-paginate!
|
22
|
-
class Response
|
23
|
-
attr_reader :client
|
24
|
-
def initialize(client)
|
25
|
-
@client = client
|
26
|
-
end
|
27
|
-
|
28
|
-
def each(&block)
|
29
|
-
last_response = client.last_response
|
30
|
-
loop do
|
31
|
-
last_response.data.items.each(&block)
|
32
|
-
last_response = last_response.rels[:next].get
|
33
|
-
break if last_response.rels[:next].nil?
|
34
|
-
end
|
35
|
-
end
|
36
15
|
end
|
37
16
|
end
|
38
17
|
end
|
@@ -11,25 +11,39 @@ module GithubStats
|
|
11
11
|
|
12
12
|
def ingest
|
13
13
|
return unless options[:ingest]
|
14
|
-
github.search_issues(search_string).each(&method(:insert_or_update))
|
14
|
+
github.search_issues(search_string).items.each(&method(:insert_or_update))
|
15
15
|
end
|
16
16
|
|
17
17
|
private def insert_or_update(result)
|
18
18
|
issue = issues.first(github_id: result[:id])
|
19
|
-
|
20
|
-
return
|
21
|
-
|
19
|
+
started_at = started_at(result)
|
20
|
+
return insert(result, started_at: started_at) unless issue
|
21
|
+
return update(issue, result, started_at: started_at) if issue[:closed_at] != result[:closed_at] ||
|
22
|
+
issue[:created_at] != result[:created_at] ||
|
23
|
+
issue[:started_at] != started_at
|
22
24
|
end
|
23
25
|
|
24
|
-
private def
|
26
|
+
private def started_at(result)
|
27
|
+
starting_events = result.rels[:events].get.data.select do |event|
|
28
|
+
event[:event] == 'labeled' && (event[:label] || {})[:name] == 'in-progress'
|
29
|
+
end.sort_by do |event|
|
30
|
+
event[:created_at]
|
31
|
+
end
|
32
|
+
starting_event = starting_events.last
|
33
|
+
starting_event.nil? ? nil : starting_event[:created_at]
|
34
|
+
end
|
35
|
+
|
36
|
+
private def update(issue, result, started_at:)
|
25
37
|
issue.update(closed_at: result[:closed_at],
|
26
|
-
created_at: result[:created_at]
|
38
|
+
created_at: result[:created_at],
|
39
|
+
started_at: started_at)
|
27
40
|
end
|
28
41
|
|
29
|
-
private def insert(result)
|
42
|
+
private def insert(result, started_at:)
|
30
43
|
issues.insert(search_string: search_string, github_id: result[:id],
|
31
44
|
closed_at: result[:closed_at],
|
32
45
|
created_at: result[:created_at],
|
46
|
+
started_at: started_at,
|
33
47
|
url: result[:url])
|
34
48
|
end
|
35
49
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module GithubStats
|
2
|
+
module Reports
|
3
|
+
module Report
|
4
|
+
def initialize(search_string, options)
|
5
|
+
self.search_string = search_string
|
6
|
+
self.options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
private def with_moving_average(label, weeks)
|
10
|
+
closed_two_weeks_ago = 0
|
11
|
+
closed_last_week = 0
|
12
|
+
weeks.map do |week|
|
13
|
+
week[label] = average(closed_two_weeks_ago, closed_last_week,
|
14
|
+
week[:qty])
|
15
|
+
closed_two_weeks_ago = closed_last_week
|
16
|
+
closed_last_week = week[:qty]
|
17
|
+
week
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private def average(*numbers)
|
22
|
+
numbers.reduce(:+) / numbers.length
|
23
|
+
end
|
24
|
+
|
25
|
+
private def with_qty(dataset)
|
26
|
+
dataset.select { count(:id).as qty }
|
27
|
+
end
|
28
|
+
|
29
|
+
private def db
|
30
|
+
@db ||= Database.new(options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'closed_by_week_report'
|
2
|
+
require_relative 'cycle_time_report'
|
3
|
+
require_relative 'created_by_week_report'
|
4
|
+
|
5
|
+
module GithubStats
|
6
|
+
module Reports
|
7
|
+
def self.for(report_type)
|
8
|
+
{ 'closed_by_week' => ClosedByWeekReport,
|
9
|
+
'created_by_week' => CreatedByWeekReport,
|
10
|
+
'cycle_time' => CycleTimeReport }.fetch(report_type)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Provides enumerable access to the results
|
14
|
+
class Results
|
15
|
+
attr_accessor :data, :keys
|
16
|
+
extend Forwardable
|
17
|
+
def_delegators :data, :each, :map
|
18
|
+
|
19
|
+
def initialize(data, keys: [])
|
20
|
+
self.data = data
|
21
|
+
self.keys = keys
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: github-stats
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zee@Zinc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-11-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: octokit
|
@@ -69,9 +69,13 @@ files:
|
|
69
69
|
- lib/github_stats.rb
|
70
70
|
- lib/github_stats/cli.rb
|
71
71
|
- lib/github_stats/closed_by_week_report.rb
|
72
|
+
- lib/github_stats/created_by_week_report.rb
|
73
|
+
- lib/github_stats/cycle_time_report.rb
|
72
74
|
- lib/github_stats/database.rb
|
73
75
|
- lib/github_stats/github_client.rb
|
74
76
|
- lib/github_stats/issue_ingester.rb
|
77
|
+
- lib/github_stats/report.rb
|
78
|
+
- lib/github_stats/reports.rb
|
75
79
|
homepage: https://github.com/zincmade/github-stats
|
76
80
|
licenses:
|
77
81
|
- MIT
|