gem_collector 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 +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: []
|