gem_collector 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +80 -0
  4. data/Rakefile +25 -0
  5. data/app/assets/config/gem_collector_manifest.js +2 -0
  6. data/app/assets/javascripts/gem_collector/application.js +13 -0
  7. data/app/assets/stylesheets/gem_collector/application.css +15 -0
  8. data/app/controllers/gem_collector/application_controller.rb +5 -0
  9. data/app/controllers/gem_collector/gem_news_controller.rb +22 -0
  10. data/app/controllers/gem_collector/repositories_controller.rb +84 -0
  11. data/app/controllers/gem_collector/repository_gems_controller.rb +22 -0
  12. data/app/helpers/gem_collector/application_helper.rb +19 -0
  13. data/app/jobs/gem_collector/application_job.rb +4 -0
  14. data/app/jobs/gem_collector/update_gemfile_job.rb +5 -0
  15. data/app/mailers/gem_collector/application_mailer.rb +6 -0
  16. data/app/models/gem_collector/application_record.rb +5 -0
  17. data/app/models/gem_collector/octokit_provider.rb +10 -0
  18. data/app/models/gem_collector/repository.rb +84 -0
  19. data/app/models/gem_collector/repository_gem.rb +2 -0
  20. data/app/models/gem_collector/webhooks.rb +29 -0
  21. data/app/services/gem_collector/create_gem_news.rb +37 -0
  22. data/app/services/gem_collector/create_repository.rb +32 -0
  23. data/app/services/gem_collector/delete_repository.rb +12 -0
  24. data/app/services/gem_collector/update_gemfile.rb +65 -0
  25. data/app/services/gem_collector/update_repository.rb +10 -0
  26. data/app/views/gem_collector/gem_news/new.html.haml +13 -0
  27. data/app/views/gem_collector/repositories/index.html.haml +22 -0
  28. data/app/views/gem_collector/repositories/new.html.haml +8 -0
  29. data/app/views/gem_collector/repositories/show.html.haml +28 -0
  30. data/app/views/gem_collector/repository_gems/index.html.haml +17 -0
  31. data/app/views/gem_collector/repository_gems/show.html.haml +33 -0
  32. data/app/views/layouts/gem_collector/application.html.haml +7 -0
  33. data/config/routes.rb +9 -0
  34. data/db/migrate/20170317021645_create_gem_collector_repository.rb +13 -0
  35. data/db/migrate/20170317021845_create_gem_collector_repository_gem.rb +13 -0
  36. data/lib/autoload/gem_collector/gem_version_validation_filter.rb +16 -0
  37. data/lib/gem_collector/engine.rb +12 -0
  38. data/lib/gem_collector/version.rb +3 -0
  39. data/lib/gem_collector.rb +6 -0
  40. data/lib/tasks/gem_collector_tasks.rake +6 -0
  41. 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,2 @@
1
+ //= link_directory ../javascripts/gem_collector .js
2
+ //= link_directory ../stylesheets/gem_collector .css
@@ -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,5 @@
1
+ module GemCollector
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -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,4 @@
1
+ module GemCollector
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ class GemCollector::UpdateGemfileJob < GemCollector::ApplicationJob
2
+ def perform(repository_id)
3
+ GemCollector::UpdateGemfile.new.run(GemCollector::Repository.find(repository_id))
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module GemCollector
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module GemCollector
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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,2 @@
1
+ class GemCollector::RepositoryGem < GemCollector::ApplicationRecord
2
+ 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,8 @@
1
+ %h2 New repository
2
+
3
+ = form_for(@repository) do |f|
4
+ = f.label :site
5
+ = f.text_field :site, value: default_github_site
6
+ = f.label :full_name
7
+ = f.text_field :full_name, placeholder: 'cookpad/gem_collector'
8
+ = f.submit
@@ -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))
@@ -0,0 +1,7 @@
1
+ !!! 5
2
+ %html
3
+ %head
4
+ %title GemCollector
5
+ = csrf_meta_tags
6
+ %body
7
+ = yield
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
@@ -0,0 +1,3 @@
1
+ module GemCollector
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'activerecord-import'
2
+ require 'addressable'
3
+ require 'haml'
4
+ require 'octokit'
5
+
6
+ require 'gem_collector/engine'
@@ -0,0 +1,6 @@
1
+ namespace :gem_collector do
2
+ namespace :ridgepole do
3
+ desc 'Apply Schemafile' do
4
+ end
5
+ end
6
+ 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: []