gem_collector 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +80 -0
- data/Rakefile +25 -0
- data/app/assets/config/gem_collector_manifest.js +2 -0
- data/app/assets/javascripts/gem_collector/application.js +13 -0
- data/app/assets/stylesheets/gem_collector/application.css +15 -0
- data/app/controllers/gem_collector/application_controller.rb +5 -0
- data/app/controllers/gem_collector/gem_news_controller.rb +22 -0
- data/app/controllers/gem_collector/repositories_controller.rb +84 -0
- data/app/controllers/gem_collector/repository_gems_controller.rb +22 -0
- data/app/helpers/gem_collector/application_helper.rb +19 -0
- data/app/jobs/gem_collector/application_job.rb +4 -0
- data/app/jobs/gem_collector/update_gemfile_job.rb +5 -0
- data/app/mailers/gem_collector/application_mailer.rb +6 -0
- data/app/models/gem_collector/application_record.rb +5 -0
- data/app/models/gem_collector/octokit_provider.rb +10 -0
- data/app/models/gem_collector/repository.rb +84 -0
- data/app/models/gem_collector/repository_gem.rb +2 -0
- data/app/models/gem_collector/webhooks.rb +29 -0
- data/app/services/gem_collector/create_gem_news.rb +37 -0
- data/app/services/gem_collector/create_repository.rb +32 -0
- data/app/services/gem_collector/delete_repository.rb +12 -0
- data/app/services/gem_collector/update_gemfile.rb +65 -0
- data/app/services/gem_collector/update_repository.rb +10 -0
- data/app/views/gem_collector/gem_news/new.html.haml +13 -0
- data/app/views/gem_collector/repositories/index.html.haml +22 -0
- data/app/views/gem_collector/repositories/new.html.haml +8 -0
- data/app/views/gem_collector/repositories/show.html.haml +28 -0
- data/app/views/gem_collector/repository_gems/index.html.haml +17 -0
- data/app/views/gem_collector/repository_gems/show.html.haml +33 -0
- data/app/views/layouts/gem_collector/application.html.haml +7 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20170317021645_create_gem_collector_repository.rb +13 -0
- data/db/migrate/20170317021845_create_gem_collector_repository_gem.rb +13 -0
- data/lib/autoload/gem_collector/gem_version_validation_filter.rb +16 -0
- data/lib/gem_collector/engine.rb +12 -0
- data/lib/gem_collector/version.rb +3 -0
- data/lib/gem_collector.rb +6 -0
- data/lib/tasks/gem_collector_tasks.rake +6 -0
- metadata +195 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1fe2c15637de46fba3c2a0c0018de249256364f1
|
4
|
+
data.tar.gz: 17c6f80072475e010cdc77e5a4bd707f8202590f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9d6e030866974a479fed1d725112a9edc09f30d1c9c4da49ee354e3115eef3a20e36ac2d1f01320e159f1a4236a41ca7b5b7dbe48e70d629a3dd8f9691e6002c
|
7
|
+
data.tar.gz: 8174947d708aea1323bd7d47cb171e1cdb5b1b53883794432b95fed6a89253c1c2b1b0229a4354b8471e65af747b2821043d2612cd964f38e1f7d913922c59e9
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Kohei Suzuki
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# GemCollector
|
2
|
+
Collect gems used by applications.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'gem_collector'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install gem_collector
|
22
|
+
```
|
23
|
+
|
24
|
+
Put config/octokit.yml like below.
|
25
|
+
|
26
|
+
```yaml
|
27
|
+
default: &default
|
28
|
+
github.com:
|
29
|
+
access_token: <%= ENV['GITHUB_ACCESS_TOKEN'] %>
|
30
|
+
webhook_secret: <%= ENV['GITHUB_WEBHOOK_SECRET'] %>
|
31
|
+
github-enterprise.example.com:
|
32
|
+
api_endpoint: https://github-enterprise.example.com/api/v3
|
33
|
+
web_endpoint: https://github-enterprise.example.com
|
34
|
+
access_token: <%= ENV['GHE_ACCESS_TOKEN'] %>
|
35
|
+
|
36
|
+
development:
|
37
|
+
<<: *default
|
38
|
+
|
39
|
+
production:
|
40
|
+
<<: *default
|
41
|
+
```
|
42
|
+
|
43
|
+
Configure database.yml. GemCollector requires PostgreSQL.
|
44
|
+
|
45
|
+
```yaml
|
46
|
+
# config/database.yml
|
47
|
+
development:
|
48
|
+
adapter: postgresql
|
49
|
+
encoding: unicode
|
50
|
+
database: gem_collector_development
|
51
|
+
|
52
|
+
production:
|
53
|
+
url: <%= ENV['DATABASE_URL'] %>
|
54
|
+
```
|
55
|
+
|
56
|
+
Configure ActiveJob adapter. We're using [Barbeque](https://github.com/cookpad/barbeque), but other adapters should work.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# config/initializers/barbeque.rb
|
60
|
+
Rails.application.configure do
|
61
|
+
config.active_job.queue_adapter = :barbeque
|
62
|
+
end
|
63
|
+
|
64
|
+
BarbequeClient.configure do |config|
|
65
|
+
config.application = 'gem-collector'
|
66
|
+
config.default_queue = 'default'
|
67
|
+
config.endpoint =
|
68
|
+
if Rails.env.production?
|
69
|
+
ENV.fetch('BARBEQUE_ENDPOINT')
|
70
|
+
else
|
71
|
+
ENV.fetch('BARBEQUE_ENDPOINT', 'http://localhost:3003')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
Contribution directions go here.
|
78
|
+
|
79
|
+
## License
|
80
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'GemCollector'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class GemCollector::GemNewsController < GemCollector::ApplicationController
|
2
|
+
before_action GemCollector::GemVersionValidationFilter
|
3
|
+
before_action :validate_body, only: :create
|
4
|
+
|
5
|
+
def new
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
repositories = GemCollector::Repository.find_by_dependent_gem(params[:name], from_version: params[:from_version], to_version: params[:to_version])
|
10
|
+
begin
|
11
|
+
GemCollector::CreateGemNews.new(request.origin, *params.permit(:name, :title, :body, :from_version, :to_version).values).run(repositories)
|
12
|
+
flash[:notice] = 'Issues were created successfully'
|
13
|
+
rescue GemCollector::CreateGemNews::Error => e
|
14
|
+
flash[:error] = e.message
|
15
|
+
end
|
16
|
+
redirect_to repository_gem_path(params.permit(:name, :from_version, :to_version))
|
17
|
+
end
|
18
|
+
|
19
|
+
private def validate_body
|
20
|
+
render status: 400, plain: 'Empty body given' if params[:body].blank?
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class GemCollector::RepositoriesController < GemCollector::ApplicationController
|
2
|
+
skip_before_action :verify_authenticity_token, only: %i[github_webhook]
|
3
|
+
|
4
|
+
def index
|
5
|
+
@repositories = GemCollector::Repository.all_with_version_point
|
6
|
+
render 'gem_collector/repositories/index'
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@repository = GemCollector::Repository.find(params[:id])
|
11
|
+
end
|
12
|
+
|
13
|
+
def new
|
14
|
+
@repository = GemCollector::Repository.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
form = params.require(:repository)
|
19
|
+
repository = GemCollector::CreateRepository.new.run(
|
20
|
+
site: form[:site],
|
21
|
+
full_name: form[:full_name],
|
22
|
+
)
|
23
|
+
redirect_to repository_path(repository.id)
|
24
|
+
rescue GemCollector::CreateRepository::Error => e
|
25
|
+
redirect_to repositories_path, alert: e.message
|
26
|
+
end
|
27
|
+
|
28
|
+
def destroy
|
29
|
+
repository = GemCollector::Repository.find(params[:id])
|
30
|
+
GemCollector::DeleteRepository.new.run(repository)
|
31
|
+
redirect_to repositories_path
|
32
|
+
end
|
33
|
+
|
34
|
+
def github_webhook
|
35
|
+
event = request.headers['X-GitHub-Event']
|
36
|
+
case event
|
37
|
+
when 'ping'
|
38
|
+
render plain: 'pong'
|
39
|
+
when 'push'
|
40
|
+
html_url = params[:repository][:html_url]
|
41
|
+
site = Addressable::URI.parse(html_url).host
|
42
|
+
unless has_valid_signature?(site)
|
43
|
+
render status: 403, plain: "Signatures didn't match"
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
repository = GemCollector::UpdateRepository.new.run(
|
48
|
+
repository_id: params[:repository][:id],
|
49
|
+
full_name: params[:repository][:full_name],
|
50
|
+
html_url: html_url,
|
51
|
+
ssh_url: params[:repository][:ssh_url],
|
52
|
+
)
|
53
|
+
GemCollector::UpdateGemfileJob.perform_later(repository.id)
|
54
|
+
render plain: 'OK'
|
55
|
+
else
|
56
|
+
render status: 400, plain: "Unknown event #{event}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# https://developer.github.com/webhooks/securing/
|
63
|
+
def has_valid_signature?(site)
|
64
|
+
secret = Rails.application.config.octokit.fetch(site)['webhook_secret']
|
65
|
+
if secret
|
66
|
+
request.body.rewind
|
67
|
+
payload_body = request.body.read
|
68
|
+
given_signature = request.headers['X-Hub-Signature']
|
69
|
+
unless given_signature
|
70
|
+
return false
|
71
|
+
end
|
72
|
+
expected_signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), secret, payload_body)
|
73
|
+
Rack::Utils.secure_compare(expected_signature, given_signature)
|
74
|
+
else
|
75
|
+
# No validation
|
76
|
+
true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_github_site
|
81
|
+
ENV['DEFAULT_GITHUB_SITE'] || 'github.com'
|
82
|
+
end
|
83
|
+
helper_method :default_github_site
|
84
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class GemCollector::RepositoryGemsController < GemCollector::ApplicationController
|
2
|
+
before_action GemCollector::GemVersionValidationFilter, only: :show
|
3
|
+
|
4
|
+
def index
|
5
|
+
count_col = 'count(repository_id)'
|
6
|
+
order_by = [:name]
|
7
|
+
if order_by_popularity?
|
8
|
+
order_by.unshift("#{count_col} desc")
|
9
|
+
end
|
10
|
+
@gems = GemCollector::RepositoryGem.order(order_by).group(:name).pluck(:name, count_col)
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
@gem_name = params[:name]
|
15
|
+
@repositories = GemCollector::Repository.find_by_dependent_gem(@gem_name, from_version: params[:from_version], to_version: params[:to_version])
|
16
|
+
end
|
17
|
+
|
18
|
+
private def order_by_popularity?
|
19
|
+
params[:order] == 'popularity'
|
20
|
+
end
|
21
|
+
helper_method :order_by_popularity?
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module GemCollector
|
2
|
+
module ApplicationHelper
|
3
|
+
def gem_news_body_template
|
4
|
+
<<~TEMPLATE
|
5
|
+
## Problem
|
6
|
+
{{Write problem here}}
|
7
|
+
|
8
|
+
## Required actions
|
9
|
+
{{Write required actions here}}
|
10
|
+
|
11
|
+
## Background
|
12
|
+
{{Write background here}}
|
13
|
+
|
14
|
+
## Contact information
|
15
|
+
{{Write your department or inquiry counter about this news}}
|
16
|
+
TEMPLATE
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module GemCollector::OctokitProvider
|
2
|
+
def self.get(site)
|
3
|
+
conf = Rails.application.config.octokit.fetch(site)
|
4
|
+
Octokit::Client.new(
|
5
|
+
api_endpoint: conf['api_endpoint'],
|
6
|
+
web_endpoint: conf['web_endpoint'],
|
7
|
+
access_token: conf['access_token'],
|
8
|
+
)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class GemCollector::Repository < GemCollector::ApplicationRecord
|
2
|
+
has_many :repository_gems, dependent: :delete_all
|
3
|
+
|
4
|
+
def url(path = nil)
|
5
|
+
u = "https://#{site}/#{full_name}"
|
6
|
+
if path
|
7
|
+
"#{u}/blob/master/#{path}"
|
8
|
+
else
|
9
|
+
u
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def canonical_name
|
14
|
+
"#{site}/#{full_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
POINTS_FOR_GEMS_SQL = <<-SQL.strip_heredoc
|
18
|
+
select
|
19
|
+
gems.repository_id, gems.path, gems.name, gems.version
|
20
|
+
, cume_dist() over (partition by gems.name order by regexp_split_to_array(regexp_replace(version, '\.[^0-9.]+$', ''), '[^0-9]+') :: bigint[]) as version_point
|
21
|
+
from
|
22
|
+
#{GemCollector::RepositoryGem.table_name} gems
|
23
|
+
SQL
|
24
|
+
|
25
|
+
def gems_with_version_point
|
26
|
+
GemCollector::RepositoryGem.find_by_sql([<<-SQL.strip_heredoc, repository_id: id])
|
27
|
+
select *
|
28
|
+
from (#{POINTS_FOR_GEMS_SQL}) points_for_gems
|
29
|
+
where
|
30
|
+
repository_id = :repository_id
|
31
|
+
order by
|
32
|
+
path, name
|
33
|
+
SQL
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.all_with_version_point
|
37
|
+
# points_for_gems: gem ごとにバージョンでソートして、どれくらい上位にいるか
|
38
|
+
# points_for_repos: そのリポジトリが依存している gem について、↑の平均値
|
39
|
+
# 大きければ大きいほど最新の gem を使っていることになりそう
|
40
|
+
find_by_sql(<<-SQL.strip_heredoc)
|
41
|
+
select
|
42
|
+
repos.*, points_for_repos.path, points_for_repos.point
|
43
|
+
from (
|
44
|
+
select
|
45
|
+
repository_id, path, avg(version_point) as point
|
46
|
+
from (#{POINTS_FOR_GEMS_SQL}) points_for_gems
|
47
|
+
group by repository_id, path
|
48
|
+
) points_for_repos
|
49
|
+
inner join #{table_name} repos on repos.id = points_for_repos.repository_id
|
50
|
+
order by
|
51
|
+
point desc
|
52
|
+
SQL
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [String] gem_name
|
56
|
+
# @param [String] from_version Version string in Ruby gem manner.
|
57
|
+
# @param [String] to_version
|
58
|
+
def self.find_by_dependent_gem(gem_name, from_version: nil, to_version: nil)
|
59
|
+
from_version = '0.0.0' if from_version.blank?
|
60
|
+
find_by_sql([<<-SQL.strip_heredoc, gem_name: gem_name, from_version: from_version, to_version: to_version])
|
61
|
+
select
|
62
|
+
repos.id
|
63
|
+
, site
|
64
|
+
, full_name
|
65
|
+
, gems.version as gem_version
|
66
|
+
, gems.path as gem_path
|
67
|
+
from
|
68
|
+
#{table_name} repos
|
69
|
+
inner join #{GemCollector::RepositoryGem.table_name} gems on gems.repository_id = repos.id
|
70
|
+
where gems.name = :gem_name
|
71
|
+
and #{build_version_exp('version')} >= #{build_version_exp(':from_version')}
|
72
|
+
#{to_version.blank? ? '' : "and #{build_version_exp('version')} < #{build_version_exp(':to_version')}"}
|
73
|
+
order by
|
74
|
+
#{build_version_exp('version')} desc
|
75
|
+
, site
|
76
|
+
, full_name
|
77
|
+
, gems.path
|
78
|
+
SQL
|
79
|
+
end
|
80
|
+
|
81
|
+
private_class_method def self.build_version_exp(column_or_exp)
|
82
|
+
"(regexp_split_to_array(regexp_replace(#{column_or_exp}, '\.[^0-9.].+$', ''), '[^0-9]+') :: bigint[])"
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class GemCollector::Webhooks
|
2
|
+
def initialize(octokit)
|
3
|
+
@octokit = octokit
|
4
|
+
end
|
5
|
+
|
6
|
+
def create(full_name)
|
7
|
+
hook = find_hook(full_name)
|
8
|
+
unless hook
|
9
|
+
@octokit.create_hook(full_name, WEBHOOK_NAME, { url: WEBHOOK_URL, content_type: 'json' }, { events: WEBHOOK_EVENTS, active: true })
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def remove(full_name)
|
14
|
+
hook = find_hook(full_name)
|
15
|
+
if hook
|
16
|
+
@octokit.remove_hook(full_name, hook[:id])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
WEBHOOK_NAME = 'web'
|
23
|
+
WEBHOOK_URL = ENV['WEBHOOK_URL']
|
24
|
+
WEBHOOK_EVENTS = ['push']
|
25
|
+
|
26
|
+
def find_hook(full_name)
|
27
|
+
@octokit.hooks(full_name).find { |hook| hook.name == WEBHOOK_NAME && hook.config.url == WEBHOOK_URL && hook.events == WEBHOOK_EVENTS }
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class GemCollector::CreateGemNews
|
2
|
+
def initialize(homepage, gem_name, title, body, from_version, to_version)
|
3
|
+
@gem_name, @body, @from_version, @to_version = gem_name, body, from_version, to_version
|
4
|
+
@title = title.present? ? title : %!Please check for "#{gem_name}"!
|
5
|
+
end
|
6
|
+
|
7
|
+
# @params [Array<Repository>] repositories
|
8
|
+
def run(repositories)
|
9
|
+
repositories.each {|repo| create_news_issue(repo) }
|
10
|
+
end
|
11
|
+
|
12
|
+
private def create_news_issue(repo)
|
13
|
+
octokit = GemCollector::OctokitProvider.get(repo.site)
|
14
|
+
begin
|
15
|
+
octokit.create_issue(repo.full_name, @title, issue_body)
|
16
|
+
rescue Octokit::NotFound
|
17
|
+
raise Error.new("Cannot find repository #{repo.full_name}. @#{octokit.login} cannot find it")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private def issue_body
|
22
|
+
[<<-NEWS_HEADER.strip_heredoc, '', '---', '', @body].join("\n")
|
23
|
+
This issue was delivered from [gem_collector](homepage) because this repository depends on #{gem_name_with_version}.
|
24
|
+
NEWS_HEADER
|
25
|
+
end
|
26
|
+
|
27
|
+
private def gem_name_with_version
|
28
|
+
[
|
29
|
+
%!"#{@gem_name}"!,
|
30
|
+
@from_version.present? ? "(>= #{@from_version})" : nil,
|
31
|
+
@to_version.present? ? "(< #{@to_version})" : nil,
|
32
|
+
].compact.join(' ')
|
33
|
+
end
|
34
|
+
|
35
|
+
class Error < ::StandardError
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class GemCollector::CreateRepository
|
2
|
+
def run(site:, full_name:)
|
3
|
+
octokit = GemCollector::OctokitProvider.get(site)
|
4
|
+
begin
|
5
|
+
repo = octokit.repository(full_name)
|
6
|
+
rescue Octokit::NotFound
|
7
|
+
raise Error.new("Cannot find repository #{full_name}. @#{octokit.login} cannot find it")
|
8
|
+
end
|
9
|
+
|
10
|
+
repository = GemCollector::UpdateRepository.new.run(
|
11
|
+
repository_id: repo[:id],
|
12
|
+
full_name: repo[:full_name],
|
13
|
+
html_url: repo[:html_url],
|
14
|
+
ssh_url: repo[:ssh_url],
|
15
|
+
)
|
16
|
+
GemCollector::UpdateGemfile.new.run(repository)
|
17
|
+
begin
|
18
|
+
register_webhook(repository)
|
19
|
+
rescue Octokit::NotFound
|
20
|
+
raise Error.new("Cannot register webhook: @#{octokit.login} doesn't have admin permission on #{full_name}")
|
21
|
+
end
|
22
|
+
repository
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_webhook(repository)
|
26
|
+
octokit = GemCollector::OctokitProvider.get(repository.site)
|
27
|
+
GemCollector::Webhooks.new(octokit).create(repository.full_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
class Error < StandardError
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class GemCollector::DeleteRepository
|
2
|
+
def run(repository)
|
3
|
+
octokit = GemCollector::OctokitProvider.get(repository.site)
|
4
|
+
begin
|
5
|
+
GemCollector::Webhooks.new(octokit).remove(repository.full_name)
|
6
|
+
rescue Octokit::Error
|
7
|
+
Rails.logger.warn("Cannot check webhook in #{repository.full_name}")
|
8
|
+
end
|
9
|
+
|
10
|
+
repository.destroy!
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'find'
|
2
|
+
|
3
|
+
class GemCollector::UpdateGemfile
|
4
|
+
def run(repository)
|
5
|
+
Dir.mktmpdir("gem_collector_update_gemfile_#{repository.id}") do |dir|
|
6
|
+
dir_path = Pathname.new(dir)
|
7
|
+
system!('git', 'clone', '--depth=1', repository.ssh_url, dir)
|
8
|
+
retry_on_serialization_failure(tries: 3) do
|
9
|
+
GemCollector::RepositoryGem.transaction(isolation: :serializable) do
|
10
|
+
GemCollector::RepositoryGem.where(repository_id: repository.id).delete_all
|
11
|
+
each_gemfile_lock(dir) do |path|
|
12
|
+
update_gemfile_lock(repository, dir_path, path)
|
13
|
+
end
|
14
|
+
repository.touch(:updated_at)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def system!(*args)
|
23
|
+
unless system(*args)
|
24
|
+
raise "Command execution failure: #{args}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def retry_on_serialization_failure(tries:, &block)
|
29
|
+
tries.times do |i|
|
30
|
+
begin
|
31
|
+
return block.call
|
32
|
+
rescue ActiveRecord::StatementInvalid => e
|
33
|
+
if e.cause.is_a?(PG::TRSerializationFailure)
|
34
|
+
$stderr.puts "#{e.cause.class}: #{e.cause.message}"
|
35
|
+
sleep(2**i)
|
36
|
+
$stderr.puts "Retrying... (try: #{i+1})"
|
37
|
+
else
|
38
|
+
raise e
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
raise 'Too many serialization failures'
|
43
|
+
end
|
44
|
+
|
45
|
+
def each_gemfile_lock(dir, &block)
|
46
|
+
Find.find(dir) do |f|
|
47
|
+
if f == '.git'
|
48
|
+
Find.prune
|
49
|
+
end
|
50
|
+
path = Pathname.new(f)
|
51
|
+
if path.basename.to_s == 'Gemfile.lock'
|
52
|
+
block.call(path)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def update_gemfile_lock(repository, dir, path)
|
58
|
+
lockfile_parser = Bundler::LockfileParser.new(path.read)
|
59
|
+
lock_path = path.relative_path_from(dir).to_s
|
60
|
+
records = lockfile_parser.specs.map do |spec|
|
61
|
+
repository.repository_gems.build(path: lock_path, name: spec.name, version: spec.version.to_s)
|
62
|
+
end
|
63
|
+
GemCollector::RepositoryGem.import(records)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class GemCollector::UpdateRepository
|
2
|
+
def run(repository_id:, full_name:, html_url:, ssh_url:)
|
3
|
+
host = Addressable::URI.parse(html_url).host
|
4
|
+
repo = GemCollector::Repository.find_or_initialize_by(site: host, repository_id: repository_id)
|
5
|
+
repo.full_name = full_name
|
6
|
+
repo.ssh_url = ssh_url
|
7
|
+
repo.save!
|
8
|
+
repo
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
%h3 Create news issue on repositories using #{params[:name]}
|
2
|
+
|
3
|
+
= form_tag(create_gem_news_path(name: params[:name])) do
|
4
|
+
= hidden_field_tag(:from_version, params[:from_version])
|
5
|
+
= hidden_field_tag(:to_version, params[:to_version])
|
6
|
+
= label_tag(:title, 'Issue title:')
|
7
|
+
= text_field_tag(:title, nil, size: 80)
|
8
|
+
%p
|
9
|
+
= label_tag(:body, 'Issue body:')
|
10
|
+
%p
|
11
|
+
= text_area_tag(:body, gem_news_body_template, cols: 80, rows: 20)
|
12
|
+
%p
|
13
|
+
= submit_tag('Create issues')
|
@@ -0,0 +1,22 @@
|
|
1
|
+
%h2 Repositories
|
2
|
+
|
3
|
+
- if flash[:alert]
|
4
|
+
%div
|
5
|
+
%p= flash[:alert]
|
6
|
+
|
7
|
+
%div
|
8
|
+
%p= link_to('Add new repository', new_repository_path)
|
9
|
+
%p= link_to('All gems', repository_gems_path)
|
10
|
+
|
11
|
+
%table
|
12
|
+
%thead
|
13
|
+
%tr
|
14
|
+
%th Repository
|
15
|
+
%th Path
|
16
|
+
%th Up-to-date Point
|
17
|
+
%thead
|
18
|
+
- @repositories.each do |repository|
|
19
|
+
%tr
|
20
|
+
%td= link_to(repository.canonical_name, repository_path(repository.id))
|
21
|
+
%td= repository.path
|
22
|
+
%td= sprintf('%.4f', repository.point)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
%h2 Repository #{@repository.canonical_name}
|
2
|
+
|
3
|
+
%div
|
4
|
+
%p= link_to(@repository.url, @repository.url)
|
5
|
+
|
6
|
+
%div
|
7
|
+
- @repository.gems_with_version_point.group_by(&:path).each do |path, gems|
|
8
|
+
%h3= path
|
9
|
+
%div
|
10
|
+
%p Up-to-date Point: #{sprintf('%.4f', gems.sum(&:version_point) / gems.size)}
|
11
|
+
%table
|
12
|
+
%thead
|
13
|
+
%tr
|
14
|
+
%th Gem
|
15
|
+
%th Version
|
16
|
+
%th Up-to-date Point
|
17
|
+
%tbody
|
18
|
+
- gems.each do |gem|
|
19
|
+
%tr
|
20
|
+
%td= link_to(gem.name, repository_gem_path(gem.name))
|
21
|
+
%td= gem.version
|
22
|
+
%td= sprintf('%.4f', gem.version_point)
|
23
|
+
|
24
|
+
%div
|
25
|
+
%h3 Danger Zone
|
26
|
+
|
27
|
+
= form_tag(repository_path(@repository.id), method: :delete) do
|
28
|
+
= submit_tag('Destroy')
|
@@ -0,0 +1,17 @@
|
|
1
|
+
%h2 All gems
|
2
|
+
|
3
|
+
- if order_by_popularity?
|
4
|
+
= link_to 'Order by name', repository_gems_path
|
5
|
+
- else
|
6
|
+
= link_to 'Order by popularity', repository_gems_path(order: 'popularity')
|
7
|
+
|
8
|
+
%table
|
9
|
+
%thead
|
10
|
+
%tr
|
11
|
+
%th Gem
|
12
|
+
%th Number of repository
|
13
|
+
%tbody
|
14
|
+
- @gems.each do |name, count|
|
15
|
+
%tr
|
16
|
+
%td= link_to(name, repository_gem_path(name))
|
17
|
+
%td= count
|
@@ -0,0 +1,33 @@
|
|
1
|
+
- if flash[:notice]
|
2
|
+
= flash[:notice]
|
3
|
+
%hr
|
4
|
+
- elsif flash[:error]
|
5
|
+
= flash[:error]
|
6
|
+
%hr
|
7
|
+
|
8
|
+
%h2 Repositories using #{@gem_name}
|
9
|
+
|
10
|
+
%p
|
11
|
+
= form_tag('', method: :get) do |f|
|
12
|
+
= label_tag(:from_version, 'From version:')
|
13
|
+
= text_field_tag(:from_version, params[:from_version])
|
14
|
+
= label_tag(:to_version, 'To version:')
|
15
|
+
= text_field_tag(:to_version, params[:to_version])
|
16
|
+
= submit_tag('Filter')
|
17
|
+
|
18
|
+
%table
|
19
|
+
%thead
|
20
|
+
%tr
|
21
|
+
%th Repository
|
22
|
+
%th Path
|
23
|
+
%th Version
|
24
|
+
%tbody
|
25
|
+
- @repositories.each do |repository|
|
26
|
+
%tr
|
27
|
+
%td= link_to(repository.canonical_name, repository_path(repository.id))
|
28
|
+
%td= link_to(repository.gem_path, repository.url(repository.gem_path))
|
29
|
+
%td= repository.gem_version
|
30
|
+
|
31
|
+
%hr
|
32
|
+
|
33
|
+
= link_to('Create news issues on these repositories', new_gem_news_path(**params.permit(:name, :from_version, :to_version).to_h.symbolize_keys))
|
data/config/routes.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
GemCollector::Engine.routes.draw do
|
2
|
+
resources :repositories, only: %i[index show new create destroy]
|
3
|
+
post '/github-webhook' => 'repositories#github_webhook'
|
4
|
+
|
5
|
+
get '/gems' => 'repository_gems#index', as: :repository_gems
|
6
|
+
get '/gems/:name' => 'repository_gems#show', as: :repository_gem
|
7
|
+
get '/gems/:name/new_issue' => 'gem_news#new', as: :new_gem_news
|
8
|
+
post '/gems/:name/new_issue' => 'gem_news#create', as: :create_gem_news
|
9
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateGemCollectorRepository < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :gem_collector_repositories do |t|
|
4
|
+
t.string :site, null: false
|
5
|
+
t.integer :repository_id, null: false
|
6
|
+
t.string :full_name, null: false
|
7
|
+
t.string :ssh_url, null: false
|
8
|
+
t.timestamps
|
9
|
+
|
10
|
+
t.index [:site, :repository_id], unique: true, name: 'idx_repositories'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateGemCollectorRepositoryGem < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :gem_collector_repository_gems do |t|
|
4
|
+
t.integer :repository_id, null: false
|
5
|
+
t.string :name, null: false
|
6
|
+
t.string :version, null: false
|
7
|
+
t.string :path, null: false
|
8
|
+
t.timestamps
|
9
|
+
|
10
|
+
t.index [:repository_id, :path, :name], unique: true, name: 'idx_repository_gems'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module GemCollector::GemVersionValidationFilter
|
2
|
+
def self.before(controller)
|
3
|
+
[controller.params[:from_version], controller.params[:to_version]].each do |v|
|
4
|
+
controller.render status: 400, plain: "Invalid version string: #{v}" unless valid_version?(v)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.valid_version?(v)
|
9
|
+
begin
|
10
|
+
::Gem::Version.new(v)
|
11
|
+
true
|
12
|
+
rescue ArgumentError
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module GemCollector
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace GemCollector
|
4
|
+
|
5
|
+
config.autoload_paths << root.join('lib/autoload').to_s
|
6
|
+
config.eager_load_paths << root.join('lib/autoload').to_s
|
7
|
+
|
8
|
+
initializer 'gem_collector.octokit' do |app|
|
9
|
+
app.config.octokit = app.config_for(:octokit)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gem_collector
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kohei Suzuki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord-import
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: addressable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: haml
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: octokit
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 5.0.2
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 5.0.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: factory_girl_rails
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Collect gems used by applications
|
126
|
+
email:
|
127
|
+
- kohei-suzuki@cookpad.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- MIT-LICENSE
|
133
|
+
- README.md
|
134
|
+
- Rakefile
|
135
|
+
- app/assets/config/gem_collector_manifest.js
|
136
|
+
- app/assets/javascripts/gem_collector/application.js
|
137
|
+
- app/assets/stylesheets/gem_collector/application.css
|
138
|
+
- app/controllers/gem_collector/application_controller.rb
|
139
|
+
- app/controllers/gem_collector/gem_news_controller.rb
|
140
|
+
- app/controllers/gem_collector/repositories_controller.rb
|
141
|
+
- app/controllers/gem_collector/repository_gems_controller.rb
|
142
|
+
- app/helpers/gem_collector/application_helper.rb
|
143
|
+
- app/jobs/gem_collector/application_job.rb
|
144
|
+
- app/jobs/gem_collector/update_gemfile_job.rb
|
145
|
+
- app/mailers/gem_collector/application_mailer.rb
|
146
|
+
- app/models/gem_collector/application_record.rb
|
147
|
+
- app/models/gem_collector/octokit_provider.rb
|
148
|
+
- app/models/gem_collector/repository.rb
|
149
|
+
- app/models/gem_collector/repository_gem.rb
|
150
|
+
- app/models/gem_collector/webhooks.rb
|
151
|
+
- app/services/gem_collector/create_gem_news.rb
|
152
|
+
- app/services/gem_collector/create_repository.rb
|
153
|
+
- app/services/gem_collector/delete_repository.rb
|
154
|
+
- app/services/gem_collector/update_gemfile.rb
|
155
|
+
- app/services/gem_collector/update_repository.rb
|
156
|
+
- app/views/gem_collector/gem_news/new.html.haml
|
157
|
+
- app/views/gem_collector/repositories/index.html.haml
|
158
|
+
- app/views/gem_collector/repositories/new.html.haml
|
159
|
+
- app/views/gem_collector/repositories/show.html.haml
|
160
|
+
- app/views/gem_collector/repository_gems/index.html.haml
|
161
|
+
- app/views/gem_collector/repository_gems/show.html.haml
|
162
|
+
- app/views/layouts/gem_collector/application.html.haml
|
163
|
+
- config/routes.rb
|
164
|
+
- db/migrate/20170317021645_create_gem_collector_repository.rb
|
165
|
+
- db/migrate/20170317021845_create_gem_collector_repository_gem.rb
|
166
|
+
- lib/autoload/gem_collector/gem_version_validation_filter.rb
|
167
|
+
- lib/gem_collector.rb
|
168
|
+
- lib/gem_collector/engine.rb
|
169
|
+
- lib/gem_collector/version.rb
|
170
|
+
- lib/tasks/gem_collector_tasks.rake
|
171
|
+
homepage: https://github.com/cookpad/gem_collector
|
172
|
+
licenses:
|
173
|
+
- MIT
|
174
|
+
metadata: {}
|
175
|
+
post_install_message:
|
176
|
+
rdoc_options: []
|
177
|
+
require_paths:
|
178
|
+
- lib
|
179
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: '0'
|
189
|
+
requirements: []
|
190
|
+
rubyforge_project:
|
191
|
+
rubygems_version: 2.6.10
|
192
|
+
signing_key:
|
193
|
+
specification_version: 4
|
194
|
+
summary: Collect gems used by applications
|
195
|
+
test_files: []
|