github_bus_factor 0.1.4 → 0.1.6
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/github_bus_factor.gemspec +6 -6
- data/lib/github_bus_factor/version.rb +1 -1
- data/lib/github_bus_factor.rb +200 -204
- metadata +1 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae9a67035be44381b94300dfc621e8cb5a3f34d2
|
4
|
+
data.tar.gz: d0d162e0cc74061d6a987ae1e92182f9345d2635
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d347a0d09033802ac2248100d6dfbf81a3864caa32d09f87cd99a6a61b5296f3b08f67f6352764e8ff7a85aa7c616d2e18170526f1d94c84ff055a129741c74
|
7
|
+
data.tar.gz: e2f020b138a21f9f781b2114477e2dbdea82a72ce8ff792bae59d53c9f1cc871eecf35c02a84940d6931d9dba6f6dec8f8ecd0b137e73dfd2401f11ebe81b681
|
data/github_bus_factor.gemspec
CHANGED
@@ -29,10 +29,10 @@ Gem::Specification.new do |spec|
|
|
29
29
|
|
30
30
|
spec.add_development_dependency "bundler", "~> 1.10"
|
31
31
|
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
-
spec.
|
33
|
-
spec.
|
34
|
-
spec.
|
35
|
-
spec.
|
36
|
-
spec.
|
37
|
-
spec.
|
32
|
+
spec.add_dependency 'octokit', '~> 4.2'
|
33
|
+
spec.add_dependency 'commander', '~> 4.3'
|
34
|
+
spec.add_dependency 'security', '~> 0.1.3'
|
35
|
+
spec.add_dependency 'terminal-table', '~> 1.5'
|
36
|
+
spec.add_dependency 'faraday-http-cache', '~> 1.2'
|
37
|
+
spec.add_dependency 'activesupport', '~> 4.2'
|
38
38
|
end
|
data/lib/github_bus_factor.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require 'github_bus_factor/version'
|
2
2
|
require 'rubygems'
|
3
3
|
require 'commander/import'
|
4
4
|
require 'octokit'
|
@@ -10,221 +10,217 @@ require 'active_support/core_ext/numeric/time'
|
|
10
10
|
include ActionView::Helpers::DateHelper
|
11
11
|
|
12
12
|
|
13
|
+
KEYCHAIN_SERVICE = 'github_bus_factor'
|
14
|
+
API_CALL_RETRY_COUNT = 3
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
program :version, GitHubBusFactor::VERSION
|
17
|
+
program :description, 'More than just stars'
|
18
|
+
default_command :fetch
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
return if !block.call(count).nil?
|
27
|
-
puts "Waiting for GitHub cache. Will retry in 3 seconds…"
|
28
|
-
sleep(3)
|
29
|
-
helper(count - 1, block)
|
30
|
-
end
|
20
|
+
# There must be a better way to deal with GitHub caching…
|
21
|
+
def helper(count, block)
|
22
|
+
return unless count > 0
|
23
|
+
return if !block.call(count).nil?
|
24
|
+
puts "Waiting for GitHub cache. Will retry in 3 seconds…"
|
25
|
+
sleep(3)
|
26
|
+
helper(count - 1, block)
|
27
|
+
end
|
31
28
|
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
50
|
-
puts table
|
30
|
+
command :about do |c|
|
31
|
+
c.syntax = 'github_bus_factor about'
|
32
|
+
c.summary = 'Explains every line of the report.'
|
33
|
+
c.action do |args, options|
|
34
|
+
table = Terminal::Table.new do |t|
|
35
|
+
t.style = {:padding_left => 1, :padding_right => 2}
|
36
|
+
t.title = 'GitHub Score'
|
37
|
+
t.headings = ['', 'Description']
|
38
|
+
t << ['🍴', 'Forks. Might mean people planning are fixing bugs or adding features.']
|
39
|
+
t << ['🔭', 'Watchers. Shows number of people interested in project changes.']
|
40
|
+
t << ['🌟', 'Stars. Might mean it is a good project or that it was featured in a mailing list. Some people use 🌟 as a "Like".']
|
41
|
+
t << ['🗓', 'Age. Mature projects might mean battle tested project. Recent pushes might mean project is actively maintained.']
|
42
|
+
t << ['🍻', 'Pull Requests. Community contributions to the project. Many closed PRs usually is a good sign, while no PRs usual is bad.']
|
43
|
+
t << ['🛠', 'Refactoring. Balance between added and deleted code. Crude value not including semantic understanding of the code.']
|
44
|
+
t << ['📦', 'Releases. Might mean disciplined maintainer. Certain dependency managers rely on releases to be present.']
|
45
|
+
t << ['🚌', 'Bus factor. Chances of the project to become abandoned once current collaborators stop updating it. The higher - the worse.']
|
51
46
|
end
|
47
|
+
puts table
|
52
48
|
end
|
49
|
+
end
|
53
50
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
51
|
+
command :logout do |c|
|
52
|
+
c.syntax = 'github_bus_factor logout'
|
53
|
+
c.summary = 'Remove GitHub token from your keychain.'
|
54
|
+
c.action do |args, options|
|
55
|
+
Security::GenericPassword.delete(service: KEYCHAIN_SERVICE)
|
60
56
|
end
|
57
|
+
end
|
61
58
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
else
|
84
|
-
token = tokenPassword.password
|
59
|
+
command :fetch do |c|
|
60
|
+
c.syntax = 'github_bus_factor fetch [options]'
|
61
|
+
c.summary = 'Fetches GitHub score for a given owner/repository'
|
62
|
+
c.option '--verbose', 'Add extra logging'
|
63
|
+
c.action do |args, options|
|
64
|
+
# owner / repo
|
65
|
+
throw "Expect owner/repo as an argument" unless args.count == 1
|
66
|
+
matches = args.first.match(/^(.+)\/(.+)$/)
|
67
|
+
throw "Expect owner/repo as an argument" unless !matches.nil?
|
68
|
+
ownerName, repoName = matches.captures
|
69
|
+
throw "Expect owner/repo as an argument" if ownerName.nil? || ownerName.empty? || repoName.nil? || repoName.empty?
|
70
|
+
|
71
|
+
# Token
|
72
|
+
tokenPassword = Security::GenericPassword.find(service: KEYCHAIN_SERVICE)
|
73
|
+
token = nil
|
74
|
+
unless tokenPassword
|
75
|
+
puts "Please create a GitHub access token at https://github.com/settings/tokens"
|
76
|
+
while token.nil? || token.empty? do
|
77
|
+
token = ask("Token: ")
|
78
|
+
Security::GenericPassword.add(KEYCHAIN_SERVICE, '', token) unless token.nil? || token.empty?
|
85
79
|
end
|
80
|
+
else
|
81
|
+
token = tokenPassword.password
|
82
|
+
end
|
86
83
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
84
|
+
# GitHub client
|
85
|
+
client = Octokit::Client.new(:access_token => token)
|
86
|
+
client.auto_paginate = true
|
87
|
+
repository = Octokit::Repository.new(:owner => ownerName, :repo => repoName)
|
91
88
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
end
|
218
|
-
output << ['🚌', bus_factor_value]
|
219
|
-
|
220
|
-
# Output
|
221
|
-
puts("Thank you for you patience 💕\n\n")
|
222
|
-
table = Terminal::Table.new do |t|
|
223
|
-
t.title = "#{ownerName}/#{repoName}"
|
224
|
-
t.style = {:padding_left => 1, :padding_right => 2}
|
225
|
-
t.rows = output.each_with_index
|
226
|
-
end
|
227
|
-
puts table
|
89
|
+
# Cache
|
90
|
+
stack = Faraday::RackBuilder.new do |builder|
|
91
|
+
builder.use Faraday::HttpCache
|
92
|
+
builder.use Octokit::Response::RaiseError
|
93
|
+
builder.adapter Faraday.default_adapter
|
94
|
+
end
|
95
|
+
client.middleware = stack
|
96
|
+
|
97
|
+
# Output
|
98
|
+
output = []
|
99
|
+
|
100
|
+
# Info
|
101
|
+
puts("1/6 Fetching repository info…")
|
102
|
+
repository_info = client.repository(repository)
|
103
|
+
puts(repository_info.inspect) if options.verbose
|
104
|
+
|
105
|
+
# Forks
|
106
|
+
FORKS_THRESHOLD = 5
|
107
|
+
if repository_info.forks_count > FORKS_THRESHOLD
|
108
|
+
forks_value = "#{repository_info.forks_count} forks."
|
109
|
+
else
|
110
|
+
forks_value = "Few forks (#{repository_info.forks_count})."
|
111
|
+
end
|
112
|
+
output << ['🍴', forks_value]
|
113
|
+
|
114
|
+
# Watchers
|
115
|
+
WATCHERS_THRESHOLD = 5
|
116
|
+
if repository_info.subscribers_count > WATCHERS_THRESHOLD
|
117
|
+
watchers_value = "#{repository_info.subscribers_count} watchers."
|
118
|
+
else
|
119
|
+
watchers_value = "Few watchers (#{repository_info.subscribers_count})."
|
120
|
+
end
|
121
|
+
output << ['🔭', watchers_value]
|
122
|
+
|
123
|
+
# Stars
|
124
|
+
STARS_THRESHOLD = 10
|
125
|
+
if repository_info.stargazers_count > STARS_THRESHOLD
|
126
|
+
stars_value = "#{repository_info.stargazers_count} stars."
|
127
|
+
else
|
128
|
+
stars_value = "Few stars (#{repository_info.stargazers_count})."
|
129
|
+
end
|
130
|
+
output << ['🌟', stars_value]
|
131
|
+
|
132
|
+
# Age
|
133
|
+
created_at = repository_info.created_at
|
134
|
+
last_push = repository_info.pushed_at
|
135
|
+
output << ['🗓', "Created #{time_ago_in_words(created_at)} ago; last push #{time_ago_in_words(last_push)} ago."]
|
136
|
+
|
137
|
+
# PRs
|
138
|
+
puts("2/6 Fetching open PRs…")
|
139
|
+
open_PRs = client.pull_requests(repository, {:per_page => 100})
|
140
|
+
puts(open_PRs.inspect) if options.verbose
|
141
|
+
|
142
|
+
puts("3/6 Fetching closed PRs…")
|
143
|
+
closed_PRs = client.pull_requests(repository, {:state => 'closed'})
|
144
|
+
puts(closed_PRs.inspect) if options.verbose
|
145
|
+
|
146
|
+
total_PRs_count = open_PRs.count + closed_PRs.count
|
147
|
+
if total_PRs_count > 0
|
148
|
+
ratio = (Float(closed_PRs.count) / total_PRs_count * 100).round(2)
|
149
|
+
prs_value = "#{total_PRs_count} PRs: #{closed_PRs.count} closed; #{open_PRs.count} opened; #{ratio}% PRs are closed."
|
150
|
+
else
|
151
|
+
prs_value = 'No PRs opened yet for this repository.'
|
152
|
+
end
|
153
|
+
output << ['🍻', prs_value]
|
154
|
+
|
155
|
+
# Refactoring
|
156
|
+
code_frequency = nil
|
157
|
+
helper(API_CALL_RETRY_COUNT, lambda { |c|
|
158
|
+
puts("4/6 Fetching code frequency…")
|
159
|
+
code_frequency = client.code_frequency_stats(repository)
|
160
|
+
})
|
161
|
+
puts(code_frequency.inspect) if options.verbose
|
162
|
+
|
163
|
+
deletions = 0
|
164
|
+
additions = 0
|
165
|
+
refactoring = code_frequency.each { |frequency|
|
166
|
+
additions += frequency[1]
|
167
|
+
deletions += frequency[2]
|
168
|
+
}
|
169
|
+
refactoring = (Float(deletions.abs) / Float(additions) * 100).round(2)
|
170
|
+
REFACTORING_THRESHOLD = 5
|
171
|
+
if refactoring > REFACTORING_THRESHOLD
|
172
|
+
refactoring_value = "Deletions to additions ratio: #{refactoring}% (#{deletions}/#{additions})."
|
173
|
+
else
|
174
|
+
refactoring_value = "Mostly additions, few deletions (#{deletions}/#{additions})."
|
175
|
+
end
|
176
|
+
output << ['🛠', refactoring_value]
|
177
|
+
|
178
|
+
# Releases
|
179
|
+
puts("5/6 Fetching releases…")
|
180
|
+
releases = client.releases(repository)
|
181
|
+
puts(releases.inspect) if options.verbose
|
182
|
+
if !releases.empty?
|
183
|
+
latest_release = releases.first
|
184
|
+
release_name = latest_release.name.nil? || latest_release.name.empty? ? latest_release.tag_name : latest_release.name
|
185
|
+
releases_value = "#{releases.count} releases; latest release \"#{release_name}\": #{time_ago_in_words(latest_release.published_at)}."
|
186
|
+
else
|
187
|
+
releases_value = 'No releases.'
|
188
|
+
end
|
189
|
+
output << ['📦', releases_value]
|
190
|
+
|
191
|
+
# Bus factor
|
192
|
+
contributions = nil
|
193
|
+
helper(API_CALL_RETRY_COUNT, lambda { |c|
|
194
|
+
puts("6/6 Fetching contribution statistics…")
|
195
|
+
contributions = client.contributors_stats(repository)
|
196
|
+
})
|
197
|
+
puts(contributions.inspect) if options.verbose
|
198
|
+
contributions = contributions.map { |c| c.total }
|
199
|
+
min, max = contributions.minmax
|
200
|
+
delta = max - min
|
201
|
+
CONTRIBUTION_THRESHOLD = 0.7
|
202
|
+
meaningful = contributions.select { |c| (max - c) < delta * CONTRIBUTION_THRESHOLD }
|
203
|
+
total_contributions = contributions.reduce(0) { |t, c| t + c }
|
204
|
+
if meaningful.empty?
|
205
|
+
bus_factor = 100
|
206
|
+
else
|
207
|
+
average = contributions.reduce(0) { |a, c| a + Float(c) / total_contributions }
|
208
|
+
bus_factor = (average / meaningful.count * 100.0).round(2)
|
209
|
+
end
|
210
|
+
if bus_factor > 90
|
211
|
+
bus_factor_value = "Bus factor: #{bus_factor}%. Most likely one core contributor."
|
212
|
+
else
|
213
|
+
bus_factor_value = "Bus factor: #{bus_factor}% (#{meaningful.count} impactful contributors out of #{contributions.count})."
|
228
214
|
end
|
215
|
+
output << ['🚌', bus_factor_value]
|
216
|
+
|
217
|
+
# Output
|
218
|
+
puts("Thank you for you patience 💕\n\n")
|
219
|
+
table = Terminal::Table.new do |t|
|
220
|
+
t.title = "#{ownerName}/#{repoName}"
|
221
|
+
t.style = {:padding_left => 1, :padding_right => 2}
|
222
|
+
t.rows = output.each_with_index
|
223
|
+
end
|
224
|
+
puts table
|
229
225
|
end
|
230
226
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: github_bus_factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sash Zats
|
@@ -59,9 +59,6 @@ dependencies:
|
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '4.3'
|
62
|
-
- - ">="
|
63
|
-
- !ruby/object:Gem::Version
|
64
|
-
version: 4.3.3
|
65
62
|
type: :runtime
|
66
63
|
prerelease: false
|
67
64
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -69,9 +66,6 @@ dependencies:
|
|
69
66
|
- - "~>"
|
70
67
|
- !ruby/object:Gem::Version
|
71
68
|
version: '4.3'
|
72
|
-
- - ">="
|
73
|
-
- !ruby/object:Gem::Version
|
74
|
-
version: 4.3.3
|
75
69
|
- !ruby/object:Gem::Dependency
|
76
70
|
name: security
|
77
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -93,9 +87,6 @@ dependencies:
|
|
93
87
|
- - "~>"
|
94
88
|
- !ruby/object:Gem::Version
|
95
89
|
version: '1.5'
|
96
|
-
- - ">="
|
97
|
-
- !ruby/object:Gem::Version
|
98
|
-
version: 1.5.2
|
99
90
|
type: :runtime
|
100
91
|
prerelease: false
|
101
92
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -103,9 +94,6 @@ dependencies:
|
|
103
94
|
- - "~>"
|
104
95
|
- !ruby/object:Gem::Version
|
105
96
|
version: '1.5'
|
106
|
-
- - ">="
|
107
|
-
- !ruby/object:Gem::Version
|
108
|
-
version: 1.5.2
|
109
97
|
- !ruby/object:Gem::Dependency
|
110
98
|
name: faraday-http-cache
|
111
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -113,9 +101,6 @@ dependencies:
|
|
113
101
|
- - "~>"
|
114
102
|
- !ruby/object:Gem::Version
|
115
103
|
version: '1.2'
|
116
|
-
- - ">="
|
117
|
-
- !ruby/object:Gem::Version
|
118
|
-
version: 1.2.2
|
119
104
|
type: :runtime
|
120
105
|
prerelease: false
|
121
106
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -123,9 +108,6 @@ dependencies:
|
|
123
108
|
- - "~>"
|
124
109
|
- !ruby/object:Gem::Version
|
125
110
|
version: '1.2'
|
126
|
-
- - ">="
|
127
|
-
- !ruby/object:Gem::Version
|
128
|
-
version: 1.2.2
|
129
111
|
- !ruby/object:Gem::Dependency
|
130
112
|
name: activesupport
|
131
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,9 +115,6 @@ dependencies:
|
|
133
115
|
- - "~>"
|
134
116
|
- !ruby/object:Gem::Version
|
135
117
|
version: '4.2'
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: 4.2.5
|
139
118
|
type: :runtime
|
140
119
|
prerelease: false
|
141
120
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -143,9 +122,6 @@ dependencies:
|
|
143
122
|
- - "~>"
|
144
123
|
- !ruby/object:Gem::Version
|
145
124
|
version: '4.2'
|
146
|
-
- - ">="
|
147
|
-
- !ruby/object:Gem::Version
|
148
|
-
version: 4.2.5
|
149
125
|
description: Provides few more parameters to estimate quality of the GitHub project
|
150
126
|
besides stars.
|
151
127
|
email:
|