diff --git a/app/models/label.rb b/app/models/label.rb new file mode 100644 index 000000000..6e2be360f --- /dev/null +++ b/app/models/label.rb @@ -0,0 +1,4 @@ +class Label < ActiveRecord::Base + has_many :repo_labels + has_many :repos, through: :repo_lables +end diff --git a/app/models/repo.rb b/app/models/repo.rb index 7573806e4..d9719b079 100644 --- a/app/models/repo.rb +++ b/app/models/repo.rb @@ -16,6 +16,9 @@ class Repo < ActiveRecord::Base has_many :doc_methods, dependent: :destroy delegate :open_issues, to: :issues + has_many :repo_labels + has_many :labels, through: :repo_labels + before_validation :set_full_name!, :downcase_name, :strip_whitespaces after_create :background_populate_issues!, :update_repo_info!, :background_populate_docs! diff --git a/app/models/repo_label.rb b/app/models/repo_label.rb new file mode 100644 index 000000000..5e12854a6 --- /dev/null +++ b/app/models/repo_label.rb @@ -0,0 +1,4 @@ +class RepoLabel < ActiveRecord::Base + belongs_to :repo + belongs_to :label +end diff --git a/app/services/repo_label_assigner.rb b/app/services/repo_label_assigner.rb new file mode 100644 index 000000000..09177abf3 --- /dev/null +++ b/app/services/repo_label_assigner.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# PORO +# This class takes in a repo and fetch all labels for that repo. It then +# creates labels and associates them with the passed in repo +# +# Example: +# +# repo = Repo.first +# assigner = RepoLabelAssigner.new(repo: repo) +# assigner.create_and_associate_labels! +# +class RepoLabelAssigner + def initialize(repo:) + @repo = repo + url = ['repos', repo.user_name, repo.name, 'labels'].join('/') + @github_bub_response = GitHubBub.get(url) + end + + def create_and_associate_labels! + return unless github_bub_response.success? + + remote_labels.each do |label_hash| + label_name = label_hash['name'].downcase + label = Label.where(name: label_name).first_or_create! + repo.repo_labels.where(label: label).first_or_create + end + end + + private + + attr_reader :github_bub_response, :repo + + def remote_labels + Array(github_bub_response.json_body) + end +end diff --git a/db/migrate/20221004010742_create_labels_table.rb b/db/migrate/20221004010742_create_labels_table.rb new file mode 100644 index 000000000..09ef2edea --- /dev/null +++ b/db/migrate/20221004010742_create_labels_table.rb @@ -0,0 +1,9 @@ +class CreateLabelsTable < ActiveRecord::Migration[6.1] + def change + create_table :labels do |t| + t.string :name, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20221004010749_create_repo_labels_table.rb b/db/migrate/20221004010749_create_repo_labels_table.rb new file mode 100644 index 000000000..c47ae70ed --- /dev/null +++ b/db/migrate/20221004010749_create_repo_labels_table.rb @@ -0,0 +1,12 @@ +class CreateRepoLabelsTable < ActiveRecord::Migration[6.1] + def change + create_table :repo_labels do |t| + t.references :repo, foreign_key: true, null: false + t.references :label, foreign_key: true, null: false + + t.timestamps + end + + add_index :repo_labels, [:repo_id, :label_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 403d6279f..6dc16898f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_09_08_011235) do +ActiveRecord::Schema.define(version: 2022_10_04_010749) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -106,6 +106,22 @@ t.index ["updated_at"], name: "index_issues_on_updated_at", where: "((state)::text = 'open'::text)" end + create_table "labels", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "repo_labels", force: :cascade do |t| + t.bigint "repo_id", null: false + t.bigint "label_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["label_id"], name: "index_repo_labels_on_label_id" + t.index ["repo_id", "label_id"], name: "index_repo_labels_on_repo_id_and_label_id", unique: true + t.index ["repo_id"], name: "index_repo_labels_on_repo_id" + end + create_table "repo_subscriptions", id: :serial, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -190,6 +206,8 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "repo_labels", "labels" + add_foreign_key "repo_labels", "repos" add_foreign_key "repo_subscriptions", "repos" add_foreign_key "repo_subscriptions", "users" end diff --git a/lib/tasks/schedule.rake b/lib/tasks/schedule.rake index 393e534d6..7c2d85873 100644 --- a/lib/tasks/schedule.rake +++ b/lib/tasks/schedule.rake @@ -111,4 +111,11 @@ namespace :schedule do next unless Date.today.sunday? Rake::Task['sitemap:refresh'].invoke end + + desc 'fetch and assign labels for repos' + task fetch_labels_and_assign: :environment do + Repo.find_each(batch_size: 100) do |repo| + RepoLabelAssigner.new(repo: repo).create_and_associate_labels! + end + end end diff --git a/test/unit/repo_labels_assigner_test.rb b/test/unit/repo_labels_assigner_test.rb new file mode 100644 index 000000000..fbda22d09 --- /dev/null +++ b/test/unit/repo_labels_assigner_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'test_helper' + +class RepoLabelsAssignerTest < ActiveSupport::TestCase + test '#create_and_associate_labels! creates labels' do + repo = repos(:rails_rails) + VCR.use_cassette('fetch_labels_for_repo', record: :once) do + assigner = RepoLabelAssigner.new(repo: repo) + assert_difference('Label.count', 30) do + assigner.create_and_associate_labels! + end + end + end + + test '#create_and_associate_labels! it does not create existing labels' do + repo = repos(:rails_rails) + Label.create!(name: :autoloading) + VCR.use_cassette('fetch_labels_for_repo', record: :once) do + assigner = RepoLabelAssigner.new(repo: repo) + assigner.create_and_associate_labels! + assert_equal Label.where(name: :autoloading).count, 1 + end + end + + test '#create_and_associate_labels! it does not care about label case' do + repo = repos(:rails_rails) + VCR.use_cassette('fetch_labels_for_repo', record: :once) do + assigner = RepoLabelAssigner.new(repo: repo) + assigner.create_and_associate_labels! + assert_equal Label.where(name: :ActionMailer).count, 0 + end + end + + test '#create_and_associate_labels! associates labels with the repo' do + repo = repos(:rails_rails) + VCR.use_cassette('fetch_labels_for_repo', record: :once) do + assigner = RepoLabelAssigner.new(repo: repo) + assigner.create_and_associate_labels! + end + + assert_equal Label.count, RepoLabel.where(repo: repo).count + end +end diff --git a/test/vcr_cassettes/fetch_labels_for_repo.yml b/test/vcr_cassettes/fetch_labels_for_repo.yml new file mode 100644 index 000000000..3926d0d01 --- /dev/null +++ b/test/vcr_cassettes/fetch_labels_for_repo.yml @@ -0,0 +1,91 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.github.com/repos/rails/rails/labels + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/vnd.github.3.raw+json + User-Agent: + - 81fd8d500e69f5aea0fb8fa8337c73b8 + Authorization: + - token + response: + status: + code: 200 + message: OK + headers: + Server: + - GitHub.com + Date: + - Tue, 04 Oct 2022 14:00:06 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '5784' + Cache-Control: + - private, max-age=60, s-maxage=60 + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding, Accept, X-Requested-With + Etag: + - '"9e60597066fa8eff0c1230a7c7c94366bdf497b43d1442f01fa98a2f18cf4547"' + X-Oauth-Scopes: + - user:email + X-Accepted-Oauth-Scopes: + - repo + X-Oauth-Client-Id: + - "" + X-Github-Media-Type: + - github.v3; param=3.raw; format=json + Link: + - ; rel="next", ; + rel="last" + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4956' + X-Ratelimit-Reset: + - '1664894372' + X-Ratelimit-Used: + - '44' + X-Ratelimit-Resource: + - core + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Access-Control-Allow-Origin: + - "*" + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Frame-Options: + - deny + X-Content-Type-Options: + - nosniff + X-Xss-Protection: + - '0' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Content-Security-Policy: + - default-src 'none' + X-Github-Request-Id: + - D362:3CB7:1060BB6:2170F46:633C3C66 + body: + encoding: ASCII-8BIT + string: '[{"id":300028327,"node_id":"MDU6TGFiZWwzMDAwMjgzMjc=","url":"https://api.github.com/repos/rails/rails/labels/actioncable","name":"actioncable","color":"bfdadc","default":false,"description":null},{"id":1174770998,"node_id":"MDU6TGFiZWwxMTc0NzcwOTk4","url":"https://api.github.com/repos/rails/rails/labels/actionmailbox","name":"actionmailbox","color":"f4a6cb","default":false,"description":""},{"id":107188,"node_id":"MDU6TGFiZWwxMDcxODg=","url":"https://api.github.com/repos/rails/rails/labels/actionmailer","name":"ActionMailer","color":"8B00FC","default":false,"description":null},{"id":107189,"node_id":"MDU6TGFiZWwxMDcxODk=","url":"https://api.github.com/repos/rails/rails/labels/actionpack","name":"actionpack","color":"FFF700","default":false,"description":null},{"id":1180817762,"node_id":"MDU6TGFiZWwxMTgwODE3NzYy","url":"https://api.github.com/repos/rails/rails/labels/actiontext","name":"actiontext","color":"3bc667","default":false,"description":""},{"id":3666649,"node_id":"MDU6TGFiZWwzNjY2NjQ5","url":"https://api.github.com/repos/rails/rails/labels/actionview","name":"actionview","color":"d7e102","default":false,"description":null},{"id":123812746,"node_id":"MDU6TGFiZWwxMjM4MTI3NDY=","url":"https://api.github.com/repos/rails/rails/labels/activejob","name":"activejob","color":"5319e7","default":false,"description":null},{"id":107190,"node_id":"MDU6TGFiZWwxMDcxOTA=","url":"https://api.github.com/repos/rails/rails/labels/activemodel","name":"activemodel","color":"00E5FF","default":false,"description":null},{"id":107191,"node_id":"MDU6TGFiZWwxMDcxOTE=","url":"https://api.github.com/repos/rails/rails/labels/activerecord","name":"activerecord","color":"0b02e1","default":false,"description":null},{"id":664533972,"node_id":"MDU6TGFiZWw2NjQ1MzM5NzI=","url":"https://api.github.com/repos/rails/rails/labels/activestorage","name":"activestorage","color":"bfd4f2","default":false,"description":null},{"id":107194,"node_id":"MDU6TGFiZWwxMDcxOTQ=","url":"https://api.github.com/repos/rails/rails/labels/activesupport","name":"activesupport","color":"FC9300","default":false,"description":null},{"id":128693,"node_id":"MDU6TGFiZWwxMjg2OTM=","url":"https://api.github.com/repos/rails/rails/labels/asset%20pipeline","name":"asset + pipeline","color":"d7e102","default":false,"description":null},{"id":41328116,"node_id":"MDU6TGFiZWw0MTMyODExNg==","url":"https://api.github.com/repos/rails/rails/labels/attached%20PR","name":"attached + PR","color":"006b75","default":false,"description":null},{"id":1907985386,"node_id":"MDU6TGFiZWwxOTA3OTg1Mzg2","url":"https://api.github.com/repos/rails/rails/labels/autoloading","name":"autoloading","color":"f4ff5e","default":false,"description":""},{"id":776781281,"node_id":"MDU6TGFiZWw3NzY3ODEyODE=","url":"https://api.github.com/repos/rails/rails/labels/ci%20issues","name":"ci + issues","color":"aaafff","default":false,"description":null},{"id":150377,"node_id":"MDU6TGFiZWwxNTAzNzc=","url":"https://api.github.com/repos/rails/rails/labels/docs","name":"docs","color":"02d7e1","default":false,"description":null},{"id":4782967,"node_id":"MDU6TGFiZWw0NzgyOTY3","url":"https://api.github.com/repos/rails/rails/labels/engines","name":"engines","color":"e102d8","default":false,"description":null},{"id":126788695,"node_id":"MDU6TGFiZWwxMjY3ODg2OTU=","url":"https://api.github.com/repos/rails/rails/labels/enum","name":"enum","color":"d4c5f9","default":false,"description":null},{"id":384913768,"node_id":"MDU6TGFiZWwzODQ5MTM3Njg=","url":"https://api.github.com/repos/rails/rails/labels/good%20first%20issue","name":"good + first issue","color":"0e8a16","default":true,"description":""},{"id":202293,"node_id":"MDU6TGFiZWwyMDIyOTM=","url":"https://api.github.com/repos/rails/rails/labels/i18n","name":"i18n","color":"e102d8","default":false,"description":null},{"id":5944892,"node_id":"MDU6TGFiZWw1OTQ0ODky","url":"https://api.github.com/repos/rails/rails/labels/JRuby","name":"JRuby","color":"02d7e1","default":false,"description":null},{"id":3451328742,"node_id":"LA_kwDNIULOzbcY5g","url":"https://api.github.com/repos/rails/rails/labels/Missing%20changelog","name":"Missing + changelog","color":"AEB90A","default":false,"description":""},{"id":1071962662,"node_id":"MDU6TGFiZWwxMDcxOTYyNjYy","url":"https://api.github.com/repos/rails/rails/labels/more-information-needed","name":"more-information-needed","color":"bfdadc","default":false,"description":"When + reporter needs to provide more information"},{"id":309390193,"node_id":"MDU6TGFiZWwzMDkzOTAxOTM=","url":"https://api.github.com/repos/rails/rails/labels/MySQL","name":"MySQL","color":"fef2c0","default":false,"description":null},{"id":3451848882,"node_id":"LA_kwDNIULOzb8Isg","url":"https://api.github.com/repos/rails/rails/labels/need%20rebase","name":"need + rebase","color":"5D2996","default":false,"description":""},{"id":377277835,"node_id":"MDU6TGFiZWwzNzcyNzc4MzU=","url":"https://api.github.com/repos/rails/rails/labels/needs%20backport","name":"needs + backport","color":"f9d0c4","default":false,"description":null},{"id":128692,"node_id":"MDU6TGFiZWwxMjg2OTI=","url":"https://api.github.com/repos/rails/rails/labels/needs%20feedback","name":"needs + feedback","color":"ededed","default":false,"description":null},{"id":257468118,"node_id":"MDU6TGFiZWwyNTc0NjgxMTg=","url":"https://api.github.com/repos/rails/rails/labels/needs%20work","name":"needs + work","color":"000000","default":false,"description":null},{"id":2036522896,"node_id":"MDU6TGFiZWwyMDM2NTIyODk2","url":"https://api.github.com/repos/rails/rails/labels/onboarding","name":"onboarding","color":"ddf76a","default":false,"description":""},{"id":4982956,"node_id":"MDU6TGFiZWw0OTgyOTU2","url":"https://api.github.com/repos/rails/rails/labels/onhold","name":"onhold","color":"ededed","default":false,"description":null}]' + recorded_at: Tue, 04 Oct 2022 14:00:06 GMT +recorded_with: VCR 6.0.0