From 3f7e988aeecdb816f1a1ce898c419f5cab57d3a8 Mon Sep 17 00:00:00 2001 From: Vadser Date: Thu, 22 Aug 2024 12:17:26 +0200 Subject: [PATCH 1/9] Add service for fetching Github PR data --- Gemfile | 2 + Gemfile.lock | 16 ++- app/lib/github/client.rb | 33 +++++++ .../fetch_github_pull_request_data_service.rb | 97 +++++++++++++++++++ config/credentials.yml.enc | 2 +- 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 app/lib/github/client.rb create mode 100644 app/services/fetch_github_pull_request_data_service.rb diff --git a/Gemfile b/Gemfile index 25a5271..dbf33ff 100644 --- a/Gemfile +++ b/Gemfile @@ -40,3 +40,5 @@ group :development, :test do gem "rubocop-rails-omakase", require: false gem "rspec-rails", "~> 6.1.0" end + +gem "rest-client", "~> 2.1" diff --git a/Gemfile.lock b/Gemfile.lock index f48dba0..c051181 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,10 +87,14 @@ GEM irb (~> 1.10) reline (>= 0.3.8) diff-lcs (1.5.1) + domain_name (0.6.20240107) drb (2.2.1) erubi (1.13.0) globalid (1.2.1) activesupport (>= 6.1) + http-accept (1.7.0) + http-cookie (1.0.7) + domain_name (~> 0.5) i18n (1.14.5) concurrent-ruby (~> 1.0) io-console (0.7.2) @@ -109,6 +113,9 @@ GEM net-pop net-smtp marcel (1.0.4) + mime-types (3.5.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2024.0820) mini_mime (1.1.5) minitest (5.25.1) msgpack (1.7.2) @@ -121,6 +128,7 @@ GEM timeout net-smtp (0.5.0) net-protocol + netrc (0.11.0) nio4r (2.7.3) nokogiri (1.16.7-aarch64-linux) racc (~> 1.4) @@ -187,6 +195,11 @@ GEM regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rexml (3.3.6) strscan rspec-core (3.13.0) @@ -283,10 +296,11 @@ DEPENDENCIES debug puma (>= 5.0) rails (~> 7.2.0) + rest-client (~> 2.1) rspec-rails (~> 6.1.0) rubocop-rails-omakase sqlite3 (>= 1.4) tzinfo-data BUNDLED WITH - 2.5.17 + 2.5.3 diff --git a/app/lib/github/client.rb b/app/lib/github/client.rb new file mode 100644 index 0000000..44f74d6 --- /dev/null +++ b/app/lib/github/client.rb @@ -0,0 +1,33 @@ +class Github::Client + BASE_URL = 'https://api.github.com' + + def send_get_request(url) + RestClient.get(url, headers) + rescue RestClient::ExceptionWithResponse => e + e.response + end + + private + + def parse_pull_request_url(url) + pattern = %r{\Ahttps://github\.com/(?[^/]+)/(?[^/]+)/pull/(?\d+)\z} + + if match_data = url.match(pattern) + owner = match_data[:owner] + repo_name = match_data[:repo] + pull_request_number = match_data[:pr_number] + + { owner: owner, repo_name: repo_name, pull_request_number: pull_request_number } + else + raise ArgumentError, "Invalid GitHub pull request URL" + end + end + + def headers + @_headers ||= { + 'X-GitHub-Api-Version' => '2022-11-28', + 'Authorization' => "Bearer #{Rails.application.credentials.github.api_token}", + 'Accept' => 'application/vnd.github+json' + } + end +end diff --git a/app/services/fetch_github_pull_request_data_service.rb b/app/services/fetch_github_pull_request_data_service.rb new file mode 100644 index 0000000..c28d148 --- /dev/null +++ b/app/services/fetch_github_pull_request_data_service.rb @@ -0,0 +1,97 @@ +class FetchGithubPullRequestDataService + attr_reader :pull_request_url, :client + GITHUB_BASE_URL = 'https://api.github.com' + + def initialize(pull_request_url) + @pull_request_url = pull_request_url + @client = Github::Client.new + end + + def call + data = parse_pull_request_url + pr_data = fetch_pull_request_data(data) + + return unless pr_data + + diff_data = fetch_diff_data(pr_data[:diff_url]) + commits_data = fetch_commits_data(pr_data[:commits_url]) + comments_data = fetch_comments_data(pr_data[:review_comments_url]) + + assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data) + end + + private + + def parse_pull_request_url + pattern = %r{\Ahttps://github\.com/(?[^/]+)/(?[^/]+)/pull/(?\d+)\z} + + match_data = pull_request_url.match(pattern) + raise ArgumentError, 'Invalid GitHub pull request URL' unless match_data + + { + owner: match_data[:owner], + repo_name: match_data[:repo], + pull_request_number: match_data[:pr_number] + } + end + + def fetch_pull_request_data(data) + response = client.send_get_request("#{GITHUB_BASE_URL}/repos/#{data[:owner]}/#{data[:repo_name]}/pulls/#{data[:pull_request_number]}") + return nil unless response.code == 200 + + JSON.parse(response.body).deep_symbolize_keys + end + + def fetch_diff_data(diff_url) + response = client.send_get_request(diff_url) + response.code == 200 ? response.body : '' + end + + def fetch_commits_data(commits_url) + response = client.send_get_request(commits_url) + return [] unless response.code == 200 + + JSON.parse(response.body).map do |commit| + { + sha: commit['sha'], + author: commit['commit']['author'].slice('name', 'email'), + message: commit['commit']['message'], + date: commit['commit']['author']['date'] + } + end + end + + def fetch_comments_data(comments_url) + response = client.send_get_request(comments_url) + return [] unless response.code == 200 + + JSON.parse(response.body).map do |comment| + { + body: comment['body'], + id: comment['id'], + diff_hunk: comment['diff_hunk'], + path: comment['path'], + user: comment['user']['login'], + created_at: comment['created_at'], + updated_at: comment['updated_at'], + reactions: comment['reactions'].except('url') + } + end + end + + def assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data) + { + url: pr_data[:html_url], + title: pr_data[:title], + body: pr_data[:body], + repo_owner: pr_data[:head][:repo][:owner][:login], + repo: pr_data[:head][:repo][:name], + creator: pr_data[:user][:login], + comments: comments_data, + commits: commits_data, + diff: diff_data, + created_at: pr_data[:created_at], + updated_at: pr_data[:updated_at] + } + end +end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 82337d6..7141b0f 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -t/Y066Fp5O76svbHfSbuw06fluX1zik2MjqjgrgO/2iB+LFh7jPddOdgN+SWeoximSDj12i2ExiTWuhgK5j9dJrgbus81QkY0SZXpxuXWRtwrPVb2FXZVJ9cvYToiG3TtkLYxTPhqrU8yzJiRH5LDulqvHNO4hPfWFXXAj11UDFtEHuiyE/A2jLnpTkKJLeH9toumcRO1d6ppepY5bf+xMzCGA8Js/YRxGuqElofXExspc9vGnuPX4Fi8A7bzi8WnpJ+0oax+VSpUvxqv3d5Hkbga4C9x1eITcNR1nwbYezGPi2z4DoFti9SSZsGQaZAJVY3bt9fuREaCWrFzoJhMB0Eab8JEDpsYJR+taWX/nfrCRcbN4ms5VVGLZB8on1WjvOyHJvtaihhfzd0tVrCRnVMt/T7--Cde6kiXZZMTL+Xr8--lmK43gzy8/wyDxfGPOd8xQ== \ No newline at end of file +/DHwqsNHUbO+v/Gw7Nhq+j3ufU3ffcc6zHsxIy0rO6qROENCFQG/mEU/GUNNhx44KbZEdNBpVKXeirbv6Tg8pYT5pO6zpYhQYhWbG+hl30a4cF0UDzZU8v0dLOnerhQxAXX4pPa04avG1VSyzsoboC66h/y+6DSbt7zrGbCaH38/Zse6MunjFpEKC39tXRBGcCiFYSPM/3xEoZau+iPefsybq4yPR+Kq6HkerfuweJwYFAULShGGNNnr8ZVqJbb2D8IvmVAvQS2OmdSVATfsei5FCPEt/YAioqsh2ybcqIK8E/5z7SvoeUQCwAPhyUg0BBFj7VQGSxAwkt+8wKLQ0uOQNFYC54hOS9DBp0EZL3d0jY+Oz138PqvYoleNVgEdWdoTP7jDEYhr0Z7gogfbYIMgA4w+qxxqkZTtFaVN/cjJCD4hsICxAp1GcIqD2hE6jh6hD/OEtnSINdIWNt3YVqu3c6Rdh4kQj3n9Pp+LE5Q4vRJMGnZIFkVU/2tBzYZN/RPpDXVdX6rP4+w4bkBtPoD5njyTQptRvPwHrKiS06d6mmAXxd1a--aqV86+5maj3bza49--KX5CzjSSYPMrf5beOY9I7w== \ No newline at end of file From 4abaa2785e58fef652d926d0407369238ab11669 Mon Sep 17 00:00:00 2001 From: Vadser Date: Thu, 22 Aug 2024 12:24:07 +0200 Subject: [PATCH 2/9] Add pull request model --- app/models/pull_request.rb | 2 ++ .../fetch_github_pull_request_data_service.rb | 6 ++-- config/credentials.yml.enc | 2 +- .../20240822102014_create_pull_requests.rb | 19 ++++++++++++ db/schema.rb | 29 +++++++++++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 app/models/pull_request.rb create mode 100644 db/migrate/20240822102014_create_pull_requests.rb create mode 100644 db/schema.rb diff --git a/app/models/pull_request.rb b/app/models/pull_request.rb new file mode 100644 index 0000000..ac74de8 --- /dev/null +++ b/app/models/pull_request.rb @@ -0,0 +1,2 @@ +class PullRequest < ApplicationRecord +end diff --git a/app/services/fetch_github_pull_request_data_service.rb b/app/services/fetch_github_pull_request_data_service.rb index c28d148..678a976 100644 --- a/app/services/fetch_github_pull_request_data_service.rb +++ b/app/services/fetch_github_pull_request_data_service.rb @@ -17,7 +17,7 @@ def call commits_data = fetch_commits_data(pr_data[:commits_url]) comments_data = fetch_comments_data(pr_data[:review_comments_url]) - assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data) + PullRequest.create(assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data)) end private @@ -90,8 +90,8 @@ def assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data) comments: comments_data, commits: commits_data, diff: diff_data, - created_at: pr_data[:created_at], - updated_at: pr_data[:updated_at] + pr_created_at: pr_data[:created_at], + pr_updated_at: pr_data[:updated_at] } end end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 7141b0f..5a2c901 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -/DHwqsNHUbO+v/Gw7Nhq+j3ufU3ffcc6zHsxIy0rO6qROENCFQG/mEU/GUNNhx44KbZEdNBpVKXeirbv6Tg8pYT5pO6zpYhQYhWbG+hl30a4cF0UDzZU8v0dLOnerhQxAXX4pPa04avG1VSyzsoboC66h/y+6DSbt7zrGbCaH38/Zse6MunjFpEKC39tXRBGcCiFYSPM/3xEoZau+iPefsybq4yPR+Kq6HkerfuweJwYFAULShGGNNnr8ZVqJbb2D8IvmVAvQS2OmdSVATfsei5FCPEt/YAioqsh2ybcqIK8E/5z7SvoeUQCwAPhyUg0BBFj7VQGSxAwkt+8wKLQ0uOQNFYC54hOS9DBp0EZL3d0jY+Oz138PqvYoleNVgEdWdoTP7jDEYhr0Z7gogfbYIMgA4w+qxxqkZTtFaVN/cjJCD4hsICxAp1GcIqD2hE6jh6hD/OEtnSINdIWNt3YVqu3c6Rdh4kQj3n9Pp+LE5Q4vRJMGnZIFkVU/2tBzYZN/RPpDXVdX6rP4+w4bkBtPoD5njyTQptRvPwHrKiS06d6mmAXxd1a--aqV86+5maj3bza49--KX5CzjSSYPMrf5beOY9I7w== \ No newline at end of file +zdXnhs4q2NxhuciKxZn6KEpUK2v+kwlphY5XyUWqQas7aycFLA+e4B4WDqWprW9NA1ZiYPURuglIplIVAQjHUFh7+xHbsUcWTVr5DEp+guj04hIHzNNmnuaWE1ha7qVcqAz3oejhwXR/COyAWqUDyh/XCkINuOQTdp5oV6htZ5n7fjrq8ctFy8iJ6L9AXDq5Ka+ZasVTmll5/l899vGly7ecoXaE3Ra0l+R20F2kEaom6XwRxehgtDN+WUMzqYazsCUhwyEefTKtzzVJSfmbwnSWgMk+mfgcyGZQ1LHKrOeYafa9W3JSYO4bmQR/O0z06J2OSm21OAdw7hkByS3T+jDb8wVotSFRtSq2HDBTSbr3y21tV569K9deg3pp253aPbS/Nk27o9w0E9d0X6vCNXl6e7jofvVln7XWKwrK1xUnG0TWxleHR1Fogni9V+PI2HrDr0q1QtuvSeqpI/VTwx5dmH/BAHqq6See8zVLP4/14O5d7qfQzlS19C420By8vXtNcplzzloTGMiKbCzbsn3dzQsgnijUWWoNdd9L0ksrgEMn0EK5--4CPKoh4crqcTEyUh--ajYgrnaCjCB8rTT6qGBXTw== \ No newline at end of file diff --git a/db/migrate/20240822102014_create_pull_requests.rb b/db/migrate/20240822102014_create_pull_requests.rb new file mode 100644 index 0000000..7d23044 --- /dev/null +++ b/db/migrate/20240822102014_create_pull_requests.rb @@ -0,0 +1,19 @@ +class CreatePullRequests < ActiveRecord::Migration[7.2] + def change + create_table :pull_requests do |t| + t.string :url + t.string :title + t.text :body + t.string :repo_owner + t.string :repo + t.string :creator + t.json :comments + t.json :commits + t.text :diff + t.datetime :pr_created_at + t.datetime :pr_updated_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..bd3cd5c --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,29 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 2024_08_22_102014) do + create_table "pull_requests", force: :cascade do |t| + t.string "url" + t.string "title" + t.text "body" + t.string "repo_owner" + t.string "repo" + t.string "creator" + t.json "comments" + t.json "commits" + t.text "diff" + t.datetime "pr_created_at" + t.datetime "pr_updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end +end From fc1ce3511adb591317d9941cd0504eb4e022bb02 Mon Sep 17 00:00:00 2001 From: Vadser Date: Fri, 23 Aug 2024 11:38:36 +0200 Subject: [PATCH 3/9] Add specs --- Gemfile | 4 + Gemfile.lock | 8 + app/lib/github/client.rb | 33 ----- .../fetch_github_pull_request_data_service.rb | 30 ++-- config/application.rb | 1 + config/credentials.yml.enc | 1 - lib/github/client.rb | 35 +++++ spec/lib/github/client_spec.rb | 44 ++++++ ...h_github_pull_request_data_service_spec.rb | 140 ++++++++++++++++++ 9 files changed, 247 insertions(+), 49 deletions(-) delete mode 100644 app/lib/github/client.rb delete mode 100644 config/credentials.yml.enc create mode 100644 lib/github/client.rb create mode 100644 spec/lib/github/client_spec.rb create mode 100644 spec/services /fetch_github_pull_request_data_service_spec.rb diff --git a/Gemfile b/Gemfile index dbf33ff..5423c44 100644 --- a/Gemfile +++ b/Gemfile @@ -42,3 +42,7 @@ group :development, :test do end gem "rest-client", "~> 2.1" + +gem "dotenv", "~> 3.1" + +gem "pry", "~> 0.14.2" diff --git a/Gemfile.lock b/Gemfile.lock index c051181..77eb561 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,6 +79,7 @@ GEM brakeman (6.1.2) racc builder (3.3.0) + coderay (1.1.3) concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) @@ -88,6 +89,7 @@ GEM reline (>= 0.3.8) diff-lcs (1.5.1) domain_name (0.6.20240107) + dotenv (3.1.2) drb (2.2.1) erubi (1.13.0) globalid (1.2.1) @@ -113,6 +115,7 @@ GEM net-pop net-smtp marcel (1.0.4) + method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2024.0820) @@ -146,6 +149,9 @@ GEM parser (3.3.4.2) ast (~> 2.4.1) racc + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) psych (5.1.2) stringio puma (6.4.2) @@ -294,6 +300,8 @@ DEPENDENCIES bootsnap brakeman debug + dotenv (~> 3.1) + pry (~> 0.14.2) puma (>= 5.0) rails (~> 7.2.0) rest-client (~> 2.1) diff --git a/app/lib/github/client.rb b/app/lib/github/client.rb deleted file mode 100644 index 44f74d6..0000000 --- a/app/lib/github/client.rb +++ /dev/null @@ -1,33 +0,0 @@ -class Github::Client - BASE_URL = 'https://api.github.com' - - def send_get_request(url) - RestClient.get(url, headers) - rescue RestClient::ExceptionWithResponse => e - e.response - end - - private - - def parse_pull_request_url(url) - pattern = %r{\Ahttps://github\.com/(?[^/]+)/(?[^/]+)/pull/(?\d+)\z} - - if match_data = url.match(pattern) - owner = match_data[:owner] - repo_name = match_data[:repo] - pull_request_number = match_data[:pr_number] - - { owner: owner, repo_name: repo_name, pull_request_number: pull_request_number } - else - raise ArgumentError, "Invalid GitHub pull request URL" - end - end - - def headers - @_headers ||= { - 'X-GitHub-Api-Version' => '2022-11-28', - 'Authorization' => "Bearer #{Rails.application.credentials.github.api_token}", - 'Accept' => 'application/vnd.github+json' - } - end -end diff --git a/app/services/fetch_github_pull_request_data_service.rb b/app/services/fetch_github_pull_request_data_service.rb index 678a976..a6d199f 100644 --- a/app/services/fetch_github_pull_request_data_service.rb +++ b/app/services/fetch_github_pull_request_data_service.rb @@ -1,6 +1,6 @@ class FetchGithubPullRequestDataService attr_reader :pull_request_url, :client - GITHUB_BASE_URL = 'https://api.github.com' + GITHUB_BASE_URL = "https://api.github.com" def initialize(pull_request_url) @pull_request_url = pull_request_url @@ -26,7 +26,7 @@ def parse_pull_request_url pattern = %r{\Ahttps://github\.com/(?[^/]+)/(?[^/]+)/pull/(?\d+)\z} match_data = pull_request_url.match(pattern) - raise ArgumentError, 'Invalid GitHub pull request URL' unless match_data + raise ArgumentError, "Invalid GitHub pull request URL" unless match_data { owner: match_data[:owner], @@ -44,7 +44,7 @@ def fetch_pull_request_data(data) def fetch_diff_data(diff_url) response = client.send_get_request(diff_url) - response.code == 200 ? response.body : '' + response.code == 200 ? response.body : "" end def fetch_commits_data(commits_url) @@ -53,10 +53,10 @@ def fetch_commits_data(commits_url) JSON.parse(response.body).map do |commit| { - sha: commit['sha'], - author: commit['commit']['author'].slice('name', 'email'), - message: commit['commit']['message'], - date: commit['commit']['author']['date'] + sha: commit["sha"], + author: commit["commit"]["author"].slice("name", "email"), + message: commit["commit"]["message"], + date: commit["commit"]["author"]["date"] } end end @@ -67,14 +67,14 @@ def fetch_comments_data(comments_url) JSON.parse(response.body).map do |comment| { - body: comment['body'], - id: comment['id'], - diff_hunk: comment['diff_hunk'], - path: comment['path'], - user: comment['user']['login'], - created_at: comment['created_at'], - updated_at: comment['updated_at'], - reactions: comment['reactions'].except('url') + body: comment["body"], + id: comment["id"], + diff_hunk: comment["diff_hunk"], + path: comment["path"], + user: comment["user"]["login"], + created_at: comment["created_at"], + updated_at: comment["updated_at"], + reactions: comment["reactions"].except("url") } end end diff --git a/config/application.rb b/config/application.rb index b74c5bf..79d0501 100644 --- a/config/application.rb +++ b/config/application.rb @@ -17,6 +17,7 @@ # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) +Dotenv.load module BlameAi class Application < Rails::Application diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index 5a2c901..0000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -zdXnhs4q2NxhuciKxZn6KEpUK2v+kwlphY5XyUWqQas7aycFLA+e4B4WDqWprW9NA1ZiYPURuglIplIVAQjHUFh7+xHbsUcWTVr5DEp+guj04hIHzNNmnuaWE1ha7qVcqAz3oejhwXR/COyAWqUDyh/XCkINuOQTdp5oV6htZ5n7fjrq8ctFy8iJ6L9AXDq5Ka+ZasVTmll5/l899vGly7ecoXaE3Ra0l+R20F2kEaom6XwRxehgtDN+WUMzqYazsCUhwyEefTKtzzVJSfmbwnSWgMk+mfgcyGZQ1LHKrOeYafa9W3JSYO4bmQR/O0z06J2OSm21OAdw7hkByS3T+jDb8wVotSFRtSq2HDBTSbr3y21tV569K9deg3pp253aPbS/Nk27o9w0E9d0X6vCNXl6e7jofvVln7XWKwrK1xUnG0TWxleHR1Fogni9V+PI2HrDr0q1QtuvSeqpI/VTwx5dmH/BAHqq6See8zVLP4/14O5d7qfQzlS19C420By8vXtNcplzzloTGMiKbCzbsn3dzQsgnijUWWoNdd9L0ksrgEMn0EK5--4CPKoh4crqcTEyUh--ajYgrnaCjCB8rTT6qGBXTw== \ No newline at end of file diff --git a/lib/github/client.rb b/lib/github/client.rb new file mode 100644 index 0000000..70b8dc0 --- /dev/null +++ b/lib/github/client.rb @@ -0,0 +1,35 @@ +module Github + class Client + BASE_URL = "https://api.github.com" + + def send_get_request(url) + RestClient.get(url, headers) + rescue RestClient::ExceptionWithResponse => e + e.response + end + + private + + def parse_pull_request_url(url) + pattern = %r{\Ahttps://github\.com/(?[^/]+)/(?[^/]+)/pull/(?\d+)\z} + + if match_data = url.match(pattern) + owner = match_data[:owner] + repo_name = match_data[:repo] + pull_request_number = match_data[:pr_number] + + { owner: owner, repo_name: repo_name, pull_request_number: pull_request_number } + else + raise ArgumentError, "Invalid GitHub pull request URL" + end + end + + def headers + @_headers ||= { + "X-GitHub-Api-Version" => "2022-11-28", + "Authorization" => "Bearer #{ENV['GITHUB_API_TOKEN']}", + "Accept" => "application/vnd.github+json" + } + end + end +end diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb new file mode 100644 index 0000000..7d876fc --- /dev/null +++ b/spec/lib/github/client_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' +require 'rest-client' +require 'github/client' + +RSpec.describe Github::Client do + let(:client) { described_class.new } + let(:valid_url) { "https://github.com/owner/repo/pull/123" } + let(:invalid_url) { "https://invalid.com/owner/repo/pull/123" } + + describe "#send_get_request" do + let(:url) { "https://api.github.com/some_endpoint" } + let(:response) { double("response") } + + context "when the request is successful" do + before do + allow(RestClient).to receive(:get).and_return(response) + end + + it "sends a GET request to the given URL" do + expect(RestClient).to receive(:get).with(url, anything) + client.send_get_request(url) + end + + it "returns the response" do + result = client.send_get_request(url) + expect(result).to eq(response) + end + end + + context "when the request fails" do + let(:error_response) { double("error_response") } + let(:exception) { RestClient::ExceptionWithResponse.new(error_response) } + + before do + allow(RestClient).to receive(:get).and_raise(exception) + end + + it "rescues the exception and returns the response" do + result = client.send_get_request(url) + expect(result).to eq(error_response) + end + end + end +end diff --git a/spec/services /fetch_github_pull_request_data_service_spec.rb b/spec/services /fetch_github_pull_request_data_service_spec.rb new file mode 100644 index 0000000..8e376a8 --- /dev/null +++ b/spec/services /fetch_github_pull_request_data_service_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' +require 'json' + +RSpec.describe FetchGithubPullRequestDataService do + let(:pull_request_url) { "https://github.com/owner/repo/pull/123" } + let(:pull_request_api_url) { "https://api.github.com/repos/owner/repo/pulls/123" } + let(:service) { described_class.new(pull_request_url) } + let(:client) { instance_double(Github::Client) } + + let(:pr_data) do + { + html_url: pull_request_url, + title: "Example Pull Request", + body: "This is a test PR", + head: { + repo: { + owner: { login: "owner" }, + name: "repo" + } + }, + user: { login: "creator" }, + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-02T00:00:00Z", + diff_url: "https://github.com/owner/repo/pull/123.diff", + commits_url: "https://api.github.com/repos/owner/repo/pulls/123/commits", + review_comments_url: "https://api.github.com/repos/owner/repo/pulls/123/comments" + } + end + + let(:diff_data) { "diff --git a/file.txt b/file.txt\nindex 83db48f..a8d38ed 100644\n--- a/file.txt\n+++ b/file.txt\n" } + + let(:commits_data) do + [ + { + "sha" => "abc123", + "commit" => { + "author" => { + "name" => "John Doe", + "email" => "john.doe@example.com", + "date" => "2023-01-01T12:00:00Z" + } + } + } + ] + end + + let(:comments_data) do + [ + { + "body" => "This is a comment", + "id" => 1, + "diff_hunk" => "@@ -0,0 +1,2 @@", + "path" => "file.txt", + "user" => { "login" => "commenter" }, + "created_at" => "2023-01-01T12:30:00Z", + "updated_at" => "2023-01-01T12:45:00Z", + "reactions" => { "+1" => 1, "-1" => 0 } + } + ] + end + + before do + allow(Github::Client).to receive(:new).and_return(client) + end + + describe "#call" do + context "when all data is successfully fetched" do + before do + allow(client).to receive(:send_get_request) + .with(pull_request_api_url) + .and_return(instance_double(RestClient::Response, code: 200, body: pr_data.to_json)) + + allow(client).to receive(:send_get_request) + .with(pr_data[:diff_url]) + .and_return(instance_double(RestClient::Response, code: 200, body: diff_data)) + + allow(client).to receive(:send_get_request) + .with(pr_data[:commits_url]) + .and_return(instance_double(RestClient::Response, code: 200, body: commits_data.to_json)) + + allow(client).to receive(:send_get_request) + .with(pr_data[:review_comments_url]) + .and_return(instance_double(RestClient::Response, code: 200, body: comments_data.to_json)) + end + + it "creates a PullRequest with the correct data" do + expected_data = { + url: pr_data[:html_url], + title: pr_data[:title], + body: pr_data[:body], + repo_owner: pr_data[:head][:repo][:owner][:login], + repo: pr_data[:head][:repo][:name], + creator: pr_data[:user][:login], + comments: comments_data.map do |comment| + { + body: comment["body"], + id: comment["id"], + diff_hunk: comment["diff_hunk"], + path: comment["path"], + user: comment["user"]["login"], + created_at: comment["created_at"], + updated_at: comment["updated_at"], + reactions: comment["reactions"].except("url") + } + end, + commits: commits_data.map do |commit| + { + sha: commit["sha"], + author: { + 'name' => commit["commit"]["author"]["name"], + 'email' => commit["commit"]["author"]["email"] + }, + message: commit["commit"]["message"], + date: commit["commit"]["author"]["date"] + } + end, + diff: diff_data, + pr_created_at: pr_data[:created_at], + pr_updated_at: pr_data[:updated_at] + } + + expect(PullRequest).to receive(:create).with(expected_data) + service.call + end + end + + context "when pull request data cannot be fetched" do + before do + allow(client).to receive(:send_get_request) + .with(pull_request_api_url) + .and_return(instance_double(RestClient::Response, code: 404, body: "{}")) + end + + it "does not create a PullRequest" do + expect(PullRequest).not_to receive(:create) + service.call + end + end + end +end From a7e928dcd63da4638a2df57b4d7819f8c5fd24a2 Mon Sep 17 00:00:00 2001 From: Vadser Date: Fri, 23 Aug 2024 14:38:24 +0200 Subject: [PATCH 4/9] Remove pry gem --- Gemfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index 5423c44..c048854 100644 --- a/Gemfile +++ b/Gemfile @@ -44,5 +44,3 @@ end gem "rest-client", "~> 2.1" gem "dotenv", "~> 3.1" - -gem "pry", "~> 0.14.2" From b3d0a6fd5ebca393477c2717aa69d7eda6c3c052 Mon Sep 17 00:00:00 2001 From: Vadser Date: Fri, 23 Aug 2024 14:47:34 +0200 Subject: [PATCH 5/9] Add error handling for fetch pr data service class --- Gemfile.lock | 6 ------ .../fetch_github_pull_request_data_service.rb | 16 ++++++++++------ ...etch_github_pull_request_data_service_spec.rb | 6 +++--- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 77eb561..ffe7c90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,7 +79,6 @@ GEM brakeman (6.1.2) racc builder (3.3.0) - coderay (1.1.3) concurrent-ruby (1.3.4) connection_pool (2.4.1) crass (1.0.6) @@ -115,7 +114,6 @@ GEM net-pop net-smtp marcel (1.0.4) - method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2024.0820) @@ -149,9 +147,6 @@ GEM parser (3.3.4.2) ast (~> 2.4.1) racc - pry (0.14.2) - coderay (~> 1.1) - method_source (~> 1.0) psych (5.1.2) stringio puma (6.4.2) @@ -301,7 +296,6 @@ DEPENDENCIES brakeman debug dotenv (~> 3.1) - pry (~> 0.14.2) puma (>= 5.0) rails (~> 7.2.0) rest-client (~> 2.1) diff --git a/app/services/fetch_github_pull_request_data_service.rb b/app/services/fetch_github_pull_request_data_service.rb index a6d199f..6222aac 100644 --- a/app/services/fetch_github_pull_request_data_service.rb +++ b/app/services/fetch_github_pull_request_data_service.rb @@ -11,13 +11,13 @@ def call data = parse_pull_request_url pr_data = fetch_pull_request_data(data) - return unless pr_data - diff_data = fetch_diff_data(pr_data[:diff_url]) commits_data = fetch_commits_data(pr_data[:commits_url]) comments_data = fetch_comments_data(pr_data[:review_comments_url]) - PullRequest.create(assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data)) + { status: :ok, pull_request: PullRequest.create(assemble_pull_request_data(pr_data, diff_data, commits_data, comments_data)) } + rescue StandardError => e + { status: :failed, message: e.message } end private @@ -26,6 +26,7 @@ def parse_pull_request_url pattern = %r{\Ahttps://github\.com/(?[^/]+)/(?[^/]+)/pull/(?\d+)\z} match_data = pull_request_url.match(pattern) + raise ArgumentError, "Invalid GitHub pull request URL" unless match_data { @@ -37,7 +38,8 @@ def parse_pull_request_url def fetch_pull_request_data(data) response = client.send_get_request("#{GITHUB_BASE_URL}/repos/#{data[:owner]}/#{data[:repo_name]}/pulls/#{data[:pull_request_number]}") - return nil unless response.code == 200 + + raise StandardError, JSON.parse(response.body)["message"] unless response.code == 200 JSON.parse(response.body).deep_symbolize_keys end @@ -49,7 +51,8 @@ def fetch_diff_data(diff_url) def fetch_commits_data(commits_url) response = client.send_get_request(commits_url) - return [] unless response.code == 200 + + raise StandardError, JSON.parse(response.body)["message"] unless response.code == 200 JSON.parse(response.body).map do |commit| { @@ -63,7 +66,8 @@ def fetch_commits_data(commits_url) def fetch_comments_data(comments_url) response = client.send_get_request(comments_url) - return [] unless response.code == 200 + + raise StandardError, JSON.parse(response.body)["message"] unless response.code == 200 JSON.parse(response.body).map do |comment| { diff --git a/spec/services /fetch_github_pull_request_data_service_spec.rb b/spec/services /fetch_github_pull_request_data_service_spec.rb index 8e376a8..5bbca70 100644 --- a/spec/services /fetch_github_pull_request_data_service_spec.rb +++ b/spec/services /fetch_github_pull_request_data_service_spec.rb @@ -120,7 +120,7 @@ } expect(PullRequest).to receive(:create).with(expected_data) - service.call + expect(service.call[:status]).to eq(:ok) end end @@ -128,12 +128,12 @@ before do allow(client).to receive(:send_get_request) .with(pull_request_api_url) - .and_return(instance_double(RestClient::Response, code: 404, body: "{}")) + .and_return(instance_double(RestClient::Response, code: 404, body: { message: "Not found" }.to_json)) end it "does not create a PullRequest" do expect(PullRequest).not_to receive(:create) - service.call + expect(service.call).to eq({ status: :failed, message: "Not found" }) end end end From d913e41f8dc03b26a823ff77de71d9285d2695b2 Mon Sep 17 00:00:00 2001 From: Vadser Date: Mon, 26 Aug 2024 11:17:33 +0200 Subject: [PATCH 6/9] Remove base url from service class --- app/services/fetch_github_pull_request_data_service.rb | 9 ++++----- lib/github/client.rb | 4 ++-- spec/lib/github/client_spec.rb | 7 ++++--- .../fetch_github_pull_request_data_service_spec.rb | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/services/fetch_github_pull_request_data_service.rb b/app/services/fetch_github_pull_request_data_service.rb index 6222aac..6009165 100644 --- a/app/services/fetch_github_pull_request_data_service.rb +++ b/app/services/fetch_github_pull_request_data_service.rb @@ -1,6 +1,5 @@ class FetchGithubPullRequestDataService attr_reader :pull_request_url, :client - GITHUB_BASE_URL = "https://api.github.com" def initialize(pull_request_url) @pull_request_url = pull_request_url @@ -37,7 +36,7 @@ def parse_pull_request_url end def fetch_pull_request_data(data) - response = client.send_get_request("#{GITHUB_BASE_URL}/repos/#{data[:owner]}/#{data[:repo_name]}/pulls/#{data[:pull_request_number]}") + response = client.send_get_request("/repos/#{data[:owner]}/#{data[:repo_name]}/pulls/#{data[:pull_request_number]}") raise StandardError, JSON.parse(response.body)["message"] unless response.code == 200 @@ -45,12 +44,12 @@ def fetch_pull_request_data(data) end def fetch_diff_data(diff_url) - response = client.send_get_request(diff_url) + response = client.send_get_request(URI(diff_url).path) response.code == 200 ? response.body : "" end def fetch_commits_data(commits_url) - response = client.send_get_request(commits_url) + response = client.send_get_request(URI(commits_url).path) raise StandardError, JSON.parse(response.body)["message"] unless response.code == 200 @@ -65,7 +64,7 @@ def fetch_commits_data(commits_url) end def fetch_comments_data(comments_url) - response = client.send_get_request(comments_url) + response = client.send_get_request(URI(comments_url).path) raise StandardError, JSON.parse(response.body)["message"] unless response.code == 200 diff --git a/lib/github/client.rb b/lib/github/client.rb index 70b8dc0..025c781 100644 --- a/lib/github/client.rb +++ b/lib/github/client.rb @@ -2,8 +2,8 @@ module Github class Client BASE_URL = "https://api.github.com" - def send_get_request(url) - RestClient.get(url, headers) + def send_get_request(path) + RestClient.get("#{BASE_URL}#{path}", headers) rescue RestClient::ExceptionWithResponse => e e.response end diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb index 7d876fc..499c738 100644 --- a/spec/lib/github/client_spec.rb +++ b/spec/lib/github/client_spec.rb @@ -8,7 +8,8 @@ let(:invalid_url) { "https://invalid.com/owner/repo/pull/123" } describe "#send_get_request" do - let(:url) { "https://api.github.com/some_endpoint" } + let(:path) { "/some_endpoint" } + let(:url) { "https://api.github.com#{path}" } let(:response) { double("response") } context "when the request is successful" do @@ -18,7 +19,7 @@ it "sends a GET request to the given URL" do expect(RestClient).to receive(:get).with(url, anything) - client.send_get_request(url) + client.send_get_request(path) end it "returns the response" do @@ -36,7 +37,7 @@ end it "rescues the exception and returns the response" do - result = client.send_get_request(url) + result = client.send_get_request(path) expect(result).to eq(error_response) end end diff --git a/spec/services /fetch_github_pull_request_data_service_spec.rb b/spec/services /fetch_github_pull_request_data_service_spec.rb index 5bbca70..803cbd4 100644 --- a/spec/services /fetch_github_pull_request_data_service_spec.rb +++ b/spec/services /fetch_github_pull_request_data_service_spec.rb @@ -67,19 +67,19 @@ context "when all data is successfully fetched" do before do allow(client).to receive(:send_get_request) - .with(pull_request_api_url) + .with(URI(pull_request_api_url).path) .and_return(instance_double(RestClient::Response, code: 200, body: pr_data.to_json)) allow(client).to receive(:send_get_request) - .with(pr_data[:diff_url]) + .with(URI(pr_data[:diff_url]).path) .and_return(instance_double(RestClient::Response, code: 200, body: diff_data)) allow(client).to receive(:send_get_request) - .with(pr_data[:commits_url]) + .with(URI(pr_data[:commits_url]).path) .and_return(instance_double(RestClient::Response, code: 200, body: commits_data.to_json)) allow(client).to receive(:send_get_request) - .with(pr_data[:review_comments_url]) + .with(URI(pr_data[:review_comments_url]).path) .and_return(instance_double(RestClient::Response, code: 200, body: comments_data.to_json)) end @@ -127,7 +127,7 @@ context "when pull request data cannot be fetched" do before do allow(client).to receive(:send_get_request) - .with(pull_request_api_url) + .with(URI(pull_request_api_url).path) .and_return(instance_double(RestClient::Response, code: 404, body: { message: "Not found" }.to_json)) end From df8bcc5821547cd32d7a910b8d38e0bb3a16a0c3 Mon Sep 17 00:00:00 2001 From: Vadser Date: Mon, 26 Aug 2024 12:09:59 +0200 Subject: [PATCH 7/9] Rename fetch pull request data service, added github vlient param --- Gemfile.lock | 2 +- ..._data_service.rb => fetch_github_pull_request_data.rb} | 6 +++--- ...ice_spec.rb => fetch_github_pull_request_data_spec.rb} | 8 ++------ 3 files changed, 6 insertions(+), 10 deletions(-) rename app/services/{fetch_github_pull_request_data_service.rb => fetch_github_pull_request_data.rb} (95%) rename spec/services /{fetch_github_pull_request_data_service_spec.rb => fetch_github_pull_request_data_spec.rb} (95%) diff --git a/Gemfile.lock b/Gemfile.lock index ffe7c90..cec6ec3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -305,4 +305,4 @@ DEPENDENCIES tzinfo-data BUNDLED WITH - 2.5.3 + 2.5.17 diff --git a/app/services/fetch_github_pull_request_data_service.rb b/app/services/fetch_github_pull_request_data.rb similarity index 95% rename from app/services/fetch_github_pull_request_data_service.rb rename to app/services/fetch_github_pull_request_data.rb index 6009165..e10a01a 100644 --- a/app/services/fetch_github_pull_request_data_service.rb +++ b/app/services/fetch_github_pull_request_data.rb @@ -1,9 +1,9 @@ -class FetchGithubPullRequestDataService +class FetchGithubPullRequestData attr_reader :pull_request_url, :client - def initialize(pull_request_url) + def initialize(pull_request_url, github_client: Github::Client.new) @pull_request_url = pull_request_url - @client = Github::Client.new + @client = github_client end def call diff --git a/spec/services /fetch_github_pull_request_data_service_spec.rb b/spec/services /fetch_github_pull_request_data_spec.rb similarity index 95% rename from spec/services /fetch_github_pull_request_data_service_spec.rb rename to spec/services /fetch_github_pull_request_data_spec.rb index 803cbd4..3e138ef 100644 --- a/spec/services /fetch_github_pull_request_data_service_spec.rb +++ b/spec/services /fetch_github_pull_request_data_spec.rb @@ -1,11 +1,11 @@ require 'rails_helper' require 'json' -RSpec.describe FetchGithubPullRequestDataService do +RSpec.describe FetchGithubPullRequestData do let(:pull_request_url) { "https://github.com/owner/repo/pull/123" } let(:pull_request_api_url) { "https://api.github.com/repos/owner/repo/pulls/123" } - let(:service) { described_class.new(pull_request_url) } let(:client) { instance_double(Github::Client) } + let(:service) { described_class.new(pull_request_url, github_client: client) } let(:pr_data) do { @@ -59,10 +59,6 @@ ] end - before do - allow(Github::Client).to receive(:new).and_return(client) - end - describe "#call" do context "when all data is successfully fetched" do before do From 9da9699782ba818931c5d97d0020793563ece450 Mon Sep 17 00:00:00 2001 From: Vadser Date: Mon, 26 Aug 2024 12:26:39 +0200 Subject: [PATCH 8/9] Update brakeman version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cec6ec3..4aaf3aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,7 @@ GEM bigdecimal (3.1.8) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.1.2) + brakeman (6.2.1) racc builder (3.3.0) concurrent-ruby (1.3.4) From 408c2c95e7171bad07173867209e15119c3ace8d Mon Sep 17 00:00:00 2001 From: Vadser Date: Mon, 26 Aug 2024 12:32:00 +0200 Subject: [PATCH 9/9] Remove scan_js github workflow --- .github/workflows/ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 875e92e..fb0f4ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,22 +22,6 @@ jobs: - name: Scan for common Rails security vulnerabilities using static analysis run: bin/brakeman --no-pager - scan_js: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true - - - name: Scan for security vulnerabilities in JavaScript dependencies - run: bin/importmap audit - lint: runs-on: ubuntu-latest steps: