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.
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: []