gitrob 0.0.6 → 1.0.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/.gitignore +30 -7
- data/.rspec +0 -1
- data/.rubocop.yml +55 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +42 -0
- data/CONTRIBUTING.md +137 -9
- data/Gemfile +11 -1
- data/Guardfile +42 -0
- data/LICENSE.txt +17 -18
- data/README.md +79 -29
- data/Rakefile +6 -0
- data/bin/console +34 -0
- data/bin/setup +7 -0
- data/db/migrations/001_create_assessments.rb +19 -0
- data/db/migrations/002_create_github_access_tokens.rb +11 -0
- data/db/migrations/003_create_owners.rb +24 -0
- data/db/migrations/004_create_repositories.rb +23 -0
- data/db/migrations/005_create_blobs.rb +16 -0
- data/db/migrations/006_create_flags.rb +13 -0
- data/db/migrations/007_create_comparisons.rb +17 -0
- data/db/migrations/008_create_blobs_comparisons.rb +8 -0
- data/db/migrations/009_create_comparisons_repositories.rb +8 -0
- data/db/migrations/010_create_comparisons_owners.rb +8 -0
- data/exe/gitrob +6 -0
- data/gitrob.gemspec +25 -18
- data/lib/gitrob/blob_observer.rb +103 -0
- data/lib/gitrob/cli/command.rb +58 -0
- data/lib/gitrob/cli/commands/accept_terms_of_use.rb +61 -0
- data/lib/gitrob/cli/commands/analyze/analysis.rb +75 -0
- data/lib/gitrob/cli/commands/analyze/gathering.rb +101 -0
- data/lib/gitrob/cli/commands/analyze.rb +63 -0
- data/lib/gitrob/cli/commands/banner.rb +25 -0
- data/lib/gitrob/cli/commands/configure.rb +123 -0
- data/lib/gitrob/cli/commands/server.rb +21 -0
- data/lib/gitrob/cli/progress_bar.rb +47 -0
- data/lib/gitrob/cli.rb +213 -0
- data/lib/gitrob/github/client_manager.rb +46 -0
- data/lib/gitrob/github/data_manager.rb +121 -0
- data/lib/gitrob/jobs/assessment.rb +12 -0
- data/lib/gitrob/jobs/comparison.rb +55 -0
- data/lib/gitrob/models/assessment.rb +96 -0
- data/lib/gitrob/models/blob.rb +50 -0
- data/lib/gitrob/models/comparison.rb +15 -0
- data/lib/gitrob/models/flag.rb +15 -0
- data/lib/gitrob/models/github_access_token.rb +17 -0
- data/lib/gitrob/models/owner.rb +23 -0
- data/lib/gitrob/models/repository.rb +20 -0
- data/lib/gitrob/utils.rb +19 -0
- data/lib/gitrob/version.rb +1 -1
- data/lib/gitrob/web_app.rb +292 -0
- data/lib/gitrob.rb +30 -113
- data/public/css/bootstrap.min.css +11 -0
- data/public/css/main.css +130 -0
- data/public/css/tomorrow-night.css +75 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +273 -214
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/public/images/blob_spinner.gif +0 -0
- data/public/images/gear_spinner.gif +0 -0
- data/public/js/bootstrap.min.js +7 -0
- data/public/js/highlight.pack.js +2 -0
- data/public/js/highlight.worker.js +13 -0
- data/public/js/jquery-2.1.4.min.js +4 -0
- data/public/js/main.js +239 -0
- data/public/robots.txt +2 -0
- data/signatures.json +541 -0
- data/views/assessments/_assessments.erb +57 -0
- data/views/assessments/_comparable_assessments.erb +38 -0
- data/views/assessments/_comparisons.erb +111 -0
- data/views/assessments/compare.erb +22 -0
- data/views/assessments/findings.erb +55 -0
- data/views/assessments/repositories.erb +35 -0
- data/views/assessments/show.erb +1 -0
- data/views/assessments/users.erb +46 -0
- data/views/blobs/show.erb +37 -0
- data/views/comparisons/show.erb +125 -0
- data/views/errors/internal_server_error.erb +9 -0
- data/views/errors/not_found.erb +5 -0
- data/views/index.erb +43 -28
- data/views/layout.erb +38 -12
- data/views/repositories/show.erb +49 -0
- data/views/users/show.erb +54 -0
- metadata +217 -106
- data/bin/gitrob +0 -260
- data/lib/gitrob/github/blob.rb +0 -41
- data/lib/gitrob/github/http_client.rb +0 -127
- data/lib/gitrob/github/organization.rb +0 -99
- data/lib/gitrob/github/repository.rb +0 -72
- data/lib/gitrob/github/user.rb +0 -84
- data/lib/gitrob/observers/sensitive_files.rb +0 -83
- data/lib/gitrob/progressbar.rb +0 -52
- data/lib/gitrob/util.rb +0 -11
- data/lib/gitrob/webapp.rb +0 -76
- data/models/blob.rb +0 -35
- data/models/finding.rb +0 -14
- data/models/organization.rb +0 -32
- data/models/repo.rb +0 -22
- data/models/user.rb +0 -28
- data/patterns.json +0 -394
- data/public/javascripts/bootstrap.min.js +0 -7
- data/public/javascripts/gitrob.js +0 -75
- data/public/javascripts/jquery-2.1.1.min.js +0 -4
- data/public/javascripts/lang-apollo.js +0 -2
- data/public/javascripts/lang-basic.js +0 -3
- data/public/javascripts/lang-clj.js +0 -18
- data/public/javascripts/lang-css.js +0 -2
- data/public/javascripts/lang-dart.js +0 -3
- data/public/javascripts/lang-erlang.js +0 -2
- data/public/javascripts/lang-go.js +0 -1
- data/public/javascripts/lang-hs.js +0 -2
- data/public/javascripts/lang-lisp.js +0 -3
- data/public/javascripts/lang-llvm.js +0 -1
- data/public/javascripts/lang-lua.js +0 -2
- data/public/javascripts/lang-matlab.js +0 -6
- data/public/javascripts/lang-ml.js +0 -2
- data/public/javascripts/lang-mumps.js +0 -2
- data/public/javascripts/lang-n.js +0 -4
- data/public/javascripts/lang-pascal.js +0 -3
- data/public/javascripts/lang-proto.js +0 -1
- data/public/javascripts/lang-r.js +0 -2
- data/public/javascripts/lang-rd.js +0 -1
- data/public/javascripts/lang-scala.js +0 -2
- data/public/javascripts/lang-sql.js +0 -2
- data/public/javascripts/lang-tcl.js +0 -3
- data/public/javascripts/lang-tex.js +0 -1
- data/public/javascripts/lang-vb.js +0 -2
- data/public/javascripts/lang-vhdl.js +0 -3
- data/public/javascripts/lang-wiki.js +0 -2
- data/public/javascripts/lang-xq.js +0 -3
- data/public/javascripts/lang-yaml.js +0 -2
- data/public/javascripts/prettify.js +0 -30
- data/public/javascripts/run_prettify.js +0 -34
- data/public/stylesheets/bootstrap.min.css +0 -7
- data/public/stylesheets/bootstrap.min.css.vanilla +0 -5
- data/public/stylesheets/gitrob.css +0 -88
- data/public/stylesheets/prettify.css +0 -51
- data/spec/lib/gitrob/observers/sensitive_files_spec.rb +0 -691
- data/spec/spec_helper.rb +0 -127
- data/views/blob.erb +0 -22
- data/views/organization.erb +0 -126
- data/views/repository.erb +0 -51
- data/views/user.erb +0 -51
data/bin/gitrob
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# coding: utf-8
|
|
3
|
-
|
|
4
|
-
require 'gitrob'
|
|
5
|
-
|
|
6
|
-
class App
|
|
7
|
-
include Methadone::Main
|
|
8
|
-
|
|
9
|
-
leak_exceptions true
|
|
10
|
-
|
|
11
|
-
main do
|
|
12
|
-
Thread.abort_on_exception = true
|
|
13
|
-
Paint.mode = 0 if options.include?('no-color')
|
|
14
|
-
|
|
15
|
-
puts Paint[Gitrob::banner, :bright, :blue] unless options.include?('no-banner')
|
|
16
|
-
Gitrob::status("Starting Gitrob version #{Gitrob::VERSION} at #{Time.now.strftime("%Y-%m-%d %I:%S %Z")}")
|
|
17
|
-
|
|
18
|
-
if !Gitrob::agreement_accepted?
|
|
19
|
-
puts Gitrob::agreement
|
|
20
|
-
print Paint["\n\nDo you agree to the terms of use? [y/n]: ", :bright, :green]
|
|
21
|
-
choice = $stdin.gets.chomp
|
|
22
|
-
if %w{y yes}.include?(choice)
|
|
23
|
-
Gitrob::agreement_accepted
|
|
24
|
-
else
|
|
25
|
-
Gitrob::fatal("Exiting Gitrob.")
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
if !Gitrob::configured? || options.include?('configure')
|
|
30
|
-
Gitrob::status("Starting Gitrob configuration wizard\n\n")
|
|
31
|
-
|
|
32
|
-
hostname = ask("#{Paint[" Enter PostgreSQL hostname:", :bright, :white]} ") { |q| q.default = "localhost" }
|
|
33
|
-
port = ask("#{Paint[" Enter PostgreSQL port:", :bright, :white]} ", Integer) { |q| q.default = 5432; q.in = 1..65535 }
|
|
34
|
-
username = ask("#{Paint[" Enter PostgreSQL username:", :bright, :white]} ")
|
|
35
|
-
password = ask("#{Paint[" Enter PostgreSQL password for #{username} (masked):", :bright, :white]} ") { |q| q.echo = 'x' }
|
|
36
|
-
database = ask("#{Paint[" Enter PostgreSQL database name:", :bright, :white]} ") { |q| q.default = 'gitrob' }
|
|
37
|
-
|
|
38
|
-
access_tokens = Array.new
|
|
39
|
-
while access_tokens.uniq.empty?
|
|
40
|
-
access_tokens = ask(Paint[" Enter GitHub access tokens (blank line to stop):", :bright, :white],
|
|
41
|
-
lambda { |ans| ans =~ /[a-f0-9]{40}/ ? ans : nil } ) do |q|
|
|
42
|
-
q.gather = ""
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
config = {
|
|
47
|
-
'sql_connection_uri' => "postgres://#{username}:#{password}@#{hostname}:#{port}/#{database}",
|
|
48
|
-
'github_access_tokens' => access_tokens.uniq
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
Gitrob::task("Saving configuration to file...") do
|
|
52
|
-
Gitrob::save_configuration!(config)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
exit unless options['organization']
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
Gitrob::task("Loading configuration...") do
|
|
59
|
-
Gitrob::load_configuration!
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
Gitrob::task("Preparing SQL database...") do
|
|
63
|
-
Gitrob::prepare_database!
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
if options['reset-db']
|
|
67
|
-
Gitrob::task("Resetting database tables...") do
|
|
68
|
-
DataMapper::auto_migrate!
|
|
69
|
-
end
|
|
70
|
-
exit
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
if options['delete']
|
|
74
|
-
Gitrob::delete_organization(options['delete'])
|
|
75
|
-
exit
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
if options['start-server']
|
|
79
|
-
Gitrob::status("Starting web application on http://#{options['bind-address']}:#{options['port']}/...")
|
|
80
|
-
puts "\n\n"
|
|
81
|
-
Gitrob::WebApp.run!(:port => options['port'].to_i, :bind => options['bind-address'])
|
|
82
|
-
exit
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
org_name = options['organization']
|
|
86
|
-
repo_count = 0
|
|
87
|
-
members = Array.new
|
|
88
|
-
http_client = Gitrob::Github::HttpClient.new({:access_tokens => Gitrob::configuration['github_access_tokens']})
|
|
89
|
-
observers = Gitrob::Observers.constants.collect { |c| Gitrob::Observers::const_get(c) }
|
|
90
|
-
|
|
91
|
-
Gitrob::task("Loading file patterns...") do
|
|
92
|
-
Gitrob::Observers::SensitiveFiles::load_patterns!
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
begin
|
|
96
|
-
org = Gitrob::Github::Organization.new(org_name, http_client)
|
|
97
|
-
|
|
98
|
-
Gitrob::task("Collecting organization repositories...") do
|
|
99
|
-
repo_count = org.repositories.count
|
|
100
|
-
end
|
|
101
|
-
rescue Gitrob::Github::HttpClient::ClientError => e
|
|
102
|
-
if e.status == 404
|
|
103
|
-
Gitrob::fatal("Cannot find GitHub organization with that name; exiting.")
|
|
104
|
-
else
|
|
105
|
-
raise e
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
Gitrob::task("Collecting organization members...") do
|
|
110
|
-
members = org.members
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
progress = Gitrob::ProgressBar.new("Collecting member repositories...",
|
|
114
|
-
:total => members.count
|
|
115
|
-
)
|
|
116
|
-
thread_pool = Thread.pool(options['threads'].to_i)
|
|
117
|
-
|
|
118
|
-
members.each do |member|
|
|
119
|
-
thread_pool.process do
|
|
120
|
-
if member.repositories.count > 0
|
|
121
|
-
repo_count += member.repositories.count
|
|
122
|
-
progress.log("Collected #{Gitrob::Util::pluralize(member.repositories.count, 'repository', 'repositories')} from #{Paint[member.username, :bright, :white]}")
|
|
123
|
-
end
|
|
124
|
-
progress.increment
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
thread_pool.shutdown
|
|
129
|
-
|
|
130
|
-
if repo_count.zero?
|
|
131
|
-
Gitrob::fatal("Organization has no repositories to check; exiting.")
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
progress = Gitrob::ProgressBar.new("Processing repositories...",
|
|
135
|
-
:total => repo_count
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
db_org = org.save_to_database!
|
|
139
|
-
thread_pool = Thread.pool(options['threads'].to_i)
|
|
140
|
-
|
|
141
|
-
org.repositories.each do |repo|
|
|
142
|
-
thread_pool.process do
|
|
143
|
-
begin
|
|
144
|
-
if repo.contents.count > 0
|
|
145
|
-
db_repo = repo.save_to_database!(db_org)
|
|
146
|
-
findings = 0
|
|
147
|
-
|
|
148
|
-
repo.contents.each do |blob|
|
|
149
|
-
db_blob = blob.to_model(db_org, db_repo)
|
|
150
|
-
|
|
151
|
-
observers.each do |observer|
|
|
152
|
-
observer.observe(db_blob)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
db_blob.findings.each do |f|
|
|
156
|
-
db_blob.findings_count += 1
|
|
157
|
-
findings += 1
|
|
158
|
-
f.organization = db_org
|
|
159
|
-
f.repo = db_repo
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
db_blob.save
|
|
163
|
-
end
|
|
164
|
-
progress.log("Processed #{Gitrob::Util::pluralize(repo.contents.count, 'file', 'files')} from #{Paint[repo.full_name, :bright, :white]} with #{findings.zero? ? 'no findings' : Paint[Gitrob::Util.pluralize(findings, 'finding', 'findings'), :yellow]}")
|
|
165
|
-
end
|
|
166
|
-
progress.increment
|
|
167
|
-
rescue Exception => e
|
|
168
|
-
progress.log_error("Encountered error when processing #{Paint[repo.full_name, :bright, :white]} (#{e.class.name})")
|
|
169
|
-
progress.increment
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
org.members.each do |member|
|
|
175
|
-
thread_pool.process do
|
|
176
|
-
begin
|
|
177
|
-
db_user = member.save_to_database!(db_org)
|
|
178
|
-
|
|
179
|
-
member.repositories.each do |repo|
|
|
180
|
-
if repo.contents.count > 0
|
|
181
|
-
db_repo = repo.save_to_database!(db_org, db_user)
|
|
182
|
-
findings = 0
|
|
183
|
-
|
|
184
|
-
repo.contents.each do |blob|
|
|
185
|
-
db_blob = blob.to_model(db_org, db_repo)
|
|
186
|
-
|
|
187
|
-
observers.each do |observer|
|
|
188
|
-
observer.observe(db_blob)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
db_blob.findings.each do |f|
|
|
192
|
-
db_blob.findings_count += 1
|
|
193
|
-
findings += 1
|
|
194
|
-
f.organization = db_org
|
|
195
|
-
f.repo = db_repo
|
|
196
|
-
f.user = db_user
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
db_blob.save
|
|
200
|
-
end
|
|
201
|
-
progress.log("Processed #{Gitrob::Util::pluralize(repo.contents.count, 'file', 'files')} from #{Paint[repo.full_name, :bright, :white]} with #{findings.zero? ? 'no findings' : Paint[Gitrob::Util.pluralize(findings, 'finding', 'findings'), :yellow]}")
|
|
202
|
-
end
|
|
203
|
-
progress.increment
|
|
204
|
-
end
|
|
205
|
-
rescue Exception => e
|
|
206
|
-
progress.log_error("Encountered error when processing #{Paint[repo.full_name, :bright, :white]} (#{e.class.name})")
|
|
207
|
-
progress.increment
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
thread_pool.shutdown
|
|
213
|
-
|
|
214
|
-
if !options.include?('no-server')
|
|
215
|
-
Gitrob::status("Starting web application on port #{options['port']}...")
|
|
216
|
-
Gitrob::status("Browse to http://#{options['bind-address']}:#{options['port']}/ to see results!")
|
|
217
|
-
puts "\n\n"
|
|
218
|
-
Gitrob::WebApp.run!(:port => options[:port].to_i, :bind => options['bind-address'])
|
|
219
|
-
exit
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
version Gitrob::VERSION
|
|
224
|
-
description "Reconnaissance tool for GitHub organizations."
|
|
225
|
-
|
|
226
|
-
options['bind-address'] = '127.0.0.1'
|
|
227
|
-
options['port'] = 9393
|
|
228
|
-
options['threads'] = 3
|
|
229
|
-
|
|
230
|
-
on('-o', '--organization NAME', "Name of GitHub organization")
|
|
231
|
-
on('-s', '--start-server', "Start web server, don't run any checks")
|
|
232
|
-
on('-p', '--port PORT', "Port to bind web server to")
|
|
233
|
-
on('-b', '--bind-address ADDRESS', "Address to bind web server to")
|
|
234
|
-
on('-t', '--threads THREADS', "Number of threads to use")
|
|
235
|
-
on('--delete NAME', "Delete an organization in the database")
|
|
236
|
-
on('--reset-db', "Resets the database")
|
|
237
|
-
on('--configure', "Start configuration wizard")
|
|
238
|
-
on('--no-server', "Don't start the server when finished")
|
|
239
|
-
on('--no-color', "Don't colorize output")
|
|
240
|
-
on('--no-banner', "Don't print Gitrob banner")
|
|
241
|
-
|
|
242
|
-
begin
|
|
243
|
-
if ARGV.empty?
|
|
244
|
-
Gitrob::fatal("No options given; see gitrob --help for options.")
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
go!
|
|
248
|
-
rescue Gitrob::Github::HttpClient::MissingAccessTokensError
|
|
249
|
-
Gitrob::fatal("Configuration file does not contain any GitHub access tokens. Run Gitrob with --configure flag to set it up.")
|
|
250
|
-
rescue Gitrob::Github::HttpClient::AccessTokensDepletedError
|
|
251
|
-
Gitrob::fatal("All GitHub access tokens under rate limiting. Go have a cup of coffee and try again.")
|
|
252
|
-
rescue Gitrob::Github::HttpClient::RequestError => e
|
|
253
|
-
Gitrob::fatal("A request to the GitHub API failed: #{e.message}")
|
|
254
|
-
rescue Interrupt
|
|
255
|
-
print "\b\b\n"; # Remove ^C from screen
|
|
256
|
-
Gitrob::fatal("Caught interrupt; exiting.")
|
|
257
|
-
rescue => e
|
|
258
|
-
Gitrob::fatal("An error occurred: #{e.class.name}: #{e.message}")
|
|
259
|
-
end
|
|
260
|
-
end
|
data/lib/gitrob/github/blob.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
module Gitrob
|
|
2
|
-
module Github
|
|
3
|
-
class Blob
|
|
4
|
-
attr_reader :path, :size, :repository
|
|
5
|
-
|
|
6
|
-
def initialize(path, size, repository)
|
|
7
|
-
@path, @size, @repository = path, size, repository
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def extension
|
|
11
|
-
File.extname(path)[1..-1]
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def filename
|
|
15
|
-
File.basename(path)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def dirname
|
|
19
|
-
File.dirname(path)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def url
|
|
23
|
-
"https://github.com/#{URI.escape(repository.owner)}/#{URI.escape(repository.name)}/blob/master/#{URI.escape(path)}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def to_model(organization, repository)
|
|
27
|
-
repository.blobs.new(
|
|
28
|
-
:path => self.path,
|
|
29
|
-
:filename => self.filename,
|
|
30
|
-
:extension => self.extension,
|
|
31
|
-
:size => self.size,
|
|
32
|
-
:organization => organization
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def save_to_database!(organization, repository)
|
|
37
|
-
self.to_model(organization, repository).tap { |m| m.save }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
module Gitrob
|
|
2
|
-
module Github
|
|
3
|
-
class HttpClient
|
|
4
|
-
include HTTParty
|
|
5
|
-
base_uri 'https://api.github.com'
|
|
6
|
-
|
|
7
|
-
class HttpError < StandardError; end
|
|
8
|
-
class ConnectionError < HttpError; end
|
|
9
|
-
|
|
10
|
-
class RequestError < HttpError
|
|
11
|
-
attr_reader :status, :body
|
|
12
|
-
def initialize(method, path, status, body, options)
|
|
13
|
-
@status = status
|
|
14
|
-
@body = body
|
|
15
|
-
super("#{method} to #{path} returned status #{status} - options: #{options.inspect}")
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
class ClientError < RequestError; end
|
|
20
|
-
class ServerError < RequestError; end
|
|
21
|
-
|
|
22
|
-
class UnhandledError < StandardError; end
|
|
23
|
-
|
|
24
|
-
class AccessTokenError < StandardError; end
|
|
25
|
-
class MissingAccessTokensError < AccessTokenError; end
|
|
26
|
-
class AccessTokensDepletedError < AccessTokenError; end
|
|
27
|
-
|
|
28
|
-
DEFAULT_TIMEOUT = 0.5 #seconds
|
|
29
|
-
DEFAULT_RETRIES = 3
|
|
30
|
-
|
|
31
|
-
Response = Struct.new(:status, :headers, :body)
|
|
32
|
-
|
|
33
|
-
RETRIABLE_EXCEPTIONS = [
|
|
34
|
-
ServerError,
|
|
35
|
-
AccessTokenError,
|
|
36
|
-
Timeout::Error,
|
|
37
|
-
Errno::ETIMEDOUT,
|
|
38
|
-
Errno::ECONNRESET,
|
|
39
|
-
Errno::ECONNREFUSED,
|
|
40
|
-
Errno::ENETUNREACH,
|
|
41
|
-
Errno::EHOSTUNREACH,
|
|
42
|
-
EOFError
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
def initialize(options)
|
|
46
|
-
@config = {
|
|
47
|
-
:timeout => DEFAULT_TIMEOUT,
|
|
48
|
-
:retries => DEFAULT_RETRIES
|
|
49
|
-
}.merge(options)
|
|
50
|
-
raise MissingAccessTokensErrors.new("No access tokens given") unless @config[:access_tokens]
|
|
51
|
-
default_timeout = @config[:timeout]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def do_get(path, params=nil, options={})
|
|
55
|
-
do_request(:get, path, {:query => params}.merge(options))
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def do_post(path, params=nil, options={})
|
|
59
|
-
do_request(:post, path, {:query => params}.merge(options))
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def do_put(path, params=nil, options={})
|
|
63
|
-
do_request(:put, path, {:query => params}.merge(options))
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def do_delete(path, params=nil, options={})
|
|
67
|
-
do_request(:delete, path, {:query => params}.merge(options))
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def do_request(method, path, options)
|
|
73
|
-
with_retries do
|
|
74
|
-
access_token = get_access_token!
|
|
75
|
-
response = self.class.send(method, path, {
|
|
76
|
-
:headers => {
|
|
77
|
-
'Authorization' => "token #{access_token}",
|
|
78
|
-
'User-Agent' => "Gitrob v#{Gitrob::VERSION}"
|
|
79
|
-
}
|
|
80
|
-
}.merge(options))
|
|
81
|
-
handle_possible_error!(method, path, response, options, access_token)
|
|
82
|
-
Response.new(response.code, response.headers, response.body)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def with_retries(&block)
|
|
87
|
-
tries = @config[:retries]
|
|
88
|
-
yield
|
|
89
|
-
rescue *RETRIABLE_EXCEPTIONS => ex
|
|
90
|
-
if (tries -= 1) > 0
|
|
91
|
-
sleep 0.2
|
|
92
|
-
retry
|
|
93
|
-
else
|
|
94
|
-
raise ex
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def handle_possible_error!(method, path, response, options, access_token)
|
|
99
|
-
if access_token_rate_limited?(response) || access_token_unauthorized?(response)
|
|
100
|
-
access_tokens.delete(access_token)
|
|
101
|
-
raise AccessTokenError
|
|
102
|
-
elsif response.code >= 500
|
|
103
|
-
raise ServerError.new(method, path, response.code, response.body, options)
|
|
104
|
-
elsif response.code >= 400
|
|
105
|
-
raise ClientError.new(method, path, response.code, response.body, options)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def access_token_rate_limited?(response)
|
|
110
|
-
response.code == 403 && response.headers['X-RateLimit-Remaining'].to_i.zero?
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def access_token_unauthorized?(response)
|
|
114
|
-
response.code == 401
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def get_access_token!
|
|
118
|
-
raise AccessTokensDepletedError.new("Rate limit on all access tokens has been used up") if access_tokens.count.zero?
|
|
119
|
-
access_tokens.sample
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def access_tokens
|
|
123
|
-
@config[:access_tokens]
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
module Gitrob
|
|
2
|
-
module Github
|
|
3
|
-
class Organization
|
|
4
|
-
attr_reader :name, :http_client
|
|
5
|
-
|
|
6
|
-
def initialize(name, http_client)
|
|
7
|
-
@name, @http_client = name, http_client
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def display_name
|
|
11
|
-
info['name'].to_s.empty? ? info['login'] : info['name']
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def login
|
|
15
|
-
info['login']
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def website
|
|
19
|
-
info['blog']
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def location
|
|
23
|
-
info['location']
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def email
|
|
27
|
-
info['email']
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def url
|
|
31
|
-
"https://github.com/#{name}"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def avatar_url
|
|
35
|
-
info['avatar_url']
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def repositories
|
|
39
|
-
@repositories ||= recursive_repositories
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def members
|
|
43
|
-
@members ||= recursive_members
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def to_model
|
|
47
|
-
Gitrob::Organization.new(
|
|
48
|
-
:name => self.display_name,
|
|
49
|
-
:login => self.login,
|
|
50
|
-
:website => self.website,
|
|
51
|
-
:location => self.location,
|
|
52
|
-
:email => self.email,
|
|
53
|
-
:avatar_url => self.avatar_url,
|
|
54
|
-
:url => self.url
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def save_to_database!
|
|
59
|
-
self.to_model.tap { |m| m.save }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def recursive_members(page = 1)
|
|
65
|
-
members = Array.new
|
|
66
|
-
response = http_client.do_get("/orgs/#{name}/members?page=#{page.to_i}")
|
|
67
|
-
JSON.parse(response.body).each do |member|
|
|
68
|
-
members << User.new(member['login'], http_client)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
if response.headers.include?('link') && response.headers['link'].include?('rel="next"')
|
|
72
|
-
members += recursive_members(page + 1)
|
|
73
|
-
end
|
|
74
|
-
members
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def recursive_repositories(page = 1)
|
|
78
|
-
repositories = Array.new
|
|
79
|
-
response = http_client.do_get("/orgs/#{name}/repos?page=#{page.to_i}")
|
|
80
|
-
JSON.parse(response.body).each do |repo|
|
|
81
|
-
next if repo['fork']
|
|
82
|
-
repositories << Repository.new(name, repo['name'], http_client)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
if response.headers.include?('link') && response.headers['link'].include?('rel="next"')
|
|
86
|
-
repositories += recursive_repositories(page + 1)
|
|
87
|
-
end
|
|
88
|
-
repositories
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def info
|
|
92
|
-
if !@info
|
|
93
|
-
@info = JSON.parse(http_client.do_get("/orgs/#{name}").body)
|
|
94
|
-
end
|
|
95
|
-
@info
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
module Gitrob
|
|
2
|
-
module Github
|
|
3
|
-
class Repository
|
|
4
|
-
|
|
5
|
-
attr_reader :owner, :name, :http_client
|
|
6
|
-
def initialize(owner, name, http_client)
|
|
7
|
-
@owner, @name, @http_client = owner, name, http_client
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def contents
|
|
11
|
-
if !@contents
|
|
12
|
-
@contents = []
|
|
13
|
-
response = JSON.parse(http_client.do_get("/repos/#{owner}/#{name}/git/trees/master?recursive=1").body)
|
|
14
|
-
response['tree'].each do |object|
|
|
15
|
-
next unless object['type'] == 'blob'
|
|
16
|
-
@contents << Blob.new(object['path'], object['size'], self)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
@contents
|
|
20
|
-
rescue HttpClient::ClientError => ex
|
|
21
|
-
if ex.status == 409 || ex.status == 404
|
|
22
|
-
@contents = []
|
|
23
|
-
else
|
|
24
|
-
raise ex
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def full_name
|
|
29
|
-
[owner, name].join('/')
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def url
|
|
33
|
-
info['html_url']
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def description
|
|
37
|
-
info['description']
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def website
|
|
41
|
-
info['homepage']
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def to_model(organization, user = nil)
|
|
45
|
-
Gitrob::Repo.new(
|
|
46
|
-
:name => self.name,
|
|
47
|
-
:owner_name => self.owner,
|
|
48
|
-
:description => self.description,
|
|
49
|
-
:website => self.website,
|
|
50
|
-
:url => self.url,
|
|
51
|
-
:organization => organization,
|
|
52
|
-
:user => user
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def save_to_database!(organization, user = nil)
|
|
57
|
-
self.to_model(organization, user).tap { |m| m.save }
|
|
58
|
-
rescue DataMapper::SaveFailureError => e
|
|
59
|
-
puts e.resource.errors.inspect
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def info
|
|
65
|
-
if !@info
|
|
66
|
-
@info = JSON.parse(http_client.do_get("/repos/#{owner}/#{name}").body)
|
|
67
|
-
end
|
|
68
|
-
@info
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
data/lib/gitrob/github/user.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
module Gitrob
|
|
2
|
-
module Github
|
|
3
|
-
class User
|
|
4
|
-
|
|
5
|
-
attr_reader :username, :http_client
|
|
6
|
-
|
|
7
|
-
def initialize(username, http_client)
|
|
8
|
-
@username, @http_client = username, http_client
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def name
|
|
12
|
-
info['name'] || username
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def email
|
|
16
|
-
info['email']
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def website
|
|
20
|
-
info['blog']
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def location
|
|
24
|
-
info['location']
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def bio
|
|
28
|
-
info['bio']
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def url
|
|
32
|
-
info['html_url']
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def avatar_url
|
|
36
|
-
info['avatar_url']
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def repositories
|
|
40
|
-
@repositories ||= recursive_repositories
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def to_model(organization)
|
|
44
|
-
organization.users.new(
|
|
45
|
-
:username => self.username,
|
|
46
|
-
:name => self.name,
|
|
47
|
-
:website => self.website,
|
|
48
|
-
:location => self.location,
|
|
49
|
-
:email => self.email,
|
|
50
|
-
:bio => self.bio,
|
|
51
|
-
:url => self.url,
|
|
52
|
-
:avatar_url => self.avatar_url
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def save_to_database!(organization)
|
|
57
|
-
self.to_model(organization).tap { |m| m.save }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def recursive_repositories(page = 1)
|
|
63
|
-
repositories = Array.new
|
|
64
|
-
response = http_client.do_get("/users/#{username}/repos?page=#{page.to_i}")
|
|
65
|
-
JSON.parse(response.body).each do |repo|
|
|
66
|
-
next if repo['fork']
|
|
67
|
-
repositories << Repository.new(username, repo['name'], http_client)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
if response.headers.include?('link') && response.headers['link'].include?('rel="next"')
|
|
71
|
-
repositories += recursive_repositories(page + 1)
|
|
72
|
-
end
|
|
73
|
-
repositories
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def info
|
|
77
|
-
if !@info
|
|
78
|
-
@info = JSON.parse(http_client.do_get("/users/#{username}").body)
|
|
79
|
-
end
|
|
80
|
-
@info
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|