-
Notifications
You must be signed in to change notification settings - Fork 0
/
stats.rb
executable file
·240 lines (184 loc) · 6.5 KB
/
stats.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "colored2"
gem "faraday-retry"
gem "octokit"
gem "tty-prompt"
end
require "date"
require "optparse"
class GitHubStats
ORG_NAME = "discourse"
MAIN_REPO = "discourse/discourse"
INCLUDED_FORKS = %w[discourse-akismet discourse-signatures discourse-sitemap]
IGNORED_USERNAMES = %w{dependabot[bot] discourse-translator-bot github-actions[bot]}
STAFF_UNTIL = {
"riking" => "2021-07-14",
"eviltrout" => "2022-04-11",
"udan11" => "2022-04-11",
"markvanlan" => "2022-04-25",
"justindirose" => "2022-05-17",
"hnb-ku" => "2022-09-15",
"scossar" => "2022-09-15",
"frank3manuel" => "2022-11-02",
}
def initialize(options)
token =
if options[:token]
options[:token]
else
prompt = TTY::Prompt.new
prompt.say (<<~TIP).green
You need a GitHub Personal Access Token with 'read:org' scope to run this script.
You can create a classic token at https://github.com/settings/tokens/new
TIP
prompt.mask("GitHub Personal Access Token:")
end
@client = Octokit::Client.new(access_token: token)
@verbose = options[:verbose]
@start_tag_name = options[:start_tag]
@end_tag_name = options[:end_tag]
STAFF_UNTIL.transform_values! { |value| DateTime.parse(value).to_time }
end
def calculate
puts "Calculating start and end date..."
start_date, end_date = find_start_end_dates
puts "Counting contributions between #{start_date.iso8601} and #{end_date.iso8601}"
puts "Reading org members..."
org_members = member_names
contributors = find_contributors(start_date, end_date, org_members)
puts "\n\nContributors (#{contributors.length}):"
contributors.each { |name, contributor| puts format_contributor(name, contributor) }
rescue StandardError => e
STDERR.puts e.message
exit(1)
end
private
def find_start_end_dates
start_tag = end_tag = nil
tags = @client.tags(MAIN_REPO)
last_response = @client.last_response
loop do
start_tag = tags.find { |tag| tag.name == @start_tag_name } if start_tag.nil?
end_tag = tags.find { |tag| tag.name == @end_tag_name } if @end_tag_name && end_tag.nil?
break if (start_tag && (!@end_tag_name || end_tag)) || last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
tags = last_response.data
end
raise "Could not find start tag #{@start_tag_name}" if start_tag.nil?
raise "Could not find end tag #{@end_tag_name}" if @end_tag_name && end_tag.nil?
start_date = commit_date(start_tag)
end_date = @end_tag_name ? commit_date(end_tag) : Time.now.utc
[start_date, end_date]
end
def commit_date(tag)
commit = tag.commit.rels[:self].get.data
commit.commit.committer.date
end
def repositories(start_date)
repositories = @client.org_repositories(ORG_NAME, type: :public)
last_response = @client.last_response
while last_response.rels[:next]
last_response = last_response.rels[:next].get
repositories.concat(last_response.data)
end
repositories.select { |repo| repo.pushed_at >= start_date }
end
def member_names
members = @client.org_members(ORG_NAME)
last_response = @client.last_response
while last_response.rels[:next]
last_response = last_response.rels[:next].get
members.concat(last_response.data)
end
members.map { |m| m.login }.to_set
end
def find_contributors(start_date, end_date, org_members)
contributors = {}
ignored_repositories = []
repositories(start_date).each do |repo|
if repo.fork && !INCLUDED_FORKS.include?(repo.name)
ignored_repositories << repo.name
else
puts "Reading commits for #{repo.full_name}..."
add_contributors(contributors, repo.full_name, start_date, end_date)
end
end
puts "", "Ignored repositories: ", ignored_repositories unless ignored_repositories.empty?
contributors
.reject { |name| org_members.include?(name) }
.sort_by { |name, contributions| [contributions[:count], name] }
.reverse
end
def add_contributors(contributors, repo, start_date, end_date)
commits = @client.commits_between(repo, start_date, end_date, per_page: 100)
last_response = @client.last_response
loop do
commits.each do |commit|
author = commit.author&.login || commit.commit.author.name
next if IGNORED_USERNAMES.include?(author)
next if was_staff_at_time_of_commit?(author, commit)
if contributors.has_key?(author)
contributors[author][:count] += 1
else
contributors[author] = { count: 1, url: commit.author&.html_url, repos: {} }
end
if contributors[author][:repos].has_key?(repo)
contributors[author][:repos][repo] += 1
else
contributors[author][:repos][repo] = 1
end
end
break if last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
commits = last_response.data
end
end
def was_staff_at_time_of_commit?(author, commit)
STAFF_UNTIL.has_key?(author) && commit.commit.author.date < STAFF_UNTIL[author]
end
def format_contributor(name, contributor)
count = contributor[:count].to_s.rjust(3, " ")
url = contributor[:url]
text = url ? "#{count} [#{name}](#{url})" : "#{count} #{name}"
if @verbose
repos = contributor[:repos].sort_by { |_, count| count }.reverse
text << " (#{repos.inspect})"
end
text
end
end
def parse_options
options = {}
parser =
OptionParser.new do |opts|
opts.banner =
"Usage: #{File.basename($0)} --start-tag TAG [--end-tag TAG] [--verbose] [--token TOKEN]"
opts.on(
"-s TAG",
"--start-tag TAG",
"The git tag used to calculate the start date",
) { |value| options[:start_tag] = value }
opts.on("-e TAG", "--end-tag TAG", "The git tag used to calculate the end date") do |value|
options[:end_tag] = value
end
opts.on("-v", "--verbose", "Run verbosely") { |value| options[:verbose] = value }
opts.on("-t TOKEN", "--token TOKEN", "GitHub Personal Access Token") do |value|
options[:token] = value
end
opts.on("-h", "--help", "Print usage") do
puts opts
exit
end
end
parser.parse!
unless options.keys.include?(:start_tag)
puts parser.help
exit 1
end
options
end
GitHubStats.new(parse_options).calculate