github-stats 0.1.0 → 0.2.0
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.
- 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 [](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
|